2026-07-03 09:26:27 +08:00
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
|
|
|
|
|
|
|
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
2026-07-07 14:21:30 +08:00
|
|
|
|
$repoRoot = (Resolve-Path (Join-Path $scriptDir "..")).Path
|
2026-07-03 09:26:27 +08:00
|
|
|
|
Set-Location $repoRoot
|
|
|
|
|
|
|
2026-07-07 14:21:30 +08:00
|
|
|
|
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 ', ')"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 14:21:31 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 14:21:30 +08:00
|
|
|
|
$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"
|
2026-07-03 09:26:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
$spec = Join-Path $repoRoot "cmshopee.spec"
|
|
|
|
|
|
if (-not (Test-Path -LiteralPath $spec)) {
|
|
|
|
|
|
throw "Missing cmshopee.spec"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 14:21:30 +08:00
|
|
|
|
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"
|
|
|
|
|
|
}
|
2026-07-03 09:26:27 +08:00
|
|
|
|
|
2026-07-13 12:07:59 +08:00
|
|
|
|
$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"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 09:26:27 +08:00
|
|
|
|
$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",
|
2026-07-07 14:12:32 +08:00
|
|
|
|
"title_prompt.txt",
|
|
|
|
|
|
"data"
|
2026-07-03 09:26:27 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-07 14:21:30 +08:00
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
|
$releaseRoot = Join-Path $repoRoot "release"
|
2026-07-08 14:21:31 +08:00
|
|
|
|
$releaseName = "$appName$appVersion"
|
2026-07-07 14:21:30 +08:00
|
|
|
|
$releaseDir = Join-Path $releaseRoot $releaseName
|
2026-07-08 14:21:31 +08:00
|
|
|
|
$portableZip = Join-Path $releaseRoot "$releaseName.zip"
|
2026-07-13 17:36:46 +08:00
|
|
|
|
$releaseMetadata = Join-Path $releaseRoot "release-metadata${appVersion}.json"
|
|
|
|
|
|
$legacyReleaseMetadata = Join-Path $releaseRoot "release-metadata.json"
|
2026-07-07 14:21:30 +08:00
|
|
|
|
|
2026-07-13 17:36:46 +08:00
|
|
|
|
foreach ($path in @($releaseDir, $portableZip, $releaseMetadata, $legacyReleaseMetadata)) {
|
2026-07-07 14:21:30 +08:00
|
|
|
|
Assert-InProject -Path $path
|
|
|
|
|
|
if (Test-Path -LiteralPath $path) {
|
|
|
|
|
|
Remove-Item -LiteralPath $path -Recurse -Force
|
2026-07-03 09:26:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 14:21:30 +08:00
|
|
|
|
New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null
|
|
|
|
|
|
Get-ChildItem -LiteralPath $distDir -Force | Copy-Item -Destination $releaseDir -Recurse -Force
|
2026-07-13 12:07:59 +08:00
|
|
|
|
Copy-Item -LiteralPath $updaterExe -Destination (Join-Path $releaseDir "cmshopee-updater.exe") -Force
|
2026-07-07 14:21:30 +08:00
|
|
|
|
Set-Content -LiteralPath (Join-Path $releaseDir "version.txt") -Value $appVersion -Encoding ascii -NoNewline
|
2026-07-03 09:26:27 +08:00
|
|
|
|
|
2026-07-07 14:21:30 +08:00
|
|
|
|
$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
|
2026-07-13 11:58:05 +08:00
|
|
|
|
& $pythonExe @pythonArgs -m app.release_manifest manifest --release-dir $releaseDir --version $appVersion
|
|
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
|
|
|
|
throw "生成 package-manifest.json 失败,退出码: $LASTEXITCODE"
|
|
|
|
|
|
}
|
2026-07-08 14:21:31 +08:00
|
|
|
|
Compress-ArchiveWithRetry -SourcePath (Join-Path $releaseDir "*") -DestinationPath $portableZip
|
2026-07-13 11:58:05 +08:00
|
|
|
|
& $pythonExe @pythonArgs -m app.release_manifest metadata --zip $portableZip --version $appVersion --output $releaseMetadata
|
|
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
2026-07-13 17:36:46 +08:00
|
|
|
|
throw "生成 release-metadata${appVersion}.json 失败,退出码: $LASTEXITCODE"
|
2026-07-13 11:58:05 +08:00
|
|
|
|
}
|
2026-07-07 14:21:30 +08:00
|
|
|
|
|
|
|
|
|
|
Write-Host "$(Decode-Utf8Base64 "5p6E5bu65a6M5oiQOg==") $exePath"
|
|
|
|
|
|
Write-Host "$(Decode-Utf8Base64 "5Y+R5biD55uu5b2VOg==") $releaseDir"
|
|
|
|
|
|
Write-Host "$(Decode-Utf8Base64 "5L6/5pC65Y6L57yp5YyFOg==") $portableZip"
|
2026-07-13 12:23:46 +08:00
|
|
|
|
Write-Host "$(Decode-Utf8Base64 "5Y+R5biD5YWD5pWw5o2uOg==") $releaseMetadata"
|
2026-07-07 14:21:30 +08:00
|
|
|
|
Write-Host (Decode-Utf8Base64 "5LiN6KaB5oqK5pys5ZywIGRhdGHjgIHphY3nva7jgIHmlbDmja7lupPjgIHlm77niYfjgIHml6Xlv5fmiJbnmbvlvZXmgIHlpI3liLbov5vlj5HluIPljIXjgII=")
|