Vernext 3.0.0

dotnet add package Vernext --version 3.0.0
                    
NuGet\Install-Package Vernext -Version 3.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Vernext" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Vernext" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Vernext" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Vernext --version 3.0.0
                    
#r "nuget: Vernext, 3.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Vernext@3.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Vernext&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Vernext&version=3.0.0
                    
Install as a Cake Tool

Vernext SDK

最低 .NET 8,一个 Vernext NuGet,提供检查、下载校验、准备和更新交接。默认 Rust helper 按 RID 随应用交付;helper 无需外部 .NET Runtime,应用不配置 helper 密钥或选择语言。SDK 可独立使用,不要求配套 Server、Publisher、UI 框架或 DI 容器。

安装与平台

本说明对应 3.0.0,从 NuGet.org 安装:

dotnet add package Vernext --version 3.0.0

包目标 RID 为 win-x64、linux-x64、osx-arm64、osx-x64;本机生成的测试候选只含当前 RID。正式发布流程在 Windows、Linux、macOS arm64/x64 上执行原生安装测试,再用同一个四 RID 包验证 .NET 8 JIT 单文件与 Native AOT 消费,通过后才上传。UI 组合、最低 OS、Developer ID 和公证分别以实测为准,历史发行证据不替代当前版本验收。

3.0.0 增加只读 IUpdateManager.CurrentVersion,展示与检查共用同一版本;自行实现该接口的消费者需要补齐属性。默认检查器在解析时使用 DI 最终配置,单独注册的 UpdateOptions 实例或工厂可放在 AddVernext() 前后。提交使用 CommitAsync(bool restart, CancellationToken),多实例保护按需调用 AcquireApplicationLock();失败回滚和事务清理由组件内部承担,无启动恢复组合入口或公开历史结果查询。

helper 与签名保持为应用旁的独立文件;许可证和第三方声明也会自动复制到构建与发布目录。分发应用时保留这些文件,无需额外配置复制步骤。

macOS 直接发布到 .app/Contents/MacOS/ 时,SDK 自动把 .sig 和 Vernext 许可放入 Contents/Resources/,避免数据进入代码目录而破坏 bundle 签名。helper 不应在生成 Vernext .sig 后重签或改写;应用自身的 bundle 签名和公证仍属于应用发行流程。

当前包的 macOS helper 尚未完成 Apple Developer ID 签名和公证,本地 ad-hoc 签名验收不代表 Gatekeeper 放行。从互联网下载的消费应用可能被拦截或需要手动允许。需要正式公证时,不能只签外层 .app:应先取得已完成 Developer ID 签名、再生成匹配 Vernext .sig 的 helper 资产并随 SDK 组包,然后签名和公证完整应用。源码维护者可以自行签名组包,但不能直接重签现有包内的 helper;普通消费应用无需配置 helper 密钥。

Vernext 采用 MIT 许可,全文随包提供。项目地址为 github.com/zeekmeta/Vernext;源码仓库目前为私有,计划后续开源。安装和使用 NuGet 包不需要仓库访问权限。

不使用容器

普通 JSON 文件和返回相同 JSON 的 API 使用同一入口,不根据 URL 后缀判断协议:

using Vernext;

using var updater = UpdateManager.Create(
    new Uri("https://downloads.example.com/my-app/win-x64.json"));
Console.WriteLine(updater.CurrentVersion);

如果使用配套 Server,从管理台复制应用配置。SDK 组合渠道和当前 RID 的地址,并验证发布签名和目标身份:

using var updater = UpdateManager.Create(new UpdateOptions
{
    ServiceAddress = new Uri("https://updates.example.com/"),
    ApplicationId = "my-app",
    PublicKeyPem = publicKeyPem
});

updater.CurrentVersion 是检查更新实际使用的当前版本,界面可直接读取。优先使用 UpdateOptions.CurrentVersion,否则读取入口程序集的 AssemblyInformationalVersion,缺失时读取程序集版本;无法确定时明确失败。版本在访问属性或检查时读取,保留预发布标签与构建元数据,不读取 SDK 自身版本。使用 SDK 的应用无需重复维护一个版本变量;特殊宿主才覆盖。默认比较接受 2~4 段数字及 SemVer 预发布规则,构建元数据不影响优先级。

使用容器

using Microsoft.Extensions.DependencyInjection;
using Vernext;

services.AddVernext(updateUrl);
services.AddSingleton<MainViewModel>();

public sealed class MainViewModel(IUpdateManager updater)
{
    public string CurrentVersion => updater.CurrentVersion;

    public Task<UpdateCandidate?> CheckAsync(CancellationToken ct)
        => updater.CheckAsync(ct);
}

Server 配置使用 services.AddVernext(new UpdateOptions { ... })。已有企业更新接口时:

