diff --git a/.changeset/warm-garlics-move.md b/.changeset/warm-garlics-move.md new file mode 100644 index 000000000..23981d3a7 --- /dev/null +++ b/.changeset/warm-garlics-move.md @@ -0,0 +1,5 @@ +--- +"fnm": patch +--- + +windows: fix shell inference in powershell when using scoop's shims diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5d73a1555..4c449038f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -138,6 +138,7 @@ jobs: with: name: fnm-windows path: target/release + - uses: MinoruSekine/setup-scoop@v4 - uses: pnpm/action-setup@v2.4.0 with: run_install: false diff --git a/e2e/shellcode/shells/cmdEnv.ts b/e2e/shellcode/shells/cmdEnv.ts index 5edbf5ae8..8f2cc5671 100644 --- a/e2e/shellcode/shells/cmdEnv.ts +++ b/e2e/shellcode/shells/cmdEnv.ts @@ -1,6 +1,7 @@ import { ScriptLine, define } from "./types.js" type EnvConfig = { + executableName: string useOnCd: boolean logLevel: string corepackEnabled: boolean @@ -9,9 +10,15 @@ type EnvConfig = { export type HasEnv = { env(cfg: Partial): ScriptLine } function stringify(envConfig: Partial = {}) { - const { useOnCd, logLevel, corepackEnabled, resolveEngines } = envConfig + const { + useOnCd, + logLevel, + corepackEnabled, + resolveEngines, + executableName = "fnm", + } = envConfig return [ - `fnm env`, + `${executableName} env`, useOnCd && "--use-on-cd", logLevel && `--log-level=${logLevel}`, corepackEnabled && "--corepack-enabled", diff --git a/e2e/windows-scoop.test.ts b/e2e/windows-scoop.test.ts new file mode 100644 index 000000000..5b62c4857 --- /dev/null +++ b/e2e/windows-scoop.test.ts @@ -0,0 +1,33 @@ +import { script } from "./shellcode/script.js" +import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js" +import testNodeVersion from "./shellcode/test-node-version.js" +import describe from "./describe.js" +import os from "node:os" +import { execa } from "execa" + +if (os.platform() !== "win32") { + test.skip("scoop shims only work on Windows", () => {}) +} else { + beforeAll(async () => { + // Create a scoop shim for tests + await execa(`scoop`, [ + "shim", + "add", + "fnm_release", + "target/release/fnm.exe", + ]) + }) + + for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) { + describe(shell, () => { + test(`scoop shims infer the shell`, async () => { + await script(shell) + .then(shell.env({ executableName: "fnm_release" })) + .then(shell.call("fnm_release", ["install", "v20.14.0"])) + .then(shell.call("fnm_release", ["use", "v20.14.0"])) + .then(testNodeVersion(shell, "v20.14.0")) + .execute(shell) + }) + }) + } +} diff --git a/src/shell/infer/windows.rs b/src/shell/infer/windows.rs index 255ddceef..0abda183b 100644 --- a/src/shell/infer/windows.rs +++ b/src/shell/infer/windows.rs @@ -1,20 +1,32 @@ #![cfg(not(unix))] use crate::shell::Shell; -use sysinfo::System; +use log::{debug, warn}; +use sysinfo::{ProcessRefreshKind, System, UpdateKind}; pub fn infer_shell() -> Option> { let mut system = System::new(); let mut current_pid = sysinfo::get_current_pid().ok(); + system + .refresh_processes_specifics(ProcessRefreshKind::new().with_exe(UpdateKind::OnlyIfNotSet)); + while let Some(pid) = current_pid { - system.refresh_process(pid); if let Some(process) = system.process(pid) { current_pid = process.parent(); + debug!("pid {pid} parent process is {current_pid:?}"); let process_name = process .exe() - .and_then(|x| x.file_stem()) - .and_then(|x| x.to_str()) + .and_then(|x| { + tap_none(x.file_stem(), || { + warn!("failed to get file stem from {:?}", x); + }) + }) + .and_then(|x| { + tap_none(x.to_str(), || { + warn!("failed to convert file stem to string: {:?}", x); + }) + }) .map(str::to_lowercase); if let Some(shell) = process_name .as_ref() @@ -24,9 +36,22 @@ pub fn infer_shell() -> Option> { return Some(shell); } } else { + warn!("process not found for {pid}"); current_pid = None; } } None } + +fn tap_none(opt: Option, f: F) -> Option +where + F: FnOnce(), +{ + match &opt { + Some(_) => (), + None => f(), + }; + + opt +}