param( [string]$Output = "dist", [string]$FactoryFirmware = "", [string]$OfficialFirmware = "" ) $ErrorActionPreference = "Stop" $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path $factoryHostRoot = Split-Path -Parent $scriptRoot $projectRoot = Split-Path -Parent $factoryHostRoot $buildRoot = Join-Path $factoryHostRoot "build" function Resolve-ProjectInput([string]$Value, [string]$DefaultRelativePath) { $selected = if ([string]::IsNullOrWhiteSpace($Value)) { $DefaultRelativePath } else { $Value } if ([System.IO.Path]::IsPathRooted($selected)) { return [System.IO.Path]::GetFullPath($selected) } return [System.IO.Path]::GetFullPath((Join-Path $projectRoot $selected)) } $outputCandidate = if ([System.IO.Path]::IsPathRooted($Output)) { $Output } else { Join-Path $factoryHostRoot $Output } $outDir = [System.IO.Path]::GetFullPath($outputCandidate) $factoryHostPrefix = [System.IO.Path]::GetFullPath($factoryHostRoot).TrimEnd("\") + "\" if (-not $outDir.StartsWith($factoryHostPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { throw "Output 必须位于 factory_host 目录内: $outDir" } $pyinstallerDist = Join-Path $buildRoot "pyinstaller-dist" $pyinstallerWork = Join-Path $buildRoot "pyinstaller-work" $pyinstallerSpec = Join-Path $buildRoot "pyinstaller-spec" $buildExe = Join-Path $pyinstallerDist "EG800FactoryHost.exe" $releaseExe = Join-Path $outDir "START-EG800-FACTORY-TOOL.exe" $runtimeDir = Join-Path $outDir ".runtime" $firmwareDir = Join-Path $outDir "firmware" $toolSource = Join-Path $factoryHostRoot "vendor\aboot-tools-2024.08.08-win-x64" $toolDestination = Join-Path $runtimeDir "burn_tool\aboot-tools-2024.08.08-win-x64" $factoryFirmwareSource = Resolve-ProjectInput $FactoryFirmware "factory_host\firmware\test\EG800AKCN_11LG-FT1-factory-test.zip" $officialFirmwareSource = Resolve-ProjectInput $OfficialFirmware "factory_host\firmware\official\EG800AKCN_11LG-official-fac.zip" $factoryFirmwareDestination = Join-Path $firmwareDir "EG800AKCN_11LG-FT1-factory-test.zip" $officialFirmwareDestination = Join-Path $firmwareDir "EG800AKCN_11LG-official-fac.zip" $releaseZip = Join-Path $outDir "EG800-Factory-Tool-Full-Package.zip" python -c "import PyInstaller" 2>$null if ($LASTEXITCODE -ne 0) { throw "未找到 PyInstaller。先执行: python -m pip install pyinstaller pyserial" } foreach ($required in @($toolSource, $factoryFirmwareSource, $officialFirmwareSource)) { if (-not (Test-Path -LiteralPath $required)) { throw "发布所需文件不存在: $required" } } # dist is disposable. Source and intermediate artifacts live elsewhere. New-Item -ItemType Directory -Force -Path $outDir, $buildRoot | Out-Null Get-ChildItem -LiteralPath $outDir -Force | Remove-Item -Recurse -Force if (Test-Path -LiteralPath $pyinstallerDist) { Remove-Item -LiteralPath $pyinstallerDist -Recurse -Force } python -m PyInstaller --noconfirm --clean --onefile --windowed ` --name EG800FactoryHost ` --distpath $pyinstallerDist ` --workpath $pyinstallerWork ` --specpath $pyinstallerSpec ` (Join-Path $factoryHostRoot "src\factory_host.py") if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $buildExe)) { throw "PyInstaller 构建失败,退出码: $LASTEXITCODE" } New-Item -ItemType Directory -Force -Path $runtimeDir, $firmwareDir | Out-Null Move-Item -LiteralPath $buildExe -Destination $releaseExe -Force Copy-Item -LiteralPath (Join-Path $factoryHostRoot "config\factory_host_config.json") ` -Destination (Join-Path $runtimeDir "factory_host_config.json") -Force Copy-Item -LiteralPath $toolSource -Destination $toolDestination -Recurse -Force Copy-Item -LiteralPath $factoryFirmwareSource -Destination $factoryFirmwareDestination -Force Copy-Item -LiteralPath $officialFirmwareSource -Destination $officialFirmwareDestination -Force New-Item -ItemType Directory -Force -Path ` (Join-Path $runtimeDir "docs"), (Join-Path $runtimeDir "logs") | Out-Null Copy-Item -LiteralPath (Join-Path $factoryHostRoot "docs\USAGE.md") ` -Destination (Join-Path $runtimeDir "docs\USAGE.md") -Force $revision = "" try { $revisionOutput = & git -C $projectRoot rev-parse HEAD 2>$null if ($LASTEXITCODE -eq 0 -and $revisionOutput) { $revision = ($revisionOutput | Select-Object -First 1).Trim() } } catch { $revision = "" } $manifest = [ordered]@{ format = "eg800-factory-host-release-v1" generated_at = (Get-Date).ToUniversalTime().ToString("o") git_revision = $revision files = [ordered]@{ executable = [ordered]@{ path = "START-EG800-FACTORY-TOOL.exe" bytes = (Get-Item -LiteralPath $releaseExe).Length sha256 = (Get-FileHash -LiteralPath $releaseExe -Algorithm SHA256).Hash } factory_firmware = [ordered]@{ path = "firmware/EG800AKCN_11LG-FT1-factory-test.zip" bytes = (Get-Item -LiteralPath $factoryFirmwareDestination).Length sha256 = (Get-FileHash -LiteralPath $factoryFirmwareDestination -Algorithm SHA256).Hash } official_firmware = [ordered]@{ path = "firmware/EG800AKCN_11LG-official-fac.zip" bytes = (Get-Item -LiteralPath $officialFirmwareDestination).Length sha256 = (Get-FileHash -LiteralPath $officialFirmwareDestination -Algorithm SHA256).Hash } downloader = [ordered]@{ path = ".runtime/burn_tool/aboot-tools-2024.08.08-win-x64/adownload.exe" bytes = (Get-Item -LiteralPath (Join-Path $toolDestination "adownload.exe")).Length sha256 = (Get-FileHash -LiteralPath (Join-Path $toolDestination "adownload.exe") -Algorithm SHA256).Hash } } } $manifest | ConvertTo-Json -Depth 5 | Set-Content ` -LiteralPath (Join-Path $runtimeDir "release_manifest.json") -Encoding UTF8 $runtimeItem = Get-Item -LiteralPath $runtimeDir -Force $runtimeItem.Attributes = $runtimeItem.Attributes -bor [System.IO.FileAttributes]::Hidden Push-Location $outDir try { & tar.exe -a -c -f $releaseZip "START-EG800-FACTORY-TOOL.exe" "firmware" ".runtime" if ($LASTEXITCODE -ne 0) { throw "tar.exe 创建完整交付包失败,退出码: $LASTEXITCODE" } } finally { Pop-Location } Write-Host "发布目录: $outDir" Write-Host "上位机: $releaseExe" Write-Host "完整交付包: $releaseZip" Write-Host "SHA256: $((Get-FileHash -LiteralPath $releaseZip -Algorithm SHA256).Hash)"