visionA/local-tool/scripts/bootstrap-windows.ps1
jim800121chen 61593f004d fix(build): bootstrap-windows 讓 MSYS2 bash 找得到 Go / wails / node / pnpm
Windows build 卡在 `go: command not found`,但 PowerShell 端 go 明明可用。

原因是腳本用 `bash.exe -l` 呼叫 make,login shell 會 source /etc/profile
重建 PATH,而非繼承 Windows 的。`MSYS2_PATH_TYPE=inherit` 只對 MSYS2
自己的啟動器有效,直接呼叫 bash.exe 時不生效。

腳本原本就為 Inno Setup 與 Python 手動 export 路徑,只是漏了這四個工具。
現在比照辦理:

- 新增 Find-ToolDir,先用 Get-Command(可找到自訂磁碟的安裝),
  找不到再退回已知安裝位置,都失敗則明確 Fail 並附安裝指引
- wails 路徑改問 `go env GOPATH` 而非假設 %USERPROFILE%\go,
  且用 go.exe 絕對路徑呼叫(否則在本 bug 情境下自己也會找不到 go)
- pnpm 獨立偵測,npm i -g 的安裝位置與 node 不同
- 各目錄個別加雙引號後以 : 串接,避免 Program Files 的空白被斷詞

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:30:31 +08:00

