Skip to content

Commit 242b504

Browse files
JusterZhuCopilot
andcommitted
Document Core manifest support
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 86ba40d commit 242b504

3 files changed

Lines changed: 390 additions & 3 deletions

File tree

website/docs/doc/GeneralUpdate.Core.md

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public GeneralUpdateBootstrap SetSource(
177177
string? token = null)
178178
```
179179

180-
`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json` 或运行时发现机制里,只在代码中指定服务端入口和密钥。
180+
`SetSource` 是轻配置入口,适合把应用身份信息放到 `generalupdate.manifest.json` 或运行时发现机制里,只在代码中指定服务端入口和密钥。若需要精确控制清单中的每个身份字段,建议使用后文的 `ManifestInfo.Load().ToUpdateRequest()` 再调用 `SetConfig(request)`
181181

182182
```csharp
183183
await new GeneralUpdateBootstrap()
@@ -283,6 +283,134 @@ var request = new UpdateRequestBuilder()
283283
var request = UpdateRequestBuilder.Create().Build();
284284
```
285285

286+
## 应用身份清单:generalupdate.manifest.json
287+
288+
`generalupdate.manifest.json` 是由 `GeneralUpdate.Tools` 生成、由 Core 消费的应用身份清单。它把“主程序叫什么、当前版本是多少、升级程序叫什么、产品标识是什么、升级程序放在哪个目录”等稳定元数据从代码配置中移出来,代码里只保留服务端地址、密钥、令牌等运行时或敏感参数。
289+
290+
推荐把它放在应用安装目录,也就是 `UpdateRequest.InstallPath` 指向的目录。默认情况下 `InstallPath``AppDomain.CurrentDomain.BaseDirectory`,因此普通桌面应用通常把清单放在主程序输出目录根部。
291+
292+
```text
293+
MyProduct/
294+
├─ ClientSample.exe
295+
├─ generalupdate.manifest.json
296+
└─ update/
297+
└─ UpgradeSample.exe
298+
```
299+
300+
### 清单结构
301+
302+
Tools 生成的 JSON 使用小驼峰字段名,Core 中对应类型是 `ManifestInfo`
303+
304+
```json
305+
{
306+
"mainAppName": "ClientSample.exe",
307+
"clientVersion": "1.0.0",
308+
"appType": "Client",
309+
"updateAppName": "UpgradeSample.exe",
310+
"upgradeClientVersion": "1.0.0",
311+
"productId": "sample-product",
312+
"updatePath": "update/"
313+
}
314+
```
315+
316+
| JSON 字段 | Core 字段 | 说明 |
317+
| --- | --- | --- |
318+
| `mainAppName` | `MainAppName` | 主程序可执行文件名。升级完成后用于重新启动主程序,也用于识别当前产品。 |
319+
| `clientVersion` | `ClientVersion` | 当前主程序版本。Core 用它向服务端询问是否有主程序更新。 |
320+
| `appType` | `AppType` | 当前进程角色字符串,例如 `Client``Upgrade``OssClient``OssUpgrade`|
321+
| `updateAppName` | `UpdateAppName` | 升级程序文件名,默认 `Update.exe`|
322+
| `upgradeClientVersion` | `UpgradeClientVersion` | 升级程序自身版本。Core 用它判断是否需要先更新升级程序。 |
323+
| `productId` | `ProductId` | 产品标识。一个更新服务管理多个产品时用于区分产品。 |
324+
| `updatePath` | `UpdatePath` | 升级程序所在目录;可以是相对 `InstallPath` 的目录,例如 `update/`|
325+
326+
清单刻意不包含 `UpdateUrl``ReportUrl``AppSecretKey``Scheme``Token` 等服务端和认证信息。这样可以让 Tools 负责构建可发布的身份元数据,而密钥仍由应用代码、配置中心或部署环境提供。
327+
328+
### Tools 如何生成清单
329+
330+
`GeneralUpdate.Tools` 的配置生成流程会解析主程序和升级程序的 `.csproj`,校验版本号,然后写出 `generalupdate.manifest.json`
331+
332+
| Tools 阶段 | 作用 |
333+
| --- | --- |
334+
| `CsprojParseStep` | 解析主程序 `.csproj`;如果提供升级程序 `.csproj`,也会一起解析。 |
335+
| `SemverValidateStep` | 校验 `ClientVersion``UpgradeClientVersion` 必须符合 semver,例如 `1.0.0`|
336+
| `ManifestBuildStep` | 如果 UI 中没有手动填写 `MainAppName` / `UpdateAppName`,使用 `.csproj``AssemblyName` 补齐。 |
337+
| `FileEmitStep` | 把清单写到输出目录,文件名固定为 `generalupdate.manifest.json`|
338+
339+
配置界面的发布样例流程还会调用 `SamplePublisherService.PublishAsync(...)`,把主程序输出、升级程序输出和清单一起组织到可运行样例目录中。因此新手不需要从零手写完整 `UpdateRequest`,可以先用 Tools 生成清单,再在应用代码中补充服务端入口和密钥。
340+
341+
### Core 如何读取清单
342+
343+
Core 侧提供 `ManifestInfo`
344+
345+
```csharp
346+
using GeneralUpdate.Core.Configuration;
347+
348+
var manifest = ManifestInfo.Load();
349+
if (manifest == null)
350+
throw new FileNotFoundException("generalupdate.manifest.json was not found.");
351+
352+
var request = manifest.ToUpdateRequest();
353+
request.UpdateUrl = "https://update.example.com/api/upgrade/verification";
354+
request.ReportUrl = "https://update.example.com/api/upgrade/report";
355+
request.AppSecretKey = "your-app-secret";
356+
request.InstallPath = AppDomain.CurrentDomain.BaseDirectory;
357+
358+
await new GeneralUpdateBootstrap()
359+
.SetConfig(request)
360+
.SetOption(Option.AppType, AppType.Client)
361+
.LaunchAsync();
362+
```
363+
364+
`ManifestInfo.Load()` 默认从 `AppDomain.CurrentDomain.BaseDirectory` 读取清单;`ManifestInfo.Load(path)` 可以从指定文件读取。`ToUpdateRequest()` 会把清单转换成一个最小 `UpdateRequest`,但不会填充服务端地址和密钥。
365+
366+
`ClientStrategy` 在标准工作流开始时也会调用 `AppMetadataDiscoverer.Discover(context)`,从 `InstallPath/generalupdate.manifest.json` 补齐空的身份字段。优先级规则很重要:**代码中已经提供的值优先,清单只补齐空字段**。这意味着你可以用清单作为默认身份来源,同时在特殊环境中用代码覆盖某个字段。
367+
368+
### 推荐配置方式
369+
370+
对业务应用来说,最清晰的方式是“清单提供身份,代码提供服务端和密钥”。
371+
372+
```csharp
373+
using GeneralUpdate.Core;
374+
using GeneralUpdate.Core.Configuration;
375+
376+
static UpdateRequest CreateRequestFromManifest()
377+
{
378+
var manifest = ManifestInfo.Load()
379+
?? throw new InvalidOperationException("Missing generalupdate.manifest.json.");
380+
381+
var request = manifest.ToUpdateRequest();
382+
request.UpdateUrl = "https://update.example.com/api/upgrade/verification";
383+
request.ReportUrl = "https://update.example.com/api/upgrade/report";
384+
request.AppSecretKey = Environment.GetEnvironmentVariable("GENERALUPDATE_APP_SECRET")
385+
?? throw new InvalidOperationException("Missing update secret.");
386+
request.InstallPath = AppDomain.CurrentDomain.BaseDirectory;
387+
388+
return request;
389+
}
390+
391+
await new GeneralUpdateBootstrap()
392+
.SetConfig(CreateRequestFromManifest())
393+
.SetOption(Option.AppType, AppType.Client)
394+
.LaunchAsync();
395+
```
396+
397+
这种写法的好处是:
398+
399+
| 不再硬编码 | 仍由代码或环境提供 |
400+
| --- | --- |
401+
| `MainAppName``ClientVersion``UpdateAppName``UpgradeClientVersion``ProductId``UpdatePath` | `UpdateUrl``ReportUrl``AppSecretKey``Scheme``Token`、事件、扩展点、运行选项 |
402+
403+
### 版本回写
404+
405+
清单不只是启动时读取。更新成功后,Core 会把新版本写回安装目录下的同一个 `generalupdate.manifest.json`
406+
407+
| 场景 | 回写字段 |
408+
| --- | --- |
409+
| 主程序更新完成 | `ClientVersion` |
410+
| 升级程序自身更新完成 | `UpgradeClientVersion` |
411+
412+
这样下一次轮询或启动时,Core 会从最新版本继续向服务端验证,而不是继续使用打包时的旧版本。这个行为依赖安装目录可写;如果应用安装在受限目录,需要确保升级程序拥有写入清单的权限。
413+
286414
## 运行选项:Option
287415

288416
Core 使用强类型 `Option<T>` 注册运行时选项,并通过 `SetOption` 设置值。
@@ -822,6 +950,7 @@ Core 消费更新清单和更新包;`GeneralUpdate.Tools` 负责辅助生成
822950
| Tools 能力 | Core 中对应消费点 |
823951
| --- | --- |
824952
| Patch Package | `Option.PatchEnabled``UseDiffPipeline`、差分补丁处理。 |
953+
| Manifest Generator | `ManifestInfo``AppMetadataDiscoverer`、版本回写。 |
825954
| Extension Package | 作为更新包内容或扩展包分发,由下载和部署流程消费。 |
826955
| OSS Config | `OssClient` / `OssUpgrade` 角色读取 OSS 配置并下载。 |
827956
| Hash / Simulation / Report | 对应 `Option.VerifyChecksum`、下载后校验和状态上报。 |

website/i18n/en/docusaurus-plugin-content-docs/current/doc/GeneralUpdate.Core.md

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ public GeneralUpdateBootstrap SetSource(
175175
string? token = null)
176176
```
177177

