Files
simba-rmic-2026/desktop/scripts/prepare-windows-bundle.ps1
T
sloray 054379dc64
构建客户端 / 检查(类型 + 单元测试) (push) Canceled after 0s
构建客户端 / Linux x64(deb + AppImage) (push) Canceled after 0s
构建客户端 / Windows x64(NSIS 安装包) (push) Canceled after 0s
构建客户端 / macOS 通用(.app + .dmg) (push) Canceled after 0s
fix(packaging): 修复 Windows 安装与固件打包并完善客户端文案
- fix(installer): 修复低完整性产物无法写入临时目录的问题,并在 CI 打包前后检查完整性标签
- fix(firmware): 从本次 Cargo ELF 打包并同步配套镜像,修正短目录说明与 LLVM 搜索顺序
- fix(installer): 修复中文完成页和页眉遮挡,侧栏使用等比缩放的原始 Logo
- style(client): 应用图标增加圆角,统一“裁判系统客户端”命名
- fix(config): 安装提示改用可选本地热点配置,排除 Wi-Fi 凭据
- docs: 润色项目说明并统一裁判系统术语
- test(packaging): 补充 Windows 完整性和固件打包回归,覆盖外部输出目录、旧镜像与重复生成

验证:Windows NSIS 构建及原生页面预览;通用与本地安装配置编译;固件 release 构建、重复打包和客户端离线校验;6 项固件打包回归与 Windows 权限回归;git diff --check。未执行实机烧录。
2026-10-21 09:25:17 +08:00

138 lines
6.5 KiB
PowerShell

