diff --git a/Docs/00-先读我-客户端文档入口.txt b/Docs/00-先读我-客户端文档入口.txt deleted file mode 100644 index f052228..0000000 --- a/Docs/00-先读我-客户端文档入口.txt +++ /dev/null @@ -1,23 +0,0 @@ -SimCAE Hub 客户端文档入口 -======================== - -本目录记录 SimCAE Hub 的 Qt/C++ 客户端更新链路。当前方向是保留 Launcher / Updater / Bootstrap 的桌面客户端机制,适配 SimCAE Hub 当前 Go API,不用 Go 或 Web 技术重写客户端。 - -建议先按这个顺序阅读: - -1. 01-客户端接入打包部署指南.md - 说明客户端运行链路、配置字段、安装目录口径和接入限制。 - -2. 02-编译环境和第三方依赖说明.md - 说明 Windows 和 Linux 下编译 Qt/C++ 客户端需要的工具、Qt、OpenSSL 和 CMake 命令。 - -3. ../打包成SDK.md - 说明如何从 update-client 编译产物生成给 SIMCAE 开发者使用的 SDK 包。 - -4. ../config/server_config.json - 编译进客户端资源的服务端地址配置,当前测试服务器是 http://192.168.1.158:18000。 - -5. ../scripts/ReadMe.txt - SDK 打包脚本和客户安装包打包脚本的简短说明。 - -客户端能力、接口链路、配置字段和接入限制统一看 01 文档。当前不包含邮箱、支付、告警、灰度发布等页面上没有的业务模块;崩溃报告作为旧系统兼容后端接口保留,具体看项目根目录的 项目细节.md。 diff --git a/Docs/01-客户端接入打包部署指南.md b/Docs/01-客户端接入打包部署指南.md deleted file mode 100644 index c21e304..0000000 --- a/Docs/01-客户端接入打包部署指南.md +++ /dev/null @@ -1,210 +0,0 @@ -# SimCAE Hub 客户端接入、打包和部署指南 - -本文说明 `update-client` 的当前实现。它保留 Launcher / Updater / Bootstrap 的桌面客户端机制,服务端协议使用 SimCAE Hub 当前 Go API。 - -## 1. 适用范围 - -当前客户端只覆盖项目已有页面和接口对应的能力: - -1. 产品版本、软件发布、发布包和 Manifest。 -2. 客户授权、在线命名用户席位和门户受控下载。 -3. 在线检查更新、Manifest 拉取、受控下载、SHA-256 校验。 -4. Manifest 签名验签、临时文件、断点重试、安装前后完整性校验。 - -邮箱、支付、灰度、告警等页面上没有的能力不属于当前范围。崩溃报告是 SimCAE Hub 保留的旧系统兼容后端接口,不属于 Launcher / Updater / Bootstrap 的更新链路;接入方需要崩溃上报时,按项目根目录 `项目细节.md` 里的崩溃报告接口说明调用。 - -## 2. 客户端程序组成 - -| 程序 | 作用 | -| --- | --- | -| `Launcher` | 客户日常启动入口,负责导入配置、使用 `client_token` 检查更新、启动 Updater 或主程序 | -| `Updater` | 负责拉取 Manifest、下载发布包、校验文件、准备安装事务 | -| `Bootstrap` | 负责在需要替换运行中文件时接管安装,并把结果交回 Updater | -| `MainApp` | 示例主程序,用来验证 launch ticket 和安装后完整性校验 | -| `Common` | 配置、HTTP、票据、完整性校验等公共代码 | - -## 3. 当前在线更新链路 - -1. `Launcher` 启动后读取服务端生成的 `config/app_config.json`,并把静态配置导入当前用户的运行配置。 -2. 如果配置里没有 `api_base_url`,客户端会回退到编译进 EXE 资源中的 `server_config.json`。 -3. `Launcher` 确保存在 `device_id`,并检查 `client_token` 是否存在。 -4. `Launcher` 调用 `GET /api/v1/client/update/authorized-check`,请求头带 `X-Client-Token`。 -5. 如果服务端返回可用发布,`Launcher` 启动 `Updater`,并传入产品编码、渠道、目标版本和发布 ID。 -6. `Updater` 调用 `GET /api/v1/client/update/manifest`,请求头继续带 `X-Client-Token`。 -7. `Updater` 先校验服务端返回的 `manifestSha256`,再按配置决定是否强制要求 RSA-SHA256 签名。 -8. `Updater` 从 Manifest 中读取每个文件的 `downloadUrl`、`sizeBytes` 和 `sha256`。 -9. 下载请求统一带 `X-Client-Token`。 -10. 下载使用 `.part` 临时文件保存进度,请求失败后按网络重试策略处理。 -11. 文件下载完成后,客户端按 Manifest 校验文件大小和 SHA-256。 -12. 安装前校验 staging 目录,安装完成后保存 Manifest 缓存,并可在主程序启动时再次校验已安装文件。 - -## 4. 关键配置字段 - -正式客户安装包里的 `config/app_config.json` 由服务端在上传发布包 ZIP 时自动生成。常用字段如下: - -| 字段 | 说明 | -| --- | --- | -| `product_code` | SimCAE Hub 后台产品目录中的产品编码,例如 `stage2-dap` | -| `app_id` | 本地应用标识,默认和产品编码一致 | -| `channel` | 发布渠道,例如 `stable` | -| `current_version` | 当前本地安装版本,例如 `1.0.0` | -| `api_base_url` | 后端 API 地址,例如 `http://192.168.1.158:18000` | -| `client_token` | Launcher/Updater 调更新接口使用的部署级令牌,不绑定某一个客户 | -| `install_root` | 相对 Launcher/Updater 所在运行目录解析的更新根目录,决定 Updater、Bootstrap 和启动校验作用在哪棵目录 | -| `main_executable` | 相对运行目录解析的业务入口程序,通常是 `MainApp.exe` 或真实软件入口 | -| `launcher_executable` | 相对运行目录解析的 Launcher 文件名,主要用于提示和保持启动链路配置一致 | -| `updater_executable` | 相对运行目录解析的 Updater 文件名,Launcher 检查到更新后会启动它 | -| `bootstrap_executable` | 相对运行目录解析的 Bootstrap 文件名,Updater 需要替换文件时会启动它 | -| `platform` | 操作系统,例如 `windows` 或 `linux` | -| `arch` | 架构,例如 `x86_64` | -| `abi` | ABI,例如 `msvc`;没有时可留空 | -| `launch_token` | Launcher 和 MainApp 之间生成一次性启动票据的本地密钥 | -| `require_manifest_signature` | 是否强制要求 Manifest 必须带签名 | -| `verify_installed_on_start` | 主程序启动时是否按 Manifest 缓存校验已安装文件 | - -`config/server_config.json` 会编译进客户端资源,作为 `api_base_url` 缺失时的兜底地址,当前测试服务器地址为: - -`http://192.168.1.158:18000` - -如果换服务器,可以改完该文件后重新编译客户端;正式客户包通常由服务端写入 `api_base_url`,不需要把 `server_config.json` 暴露给客户。 - -## 5. 编译和打包 SDK - -客户端编译、带 Qt 和不带 Qt 两种 SDK 打包方式、输出目录、输出 ZIP 文件名,统一维护在 [../打包成SDK.md](../打包成SDK.md)。 - -本文件只说明 SDK 在客户软件里的接入位置和运行逻辑,不重复维护打包命令。 - -SDK 包不会包含最终 `app_config.json`、`server_config.json`、`server_config.qrc` 或 `manifest_public_key.pem`。这些最终配置在完整客户软件包或 Qt IFW 交付包上传到 SimCAE Hub 后由服务端生成。 - -## 6. 客户安装包配置 - -接入方应把以下文件放到客户软件目录的根目录或 `bin/` 目录: - -1. `Launcher` -2. `Updater` -3. `Bootstrap` -4. `MainApp` 或真实业务主程序 -5. `config/` 目录,可以先为空 - -### 6.1 标准目录结构和路径口径 - -更新系统不要求必须放在客户软件根目录。它可以放在 `SimCAE/` 根目录,也可以放在 `SimCAE/bin/` 目录。关键是让客户端配置里的 `install_root` 和服务端 Manifest 文件路径使用同一套口径。 - -先区分三个目录概念: - -| 概念 | 说明 | -| --- | --- | -| 运行目录 | Launcher、Updater、Bootstrap 所在目录,由客户端自动识别 | -| `install_root` | 相对运行目录解析的更新根目录,Updater 下载、校验、备份、回滚和 Bootstrap 替换文件都以它为范围 | -| Manifest `files[].path` | 服务端生成的安装相对路径,客户端会把它拼到 `install_root` 下面 | - -目录结构一:更新系统放在软件根目录。 - -```text -SimCAE/ - Launcher.exe - Updater.exe - Bootstrap.exe - MainApp.exe - config/ - app_config.json - App/ - ... -``` - -对应配置: - -```json -{ - "install_root": ".", - "main_executable": "MainApp.exe", - "launcher_executable": "Launcher.exe", - "updater_executable": "Updater.exe", - "bootstrap_executable": "Bootstrap.exe" -} -``` - -目录结构二:更新系统和启动入口放在 `bin/`。 - -```text -SimCAE/ - bin/ - Launcher.exe - Updater.exe - Bootstrap.exe - MainApp.exe - config/ - app_config.json - App/ - ... -``` - -如果希望整个 `SimCAE/` 都属于更新范围,对应配置: - -```json -{ - "install_root": "..", - "main_executable": "MainApp.exe", - "launcher_executable": "Launcher.exe", - "updater_executable": "Updater.exe", - "bootstrap_executable": "Bootstrap.exe" -} -``` - -这表示 Launcher 从 `bin/` 启动 `MainApp.exe`,Updater 和 Bootstrap 更新的是 `bin/` 的上一级,也就是整个 `SimCAE/`。 - -服务端发布包路径要和客户端 `install_root` 对应: - -| 客户端配置 | 服务端 Manifest 路径口径 | -| --- | --- | -| 更新系统在根目录,`install_root` 是 `.` | `MainApp.exe`、`App/xxx.dll` 相对 `SimCAE/` | -| 更新系统在 `bin/`,`install_root` 是 `..` | `bin/MainApp.exe`、`App/xxx.dll` 相对 `SimCAE/` | - -当前管理后台“发布包”上传接口会使用上传文件名作为 `artifactName`,后端按安全文件名校验。当前稳定支持的是发布一个完整安装包或压缩包文件,或者把文件放在 `install_root` 根层级;还不是“自动解析压缩包并生成 App/bin 多文件 Manifest”的完整安装器。后续如果要让在线 Updater 直接把多个文件铺到 `App/`、`bin/` 等子目录,需要在现有发布包页面和 Go 后端上继续增强安全相对路径或 Manifest 文件清单生成能力。 - -如果后续要支持“更新系统在 `bin/`,但只校验和更新 `App/`”这类更窄的安装根目录,需要在管理后台和 Go 后端增加对应配置项,让服务端生成 `../App` 这类定制 `install_root`。当前服务端自动生成配置时只使用标准的 `.` 或 `..`。 - -如果开启 `verify_installed_on_start`,客户端会扫描 `install_root` 下的 EXE 和 DLL。整包 Manifest 中 `required=true` 的核心文件必须存在且 SHA-256 匹配;`required=false` 的可选组件文件可以由 MaintenanceTool 管理,缺失时不会阻止启动。可选组件建议放在独立目录中,例如 `plugins/dap/`,不要和核心程序 DLL 混放。 - -上传完整客户软件 ZIP 时,服务端会根据发布包记录自动写入这些关键值: - -```json -{ - "product_code": "stage2-dap", - "channel": "stable", - "current_version": "1.1.0", - "api_base_url": "http://192.168.1.158:18000", - "client_token": "<由服务器 .env 配置>", - "install_root": ". 或 ..", - "platform": "windows", - "arch": "x86_64", - "abi": "msvc" -} -``` - -`install_root` 由服务端根据 Launcher 所在位置自动判断:更新系统在软件根目录时写 `.`,在 `bin/` 目录时写 `..`。 - -## 7. 运行数据位置 - -Windows 运行数据目录: - -`%LOCALAPPDATA%\SimCAE\HubUpdateClient\installations\<安装目录SHA256>\` - -Linux 运行数据目录: - -`$XDG_DATA_HOME/SimCAE/HubUpdateClient/installations/<安装目录SHA256>/` - -未设置 `XDG_DATA_HOME` 时通常是: - -`~/.local/share/SimCAE/HubUpdateClient/installations/<安装目录SHA256>/` - -Manifest 缓存保存在运行数据目录下的 `update/manifest_cache`。 - -## 8. 常见问题 - -1. 客户门户登录失败:检查客户门户账号是否已激活、客户是否生效、密码是否正确。 -2. 检查更新没有结果:检查后台发布是否已发布、发布包是否可用、产品编码、渠道和平台参数是否一致。 -3. 下载 401:检查发布包中的 `config/app_config.json` 是否由服务端生成,`client_token` 是否和服务器 `.env` 中的 `SIMCAE_CLIENT_TOKEN` 一致。 -4. Manifest 校验失败:检查服务端 Manifest 是否被篡改、发布包 SHA-256 是否和实际文件一致。 -5. 强制签名失败:确认 `manifest_public_key.pem` 与服务端私钥匹配;如果服务端暂未启用签名,测试环境可先把 `require_manifest_signature` 设为 `false`。 -6. 启动主程序失败:检查 `main_executable` 和 `install_root` 是否指向真实文件。 diff --git a/Docs/02-编译环境和第三方依赖说明.md b/Docs/02-编译环境和第三方依赖说明.md deleted file mode 100644 index 211c674..0000000 --- a/Docs/02-编译环境和第三方依赖说明.md +++ /dev/null @@ -1,75 +0,0 @@ -# SimCAE Hub 客户端编译环境和第三方依赖说明 - -本文说明 `update-client` 的 Qt/C++ 客户端编译环境。客户端保留 Launcher / Updater / Bootstrap 机制,依赖 Qt、CMake 和 OpenSSL。 - -## 1. 通用要求 - -| 依赖 | 要求 | -| --- | --- | -| CMake | 建议 3.20 或更高版本 | -| C++ | C++17 | -| Qt | Qt 5,至少需要 Core、Network、Gui、Widgets | -| OpenSSL | 用于 Manifest RSA-SHA256 验签 | -| 编译器 | Windows 推荐 Visual Studio 2022 x64,Linux 推荐 gcc/g++ | - -项目已提供 CMake Preset: - -| Preset | 平台 | 用途 | -| --- | --- | --- | -| `x64-debug` | Windows | Debug 编译 | -| `x64-release` | Windows | Release 编译 | -| `linux-x64-debug` | Linux | Debug 编译 | -| `linux-x64-release` | Linux | Release 编译 | - -## 2. Windows 环境 - -建议安装: - -1. Visual Studio 2022,勾选 Desktop development with C++。 -2. Qt 5 x64,版本可以与当前团队环境保持一致。 -3. CMake。 -4. OpenSSL x64。 - -如果 Qt 没有加入环境变量,可以在编译前指定 `CMAKE_PREFIX_PATH` 或 `Qt5_DIR`。示例: - -```powershell -$env:CMAKE_PREFIX_PATH = "C:\Qt\5.15.2\msvc2019_64" -``` - -如果 Qt 安装在别的位置,只改这一行。 - -OpenSSL 可以放在 `update-client/thirdparty/OpenSSL-Win64`,也可以在配置时通过 `SIMCAE_OPENSSL_ROOT` 指向自定义目录。 - -## 3. Linux 环境 - -Ubuntu 示例: - -```bash -sudo apt update -sudo apt install -y build-essential cmake qtbase5-dev qttools5-dev qttools5-dev-tools libssl-dev -``` - -Linux 下通常直接使用系统 OpenSSL;如需指定自定义 OpenSSL,可用 CMake 变量配置。 - -## 4. 编译输出 - -Windows Release 可执行文件输出目录: - -`update-client/out/bin/Release` - -Windows CMake 构建目录: - -`update-client/out/build/x64-release` - -Linux Release 可执行文件输出目录以当前 CMake Preset 和构建脚本为准,SDK 打包时通过 `--source-dir` 指定。 - -实际打包 SDK 前,应确认 Release 输出目录中至少包含: - -1. `Launcher` -2. `Updater` -3. `Bootstrap` -4. 可选的示例 `MainApp` - -最终客户软件包里的 `config/app_config.json` 和 `config/manifest_public_key.pem` 由服务端在发布包上传时生成;`server_config.json` 会编译进 EXE 作为兜底地址,不需要进入 SDK 包。 - -SDK 打包见 `../打包成SDK.md`,客户端运行链路和接入限制见 `01-客户端接入打包部署指南.md`。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed87da6 --- /dev/null +++ b/README.md @@ -0,0 +1,265 @@ +# SimCAE Hub 更新客户端 + +`update-client` 是 SimCAE Hub 的 Qt/C++ 整包更新客户端,包含 `Launcher`、`Updater`、`Bootstrap` 和一个示例 `MainApp`。它负责检查整包更新、拉取 Manifest、下载发布包、校验哈希并完成本地安装。 + +组件级安装、组件级更新和卸载由 Qt IFW 生成的 `maintenancetool.exe` 负责。`update-client` 不替代 MaintenanceTool,也不读取 Qt IFW 的 `Updates.xml`。 + +## 一、当前接入方式 + +当前 SIMCAE 的标准接入方式是: + +| 阶段 | 发生什么 | +| --- | --- | +| SDK 打包 | `update-client` 只打出 `Launcher.exe`、`Updater.exe`、`Bootstrap.exe` 和运行库 | +| SIMCAE 打包 | SIMCAE 的 `installer` 规则把 SDK 文件放进核心组件 `com.simcae.app` | +| 本地 IFW package | Hub 更新客户端位于 `package/packages/com.simcae.app/data/view/bin` | +| 上传到 Hub | 服务端校验 IFW 交付包,并自动注入最终客户配置 | +| 客户安装 | 客户从门户下载安装器,安装后得到 `maintenancetool.exe` 和 `view/bin/Launcher.exe` | +| 日常启动 | 客户通过 `Launcher.exe` 或安装器创建的快捷方式启动 SimCAE | + +最终客户不需要手动填写 `app_config.json`、服务器地址、token、产品编码、平台架构或版本号。 + +## 二、程序组成 + +| 程序 | 作用 | +| --- | --- | +| `Launcher` | 客户日常启动入口,读取配置、检查整包更新、启动 `Updater` 或主程序 | +| `Updater` | 拉取 Manifest、下载发布包、校验文件、准备安装事务 | +| `Bootstrap` | 替换运行中文件,并把安装结果交回 `Updater` | +| `MainApp` | 示例主程序,用于验证 launch ticket 和启动前完整性校验 | +| `Common` | 配置、HTTP、票据、路径、Manifest 和完整性校验等公共代码 | + +真实接入 SIMCAE 时,`MainApp` 只是示例程序。正式主程序是 SIMCAE 自己的 `SimCAE.exe`。 + +## 三、在线更新链路 + +1. 客户启动 `Launcher.exe`。 +2. 客户端读取服务端注入的初始配置。 +3. 首次启动时,客户端会把 `app_config.json` 中的运行配置导入本机用户配置。 +4. 为减少明文配置暴露,导入成功后客户端可能清空安装目录里的 `app_config.json`。 +5. `Launcher` 确保本机有 `device_id`。 +6. `Launcher` 使用 `X-Client-Token` 调用更新检查接口。 +7. 如果服务端返回可用发布,`Launcher` 启动 `Updater`。 +8. `Updater` 使用 `X-Client-Token` 拉取 Manifest。 +9. `Updater` 校验 Manifest 摘要,并按配置决定是否要求签名。 +10. `Updater` 按 Manifest 下载文件。 +11. 每个文件下载完成后校验大小和 SHA-256。 +12. 安装前校验 staging 目录。 +13. 如需替换运行中文件,`Bootstrap` 接管安装。 +14. 安装完成后保存 Manifest 缓存和本地状态。 +15. 如果主程序开启启动前完整性校验,下次启动时会按本地 Manifest 缓存校验已安装文件。 + +更新接口使用部署级 `client_token`,不使用客户邮箱密码。客户账号和授权主要控制门户下载、客户权益和席位,不要求最终客户启动软件时再登录。 + +## 四、当前 SIMCAE 目录规则 + +客户安装完成后的关键目录是: + +| 路径 | 说明 | +| --- | --- | +| `maintenancetool.exe` | Qt IFW 生成的组件维护工具,位于安装根目录 | +| `components.xml` | Qt IFW 记录的已安装组件状态,位于安装根目录 | +| `network.xml` | Qt IFW 记录的组件仓库地址,位于安装根目录 | +| `view/bin/Launcher.exe` | Hub 更新客户端启动入口 | +| `view/bin/Updater.exe` | Hub 整包更新程序 | +| `view/bin/Bootstrap.exe` | Hub 安装接管程序 | +| `view/bin/SimCAE.exe` | SIMCAE 业务主程序 | +| `view/bin/config/app_config.json` | 服务端注入的初始客户配置 | + +当前服务端会识别三种运行目录: + +| 运行目录 | 服务端写入的 `install_root` | 说明 | +| --- | --- | --- | +| 软件根目录 | `.` | `Launcher` 和主程序就在软件根目录 | +| `bin` | `..` | `Launcher` 在一层 `bin` 目录中 | +| `view/bin` | `../..` | 当前 SIMCAE 标准结构,`Launcher` 在 `view/bin` 中 | + +`install_root` 不是让客户手动填写的字段。上传发布包或 Qt IFW 交付包时,服务端会根据 `Launcher`、`Updater`、`Bootstrap` 的实际位置自动判断。 + +## 五、服务端注入的配置 + +正式客户包中的 `app_config.json` 由服务端生成或替换。开发者打 SDK 时不放最终配置,客户也不手动改配置。 + +服务端生成配置时,信息来源如下: + +| 配置内容 | 来源 | +| --- | --- | +| `product_code`、`app_id` | 管理后台“产品目录”的产品编码 | +| `app_name` | 管理后台“产品目录”的产品名称 | +| `channel` | 管理后台“软件发布”的发布通道 | +| `current_version` | 管理后台“产品版本”的版本号 | +| `platform`、`arch`、`abi` | 管理后台“平台管理”和发布包选择的平台 | +| `api_base_url` | 服务端 `.env` 的 `SIMCAE_CLIENT_API_BASE_URL` | +| `client_token` | 服务端 `.env` 的 `SIMCAE_CLIENT_TOKEN` | +| `launch_token` | 服务端 `.env` 的 `SIMCAE_LAUNCH_TOKEN` | +| `install_root` | 服务端根据运行目录自动判断 | +| `main_executable` | 服务端在运行目录中识别到的业务主程序,SIMCAE 当前为 `SimCAE.exe` | +| `launcher_executable` | 按平台生成,Windows 为 `Launcher.exe` | +| `updater_executable` | 按平台生成,Windows 为 `Updater.exe` | +| `bootstrap_executable` | 按平台生成,Windows 为 `Bootstrap.exe` | +| `require_manifest_signature` | 服务端 Manifest 签名配置 | +| `verify_installed_on_start` | 当前服务端默认写入 `false`,需要强制启动校验时再按发布策略开启 | + +如果上传包里已经带了旧的 `app_config.json`、`server_config.json`、`server_config.qrc` 或 `manifest_public_key.pem`,服务端会按当前发布信息重新处理,不让开发机临时配置直接进入最终客户包。 + +## 六、本地调试配置 + +正式发布不要手写最终 `app_config.json`。如果开发者只是在本机调试 `Launcher` 或 `Updater`,可以创建未提交的 `config/app_config.local.json`。 + +CMake 只在本地输出目录还没有 `config/app_config.json` 时,才会把 `app_config.local.json` 复制成调试用配置。这个文件只服务本机调试,不代表服务端最终注入结果。 + +`config/server_config.json` 和 `config/server_config.qrc` 用于把兜底 API 地址编译进 EXE。正式客户包优先使用服务端注入的 `api_base_url`,一般不需要让客户看到或修改 `server_config.json`。 + +## 七、启动门禁 + +如果 SIMCAE 主程序开启 `SimCAE_UseLauncher=ON`,用户直接双击 `SimCAE.exe` 会被拦截,必须通过 `Launcher.exe` 启动。 + +这套机制依赖 `launch_token`: + +| 位置 | 要求 | +| --- | --- | +| 服务端 `.env` | 必须配置 `SIMCAE_LAUNCH_TOKEN` | +| SIMCAE 编译期 | 主程序编译时使用同一个 token | +| 客户端配置 | 服务端把同一个 token 写入最终客户配置 | + +如果三处 token 不一致,就会出现“直接双击被拦住,但从 `Launcher` 启动也失败”的问题。 + +## 八、Manifest 和哈希校验 + +整包更新使用 SimCAE Hub Manifest,不使用 Qt IFW 的 `Updates.xml`。 + +Manifest 负责描述: + +| 内容 | 说明 | +| --- | --- | +| 发布版本 | 本次更新属于哪个产品、版本线、版本和通道 | +| 文件清单 | 本次发布包含哪些文件 | +| 下载地址 | 每个文件从哪个受控接口下载 | +| 文件大小 | 客户端下载后必须一致 | +| SHA-256 | 客户端下载后必须一致 | +| 是否必选 | 必选文件缺失会阻止启动,可选组件文件可由 MaintenanceTool 管理 | + +服务端返回 Manifest 前会重新检查发布包文件是否存在、大小是否一致、SHA-256 是否一致。客户端下载完成后也会再次校验大小和 SHA-256。 + +## 九、和 MaintenanceTool 的边界 + +| 能力 | 使用程序 | 文件格式 | +| --- | --- | --- | +| 整包更新 | `Launcher`、`Updater`、`Bootstrap` | SimCAE Hub 发布包和 Manifest | +| 组件安装、更新、移除 | `maintenancetool.exe` | Qt IFW repository 和 `Updates.xml` | + +两条线可以共存,但不要混淆: + +1. `Updater` 不读取 `Updates.xml`。 +2. `MaintenanceTool` 不读取 SimCAE Hub Manifest。 +3. `Updater` 不拉起 `MaintenanceTool`。 +4. `MaintenanceTool` 由客户手动打开,或由 Qt IFW 自己的流程使用。 +5. 核心组件通常包含 `Launcher`、`Updater`、`Bootstrap` 和 `SimCAE.exe`。 +6. 可选组件例如 DAP 插件,可以由 MaintenanceTool 单独安装、更新或移除。 + +## 十、编译环境 + +| 依赖 | 要求 | +| --- | --- | +| CMake | 建议 3.20 或更高版本 | +| C++ | C++17 | +| Qt | Qt 5,至少需要 Core、Network、Gui、Widgets | +| OpenSSL | 用于 Manifest RSA-SHA256 验签 | +| 编译器 | Windows 推荐 Visual Studio 2022 x64,Linux 推荐 gcc/g++ | + +项目提供的 CMake Preset: + +| Preset | 平台 | 用途 | +| --- | --- | --- | +| `x64-debug` | Windows | Debug 编译 | +| `x64-release` | Windows | Release 编译 | +| `linux-x64-debug` | Linux | Debug 编译 | +| `linux-x64-release` | Linux | Release 编译 | + +## 十一、Windows 编译 + +建议安装 Visual Studio 2022、Qt 5 x64、CMake 和 OpenSSL x64。 + +如果 Qt 没有加入环境变量,可以在编译前指定: + +```powershell +$env:CMAKE_PREFIX_PATH = "C:\Qt\5.15.2\msvc2019_64" +``` + +当前测试机 OpenSSL 路径是 `C:\Program Files\OpenSSL-Win64`,Release 编译命令: + +```powershell +cmake --preset x64-release -DSIMCAE_OPENSSL_ROOT="C:\Program Files\OpenSSL-Win64" +cmake --build --preset x64-release +``` + +如果 OpenSSL 安装在其他目录,只改 `SIMCAE_OPENSSL_ROOT` 这一项。 + +Windows Release 产物通常输出到 `out/bin/Release`。 + +检查核心程序: + +```powershell +Test-Path .\out\bin\Release\Launcher.exe +Test-Path .\out\bin\Release\Updater.exe +Test-Path .\out\bin\Release\Bootstrap.exe +``` + +预期都返回 `True`。 + +## 十二、Linux 编译 + +Ubuntu 示例: + +```bash +sudo apt update +sudo apt install -y build-essential cmake qtbase5-dev qttools5-dev qttools5-dev-tools libssl-dev +cmake --preset linux-x64-release +cmake --build --preset linux-x64-release +``` + +Linux 下通常直接使用系统 OpenSSL;如果需要指定自定义 OpenSSL,也可以通过 CMake 变量配置。 + +## 十三、打包 SDK + +SDK 打包流程见当前目录 [updater打包成SDK.md](updater打包成SDK.md)。SIMCAE 拿到 SDK 后的安装器、交付包和上传流程见 [SIMCAE打包上传.md](SIMCAE打包上传.md)。 + +常用输出: + +| 输出 | 说明 | +| --- | --- | +| `dist\UpdateClientSDK` | 不带 Qt 运行库的 SDK 展开目录 | +| `dist\UpdateClientSDK.zip` | 不带 Qt 运行库的 SDK 压缩包 | +| `dist\UpdateClientSDK-With-QtDll` | 带 Qt 运行库 DLL 的 SDK 展开目录 | +| `dist\UpdateClientSDK-With-QtDll.zip` | 推荐交给 SIMCAE 开发者的 SDK 压缩包 | + +SDK 不包含最终客户配置。`With-QtDll` 表示包里带的是运行所需的 Qt DLL,不是完整 Qt SDK。SDK 的目标是给 SIMCAE 打包流程提供更新客户端程序和运行库。 + +## 十四、运行数据位置 + +Windows 运行配置会导入当前用户配置,按安装运行目录计算 installation id。运行数据目录通常位于: + +`%LOCALAPPDATA%\SimCAE\HubUpdateClient\installations\<安装目录SHA256>\` + +Linux 运行数据目录通常位于: + +`$XDG_DATA_HOME/SimCAE/HubUpdateClient/installations/<安装目录SHA256>/` + +未设置 `XDG_DATA_HOME` 时通常是: + +`~/.local/share/SimCAE/HubUpdateClient/installations/<安装目录SHA256>/` + +Manifest 缓存保存在运行数据目录下的 `update/manifest_cache`。 + +## 十五、常见问题 + +| 现象 | 排查方向 | +| --- | --- | +| 客户启动后提示配置不完整 | 检查交付包是否经过 SimCAE Hub 上传注入,不要直接拿本地未注入包给客户 | +| 检查更新没有结果 | 检查后台发布是否已发布、发布包是否可用、产品编码、通道和平台是否一致 | +| 下载返回 401 | 检查客户包里的 `client_token` 是否来自当前服务端 `.env` | +| Manifest 校验失败 | 检查服务端文件是否被手工改过,大小和 SHA-256 是否与数据库一致 | +| 强制签名失败 | 检查 `manifest_public_key.pem` 与服务端私钥是否匹配 | +| 主程序启动失败 | 检查 `main_executable` 和运行目录是否正确 | +| 从 `Launcher` 启动也被拦截 | 检查服务端、SIMCAE 编译期和客户配置中的 `launch_token` 是否一致 | +| MaintenanceTool 看不到组件更新 | 检查 IFW repository 地址、`Updates.xml` 和组件版本是否正确 | diff --git a/SIMCAE打包上传.md b/SIMCAE打包上传.md new file mode 100644 index 0000000..059bad3 --- /dev/null +++ b/SIMCAE打包上传.md @@ -0,0 +1,613 @@ +# SIMCAE 打包上传 + +本文站在 SIMCAE 开发者和发布人员的角度,说明拿到 Hub 更新客户端 SDK 后,SIMCAE 怎么打安装器、怎么生成 Qt IFW 交付包、怎么上传到 SimCAE Hub。 + +服务端部署流程见 simcae-hub 项目根目录《服务端部署.md》。普通发布人员只需要拿到已经打好的更新客户端 SDK;如果需要重新生成 SDK,见源代码仓库 `SIMCAE/update-client/updater打包成SDK.md`。 + +本文下面的命令默认在 SIMCAE 项目根目录执行。下面用 SIMCAE 当前放在 simcae-hub 项目里的情况举例: + +```powershell +cd .\SIMCAE +``` + +进入后再使用相对路径,例如 `.\installer`、`.\update-client`、`.\out\build\...`。这样不要求开发者的 SIMCAE 一定放在某个固定磁盘目录。 + +## 一、先理解交付物 + +客户端交付会涉及三类文件: + +| 交付物 | 给谁用 | 作用 | +| --- | --- | --- | +| Hub 更新客户端 SDK | SIMCAE 开发者 | 提供 `Launcher.exe`、`Updater.exe`、`Bootstrap.exe` 和必要运行库 | +| Qt IFW 交付包 ZIP | 上传到 SimCAE Hub | 包含 IFW package、repository 和可选客户安装器,服务端会校验、注入配置并发布 | +| 客户安装器 | 最终客户 | 客户从门户下载后双击安装,安装后得到 `maintenancetool.exe` | + +正式主线是:开发者只上传一个 Qt IFW 交付包 ZIP,客户只从门户下载客户安装器。客户不需要手动改服务器地址、token 或 `app_config.json`。 + +## 二、准备 Hub 更新客户端 SDK + +开发者应拿到 `UpdateClientSDK-With-QtDll.zip`。 + +这个 ZIP 由 `update-client` 仓库的 SDK 打包脚本生成,SDK 维护者按源代码仓库 `SIMCAE/update-client/updater打包成SDK.md` 操作即可。 + +建议手动解压到 SIMCAE 仓库内的固定相对目录:`.\update-client\dist\UpdateClientSDK-With-QtDll`。 + +这里的 `With-QtDll` 表示包里带的是运行所需的 Qt DLL,不是完整 Qt SDK。 + +如果公司内部统一把 SDK 放在别的位置,也可以,只要后面 `$UpdateClientSdk` 指向解压后的 SDK 目录即可。 + +解压后至少应有: + +- `bin\Launcher.exe` +- `bin\Updater.exe` +- `bin\Bootstrap.exe` + +SDK 包不应该包含最终客户配置,例如 `app_config.json`、`server_config.json`、`server_config.qrc`、`manifest_public_key.pem`。这些最终配置由服务端在上传发布包时生成或注入。 + +## 三、准备 SIMCAE 已编译产物 + +默认 SIMCAE 已经在开发机上完成 Release 编译。客户端打包文档不要求每次重新全量编译 SIMCAE,因为 SIMCAE 工程很大,打安装包时通常只需要复用已有 Release 产物。 + +需要确认: + +| 内容 | 说明 | +| --- | --- | +| SIMCAE Release 构建目录 | 已经存在 `SimCAE.exe`、库文件、资源文件 | +| Qt Installer Framework | 已经安装 `binarycreator.exe` 和 `repogen.exe` | +| DAP 运行时 | 如果启用 DAP 组件,`DAPrailCalxml` 等运行时已放在打包规则要求的位置 | +| Hub 更新客户端 SDK | 已解压,并能找到 `Launcher.exe`、`Updater.exe`、`Bootstrap.exe` | + +如果业务主程序启用了“必须从 Launcher 启动”的门禁,SIMCAE 编译时使用的 launch token 必须和服务端 `.env` 里的 `SIMCAE_LAUNCH_TOKEN` 一致。 + +## 四、设置本次打包版本 + +SIMCAE 安装器文件名、IFW 组件 `package.xml` 版本、repository 里的 `Updates.xml` 版本都来自 CMake 变量 `SimCAE_Version`。这个版本默认读取 SIMCAE 仓库最近的纯数字 Git tag,例如 `1.1.3`。 + +先把几个容易混淆的“版本标签”分清楚: + +| 名称 | 写在哪里 | 谁会读取 | 作用 | +| --- | --- | --- | --- | +| Git tag | SIMCAE 仓库提交,例如 `git tag 1.1.3` | CMake 版本脚本 | 生成 `SimCAE_Version`,再写入安装器文件名和组件元数据 | +| 组件版本 | `packages/<组件ID>/meta/package.xml` 的 `` | `repogen.exe` | 生成 repository 时写入 `Updates.xml` | +| repository 组件版本 | `Updates.xml` 里的 `...` 和 `...` | `maintenancetool.exe` | 客户端判断某个组件是否需要更新 | +| ZIP 文件名 | 例如 `SimCAE-Delivery-1.1.3-windows_x86_64-msvc.zip` | 人和管理后台记录 | 方便识别上传文件,不是 MaintenanceTool 的更新依据 | + +所以 `git tag 1.1.3` 打的是 SIMCAE 源码提交标签,不是给 ZIP 文件打标签。它会被 CMake 读取后间接变成组件 `package.xml` 里的版本。真正决定 MaintenanceTool 是否更新的是服务器 repository 的 `Updates.xml`,而 `Updates.xml` 又来自组件自己的 `package.xml`。 + +正式发布时推荐在 SIMCAE 仓库给本次发布提交打纯数字 tag,再打包: + +```powershell +git tag 1.1.3 +``` + +如果只是本机演示,不想改 SIMCAE 仓库 tag,可以临时指定本次打包版本。下面命令会创建一个本地临时脚本,让 CMake 本次配置时读到 `1.1.3`: + +```powershell +$Version = "1.1.3" +$GitVersionShim = "..\.tmp\git-version-$Version.cmd" +New-Item -ItemType Directory -Force (Split-Path $GitVersionShim) | Out-Null + +@" +@echo off +if /I "%1"=="describe" ( + echo $Version + exit /b 0 +) +git %* +"@ | Set-Content -LiteralPath $GitVersionShim -Encoding ASCII +``` + +后续所有命令都复用这个 `$Version`。不要只改 ZIP 文件名,否则会出现文件名是 `1.1.3`,但组件 `package.xml` 和 `Updates.xml` 里版本还是 `0.10.1` 的错包;这种包上传后,MaintenanceTool 仍然会按 `0.10.1` 判断。 + +## 五、刷新现有 CMake 打包配置 + +下面命令只刷新已有构建目录的 CMake 配置,用来告诉打包目标 Qt IFW 和 Hub SDK 在哪里,不是全量重新编译 SIMCAE。 + +先确认当前 PowerShell 已经在 SIMCAE 项目根目录。下面给出一个常见 Qt IFW 安装路径示例;如果你的 Qt IFW 装在别的位置,只改 `$QtIfwRoot` 这一行。 + +```powershell +$Build = ".\out\build\SimCAE-release-vs2022-qt515-ifw" +$QtIfwRoot = "C:\Qt\Tools\QtInstallerFramework\4.11" +$UpdateClientSdk = ".\update-client\dist\UpdateClientSDK-With-QtDll" +``` + +检查关键文件: + +```powershell +Test-Path "$QtIfwRoot\bin\binarycreator.exe" +Test-Path "$QtIfwRoot\bin\repogen.exe" +Test-Path "$UpdateClientSdk\bin\Launcher.exe" +Test-Path "$UpdateClientSdk\bin\Updater.exe" +Test-Path "$UpdateClientSdk\bin\Bootstrap.exe" +``` + +如果本次要让 `SimCAE.exe` 只能从 `Launcher.exe` 启动,先准备 SIMCAE 编译期使用的本地打包配置。这里的 `launch_token` 必须和服务端 `.env` 里的 `SIMCAE_LAUNCH_TOKEN` 完全一致。 + +```powershell +$LauncherProductConfig = "..\.tmp\simcae-launcher-product-config.json" + +@' +{ + "app_id": "simcae", + "product_code": "simcae", + "app_name": "SimCAE", + "launch_token": "SimCAE_Launch_Token_2026_ChangeMe_32Bytes", + "license_key": "SIMCAE_LOCAL_PACKAGING_LICENSE_2026" +} +'@ | Set-Content -LiteralPath $LauncherProductConfig -Encoding UTF8 +``` + +刷新配置: + +```powershell +cmake -S . -B $Build ` + "-DSimCAE_QtIfwRoot=$QtIfwRoot" ` + "-DSimCAE_PackageHubUpdateClient=ON" ` + "-DSimCAE_HubUpdateClientSdkDir=$UpdateClientSdk" ` + "-DSimCAE_UseLauncher=ON" ` + "-DSimCAE_LauncherProductConfigFile=$LauncherProductConfig" ` + "-DGIT_EXECUTABLE=$GitVersionShim" +``` + +如果只想把 Hub 更新客户端打进安装包,但暂时不限制用户直接双击 `SimCAE.exe`,则把上面命令中的 `SimCAE_UseLauncher` 改为 `OFF`,并去掉 `SimCAE_LauncherProductConfigFile` 这一项。 + +## 六、生成客户安装器和 IFW package + +执行打包目标: + +```powershell +cmake --build $Build --config Release --target package_installer +``` + +这个目标会读取 `SIMCAE\installer` 下的配置和组件规则,整理 IFW package staging,并生成安装器。组件有哪些、每个组件包含哪些文件、组件是否必选、依赖哪些组件,应该由 SIMCAE 开发者在打包配置和 `package.xml.in` 里提前定义好;SimCAE Hub 不会自动猜测业务应该拆成哪些组件。 + +当前示例里已有两个 IFW 组件: + +| 组件目录 | 组件含义 | 元数据来源 | +| --- | --- | --- | +| `packages/com.simcae.app` | 核心程序、Launcher、Updater、Bootstrap、核心库和通用资源 | `installer/packages/meta/package.xml.in` | +| `packages/com.simcae.dap` | DAP 求解器插件及运行资源 | `installer/packages/com.simcae.dap/meta/package.xml.in` | + +Qt IFW 的组件 ID 来自 `packages/<组件ID>` 目录名,例如 `com.simcae.dap`。组件显示名称、版本、是否强制安装、依赖关系等来自该组件的 `meta/package.xml`,例如 ``、``、``、``。 + +常见输出: + +| 输出 | 说明 | +| --- | --- | +| `$Build\package` | Qt IFW package staging 目录 | +| `$Build\SimCAE-<版本>-Windows-installer.exe` | 本地生成的客户安装器 | + +确认核心组件里已经带上 Hub 更新客户端: + +```powershell +Test-Path "$Build\package\packages\com.simcae.app\data\view\bin\Launcher.exe" +Test-Path "$Build\package\packages\com.simcae.app\data\view\bin\Updater.exe" +Test-Path "$Build\package\packages\com.simcae.app\data\view\bin\Bootstrap.exe" +Test-Path "$Build\package\packages\com.simcae.app\data\view\bin\SimCAE.exe" +``` + +预期都返回 `True`。 + +再确认 package 里的组件版本就是本次 `$Version`: + +```powershell +$AppPackageXml = "$Build\package\packages\com.simcae.app\meta\package.xml" +$DapPackageXml = "$Build\package\packages\com.simcae.dap\meta\package.xml" +$AppVersion = ([xml](Get-Content -LiteralPath $AppPackageXml -Encoding UTF8 -Raw)).Package.Version +$DapVersion = ([xml](Get-Content -LiteralPath $DapPackageXml -Encoding UTF8 -Raw)).Package.Version + +if ($AppVersion -ne $Version -or $DapVersion -ne $Version) { + throw "组件版本不一致:app=$AppVersion dap=$DapVersion expected=$Version" +} + +Test-Path "$Build\SimCAE-$Version-Windows-installer.exe" +``` + +最后一行预期返回 `True`。如果这里不是 `True`,不要继续生成 repository。 + +## 七、生成 IFW repository + +MaintenanceTool 读取的是 Qt IFW repository,不是客户安装器。 + +生成前先确认 `$Build\package` 已经存在,并且里面至少有 `config` 和 `packages`: + +```powershell +Test-Path "$Build\package\config\config.xml" +Test-Path "$Build\package\packages" +``` + +预期都返回 `True`。然后生成完整 repository: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File ".\installer\scripts\build-ifw-repository.ps1" ` + -PackageDir "$Build\package" ` + -OutputDir "$Build\ifw-repository" ` + -ZipFile "$Build\SimCAE-IFW-Repository-$Version-windows_x86_64-msvc.zip" +``` + +这条命令不是“给压缩包打版本标签”。它只是调用 Qt IFW 的 `repogen.exe`,从 `$Build\package\packages` 读取已经准备好的组件目录和 `meta/package.xml`,生成 `Updates.xml` 和组件 `.7z` 包,最后把 repository 目录压成 ZIP。ZIP 文件名里的 `$Version` 只是为了让发布人员识别文件。 + +生成后检查: + +```powershell +Test-Path "$Build\ifw-repository\Updates.xml" +Select-String -LiteralPath "$Build\ifw-repository\Updates.xml" -Pattern "com.simcae.app|com.simcae.dap|" +``` + +生成的 repository 根目录必须包含 `Updates.xml`。这个 ZIP 一般不直接给客户,它是给 SimCAE Hub 后端托管,供 `maintenancetool.exe` 后续检查组件更新。 + +确认 repository 里的组件版本也是本次 `$Version`: + +```powershell +$UpdatesXml = "$Build\ifw-repository\Updates.xml" +$UpdatesContent = Get-Content -LiteralPath $UpdatesXml -Encoding UTF8 -Raw + +if ($UpdatesContent -notmatch "$([regex]::Escape($Version))") { + throw "repository Updates.xml 中没有本次版本 $Version" +} +``` + +## 八、组装 Qt IFW 交付包 ZIP + +推荐上传给 SimCAE Hub 的是交付包 ZIP,它把 package、repository 和可选安装器放在一起。 + +目录结构建议: + +- `package/config/` +- `package/packages/com.simcae.app/` +- `package/packages/com.simcae.dap/` +- `repository/Updates.xml` +- `repository/com.simcae.app/` +- `repository/com.simcae.dap/` +- `installer/SimCAE-<版本>-Windows-installer.exe` + +示例命令: + +```powershell +$PlatformKey = "windows_x86_64-msvc" +$Bundle = "$Build\SimCAE-Delivery-$Version-$PlatformKey" +$Installer = "$Build\SimCAE-$Version-Windows-installer.exe" + +if (-not (Test-Path $Installer)) { + throw "缺少本次版本安装器:$Installer" +} +if (-not (Test-Path "$Build\ifw-repository\Updates.xml")) { + throw "缺少 repository/Updates.xml,请先生成 IFW repository" +} + +Remove-Item -LiteralPath $Bundle -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force "$Bundle" | Out-Null + +Copy-Item "$Build\package" "$Bundle\package" -Recurse -Force +Copy-Item "$Build\ifw-repository" "$Bundle\repository" -Recurse -Force + +New-Item -ItemType Directory -Force "$Bundle\installer" | Out-Null +Copy-Item $Installer "$Bundle\installer\" -Force + +Compress-Archive -Path "$Bundle\*" -DestinationPath "$Build\SimCAE-Delivery-$Version-$PlatformKey.zip" -Force +``` + +`release.json` 不需要开发者手写。产品编码、产品名称、版本号、通道、平台、架构和 ABI 来自管理后台表单;运行目录和主程序名由服务端从核心组件中自动识别。 + +## 九、上传到管理后台 + +在浏览器打开管理后台,例如 `http://192.168.1.158:1798/login`。 + +按左侧菜单顺序准备基础数据。第一次发布某个产品时要完整走一遍;后续同产品、同通道、同平台发布新版本时,只需要确认这些数据仍然存在: + +1. 产品目录:确认产品编码,例如 `simcae`。 +2. 版本线:确认通道或版本线,例如 `stable`。 +3. 组件管理:确认核心组件和可选组件,例如 `com.simcae.app`、`com.simcae.dap`;如果用于 MaintenanceTool 更新,后台组件编码要和 IFW package 的 `packages/<组件ID>` 目录名一致。 +4. 平台管理:确认 `windows`、`x86_64`、`msvc`。 +5. 产品版本:创建本次版本,例如 `1.1.3`。 +6. 软件发布:创建本次发布,关联产品版本和版本线。 +7. 发布包:新增或编辑发布包。 + +上传完整 Qt IFW 交付包时,在“发布包”页面直接点击右上角新增发布包,不需要先点击某个软件卡片。这个入口只用于新建整包更新包和 Qt IFW 交付包。 + +发布包页面选择: + +| 字段 | 建议 | +| --- | --- | +| 包类型 | Qt IFW 交付包 | +| 文件 | `SimCAE-Delivery-<版本>-windows_x86_64-msvc.zip` | +| 平台 | `windows / x86_64 / msvc` | +| 状态 | 上传校验通过后变为可用 | + +操作顺序: + +1. 打开“发布包”页面。 +2. 直接点击右上角“新建发布包”。 +3. 包类型选择“Qt IFW 交付包”。此时普通新建入口只应看到“整包更新包”和“Qt IFW 交付包”。 +4. 文件名填写本次交付包文件名,例如 `SimCAE-Delivery-1.1.3-windows_x86_64-msvc.zip`。 +5. 选择产品、版本线、产品版本、发布和平台。 +6. 保存发布包记录。 +7. 点击该记录的“上传”。 +8. 选择本地生成的交付包 ZIP。 +9. 等待上传完成,状态应变为“可用”。 + +上传成功后,服务端会: + +- 校验 ZIP 安全路径和 IFW 结构。 +- 校验 `package/` 和 `repository/Updates.xml`。 +- 读取组件清单和组件版本。 +- 自动注入 `config/app_config.json`。 +- 自动写入必要的公钥配置。 +- 发布 IFW repository。 +- 生成或登记客户门户首次下载的安装器。 + +上传后建议立刻确认: + +```powershell +$Base = "http://192.168.1.158:18000/api/v1/client/ifw/repositories/simcae/stable/windows_x86_64-msvc" +(Invoke-WebRequest "$Base/Updates.xml" -UseBasicParsing).Content +``` + +预期能看到本次版本号、组件 ID 和组件名称。如果这里还是旧版本,先确认发布包状态是否为“可用”,再确认上传时选择的产品、通道和平台是否一致。 + +## 十、客户下载和安装 + +客户登录客户门户后,在下载中心下载客户安装器。客户下载到的是 `.exe` 安装器,不是 IFW repository ZIP,也不是开发者上传的交付包 ZIP。 + +客户安装后,安装目录中应包含: + +- `maintenancetool.exe` +- `components.xml` +- `network.xml` +- `view\bin\SimCAE.exe` +- `view\bin\Launcher.exe` +- `view\bin\Updater.exe` +- `view\bin\Bootstrap.exe` +- `view\bin\config\app_config.json` + +客户日常启动软件应使用 `Launcher.exe` 或安装器创建的快捷方式。组件更新、添加、移除由 `maintenancetool.exe` 负责。 + +首次安装建议按这个顺序检查: + +1. 打开客户门户。 +2. 登录有授权的客户账号。 +3. 进入下载中心。 +4. 找到对应产品和版本。 +5. 点击下载,得到 `SimCAE-<版本>-Windows-installer.exe`。 +6. 双击安装器,按页面提示完成安装。 +7. 安装后进入安装目录,确认 `maintenancetool.exe`、`components.xml` 和 `view\bin\Launcher.exe` 都存在。 +8. 双击 `Launcher.exe`,预期能启动 SimCAE。 +9. 双击 `maintenancetool.exe`,预期能看到“添加或移除组件”“更新组件”“移除所有组件”。 + +## 十一、组件更新 + +这里的“组件”指 `maintenancetool.exe` 里能看到的 Qt IFW 组件,例如 `com.simcae.app`、`com.simcae.dap`。组件更新就是“只发布某些组件的新版本,或新增一个组件”,让客户后续通过 MaintenanceTool 更新;它不是客户首次安装用的安装器,也不是 Launcher / Updater 用的整包更新 ZIP。 + +适合使用组件更新的情况: + +| 场景 | 应该怎么做 | +| --- | --- | +| 只更新 DAP 插件 | 做一个只包含 `com.simcae.dap` 的组件更新包 | +| 新增示例、模板、插件等可选功能 | 做一个包含新组件的组件更新包 | +| 一次更新几个互相依赖的组件 | 做一个多组件更新包,把这些组件一起放进去 | +| 更新核心程序、Launcher、Updater、Bootstrap 或 `app_config.json` | 更推荐重新发完整 Qt IFW 交付包 | +| 第一次发布某个产品、通道、平台 | 先发完整 Qt IFW 交付包,后面才能发组件更新 | + +组件更新包不是单独飘在系统外面的文件。它上传时必须挂到某个产品、某个产品版本、某次发布、某个平台下面。服务端会把它合并到这个产品对应通道和平台的 current repository 中。 + +### 11.1 组件版本怎么定 + +组件版本以组件自己的 `package.xml` 为准。比如 DAP 组件的版本写在这里: + +- `$Build\package\packages\com.simcae.dap\meta\package.xml` +- XML 节点是 `1.1.4` + +如果说“组件标签”,这里真正参与更新判断的是组件 ID 和组件版本:组件 ID 来自目录名 `packages/com.simcae.dap`,组件版本来自 `meta/package.xml` 里的 ``。开发者在维护 IFW package 时就应该把组件拆分、显示名、版本、必选状态和依赖关系写清楚。`repogen.exe` 生成 repository 时,会把这些信息写进 `Updates.xml`。MaintenanceTool 也是根据 `Updates.xml` 里的组件版本判断是否可更新。 + +当前 SIMCAE 全量打包默认会让所有组件跟随同一个 `$Version`,这个 `$Version` 来自 SIMCAE 仓库的纯数字 Git tag,或者前面文档里的 `$GitVersionShim` 临时版本脚本。例如 `$Version = "1.1.3"` 时,`com.simcae.app` 和 `com.simcae.dap` 默认都会变成 `1.1.3`。 + +如果只更新 DAP,不更新核心组件,规则是: + +1. 服务器当前 `com.simcae.app` 是 `1.1.3`,`com.simcae.dap` 是 `1.1.3`。 +2. 本次只把 `com.simcae.dap` 的 `package.xml` 改成 `1.1.4`。 +3. 不改 `com.simcae.app` 的 `package.xml`,它仍然保持 `1.1.3`。 +4. 生成只包含 `com.simcae.dap` 的组件更新包。 +5. 上传后,服务器 current repository 里应变成 `com.simcae.app=1.1.3`、`com.simcae.dap=1.1.4`。 + +正式流程里,建议 SIMCAE 打包侧给每个组件提供独立版本参数。当前如果只是本地演示,可以在 `$Build\package\packages\<组件ID>\meta\package.xml` 里调整目标组件的 ``,然后再生成组件更新包。不要改不更新的组件版本,也不要只改 ZIP 文件名。ZIP 名字里写了 `1.1.4`,但 `package.xml` 仍是 `1.1.3` 时,生成出来的 `Updates.xml` 也会是 `1.1.3`。 + +### 11.2 先确认服务器已有当前仓库 + +以 `simcae / stable / windows_x86_64-msvc` 为例: + +```powershell +$Base = "http://192.168.1.158:18000/api/v1/client/ifw/repositories/simcae/stable/windows_x86_64-msvc" +(Invoke-WebRequest "$Base/Updates.xml" -UseBasicParsing).Content +``` + +预期能看到当前仓库的组件,例如: + +- `com.simcae.app` +- `1.1.3` +- `com.simcae.dap` +- `1.1.3` + +如果这里访问失败,先不要上传组件更新包,说明服务器还没有这个产品、通道、平台的 current repository。 + +### 11.3 准备本地组件产物 + +开发者先按 SIMCAE 自己的规则把组件文件准备到 IFW package staging 里。组件边界应该在开发和打包配置阶段就已经分好;后面的 repository 生成命令只是读取这些组件,不会自动分析文件并替开发者拆组件。当前打包目标会把组件整理到: + +- `$Build\package\packages\com.simcae.app` +- `$Build\package\packages\com.simcae.dap` + +如果只更新 DAP 插件,先确认 DAP 组件目录存在: + +```powershell +Test-Path "$Build\package\packages\com.simcae.dap\meta\package.xml" +Test-Path "$Build\package\packages\com.simcae.dap\data" +``` + +预期都返回 `True`。同时要确认 `package.xml` 里的版本已经升高: + +```powershell +Select-String -LiteralPath "$Build\package\packages\com.simcae.dap\meta\package.xml" -Pattern "" +``` + +例如服务器当前 DAP 是 `1.1.3`,本次 DAP 组件更新包应改成 `1.1.4` 或更高。 + +### 11.4 生成组件更新 ZIP + +使用 `-Include` 只把要更新的组件打进 repository。`-Include "com.simcae.dap"` 里的值是组件 ID,也就是 `packages/com.simcae.dap` 这个目录名;它不是 ZIP 标签,也不是版本号。下面以只更新 DAP 为例: + +```powershell +$ComponentVersion = "1.1.4" +$PlatformKey = "windows_x86_64-msvc" +$ComponentUpdateRepository = "$Build\ifw-component-update-com.simcae.dap-$ComponentVersion" +$ComponentUpdateZip = "$Build\SimCAE-IFW-ComponentUpdate-com.simcae.dap-$ComponentVersion-$PlatformKey.zip" + +Remove-Item -LiteralPath $ComponentUpdateRepository -Recurse -Force -ErrorAction SilentlyContinue + +powershell -NoProfile -ExecutionPolicy Bypass -File ".\installer\scripts\build-ifw-repository.ps1" ` + -PackageDir "$Build\package" ` + -OutputDir $ComponentUpdateRepository ` + -ZipFile $ComponentUpdateZip ` + -Include "com.simcae.dap" +``` + +这条命令不会修改 `com.simcae.dap` 的 ``。它只是根据 `-Include` 选择已有组件,把该组件当前 `package.xml` 中写好的版本、显示名和依赖交给 `repogen.exe`,再生成本次组件更新 repository ZIP。 + +如果一次更新多个组件: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File ".\installer\scripts\build-ifw-repository.ps1" ` + -PackageDir "$Build\package" ` + -OutputDir "$Build\ifw-component-update-multi-$ComponentVersion" ` + -ZipFile "$Build\SimCAE-IFW-ComponentUpdate-multi-$ComponentVersion-$PlatformKey.zip" ` + -Include "com.simcae.app","com.simcae.dap" +``` + +生成后检查 ZIP 对应的展开目录: + +```powershell +Test-Path "$ComponentUpdateRepository\Updates.xml" +Get-ChildItem -LiteralPath $ComponentUpdateRepository +Select-String -LiteralPath "$ComponentUpdateRepository\Updates.xml" -Pattern "com.simcae.dap||" +``` + +单 DAP 更新包的典型结构应类似: + +- `Updates.xml` +- `com.simcae.dap/1.1.4meta.7z` +- `com.simcae.dap/1.1.4view.7z` +- `com.simcae.dap/1.1.4view.7z.sha1` + +如果 ZIP 解开后外面多套了一层目录,也可以上传;服务端会识别常见外层目录。但推荐让 ZIP 根部直接就是 `Updates.xml` 和组件目录,最不容易出错。 + +### 11.5 上传组件更新 + +管理后台操作: + +1. 打开“产品版本”,创建本次发布批次版本,例如 `1.1.4`。 +2. 打开“软件发布”,创建本次发布,通道仍选择 `stable`。 +3. 打开“发布包”,先点击要更新的软件卡片,进入该软件的发布包视图。 +4. 点击“新建组件更新包”。进入某个软件后,包类型固定为“组件更新”,产品固定为当前软件。 +5. 发布选择刚创建的 `1.1.4` 发布。 +6. 平台选择和 current repository 完全一致的 `windows / x86_64 / msvc`。 +7. 文件名填写 `SimCAE-IFW-ComponentUpdate-com.simcae.dap-1.1.4-windows_x86_64-msvc.zip`。 +8. 保存后点击“上传”。 +9. 选择上一步生成的 `$ComponentUpdateZip`。 +10. 上传成功后,发布包状态应变为“可用”。 + +上传成功后,服务端会把更新组件合并进 current repository,未变化组件保持不变。 + +### 11.6 上传后确认合并结果 + +重新读取服务器仓库: + +```powershell +$Base = "http://192.168.1.158:18000/api/v1/client/ifw/repositories/simcae/stable/windows_x86_64-msvc" +(Invoke-WebRequest "$Base/Updates.xml" -UseBasicParsing).Content +``` + +预期: + +1. 更新过的组件版本变成新版本,例如 `com.simcae.dap` 是 `1.1.4`。 +2. 未更新的组件仍然存在,例如 `com.simcae.app` 还在。 +3. 新增组件能出现在 `Updates.xml` 中。 +4. 旧版本仓库仍保存在服务端 `releases/<版本>` 目录中,current 指向最新合并结果。 + +### 11.7 常见失败提示 + +| 提示含义 | 原因 | 处理 | +| --- | --- | --- | +| 当前产品、通道和平台下还没有可合并的 IFW 当前仓库 | 还没上传过完整交付包 | 先上传完整 Qt IFW 交付包 | +| 组件版本不能倒退或重复 | 上传组件版本小于或等于服务器 current 版本 | 升高组件 `package.xml` 里的版本后重新生成 | +| 组件依赖不存在 | 新组件依赖的组件不在 current 仓库,也不在本次包里 | 先发布依赖组件,或把依赖组件一起打进本次更新包 | +| 缺少组件目录 | `Updates.xml` 声明了组件,但 ZIP 里没有对应目录 | 重新用 `build-ifw-repository.ps1` 生成 | +| 包含未在 `Updates.xml` 声明的组件目录 | ZIP 里多了未声明目录 | 删除多余目录后重新压包 | +| 未包含 `Updates.xml` | 上传的不是 repository ZIP,或服务端没有配置 `SIMCAE_IFW_REPOGEN_PATH` 来从 packages 自动生成 | 上传 repository 形态 ZIP | + +更新包失败时,服务端不会破坏原 current repository。 + +## 十二、换源 + +服务器地址变化时有两种处理方式: + +| 方式 | 适用场景 | +| --- | --- | +| 临时换源命令 | 单台客户机器临时切到新仓库 | +| RepositoryUpdate 批量换源 | 已安装客户软件批量迁移仓库地址 | + +换源只影响 `maintenancetool.exe` 访问 IFW repository。Launcher / Updater 的 API 地址来自服务端注入的 `app_config.json`,需要通过新发布包或重新安装包更新。 + +### 12.1 临时换源 + +临时换源适合开发、测试、临时排查。它不会永久改安装包里的默认源。 + +在 SIMCAE 项目根目录执行,假设客户软件安装在 `.tmp\maintenance-installed\SimCAE`: + +```powershell +$Install = ".\.tmp\maintenance-installed\SimCAE" +$Repo = "http://192.168.1.158:18000/api/v1/client/ifw/repositories/simcae/stable/windows_x86_64-msvc/" + +powershell -NoProfile -ExecutionPolicy Bypass -File ".\installer\scripts\switch-maintenance-repository.ps1" ` + -MaintenanceToolPath "$Install\maintenancetool.exe" ` + -RepositoryUrl $Repo ` + -Mode Temp ` + -Command check-updates ` + -ClearCache +``` + +注意 `$Repo` 必须是仓库根地址,不能写到 `Updates.xml`: + +- 正确:`http://192.168.1.158:18000/api/v1/client/ifw/repositories/simcae/stable/windows_x86_64-msvc/` +- 错误:`http://192.168.1.158:18000/api/v1/client/ifw/repositories/simcae/stable/windows_x86_64-msvc/Updates.xml` + +### 12.2 批量换源 + +批量换源适合服务器域名或 IP 变更。做法是在下一次 repository 的 `Updates.xml` 里加入 `RepositoryUpdate`,让 MaintenanceTool 更新组件时顺便替换本机源地址。 + +示例:把旧源 `http://192.168.1.158:18000/...` 替换为新源 `https://download.simcae.example.com/...`: + +```powershell +$UpdatesXml = "$Build\ifw-repository\Updates.xml" +$OldRepo = "http://192.168.1.158:18000/api/v1/client/ifw/repositories/simcae/stable/windows_x86_64-msvc/" +$NewRepo = "https://download.simcae.example.com/api/v1/client/ifw/repositories/simcae/stable/windows_x86_64-msvc/" + +powershell -NoProfile -ExecutionPolicy Bypass -File ".\installer\scripts\write-repository-update.ps1" ` + -UpdatesXml $UpdatesXml ` + -Action replace ` + -OldUrl $OldRepo ` + -NewUrl $NewRepo ` + -DisplayName "SimCAE stable component repository" ` + -ClearExisting +``` + +写入后重新压 repository 或重新组装 Qt IFW 交付包,再上传到 SimCAE Hub。客户下一次通过 MaintenanceTool 检查或更新组件后,会把仓库地址换成新地址。 + +## 十三、常见问题 + +| 现象 | 原因和处理 | +| --- | --- | +| 上传提示缺少 `Updates.xml` | 选择的不是 repository 或交付包结构不对 | +| MaintenanceTool 看不到更新 | 服务器仓库版本没有高于本机 `components.xml` 里的版本 | +| 客户下载不到安装器 | 发布包不是 Qt IFW 交付包,或客户安装器生成/登记失败 | +| Launcher 启动主程序失败 | `SIMCAE_LAUNCH_TOKEN` 和业务主程序编译时 token 不一致 | +| 直接双击 `SimCAE.exe` 被拦截 | 这是启用 Launcher 启动门禁后的预期行为 | +| 可选组件删除后 Updater 报缺文件 | Manifest 中可选组件文件没有标为可选,或组件边界划分不对 | diff --git a/scripts/ReadMe.txt b/scripts/ReadMe.txt index 7a14b87..b0cb002 100644 --- a/scripts/ReadMe.txt +++ b/scripts/ReadMe.txt @@ -25,9 +25,8 @@ SimCAE Hub 的 Go API。 SDK 打包命令、两种打包模式、参数含义和输出位置,统一看: -../打包成SDK.md +../updater打包成SDK.md -生成 SDK 后,把 Launcher、Updater、Bootstrap 和必要运行库放进业务软件 -根目录或 bin 目录,再把完整软件目录压缩上传到 SimCAE Hub 管理后台的 -发布包页面。服务端会生成 config/app_config.json,并在启用 Manifest 签名 -时生成 config/manifest_public_key.pem。 +生成 SDK 后,SDK 根目录里只带给 SIMCAE 发布人员看的 SIMCAE打包上传.md。 +后续如何把 Launcher、Updater、Bootstrap 和必要运行库放进业务软件、如何 +组装 Qt IFW 交付包并上传,以该文档为准。 diff --git a/scripts/package-sdk.ps1 b/scripts/package-sdk.ps1 index b8dde53..4be1df8 100644 --- a/scripts/package-sdk.ps1 +++ b/scripts/package-sdk.ps1 @@ -10,14 +10,15 @@ param( $ErrorActionPreference = "Stop" $RepoRoot = Split-Path -Parent $PSScriptRoot +$DefaultSdkName = if ($IncludeQtRuntime) { "UpdateClientSDK-With-QtDll" } else { "UpdateClientSDK" } if ([string]::IsNullOrWhiteSpace($SourceDir)) { $SourceDir = Join-Path $RepoRoot "out/bin/Release" } if ([string]::IsNullOrWhiteSpace($OutputDir)) { - $OutputDir = Join-Path $RepoRoot "dist/SimCAEHubUpdateClientSDK" + $OutputDir = Join-Path $RepoRoot "dist/$DefaultSdkName" } if ([string]::IsNullOrWhiteSpace($ZipFile)) { - $ZipFile = Join-Path $RepoRoot "dist/SimCAEHubUpdateClientSDK.zip" + $ZipFile = Join-Path $RepoRoot "dist/$DefaultSdkName.zip" } $source = (Resolve-Path $SourceDir).Path @@ -45,8 +46,7 @@ $binDir = Join-Path $OutputDir "bin" $configDir = Join-Path $OutputDir "config" $scriptsDir = Join-Path $OutputDir "scripts" $commonDir = Join-Path $OutputDir "Common" -$docsDir = Join-Path $OutputDir "Docs" -New-Item $binDir,$configDir,$scriptsDir,$commonDir,$docsDir -ItemType Directory -Force | Out-Null +New-Item $binDir,$configDir,$scriptsDir,$commonDir -ItemType Directory -Force | Out-Null $excludedTopLevel = @("config", "update", "update_temp", "manifest_public_key.pem") if (-not $IncludeDemoMainApp) { $excludedTopLevel += "MainApp.exe" } @@ -108,21 +108,12 @@ foreach ($commonFile in $commonSourceFiles) { Copy-Item $commonPath (Join-Path $commonDir $commonFile) -Force } -$docsSourceDir = Join-Path $RepoRoot "Docs" -if (Test-Path $docsSourceDir) { - Copy-Item (Join-Path $docsSourceDir "*") $docsDir -Recurse -Force -} - -$wordGuideSource = @($RepoRoot, $docsSourceDir) | - Where-Object { Test-Path $_ } | - ForEach-Object { Get-ChildItem $_ -File -Filter "*.docx" } | - Where-Object { $_.Name -like "*SDK*.docx" -and $_.Name -notlike "~$*" } | - Sort-Object Name | - Select-Object -First 1 -if ($wordGuideSource) { - Copy-Item $wordGuideSource.FullName (Join-Path $OutputDir $wordGuideSource.Name) -Force +$sdkGuideName = [string]::Concat("SIMCAE", [char]0x6253, [char]0x5305, [char]0x4e0a, [char]0x4f20, ".md") +$sdkGuideSource = Join-Path $RepoRoot $sdkGuideName +if (Test-Path $sdkGuideSource) { + Copy-Item $sdkGuideSource (Join-Path $OutputDir $sdkGuideName) -Force } else { - Write-Warning "SDK Word guide is missing. Continue packaging with Markdown documents in Docs/." + throw "SIMCAE packaging guide is missing: $sdkGuideSource" } Copy-Item (Join-Path $PSScriptRoot "package-client.ps1") (Join-Path $scriptsDir "package-client.ps1") -Force @@ -137,8 +128,8 @@ Copy-Item (Join-Path $PSScriptRoot "install-sdk.ps1") (Join-Path $scriptsDir "in contains_demo_main_app = [bool]$IncludeDemoMainApp contains_qt_runtime = [bool]$IncludeQtRuntime contains_final_config = $false - docs_entry = "Docs/01-客户端接入打包部署指南.md" - word_guide_included = [bool]$wordGuideSource + docs_entry = $sdkGuideName + word_guide_included = $false integration_sources = $commonSourceFiles } | ConvertTo-Json -Depth 3 | Set-Content (Join-Path $OutputDir "sdk_manifest.json") -Encoding UTF8 diff --git a/scripts/package-sdk.sh b/scripts/package-sdk.sh index 7fc54ef..39cac53 100644 --- a/scripts/package-sdk.sh +++ b/scripts/package-sdk.sh @@ -5,8 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SOURCE_DIR="$REPO_ROOT/out/linux/bin" -OUTPUT_DIR="$REPO_ROOT/dist/SimCAEHubUpdateClientSDK-linux" -ARCHIVE_FILE="$REPO_ROOT/dist/SimCAEHubUpdateClientSDK-linux.tar.gz" +OUTPUT_DIR="$REPO_ROOT/dist/UpdateClientSDK-linux" +ARCHIVE_FILE="$REPO_ROOT/dist/UpdateClientSDK-linux.tar.gz" SDK_VERSION="0.1.0" INCLUDE_DEMO_MAIN_APP=0 INCLUDE_QT_RUNTIME=0 @@ -17,8 +17,8 @@ Usage: package-sdk.sh [options] Options: --source-dir DIR Linux Release output directory. Default: ./out/linux/bin - --output-dir DIR SDK directory to generate. Default: ./dist/SimCAEHubUpdateClientSDK-linux - --archive FILE SDK tar.gz path. Default: ./dist/SimCAEHubUpdateClientSDK-linux.tar.gz + --output-dir DIR SDK directory to generate. Default: ./dist/UpdateClientSDK-linux + --archive FILE SDK tar.gz path. Default: ./dist/UpdateClientSDK-linux.tar.gz --sdk-version VERSION SDK version. Default: 0.1.0 --include-demo-mainapp Include MainApp demo executable in SDK bin. --include-qt-runtime Include Qt runtime files from the Release output directory. @@ -58,7 +58,7 @@ if [[ -n "$DEBUG_ARTIFACT" ]]; then fi rm -rf "$OUTPUT_DIR" -mkdir -p "$OUTPUT_DIR/bin" "$OUTPUT_DIR/config" "$OUTPUT_DIR/scripts" "$OUTPUT_DIR/Common" "$OUTPUT_DIR/Docs" +mkdir -p "$OUTPUT_DIR/bin" "$OUTPUT_DIR/config" "$OUTPUT_DIR/scripts" "$OUTPUT_DIR/Common" is_qt_runtime_item() { local base="$1" @@ -103,17 +103,13 @@ for common_file in ConfigHelper.h ConfigHelper.cpp IntegrityHelper.h IntegrityHe cp "$common_path" "$OUTPUT_DIR/Common/$common_file" done -if [[ -d "$REPO_ROOT/Docs" ]]; then - cp -a "$REPO_ROOT/Docs/." "$OUTPUT_DIR/Docs/" -fi - -WORD_GUIDE="$(find "$REPO_ROOT" "$REPO_ROOT/Docs" -maxdepth 1 -type f -name '*SDK*.docx' ! -name '~$*' 2>/dev/null | sort | sed -n '1p')" -WORD_GUIDE_INCLUDED=false -if [[ -n "$WORD_GUIDE" ]]; then - cp "$WORD_GUIDE" "$OUTPUT_DIR/$(basename "$WORD_GUIDE")" - WORD_GUIDE_INCLUDED=true +SDK_GUIDE_NAME="SIMCAE打包上传.md" +SDK_GUIDE_SOURCE="$REPO_ROOT/$SDK_GUIDE_NAME" +if [[ -f "$SDK_GUIDE_SOURCE" ]]; then + cp "$SDK_GUIDE_SOURCE" "$OUTPUT_DIR/$SDK_GUIDE_NAME" else - echo "Warning: SDK Word guide is missing. Continue packaging with Markdown documents in Docs/." >&2 + echo "SIMCAE packaging guide is missing: $SDK_GUIDE_SOURCE" >&2 + exit 1 fi cp "$SCRIPT_DIR/package-sdk.sh" "$OUTPUT_DIR/scripts/package-sdk.sh" @@ -135,8 +131,8 @@ cat > "$OUTPUT_DIR/sdk_manifest.json" <