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>
359 lines
14 KiB
PowerShell
359 lines
14 KiB
PowerShell
# 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'
|
||
|
||
# 重新載入 PATH(winget 裝完目前 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] 開始 build(target=$Target)"
|
||
Log 'ffmpeg 使用 LGPL v3 build(v2 TDD §3:BtbN 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 '找不到真實 Python(winget 可能沒裝成功,或 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 會
|
||
# 重建 PATH,Windows 的 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
|
||
}
|
||
|
||
# Go:winget 預設 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"
|
||
|
||
# wails:go 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"
|
||
|
||
# node:Makefile 的 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"
|
||
|
||
# pnpm:npm 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 key(HKLM + 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 Setup(iscc.exe),make 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,只重跑 iscc(debug iteration 用)
|
||
# 預設 → 每次 clean build(wails 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 PATH:VISIONA_FAST=1 + 前置產物齊全,跳過 vendor/payload/wails,只重跑 iscc'
|
||
$bashParts += 'make exe-only'
|
||
} else {
|
||
Log '預設 clean build:每次重做 wails + server binary + frontend embed(vendor 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 是 phony,make 成功 != 產物存在)
|
||
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
|