# Windows uses the executable's integrity label when creating its process. A Low
# label inherited from a sandboxed workspace prevents NSIS from writing to TEMP,
# before .onInit or any installer hook can run.
[CmdletBinding()]
param(
[string]$OutputDir,
[switch]$CheckProcessOnly,
[switch]$CheckOnly
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
# whoami's group names are localized; the mandatory-label SID is stable.
$groups = (& whoami.exe /groups) -join "`n"
if ($LASTEXITCODE -ne 0 -or $groups -notmatch 'S-1-16-(\d+)') {
throw 'Cannot determine the Windows process integrity level.'
}
if ([int]$Matches[1] -lt 8192) {
throw 'Windows packaging requires Medium integrity or higher. Start the terminal / CI runner outside the Low integrity sandbox. Redirecting TEMP or changing directory write permissions does not fix executable integrity labels.'
}
if ($CheckProcessOnly) { return }
if (-not ('RmicBundleIntegrity' -as [type])) {
$compilerOptions = @{}
if ($PSVersionTable.PSEdition -eq 'Desktop') {
# Framework CodeDOM otherwise tries to label its private temp directory
# High when an admin token has been capped at Medium by an exe label.
$parameters = New-Object System.CodeDom.Compiler.CompilerParameters
$parameters.ReferencedAssemblies.Add('System.dll') | Out-Null
$parameters.TempFiles = New-Object System.CodeDom.Compiler.TempFileCollection([IO.Path]::GetTempPath(), $false)
$compilerOptions.CompilerParameters = $parameters
}
Add-Type @compilerOptions -TypeDefinition @'
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
public static class RmicBundleIntegrity {
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
private static extern uint GetNamedSecurityInfoW(string name, int type, uint info,
out IntPtr owner, out IntPtr group, out IntPtr dacl, out IntPtr sacl, out IntPtr sd);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool ConvertSecurityDescriptorToStringSecurityDescriptorW(
IntPtr sd, uint revision, uint info, out IntPtr text, out uint length);
[DllImport("kernel32.dll")]
private static extern IntPtr LocalFree(IntPtr memory);
public static string ReadLabel(string path) {
// LABEL_SECURITY_INFORMATION reads only the label, without requiring the
// SeSecurityPrivilege needed to read an entire SACL (Get-Acl -Audit).
IntPtr owner, group, dacl, sacl, sd, text = IntPtr.Zero;
uint error = GetNamedSecurityInfoW(path, 1, 0x10, out owner, out group,
out dacl, out sacl, out sd);
if (error != 0) throw new Win32Exception((int)error, path);
try {
uint length;
if (!ConvertSecurityDescriptorToStringSecurityDescriptorW(sd, 1, 0x10,
out text, out length)) throw new Win32Exception(Marshal.GetLastWin32Error());
return Marshal.PtrToStringUni(text);
} finally {
if (text != IntPtr.Zero) LocalFree(text);
LocalFree(sd);
}
}
}
'@
}
function Assert-MediumIntegrity([string]$Path) {
$label = [RmicBundleIntegrity]::ReadLabel($Path)
# An absent label is Medium. Windows may render known SIDs as SDDL aliases.
if ($label -match ';(LW|UN|S-1-16-(\d+))\)') {
if ($Matches[1] -in @('LW', 'UN') -or [int]$Matches[2] -lt 8192) {
throw "Low / Untrusted integrity label on $Path. Run prepare-windows-bundle.ps1 from a normal Windows terminal before distributing this artifact."
}
}
}
$hostOutputDir = $null
if (-not $OutputDir) {
# Ask Cargo so CARGO_TARGET_DIR and Cargo's target-dir setting are respected.
$manifest = Join-Path $PSScriptRoot '../src-tauri/Cargo.toml'
$metadata = & cargo +stable metadata --no-deps --format-version 1 --manifest-path $manifest
if ($LASTEXITCODE -ne 0) { throw 'Cannot resolve the Cargo output directory.' }
$target = $env:TAURI_ENV_TARGET_TRIPLE
if (-not $target) { $target = 'x86_64-pc-windows-msvc' }
$profile = if ($env:TAURI_ENV_DEBUG -eq 'true') { 'debug' } else { 'release' }
$targetDir = ($metadata | ConvertFrom-Json).target_directory
$OutputDir = Join-Path $targetDir "$target/$profile"
# Cross-target Cargo builds also run host build scripts from target/<profile>.
# Old Low executables there can fail before the bundling hook is reached.
$hostOutputDir = Join-Path $targetDir $profile
}
$OutputDir = [IO.Path]::GetFullPath($OutputDir)
# Limit changes to build outputs. Do not relabel the workspace, TEMP, user data,
# or DACLs, and do not request elevation for the installer.
$directories = @(
$OutputDir
(Join-Path $OutputDir 'bundle')
(Join-Path $OutputDir 'bundle/nsis')
(Join-Path $OutputDir 'nsis')
)
if ($hostOutputDir) { $directories = @($hostOutputDir) + $directories }
if (Test-Path -LiteralPath (Join-Path $OutputDir 'nsis')) {
$directories += @(Get-ChildItem -LiteralPath (Join-Path $OutputDir 'nsis') -Directory -Recurse |
Select-Object -ExpandProperty FullName)
}
foreach ($directory in $directories) {
if (-not $CheckOnly) {
New-Item -ItemType Directory -Force -Path $directory | Out-Null
& icacls.exe $directory /setintegritylevel '(OI)(CI)M' | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Cannot set Medium integrity on $directory" }
}
Assert-MediumIntegrity $directory
}
$executables = @(Get-ChildItem -LiteralPath $OutputDir -Filter '*.exe' -File)
foreach ($directory in @('nsis', 'bundle/nsis')) {
$executables += @(Get-ChildItem -LiteralPath (Join-Path $OutputDir $directory) -Filter '*.exe' -File -Recurse)
}
if ($hostOutputDir -and (Test-Path -LiteralPath (Join-Path $hostOutputDir 'build'))) {
$executables += @(Get-ChildItem -LiteralPath (Join-Path $hostOutputDir 'build') -Filter '*.exe' -File -Recurse)
}
foreach ($executable in $executables) {
if (-not $CheckOnly) {
# Also replace explicit Low labels left by an earlier Low integrity build.
& icacls.exe $executable.FullName /setintegritylevel M | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Cannot set Medium integrity on $($executable.FullName)" }
}
Assert-MediumIntegrity $executable.FullName
}
if ($CheckOnly -and -not (Get-ChildItem -LiteralPath (Join-Path $OutputDir 'bundle/nsis') -Filter '*.exe' -File)) {
throw "No NSIS installer found in $OutputDir/bundle/nsis."
}
Write-Host "Windows bundle integrity verified: $OutputDir ($($executables.Count) executables)."