242 lines
9.2 KiB
PowerShell
242 lines
9.2 KiB
PowerShell
param(
|
|
[string]$PythonLauncher = "py",
|
|
[string]$PythonVersion = "-3.12",
|
|
[switch]$SkipIcon,
|
|
[switch]$SkipBackend,
|
|
[switch]$SkipFrontend,
|
|
[switch]$SkipInstaller,
|
|
[switch]$Sign,
|
|
[string]$CertificateFile,
|
|
[string]$CertificatePassword,
|
|
[string]$PublisherName,
|
|
[string]$TimestampServer = "http://timestamp.digicert.com",
|
|
[string[]]$VoiceSmokeKeys = @(),
|
|
[switch]$ForceSign
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$Root = Resolve-Path (Join-Path $PSScriptRoot "..")
|
|
$Frontend = Join-Path $Root "chatlab-web\frontend"
|
|
$Backend = Join-Path $Root "chatlog_fastAPI"
|
|
$Electron = Join-Path $Root "electron-launcher"
|
|
$Resources = Join-Path $Electron "build-resources"
|
|
$Release = Join-Path $Root "release"
|
|
|
|
function Invoke-Python312($Arguments) {
|
|
$pyCommand = Get-Command $PythonLauncher -ErrorAction SilentlyContinue
|
|
if ($pyCommand) {
|
|
try {
|
|
& $PythonLauncher $PythonVersion -V | Out-Null
|
|
if ($LASTEXITCODE -eq 0) {
|
|
& $PythonLauncher $PythonVersion @Arguments
|
|
return
|
|
}
|
|
} catch {
|
|
Write-Host "Python launcher $PythonLauncher $PythonVersion is not available, falling back to user Python312."
|
|
}
|
|
}
|
|
|
|
$fallback = Join-Path $env:LOCALAPPDATA "Programs\Python\Python312\python.exe"
|
|
if (-not (Test-Path $fallback)) {
|
|
throw "Python 3.12 was not found. Install it first or pass -PythonLauncher/-PythonVersion."
|
|
}
|
|
|
|
& $fallback @Arguments
|
|
}
|
|
|
|
function Reset-Dir($Path) {
|
|
if (Test-Path $Path) {
|
|
$resolved = Resolve-Path $Path
|
|
if (-not $resolved.Path.StartsWith($Root.Path)) {
|
|
throw "Refusing to remove path outside project: $($resolved.Path)"
|
|
}
|
|
Remove-Item -LiteralPath $resolved.Path -Recurse -Force
|
|
}
|
|
New-Item -ItemType Directory -Force -Path $Path | Out-Null
|
|
}
|
|
|
|
function Copy-Dir($Source, $Dest) {
|
|
if (-not (Test-Path $Source)) {
|
|
throw "Missing required source: $Source"
|
|
}
|
|
New-Item -ItemType Directory -Force -Path (Split-Path $Dest) | Out-Null
|
|
Copy-Item -LiteralPath $Source -Destination $Dest -Recurse -Force
|
|
}
|
|
|
|
function Set-OptionalEnv($Name, $Value) {
|
|
if ([string]::IsNullOrWhiteSpace($Value)) {
|
|
Remove-Item -Path "Env:$Name" -ErrorAction SilentlyContinue
|
|
} else {
|
|
Set-Item -Path "Env:$Name" -Value $Value
|
|
}
|
|
}
|
|
|
|
function Test-ForbiddenReleaseFile($File) {
|
|
$inBackendInternal = $File.FullName -match '[\\/]backend[\\/]_internal[\\/]'
|
|
return $File.Name -eq ".env" -or
|
|
$File.Name -like "knowledge*.db" -or
|
|
($File.FullName -match "\\__pycache__\\" -and -not $inBackendInternal) -or
|
|
$File.Name -like "*.pfx" -or
|
|
$File.Name -like "*.p12" -or
|
|
$File.Name -like "*.pvk" -or
|
|
$File.Name -like "*.cer" -or
|
|
$File.Name -like "*.crt" -or
|
|
$File.Name -like "*.key" -or
|
|
$File.FullName -match "\\certs\\"
|
|
}
|
|
|
|
Set-Location $Root
|
|
|
|
$resolvedCertificateFile = $null
|
|
if ($CertificateFile) {
|
|
if (-not (Test-Path -LiteralPath $CertificateFile)) {
|
|
throw "Certificate file was not found: $CertificateFile"
|
|
}
|
|
$resolvedCertificateFile = (Resolve-Path -LiteralPath $CertificateFile).Path
|
|
if ($resolvedCertificateFile.StartsWith($Root.Path)) {
|
|
throw "For safety, keep the code signing certificate outside the project folder: $resolvedCertificateFile"
|
|
}
|
|
}
|
|
|
|
$envCertificateFile = if ($resolvedCertificateFile) { $resolvedCertificateFile } else { $env:CHATLAB_PFX_FILE }
|
|
$envCertificateFile = if ($envCertificateFile) { $envCertificateFile.Trim() } else { "" }
|
|
$shouldSign = $Sign -or -not [string]::IsNullOrWhiteSpace($envCertificateFile)
|
|
|
|
if ($ForceSign -and -not $shouldSign) {
|
|
throw "-ForceSign requires -CertificateFile or CHATLAB_PFX_FILE."
|
|
}
|
|
|
|
if ($shouldSign) {
|
|
if (-not $envCertificateFile) {
|
|
throw "Signing was requested, but no certificate was provided. Use -CertificateFile or CHATLAB_PFX_FILE."
|
|
}
|
|
if (-not (Test-Path -LiteralPath $envCertificateFile)) {
|
|
throw "Certificate file was not found: $envCertificateFile"
|
|
}
|
|
$envCertificateFile = (Resolve-Path -LiteralPath $envCertificateFile).Path
|
|
if ($envCertificateFile.StartsWith($Root.Path)) {
|
|
throw "For safety, keep the code signing certificate outside the project folder: $envCertificateFile"
|
|
}
|
|
Set-OptionalEnv "CHATLAB_PFX_FILE" $envCertificateFile
|
|
if ($CertificatePassword) {
|
|
Set-OptionalEnv "CHATLAB_PFX_PASSWORD" $CertificatePassword
|
|
Set-OptionalEnv "WIN_CSC_KEY_PASSWORD" $CertificatePassword
|
|
Set-OptionalEnv "CSC_KEY_PASSWORD" $CertificatePassword
|
|
}
|
|
if ($PublisherName) { Set-OptionalEnv "CHATLAB_CERT_PUBLISHER_NAME" $PublisherName }
|
|
Set-OptionalEnv "CHATLAB_TIMESTAMP_SERVER" $TimestampServer
|
|
if ($ForceSign) { Set-OptionalEnv "CHATLAB_FORCE_SIGN" "1" }
|
|
Write-Host "Code signing enabled. Certificate: $envCertificateFile"
|
|
} else {
|
|
Remove-Item -Path "Env:CHATLAB_PFX_FILE" -ErrorAction SilentlyContinue
|
|
Remove-Item -Path "Env:CHATLAB_PFX_PASSWORD" -ErrorAction SilentlyContinue
|
|
Remove-Item -Path "Env:CHATLAB_CERT_PUBLISHER_NAME" -ErrorAction SilentlyContinue
|
|
Remove-Item -Path "Env:CHATLAB_FORCE_SIGN" -ErrorAction SilentlyContinue
|
|
Remove-Item -Path "Env:WIN_CSC_KEY_PASSWORD" -ErrorAction SilentlyContinue
|
|
Remove-Item -Path "Env:CSC_KEY_PASSWORD" -ErrorAction SilentlyContinue
|
|
Write-Host "Code signing disabled. Unsigned installer build is allowed."
|
|
}
|
|
|
|
if (-not $SkipIcon) {
|
|
& node (Join-Path $Root "scripts\make-icon.cjs")
|
|
}
|
|
|
|
$logoDest = Join-Path $Electron "build\company-logo.jpg"
|
|
$logoSrc = Join-Path $Frontend "public\company-logo.jpg"
|
|
if (Test-Path $logoSrc) {
|
|
Copy-Item -LiteralPath $logoSrc -Destination $logoDest -Force
|
|
Write-Host "company-logo.jpg synced to electron-launcher\build\"
|
|
}
|
|
if (-not $SkipFrontend) {
|
|
$dist = Join-Path $Frontend "dist"
|
|
if (Test-Path $dist) {
|
|
Remove-Item -LiteralPath (Resolve-Path $dist).Path -Recurse -Force
|
|
}
|
|
Push-Location $Frontend
|
|
& npm.cmd run build
|
|
Pop-Location
|
|
}
|
|
|
|
if (-not $SkipBackend) {
|
|
Push-Location $Backend
|
|
Invoke-Python312 @("-m", "PyInstaller", "ChatLabBackend.spec", "--noconfirm", "--clean")
|
|
Pop-Location
|
|
}
|
|
|
|
Reset-Dir $Resources
|
|
New-Item -ItemType Directory -Force -Path $Release | Out-Null
|
|
|
|
Copy-Item -LiteralPath (Join-Path $Root "chatlog.exe") -Destination (Join-Path $Resources "chatlog.exe") -Force
|
|
$previousErrorActionPreference = $ErrorActionPreference
|
|
$ErrorActionPreference = "Continue"
|
|
$chatlogVersionOutput = & (Join-Path $Resources "chatlog.exe") version 2>&1
|
|
$chatlogVersionExitCode = $LASTEXITCODE
|
|
$ErrorActionPreference = $previousErrorActionPreference
|
|
if ($chatlogVersionExitCode -ne 0) {
|
|
throw "chatlog.exe version check failed:`n$chatlogVersionOutput"
|
|
}
|
|
Write-Host "chatlog.exe version: $chatlogVersionOutput"
|
|
|
|
foreach ($voiceKey in $VoiceSmokeKeys) {
|
|
if ([string]::IsNullOrWhiteSpace($voiceKey)) { continue }
|
|
try {
|
|
$response = Invoke-WebRequest -Uri "http://127.0.0.1:5030/voice/$voiceKey" -UseBasicParsing -TimeoutSec 15
|
|
if ($response.StatusCode -ge 400 -or $response.RawContentLength -le 0) {
|
|
throw "HTTP $($response.StatusCode), length=$($response.RawContentLength)"
|
|
}
|
|
Write-Host "voice smoke passed: $voiceKey ($($response.RawContentLength) bytes)"
|
|
} catch {
|
|
throw "voice smoke failed for $voiceKey. Do not ship this installer until chatlog can read WeChat voice media. $($_.Exception.Message)"
|
|
}
|
|
}
|
|
Copy-Dir (Join-Path $Root "lib") (Join-Path $Resources "lib")
|
|
Copy-Dir (Join-Path $Frontend "dist") (Join-Path $Resources "frontend")
|
|
Copy-Dir (Join-Path $Backend "dist\ChatLabBackend") (Join-Path $Resources "backend")
|
|
Copy-Item -LiteralPath (Join-Path $Root "DISCLAIMER.md") -Destination (Join-Path $Resources "DISCLAIMER.md") -Force
|
|
Copy-Item -LiteralPath (Join-Path $Root "LICENSE") -Destination (Join-Path $Resources "LICENSE") -Force
|
|
|
|
$forbidden = Get-ChildItem -LiteralPath $Resources -Recurse -Force |
|
|
Where-Object { Test-ForbiddenReleaseFile $_ }
|
|
|
|
if ($forbidden) {
|
|
$names = ($forbidden | Select-Object -ExpandProperty FullName) -join "`n"
|
|
throw "Sensitive or cache files found in release resources:`n$names"
|
|
}
|
|
|
|
Get-ChildItem -LiteralPath $Resources -Recurse -File |
|
|
Select-Object FullName, Length |
|
|
Out-File -Encoding UTF8 (Join-Path $Release "manifest.txt")
|
|
|
|
if (-not $SkipInstaller) {
|
|
Push-Location $Electron
|
|
& npm.cmd run build
|
|
Pop-Location
|
|
Copy-Item -Path (Join-Path $Electron "dist\*.exe") -Destination $Release -Force
|
|
Copy-Item -Path (Join-Path $Electron "dist\*.blockmap") -Destination $Release -Force -ErrorAction SilentlyContinue
|
|
|
|
$releaseForbidden = Get-ChildItem -LiteralPath $Release -Recurse -Force |
|
|
Where-Object { Test-ForbiddenReleaseFile $_ }
|
|
if ($releaseForbidden) {
|
|
$names = ($releaseForbidden | Select-Object -ExpandProperty FullName) -join "`n"
|
|
throw "Sensitive certificate, private data, or cache files found in release output:`n$names"
|
|
}
|
|
|
|
$installer = Get-ChildItem -LiteralPath $Release -Filter "ChatLab-Setup-*.exe" -File |
|
|
Sort-Object LastWriteTime -Descending |
|
|
Select-Object -First 1
|
|
if ($installer) {
|
|
$signature = Get-AuthenticodeSignature -FilePath $installer.FullName
|
|
if ($shouldSign -or $ForceSign) {
|
|
if ($signature.Status -ne "Valid") {
|
|
throw "Installer signing verification failed: $($signature.Status) $($signature.StatusMessage)"
|
|
}
|
|
Write-Host "Installer signature verified: $($signature.SignerCertificate.Subject)"
|
|
} else {
|
|
Write-Host "Installer is unsigned by design for this build."
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Host "Desktop build completed. Output: $Release"
|