$ErrorActionPreference = "Stop" $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $repoRoot = (Resolve-Path (Join-Path $scriptDir "..")).Path Set-Location $repoRoot function Read-VersionValue { param( [string]$Path, [string]$Name ) $pattern = '^\s*{0}\s*=\s*"([^"]+)"' -f [regex]::Escape($Name) foreach ($line in Get-Content -LiteralPath $Path -Encoding UTF8) { if ($line -match $pattern) { return $Matches[1] } } throw "Cannot find $Name in $Path" } function Decode-Utf8Base64 { param([string]$Text) return [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Text)) } function Assert-InProject { param([string]$Path) $resolvedRoot = (Resolve-Path -LiteralPath $repoRoot).Path $rootPrefix = $resolvedRoot.TrimEnd("\") + "\" if (Test-Path -LiteralPath $Path) { $resolvedPath = (Resolve-Path -LiteralPath $Path).Path if ($resolvedPath -ne $resolvedRoot -and -not $resolvedPath.StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { throw "Refusing to modify path outside project: $resolvedPath" } } else { $fullPath = [System.IO.Path]::GetFullPath($Path) if ($fullPath -ne $resolvedRoot -and -not $fullPath.StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { throw "Refusing to modify path outside project: $fullPath" } } } function Assert-NoExcludedData { param( [string]$Root, [string[]]$Excluded ) $found = @() foreach ($relative in $Excluded) { $candidate = Join-Path $Root $relative if (Test-Path -LiteralPath $candidate) { $found += $relative } } if ($found.Count -gt 0) { throw "Build output contains local user data: $($found -join ', ')" } } function Assert-PackagedStartup { param( [string]$ExePath, [int]$TimeoutSeconds = 30 ) $process = Start-Process ` -FilePath $ExePath ` -ArgumentList @("--startup-smoke-test") ` -PassThru ` -WindowStyle Hidden if (-not $process.WaitForExit($TimeoutSeconds * 1000)) { if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force } throw "打包程序启动自检超时(${TimeoutSeconds}秒): $ExePath" } $process.Refresh() if ($process.ExitCode -ne 0) { throw "打包程序启动自检失败,退出码: $($process.ExitCode)" } } function Compress-ArchiveWithRetry { param( [string]$SourcePath, [string]$DestinationPath, [int]$Attempts = 5, [int]$DelaySeconds = 2 ) for ($attempt = 1; $attempt -le $Attempts; $attempt++) { try { if (Test-Path -LiteralPath $DestinationPath) { Remove-Item -LiteralPath $DestinationPath -Force } Compress-Archive -Path $SourcePath -DestinationPath $DestinationPath -Force return } catch { if ($attempt -ge $Attempts) { throw } Write-Host "压缩发布包被临时文件占用阻塞,等待 $DelaySeconds 秒后重试 ($attempt/$Attempts):$($_.Exception.Message)" Start-Sleep -Seconds $DelaySeconds } } } $versionFile = Join-Path $repoRoot "app\version.py" if (-not (Test-Path -LiteralPath $versionFile)) { throw "Missing app\version.py" } $appName = Read-VersionValue -Path $versionFile -Name "APP_NAME" $appCodeName = Read-VersionValue -Path $versionFile -Name "APP_CODE_NAME" $appVersion = Read-VersionValue -Path $versionFile -Name "APP_VERSION" if ($appCodeName -ne "cmshopee") { throw "APP_CODE_NAME must be cmshopee, current value: $appCodeName" } $pythonExe = "py" $pythonArgs = @("-3.10") $pythonVersionOutput = & $pythonExe @pythonArgs -c "import sys; print('%d.%d.%d' % sys.version_info[:3])" if ($LASTEXITCODE -ne 0 -or -not $pythonVersionOutput) { throw (Decode-Utf8Base64 "5pyq5om+5YiwIFB5dGhvbiAzLjEw44CC6K+35YWI5a6J6KOFIFB5dGhvbiAzLjEw77yM5bm256Gu6K6kIFdpbmRvd3MgUHl0aG9uIExhdW5jaGVyIOWPr+mAmui/hyBweSAtMy4xMCDosIPnlKjjgII=") } $pythonVersion = ($pythonVersionOutput | Select-Object -First 1).Trim() Write-Host "$(Decode-Utf8Base64 "5L2/55SoIFB5dGhvbiAzLjEwOg==") $pythonVersion" if (-not $pythonVersion.StartsWith("3.10.")) { throw "$(Decode-Utf8Base64 "5b2T5YmN5omT5YyF6ISa5pys5Zu65a6a5L2/55SoIFB5dGhvbiAzLjEw77yM5b2T5YmN54mI5pys"): $pythonVersion" } $spec = Join-Path $repoRoot "cmshopee.spec" if (-not (Test-Path -LiteralPath $spec)) { throw "Missing cmshopee.spec" } Write-Host "$(Decode-Utf8Base64 "5q2j5Zyo5p6E5bu6") $appName $appVersion" Write-Host (Decode-Utf8Base64 "5omT5YyF5ZG95Luk5Zu65a6a5L2/55SoIHB5IC0zLjEwIC1tIFB5SW5zdGFsbGVy44CC") & $pythonExe @pythonArgs -m PyInstaller --noconfirm --clean $spec if ($LASTEXITCODE -ne 0) { throw "$(Decode-Utf8Base64 "UHlJbnN0YWxsZXIg5omT5YyF5aSx6LSl77yM6YCA5Ye656CB"): $LASTEXITCODE" } $updaterSpec = Join-Path $repoRoot "cmshopee-updater.spec" $updaterDist = Join-Path $repoRoot "dist\updater" $updaterWork = Join-Path $repoRoot "build\updater" & $pythonExe @pythonArgs -m PyInstaller --noconfirm --clean --distpath $updaterDist --workpath $updaterWork $updaterSpec if ($LASTEXITCODE -ne 0) { throw "独立更新器打包失败,退出码: $LASTEXITCODE" } $updaterExe = Join-Path $updaterDist "cmshopee-updater.exe" if (-not (Test-Path -LiteralPath $updaterExe)) { throw "独立更新器产物不存在: $updaterExe" } $distDir = Join-Path $repoRoot "dist\cmshopee" $exePath = Join-Path $distDir "cmshopee.exe" if (-not (Test-Path -LiteralPath $exePath)) { throw "Build failed: $exePath not found" } $excluded = @( "config.json", "config\ai_models.json", "cmshopee.db", "cmshopee.db-wal", "cmshopee.db-shm", "db.sqlite", "chrome_user_data_dir", "images", "logs", "prompts", "title_prompt.txt", "data" ) Assert-NoExcludedData -Root $distDir -Excluded $excluded $internalDir = Join-Path $distDir "_internal" if (-not (Test-Path -LiteralPath $internalDir)) { throw "Build output missing _internal directory. Expected PyInstaller 6.x onedir layout (cmshopee.exe + _internal). Ensure PyInstaller >= 6.0 is installed: py -3.10 -m pip install -r requirements-build.txt" } Write-Host "Verified onedir layout: _internal present" Assert-PackagedStartup -ExePath $exePath -TimeoutSeconds 30 Write-Host "Verified packaged startup smoke test" $releaseRoot = Join-Path $repoRoot "release" $releaseName = "$appName$appVersion" $releaseDir = Join-Path $releaseRoot $releaseName $portableZip = Join-Path $releaseRoot "$releaseName.zip" $releaseMetadata = Join-Path $releaseRoot "release-metadata${appVersion}.json" $legacyReleaseMetadata = Join-Path $releaseRoot "release-metadata.json" foreach ($path in @($releaseDir, $portableZip, $releaseMetadata, $legacyReleaseMetadata)) { Assert-InProject -Path $path if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Recurse -Force } } New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null Get-ChildItem -LiteralPath $distDir -Force | Copy-Item -Destination $releaseDir -Recurse -Force Copy-Item -LiteralPath $updaterExe -Destination (Join-Path $releaseDir "cmshopee-updater.exe") -Force Set-Content -LiteralPath (Join-Path $releaseDir "version.txt") -Value $appVersion -Encoding ascii -NoNewline $readme = @( "$appName v$appVersion", "", (Decode-Utf8Base64 "5ZCv5Yqo5pa55byP77ya"), (Decode-Utf8Base64 "5Y+M5Ye7IGNtc2hvcGVlLmV4ZSDlkK/liqjjgII="), "", (Decode-Utf8Base64 "5L2/55So6K+05piO77ya"), (Decode-Utf8Base64 "LSDor7fmiormlbTkuKrmlofku7blpLnop6PljovliLDlj6/lhpnnm67lvZXvvIzkvovlpoIgRDpcY21zaG9wZWUg5oiW5qGM6Z2i44CC"), (Decode-Utf8Base64 "LSDkuI3opoHmlL7liLAgQzpcUHJvZ3JhbSBGaWxlcyDmiJblhbbku5bpnIDopoHnrqHnkIblkZjmnYPpmZDlhpnlhaXnmoTnm67lvZXjgII="), (Decode-Utf8Base64 "LSDpppbmrKHov5DooYzkvJrlnKjnqIvluo/lkIznuqfliJvlu7ogZGF0YVzvvIznlKjkuo7kv53lrZjotKblj7fjgIHku7vliqHjgIHlm77niYfjgIHml6Xlv5fjgIHphY3nva7lkoznmbvlvZXmgIHjgII="), (Decode-Utf8Base64 "LSDljYfnuqfml7bopobnm5bmlrDniYjnqIvluo/mlofku7blkozkvp3otZbmlofku7bvvIzkvYblv4Xpobvkv53nlZnml6fnm67lvZXph4znmoQgZGF0YVzjgII="), (Decode-Utf8Base64 "LSDkuI3opoHmioogZGF0YVwg5Y+R57uZ5YW25LuW5Lq677yM6YeM6Z2i5YyF5ZCr5pys5Zyw6YWN572u44CB5Lia5Yqh5pWw5o2u44CB5Zu+54mH5ZKM55m75b2V5oCB44CC") ) Set-Content -LiteralPath (Join-Path $releaseDir "README.txt") -Value $readme -Encoding UTF8 Assert-NoExcludedData -Root $releaseDir -Excluded $excluded & $pythonExe @pythonArgs -m app.release_manifest manifest --release-dir $releaseDir --version $appVersion if ($LASTEXITCODE -ne 0) { throw "生成 package-manifest.json 失败,退出码: $LASTEXITCODE" } Compress-ArchiveWithRetry -SourcePath (Join-Path $releaseDir "*") -DestinationPath $portableZip & $pythonExe @pythonArgs -m app.release_manifest metadata --zip $portableZip --version $appVersion --output $releaseMetadata if ($LASTEXITCODE -ne 0) { throw "生成 release-metadata${appVersion}.json 失败,退出码: $LASTEXITCODE" } Write-Host "$(Decode-Utf8Base64 "5p6E5bu65a6M5oiQOg==") $exePath" Write-Host "$(Decode-Utf8Base64 "5Y+R5biD55uu5b2VOg==") $releaseDir" Write-Host "$(Decode-Utf8Base64 "5L6/5pC65Y6L57yp5YyFOg==") $portableZip" Write-Host "$(Decode-Utf8Base64 "5Y+R5biD5YWD5pWw5o2uOg==") $releaseMetadata" Write-Host (Decode-Utf8Base64 "5LiN6KaB5oqK5pys5ZywIGRhdGHjgIHphY3nva7jgIHmlbDmja7lupPjgIHlm77niYfjgIHml6Xlv5fmiJbnmbvlvZXmgIHlpI3liLbov5vlj5HluIPljIXjgII=")