• 自动构建与自动更新


    本文记录的方法不是 WinUI 3 专属的,也可以用于其他框架。

    某不能说名字的软件在开发时需要频繁更新版本,为了兼顾开发体验和用户体验,使用自动化流程达到这一目的。

    自动构建#

    代码托管在 GitHub 上,使用 GitHub Actions 是最合适的选择。在写自动构建的流程之前,考虑了这么几个问题:

    1. GitHub 在部分地区速度太慢或不可用,不适合作为安装包的下载来源,选择了白嫖阿里云 OSS 和 Cloudflare 代理。
    2. 为了区分自动构建的版本和发行版,需要在版本号后加上 -dev.*,选择了在文件的 ProductVersion 属性中记录当前版本号。

    准备环境#

    运行 Actions 的系统为 windows-latest,默认 Shell 为 PowerShell

    - name: Checkout
      uses: actions/checkout@v3
      with:
        # 迁出所有代码,后面有用
        fetch-depth: 0
    
    - name: Install .NET Core
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: 7.0.x
    
    - name: Restore the Packages
      run: dotnet restore
    
    # 配置 阿里云 OssUtil
    - name: Setup Aliyun OssUtil
      run: |
        # 见下方 PowerShell 代码块
    
    # 下载适合 Windows 的 OssUtil 安装包到临时文件夹
    Invoke-WebRequest https://gosspublic.alicdn.com/ossutil/1.7.14/ossutil64.zip -OutFile ${{runner.temp}}/ossutil.zip
    # 解压
    Expand-Archive -Path ${{runner.temp}}/ossutil.zip -DestinationPath ${{runner.temp}}
    # 把 exe 文件移动到 system32 文件夹下,为了在后续步骤中可以直接使用 ossutil 命令
    Move-Item -Path ${{runner.temp}}/ossutil64/ossutil64.exe -Destination C:/Windows/System32/ossutil.exe -Force
    # 配置账号
    ossutil config -e ${{ secrets.OSS_ENDPOINT }} -i ${{ secrets.ACCESS_KEY_ID }} -k ${{ secrets.ACCESS_KEY_SECRET }}
    

    版本号#

    每次构建的版本必须比上一次构建的版本要大,可以通过当前时间生成版本 v1.2.3-dev.221015.2201,或者通过环境变量 ${{github.run_number}} 获取当前 Action 是第几次执行,就是 Actions 页面每次执行时 # 后面的那个数字。但是这两个方法生成的版本号是一直递增的,我比较倾向于使用 上个版本发布后的提交数 作为 -dev. 后的数字。

    下面是生成目标版本号的函数,可以不用看

    function Get-TargetVersion {
        # 获取上一个 tag,迁出代码时设置 fetch-depth: 0
        $lastTag = git describe --abbrev=0 --tags
        if ([String]::IsNullOrWhiteSpace($lastTag)) {
            # 没有 tag
            $lastTag = 'v0.1.0'
            # 提交次数
            $commitCount = git rev-list HEAD --count
        }else {
            # 有 tag
            $commitCount = git rev-list "$lastTag..HEAD" --count
        }
        # 把 tag 开头的 v 去掉
        if ($lastTag.StartsWith('v') -or $lastTag.StartsWith('V')) {
            $lastTag = $lastTag.SubString(1)
        }
        # 引入一个新库 NuGet.Versioning
        $null = [System.Reflection.Assembly]::LoadFrom('NuGet.Versioning.dll')
        # TryParse 方法中使用 out 修饰的参数必须先定义
        [ref]$lastVer = [NuGet.Versioning.SemanticVersion]::Parse('0.1.0')
        # 解析上一个版本号,有可能是 v1.2.3、v1.2.3-preview.1 或更复杂的内容
        if ([NuGet.Versioning.SemanticVersion]::TryParse($lastTag, $lastVer)) {
            if ($lastVer.Value.IsPrerelease) {
                # 上个版本是预发行版,直接在后面加上 -dev.*
                $targetVer = "$lastVer-dev.$commitCount"
            }
            else {
                # 上个版本是正式版,版本号+1后再接 -dev.*
                $targetVer = "$($lastVer.Value.Major).$($lastVer.Value.Minor).$($lastVer.Value.Patch+1)-dev.$commitCount"
            }
        }
        else {
            # 理论上来说不会运行到这里
            $targetVer = "$lastVer-dev.$commitCount"
        }
        Write-Output $targetVer
    }
    

    在这里使用了一个库 NuGet.Versioning,这是解析 语义化版本 v2.0 一个库,和 System.Version 仅支持 4 个数字不同,它支持解析和比较如 1.2.3-preview.4.5.6+abcdef 这样的格式。

    发布#

    $v = Get-TargetVersion
    dotnet publish -p:Configuration=$env:Configuration -p:Platform=$env:Platform -p:Version=$v -p:DefineConstants=DEV
    

    压缩并上传至 OSS#

    使用 zip 压缩后文件大小大约是 60 MB,使用 7zip 则是 40 MB,用户的网络环境不同,能压缩就尽量压缩。在上传到 OSS 时自定义元数据 x-oss-meta-version 为目标版本号,软件可以通过检查这个值判断是否应该更新。

    # 安装 7zip 压缩模块
    Install-Module -Name 7Zip4Powershell -Scope CurrentUser -Force
    New-Item -Path ./publish -ItemType Directory -Force
    # 压缩等级最大,压缩时包含根目录
    Compress-7Zip -ArchiveFileName software.7z -Path $env:Publish_Path -OutputPath ./publish -CompressionLevel Ultra -PreserveDirectoryRoot
    # 上传至 OSS
    ossutil cp -rf ./publish/* oss://${{ secrets.OSS_BUCKET_NAME }}/software.7z --meta x-oss-meta-version:$v
    

    自动更新#

    非打包的 WinUI 项目和 Win32 项目一样,更新时会遇到文件被占用的情况,一般是通过额外的更新进程解决这个问题。不这么一个小东西要啥自行车,用 PowerShell 一把梭,不仅能规避占用问题,还能少一个项目。

    我原本是打算在 PowerShell 中下载新版本安装包的,PowerShell 会自动展示下载进度,但是我发现下载速度始终限制在 1 MB/s 左右,而在浏览器或 C# 中下载速度能够达到 6 MB/s。在网上搜了一圈后发现是 实时显示下载数据量拖慢了性能,参考 Issue Progress bar can significantly impact cmdlet performance,这个问题已在 PowerShell Core 修复,在 Windows 自带的 PowerShell 中仍然存在,唯一的解决办法是禁止显示下载进度。

    40 MB 的安装包不显示下载进度还是不太好,只能多写一点代码在软件中下载了(略),下面是 PowerShell 更新脚本代码。

    # 出现错误时停止执行后续命令,若不加这一句,即使出现错误也无法被 catch
    # 代码省略了最外部的 try-catch 部分
    $ErrorActionPreference = 'Stop'
    # 检查新版本的安装包是否存在
    if(![System.IO.File]::Exists('./temp/software.7z')) {
        # 不存在时重新下载
        $null = New-Item "./temp" -ItemType "Directory" -Force
        Invoke-WebRequest -Uri $url -UseBasicParsing -OutFile "./temp/software.7z"
    }
    # 检查 7zip 解压模块是否存在
    if(![System.IO.File]::Exists('./7Zip4Powershell/7Zip4Powershell.psd1')) {
        # 应该使用下面这一句安装解压模块,但是大陆连接源站 PowerShell Gallery 非常困难
        # Install-Module -Name 7Zip4Powershell -Scope CurrentUser -Force
        # 所以需要自行分发解压模块
        Invoke-WebRequest "url/to/7Zip4Powershell.zip" -UseBasicParsing -OutFile "./7Zip4Powershell.zip"
        Expand-Archive -Path "./7Zip4Powershell.zip" -DestinationPath "./" -Force
        Remove-Item -Path "./7Zip4Powershell.zip" -Force -Recurse
    }
    # 导入模块
    Import-Module -Name "./7Zip4Powershell/7Zip4Powershell.psd1" -Force
    # 解压
    Expand-7Zip -ArchiveFileName "./temp/software.7z" -TargetPath "./temp/"
    # 检查软件是否仍在运行
    try {
        # 没找到进程时会抛错,需要 catch
        $null = Get-Process -Name "software"
        Write-Host "software.exe 正在运行,等待进程退出" -ForegroundColor Yellow
        Wait-Process -Name "software"
        # 停 1s 等待资源释放
        Start-Sleep -Seconds 1
    } catch { }
    # 替换文件
    Copy-Item -Path "./temp/*" -Destination "./" -Force -Recurse
    # 重启软件
    Invoke-Item -Path "./software.exe"
    # 清理安装包
    Remove-Item -Path "./temp" -Force -Recurse
    

    就这样,使用 PowerShell 代替了额外的更新进程,也不用考虑更新进程需要更新的问题。唯一需要注意的是手动导入的 7zip 解压模块中的文件会被占用,即使使用 Remove-Module 移除模块后,载入的程序集仍不会被卸载,所以不能在安装包中包含该模块,使用前下载最合适,下载后可以一直使用,不用考虑更新问题。

    有经验的读者可能会问:哎,系统默认禁用没有签名的 PowerShell 脚本,你总不可能还要让用户手动解除限制吧。这个问题很好解决,不能运行脚本可以运行命令嘛,把脚本内容当作启动参数扔给 PowerShell 进程就好了。

    // 这里不是脚本文件路径,而是脚本内容
    const string script="...";
    Process.Start("PowerShell", script);
    

    最后#

    单进程软件真的没办法自己更新吗?其实不然,可执行文件在运行期间无法被删除和覆盖,但是可以移动啊。把 exe 和 dll 文件移动到一个待删除文件夹,这个时候就可以把新文件移动到原来的位置。但是还有个问题,非可执行文件被占用后不能移动,所以还是有可能会更新失败,这个方法能用但是不靠谱。

  • 相关阅读:
    花了2个晚上,拿到了吴恩达@斯坦福大学的机器学习课程证书
    Xilinx(AMD) 7系列FPGA——主BPI配置模式
    vue2+element-ui创建顶部导航栏及下拉菜单
    设置网站页面标题的注意事项
    内置单片机的433无线模块高速连传典型运用
    【设计模式】六大基本原则
    什么人不适合当项目经理?
    让 GPT-4 来 review 开源社区贡献者的 PR - 每天5分钟玩转 GPT 编程系列(5)
    Constraints --- transition(clock transition、input transition、max transition)
    Unity3D之简单溶解特效制作
  • 原文地址:https://www.cnblogs.com/scighost/p/16795358.html