services.AddVernext();
services.AddSingleton<IUpdateChecker, CompanyUpdateChecker>();
services.AddSingleton<IUpdateFileProvider, CompanyFileProvider>();
services.AddSingleton<MainViewModel>();

默认组件使用 TryAddSingleton;自定义注册放在 AddVernext 前会被保留,放在后面则按标准容器的单服务最后注册规则生效。管理器只注入单个接口,不枚举全部实现;IEnumerable<T> 仍包含所有描述。不使用 AddVernext<TChecker>、服务定位器或内部容器。

配置也遵循同一规则。通常直接把 UpdateOptions 传给 AddVernext;已有容器配置时,也可单独注册 UpdateOptions 实例或单例工厂,放在 AddVernext() 前后均可。默认检查器在首次解析时读取容器最终选定的配置,不在注册时判断服务地址是否存在。自定义检查器生效时,不创建或校验未使用的默认检查器。

AddVernext 不合并配置,也不提供热更新;重复配置同一服务仍遵循标准 DI 规则。显式清单地址由 AddVernext(updateUrl) 提供,不能同时在最终配置中指定 ServiceAddress。

IUpdateManager 对应一个 UpdateManager 单例。构造函数必需检查器、文件获取器、验证器、执行器,直接接收这四个接口。没有地址且未注册检查器时,解析管理器明确失败。注册和构造不联网、不启动 helper;纯检查不要求安装目录可用。

只使用 Microsoft.Extensions.DependencyInjection.Abstractions 提供薄注册扩展,不要求 Host。不使用 DI 的应用仍只引用同一个 Vernext 包。

两种接入共用的调用流程

以下使用默认目录执行器。确认、保存、错误展示和退出方法代表应用已有逻辑;pendingUpdate 是窗口或退出协调器持有的 IUpdateHandoff? 字段:

if (pendingUpdate is null)
{
    var candidate = await updater.CheckAsync(ct);
    if (candidate is null || !await ConfirmUpdateAsync(candidate))
        return;

    pendingUpdate = await updater.DownloadAndPrepareAsync(candidate, progress, ct);
}

if (!await PrepareToExitAsync(ct))
    return;

// 确认通过后,才将所有权交给本次提交作用域。
await using (var handoff = pendingUpdate)
{
    pendingUpdate = null;
    var result = await handoff.CommitAsync(restart: true, cancellationToken: ct);
    if (result.Status == UpdateResultStatus.Started)
        ExitApplication();
    else
        ShowUpdateError(result);
}

下载和准备期间不替换应用文件。准备结果可以持续持有,用户拒绝关闭或保存失败时保留,之后直接重试退出与提交,不重新检查或下载。明确放弃或退出而不安装时异步释放;需要保留的准备结果不能在关闭确认前进入一次按钮事件的 await using 作用域。示例在确认通过后转交所有权,提交结束释放本次句柄;失败后重新准备,不继续持有已经使用过的句柄。

提交时传 restart: true 表示安装成功后重启,false 表示安装后保持退出。同一份准备结果只提交一次;入口和参数来自本地配置,在准备时验证并固定,提交不修改计划。未配置有效入口却请求重启会在接管前明确拒绝,准备结果仍可选择不重启。

保存和所有可否决的关闭确认必须在提交前完成。Started 表示 helper 已接手,等待当前进程退出,不能当作安装成功。提交成功后应用正常退出,helper 独立完成安装;提交后的取消或释放不会撤销已经交接的任务。

没有必调通用 InitializeAsync、公开历史结果查询或离线导入主入口。下载文件是 SDK 内部工作材料,应用无需保存或传递其路径。可以用标准 IProgress<UpdateProgress> 报告下载、校验和准备进度;取消使用 CancellationToken。

生命周期与框架接入

直接创建和标准 DI 使用相同的管理器、准备结果及所有权。AddVernext 只组合组件;它不会启动 Host、注册退出事件或取得应用运行锁。Vernext 不调用窗口关闭、StopApplication 或 Environment.Exit。

应用或框架在现有退出流程的一个位置接入更新:完成所有可否决确认与安装前必须成功的业务收尾,再在进程仍存活、能够等待异步完成的位置提交。收到 Started 后正常结束进程,不再否决退出。UI 消息循环和 Host 可以在此前或此后结束,SDK 不要求固定顺序;框架自行处理原生同步入口,不能在已停止的 UI 上下文中阻塞等待提交。仅收到停止通知不等于业务收尾已完成。

所属 负责内容
Vernext 检查、下载、验证、准备及安装事务;管理自己创建的技术资源
具体框架适配 用框架已有能力协调操作收尾、准备结果、退出时安装和所需运行锁
应用业务 更新界面、关闭判断、业务保存和安装意图

