Files
pebrel/scripts/build-installer.ps1
Kuddev bf2ed9af9b feat(mobile): integrate remote terminal, SSH and reader workflows (#343)
Integrate the Android client with native desktop pairing, remote terminal and SSH/SFTP sessions, keyboard controls, file reading and Agent conversations. Preserve authenticated connection ownership and per-device input permissions.

Validation: all ten required GitHub checks passed, including five native test platforms and both macOS release workspace checks.
2026-09-29 20:09:00 +08:00

230 lines
9.8 KiB
PowerShell

[CmdletBinding()]
param(
[ValidatePattern('^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$')]
[string] $Version,
# 与便携包相同,安装器只能收录已内嵌 GPUI 着色器的发布构建。
[ValidateSet('release')]
[string] $Configuration = 'release',
[ValidateSet('NebulaTerminal', 'Pebrel')]
[string] $PackageBrand = 'Pebrel',
[ValidateSet('x64', 'arm64')]
[string] $Architecture = 'x64',
[switch] $SkipBuild,
# 与 -SkipBuild 联用:跳过「exe 必须比源码新」的陈旧检查。仅用于脚本
# 自测;发布安装包一律走全新构建。
[switch] $AllowStale,
[switch] $Force,
[switch] $ValidateOnly,
[string] $OutputDirectory,
[string] $TargetDirectory,
[string] $InnoCompiler
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$manifestPath = Join-Path $repo 'nebula_app\Cargo.toml'
$installerScript = Join-Path $PSScriptRoot 'installer.iss'
$translationPath = Join-Path $repo 'target\installer-tools\ChineseSimplified.isl'
$translationUrl = 'https://raw.githubusercontent.com/jrsoftware/issrc/c495623a97376d524f298b1b160e8fd612375c62/Files/Languages/ChineseSimplified.isl'
$translationSha256 = '6753BE2C5E2740D859900FD902824DB2EC568DA5C5B52486524C9762D778B0B0'
if ([string]::IsNullOrWhiteSpace($Version)) {
$cargoManifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8
$match = [regex]::Match($cargoManifest, '(?m)^version\s*=\s*"(?<version>[^"]+)"\s*$')
if (-not $match.Success) {
throw "Unable to read the package version from $manifestPath"
}
$Version = $match.Groups['version'].Value
}
if ($Version -notmatch '^(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)') {
throw "Version must begin with three numeric components: $Version"
}
$numericVersion = "$($Matches.major).$($Matches.minor).$($Matches.patch).0"
if ([string]::IsNullOrWhiteSpace($OutputDirectory)) {
$OutputDirectory = Join-Path $repo 'dist'
} elseif (-not [System.IO.Path]::IsPathRooted($OutputDirectory)) {
$OutputDirectory = Join-Path $repo $OutputDirectory
}
$outputRoot = [System.IO.Path]::GetFullPath($OutputDirectory)
if ([string]::IsNullOrWhiteSpace($TargetDirectory)) {
$TargetDirectory = Join-Path $repo 'target'
} elseif (-not [System.IO.Path]::IsPathRooted($TargetDirectory)) {
$TargetDirectory = Join-Path $repo $TargetDirectory
}
$cargoTargetRoot = [System.IO.Path]::GetFullPath($TargetDirectory)
$targetRoot = Join-Path $cargoTargetRoot $Configuration
$setupPath = Join-Path $outputRoot "$PackageBrand-v$Version-windows-$Architecture-setup.exe"
$requiredFiles = @(
(Join-Path $targetRoot 'pebrel.exe'),
(Join-Path $targetRoot 'pebrel-hook.exe'),
(Join-Path $targetRoot 'conpty.dll'),
(Join-Path $targetRoot 'OpenConsole.exe'),
(Join-Path $repo 'README.md'),
(Join-Path $repo 'README.zh-CN.md'),
(Join-Path $repo 'CHANGELOG.md'),
(Join-Path $repo 'INSTALL.md'),
(Join-Path $repo 'docs\lua-configuration.md'),
(Join-Path $repo 'docs\runtime-control-api.md'),
(Join-Path $repo 'docs\runtime-api-v1.schema.json'),
(Join-Path $repo 'docs\skills\pebrel-runtime\SKILL.md'),
(Join-Path $repo 'docs\skills\pebrel-runtime\agents\openai.yaml'),
(Join-Path $repo 'assets\fonts\MapleMonoNormal-NF-CN-Regular.ttf'),
(Join-Path $repo 'LICENSE'),
(Join-Path $repo 'licenses\LICENSE-LUA'),
(Join-Path $repo 'licenses\LICENSE-MLUA'),
(Join-Path $repo 'THIRD-PARTY-NOTICES')
)
if (-not (Test-Path -LiteralPath $installerScript -PathType Leaf)) {
throw "Installer definition is missing: $installerScript"
}
if (-not $SkipBuild) {
& (Join-Path $PSScriptRoot 'build-windows-product.ps1') -Configuration $Configuration -TargetDirectory $cargoTargetRoot
}
$missing = @($requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Leaf) })
if ($missing.Count -ne 0) {
throw "Required installer files are missing:`n$($missing -join "`n")"
}
$packagedExe = Join-Path $targetRoot 'pebrel.exe'
. (Join-Path $PSScriptRoot 'windows-package-architecture.ps1')
Assert-WindowsPackageArchitecture -Root $targetRoot -Architecture $Architecture
if ($PackageBrand -eq 'Pebrel' -and (Get-Item -LiteralPath $packagedExe).VersionInfo.ProductName -ne 'Pebrel') {
throw 'Pebrel packages require a freshly built Pebrel executable, not renamed Nebula binaries.'
}
# 判据同 package-release.ps1:二进制不得早于其源码最新改动;cargo 对未
# 变更目标不重链接,不能拿运行开始时刻当基准。
if (-not $AllowStale) {
$memberDirs = @(
'nebula_app', 'nebula_terminal', 'nebula_config', 'nebula_config_derive',
'nebula-completions', 'nebula_gpui', 'nebula_settings', 'nebula_split'
) | ForEach-Object { Join-Path $repo $_ }
$sourceScopes = @(
@{ Binary = $packagedExe; Roots = $memberDirs + @(
(Join-Path $repo 'Cargo.toml'), (Join-Path $repo '..\gpui-component-fork\crates')) },
@{ Binary = (Join-Path $targetRoot 'pebrel-hook.exe'); Roots = @(
(Join-Path $repo 'nebula_hook'), (Join-Path $repo 'Cargo.toml')) }
)
foreach ($scope in $sourceScopes) {
$newestSource = [datetime]::MinValue
foreach ($root in $scope.Roots) {
if (-not (Test-Path -LiteralPath $root)) { continue }
if (Test-Path -LiteralPath $root -PathType Leaf) {
$times = @((Get-Item -LiteralPath $root).LastWriteTime)
} else {
$times = Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -in '.rs', '.toml' } |
ForEach-Object { $_.LastWriteTime }
}
foreach ($time in @($times)) {
if ($time -gt $newestSource) { $newestSource = $time }
}
}
$item = Get-Item -LiteralPath $scope.Binary
if ($item.LastWriteTime -lt $newestSource) {
throw "Stale binary: $($scope.Binary) ($($item.LastWriteTime)) is older than the newest source change ($newestSource). Rebuild before packaging, or pass -AllowStale if you really mean it."
}
}
}
$helpText = & $packagedExe --help 2>&1 | Out-String
if ($helpText -notmatch '--gpui') {
throw "pebrel.exe at $packagedExe is the legacy shell (no --gpui in --help). Rebuild with --features gpui-shell; do not package a workspace-default binary."
}
$versionText = & $packagedExe --version 2>&1 | Out-String
if ($versionText -notmatch [regex]::Escape($Version)) {
throw "pebrel.exe reports `"$($versionText.Trim())`" but the installer version is $Version. The staged exe does not match this release."
}
if ($ValidateOnly) {
[PSCustomObject]@{
InstallerScript = $installerScript
Version = $Version
Configuration = $Configuration
Architecture = $Architecture
Files = $requiredFiles.Count
} | Format-List
return
}
if (Test-Path -LiteralPath $setupPath) {
if (-not $Force) {
throw "Installer already exists: $setupPath (pass -Force to replace it)"
}
Remove-Item -LiteralPath $setupPath -Force
}
New-Item -ItemType Directory -Path $outputRoot -Force | Out-Null
if ([string]::IsNullOrWhiteSpace($InnoCompiler)) {
$candidates = @(
$env:ISCC_PATH,
(Join-Path $env:LOCALAPPDATA 'Programs\Inno Setup 6\ISCC.exe'),
(Join-Path $env:ProgramFiles 'Inno Setup 6\ISCC.exe'),
(Join-Path ${env:ProgramFiles(x86)} 'Inno Setup 6\ISCC.exe')
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
$InnoCompiler = $candidates | Where-Object {
Test-Path -LiteralPath $_ -PathType Leaf
} | Select-Object -First 1
}
if ([string]::IsNullOrWhiteSpace($InnoCompiler) -or
-not (Test-Path -LiteralPath $InnoCompiler -PathType Leaf)) {
throw 'ISCC.exe was not found. Install Inno Setup 6 or pass -InnoCompiler / set ISCC_PATH.'
}
# Inno 的基础安装不内置第三方翻译。固定提交与哈希可以保留中文向导,
# 同时避免发布构建悄悄接受上游后来被替换的内容。
$translationValid = $false
if (Test-Path -LiteralPath $translationPath -PathType Leaf) {
$actualTranslationHash =
(Get-FileHash -LiteralPath $translationPath -Algorithm SHA256).Hash
$translationValid = $actualTranslationHash -eq $translationSha256
}
if (-not $translationValid) {
$translationDirectory = Split-Path -Parent $translationPath
New-Item -ItemType Directory -Path $translationDirectory -Force | Out-Null
$temporaryTranslation = "$translationPath.$PID.tmp"
try {
Invoke-WebRequest -UseBasicParsing -Uri $translationUrl -OutFile $temporaryTranslation
$actualHash = (Get-FileHash -LiteralPath $temporaryTranslation -Algorithm SHA256).Hash
if ($actualHash -ne $translationSha256) {
throw "Chinese translation hash mismatch: expected $translationSha256, got $actualHash"
}
Move-Item -LiteralPath $temporaryTranslation -Destination $translationPath -Force
} finally {
if (Test-Path -LiteralPath $temporaryTranslation) {
Remove-Item -LiteralPath $temporaryTranslation -Force
}
}
}
Push-Location $PSScriptRoot
try {
& $InnoCompiler "/DAppVersion=$Version" "/DNumericVersion=$numericVersion" "/DConfiguration=$Configuration" "/DPackageBrand=$PackageBrand" "/DArchitecture=$Architecture" "/DBuildRoot=$targetRoot" "/O$outputRoot" $installerScript
if ($LASTEXITCODE -ne 0) {
throw "Inno Setup compilation failed with exit code $LASTEXITCODE"
}
} finally {
Pop-Location
}
if (-not (Test-Path -LiteralPath $setupPath -PathType Leaf)) {
throw "Inno Setup did not create the expected installer: $setupPath"
}
$setup = Get-Item -LiteralPath $setupPath
[PSCustomObject]@{
Path = $setup.FullName
Size = $setup.Length
SHA256 = (Get-FileHash -LiteralPath $setupPath -Algorithm SHA256).Hash
} | Format-List