359 lines
14 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# visionA-local — Windows 10/11 x86_64 一鍵 build
#
# 使用方式PowerShell 以「系統管理員」開啟):
# git clone https://github.com/jim800121/visionA.git
# cd visionA\local-tool
# powershell -ExecutionPolicy Bypass -File scripts\bootstrap-windows.ps1
#
# 環境變數:
# $env:VISIONA_TARGET 預設 exe可設 wails-windows / payload-windows
$ErrorActionPreference = 'Stop'
$Target = if ($env:VISIONA_TARGET) { $env:VISIONA_TARGET } else { 'exe' }
function Log($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
function Fail($msg) { Write-Host "!!! $msg" -ForegroundColor Red; exit 1 }
# 必須以系統管理員身份執行winget 需要)
$isAdmin = ([Security.Principal.WindowsPrincipal] `
[Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
[Security.Principal.WindowsBuiltInRole] 'Administrator')
if (-not $isAdmin) { Fail '請以系統管理員身份執行 PowerShell 再跑此腳本' }
if (-not (Test-Path 'Makefile') -or -not (Test-Path 'visiona-local')) {
Fail '請先 cd 到 local-tool\ 目錄再執行此腳本'
}
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Fail 'winget 未安裝。請先從 Microsoft Store 安裝「App Installer」'
}
function Ensure-Winget($id) {
$installed = winget list --id $id -e 2>$null | Select-String $id
if (-not $installed) {
Log "安裝 $id"
winget install -e --id $id --accept-source-agreements --accept-package-agreements
} else {
Log "$id 已安裝,跳過"
}
}
Log '[1/4] 安裝系統套件'
Ensure-Winget 'Git.Git'
Ensure-Winget 'GoLang.Go'
Ensure-Winget 'OpenJS.NodeJS.LTS'
Ensure-Winget 'Python.Python.3.12'
Ensure-Winget 'JRSoftware.InnoSetup'
Ensure-Winget 'MSYS2.MSYS2'
# 重新載入 PATHwinget 裝完目前 session 拿不到)
$env:Path = [System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' + `
[System.Environment]::GetEnvironmentVariable('Path','User')
$env:Path += ";$HOME\go\bin;C:\msys64\usr\bin;C:\msys64\mingw64\bin"
Log '[2/4] 安裝 pnpm'
if (-not (Get-Command pnpm -ErrorAction SilentlyContinue)) {
npm i -g pnpm
}
Log '[3/4] 安裝 Wails CLI + 確認 MSYS2 make'
if (-not (Get-Command wails -ErrorAction SilentlyContinue)) {
go install github.com/wailsapp/wails/v2/cmd/wails@latest
}
wails doctor
if (-not (Test-Path 'C:\msys64\usr\bin\make.exe')) {
Log '安裝 MSYS2 make'
& 'C:\msys64\usr\bin\bash.exe' -lc 'pacman -Sy --noconfirm make'
}
Log "[4/4] 開始 buildtarget=$Target"
Log 'ffmpeg 使用 LGPL v3 buildv2 TDD §3BtbN n7.1 LGPL'
# 讓 MSYS2 bash 繼承 Windows PATH才找得到 go / pnpm / python / wails
$env:MSYS2_PATH_TYPE = 'inherit'
$env:CHERE_INVOKING = '1'
# 找真實的 Python避開 Microsoft Store 的 WindowsApps stub
$realPython = $null
$pyCandidates = @(
"$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python313\python.exe",
"$env:ProgramFiles\Python312\python.exe",
"$env:ProgramFiles\Python313\python.exe"
)
foreach ($p in $pyCandidates) {
if (Test-Path $p) { $realPython = $p; break }
}
if (-not $realPython) {
# 試 py launcher
$pyLauncher = Get-Command py -ErrorAction SilentlyContinue
if ($pyLauncher) { $realPython = 'py -3' }
}
if (-not $realPython) {
Fail '找不到真實 Pythonwinget 可能沒裝成功,或 PATH 沒更新)。請重開 PowerShell 再試一次'
}
Log "偵測到 Python: $realPython"
# 把 Windows 路徑轉成 MSYS2 bash 能吃的格式
function Convert-ToMsysPath($winPath) {
if ($winPath -match '^[A-Za-z]:\\') {
return '/' + $winPath.Substring(0,1).ToLower() + '/' + ($winPath.Substring(3) -replace '\\','/')
}
return $winPath
}
$msysPython = Convert-ToMsysPath $realPython
# ── 偵測 Go / wails / node / pnpm 的實際安裝目錄 ────────────────────
# 為什麼需要這段:
# $env:MSYS2_PATH_TYPE = 'inherit' 只影響 MSYS2 自己的 shell 啟動器
# msys2.exe / mingw64.exe直接呼叫 bash.exe -l 時 /etc/profile 會
# 重建 PATHWindows 的 PATH 不會被完整繼承 → bash 找不到 go / wails
# 出現 "/bin/bash: 列 1: go: command not found"。
# 所以必須跟 Inno Setup 一樣,明確把工具目錄轉成 MSYS2 路徑後 export。
#
# 偵測策略(都不寫死路徑,允許裝在任意磁碟 / 自訂目錄):
# 1. 先用 Get-Command 問 PowerShell 目前 PATH上面已重載 Machine+User PATH
# 2. 找不到再退回常見安裝位置winget / 官方 installer 的預設路徑)
function Find-ToolDir {
param(
[string] $Name, # 執行檔名(不含 .exe
[string[]] $Candidates # fallback 的完整 exe 路徑清單
)
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
if ($cmd -and $cmd.Source) {
return (Split-Path $cmd.Source -Parent)
}
foreach ($p in $Candidates) {
if ($p -and (Test-Path $p)) { return (Split-Path $p -Parent) }
}
return $null
}
# Gowinget 預設 C:\Program Files\Go\bin但可能被裝到其他磁碟
$goDir = Find-ToolDir -Name 'go' -Candidates @(
"$env:ProgramFiles\Go\bin\go.exe",
"${env:ProgramFiles(x86)}\Go\bin\go.exe",
"$env:LOCALAPPDATA\Programs\Go\bin\go.exe",
"C:\Go\bin\go.exe"
)
if (-not $goDir) {
Fail @'
找不到 go.exe。請確認 Go 已安裝winget install -e --id GoLang.Go
然後「重開一個新的系統管理員 PowerShell」再跑一次此腳本PATH 需要重新載入)。
'@
}
Log "偵測到 Go: $goDir"
# wailsgo install 會放到 $(go env GOPATH)\bin優先問 go 自己而不是猜 $HOME\go
# 用上面偵測到的 $goDir\go.exe 絕對路徑呼叫,不依賴 go 是否在 PowerShell PATH 上
$goPath = $null
$goExe = Join-Path $goDir 'go.exe'
if (Test-Path $goExe) {
try {
$goPath = (& $goExe env GOPATH 2>$null | Select-Object -First 1)
} catch {
$goPath = $null
}
}
if ([string]::IsNullOrWhiteSpace($goPath)) { $goPath = "$env:USERPROFILE\go" }
$wailsDir = Find-ToolDir -Name 'wails' -Candidates @(
(Join-Path $goPath 'bin\wails.exe'),
"$env:USERPROFILE\go\bin\wails.exe"
)
if (-not $wailsDir) {
Fail @"
wails.exe $goPath\bin
go install github.com/wailsapp/wails/v2/cmd/wails@latest
"@
}
Log "偵測到 wails: $wailsDir"
# nodeMakefile 的 pnpm build 需要 node 在 PATH 上
$nodeDir = Find-ToolDir -Name 'node' -Candidates @(
"$env:ProgramFiles\nodejs\node.exe",
"${env:ProgramFiles(x86)}\nodejs\node.exe",
"$env:LOCALAPPDATA\Programs\nodejs\node.exe"
)
if (-not $nodeDir) {
Fail @'
找不到 node.exe。請確認 Node.js 已安裝winget install -e --id OpenJS.NodeJS.LTS
然後重開 PowerShell 再試一次。
'@
}
Log "偵測到 node: $nodeDir"
# pnpmnpm i -g 會裝到 %APPDATA%\npm與 node 目錄不同,必須另外加)
$pnpmDir = Find-ToolDir -Name 'pnpm' -Candidates @(
"$env:APPDATA\npm\pnpm.cmd",
"$env:APPDATA\npm\pnpm",
"$env:ProgramFiles\nodejs\pnpm.cmd"
)
if (-not $pnpmDir) {
Fail @'
找不到 pnpm。請執行npm i -g pnpm
然後重開 PowerShell 再試一次。
'@
}
Log "偵測到 pnpm: $pnpmDir"
# 轉成 MSYS2 路徑去重後合併成「單一」export避免多行 export 互相覆蓋,
# 也避免同一目錄重複出現在 PATH 上,例如 pnpm 與 node 同目錄的情況)
$toolDirs = @($goDir, $wailsDir, $nodeDir, $pnpmDir) |
Where-Object { $_ } |
Select-Object -Unique
# 用 @() 強制成陣列:只有一個目錄時 pipeline 會退化成單一 string
# 後面的 .Count / -join 行為才不會有歧義
$msysToolDirs = @($toolDirs | ForEach-Object { Convert-ToMsysPath $_ })
# 找 Inno Setup Compiler (ISCC.exe)
# 重要:檔名是 ISCC.exe大寫winget 裝在 Program Files (x86) 不在 PATH 裡
function Find-Iscc {
# 1. 固定路徑system + user scope
$candidates = @(
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
"$env:ProgramFiles\Inno Setup 6\ISCC.exe",
"$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe",
"$env:USERPROFILE\AppData\Local\Programs\Inno Setup 6\ISCC.exe",
"${env:ProgramFiles(x86)}\Inno Setup 5\ISCC.exe"
)
foreach ($p in $candidates) {
if ($p -and (Test-Path $p)) { return $p }
}
# 2. 登錄檔 Uninstall keyHKLM + HKCU 都看)
$regPaths = @(
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1',
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1'
)
foreach ($rp in $regPaths) {
$item = Get-ItemProperty $rp -ErrorAction SilentlyContinue
if ($item) {
if ($item.InstallLocation) {
$p = Join-Path $item.InstallLocation 'ISCC.exe'
if (Test-Path $p) { return $p }
}
if ($item.'Inno Setup: App Path') {
$p = Join-Path $item.'Inno Setup: App Path' 'ISCC.exe'
if (Test-Path $p) { return $p }
}
}
}
# 3. 遞迴掃 Program Files + user AppData
$scanRoots = @(
"${env:ProgramFiles(x86)}",
"$env:ProgramFiles",
"$env:LOCALAPPDATA\Programs"
) | Where-Object { $_ -and (Test-Path $_) }
foreach ($root in $scanRoots) {
$found = Get-ChildItem -Path $root -Filter 'ISCC.exe' -Recurse `
-ErrorAction SilentlyContinue -Force |
Select-Object -First 1
if ($found) { return $found.FullName }
}
return $null
}
$isccPath = Find-Iscc
if (-not $isccPath) {
Log 'Inno Setup 偵測不到,用 winget 重裝一次'
winget install -e --id JRSoftware.InnoSetup --accept-source-agreements --accept-package-agreements --force
$isccPath = Find-Iscc
}
if ($isccPath) {
Log "偵測到 Inno Setup: $isccPath"
$msysIsccDir = Convert-ToMsysPath (Split-Path $isccPath -Parent)
$msysIsccExe = Convert-ToMsysPath $isccPath
} else {
Log 'WARN: 仍然找不到 Inno Setupiscc.exemake exe 步驟會失敗'
$msysIsccDir = $null
$msysIsccExe = $null
}
# Makefile 需要 bash + make透過 MSYS2 bash 執行
# 將 Windows 路徑 C:\foo\bar 轉成 MSYS2 路徑 /c/foo/bar
$projectPath = (Get-Location).Path
$msysPath = '/' + $projectPath.Substring(0,1).ToLower() + '/' + `
($projectPath.Substring(3) -replace '\\','/')
$bashParts = @(
"cd '$msysPath'",
"export VISIONA_PYTHON='$msysPython'"
)
# 把 go / wails / node / pnpm 的目錄加進 bash 的 PATH。
# 用一行 export 塞完所有目錄,前面補在 $PATH 之前(優先於 MSYS2 內建工具)。
# 每個目錄各自用雙引號包住,路徑含空白(如 "Program Files")與括號
# (如 "Program Files (x86)")才不會被 bash 拆字或當成子 shell 語法。
# 註export 是 assignment context等號右邊不做 word splitting
# 所以結尾的 :$PATH 不加引號也安全(與既有 Inno Setup 那行同慣例)。
if ($msysToolDirs -and $msysToolDirs.Count -gt 0) {
$quotedDirs = ($msysToolDirs | ForEach-Object { "`"$_`"" }) -join ':'
$bashParts += "export PATH=$quotedDirs`:`$PATH"
}
if ($msysIsccDir) {
$bashParts += "export PATH=`"$msysIsccDir`":`$PATH"
}
if ($msysIsccExe) {
$bashParts += "export ISCC=`"$msysIsccExe`""
}
# Build 模式:
# VISIONA_FAST=1 → 前置產物齊全時跳過 vendor/payload/wails只重跑 isccdebug iteration 用)
# 預設 → 每次 clean buildwails build / server binary / frontend embed 全重做)
# 保留 vendor/ 快取Python runtime / wheels / ffmpeg以免重下
$fastPath = (Test-Path 'visiona-local\build\bin\visiona-local.exe') -and `
(Test-Path 'payload\windows\bin\visiona-local-server.exe') -and `
(Test-Path 'payload\windows\bin\ffmpeg.exe') -and `
(Test-Path 'payload\windows\python\python.tar.gz')
if ($env:VISIONA_FAST -eq '1' -and $fastPath -and ($Target -eq 'exe' -or -not $env:VISIONA_TARGET)) {
Log 'FAST PATHVISIONA_FAST=1 + 前置產物齊全,跳過 vendor/payload/wails只重跑 iscc'
$bashParts += 'make exe-only'
} else {
Log '預設 clean build每次重做 wails + server binary + frontend embedvendor cache 保留)'
# 清 wails / frontend / server/web/out但不清 vendor/(避免重下 Python / wheels / ffmpeg
$bashParts += 'rm -rf visiona-local/build/bin visiona-local/build/windows/Resources'
$bashParts += 'rm -rf frontend/out frontend/.next server/web/out'
$bashParts += 'rm -rf payload/windows/bin/visiona-local-server.exe'
$bashParts += 'rm -rf dist/visiona-local-*-windows-x64.exe'
$bashParts += 'make vendor-python-windows vendor-wheels-windows vendor-ffmpeg-windows'
$bashParts += 'make payload-windows'
switch ($Target) {
'payload-windows' { }
'wails-windows' { $bashParts += 'make wails-windows' }
default { $bashParts += 'make wails-windows'; $bashParts += 'make exe' }
}
}
# 把 bash 指令寫成 script 檔再執行,避開 PS → bash 多層 quoting 地獄
# (尤其是含空格的路徑如 "C:\Program Files (x86)\Inno Setup 6\"
$bashScriptLines = @('#!/usr/bin/env bash', 'set -e', '')
foreach ($line in $bashParts) {
$bashScriptLines += $line
}
$bashScriptContent = ($bashScriptLines -join "`n") + "`n"
$tmpScript = Join-Path (Get-Location) '.visiona-build.sh'
# 用 UTF8 無 BOM + LF 換行bash 才吃得下
[System.IO.File]::WriteAllText($tmpScript, $bashScriptContent, (New-Object System.Text.UTF8Encoding $false))
Log "bash 執行腳本內容:"
Get-Content $tmpScript | ForEach-Object { Write-Host " $_" }
$msysScript = Convert-ToMsysPath $tmpScript
& 'C:\msys64\usr\bin\bash.exe' -l $msysScript
$buildRc = $LASTEXITCODE
Remove-Item $tmpScript -ErrorAction SilentlyContinue
if ($buildRc -ne 0) { Fail "build 失敗exit code=$buildRc" }
# 驗證 dist 真的有 .exe 產出exe target 是 phonymake 成功 != 產物存在)
if ($Target -eq 'exe' -or $Target -eq 'default' -or -not $Target) {
$exeFiles = Get-ChildItem dist -Filter 'visiona-local-*-windows-x64.exe' -ErrorAction SilentlyContinue
if (-not $exeFiles) {
Fail "make exe 沒報錯,但 dist\ 下沒有 visiona-local-*-windows-x64.exe。往上捲看 iscc 輸出找原因,或直接跑:
& `"$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe`" installer\windows\visiona-local.iss"
}
}
Log '完成 ✅'
Log "產出位置:$(Join-Path (Get-Location) 'dist')"
Get-ChildItem dist -ErrorAction SilentlyContinue | Format-Table Name, Length