mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
ci(release): ship a native Windows ARM64 Studio
Build windows/arm64 on a native windows-11-arm runner and carry it through the signing chain as a second leg of every Windows stage: one tree per job, legs serial under the certum-signing group, a failed leg reruns alone. The package check learns app-arm64.7z and refuses any PE image that is not the leg's architecture, a new install smoke runs each installer on its own architecture before publish, and publish counts, names and hash-checks both architectures. The arm64 installer is packed with the 7-Zip BCJ filter: the default ARM64 filter yields an archive the NSIS extractor cannot decode, and the installer then exits 0 with every executable missing. Signing contract tests pin the matrix, the serial legs and the secret-free stages.
This commit is contained in:
1 parent
bbdcef877e
commit
0ae7693526
10 files changed
+621
-126
No files matched your search
@@ -0,0 +1,75 @@
|
||||
# Installs a Windows installer where it is meant to run, silently and per user,
|
||||
# checks what landed, and uninstalls. The package check unpacks with the
|
||||
# runner's modern 7-Zip, which reads archives the NSIS extractor cannot, so only
|
||||
# running the installer shows that it installs every executable. Holds no
|
||||
# credentials: it runs on pull requests against the unsigned installer and in
|
||||
# the release against the signed one.
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Installer,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("amd64", "arm64")]
|
||||
[string]$Architecture
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
. (Join-Path $PSScriptRoot "windows-signing-lib.ps1")
|
||||
|
||||
$temp = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { [IO.Path]::GetTempPath() }
|
||||
$target = Join-Path $temp ("rx-install-" + [guid]::NewGuid().ToString("N"))
|
||||
$env:REASONIX_HOME = Join-Path $temp "reasonix-home"
|
||||
|
||||
$run = Start-Process -FilePath (Resolve-Path -LiteralPath $Installer).Path -ArgumentList "/S", "/D=$target" -Wait -PassThru
|
||||
if ($run.ExitCode -ne 0) { throw "the installer exited $($run.ExitCode)" }
|
||||
|
||||
# The installer can exit before its files are on disk (real-time scanning holds
|
||||
# them), so the tree is read once the application is there and the file count
|
||||
# has stopped moving.
|
||||
function Wait-InstallSettled {
|
||||
$clock = [Diagnostics.Stopwatch]::StartNew()
|
||||
$last = -1
|
||||
$stable = 0
|
||||
while ($clock.Elapsed.TotalSeconds -lt 120) {
|
||||
$count = @(Get-ChildItem -LiteralPath $target -Recurse -File -Force -ErrorAction SilentlyContinue).Count
|
||||
if ((Test-Path -LiteralPath (Join-Path $target "Reasonix Studio.exe")) -and $count -eq $last) { $stable++ } else { $stable = 0 }
|
||||
if ($stable -ge 3) { break }
|
||||
$last = $count
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
Write-Host "install settled $([int]$clock.Elapsed.TotalSeconds)s after the installer exited, $last files"
|
||||
}
|
||||
Wait-InstallSettled
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $target "Reasonix Studio.exe"))) {
|
||||
Write-Host "target holds: $((Get-ChildItem -Force $target -ErrorAction SilentlyContinue | ForEach-Object Name) -join ', ')"
|
||||
throw "the installer reported success and installed nothing"
|
||||
}
|
||||
|
||||
# Every executable the release declares is on disk, and nothing else that is a
|
||||
# PE image besides what the installer writes itself.
|
||||
$found = @(Get-PeFiles -Root $target | Where-Object { $script:InstallerWrittenX86Pe -cnotcontains $_ })
|
||||
$declared = @($script:ReleaseSignedPe + $script:MicrosoftSignedPe)
|
||||
$diff = @(Compare-Object -CaseSensitive ($declared | Sort-Object -CaseSensitive) ($found | Sort-Object -CaseSensitive))
|
||||
if ($diff.Count -gt 0) {
|
||||
foreach ($d in $diff) { Write-Host "::error title=studio-install.pe-set::$($d.InputObject) $($d.SideIndicator)" }
|
||||
throw "the installed PE set differs from the declared one"
|
||||
}
|
||||
foreach ($owned in $script:InstallerWrittenX86Pe) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $target $owned))) { throw "the installer did not write $owned" }
|
||||
}
|
||||
Assert-PeArchitecture -Root $target -Architecture $Architecture
|
||||
|
||||
$env:ELECTRON_RUN_AS_NODE = "1"
|
||||
$out = Join-Path $temp "shell-arch.txt"
|
||||
$node = Start-Process -FilePath (Join-Path $target "Reasonix Studio.exe") -ArgumentList "-p", "process.arch" -RedirectStandardOutput $out -Wait -PassThru
|
||||
Remove-Item Env:ELECTRON_RUN_AS_NODE
|
||||
$want = if ($Architecture -eq "amd64") { "x64" } else { $Architecture }
|
||||
$got = (Get-Content -Raw $out).Trim()
|
||||
if ($node.ExitCode -ne 0 -or $got -ne $want) { throw "the installed shell reports '$got' (exit $($node.ExitCode)), want $want" }
|
||||
& (Join-Path $target "resources/bin/reasonix-studio-host.exe") -instance-id
|
||||
if ($LASTEXITCODE -ne 0) { throw "the installed kernel exited $LASTEXITCODE" }
|
||||
|
||||
$run = Start-Process -FilePath (Join-Path $target "Uninstall Reasonix Studio.exe") -ArgumentList "/S", "_?=$target" -Wait -PassThru
|
||||
if ($run.ExitCode -ne 0) { throw "the uninstaller exited $($run.ExitCode)" }
|
||||
if (Test-Path -LiteralPath (Join-Path $target "Reasonix Studio.exe")) { throw "the uninstaller left the application behind" }
|
||||
Write-Host "Installed, ran and removed the $Architecture installer."
|
||||
@@ -11,6 +11,10 @@ param(
|
||||
[Parameter(ParameterSetName = "Payload")]
|
||||
[string]$DigestPath,
|
||||
|
||||
[Parameter(Mandatory = $true, ParameterSetName = "Payload")]
|
||||
[ValidateSet("amd64", "arm64")]
|
||||
[string]$Architecture,
|
||||
|
||||
[Parameter(Mandatory = $true, ParameterSetName = "File")]
|
||||
[string]$FilePath,
|
||||
|
||||
@@ -33,6 +37,7 @@ if ($PSCmdlet.ParameterSetName -eq "File") {
|
||||
|
||||
$root = (Resolve-Path -LiteralPath $PayloadDirectory).Path
|
||||
Assert-DeclaredPeSet -Root $root
|
||||
Assert-PeArchitecture -Root $root -Architecture $Architecture
|
||||
foreach ($name in $script:ReleaseSignedPe) {
|
||||
Assert-EmbeddedSignature -Path (Join-Path $root $name) -Thumbprint $ExpectedThumbprint -Subject $ExpectedSubject
|
||||
}
|
||||
|
||||
@@ -16,7 +16,11 @@ param(
|
||||
[string]$PortableArchivePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputPath
|
||||
[string]$OutputPath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("amd64", "arm64")]
|
||||
[string]$Architecture
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -71,6 +75,7 @@ if ($payloadDigest -ne $ExpectedPayloadDigest) {
|
||||
throw "Signed payload digest $payloadDigest does not match the $ExpectedPayloadDigest the signing job recorded"
|
||||
}
|
||||
Assert-DeclaredPeSet -Root $payloadRoot
|
||||
Assert-PeArchitecture -Root $payloadRoot -Architecture $Architecture
|
||||
|
||||
# Hashed before either is opened, so the hashes name the bytes that were checked.
|
||||
$installerHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $InstallerPath).Hash.ToLowerInvariant()
|
||||
@@ -83,16 +88,19 @@ try {
|
||||
Assert-TreeMatchesPayload -Label "Portable archive" -Root $portableRoot -PayloadManifest $payloadManifest
|
||||
|
||||
# electron-builder's NSIS installer carries each architecture's application
|
||||
# as an app-<arch>.7z among its plugins; this release builds x64 alone.
|
||||
# as an app-<arch>.7z among its plugins; electron-builder names the x64 one
|
||||
# app-64.7z. A package for one architecture carries that architecture's alone.
|
||||
$installerRoot = Join-Path $extractRoot "installer"
|
||||
Invoke-SevenZip -Archive $InstallerPath -Destination $installerRoot
|
||||
$appArchives = @(Get-ChildItem -LiteralPath (Join-Path $installerRoot '$PLUGINSDIR') -Filter 'app-*.7z' -File -ErrorAction SilentlyContinue |
|
||||
ForEach-Object Name)
|
||||
if ($appArchives.Count -ne 1 -or $appArchives[0] -cne 'app-64.7z') {
|
||||
throw "Installer must carry exactly one application archive, app-64.7z; found: $($appArchives -join ', ')"
|
||||
$appArchive = @{ amd64 = 'app-64.7z'; arm64 = 'app-arm64.7z' }[$Architecture]
|
||||
if ($appArchives.Count -ne 1 -or $appArchives[0] -cne $appArchive) {
|
||||
throw "Installer must carry exactly one application archive, $appArchive; found: $($appArchives -join ', ')"
|
||||
}
|
||||
$installedRoot = Join-Path $extractRoot "installed"
|
||||
Invoke-SevenZip -Archive (Join-Path $installerRoot '$PLUGINSDIR\app-64.7z') -Destination $installedRoot
|
||||
Invoke-SevenZip -Archive (Join-Path $installerRoot "`$PLUGINSDIR\$appArchive") -Destination $installedRoot
|
||||
Assert-PeArchitecture -Root $installedRoot -Architecture $Architecture
|
||||
Assert-TreeMatchesPayload -Label "Installer" -Root $installedRoot -PayloadManifest $payloadManifest -Extra $script:InstallerOwnedFiles
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -32,6 +32,14 @@ $script:InstallerOwnedFiles = @{
|
||||
"resources/elevate.exe" = "9b1fbf0c11c520ae714af8aa9af12cfd48503eedecd7398d8992ee94d1b4dc37"
|
||||
}
|
||||
|
||||
# 32-bit images the installer writes into an installed tree on every
|
||||
# architecture: the elevation helper and the uninstaller. They are the only
|
||||
# images exempt from the architecture check, by exact path.
|
||||
$script:InstallerWrittenX86Pe = @(
|
||||
"resources/elevate.exe",
|
||||
"Uninstall Reasonix Studio.exe"
|
||||
)
|
||||
|
||||
function Get-TreeFiles {
|
||||
param([Parameter(Mandatory = $true)][string]$Root)
|
||||
|
||||
@@ -120,6 +128,49 @@ function Assert-DeclaredPeSet {
|
||||
}
|
||||
}
|
||||
|
||||
# The machine field of a PE image's COFF header, read from the bytes: an
|
||||
# emulated x64 program runs on ARM64 Windows, so what a package claims to be
|
||||
# built for has to come from the files themselves.
|
||||
function Get-PeMachine {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
|
||||
$stream = [IO.File]::OpenRead($Path)
|
||||
try {
|
||||
$dos = New-Object byte[] 64
|
||||
if ($stream.Read($dos, 0, 64) -ne 64 -or $dos[0] -ne 0x4D -or $dos[1] -ne 0x5A) { throw "Not a PE image: $Path" }
|
||||
[void]$stream.Seek([BitConverter]::ToInt32($dos, 0x3C), [IO.SeekOrigin]::Begin)
|
||||
$header = New-Object byte[] 6
|
||||
if ($stream.Read($header, 0, 6) -ne 6 -or $header[0] -ne 0x50 -or $header[1] -ne 0x45 -or $header[2] -ne 0 -or $header[3] -ne 0) {
|
||||
throw "No PE signature: $Path"
|
||||
}
|
||||
[BitConverter]::ToUInt16($header, 4)
|
||||
}
|
||||
finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
# Every PE image under Root must be built for Architecture; the 32-bit images
|
||||
# the installer writes (InstallerWrittenX86Pe) are the only exception, by path.
|
||||
function Assert-PeArchitecture {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Root,
|
||||
[Parameter(Mandatory = $true)][ValidateSet("amd64", "arm64")][string]$Architecture
|
||||
)
|
||||
|
||||
$want = @{ amd64 = 0x8664; arm64 = 0xAA64 }[$Architecture]
|
||||
$wrong = @()
|
||||
foreach ($name in Get-PeFiles -Root $Root) {
|
||||
$expected = if ($script:InstallerWrittenX86Pe -ccontains $name) { 0x014C } else { $want }
|
||||
$machine = Get-PeMachine -Path (Join-Path $Root $name)
|
||||
if ($machine -ne $expected) {
|
||||
Write-Host "::error title=studio-signing.wrong-architecture::$name is machine 0x$($machine.ToString('x')), want 0x$($expected.ToString('x')) for $Architecture"
|
||||
$wrong += $name
|
||||
}
|
||||
}
|
||||
if ($wrong.Count -gt 0) { throw "PE files under $Root are not all $Architecture`: $($wrong -join ', ')" }
|
||||
}
|
||||
|
||||
function Get-SignTool {
|
||||
$tool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" |
|
||||
Sort-Object FullName -Descending | Select-Object -First 1
|
||||
|
||||
Reference in new issue
Block a user