-
Notifications
You must be signed in to change notification settings - Fork 13
282 lines (259 loc) · 18.7 KB
/
Copy pathvalidate-wix-msi.yml
File metadata and controls
282 lines (259 loc) · 18.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# 260612Cl WiX 移行 Phase A 検証ワークフロー (計画書 .project-guidance/ReciPro_WiX移行計画.md §6 Phase A / §7)
# - WiX v7 で x64/arm64 両 MSI をビルドし、旧 vdproj MSI (最新 release) と内容 diff
# - クリーンランナー上で「vdproj 旧版 install → WiX 新版で Major Upgrade → uninstall」の連鎖テスト
# - release には一切触れない (artifact のみ・contents: read)。Phase B で release.yml に置換するまでの並行検証用
# - x64⇔arm64 cross-grade テストは windows-11-arm が必要なため Phase C の smoke で実施 (ここでは不可)
name: Validate WiX MSI (Phase A)
on:
workflow_dispatch:
permissions:
contents: read
jobs:
validate:
runs-on: windows-2025-vs2026 # release.yml とイメージを揃える (native vcxproj ビルドに devenv が必要)
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v5
with:
submodules: recursive # 260624Cl shared libs are git submodules
- name: Parse version
id: version
shell: pwsh
run: |
$content = Get-Content "ReciPro\Version.cs" -Raw
if ($content -notmatch 'ver(\d+\.\d+)\(') { throw "Could not parse version from Version.cs History." }
$ver = $Matches[1]
# アップグレード連鎖テスト用に build 番号を +1 した版数も用意 (旧 release MSI より必ず新しくする)
$parts = $ver.Split('.')
$bumped = "$($parts[0]).$([int]$parts[1] + 1)"
# FileVersion 注入は必須 (260612Cl 実測): 注入しないと csproj 静的値 (過去日付) が使われ、
# 旧 release (より新しい FileVersion) からのアップグレードで MSI のファイルバージョン規則により
# 自前アセンブリ 7 本の FileCopy が costing 段階で省略され、REP 削除後に欠落する
$now = [System.DateTime]::UtcNow
$assemblyVer = "$($now.Year).$($now.Month).$($now.Day).$($now.ToString('HHmm'))"
Write-Host "version=$ver bumped=$bumped assembly=$assemblyVer"
"version=$ver" >> $env:GITHUB_OUTPUT
"product_version=0.$ver" >> $env:GITHUB_OUTPUT
"test_product_version=0.$bumped" >> $env:GITHUB_OUTPUT
"assembly_version=$assemblyVer" >> $env:GITHUB_OUTPUT
- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
- name: Restore managed dependencies
shell: pwsh
run: dotnet restore ReciPro\ReciPro.csproj
- name: Find devenv.com
id: devenv
shell: pwsh
run: |
$devenvExe = (& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -property productPath).Trim()
if (-not $devenvExe) { throw "Visual Studio was not found." }
$cli = [System.IO.Path]::ChangeExtension($devenvExe, ".com")
if (-not (Test-Path $cli)) { throw "devenv.com was not found." }
"cli=$cli" >> $env:GITHUB_OUTPUT
- name: Build native runtime binaries (x64)
shell: pwsh
run: |
$log = Join-Path $env:RUNNER_TEMP "devenv-native-x64.log"
& "${{ steps.devenv.outputs.cli }}" "ReciPro.sln" /Build "Release|x64" /Project "Crystallography.Native" /Out $log
if (Test-Path $log) { Get-Content $log }
if ($LASTEXITCODE -ne 0) { throw "Crystallography.Native x64 build failed (exit $LASTEXITCODE)." }
- name: Build native runtime binary (ARM64 cross-build)
shell: pwsh
run: |
$log = Join-Path $env:RUNNER_TEMP "devenv-native-arm64.log"
& "${{ steps.devenv.outputs.cli }}" "ReciPro.sln" /Build "Release|ARM64" /Project "Crystallography.Native" /Out $log
if (Test-Path $log) { Get-Content $log }
if ($LASTEXITCODE -ne 0) { throw "Crystallography.Native ARM64 build failed (exit $LASTEXITCODE)." }
- name: Stage x64 MSI payload (framework-dependent publish)
shell: pwsh
run: |
$s = "artifacts\staging-msi-x64"
dotnet publish ReciPro\ReciPro.csproj -c Release -p:Platform=x64 --self-contained false -o $s --no-restore `
-p:AssemblyVersion=${{ steps.version.outputs.assembly_version }} -p:FileVersion=${{ steps.version.outputs.assembly_version }}
if ($LASTEXITCODE -ne 0) { throw "x64 publish failed." }
# Crystallography.Native 3 flavor は publish グラフ外 (vcxproj が bin\Release へ後置き) → 明示コピー
$native = Get-ChildItem "ReciPro\bin\Release" -Filter "Crystallography.Native*.dll" -File
if ($native.Count -ne 3) { throw "native DLL count != 3: $($native.Count)" }
$native | Copy-Item -Destination $s
Copy-Item ReciProSetup.Wix\LICENSE.rtf, ReciProSetup.Wix\REQUIREMENT.rtf $s
# 旧 vdproj MSI と内容を揃える: 参照プロジェクトの分離 pdb は同梱しない (portable ZIP と同方針)
Get-ChildItem $s -Filter *.pdb | Remove-Item
# 出荷前検査 (計画書 §3 D3)
if (Test-Path "$s\README-PORTABLE.txt") { throw "README-PORTABLE.txt leaked (portable marker must not be in MSI)" }
if (Test-Path "$s\coreclr.dll") { throw "self-contained runtime leaked into framework-dependent staging" }
if (-not (Test-Path "$s\ReciPro.exe")) { throw "ReciPro.exe missing from staging" }
# 260621Cl 追加 (3-D packaging 検査): 全カルチャのサテライト DLL が staging に揃っているか検査する。
# MSI は WiX の <Files Include="**"> で staging を丸ごと収集するため、ここで欠ければ MSI からも無言で欠落する。
# framework-dependent publish は SatelliteResourceLanguages 無指定 = アプリの全 resx 言語を生成する想定。
# 新言語を増やしたらこの一覧も更新すること (Crystallography.SupportedCultures と同期)。
$expectedCultures = @('ja','de','fr','es','pt','it','ru','zh-Hans','zh-Hant','ko')
$missingSat = $expectedCultures | Where-Object { -not (Get-ChildItem "$s\$_" -Filter *.resources.dll -ErrorAction SilentlyContinue) }
if ($missingSat) { throw "satellite assemblies missing for culture(s): $($missingSat -join ', ')" }
Write-Host "x64 staging OK: $((Get-ChildItem $s -Recurse -File).Count) files"
- name: Stage arm64 MSI payload (framework-dependent publish)
shell: pwsh
run: |
$s = "artifacts\staging-msi-arm64"
dotnet publish ReciPro\ReciPro.csproj -c Release -r win-arm64 -p:Platform=ARM64 --self-contained false -o $s `
-p:AssemblyVersion=${{ steps.version.outputs.assembly_version }} -p:FileVersion=${{ steps.version.outputs.assembly_version }}
if ($LASTEXITCODE -ne 0) { throw "arm64 publish failed." }
Copy-Item ReciProSetup.Wix\LICENSE.rtf, ReciProSetup.Wix\REQUIREMENT.rtf $s
Get-ChildItem $s -Filter *.pdb | Remove-Item
if (Test-Path "$s\README-PORTABLE.txt") { throw "README-PORTABLE.txt leaked" }
if (Test-Path "$s\coreclr.dll") { throw "self-contained runtime leaked" }
# arm64 は generic 1 本のみ (csproj 注入ターゲットが publish に追加する)。avx flavor 混入は BadImageFormat の元
$nat = Get-ChildItem $s -Filter "Crystallography.Native*.dll"
if ($nat.Count -ne 1) { throw "arm64 native DLL count != 1: $($nat.Count)" }
if (-not (Test-Path "$s\libxrl-11.dll") -or -not (Test-Path "$s\glfw3.dll")) { throw "arm64 native deps (libxrl/glfw) missing" }
# PE machine 検査 (0xAA64)
function Get-PEMachine($path) {
$fs = [System.IO.File]::OpenRead($path); $br = New-Object System.IO.BinaryReader($fs)
$fs.Seek(0x3C,0) | Out-Null; $peOff = $br.ReadInt32(); $fs.Seek($peOff+4,0) | Out-Null
$m = $br.ReadUInt16(); $fs.Close(); $m
}
foreach ($f in 'ReciPro.exe','Crystallography.Native.dll','libxrl-11.dll','glfw3.dll') {
$m = Get-PEMachine (Join-Path $PWD "$s\$f")
if ($m -ne 0xAA64) { throw ("{0}: PE machine 0x{1:X4} != 0xAA64" -f $f, $m) }
}
# 260621Cl 追加 (3-D packaging 検査): 全カルチャのサテライト DLL が staging に揃っているか検査 (x64 と同様。
# サテライトはプラットフォーム非依存なので arm64 でも全 resx 言語が生成される想定)。
$expectedCultures = @('ja','de','fr','es','pt','it','ru','zh-Hans','zh-Hant','ko')
$missingSat = $expectedCultures | Where-Object { -not (Get-ChildItem "$s\$_" -Filter *.resources.dll -ErrorAction SilentlyContinue) }
if ($missingSat) { throw "satellite assemblies missing for culture(s): $($missingSat -join ', ')" }
Write-Host "arm64 staging OK: $((Get-ChildItem $s -Recurse -File).Count) files"
- name: Build WiX x64 MSI (bumped version for upgrade test)
shell: pwsh
run: |
dotnet build ReciProSetup.Wix\ReciProSetup.wixproj -c Release -p:InstallerPlatform=x64 `
-p:ProductVersion=${{ steps.version.outputs.test_product_version }} `
-p:StagingDir="$PWD\artifacts\staging-msi-x64"
if ($LASTEXITCODE -ne 0) { throw "WiX x64 build failed." }
# 260613Cl アセット改名 (旧: ReciProSetup.msi)
if (-not (Test-Path "ReciProSetup.Wix\bin\Release\ReciPro-setup.msi")) { throw "ReciPro-setup.msi not produced." }
- name: Build WiX arm64 MSI (cross-build)
shell: pwsh
run: |
dotnet build ReciProSetup.Wix\ReciProSetup.wixproj -c Release -p:InstallerPlatform=arm64 `
-p:ProductVersion=${{ steps.version.outputs.product_version }} `
-p:StagingDir="$PWD\artifacts\staging-msi-arm64"
if ($LASTEXITCODE -ne 0) { throw "WiX arm64 build failed." }
# 260613Cl アセット改名 (旧: ReciProSetup-arm64.msi)
if (-not (Test-Path "ReciProSetup.Wix\bin\Release\ReciPro-setup_arm64.msi")) { throw "ReciPro-setup_arm64.msi not produced." }
- name: Verify MSI properties (Template / UpgradeCode / version / per-user)
shell: pwsh
run: |
$wi = New-Object -ComObject WindowsInstaller.Installer
function Get-MsiProp($db, $name) {
$view = $db.GetType().InvokeMember('OpenView','InvokeMethod',$null,$db,@("SELECT Value FROM Property WHERE Property='$name'"))
# Execute の戻り値が関数の出力に混入して戻り値が配列化するため Out-Null 必須 (260612Cl 実測)
$view.GetType().InvokeMember('Execute','InvokeMethod',$null,$view,$null) | Out-Null
$rec = $view.GetType().InvokeMember('Fetch','InvokeMethod',$null,$view,$null)
if ($rec) { $rec.GetType().InvokeMember('StringData','GetProperty',$null,$rec,@(1)) } else { $null }
}
function Test-Msi($path, $expectTemplate, $expectVersion) {
$db = $wi.GetType().InvokeMember('OpenDatabase','InvokeMethod',$null,$wi,@($path,0))
$si = $db.GetType().InvokeMember('SummaryInformation','GetProperty',$null,$db,@(0))
$template = $si.GetType().InvokeMember('Property','GetProperty',$null,$si,@(7))
$schema = $si.GetType().InvokeMember('Property','GetProperty',$null,$si,@(14))
$uc = Get-MsiProp $db 'UpgradeCode'; $pv = Get-MsiProp $db 'ProductVersion'; $au = Get-MsiProp $db 'ALLUSERS'
Write-Host "$path : Template=$template Schema=$schema UpgradeCode=$uc Version=$pv ALLUSERS=$au"
if ($template -notlike "$expectTemplate*") { throw "Template mismatch: $template (expected $expectTemplate)" }
if ($uc -ne '{A81303DC-E1CA-44F3-AE3A-6C7CAC08E8D8}') { throw "UpgradeCode mismatch: $uc" }
if ($pv -ne $expectVersion) { throw "ProductVersion mismatch: $pv (expected $expectVersion)" }
if ($au) { throw "ALLUSERS must be unset for per-user package (got $au)" }
if ([int]$schema -lt 500) { throw "Schema $schema < 500" }
}
# 260613Cl アセット改名 (旧: ReciProSetup.msi / ReciProSetup-arm64.msi)
Test-Msi "ReciProSetup.Wix\bin\Release\ReciPro-setup.msi" 'x64' '${{ steps.version.outputs.test_product_version }}'
Test-Msi "ReciProSetup.Wix\bin\Release\ReciPro-setup_arm64.msi" 'Arm64' '${{ steps.version.outputs.product_version }}'
- name: Download latest released vdproj MSI
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
New-Item -ItemType Directory -Force "$env:RUNNER_TEMP\old-msi" | Out-Null
# 260613Cl アセット改名後も pattern は ReciProSetup.msi のまま: 改名前 release では唯一の MSI、
# 改名後 release では旧名互換コピー (同一バイト) に一致し、どちらでもアップグレード元として有効
gh release download --pattern ReciProSetup.msi -D "$env:RUNNER_TEMP\old-msi"
if (-not (Test-Path "$env:RUNNER_TEMP\old-msi\ReciProSetup.msi")) { throw "Failed to download released MSI." }
- name: Diff file lists (old vdproj MSI vs WiX staging)
shell: pwsh
run: |
# 旧 MSI を管理展開 (msiexec /a) してファイル一覧を取り、staging と比較する (計画書 §7-12 のゲート)
$extract = "$env:RUNNER_TEMP\old-extract"
$p = Start-Process msiexec -ArgumentList '/a', "$env:RUNNER_TEMP\old-msi\ReciProSetup.msi", '/qn', "TARGETDIR=$extract" -Wait -PassThru
if ($p.ExitCode -ne 0) { throw "Admin extract failed: $($p.ExitCode)" }
$old = Get-ChildItem $extract -Recurse -File | Where-Object { $_.Name -ne 'ReciProSetup.msi' } |
ForEach-Object { $_.FullName.Substring((Resolve-Path $extract).Path.Length + 1) } |
ForEach-Object { $_ -replace '^.*?\\Crystallography Software\\ReciPro\\', '' } | Sort-Object
$new = Get-ChildItem artifacts\staging-msi-x64 -Recurse -File |
ForEach-Object { $_.FullName.Substring((Resolve-Path artifacts\staging-msi-x64).Path.Length + 1) } | Sort-Object
$diff = Compare-Object $old $new
$report = Join-Path $env:RUNNER_TEMP "msi-content-diff.txt"
@("old(vdproj release) count: $($old.Count) / new(WiX staging) count: $($new.Count)", "") +
($diff | ForEach-Object { "{0} {1}" -f $_.SideIndicator, $_.InputObject }) | Set-Content $report
Get-Content $report
Copy-Item $report artifacts\
if ($diff) { throw "MSI content differs from released vdproj MSI. Review msi-content-diff.txt (every diff must be explained and accepted)." }
- name: Upgrade chain test (vdproj old -> WiX new -> uninstall)
shell: pwsh
run: |
$wi = New-Object -ComObject WindowsInstaller.Installer
function Get-ReciProProducts {
$found = @()
foreach ($pc in $wi.GetType().InvokeMember('Products','GetProperty',$null,$wi,$null)) {
try { $name = $wi.GetType().InvokeMember('ProductInfo','GetProperty',$null,$wi,@($pc,'ProductName')) } catch { continue }
if ($name -eq 'ReciPro') { $found += [pscustomobject]@{ Code=$pc; Version=$wi.GetType().InvokeMember('ProductInfo','GetProperty',$null,$wi,@($pc,'VersionString')) } }
}
$found
}
$appDir = "$env:LOCALAPPDATA\Crystallography Software\ReciPro"
$desktopLnk = Join-Path ([Environment]::GetFolderPath('DesktopDirectory')) 'ReciPro.lnk'
$startLnk = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Crystallography Software\ReciPro.lnk"
# 1) 旧 vdproj MSI を per-user silent install
$p = Start-Process msiexec -ArgumentList '/i', "$env:RUNNER_TEMP\old-msi\ReciProSetup.msi", '/qn', '/l*v', "$env:RUNNER_TEMP\install-old.log" -Wait -PassThru
if ($p.ExitCode -ne 0) { throw "old MSI install failed: $($p.ExitCode)" }
$prods = Get-ReciProProducts
if ($prods.Count -ne 1) { throw "expected 1 product after old install, got $($prods.Count)" }
Write-Host "old installed: v$($prods[0].Version) $($prods[0].Code)"
if (-not (Test-Path "$appDir\ReciPro.exe")) { throw "old install: exe missing" }
# 2) WiX 新版 (bumped version) で Major Upgrade (260613Cl アセット改名: ReciPro-setup.msi)
$p = Start-Process msiexec -ArgumentList '/i', "$PWD\ReciProSetup.Wix\bin\Release\ReciPro-setup.msi", '/qn', '/l*v', "$env:RUNNER_TEMP\install-new.log" -Wait -PassThru
if ($p.ExitCode -ne 0) { throw "WiX MSI upgrade install failed: $($p.ExitCode)" }
$prods = Get-ReciProProducts
if ($prods.Count -ne 1) { throw "expected exactly 1 product after upgrade (ARP must be replaced), got $($prods.Count): $($prods | ConvertTo-Json -Compress)" }
if ($prods[0].Version -ne '${{ steps.version.outputs.test_product_version }}') { throw "upgraded version mismatch: $($prods[0].Version)" }
Write-Host "upgraded: v$($prods[0].Version) $($prods[0].Code)"
foreach ($f in 'ReciPro.exe','Crystallography.Native.dll','Crystallography.Native.avx2.dll','Crystallography.Native.avx512.dll','initial.xml','AMCSD.cdb3','LICENSE.rtf','REQUIREMENT.rtf') {
if (-not (Test-Path "$appDir\$f")) { throw "upgrade: $f missing in install dir" }
}
if (-not (Test-Path $desktopLnk)) { throw "upgrade: desktop shortcut missing" }
if (-not (Test-Path $startLnk)) { throw "upgrade: start menu shortcut missing" }
# 3) アンインストールで残骸ゼロ
$code = $prods[0].Code
$p = Start-Process msiexec -ArgumentList '/x', $code, '/qn', '/l*v', "$env:RUNNER_TEMP\uninstall.log" -Wait -PassThru
if ($p.ExitCode -ne 0) { throw "uninstall failed: $($p.ExitCode)" }
if ((Get-ReciProProducts).Count -ne 0) { throw "product still registered after uninstall" }
if (Test-Path "$appDir\ReciPro.exe") { throw "uninstall: exe remains" }
if (Test-Path $desktopLnk) { throw "uninstall: desktop shortcut remains" }
if (Test-Path $startLnk) { throw "uninstall: start menu shortcut remains" }
Write-Host "Upgrade chain test PASSED."
- name: Upload MSI logs on failure
if: failure()
uses: actions/upload-artifact@v6
with:
name: wix-validate-logs
path: ${{ runner.temp }}/*.log
if-no-files-found: ignore
- name: Upload artifacts (MSIs + diff report)
uses: actions/upload-artifact@v6
with:
name: wix-msi-phase-a
# 260613Cl アセット改名 (旧: ReciProSetup.msi / ReciProSetup-arm64.msi)
path: |
ReciProSetup.Wix/bin/Release/ReciPro-setup.msi
ReciProSetup.Wix/bin/Release/ReciPro-setup_arm64.msi
artifacts/msi-content-diff.txt