完善在线工具、插件运行时与安装卸载流程

This commit is contained in:
2026-08-19 16:15:52 +08:00
parent 80093f285e
commit 7a2af2646c
366 changed files with 6683 additions and 490 deletions
+143 -5
View File
@@ -41,6 +41,7 @@ $ServerPublicRoot = Join-Path $Root 'server\update\public'
$ServerDownloadRoot = Join-Path $ServerPublicRoot 'downloads'
$ToolStateRoot = Join-Path $Root '.cache\tool_state'
$NuGetRoot = Join-Path $Root '.cache\nuget'
$BuildTempRoot = Join-Path $Root '.cache\build-temp'
$TauriHostRoot = Join-Path $Root 'src\YMhut.Box.PluginTauriHost\src-tauri'
$RepositoryCargoHome = Join-Path $Root '.cache\rust-toolchain\cargo'
$RepositoryRustupHome = Join-Path $Root '.cache\rust-toolchain\rustup'
@@ -324,7 +325,10 @@ function Ensure-TauriPluginHost {
return $executable
}
Write-Warning 'Cargo was not found, so the optional Tauri plugin host will not be included. Install the Rust stable MSVC toolchain to build ExternalRuntime support.'
if ($Configuration -ieq 'Release') {
throw 'Cargo was not found and no previously built Release Tauri host is available. Release packages require the bundled plugin host.'
}
Write-Warning 'Cargo was not found, so the optional Debug Tauri plugin host will not be included.'
return $null
}
@@ -516,6 +520,12 @@ function Save-ScaledLogo {
}
function Ensure-MsixAssets {
$iconScript = Join-Path $Root 'scripts\generate-app-icons.ps1'
& $iconScript -Root $Root
if (-not $?) {
throw 'Application icon generation failed.'
}
$sourceIcon = Join-Path $Root 'assets\icons\app_icon.png'
if (-not (Test-Path -LiteralPath $sourceIcon)) {
throw "MSIX source icon was not found: $sourceIcon"
@@ -527,6 +537,18 @@ function Ensure-MsixAssets {
Save-ScaledLogo $sourceIcon (Join-Path $ProjectAssets 'Square150x150Logo.png') 150 150 16 '#00FFFFFF'
Save-ScaledLogo $sourceIcon (Join-Path $ProjectAssets 'LockScreenLogo.png') 70 70 8 '#00FFFFFF'
Save-ScaledLogo $sourceIcon (Join-Path $ProjectAssets 'Wide310x150Logo.png') 310 150 24 '#F5F6F7' -Wordmark
$iconPath = Join-Path $ProjectAssets 'app_icon.ico'
$bytes = [IO.File]::ReadAllBytes($iconPath)
$count = [BitConverter]::ToUInt16($bytes, 4)
$sizes = for ($index = 0; $index -lt $count; $index++) {
$value = $bytes[6 + ($index * 16)]
if ($value -eq 0) { 256 } else { [int] $value }
}
$missing = @(16, 20, 24, 32, 40, 48, 64, 96, 128, 256) | Where-Object { $_ -notin $sizes }
if ($missing.Count -gt 0) {
throw "Application icon is missing DPI frames: $($missing -join ', ')"
}
}
function Ensure-LocalDeveloperCertificate {
@@ -712,7 +734,13 @@ function Write-LayoutManifests([string] $Directory) {
$configDir = Join-Path $Directory 'config'
New-Directory $configDir
$required = @($files | Where-Object { $_ -in @('YMhutBox.exe', 'YMhutBox.dll', 'WebView2Loader.dll') })
$required = @($files | Where-Object { $_ -in @(
'YMhutBox.exe',
'YMhutBox.dll',
'WebView2Loader.dll',
'Assets/runtime-assets.dat',
'Assets/data/ymhut-data.ybin'
) })
if ($required.Count -eq 0) {
$required = @('YMhutBox.exe')
}
@@ -720,6 +748,7 @@ function Write-LayoutManifests([string] $Directory) {
$manifest = [System.Collections.Generic.List[string]]::new()
if ($versionInfo) {
$manifest.Add('[Release]')
$manifest.Add('ManifestVersion=2')
$manifest.Add("Version=$($versionInfo.Version)")
$manifest.Add("Build=$($versionInfo.Build)")
$manifest.Add("Channel=$($versionInfo.Channel)")
@@ -1097,6 +1126,7 @@ function Normalize-PublishLayout([string] $Directory) {
Remove-RootHelperDuplicates $Directory
Assert-UnpackagedLanguageResourcesInLang $Directory
Assert-NoDevelopmentArtifacts $Directory
Assert-AuthenticatedAssetLayout $Directory
}
function Assert-UnpackagedLanguageResourcesInLang([string] $Directory) {
@@ -1274,8 +1304,14 @@ function Assert-ToolUiPayload([string] $Directory) {
}
$files = @(Get-ChildItem -LiteralPath $folder -Filter '*.dat' -File -ErrorAction SilentlyContinue)
if ($files.Count -lt 202) {
throw "Release tool UI payload is incomplete in '$Directory': expected at least 202 .dat definitions, found $($files.Count)"
$expectedFolder = Join-Path $Root 'assets\data\tool-ui'
$expectedNames = @(Get-ChildItem -LiteralPath $expectedFolder -Filter '*.dat' -File -ErrorAction SilentlyContinue |
ForEach-Object Name | Sort-Object)
$actualNames = @($files | ForEach-Object Name | Sort-Object)
$difference = @(Compare-Object -ReferenceObject $expectedNames -DifferenceObject $actualNames)
if ($expectedNames.Count -eq 0 -or $difference.Count -gt 0) {
$details = $difference | ForEach-Object { "$($_.SideIndicator) $($_.InputObject)" }
throw "Release tool UI payload does not exactly match the repository definitions in '$Directory': $($details -join ', ')"
}
foreach ($file in $files) {
@@ -1286,6 +1322,47 @@ function Assert-ToolUiPayload([string] $Directory) {
}
}
function Assert-AuthenticatedAssetLayout([string] $Directory, [switch] $AllowMsixLogos) {
$assetsRoot = Join-Path $Directory 'Assets'
$runtimePackage = Join-Path $assetsRoot 'runtime-assets.dat'
$referencePackage = Join-Path $assetsRoot 'data\ymhut-data.ybin'
foreach ($package in @($runtimePackage, $referencePackage)) {
if (-not (Test-Path -LiteralPath $package -PathType Leaf)) {
throw "Authenticated asset package is missing from '$Directory': $package"
}
$bytes = [IO.File]::ReadAllBytes($package)
if ($bytes.Length -lt 97 -or [Text.Encoding]::ASCII.GetString($bytes, 0, 8) -ne 'YMHUTAS2' -or $bytes[8] -ne 2) {
throw "Authenticated asset package has an invalid header: $package"
}
}
$allowedPng = if ($AllowMsixLogos) {
@(
'Assets/StoreLogo.png',
'Assets/Square44x44Logo.png',
'Assets/Square150x150Logo.png',
'Assets/Wide310x150Logo.png'
)
} else { @() }
$blockedExtensions = @(
'.bmp', '.css', '.csv', '.gif', '.glb', '.gltf', '.htm', '.html', '.ico',
'.jpeg', '.jpg', '.js', '.json', '.mjs', '.otf', '.svg', '.ttf', '.wasm',
'.webp', '.woff', '.woff2', '.xls', '.xlsx'
)
$blocked = @(Get-ChildItem -LiteralPath $assetsRoot -Recurse -File -ErrorAction SilentlyContinue | Where-Object {
$relative = 'Assets/' + ($_.FullName.Substring($assetsRoot.Length).TrimStart('\', '/') -replace '\\', '/')
($blockedExtensions -contains $_.Extension.ToLowerInvariant()) -or
($_.Extension -ieq '.png' -and $allowedPng -notcontains $relative)
})
if ($blocked.Count -gt 0) {
$relativeList = $blocked | ForEach-Object {
'Assets/' + ($_.FullName.Substring($assetsRoot.Length).TrimStart('\', '/') -replace '\\', '/')
}
throw "Publish Assets contains replaceable plaintext static files: $($relativeList -join ', ')"
}
}
function Copy-PublishToLatest {
Stop-ProcessesFromDirectory $LatestRoot
Reset-DirectoryInsideRepo $LatestRoot
@@ -1293,6 +1370,7 @@ function Copy-PublishToLatest {
Normalize-PublishLayout $LatestRoot
Assert-MusicApiPayload $LatestRoot
Assert-ToolUiPayload $LatestRoot
Assert-AuthenticatedAssetLayout $LatestRoot
Write-LayoutManifests $LatestRoot
}
@@ -1305,6 +1383,7 @@ function Publish-UnpackagedApp([object] $VersionInfo) {
'--self-contained', 'true',
'--no-restore',
'-p:WindowsPackageType=None',
'-p:GenerateAppxPackageOnBuild=false',
'-p:PublishSingleFile=false',
"-p:AssemblyVersion=$($VersionInfo.PackageVersion)",
"-p:FileVersion=$($VersionInfo.PackageVersion)",
@@ -1314,6 +1393,7 @@ function Publish-UnpackagedApp([object] $VersionInfo) {
Normalize-PublishLayout $PublishRoot
Assert-MusicApiPayload $PublishRoot
Assert-ToolUiPayload $PublishRoot
Assert-AuthenticatedAssetLayout $PublishRoot
Assert-PerMonitorV2Manifest (Join-Path $PublishRoot 'YMhutBox.exe')
Write-LayoutManifests $PublishRoot
Copy-PublishToLatest
@@ -1387,6 +1467,39 @@ function Build-InstallerBootstrap([object] $VersionInfo, [string] $SignTool) {
Invoke-ToolQuiet $setupPath @('/WINDOWSELFTEST') 'WinUI installer window navigation self-test failed' " Installer window self-test: $setupPath"
}
function Build-UninstallerBootstrap([object] $VersionInfo, [string] $SignTool) {
$bootstrapRoot = Join-Path $Root 'build\winui\uninstaller-bootstrap'
Reset-DirectoryInsideRepo $bootstrapRoot
Invoke-DotNet @(
'publish', $InstallerBootstrapProject,
'-c', $Configuration,
'-r', 'win-x64',
'--self-contained', 'true',
'--no-restore',
'-p:BootstrapMode=Uninstall',
'-p:AssemblyName=unins000',
"-p:Version=$($VersionInfo.PackageVersion)",
"-p:FileVersion=$($VersionInfo.PackageVersion)",
"-p:InformationalVersion=$($VersionInfo.PackageVersion)",
'-o', $bootstrapRoot
)
$bootstrap = Join-Path $bootstrapRoot 'unins000.exe'
if (-not (Test-Path -LiteralPath $bootstrap -PathType Leaf)) {
throw "WinUI uninstaller bootstrap was not produced: $bootstrap"
}
if ($SignTool) {
Sign-Artifact $SignTool $bootstrap
}
Assert-PerMonitorV2Manifest $bootstrap
foreach ($payloadRoot in @($PublishRoot, $LatestRoot)) {
Copy-Item -LiteralPath $bootstrap -Destination (Join-Path $payloadRoot 'unins000.exe') -Force
Write-LayoutManifests $payloadRoot
}
Write-Host " Uninstaller bootstrap: $(Join-Path $LatestRoot 'unins000.exe')"
}
function Wait-FileWritable([string] $Path, [int] $Attempts = 20, [int] $DelayMilliseconds = 250) {
for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
$stream = $null
@@ -1501,8 +1614,11 @@ function Build-MsixPackage([object] $VersionInfo, [string] $MakeAppx, [string] $
Reset-DirectoryInsideRepo $MsixStageRoot
Copy-Item -Path (Join-Path $PublishRoot '*') -Destination $MsixStageRoot -Recurse -Force
Remove-Item -LiteralPath (Join-Path $MsixStageRoot 'unins000.exe') -Force -ErrorAction SilentlyContinue
Write-LayoutManifests $MsixStageRoot
Assert-MusicApiPayload $MsixStageRoot
Assert-ToolUiPayload $MsixStageRoot
Assert-AuthenticatedAssetLayout $MsixStageRoot
Assert-PerMonitorV2Manifest (Join-Path $MsixStageRoot 'YMhutBox.exe')
Expand-SatelliteResourcePackages $MsixStageRoot -RemovePackages
Restore-LangResourceLayoutForMsix $MsixStageRoot
@@ -1516,7 +1632,10 @@ function Build-MsixPackage([object] $VersionInfo, [string] $MakeAppx, [string] $
$stageAssets = Join-Path $MsixStageRoot 'Assets'
New-Directory $stageAssets
Copy-Item -Path (Join-Path $ProjectAssets '*') -Destination $stageAssets -Recurse -Force
foreach ($logo in @('StoreLogo.png', 'Square44x44Logo.png', 'Square150x150Logo.png', 'Wide310x150Logo.png')) {
Copy-Item -LiteralPath (Join-Path $ProjectAssets $logo) -Destination (Join-Path $stageAssets $logo) -Force
}
Assert-AuthenticatedAssetLayout $MsixStageRoot -AllowMsixLogos
Repair-MsixStageThirdPartyPayloads
Invoke-ToolQuiet $MakeAppx @('pack', '/d', $MsixStageRoot, '/p', $msixPath, '/o') 'MakeAppx packaging failed' " MSIX package: $msixPath"
@@ -1599,10 +1718,15 @@ pause
New-Directory $NuGetRoot
New-Directory $OutputRoot
New-Directory $BuildTempRoot
$env:DOTNET_CLI_HOME = Join-Path $ToolStateRoot 'dotnet'
$env:NUGET_PACKAGES = $NuGetRoot
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
$env:TEMP = $BuildTempRoot
$env:TMP = $BuildTempRoot
$env:DOTNET_BUNDLE_EXTRACT_BASE_DIR = Join-Path $BuildTempRoot 'dotnet-bundle'
New-Directory $env:DOTNET_CLI_HOME
New-Directory $env:DOTNET_BUNDLE_EXTRACT_BASE_DIR
$versionInfo = Get-VersionInfo
$makeAppx = Find-WindowsSdkTool 'MakeAppx.exe'
@@ -1637,12 +1761,26 @@ foreach ($ridProject in @($Project, $InstallerBootstrapProject)) {
}
Publish-UnpackagedApp $versionInfo
Build-UninstallerBootstrap $versionInfo $signTool
if ($tauriHostExecutable) {
$publishedTauriHost = Join-Path $PublishRoot 'tauri-host\ymhut-box-plugin-tauri-host.exe'
if (-not (Test-Path -LiteralPath $publishedTauriHost)) {
throw "The Tauri plugin host was built but was not included in the publish payload: $publishedTauriHost"
}
}
$tauriRuntimeFiles = @(
'tauri-host\runtime.json',
'tauri-host\template.ymtemplate',
'tauri-host\LICENSES.txt',
'tauri-host\template\dist\index.html',
'tauri-host\template\README.md'
)
foreach ($relative in $tauriRuntimeFiles) {
$runtimeFile = Join-Path $PublishRoot $relative
if (-not (Test-Path -LiteralPath $runtimeFile -PathType Leaf)) {
throw "The bundled Tauri runtime dependency is missing from the publish payload: $runtimeFile"
}
}
if (($Target -in @('exe', 'both')) -and -not $SkipExe) {
$exeBuilt = Build-InnoInstaller $versionInfo $signTool -AllowMissingCompiler:($Target -eq 'both')