框架适配维护在对应框架项目中;例如 AddVernextUpdater 属于 Zeek 的适配入口,不是 Vernext API。本仓库不实现 Zeek 注册或退出动作,框架也无需另建一套更新流程。版本、RID、目标和重启入口优先沿用 SDK 默认规则;版本展示读取管理器的 CurrentVersion。ApplicationId 是更新来源中的身份,不能无条件填入本地框架的应用名称;只有确认二者使用同一标识约定时,框架才可补充默认值。

准备结果由一个明确的持有者管理,确认通过后可转交最终提交方;容器、管理器和执行器先释放不会使它失效。接口不要求引用计数或双重持有。取消“退出时安装”的安排只改变适配层的意图,仍可保留 handoff;调用 CancelAsync 或释放未提交 handoff 则表示放弃准备材料。Vernext 不保存跨重启的安装安排。

自定义与鉴权

契约 默认实现 职责
IUpdateChecker JsonUpdateChecker 获取、认证发布信息,选择最终候选
IUpdateFileProvider UpdateFileDownloader 获取完整归档,成功后转交文件释放责任
IUpdateFileVerifier Sha256Verifier 校验归档 SHA-256 与可选长度
IUpdateExecutor DirectoryUpdateExecutor 准备独立的 IUpdateHandoff,维护安装与恢复语义

检查器签名为 CheckAsync(string currentVersion, CancellationToken);无更新返回空,失败抛出异常。自定义检查器已经完成选择,管理器不会再次比较版本或改变候选。静态 JSON 的普通格式如下,size、notes 可省略:

{"version":"2.0.0","url":"MyApp.zip","sha256":"<SHA-256>","size":123456,"notes":"更新说明"}

相对下载地址以清单地址解析。配置公钥时要求签名清单;无公钥时读取普通 JSON,可信来源由 HTTPS 或已有认证保证,SHA-256 本身不证明发布者身份。其他 API 格式通过 IUpdateChecker 接入,不要求转成 Server 协议。只更换版本顺序时可给 JsonUpdateChecker 传入标准 IComparer<string>。

默认来源的配置边界如下,直接创建与 DI 一致:

来源 必要配置与校验
Vernext Server ServiceAddress、ApplicationId、PublicKeyPem;渠道默认 stable,RID 默认当前进程,校验完整签名身份
普通 JSON/同格式 API 显式地址;不填写签名身份配置
签名清单地址 显式地址与 PublicKeyPem;另填 ApplicationId 时,同时校验渠道和 RID,使用对应默认值或覆盖值
自定义检查协议 注册 IUpdateChecker,由它负责来源认证及最终候选选择;后续仍遵守所选验证器与执行器的输入契约

手动替换一步,其余步骤仍使用默认实现:

using var downloader = new UpdateFileDownloader(downloadHttpClient);
using var updater = UpdateManager.Create(
    companyChecker, fileProvider: downloader);

检查或下载只需要鉴权、代理或 Token 刷新时,直接给对应默认实现传入已配置的 HttpClient,不重写文件传输:

using var checker = new JsonUpdateChecker(updateUrl, checkHttpClient);
using var downloader = new UpdateFileDownloader(downloadHttpClient);
using var updater = UpdateManager.Create(checker, fileProvider: downloader);

DI 使用相同实现:services.AddSingleton<IUpdateFileProvider>(_ => new UpdateFileDownloader(downloadHttpClient));。若用 IHttpClientFactory,在注册工厂中取得命名客户端即可;SDK 不额外增加 HTTP 接口。默认检查和下载各自拥有独立客户端,检查凭据不会自动发送到包地址。

完整手动构造也可使用 new UpdateManager(checker, fileProvider, fileVerifier, executor, options)。组件接口可独立使用,替换步骤无需复制默认编排。UpdateOptions 只保存参数,不保存行为委托、客户端或组件。

配置与文件规则

配置 何时需要
Channel Server 的非 stable 渠道
PreservePaths 保留包内同名本地文件,如 ["appsettings.local.json", "data/"]
RestartArguments 重启需要业务参数;默认不转发当前命令行
TargetDirectory / RestartEntryPoint 特殊布局或无法推导的入口
CurrentVersion / RuntimeIdentifier 特殊版本或 Server 目标平台覆盖
DownloadDirectory / MaxDownloadBytes 默认下载器的临时目录或 4 GiB 上限覆盖

Create / AddVernext 将配置交给默认组件;显式传入的自定义组件使用自己的配置。直接创建或传入注册入口的配置会复制集合;单独注册到 DI 的配置在组件解析时取得,由需要持有集合的组件复制。配置不在更新期间动态变化。

普通目录按包内文件新增或替换,包外内容保留;PreservePaths 保护同名本地内容,缺失时允许从包中初始化。废弃文件在归档根目录的可选 vernext-update.json 声明,应用无需写删除逻辑:

