param( [string]$PythonExe = "C:\Python310\python.exe", [switch]$SkipInstall ) $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest $env:PYTHONUTF8 = "1" $clientRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $venvRoot = Join-Path $clientRoot ".build-venv" $buildRoot = Join-Path $clientRoot "build\client-package" $releaseRoot = Join-Path $clientRoot "release" $buildPython = Join-Path $venvRoot "Scripts\python.exe" function Assert-ChildPath { param([string]$Path, [string]$Parent) $fullPath = [IO.Path]::GetFullPath($Path).TrimEnd('\') $fullParent = [IO.Path]::GetFullPath($Parent).TrimEnd('\') + '\' if (-not $fullPath.StartsWith($fullParent, [StringComparison]::OrdinalIgnoreCase)) { throw "Refusing to operate outside the Client directory: $fullPath" } } function Remove-BuildDirectory { param([string]$Path) Assert-ChildPath -Path $Path -Parent $clientRoot if (Test-Path -LiteralPath $Path) { Remove-Item -LiteralPath $Path -Recurse -Force } } function Invoke-Checked { param([scriptblock]$Command, [string]$FailureMessage) & $Command if ($LASTEXITCODE -ne 0) { throw "$FailureMessage (exit code: $LASTEXITCODE)" } } if (-not (Test-Path -LiteralPath $PythonExe -PathType Leaf)) { throw "Python 3.10 was not found: $PythonExe" } if (-not (Test-Path -LiteralPath $buildPython -PathType Leaf)) { Write-Host "[1/7] Creating the isolated build environment..." Invoke-Checked -FailureMessage "Failed to create the build environment" -Command { & $PythonExe -m venv $venvRoot } } if (-not $SkipInstall) { Write-Host "[2/7] Installing pinned runtime and build dependencies..." Invoke-Checked -FailureMessage "Failed to install build dependencies" -Command { & $buildPython -m pip install --disable-pip-version-check ` -r (Join-Path $clientRoot "requirements.txt") ` -r (Join-Path $PSScriptRoot "requirements-build.txt") } } else { Write-Host "[2/7] Skipping dependency installation." } Push-Location $clientRoot try { $version = (& $buildPython -c "from src.version import __version__; print(__version__)").Trim() if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { throw "Failed to read the software version" } Write-Host "[3/7] Cleaning old build output..." Remove-BuildDirectory -Path $buildRoot Remove-BuildDirectory -Path $releaseRoot New-Item -ItemType Directory -Force -Path $buildRoot, $releaseRoot | Out-Null $versionParts = $version.Split('.') if ($versionParts.Count -lt 3 -or $versionParts.Count -gt 4) { throw "The version must contain three or four numeric parts: $version" } foreach ($part in $versionParts) { $number = 0 if (-not [int]::TryParse($part, [ref]$number) -or $number -lt 0) { throw "The version must contain three or four numeric parts: $version" } } while ($versionParts.Count -lt 4) { $versionParts += "0" } $fileVersion = $versionParts -join ',' $versionFile = Join-Path $buildRoot "windows-version.txt" $versionInfo = @" VSVersionInfo( ffi=FixedFileInfo(filevers=($fileVersion), prodvers=($fileVersion), mask=0x3f, flags=0x0, OS=0x40004, fileType=0x1, subtype=0x0, date=(0, 0)), kids=[StringFileInfo([StringTable('080404b0', [ StringStruct('CompanyName', 'CMAutoBuy'), StringStruct('FileDescription', 'CMAutoBuy Client'), StringStruct('FileVersion', '$version'), StringStruct('InternalName', 'CMAutoBuy'), StringStruct('OriginalFilename', 'CMAutoBuy.exe'), StringStruct('ProductName', 'CMAutoBuy Client'), StringStruct('ProductVersion', '$version') ])]), VarFileInfo([VarStruct('Translation', [2052, 1200])])] ) "@ [IO.File]::WriteAllText($versionFile, $versionInfo, [Text.UTF8Encoding]::new($false)) $distPath = Join-Path $buildRoot "dist" $workPath = Join-Path $buildRoot "work" $specPath = Join-Path $buildRoot "spec" $bundledAdbSource = Join-Path $clientRoot "vendor\android-platform-tools\windows" foreach ($adbFileName in @("adb.exe", "AdbWinApi.dll", "AdbWinUsbApi.dll")) { $adbSourceFile = Join-Path $bundledAdbSource $adbFileName if (-not (Test-Path -LiteralPath $adbSourceFile -PathType Leaf)) { throw "A required bundled ADB source file is missing: $adbSourceFile" } } Write-Host "[4/7] Building the main application..." Invoke-Checked -FailureMessage "Failed to build the main application" -Command { & $buildPython -m PyInstaller --noconfirm --clean --onedir --windowed ` --name CMAutoBuy --contents-directory dependencies ` --distpath $distPath --workpath $workPath --specpath $specPath ` --version-file $versionFile ` --collect-all qfluentwidgets ` --collect-all uiautomator2 ` --collect-all adbutils ` --add-binary "$bundledAdbSource;vendor/android-platform-tools/windows" ` (Join-Path $clientRoot "buyer_main.py") } Write-Host "[5/7] Building the lightweight launcher..." Invoke-Checked -FailureMessage "Failed to build the launcher" -Command { & $buildPython -m PyInstaller --noconfirm --clean --onefile --windowed ` --name Launcher --distpath $distPath --workpath $workPath ` --specpath $specPath --version-file $versionFile ` (Join-Path $clientRoot "build_tools\launcher.py") } $mainDist = Join-Path $distPath "CMAutoBuy" $launcherExe = Join-Path $distPath "Launcher.exe" $bundledAdbDist = Join-Path $mainDist "dependencies\vendor\android-platform-tools\windows" foreach ($requiredPath in @( (Join-Path $mainDist "CMAutoBuy.exe"), (Join-Path $mainDist "dependencies\PyQt5\Qt5\plugins\platforms\qwindows.dll"), (Join-Path $bundledAdbDist "adb.exe"), (Join-Path $bundledAdbDist "AdbWinApi.dll"), (Join-Path $bundledAdbDist "AdbWinUsbApi.dll"), $launcherExe )) { if (-not (Test-Path -LiteralPath $requiredPath -PathType Leaf)) { throw "A required packaged file is missing: $requiredPath" } } Write-Host "[6/7] Assembling portable and update archives..." $packageName = "CMAutoBuy-$version" $packageRoot = Join-Path $releaseRoot $packageName $appRoot = Join-Path $packageRoot "app" New-Item -ItemType Directory -Force -Path $packageRoot | Out-Null Copy-Item -LiteralPath $mainDist -Destination $appRoot -Recurse Copy-Item -LiteralPath $launcherExe -Destination (Join-Path $packageRoot "Launcher.exe") New-Item -ItemType Directory -Force -Path ` (Join-Path $packageRoot "data"), ` (Join-Path $packageRoot "data\logs"), ` (Join-Path $packageRoot "data\artifacts") | Out-Null [IO.File]::WriteAllText( (Join-Path $appRoot "version.txt"), "$version`n", [Text.UTF8Encoding]::new($false) ) # Windows PowerShell 5 may read a UTF-8 script without BOM as the system code page. # Build the Chinese product name from Unicode code points so the release name is stable. $portableProductName = -join @( [char]0x81EA, [char]0x52A8, [char]0x91C7, [char]0x96C6, [char]0x91C7, [char]0x8D2D, [char]0x5DE5, [char]0x5177 ) $portableZip = Join-Path $releaseRoot "$($portableProductName)_$version.zip" Compress-Archive -Path (Join-Path $packageRoot "*") -DestinationPath $portableZip -CompressionLevel Optimal $portableLatestZip = Join-Path $releaseRoot "$portableProductName.zip" Copy-Item -LiteralPath $portableZip -Destination $portableLatestZip $updateStage = Join-Path $buildRoot "update-stage" New-Item -ItemType Directory -Force -Path $updateStage | Out-Null Copy-Item -LiteralPath $appRoot -Destination (Join-Path $updateStage "app") -Recurse $updateZip = Join-Path $releaseRoot "CMAutoBuy_$version.zip" Compress-Archive -Path (Join-Path $updateStage "*") -DestinationPath $updateZip -CompressionLevel Optimal Write-Host "[7/7] Generating the release manifest..." $manifestPath = Join-Path $releaseRoot "autobuy_manifest.json" Invoke-Checked -FailureMessage "Failed to generate the release manifest" -Command { & $buildPython (Join-Path $clientRoot "build_tools\release_manifest.py") ` --version $version --update $updateZip --portable $portableLatestZip ` --output $manifestPath } $versionedManifestPath = Join-Path $releaseRoot "autobuy_manifest_$version.json" Copy-Item -LiteralPath $manifestPath -Destination $versionedManifestPath Write-Host "Build complete: $releaseRoot" Write-Host "Release manifest: $manifestPath" } finally { Pop-Location }