178-
`SetSource` is a lightweight entry point when identity metadata is discovered elsewhere, such as from `generalupdate.manifest.json`.
178+
`SetSource` is a lightweight entry point when identity metadata is discovered elsewhere, such as from `generalupdate.manifest.json`. When you need precise control over every manifest identity field, prefer `ManifestInfo.Load().ToUpdateRequest()` followed by `SetConfig(request)`.
179179

180180
```csharp
181181
await new GeneralUpdateBootstrap()
@@ -281,6 +281,134 @@ var request = new UpdateRequestBuilder()
281281
var request = UpdateRequestBuilder.Create().Build();
282282
```
283283

284+
## Application identity manifest: generalupdate.manifest.json
285+
286+
`generalupdate.manifest.json` is the application identity manifest generated by `GeneralUpdate.Tools` and consumed by Core. It moves stable metadata such as the main executable name, current version, updater executable name, product ID, and updater directory out of code configuration. Application code can then focus on runtime and sensitive values such as server URLs, secrets, and tokens.
287+
288+
Place the file in the application install directory, the same directory referenced by `UpdateRequest.InstallPath`. By default, `InstallPath` is `AppDomain.CurrentDomain.BaseDirectory`, so desktop applications normally place the manifest in the main app output root.
289+
290+
```text
291+
MyProduct/
292+
├─ ClientSample.exe
293+
├─ generalupdate.manifest.json
294+
└─ update/
295+
└─ UpgradeSample.exe
296+
```
297+
298+
### Manifest structure
299+
300+
The JSON generated by Tools uses camelCase property names. The Core-side type is `ManifestInfo`.
301+
302+
```json
303+
{
304+
"mainAppName": "ClientSample.exe",
305+
"clientVersion": "1.0.0",
306+
"appType": "Client",
307+
"updateAppName": "UpgradeSample.exe",
308+
"upgradeClientVersion": "1.0.0",
309+
"productId": "sample-product",
310+
"updatePath": "update/"
311+
}
312+
```
313+
314+
| JSON field | Core field | Description |
315+
| --- | --- | --- |
316+
| `mainAppName` | `MainAppName` | Main application executable name. Used to restart the app after update and identify the current product. |
317+
| `clientVersion` | `ClientVersion` | Current main application version. Core sends it to the server to check whether a main-app update exists. |
318+
| `appType` | `AppType` | Current process role, such as `Client`, `Upgrade`, `OssClient`, or `OssUpgrade`. |
319+
| `updateAppName` | `UpdateAppName` | Updater executable name. Defaults to `Update.exe`. |
320+
| `upgradeClientVersion` | `UpgradeClientVersion` | Updater application's own version. Core uses it to decide whether the updater must be updated first. |
321+
| `productId` | `ProductId` | Product identifier for servers that manage multiple products. |
322+
| `updatePath` | `UpdatePath` | Directory containing the updater. It can be relative to `InstallPath`, for example `update/`. |
323+
324+
The manifest intentionally does not include `UpdateUrl`, `ReportUrl`, `AppSecretKey`, `Scheme`, or `Token`. This keeps Tools responsible for build-time identity metadata while secrets still come from application code, a configuration service, or the deployment environment.
325+
326+
### How Tools generates the manifest
327+
328+
The `GeneralUpdate.Tools` configuration flow parses the main-app and updater `.csproj` files, validates version values, and emits `generalupdate.manifest.json`.
329+
330+
| Tools step | Responsibility |
331+
| --- | --- |
332+
| `CsprojParseStep` | Parses the main application `.csproj`; if an updater `.csproj` is provided, parses it as well. |
333+
| `SemverValidateStep` | Validates that `ClientVersion` and `UpgradeClientVersion` use semver, for example `1.0.0`. |
334+
| `ManifestBuildStep` | Fills missing `MainAppName` / `UpdateAppName` from `.csproj` `AssemblyName` values. |
335+
| `FileEmitStep` | Writes the manifest to the output directory with the fixed name `generalupdate.manifest.json`. |
336+
337+
The sample publishing flow in the configuration UI also calls `SamplePublisherService.PublishAsync(...)` to place the main-app output, updater output, and manifest into one runnable sample directory. New users therefore do not need to hand-write a complete `UpdateRequest`: they can generate the manifest with Tools and only add server endpoints and secrets in application code.
338+
339+
### How Core reads the manifest
340+
341+
Core provides `ManifestInfo`:
342+
343+
```csharp
344+
using GeneralUpdate.Core.Configuration;
345+
346+
var manifest = ManifestInfo.Load();
347+
if (manifest == null)
348+
throw new FileNotFoundException("generalupdate.manifest.json was not found.");
349+
350+
var request = manifest.ToUpdateRequest();
351+
request.UpdateUrl = "https://update.example.com/api/upgrade/verification";
352+
request.ReportUrl = "https://update.example.com/api/upgrade/report";
353+
request.AppSecretKey = "your-app-secret";
354+
request.InstallPath = AppDomain.CurrentDomain.BaseDirectory;
355+
356+
await new GeneralUpdateBootstrap()
357+
.SetConfig(request)
358+
.SetOption(Option.AppType, AppType.Client)
359+
.LaunchAsync();
360+
```
361+
362+
`ManifestInfo.Load()` reads from `AppDomain.CurrentDomain.BaseDirectory` by default. `ManifestInfo.Load(path)` reads a specific file. `ToUpdateRequest()` converts the manifest into a minimal `UpdateRequest`, but it does not populate server URLs or secrets.
363+
364+
At the beginning of the standard workflow, `ClientStrategy` also calls `AppMetadataDiscoverer.Discover(context)` and fills empty identity fields from `InstallPath/generalupdate.manifest.json`. The precedence rule matters: **values already provided in code win; the manifest only fills empty fields**. This lets you use the manifest as the default identity source while still overriding individual fields in special environments.
365+
366+
### Recommended configuration pattern
367+
368+
For application code, the clearest pattern is "manifest for identity, code/environment for server and secrets".
369+
370+
```csharp
371+
using GeneralUpdate.Core;
372+
using GeneralUpdate.Core.Configuration;
373+
374+
static UpdateRequest CreateRequestFromManifest()
375+
{
376+
var manifest = ManifestInfo.Load()
377+
?? throw new InvalidOperationException("Missing generalupdate.manifest.json.");
378+
379+
var request = manifest.ToUpdateRequest();
380+
request.UpdateUrl = "https://update.example.com/api/upgrade/verification";
381+
request.ReportUrl = "https://update.example.com/api/upgrade/report";
382+
request.AppSecretKey = Environment.GetEnvironmentVariable("GENERALUPDATE_APP_SECRET")
383+
?? throw new InvalidOperationException("Missing update secret.");
384+
request.InstallPath = AppDomain.CurrentDomain.BaseDirectory;
385+
386+
return request;
387+
}
388+
389+
await new GeneralUpdateBootstrap()
390+
.SetConfig(CreateRequestFromManifest())
391+
.SetOption(Option.AppType, AppType.Client)
392+
.LaunchAsync();
393+
```
394+
395+
This keeps the split explicit:
396+
397+
| No longer hard-coded | Still provided by code or environment |
398+
| --- | --- |
399+
| `MainAppName`, `ClientVersion`, `UpdateAppName`, `UpgradeClientVersion`, `ProductId`, `UpdatePath` | `UpdateUrl`, `ReportUrl`, `AppSecretKey`, `Scheme`, `Token`, events, extension points, runtime options |
400+
401+
### Version write-back
402+
403+
The manifest is not only read at startup. After a successful update, Core writes the new version back to the same `generalupdate.manifest.json` under the install directory:
404+
405+
| Scenario | Field written back |
406+
| --- | --- |
407+
| Main application update completed | `ClientVersion` |
408+
| Updater self-update completed | `UpgradeClientVersion` |
409+
410+
On the next polling cycle or process start, Core therefore validates from the latest applied version instead of the build-time version. This requires the install directory to be writable; if the application is installed under a restricted directory, ensure the updater has permission to update the manifest file.
411+
284412
## Runtime options: Option
285413

286414
Core uses strongly typed `Option<T>` values and sets them with `SetOption`.
@@ -818,6 +946,7 @@ Core consumes manifests and packages. `GeneralUpdate.Tools` helps generate and v
818946
| Tools capability | Core consumption point |
819947
| --- | --- |
820948
| Patch Package | `Option.PatchEnabled`, `UseDiffPipeline`, differential patch processing. |
949+
| Manifest Generator | `ManifestInfo`, `AppMetadataDiscoverer`, version write-back. |
821950
| Extension Package | Distributed as package content and consumed by download/deploy flow. |
822951
| OSS Config | `OssClient` / `OssUpgrade` roles. |
823952
| Hash / Simulation / Report | `Option.VerifyChecksum`, post-download verification, and status reporting. |

0 commit comments

Comments
 (0)