{"deletePaths":["Legacy.Component.dll","plugins/RemovedPlugin/"]}

macOS 完整替换 .app,业务数据放在 bundle 外,不使用包内保留规则。

多实例保护与资源归属

默认 helper 等待发起进程退出并对安装目标互斥。需要保护其他运行实例及更新期间新启动的实例时,每个实例在业务开始前取得运行租约,并保持到正常退出:

var executor = new DirectoryUpdateExecutor(options);
using var running = executor.AcquireApplicationLock();
using var updater = UpdateManager.Create(updateUrl, options, executor: executor);
// 进入应用主循环,退出后才释放 running。

使用默认目录执行器时,DI 提供同一个具体实例与 IUpdateExecutor;在应用启动入口解析并取得租约:

var executor = serviceProvider.GetRequiredService<DirectoryUpdateExecutor>();
using var running = executor.AcquireApplicationLock();
// 启动应用主循环,业务类仍只注入 IUpdateManager。

这是按需的多实例协调,不是更新初始化。取得租约不运行 helper、不恢复或清理文件;目录仍有未完成替换时拒绝取得租约。普通应用无需增加启动步骤,需要多实例保证时才在业务开始前取得租约,并由入口或框架适配持有到业务运行及必要清理结束;DI 注册不会自动取得或持有它。替换 IUpdateExecutor 后按所选实现的生命周期契约接入,不额外取得无关的目录运行锁。

helper 在安装失败时尝试回滚;准备新更新前,执行器内部使用保留并验签的匹配 helper 处理遗留事务,不能完成恢复时拒绝新操作。committed / rolledBack 残留仅在共享安装锁与独占操作锁下核实归属、清理材料,不等待仍在运行的旧父进程,也不修改安装目录,因此可与当前应用的运行租约并存。未完成文件替换仍需独占安装锁,不能误当终态删除;应用无需释放运行租约来绕过冲突。

准备时直接检查各事务并回收已结束操作的归档与 helper,不查询或排序历史。结果文件和日志由组件维护,用于避免重复安装、确认清理条件和排障;没有公开历史查询接口,应用只处理本次交接结果。

断电或 helper 被终止后不会自动唤醒。主程序损坏无法启动时,从外部调用保留 helper 恢复;不依赖主程序下次初始化,不默认引入启动器或常驻服务。回滚成功不等于自动重启旧版,当前 helper 仅在安装成功且本次选择重启时启动应用。

资源规则:Create 创建的默认组件由管理器释放;构造函数或 Create 传入的外部组件由原 owner 释放;DI 创建的服务由容器释放。借用 HttpClient 不随组件释放。下载器失败或取消时自行清理,成功返回的 UpdateFile 由管理器在准备完成后释放;执行器返回的 handoff 必须独立拥有后续资源。

维护与验证

dotnet build sdk/Vernext.slnx -c Release -warnaserror
dotnet run --project sdk/probes/Vernext.DirectoryExecutorProbe -c Release -- --check-only
./eng/Verify-Helper.ps1
./eng/Verify-Packages.ps1 -PackagePath '<最终候选 nupkg>'

--check-only 覆盖来源、版本、手动与 DI 接入、资源和鉴权,不要求 helper。源码验证可直接运行 eng/Verify-Packages.ps1,使用隔离测试密钥及本机 helper;-SourceFixtures 只验证需要测试密钥的 helper 场景。

手动工作流从同一源码构建四 RID helper、签名并组包一次,再完成同包 JIT/AOT 验收,默认不发布。helper 与签名自动排除在应用 single-file 外;托管 SDK 由 AOT 应用编入自身。具体发行证据、helper 构建、NuGet 发布和 Avalonia 示例位于源码仓库的 docs/ 与 samples/,访问这些维护资料需要仓库权限。

源码保留 Rust 和 C# Native AOT 两种实现。自行打包者在 eng/Build-DirectoryHelper.ps1 末尾固定调用 Build-RustHelper(默认)或 Build-CSharpHelper,使用自己的 helper 签名密钥重新构建 SDK 与 NuGet。包内只保留选定实现;切换不增加应用代码,也不能用另一种 helper 替换未完成事务的恢复副本。

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0 106 10/8/2026
2.0.0 87 10/7/2026
1.0.1 85 10/7/2026
1.0.0 90 10/6/2026

新增 IUpdateManager.CurrentVersion,使界面展示与检查使用同一版本;自行实现 IUpdateManager 的消费者须补齐该只读属性。修复默认检查器在 DI 解析时读取最终 UpdateOptions,支持在 AddVernext 前后独立注册实例或单例工厂。明确更新来源身份与本地应用标识的区别,保留直接创建与 DI 一致的来源校验。默认交付 Rust helper;Developer ID、公证和 Gatekeeper 分发尚未验收。