scripts\build-installer.ps1¶
源码路径:
scripts\build-installer.ps1· 228 行
# 出 Windows 安装包(Inno Setup 编译 installer\nfarms.iss)
#
# 用法:
# .\scripts\build-installer.ps1 # 编 exe + 铺 dist + 出安装包
# .\scripts\build-installer.ps1 -SkipBuild # 复用已有的 dist\
# .\scripts\build-installer.ps1 -WithLibs # 连 llama.cpp / sd.cpp 运行库一起打(体积大)
# .\scripts\build-installer.ps1 -Portable # 额外出一个免安装 zip
#
# 产物:
# dist-out\NFarms-HUB-Setup-<版本>.exe 单文件安装包
# dist-out\NFarms-HUB-<版本>-portable.zip (加了 -Portable 才有)
#
# 关于 Inno Setup:
# 本机没有 MSVC、winget 也坏了,MSI/WiX 那条路走不通。Inno 只要一个
# 自包含的 ISCC.exe,这个脚本会把它静默装到 .tools\inno(工作区内,
# 不进 PATH、不写系统目录),顺带补一份官方仓库里的简体中文语言包
# (Inno 自带 29 种语言,偏偏没有中文)。
#
# 注意:首次运行要联网下载。本机的沙箱在 workspace-write 模式下创建不了
# Schannel 凭据,所有 HTTPS 都会失败 —— 要么用 danger-full-access 跑,
# 要么先把 .tools\inno\ISCC.exe 准备好,然后加 -SkipBootstrap。
[CmdletBinding()]
param(
# 跳过 cargo 编译,直接用现成的 dist\(想反复调安装包文案时省时间)
[switch]$SkipBuild,
# 把 libs\llama.cpp 和 libs\stable-diffusion.cpp 一起打进去。
# 模型权重永远不打包(libs\model 有 28 GB,由程序自己按需下载)。
[switch]$WithLibs,
# 额外出一个免安装 zip
[switch]$Portable,
# 已经装好 Inno 时跳过下载/安装那一步
[switch]$SkipBootstrap,
[string]$DistDir = 'dist',
[string]$OutDir = 'dist-out',
[string]$Version = '',
[string]$Toolchain = ''
)
# native 命令写 stderr 在 PS 5.1 下会被当成错误;配 Stop 就是终止性异常。
$ErrorActionPreference = 'Continue'
$ws = Split-Path -Parent $PSScriptRoot
function Write-Step($msg) { Write-Host ''; Write-Host "== $msg" -ForegroundColor Cyan }
function Write-Ok($msg) { Write-Host " [OK] $msg" -ForegroundColor Green }
# ---------------------------------------------------------------------------
# 1. 版本号:从工作区的 Cargo.toml 读,免得和 exe / 安卓包两处手工同步
# ---------------------------------------------------------------------------
if (-not $Version) {
$verLine = Select-String -Path (Join-Path $ws 'Cargo.toml') -Pattern '^version\s*=\s*"([^"]+)"' |
Select-Object -First 1
if (-not $verLine) { throw '读不到 Cargo.toml 里的 version' }
$Version = $verLine.Matches[0].Groups[1].Value
}
# Inno 的 VersionInfoVersion 要四段数字
$verParts = @($Version.Split('.') + @('0', '0', '0'))[0..3]
$verInfo = $verParts -join '.'
# ---------------------------------------------------------------------------
# 2. Inno Setup(没有就装到 .tools\inno)
# ---------------------------------------------------------------------------
$innoDir = Join-Path $ws '.tools\inno'
$iscc = Join-Path $innoDir 'ISCC.exe'
if (-not (Test-Path $iscc) -and -not $SkipBootstrap) {
Write-Step '准备 Inno Setup(装到 .tools\inno)'
$tools = Join-Path $ws '.tools'
New-Item -ItemType Directory -Force -Path $tools | Out-Null
# 官网的 download.php 不直接给文件,返回的是个 HTML 页面 —— 从里面抠出真实版本号。
# 别写死版本:Inno 一发新版,写死的链接就 404。
$page = Join-Path $tools 'is-download.html'
& curl.exe -sL --fail --connect-timeout 20 --max-time 90 -o $page 'https://jrsoftware.org/download.php/is.exe' 2>&1 | Out-Null
$name = $null
if (Test-Path $page) {
$hit = (Get-Content $page -Raw) -split '[<>"]' | Where-Object { $_ -match '^innosetup-[\d.]+\.exe$' } | Select-Object -First 1
if ($hit) { $name = $hit.Trim() }
}
if (-not $name) { $name = 'innosetup-6.7.3.exe' } # 兜底:至少还有一个能用的版本
Write-Host " 安装器 = $name"
$isetup = Join-Path $tools $name
if (-not (Test-Path $isetup) -or (Get-Item $isetup).Length -lt 1MB) {
# GitHub 直连在本机是不通的,走 ghfast.top / gh-proxy.com 这类镜像。
# 每个源都试一遍,谁先成功用谁。
$tag = ($name -replace '^innosetup-', '') -replace '\.exe$', ''
$ghPath = "https://github.com/jrsoftware/issrc/releases/download/is-$($tag.Replace('.', '_'))/$name"
$sources = @(
$ghPath,
"https://ghfast.top/$ghPath",
"https://gh-proxy.com/$ghPath"
)
$got = $false
foreach ($src in $sources) {
Write-Host " 下载 $src"
Remove-Item $isetup -Force -ErrorAction SilentlyContinue
& curl.exe -L --fail --connect-timeout 20 --max-time 600 --retry 2 -o $isetup $src 2>&1 | Out-Null
if ((Test-Path $isetup) -and (Get-Item $isetup).Length -gt 1MB) { $got = $true; break }
}
if (-not $got) {
throw "下载 Inno Setup 失败。手动下载 $ghPath 放到 .tools\ 下再跑一次,或者加 -SkipBootstrap。"
}
}
Write-Ok ("安装器 {0:N1} MB" -f ((Get-Item $isetup).Length / 1MB))
# Inno 自己的安装包也是 Inno 做的,静默参数一套通用。
# /DIR 指到工作区内 —— 不需要管理员,也不会往 Program Files 里塞东西。
$p = Start-Process -FilePath $isetup -Wait -PassThru -ArgumentList @(
'/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/SP-', "/DIR=$innoDir"
)
if ($p.ExitCode -ne 0 -or -not (Test-Path $iscc)) { throw "Inno Setup 静默安装失败 (exit $($p.ExitCode))" }
Write-Ok "ISCC = $iscc"
}
if (-not (Test-Path $iscc)) { throw "找不到 $iscc —— 去掉 -SkipBootstrap 让脚本自己装" }
# 简体中文语言包:Inno 自带 29 种语言,没有中文,得从官方仓库单拉。
# (注意路径是 Files/Languages/ChineseSimplified.isl,不是 Unofficial/ 下面那个 ——
# 后者早就 404 了,网上抄来的大多还是老路径。)
$isl = Join-Path $innoDir 'Languages\ChineseSimplified.isl'
if (-not (Test-Path $isl) -and -not $SkipBootstrap) {
Write-Step '补简体中文语言包'
$raw = 'https://raw.githubusercontent.com/jrsoftware/issrc/refs/heads/main/Files/Languages/ChineseSimplified.isl'
foreach ($src in @("https://ghfast.top/$raw", "https://gh-proxy.com/$raw", $raw)) {
Remove-Item $isl -Force -ErrorAction SilentlyContinue
& curl.exe -L --fail --connect-timeout 20 --max-time 120 -o $isl $src 2>&1 | Out-Null
if ((Test-Path $isl) -and (Get-Item $isl).Length -gt 5KB) { break }
}
if (-not (Test-Path $isl)) {
Write-Host ' [!] 拿不到中文语言包,安装界面会退回英文' -ForegroundColor Yellow
} else {
Write-Ok ("ChineseSimplified.isl {0:N0} 字节" -f (Get-Item $isl).Length)
}
}
# ---------------------------------------------------------------------------
# 3. 先把 exe 和 dist\ 铺好
# ---------------------------------------------------------------------------
$dist = if ([System.IO.Path]::IsPathRooted($DistDir)) { $DistDir } else { Join-Path $ws $DistDir }
if (-not $SkipBuild) {
Write-Step 'release 编译 + 铺 dist\'
$relArgs = @{ OutDir = $DistDir }
if ($Toolchain) { $relArgs['Toolchain'] = $Toolchain }
& (Join-Path $PSScriptRoot 'build-release.ps1') @relArgs
if (-not (Test-Path (Join-Path $dist 'nfarms.exe'))) { throw 'build-release.ps1 没产出 dist\nfarms.exe' }
} else {
if (-not (Test-Path (Join-Path $dist 'nfarms.exe'))) { throw "dist 里没有 nfarms.exe:先不加 -SkipBuild 跑一次" }
Write-Host " 复用现有 $dist"
}
$icon = Join-Path $dist 'nfarms.ico'
if (-not (Test-Path $icon)) { throw "缺 $icon —— SetupIconFile 指不到文件 ISCC 会直接报错" }
# ---------------------------------------------------------------------------
# 4. ISCC 编译
# ---------------------------------------------------------------------------
Write-Step '编译安装包'
$outAbs = if ([System.IO.Path]::IsPathRooted($OutDir)) { $OutDir } else { Join-Path $ws $OutDir }
New-Item -ItemType Directory -Force -Path $outAbs | Out-Null
# 路径一律转成正斜杠:Inno 预处理器吃反斜杠当转义符
function To-InnoPath($p) { ($p -replace '\\', '/') }
$defines = @(
"/DAppVersion=$Version",
"/DVerInfoVersion=$verInfo",
"/DDistDir=$(To-InnoPath $dist)",
"/DRepoDir=$(To-InnoPath $ws)",
"/DOutDir=$(To-InnoPath $outAbs)",
"/DWithLibs=$(if ($WithLibs) { '1' } else { '0' })"
)
$iss = Join-Path $ws 'installer\nfarms.iss'
# ISCC 的参数用数组 + splat:拼接字符串时一旦某个值带空格,
# 参数会被拆成两个(cargo 那次已经吃过一回)。
$isccArgs = @('/Q') + $defines + @($iss)
& $iscc @isccArgs 2>&1 | ForEach-Object { " $_" }
if ($LASTEXITCODE -ne 0) { throw "ISCC 编译失败 (exit $LASTEXITCODE)" }
$setup = Join-Path $outAbs "NFarms-HUB-Setup-$Version.exe"
if (-not (Test-Path $setup)) { throw "ISCC 报成功但没找到 $setup" }
Write-Ok ("{0} {1:N1} MB" -f (Split-Path -Leaf $setup), ((Get-Item $setup).Length / 1MB))
# ---------------------------------------------------------------------------
# 5. 免安装 zip(可选)
# ---------------------------------------------------------------------------
if ($Portable) {
Write-Step '打免安装 zip'
$stage = Join-Path $env:TEMP ("nfarms-portable-" + [guid]::NewGuid().ToString('N').Substring(0, 6))
New-Item -ItemType Directory -Force -Path $stage | Out-Null
foreach ($d in 'libs\scripts', 'logs', 'libs', 'wiki', 'logs\bak', 'libs\live') {
New-Item -ItemType Directory -Force -Path (Join-Path $stage $d) | Out-Null
}
Copy-Item (Join-Path $dist 'nfarms.exe') $stage -Force
Copy-Item (Join-Path $dist 'nfarms.ico') $stage -Force -ErrorAction SilentlyContinue
Copy-Item (Join-Path $dist 'libs\scripts\*.ps1') (Join-Path $stage 'libs\scripts') -Force -ErrorAction SilentlyContinue
Copy-Item (Join-Path $ws 'README.md') $stage -Force -ErrorAction SilentlyContinue
if ($WithLibs) {
foreach ($lib in 'llama.cpp', 'stable-diffusion.cpp') {
$src = Join-Path $dist "libs\$lib"
if (Test-Path $src) {
Copy-Item $src (Join-Path $stage "libs\$lib") -Recurse -Force
}
}
}
$zip = Join-Path $outAbs "NFarms-HUB-$Version-portable.zip"
Remove-Item $zip -Force -ErrorAction SilentlyContinue
# Compress-Archive 对 500 MB 级的目录很慢,但没有额外依赖 —— 值得
Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $zip -CompressionLevel Optimal
Remove-Item $stage -Recurse -Force -ErrorAction SilentlyContinue
Write-Ok ("{0} {1:N1} MB" -f (Split-Path -Leaf $zip), ((Get-Item $zip).Length / 1MB))
}
# ---------------------------------------------------------------------------
# 6. 汇总
# ---------------------------------------------------------------------------
Write-Host ''
Write-Host '产物:' -ForegroundColor Cyan
Get-ChildItem $outAbs -File | Sort-Object Name | ForEach-Object {
$h = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.Substring(0, 16)
" {0,-40} {1,9:N2} MB sha256:{2}…" -f $_.Name, ($_.Length / 1MB), $h
}
Write-Host ''
Write-Host '安装包是**当前用户**安装(PrivilegesRequired=lowest),不需要管理员权限:' -ForegroundColor DarkGray
Write-Host ' 静默安装: NFarms-HUB-Setup-<版本>.exe /VERYSILENT /SUPPRESSMSGBOXES' -ForegroundColor DarkGray
Write-Host ' 默认目录: %LOCALAPPDATA%\Programs\NFarms HUB' -ForegroundColor DarkGray