chore: prepare repository for submodule split

This commit is contained in:
2026-07-09 09:00:51 +00:00
parent 9e545cb328
commit e9a2400c48
33 changed files with 4175 additions and 1545 deletions
+70
View File
@@ -0,0 +1,70 @@
# Operating systems and editors
.DS_Store
Thumbs.db
Desktop.ini
.vscode/
.idea/
.vs/
*.suo
*.user
*.userosscache
*.sln.docstates
# Temporary files
*.tmp
*.temp
*.bak
*.swp
*.log
*~
# Private requirement / handover documents
*.pdf
*.doc
*.docx
# C/C++ generated artifacts
*.obj
*.o
*.pdb
*.ilk
*.idb
*.tlog
*.lastbuildstate
*.exp
*.lib
*.dll
*.exe
# Build outputs
out/
build/
build-*/
cmake-build-*/
.cmake/
CMakeFiles/
CMakeCache.txt
CMakeSettings.json
CMakeUserPresets.json
Testing/
# Third-party/business binary drops and generated packages
App/
dist/
*.zip
*.tar
*.tar.gz
*.tgz
*.7z
*.rar
# Local runtime state and credentials
config/app_config.json
config/client_identity.dat
config/local_state.json
config/version_policy.dat
config/*.local.json
config/*private*.pem
config/*private*.key
update/
update_temp/
+60 -2
View File
@@ -28,6 +28,40 @@ QString ConfigHelper::configPath() const
return m_configPath;
}
QString ConfigHelper::installRoot() const
{
QString relativeRoot = getValue("Runtime", "install_root").trimmed();
if (relativeRoot.isEmpty())
relativeRoot = ".";
return QDir::cleanPath(QDir(runtimeRoot()).filePath(relativeRoot));
}
QString ConfigHelper::runtimeRoot() const
{
return QDir::cleanPath(QApplication::applicationDirPath());
}
QString ConfigHelper::updateRoot() const
{
return QDir(runtimeRoot()).filePath("update");
}
QString ConfigHelper::runtimeRelativePath() const
{
QString relative = QDir::fromNativeSeparators(QDir(installRoot()).relativeFilePath(runtimeRoot()));
relative = QDir::cleanPath(relative);
if (relative == ".")
return QString();
while (relative.startsWith("./"))
relative = relative.mid(2);
return relative.trimmed();
}
QString ConfigHelper::lastError() const
{
return m_error;
}
QString ConfigHelper::getValue(const QString& section, const QString& key) const
{
Q_UNUSED(section);
@@ -46,33 +80,56 @@ QString ConfigHelper::getValue(const QString& section, const QString& key) const
bool ConfigHelper::setValue(const QString& section, const QString& key, const QString& value)
{
Q_UNUSED(section);
m_error.clear();
QFile input(m_configPath);
QJsonObject config;
if (input.exists())
{
if (!input.open(QIODevice::ReadOnly))
{
m_error = QString("Cannot open config for reading: %1").arg(input.errorString());
return false;
}
QJsonParseError parseError;
const QByteArray raw = input.readAll();
input.close();
const QJsonDocument document = QJsonDocument::fromJson(raw, &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject())
{
m_error = QString("Invalid config JSON: %1").arg(parseError.errorString());
return false;
}
config = document.object();
}
config.insert(key, value);
QDir().mkpath(QFileInfo(m_configPath).path());
QDir dir(QFileInfo(m_configPath).path());
if (!dir.exists() && !dir.mkpath("."))
{
m_error = QString("Cannot create config directory: %1").arg(dir.path());
return false;
}
QSaveFile output(m_configPath);
if (!output.open(QIODevice::WriteOnly))
{
m_error = QString("Cannot open config for writing: %1").arg(output.errorString());
return false;
}
const QByteArray payload = QJsonDocument(config).toJson(QJsonDocument::Indented);
if (output.write(payload) != payload.size())
{
m_error = QString("Cannot write full config file: %1").arg(output.errorString());
output.cancelWriting();
return false;
}
return output.commit();
if (!output.commit())
{
m_error = QString("Cannot commit config file: %1").arg(output.errorString());
return false;
}
return true;
}
bool ConfigHelper::migrateLegacyIniIfNeeded()
@@ -103,6 +160,7 @@ bool ConfigHelper::migrateLegacyIniIfNeeded()
copyText("Update", "request_timeout_ms", "5000");
copyText("Update", "temp_folder", "update_temp");
copyText("Update", "device_id");
copyText("Runtime", "install_root", ".");
copyText("Runtime", "main_executable", "MainApp.exe");
copyText("Runtime", "launcher_executable", "Launcher.exe");
copyText("Runtime", "updater_executable", "Updater.exe");
+6
View File
@@ -8,9 +8,15 @@ public:
QString getValue(const QString& section, const QString& key) const;
bool setValue(const QString& section, const QString& key, const QString& value);
QString configPath() const;
QString installRoot() const;
QString runtimeRoot() const;
QString updateRoot() const;
QString runtimeRelativePath() const;
QString lastError() const;
private:
ConfigHelper();
bool migrateLegacyIniIfNeeded();
QString m_configPath;
QString m_error;
};
+10 -2
View File
@@ -13,6 +13,7 @@
#include <QSaveFile>
#include <QSysInfo>
#include <QUuid>
#include <QTimer>
#ifdef HAVE_OPENSSL
#include <openssl/evp.h>
#include <openssl/pem.h>
@@ -35,7 +36,14 @@ bool DeviceIdentityHelper::loadAndVerify(const QString& appId,const QString& cha
bool DeviceIdentityHelper::verifyLocal(const QString& appId,const QString& channel){m_error.clear();return loadAndVerify(appId,channel);}
bool DeviceIdentityHelper::ensureIssued(const QString& base,const QString& token,const QString& appId,const QString& channel,const QString& licenseKey){
m_error.clear();if(loadAndVerify(appId,channel)){ConfigHelper::instance().setValue("Update","device_id",m_deviceId);return true;}
QString installation=ConfigHelper::instance().getValue("Device","installation_id");if(installation.isEmpty()){installation=QUuid::createUuid().toString(QUuid::WithoutBraces);if(!ConfigHelper::instance().setValue("Device","installation_id",installation)){m_error="cannot save installation id";return false;}}
const QString trimmedBase=base.trimmed();
if(appId.trimmed().isEmpty()){m_error="app_id is empty in config/app_config.json";return false;}
if(channel.trimmed().isEmpty()){m_error="channel is empty in config/app_config.json";return false;}
if(trimmedBase.isEmpty()||trimmedBase.contains("YOUR_SERVER_IP",Qt::CaseInsensitive)){m_error="api_base_url is not configured. Set it to the update server address, for example http://192.168.229.128:8000";return false;}
if(token.trimmed().isEmpty()){m_error="client_token is empty in config/app_config.json";return false;}
if(licenseKey.trimmed().isEmpty()){m_error="license_key is empty. Create a License in the admin page and paste the generated key into config/app_config.json";return false;}
ConfigHelper& config=ConfigHelper::instance();
QString installation=config.getValue("Device","installation_id");if(installation.isEmpty()){installation=QUuid::createUuid().toString(QUuid::WithoutBraces);if(!config.setValue("Device","installation_id",installation)){m_error=QString("cannot save installation id to %1: %2").arg(config.configPath(),config.lastError());return false;}}
QByteArray machine=QSysInfo::machineUniqueId()+installation.toUtf8();QString mh=QString::fromLatin1(QCryptographicHash::hash(machine,QCryptographicHash::Sha256).toHex());QJsonObject body{{"app_id",appId},{"channel",channel},{"license_key",licenseKey},{"installation_id",installation},{"machine_hash",mh}};
QNetworkAccessManager manager;QNetworkRequest req{QUrl(base+"/api/v1/device/issue")};req.setHeader(QNetworkRequest::ContentTypeHeader,"application/json");req.setRawHeader("X-Client-Token",token.toUtf8());QNetworkReply* reply=manager.post(req,QJsonDocument(body).toJson(QJsonDocument::Compact));QEventLoop loop;QObject::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit);loop.exec();int status=reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();QByteArray raw=reply->readAll();reply->deleteLater();if(status!=200){m_error=QString("device issue failed (HTTP %1): %2").arg(status).arg(QString::fromUtf8(raw));return false;}auto response=QJsonDocument::fromJson(raw).object();QJsonObject wrapper{{"identity_text",response.value("identity_text")},{"signature",response.value("signature")}};QString path=QDir(m_installDir).filePath("config/client_identity.dat");QSaveFile out(path);QByteArray bytes=QJsonDocument(wrapper).toJson(QJsonDocument::Compact);if(!out.open(QIODevice::WriteOnly)||out.write(bytes)!=bytes.size()||!out.commit()){m_error="cannot save device credential";return false;}if(!loadAndVerify(appId,channel))return false;if(!ConfigHelper::instance().setValue("Update","device_id",m_deviceId)){m_error="cannot save server device id";return false;}return true;
QNetworkAccessManager manager;QNetworkRequest req{QUrl(trimmedBase+"/api/v1/device/issue")};req.setHeader(QNetworkRequest::ContentTypeHeader,"application/json");req.setRawHeader("X-Client-Token",token.toUtf8());QNetworkReply* reply=manager.post(req,QJsonDocument(body).toJson(QJsonDocument::Compact));QEventLoop loop;bool timeoutOk=false;int timeoutMs=ConfigHelper::instance().getValue("Update","request_timeout_ms").toInt(&timeoutOk);if(!timeoutOk||timeoutMs<1000)timeoutMs=5000;QTimer timer;timer.setSingleShot(true);QObject::connect(&timer,&QTimer::timeout,[&](){if(reply&&reply->isRunning())reply->abort();});QObject::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit);timer.start(timeoutMs);loop.exec();timer.stop();int status=reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();QByteArray raw=reply->readAll();reply->deleteLater();if(status!=200){m_error=QString("device issue failed (HTTP %1): %2").arg(status).arg(QString::fromUtf8(raw));return false;}auto response=QJsonDocument::fromJson(raw).object();QJsonObject wrapper{{"identity_text",response.value("identity_text")},{"signature",response.value("signature")}};QString path=QDir(m_installDir).filePath("config/client_identity.dat");QSaveFile out(path);QByteArray bytes=QJsonDocument(wrapper).toJson(QJsonDocument::Compact);if(!out.open(QIODevice::WriteOnly)||out.write(bytes)!=bytes.size()||!out.commit()){m_error=QString("cannot save device credential to %1: %2").arg(path,out.errorString());return false;}if(!loadAndVerify(appId,channel))return false;if(!config.setValue("Update","device_id",m_deviceId)){m_error=QString("cannot save server device id to %1: %2").arg(config.configPath(),config.lastError());return false;}return true;
}
+14
View File
@@ -4,6 +4,7 @@
#include <QFile>
#include <QDir>
#include <QApplication>
#include <QTimer>
void HttpHelper::postRequest(const QString& url, const QJsonObject& jsonBody,
std::function<void(int code, const QJsonObject& resp)> callback)
@@ -27,9 +28,22 @@ void HttpHelper::postRequest(const QString& url, const QJsonObject& jsonBody,
QNetworkReply* reply = manager->post(req, data);
QEventLoop loop;
bool timeoutOk = false;
int timeoutMs = ConfigHelper::instance().getValue("Update", "request_timeout_ms").toInt(&timeoutOk);
if (!timeoutOk || timeoutMs < 1000) timeoutMs = 5000;
QTimer timer;
timer.setSingleShot(true);
QObject::connect(&timer, &QTimer::timeout, [&]() {
if (reply && reply->isRunning()) {
qDebug() << "Request timeout, abort:" << url;
reply->abort();
}
});
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
timer.start(timeoutMs);
loop.exec();
timer.stop();
int retCode = 0;
QJsonObject retObj;
+25 -4
View File
@@ -1,4 +1,5 @@
#include "IntegrityHelper.h"
#include "ConfigHelper.h"
#include <QCryptographicHash>
#include <QDir>
#include <QDirIterator>
@@ -28,10 +29,19 @@ bool IntegrityHelper::safeRelativePath(const QString& path) const
bool IntegrityHelper::runtimeProtectedPath(const QString& path) const
{
const QString p = QDir::fromNativeSeparators(path).toCaseFolded();
static const QSet<QString> protectedPaths{
QSet<QString> protectedPaths{
"bootstrap.exe", "client.ini", "config/app_config.json", "config/local_state.json",
"config/client_identity.dat", "config/version_policy.dat"
};
const QString runtimePrefix = ConfigHelper::instance().runtimeRelativePath().toCaseFolded();
if (!runtimePrefix.isEmpty()) {
const QStringList runtimeProtected{
"bootstrap.exe", "client.ini", "config/app_config.json", "config/local_state.json",
"config/client_identity.dat", "config/version_policy.dat"
};
for (const QString& protectedPath : runtimeProtected)
protectedPaths.insert(runtimePrefix + "/" + protectedPath);
}
return protectedPaths.contains(p);
}
@@ -49,7 +59,10 @@ bool IntegrityHelper::verifySignature(const QByteArray& payload, const QString&
#ifndef HAVE_OPENSSL
Q_UNUSED(payload); Q_UNUSED(signatureBase64); m_error = "OpenSSL unavailable"; return false;
#else
QFile keyFile(QDir(m_installDir).filePath("config/manifest_public_key.pem"));
QString keyPath = QDir(m_installDir).filePath("config/manifest_public_key.pem");
if (!QFile::exists(keyPath))
keyPath = QFileInfo(ConfigHelper::instance().configPath()).dir().filePath("manifest_public_key.pem");
QFile keyFile(keyPath);
if (!keyFile.open(QIODevice::ReadOnly)) { m_error = "manifest public key missing"; return false; }
const QByteArray keyData = keyFile.readAll();
BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size());
@@ -72,8 +85,12 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
const QString& version)
{
m_error.clear();
const QString cachePath = QDir(m_installDir).filePath(
QString cachePath = QDir(ConfigHelper::instance().updateRoot()).filePath(
"manifest_cache/manifest_" + version + ".json");
const QString legacyCachePath = QDir(m_installDir).filePath(
"update/manifest_cache/manifest_" + version + ".json");
if (!QFile::exists(cachePath))
cachePath = legacyCachePath;
QFile cache(cachePath);
if (!cache.open(QIODevice::ReadOnly)) { m_error = "signed manifest cache missing for " + version; return false; }
QJsonParseError wrapperError;
@@ -121,8 +138,12 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
const QString fullPath = it.next();
const QString relative = QDir::fromNativeSeparators(root.relativeFilePath(fullPath));
const QString folded = relative.toCaseFolded();
const QString runtimePrefix = ConfigHelper::instance().runtimeRelativePath().toCaseFolded();
const bool runtimeWorkDir = !runtimePrefix.isEmpty()
&& (folded.startsWith(runtimePrefix + "/update/")
|| folded.startsWith(runtimePrefix + "/update_temp/"));
if (folded.startsWith("update/") || folded.startsWith("update_temp/")
|| runtimeProtectedPath(relative)) continue;
|| runtimeWorkDir || runtimeProtectedPath(relative)) continue;
const QString suffix = QFileInfo(relative).suffix().toCaseFolded();
if ((suffix == "exe" || suffix == "dll") && !declaredExecutables.contains(folded)) {
m_error = "undeclared executable or plugin: " + relative; return false;
+88 -11
View File
@@ -4,6 +4,8 @@
#include <QMessageBox>
#include <QProcess>
#include <QProgressDialog>
#include <QInputDialog>
#include <QLineEdit>
#include <QTextCodec>
#include "UpdateLogic.h"
#include "../Common/ConfigHelper.h"
@@ -13,6 +15,7 @@
#include "../Common/DeviceIdentityHelper.h"
#include <QFile>
#include <QFileDialog>
#include <QDir>
int main(int argc, char* argv[])
{
@@ -30,16 +33,81 @@ int main(int argc, char* argv[])
progress.show();
QApplication::processEvents();
const QString appDir = QApplication::applicationDirPath();
ConfigHelper& config = ConfigHelper::instance();
DeviceIdentityHelper identity(QApplication::applicationDirPath());
if (!identity.ensureIssued(config.getValue("Server", "api_base_url"),
config.getValue("Server", "client_token"),
config.getValue("App", "app_id"),
config.getValue("App", "channel"),
config.getValue("License", "license_key"))) {
progress.close();
QMessageBox::critical(nullptr, "设备身份验证失败", identity.errorString());
return -1;
const auto isLicenseError = [](const QString& errorText) {
const QString text = errorText.toLower();
return text.contains("license")
|| errorText.contains("授权")
|| errorText.contains("过期")
|| errorText.contains("设备数量已达到上限")
|| errorText.contains("已绑定其他授权");
};
const auto clearDeviceCredential = [&]() {
QFile::remove(QDir(appDir).filePath("config/client_identity.dat"));
config.setValue("Update", "device_id", QString());
};
QString licenseKey = config.getValue("License", "license_key").trimmed();
auto promptAndSaveLicense = [&](const QString& reason) -> QString {
QString promptReason = reason;
while (true) {
progress.close();
bool accepted = false;
const QString appName = config.getValue("App", "app_name").trimmed();
const QString prompt = promptReason.trimmed().isEmpty()
? QString("请输入%1授权 License").arg(appName.isEmpty() ? "软件" : appName)
: QString("%1\n\n请重新输入%2授权 License").arg(promptReason, appName.isEmpty() ? "软件" : appName);
licenseKey = QInputDialog::getText(
nullptr,
"输入 License",
prompt,
QLineEdit::Normal,
QString(),
&accepted
).trimmed();
if (!accepted) {
QMessageBox::information(nullptr, "需要 License", "首次启动需要输入管理员提供的 License。");
return QString();
}
if (licenseKey.isEmpty()) {
QMessageBox::warning(nullptr, "License 不能为空", "请粘贴管理员在后台创建的 License。");
promptReason.clear();
continue;
}
if (!config.setValue("License", "license_key", licenseKey)) {
QMessageBox::critical(nullptr, "保存 License 失败",
QString("无法写入配置文件:%1\n%2").arg(config.configPath(), config.lastError()));
return QString();
}
progress.show();
progress.setLabelText("正在验证 License...");
QApplication::processEvents();
return licenseKey;
}
};
while (true) {
if (licenseKey.isEmpty()) {
licenseKey = promptAndSaveLicense(QString());
if (licenseKey.isEmpty()) return 0;
}
DeviceIdentityHelper identity(appDir);
if (identity.ensureIssued(config.getValue("Server", "api_base_url"),
config.getValue("Server", "client_token"),
config.getValue("App", "app_id"),
config.getValue("App", "channel"),
licenseKey)) {
break;
}
const QString error = identity.errorString();
if (!isLicenseError(error)) {
progress.close();
QMessageBox::critical(nullptr, "设备身份验证失败", error);
return -1;
}
clearDeviceCredential();
licenseKey = promptAndSaveLicense(QString("当前 License 无法使用:%1").arg(error));
if (licenseKey.isEmpty()) return 0;
}
UpdateLogic logic;
@@ -57,13 +125,22 @@ int main(int argc, char* argv[])
progress.close();
const QJsonValue detail = response.value("detail");
const QString message = detail.isObject() ? detail.toObject().value("msg").toString() : detail.toString();
QMessageBox::critical(nullptr, "授权被拒绝", message.isEmpty() ? "设备或 License 授权无效。" : message);
const QString displayMessage = message.isEmpty() ? "设备或 License 授权无效。" : message;
if (isLicenseError(displayMessage)) {
clearDeviceCredential();
const QString newLicense = promptAndSaveLicense(QString("当前授权被服务端拒绝:%1").arg(displayMessage));
if (!newLicense.isEmpty()) {
progress.close();
QMessageBox::information(nullptr, "License 已保存", "请重新启动 Launcher 完成设备授权和更新检查。");
}
return 0;
}
QMessageBox::critical(nullptr, "授权被拒绝", displayMessage);
return -1;
}
const QString launchToken = config.getValue("App", "launch_token");
const QString currentVersion = config.getValue("App", "current_version");
const QString appDir = QApplication::applicationDirPath();
const auto configuredName = [&](const QString& key, const QString& fallback) {
const QString value = config.getValue("Runtime", key).trimmed();
return value.isEmpty() ? fallback : value;
+2 -1
View File
@@ -51,6 +51,7 @@ int main(int argc, char* argv[])
}
QString appDir = QApplication::applicationDirPath();
const QString installRoot = config.installRoot();
DeviceIdentityHelper identity(appDir);
if (!identity.verifyLocal(config.getValue("App", "app_id"), config.getValue("App", "channel")))
{
@@ -89,7 +90,7 @@ int main(int argc, char* argv[])
return -1;
}
IntegrityHelper integrity(appDir);
IntegrityHelper integrity(installRoot);
if (!integrity.verifyInstalledVersion(
config.getValue("App", "app_id"), config.getValue("App", "channel"),
config.getValue("App", "current_version")))
+69 -10
View File
@@ -17,12 +17,63 @@ SDK 核心程序:
1. 在服务端管理后台创建应用,例如 `app_id=your_app_id`
2. 创建或确认渠道,例如 `stable`
3. 创建 License,把生成的 `license_key` 填到客户端配置。
4. 把业务程序和依赖 DLL 放到同一个发布目录。
5. 把 SDK 的 `Launcher.exe``Updater.exe``Bootstrap.exe` 和运行时 DLL 放到该目录。
6.`config/app_config.example.json` 复制成 `config/app_config.json` 并修改字段。
4. 把业务软件完整安装目录作为发布目录,例如 `SimCAE\`,其中主程序位于 `bin\SimCAE.exe`
5. 把 SDK 的 `Launcher.exe``Updater.exe``Bootstrap.exe` 放到 `SimCAE\bin\` 目录,和 `SimCAE.exe` 同级。不要覆盖 SimCAE 自带的 `Qt5*.dll` 和 Qt 插件目录。
6.`config/app_config.example.json` 复制成 `SimCAE\bin\config\app_config.json` 并修改字段。
7. 用户入口改成 `Launcher.exe`,不要直接双击业务主程序。
8. 在管理后台发布新版本时,选择包含业务主程序和 SDK 运行时的干净 Release 根目录。
## 安装目录写权限要求
当前 SDK 会在 `Launcher.exe` 所在目录下写入运行时状态文件。SDK 放在 `SimCAE\bin` 时,这些文件实际位于 `SimCAE\bin` 下,例如:
```text
config/app_config.json
config/client_identity.dat
config/local_state.json
config/version_policy.dat
update/
update_temp/
```
所以联调和普通运行时,`Launcher.exe` 所在目录必须允许当前 Windows 用户写入。不要直接把联调目录放在 `C:\Program Files\...` 后用普通用户启动;该目录默认禁止普通程序写文件,会导致首次启动报错,例如 `cannot save installation id`
推荐联调目录:
```text
D:\SimCAE_Release\
C:\Users\<你的用户名>\Desktop\SimCAE_Release\
```
如果最终产品必须安装到 `C:\Program Files\SimCAE\bin`,需要额外设计管理员提权、Windows 服务,或把运行时状态迁移到 `ProgramData` / `AppData`。当前交付版本默认按“安装目录可写”的模式工作。
## SimCAE 目录结构建议
SimCAE 当前安装目录是根目录下有 `bin/``Licenses/``installerResources/` 等子目录。SDK 推荐放在 `bin/` 目录,和 `SimCAE.exe` 同级;后台发布时仍选择整个安装根目录:
```text
SimCAE/
bin/
Launcher.exe
Updater.exe
Bootstrap.exe
SimCAE.exe
config/
app_config.json
manifest_public_key.pem
update/
manifest_cache/
Qt5Core.dll
...
Licenses/
installerResources/
maintenancetool.exe
```
这种模式下,后台“发布新版本”时选择整个 `SimCAE/` 目录,服务端会检查 `bin/SimCAE.exe` 是否存在,并把整个安装结构写入 Manifest。客户端配置里 `install_root``..`,表示被更新的安装根目录是 `bin` 的上一级;升级事务、下载缓存和 Manifest 缓存仍放在 `SimCAE\bin\update`
注意:SimCAE 自己已经带有 Qt 运行库。SDK 的 Launcher/Updater 应使用和 SimCAE 兼容的 Qt 编译,并复用 SimCAE 的 `Qt5*.dll``platforms/``imageformats/` 等目录。不要把另一套 Qt DLL 覆盖到 `SimCAE\bin`,否则会出现“无法定位程序输入点”一类错误。
## app_config.json 关键字段
```json
@@ -39,6 +90,7 @@ SDK 核心程序:
"request_timeout_ms": "5000",
"temp_folder": "update_temp",
"device_id": "",
"install_root": "..",
"main_executable": "SimCAE.exe",
"launcher_executable": "Launcher.exe",
"updater_executable": "Updater.exe",
@@ -59,7 +111,8 @@ SDK 核心程序:
- `client_token`:服务端 `.env` 中的 `CLIENT_API_TOKEN`,默认交付包已填 `SimCAEClientToken2026`
- `license_key`:管理后台创建 License 后返回的密钥;模板里先留空,创建授权后再填。
- `launch_token`:本机启动票据 HMAC 密钥,模板已给默认值,可试跑;正式交付建议改成你自己的 32 字符以上随机字符串。
- `main_executable`:业务主程序文件名,默认填 `SimCAE.exe`;如果你的主程序不是这个名字,这里同步修改
- `install_root`:安装根目录相对 `Launcher.exe` 所在目录的位置。SDK 放在 `bin` 时填 `..`
- `main_executable`:业务主程序相对 `Launcher.exe` 所在目录的路径,SimCAE 默认填 `SimCAE.exe`
- `health_check_timeout_ms`:升级后等待业务程序写健康标记的时间。
## 业务主程序需要配合什么
@@ -92,15 +145,19 @@ cd client
-SdkVersion 0.1.0
```
默认生成的 SDK 不包含 Qt 运行库,避免覆盖业务软件自带的 Qt。只有在接入的软件本身不带 Qt,且你确认要让 SDK 自带一套 Qt 运行库时,才额外添加 `-IncludeQtRuntime`
生成结果:
```text
dist/UpdateClientSDK/
README.md
SimCAE自动升级SDK接入说明_v0.1.docx
sdk_manifest.json
bin/
config/
app_config.json
manifest_public_key.pem
scripts/
samples/
```
`UpdateClientSDK.zip` 发给接入方即可。
@@ -123,7 +180,9 @@ cd client
## 常见错误
1. 直接启动业务主程序提示 ticket 错误:应从 `Launcher.exe` 启动。
2. 首次启动设备登记失败:检查 `api_base_url``client_token``license_key`、服务端 License 状态
3. 策略或 Manifest 验签失败:检查 `config/manifest_public_key.pem` 是否和服务端私钥匹配
4. 升级后回滚:检查业务程序是否在 `health_check_timeout_ms` 内写入健康标记
5. 发布失败提示主程序不在根目录:选择发布目录时要选择业务程序所在目录,而不是上层或下层目录
2. 首次启动提示 `cannot save installation id`:当前目录不可写,常见于 `C:\Program Files\...`;请换到可写目录联调,或用管理员权限/提权方案
3. 首次启动设备登记失败:检查 `api_base_url``client_token``license_key`、服务端 License 状态
4. 策略或 Manifest 验签失败:检查 `config/manifest_public_key.pem` 是否和服务端私钥匹配
5. 升级后回滚:检查业务程序是否在 `health_check_timeout_ms` 内写入健康标记
6. 发布失败提示主程序不在根目录:选择发布目录时要选择业务主程序所在目录,而不是上层或下层目录。
7. 启动时提示 `无法定位程序输入点 ... Qt5*.dll`:通常是 Qt DLL 被不同版本覆盖或混用。恢复业务软件原始 Qt DLL,并重新打包 SDK;SimCAE 场景下不要使用 `-IncludeQtRuntime`
+10 -5
View File
@@ -9,11 +9,11 @@
#include <QSet>
#include <QUuid>
UpdateTransaction::UpdateTransaction(const QString& installDir, const QString& fromVersion,
UpdateTransaction::UpdateTransaction(const QString& installDir, const QString& updateDir, const QString& fromVersion,
const QString& toVersion, int manifestId)
: m_installDir(QDir::cleanPath(installDir)),
m_updateDir(QDir(installDir).filePath("update")),
m_stateFile(QDir(installDir).filePath("update/upgrade_state.json")),
m_updateDir(QDir::cleanPath(updateDir.isEmpty() ? QDir(installDir).filePath("update") : updateDir)),
m_stateFile(QDir(m_updateDir).filePath("upgrade_state.json")),
m_transactionId(QUuid::createUuid().toString(QUuid::WithoutBraces)),
m_fromVersion(fromVersion), m_toVersion(toVersion), m_manifestId(manifestId)
{
@@ -223,9 +223,14 @@ QString UpdateTransaction::backupDir() const { return m_backupDir; }
QStringList UpdateTransaction::obsoletePaths() const { return m_obsoletePaths; }
QString UpdateTransaction::healthFile() const { return QDir(m_updateDir).filePath("health_" + m_transactionId + ".ok"); }
bool UpdateTransaction::recoverInterrupted(const QString& installDir, QString* restoredVersion, QString* errorMessage)
bool UpdateTransaction::recoverInterrupted(const QString& installDir, const QString& updateDir,
QString* restoredVersion, QString* errorMessage)
{
const QString stateFile = QDir(installDir).filePath("update/upgrade_state.json");
QString stateFile = QDir(updateDir.isEmpty() ? QDir(installDir).filePath("update") : updateDir)
.filePath("upgrade_state.json");
const QString legacyStateFile = QDir(installDir).filePath("update/upgrade_state.json");
if (!QFile::exists(stateFile) && QFile::exists(legacyStateFile))
stateFile = legacyStateFile;
QFile file(stateFile);
if (!file.exists()) return true;
if (!file.open(QIODevice::ReadOnly)) { if (errorMessage) *errorMessage = "cannot open upgrade_state.json"; return false; }
+2 -2
View File
@@ -6,7 +6,7 @@
class UpdateTransaction
{
public:
UpdateTransaction(const QString& installDir, const QString& fromVersion,
UpdateTransaction(const QString& installDir, const QString& updateDir, const QString& fromVersion,
const QString& toVersion, int manifestId);
bool initialize();
@@ -29,7 +29,7 @@ public:
QStringList obsoletePaths() const;
QString healthFile() const;
static bool recoverInterrupted(const QString& installDir,
static bool recoverInterrupted(const QString& installDir, const QString& updateDir,
QString* restoredVersion = nullptr,
QString* errorMessage = nullptr);
+26 -5
View File
@@ -263,7 +263,7 @@ QStringList UpdaterLogic::obsoleteFilesComparedTo(const QJsonObject& oldManifest
if (isSafeRelativePath(path)) newPaths.insert(path.toCaseFolded());
}
const QSet<QString> protectedPaths{
QSet<QString> protectedPaths{
QStringLiteral("bootstrap.exe"),
QStringLiteral("launcher.exe"),
QStringLiteral("updater.exe"),
@@ -273,6 +273,17 @@ QStringList UpdaterLogic::obsoleteFilesComparedTo(const QJsonObject& oldManifest
QStringLiteral("config/client_identity.dat"),
QStringLiteral("config/version_policy.dat")
};
const QString runtimePrefix = ConfigHelper::instance().runtimeRelativePath().toCaseFolded();
if (!runtimePrefix.isEmpty()) {
const QStringList runtimeProtected{
QStringLiteral("bootstrap.exe"), QStringLiteral("launcher.exe"), QStringLiteral("updater.exe"),
QStringLiteral("client.ini"), QStringLiteral("config/app_config.json"),
QStringLiteral("config/local_state.json"), QStringLiteral("config/client_identity.dat"),
QStringLiteral("config/version_policy.dat")
};
for (const QString& path : runtimeProtected)
protectedPaths.insert(runtimePrefix + "/" + path);
}
QStringList obsolete;
QSet<QString> seen;
for (const QJsonValue& value : oldManifest.value("files").toArray()) {
@@ -550,7 +561,7 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
bool UpdaterLogic::isRuntimeProtectedPath(const QString& path) const
{
const QString normalized = QDir::fromNativeSeparators(path).toCaseFolded();
static const QSet<QString> protectedPaths{
QSet<QString> protectedPaths{
QStringLiteral("bootstrap.exe"),
QStringLiteral("client.ini"),
QStringLiteral("config/app_config.json"),
@@ -558,6 +569,16 @@ bool UpdaterLogic::isRuntimeProtectedPath(const QString& path) const
QStringLiteral("config/client_identity.dat"),
QStringLiteral("config/version_policy.dat")
};
const QString runtimePrefix = ConfigHelper::instance().runtimeRelativePath().toCaseFolded();
if (!runtimePrefix.isEmpty()) {
const QStringList runtimeProtected{
QStringLiteral("bootstrap.exe"), QStringLiteral("client.ini"),
QStringLiteral("config/app_config.json"), QStringLiteral("config/local_state.json"),
QStringLiteral("config/client_identity.dat"), QStringLiteral("config/version_policy.dat")
};
for (const QString& protectedPath : runtimeProtected)
protectedPaths.insert(runtimePrefix + "/" + protectedPath);
}
return protectedPaths.contains(normalized);
}
@@ -576,7 +597,7 @@ qint64 UpdaterLogic::estimateAdditionalDiskBytes(const QString& targetDir,
const QStringList& obsoletePaths) const
{
qint64 required = 0;
const QString cacheDir = QDir(targetDir).filePath("update/download_cache");
const QString cacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("download_cache");
for (const auto& item : m_fileItems) {
const QString relativePath = QDir::fromNativeSeparators(item.path);
if (isRuntimeProtectedPath(relativePath)) continue;
@@ -609,7 +630,7 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
QDir stagingDir(tempDir);
if (!FileHelper::createDir(tempDir))
return false;
const QString resumeCacheDir = QDir(targetDir).filePath("update/download_cache");
const QString resumeCacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("download_cache");
if (!FileHelper::createDir(resumeCacheDir))
return false;
QSet<QString> activePartialNames;
@@ -727,4 +748,4 @@ void UpdaterLogic::reportResult(const QString& deviceId,
QList<FileDownloadItem> UpdaterLogic::getFileList() const
{
return m_fileItems;
}
}
+18 -12
View File
@@ -59,16 +59,19 @@ int main(int argc, char* argv[])
bootstrapResult = arg.mid(QString("--bootstrap-resume=").size());
}
const bool resumingFromBootstrap = !bootstrapResult.isEmpty();
const QString targetDir = QApplication::applicationDirPath();
const QString runtimeDir = QApplication::applicationDirPath();
ConfigHelper& config = ConfigHelper::instance();
const QString targetDir = config.installRoot();
const QString updateDir = config.updateRoot();
QDir().mkpath(updateDir);
if (appId != config.getValue("App", "app_id") || channel != config.getValue("App", "channel")) {
QMessageBox::critical(nullptr, "离线包不适用", "更新包的应用或渠道与本机配置不一致。");
return -1;
}
QString fromVersion = config.getValue("App", "current_version");
if (!offlinePackagePath.isEmpty()) {
PolicyHelper offlinePolicy(targetDir);
PolicyHelper offlinePolicy(runtimeDir);
if (!offlinePolicy.loadPolicy() || !offlinePolicy.isValid() || offlinePolicy.isExpired()
|| !offlinePolicy.isOfflineAllowed() || !offlinePolicy.isVersionAllowed(targetVersion)) {
QMessageBox::critical(nullptr, "离线更新被拒绝", "本地签名策略已过期、禁止离线更新或不允许目标版本。");
@@ -87,7 +90,7 @@ int main(int argc, char* argv[])
if (!resumingFromBootstrap) {
QString restoredVersion;
QString recoveryError;
if (!UpdateTransaction::recoverInterrupted(targetDir, &restoredVersion, &recoveryError)) {
if (!UpdateTransaction::recoverInterrupted(targetDir, updateDir, &restoredVersion, &recoveryError)) {
QMessageBox::critical(nullptr, "更新恢复失败",
QString("检测到上次更新未完成,但无法恢复旧版本:%1\n请不要继续运行软件,并联系管理员。").arg(recoveryError));
return -1;
@@ -111,7 +114,7 @@ int main(int argc, char* argv[])
progress.show();
QApplication::processEvents();
UpdateTransaction transaction(targetDir, fromVersion, targetVersion, targetVersionId);
UpdateTransaction transaction(targetDir, updateDir, fromVersion, targetVersion, targetVersionId);
const auto setProgress = [&](int value, const QString& message) {
progress.setValue(value);
progress.setLabelText(message);
@@ -160,9 +163,9 @@ int main(int argc, char* argv[])
bool timeoutOk = false;
int healthCheckTimeoutMs = config.getValue("Runtime", "health_check_timeout_ms").toInt(&timeoutOk);
if (!timeoutOk || healthCheckTimeoutMs < 1000) healthCheckTimeoutMs = 15000;
const QString mainAppPath = QDir(targetDir).filePath(mainExecutable);
const QString updaterPath = QDir(targetDir).filePath(updaterExecutable);
const QString bootstrapPath = QDir(targetDir).filePath(bootstrapExecutable);
const QString mainAppPath = QDir(runtimeDir).filePath(mainExecutable);
const QString updaterPath = QDir(runtimeDir).filePath(updaterExecutable);
const QString bootstrapPath = QDir(runtimeDir).filePath(bootstrapExecutable);
const QString launchToken = config.getValue("App", "launch_token");
const auto launchMainApp = [&](const QString& healthFile = QString()) {
QString ticketPath;
@@ -180,7 +183,7 @@ int main(int argc, char* argv[])
return started;
};
const auto bootstrapPlanFile = [&]() {
return QDir(targetDir).filePath("update/bootstrap_plan_" + transaction.transactionId() + ".txt");
return QDir(updateDir).filePath("bootstrap_plan_" + transaction.transactionId() + ".txt");
};
const auto launchBootstrap = [&](const QString& mode) {
const QStringList args{
@@ -245,7 +248,7 @@ int main(int argc, char* argv[])
}
setProgress(resumingFromBootstrap ? 72 : 10, offlinePackagePath.isEmpty() ? "正在获取并验证版本清单..." : "正在验证离线更新包...");
const QString manifestCacheDir = QDir(targetDir).filePath("update/manifest_cache");
const QString manifestCacheDir = QDir(updateDir).filePath("manifest_cache");
if (resumingFromBootstrap) {
if (!logic.loadManifestCache(manifestCacheDir, targetVersion))
return delegateRollback("清单缓存失败", "Bootstrap 安装后无法读取签名 Manifest 缓存。");
@@ -282,7 +285,7 @@ int main(int argc, char* argv[])
if (logic.getFileList().isEmpty())
return fail("没有可更新文件", "服务器没有返回任何版本文件,更新已停止。", "empty_file_list");
QStorageInfo storage(targetDir);
QStorageInfo storage(updateDir);
storage.refresh();
const qint64 requiredBytes = logic.estimateAdditionalDiskBytes(targetDir, obsoletePaths);
const qint64 availableBytes = storage.bytesAvailable();
@@ -316,9 +319,12 @@ int main(int argc, char* argv[])
QDirIterator stagingFiles(stagingDir, QDir::Files, QDirIterator::Subdirectories);
while (stagingFiles.hasNext())
changedPaths.append(QDir::fromNativeSeparators(stagingRoot.relativeFilePath(stagingFiles.next())));
const QString runtimePrefix = config.runtimeRelativePath();
const QString bootstrapManifestPath = QDir::fromNativeSeparators(
runtimePrefix.isEmpty() ? bootstrapExecutable : runtimePrefix + "/" + bootstrapExecutable);
for (const QString& path : changedPaths) {
if (path.compare(bootstrapExecutable, Qt::CaseInsensitive) == 0)
return fail("Bootstrap 无法自更新", QString("本次版本包含新的 %1。请使用安装包升级该组件,再重新发布业务版本。").arg(bootstrapExecutable), "bootstrap_self_update_blocked");
if (path.compare(bootstrapManifestPath, Qt::CaseInsensitive) == 0)
return fail("Bootstrap 无法自更新", QString("本次版本包含新的 %1。请使用安装包升级该组件,再重新发布业务版本。").arg(bootstrapManifestPath), "bootstrap_self_update_blocked");
}
if (!transaction.recordVerifiedFiles(changedPaths, obsoletePaths))
return fail("事务记录失败", "无法保存已校验或待删除文件列表,更新已停止。", "transaction_record_failed");
-1079
View File
File diff suppressed because it is too large Load Diff
-14
View File
@@ -1,14 +0,0 @@
[Server]
api_base_url=http://192.168.229.128:8000
client_token=AuthKey202606Demo
[App]
app_id=testadmin
current_version=1.0.0
channel=stable
launch_token=UpdateSecret2026Demo
[Update]
request_timeout_ms=5000
temp_folder=update_temp
device_id=test_device_001
+1
View File
@@ -11,6 +11,7 @@
"request_timeout_ms": "5000",
"temp_folder": "update_temp",
"device_id": "",
"install_root": "..",
"main_executable": "SimCAE.exe",
"launcher_executable": "Launcher.exe",
"updater_executable": "Updater.exe",
+35 -2
View File
@@ -4,7 +4,9 @@ param(
[string]$ReleaseDir = (Get-Location).Path,
[switch]$OverwriteConfig
[switch]$OverwriteConfig,
[switch]$IncludeQtRuntime
)
$ErrorActionPreference = "Stop"
@@ -23,7 +25,38 @@ foreach ($path in @($binDir, $appConfig, $publicKey)) {
}
}
Copy-Item (Join-Path $binDir "*") $release -Recurse -Force
function Test-IsQtRuntimeItem {
param([System.IO.FileSystemInfo]$Item)
if ($IncludeQtRuntime) {
return $false
}
if ($Item.PSIsContainer) {
return $Item.Name -in @(
"bearer",
"iconengines",
"imageformats",
"platforms",
"styles",
"translations"
)
}
return (
$Item.Name -match '^Qt5.*\.dll$' -or
$Item.Name -in @(
"libEGL.dll",
"libGLESv2.dll",
"opengl32sw.dll",
"d3dcompiler_47.dll"
)
)
}
Get-ChildItem $binDir -Force | Where-Object {
-not (Test-IsQtRuntimeItem $_)
} | Copy-Item -Destination $release -Recurse -Force
$targetConfigDir = Join-Path $release "config"
New-Item $targetConfigDir -ItemType Directory -Force | Out-Null
+64 -13
View File
@@ -12,6 +12,31 @@ $source = (Resolve-Path $SourceDir).Path
$config = (Resolve-Path $ConfigFile).Path
$settings = Get-Content $config -Raw -Encoding UTF8 | ConvertFrom-Json
function Normalize-RelativePath([string]$PathValue) {
return ($PathValue -replace '\\', '/').Trim('/')
}
function Get-RelativePathUnderSource([string]$FullPath, [string]$Fallback) {
$full = (Resolve-Path $FullPath).Path
if ($full.StartsWith($source, [System.StringComparison]::OrdinalIgnoreCase)) {
return Normalize-RelativePath ($full.Substring($source.Length).TrimStart('\', '/'))
}
return Normalize-RelativePath $Fallback
}
$configRelativePath = Get-RelativePathUnderSource $config "config/app_config.json"
$configRelativeParent = Split-Path $configRelativePath -Parent
$runtimeDirRelative = Normalize-RelativePath (Split-Path $configRelativeParent -Parent)
if ($runtimeDirRelative -eq ".") { $runtimeDirRelative = "" }
function Join-RelativePath([string]$Base, [string]$Child) {
$baseNorm = Normalize-RelativePath $Base
$childNorm = Normalize-RelativePath $Child
if ([string]::IsNullOrWhiteSpace($baseNorm)) { return $childNorm }
if ([string]::IsNullOrWhiteSpace($childNorm)) { return $baseNorm }
return "$baseNorm/$childNorm"
}
$requiredFields = @(
"app_id", "channel", "api_base_url", "current_version",
"client_token", "launch_token", "license_key",
@@ -23,14 +48,20 @@ foreach ($field in $requiredFields) {
}
}
$mainExecutable = Normalize-RelativePath $settings.main_executable
$launcherExecutable = Normalize-RelativePath $settings.launcher_executable
$updaterExecutable = Normalize-RelativePath $settings.updater_executable
$bootstrapExecutable = Normalize-RelativePath $settings.bootstrap_executable
$requiredFiles = @(
$settings.main_executable,
$settings.launcher_executable,
$settings.updater_executable,
$settings.bootstrap_executable
(Join-RelativePath $runtimeDirRelative $mainExecutable),
(Join-RelativePath $runtimeDirRelative $launcherExecutable),
(Join-RelativePath $runtimeDirRelative $updaterExecutable),
(Join-RelativePath $runtimeDirRelative $bootstrapExecutable)
)
foreach ($name in $requiredFiles) {
if (-not (Test-Path (Join-Path $source $name))) {
$nativeName = $name -replace '/', [IO.Path]::DirectorySeparatorChar
if (-not (Test-Path (Join-Path $source $nativeName))) {
throw "Source directory is missing required file: $name"
}
}
@@ -43,15 +74,25 @@ if ($debugArtifacts) {
throw "Source directory contains Debug artifacts. Clean out/bin and rebuild Release first. Example: $($debugArtifacts[0].FullName)"
}
$nestedMain = Get-ChildItem $source -Recurse -File -Filter $settings.main_executable | Where-Object {
$_.DirectoryName -ne $source
$expectedMainPath = Join-RelativePath $runtimeDirRelative $mainExecutable
$mainLeafName = Split-Path $mainExecutable -Leaf
$duplicateMain = Get-ChildItem $source -Recurse -File -Filter $mainLeafName | Where-Object {
$relative = Normalize-RelativePath ($_.FullName.Substring($source.Length).TrimStart('\', '/'))
$relative -ne $expectedMainPath
} | Select-Object -First 1
if ($nestedMain) {
throw "Source directory contains a nested duplicate main executable. Use a clean Release root directory: $($nestedMain.FullName)"
if ($duplicateMain) {
throw "Source directory contains a duplicate main executable outside $expectedMainPath. Use a clean Release root directory: $($duplicateMain.FullName)"
}
$manifestName = "manifest_$($settings.current_version).json"
$sourceManifest = Join-Path $source "update/manifest_cache/$manifestName"
$sourceManifestRelative = Join-RelativePath $runtimeDirRelative "update/manifest_cache/$manifestName"
$sourceManifest = Join-Path $source ($sourceManifestRelative -replace '/', [IO.Path]::DirectorySeparatorChar)
if (-not (Test-Path $sourceManifest)) {
$legacySourceManifest = Join-Path $source "update/manifest_cache/$manifestName"
if (Test-Path $legacySourceManifest) {
$sourceManifest = $legacySourceManifest
}
}
if (-not (Test-Path $sourceManifest)) {
throw "Missing signed Manifest cache for current version: $sourceManifest. Complete online update/verification for this version before packaging."
}
@@ -65,16 +106,26 @@ Get-ChildItem $source -Force | Where-Object {
$_.Name -notin @("update", "update_temp")
} | Copy-Item -Destination $OutputDir -Recurse -Force
$outputConfigDir = Join-Path $OutputDir "config"
$outputRuntimeUpdateDir = Join-Path $OutputDir ((Join-RelativePath $runtimeDirRelative "update") -replace '/', [IO.Path]::DirectorySeparatorChar)
if (Test-Path $outputRuntimeUpdateDir) {
Remove-Item $outputRuntimeUpdateDir -Recurse -Force
}
$outputRuntimeTempDir = Join-Path $OutputDir ((Join-RelativePath $runtimeDirRelative "update_temp") -replace '/', [IO.Path]::DirectorySeparatorChar)
if (Test-Path $outputRuntimeTempDir) {
Remove-Item $outputRuntimeTempDir -Recurse -Force
}
$outputConfigPath = Join-Path $OutputDir ($configRelativePath -replace '/', [IO.Path]::DirectorySeparatorChar)
$outputConfigDir = Split-Path $outputConfigPath -Parent
New-Item $outputConfigDir -ItemType Directory -Force | Out-Null
Copy-Item $config (Join-Path $outputConfigDir "app_config.json") -Force
Copy-Item $config $outputConfigPath -Force
@("client_identity.dat", "local_state.json", "version_policy.dat") | ForEach-Object {
$runtimeFile = Join-Path $outputConfigDir $_
if (Test-Path $runtimeFile) { Remove-Item $runtimeFile -Force }
}
$manifestDir = Join-Path $OutputDir "update/manifest_cache"
$manifestDir = Join-Path $OutputDir ((Join-RelativePath $runtimeDirRelative "update/manifest_cache") -replace '/', [IO.Path]::DirectorySeparatorChar)
New-Item $manifestDir -ItemType Directory -Force | Out-Null
Copy-Item $sourceManifest (Join-Path $manifestDir $manifestName) -Force
+33 -3
View File
@@ -4,7 +4,8 @@ param(
[string]$ZipFile = "$PSScriptRoot/dist/UpdateClientSDK.zip",
[string]$SdkVersion = "0.1.0",
[string]$ExampleConfig = "$PSScriptRoot/config/app_config.example.json",
[switch]$IncludeDemoMainApp
[switch]$IncludeDemoMainApp,
[switch]$IncludeQtRuntime
)
$ErrorActionPreference = "Stop"
@@ -46,11 +47,40 @@ $configDir = Join-Path $OutputDir "config"
$scriptsDir = Join-Path $OutputDir "scripts"
New-Item $binDir,$configDir,$scriptsDir -ItemType Directory -Force | Out-Null
$excludedTopLevel = @("config", "update", "update_temp")
$excludedTopLevel = @("config", "update", "update_temp", "manifest_public_key.pem")
if (-not $IncludeDemoMainApp) { $excludedTopLevel += "MainApp.exe" }
if (-not $IncludeQtRuntime) {
$excludedTopLevel += @(
"bearer",
"iconengines",
"imageformats",
"platforms",
"styles",
"translations"
)
}
function Test-IsQtRuntimeFile {
param([System.IO.FileSystemInfo]$Item)
if ($IncludeQtRuntime -or $Item.PSIsContainer) {
return $false
}
return (
$Item.Name -match '^Qt5.*\.dll$' -or
$Item.Name -in @(
"libEGL.dll",
"libGLESv2.dll",
"opengl32sw.dll",
"d3dcompiler_47.dll"
)
)
}
Get-ChildItem $source -Force | Where-Object {
$_.Name -notin $excludedTopLevel
$_.Name -notin $excludedTopLevel -and -not (Test-IsQtRuntimeFile $_)
} | Copy-Item -Destination $binDir -Recurse -Force
Copy-Item $exampleConfigPath (Join-Path $configDir "app_config.json") -Force