本文記錄的方法不是 WinUI 3 專屬的,也可以用於其他框架。
某不能說名字的軟體在開發時需要頻繁更新版本,為了兼顧開發體驗和使用者體驗,使用自動化流程達到這一目的。
程式碼託管在 GitHub 上,使用 GitHub Actions 是最合適的選擇。在寫自動構建的流程之前,考慮了這麼幾個問題:
-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
使用 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 檔案移動到一個待刪除資料夾,這個時候就可以把新檔案移動到原來的位置。但是還有個問題,非可執行檔案被佔用後不能移動,所以還是有可能會更新失敗,這個方法能用但是不靠譜。