From 8720023e963b9e51ba6dcc69d3ecb86563e856dd Mon Sep 17 00:00:00 2001 From: xxxigm Date: Sat, 6 Jun 2026 22:21:36 +0700 Subject: [PATCH] fix(bootstrap-installer): resolve powershell.exe by absolute path on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native Windows installer spawned PowerShell via the bare program name `powershell.exe`, which trusts PATH to contain %SystemRoot%\System32\WindowsPowerShell\v1.0. On machines whose PATH was trimmed or truncated (Windows silently drops entries once the variable exceeds its length limit), the lookup fails and the spawn dies with "program not found" before install.ps1 runs at all — the installer then stalls at "0 of 0 steps". Resolve PowerShell by absolute path first (%SystemRoot%/%windir%), then fall back to PATH (powershell 5.1, then pwsh 7), then a bare name as a last resort. Also include the resolved interpreter in the spawn-failure context; the old message printed only the script path, which misleadingly read as if the .ps1 itself was missing. --- .../src-tauri/src/powershell.rs | 59 ++++++++++++++++++- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/apps/bootstrap-installer/src-tauri/src/powershell.rs b/apps/bootstrap-installer/src-tauri/src/powershell.rs index b24e2695943..3fc187c070b 100644 --- a/apps/bootstrap-installer/src-tauri/src/powershell.rs +++ b/apps/bootstrap-installer/src-tauri/src/powershell.rs @@ -72,7 +72,7 @@ pub async fn run_script( let mut child: Child = cmd .spawn() - .with_context(|| format!("spawning {}", script_path.display()))?; + .with_context(|| format!("spawning {} via {}", script_path.display(), interpreter_label()))?; let stdout = child.stdout.take().expect("stdout was piped"); let stderr = child.stderr.take().expect("stderr was piped"); @@ -177,8 +177,9 @@ async fn recv_cancel(rx: &mut Option) { fn build_command(script_path: &Path, args: &[String]) -> Command { // We want PowerShell 5.1 / 7. install.ps1 uses 5.1-safe syntax everywhere. // Prefer `powershell.exe` (5.1 baseline, present on every Windows since 7) - // over `pwsh.exe` (7+, may not be present). - let mut cmd = Command::new("powershell.exe"); + // over `pwsh.exe` (7+, may not be present). Resolve it by absolute path — + // see `windows_powershell_exe`. + let mut cmd = Command::new(windows_powershell_exe()); cmd.arg("-NoProfile"); cmd.arg("-ExecutionPolicy").arg("Bypass"); cmd.arg("-File").arg(script_path); @@ -200,6 +201,58 @@ fn build_command(script_path: &Path, args: &[String]) -> Command { cmd } +/// Canonical PowerShell 5.1 location under a Windows root (`%SystemRoot%`). +#[cfg(target_os = "windows")] +fn powershell_under_root(root: &Path) -> std::path::PathBuf { + root.join("System32") + .join("WindowsPowerShell") + .join("v1.0") + .join("powershell.exe") +} + +/// Resolves the PowerShell interpreter to spawn. +/// +/// `Command::new("powershell.exe")` trusts PATH to contain +/// `%SystemRoot%\System32\WindowsPowerShell\v1.0`. On machines whose PATH was +/// trimmed or truncated (Windows silently drops entries once the variable grows +/// past its length limit), that lookup fails and the spawn dies with +/// "program not found" before install.ps1 ever runs — the installer then stalls +/// at "0 of 0 steps". Resolve by absolute path first, then fall back to PATH +/// (powershell 5.1, then pwsh 7), then a bare name as a last resort. +#[cfg(target_os = "windows")] +fn windows_powershell_exe() -> std::path::PathBuf { + for var in ["SystemRoot", "windir"] { + if let Ok(root) = std::env::var(var) { + let candidate = powershell_under_root(Path::new(&root)); + if candidate.is_file() { + return candidate; + } + } + } + + for exe in ["powershell.exe", "pwsh.exe"] { + if let Ok(found) = which::which(exe) { + return found; + } + } + + std::path::PathBuf::from("powershell.exe") +} + +/// Human-readable interpreter name for spawn-failure context. On Windows this +/// is the resolved PowerShell path so a missing/odd interpreter is obvious in +/// the log (the old message only printed the script path, which read as if the +/// .ps1 itself was missing). +#[cfg(target_os = "windows")] +fn interpreter_label() -> String { + windows_powershell_exe().display().to_string() +} + +#[cfg(not(target_os = "windows"))] +fn interpreter_label() -> String { + "bash".to_string() +} + /// Parses the LAST line of stdout that looks like a JSON object matching /// the install.ps1 stage-result contract: `{ok: bool, stage: string, ...}`. ///