[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*"(?[^"]+)"\s*$') if (-not $match.Success) { throw "Unable to read the package version from $manifestPath" } $Version = $match.Groups['version'].Value } if ($Version -notmatch '^(?[0-9]+)\.(?[0-9]+)\.(?[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