From 40483d4576400e37827303046d0306f4e98c51fc Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Tue, 10 Mar 2026 09:41:33 -0400 Subject: [PATCH 1/4] test: add issue #1527 repro for global uninstall --- e2e/repros/issue-1527.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 e2e/repros/issue-1527.test.ts diff --git a/e2e/repros/issue-1527.test.ts b/e2e/repros/issue-1527.test.ts new file mode 100644 index 000000000..6ecbedf1b --- /dev/null +++ b/e2e/repros/issue-1527.test.ts @@ -0,0 +1,24 @@ +import { script } from "../shellcode/script.js" +import { Bash, Fish, PowerShell, Zsh } from "../shellcode/shells.js" +import describe from "../describe.js" + +for (const shell of [Bash, Zsh, Fish, PowerShell]) { + describe(shell, () => { + test(`issue #1527: global package remains available after uninstall on older node`, async () => { + await script(shell) + .then(shell.env({})) + .then(shell.call("fnm", ["install", "16"])) + .then(shell.call("fnm", ["install", "18"])) + .then(shell.call("fnm", ["use", "16"])) + .then(shell.call("npm", ["install", "-g", "cowsay"])) + .then(shell.call("fnm", ["use", "18"])) + .then(shell.call("npm", ["install", "-g", "cowsay"])) + .then(shell.call("fnm", ["use", "16"])) + .then(shell.call("npm", ["uninstall", "-g", "cowsay"])) + .then( + shell.scriptOutputContains(shell.call("cowsay", ["hello"]), "hello"), + ) + .execute(shell) + }) + }) +} From c0f324795e0820b2f29e13d94f1772466cee26ee Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Tue, 10 Mar 2026 09:42:48 -0400 Subject: [PATCH 2/4] test: assert issue #1527 does not reproduce --- e2e/repros/issue-1527.test.ts | 39 +++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/e2e/repros/issue-1527.test.ts b/e2e/repros/issue-1527.test.ts index 6ecbedf1b..7f76bb001 100644 --- a/e2e/repros/issue-1527.test.ts +++ b/e2e/repros/issue-1527.test.ts @@ -1,10 +1,42 @@ +import { writeFile } from "node:fs/promises" +import path from "node:path" import { script } from "../shellcode/script.js" import { Bash, Fish, PowerShell, Zsh } from "../shellcode/shells.js" import describe from "../describe.js" +import testCwd from "../shellcode/test-cwd.js" + +const findCowsayInPath = ` +const fs = require("node:fs") +const path = require("node:path") + +const pathEntries = (process.env.PATH || "").split(path.delimiter) +const candidates = process.platform === "win32" + ? ["cowsay.cmd", "cowsay.exe", "cowsay"] + : ["cowsay"] + +let found = false +for (const entry of pathEntries) { + for (const candidate of candidates) { + if (fs.existsSync(path.join(entry, candidate))) { + found = true + break + } + } + + if (found) { + break + } +} + +console.log(found ? "found" : "missing") +` for (const shell of [Bash, Zsh, Fish, PowerShell]) { describe(shell, () => { - test(`issue #1527: global package remains available after uninstall on older node`, async () => { + test(`issue #1527: cowsay is not in PATH after uninstall on node 16`, async () => { + const checkPathScript = path.join(testCwd(), "check-cowsay-in-path.js") + await writeFile(checkPathScript, findCowsayInPath) + await script(shell) .then(shell.env({})) .then(shell.call("fnm", ["install", "16"])) @@ -16,7 +48,10 @@ for (const shell of [Bash, Zsh, Fish, PowerShell]) { .then(shell.call("fnm", ["use", "16"])) .then(shell.call("npm", ["uninstall", "-g", "cowsay"])) .then( - shell.scriptOutputContains(shell.call("cowsay", ["hello"]), "hello"), + shell.scriptOutputContains( + shell.call("node", ["check-cowsay-in-path.js"]), + "missing", + ), ) .execute(shell) }) From c5c4001bdc2785ba326a2fc96bdc0b6ee5114e79 Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Mon, 9 Mar 2026 15:53:05 -0400 Subject: [PATCH 3/4] Pin shell in installer-generated env setup (#1525) * perf(install): pin shell in generated env setup * ci: pin docker platform for ARM installer tests * chore: add changeset for installer shell update --- .changeset/cuddly-cars-ring.md | 5 +++++ .ci/install.sh | 6 +++--- .github/workflows/installation_script.yml | 22 ++++++++++++---------- README.md | 5 ++++- docs/commands.md | 2 +- src/cli.rs | 4 ++-- 6 files changed, 27 insertions(+), 17 deletions(-) create mode 100644 .changeset/cuddly-cars-ring.md diff --git a/.changeset/cuddly-cars-ring.md b/.changeset/cuddly-cars-ring.md new file mode 100644 index 000000000..cea89e6ff --- /dev/null +++ b/.changeset/cuddly-cars-ring.md @@ -0,0 +1,5 @@ +--- +"fnm": patch +--- + +prefer explicit shell flags in installer-generated shell setup and fix ARM installer CI platform selection diff --git a/.ci/install.sh b/.ci/install.sh index e92a2905e..9a9647748 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -177,7 +177,7 @@ setup_shell() { if [ "$USE_HOMEBREW" != "true" ]; then echo ' export PATH="$FNM_PATH:$PATH"' fi - echo ' eval "`fnm env`"' + echo ' eval "$(fnm env --shell zsh)"' echo 'fi' } | tee -a "$CONF_FILE" @@ -193,7 +193,7 @@ setup_shell() { if [ "$USE_HOMEBREW" != "true" ]; then echo ' set PATH "$FNM_PATH" $PATH' fi - echo ' fnm env | source' + echo ' fnm env --shell fish | source' echo 'end' } | tee -a "$CONF_FILE" @@ -213,7 +213,7 @@ setup_shell() { if [ "$USE_HOMEBREW" != "true" ]; then echo ' export PATH="$FNM_PATH:$PATH"' fi - echo ' eval "`fnm env`"' + echo ' eval "$(fnm env --shell bash)"' echo 'fi' } | tee -a "$CONF_FILE" diff --git a/.github/workflows/installation_script.yml b/.github/workflows/installation_script.yml index c8a41ba66..d795c3380 100644 --- a/.github/workflows/installation_script.yml +++ b/.github/workflows/installation_script.yml @@ -1,5 +1,5 @@ name: Installation script -on: +on: pull_request: paths: - .ci/install.sh @@ -13,9 +13,11 @@ jobs: test_against_latest_release_arm: strategy: matrix: - docker_image: - - arm64v8/ubuntu - - arm32v7/ubuntu + include: + - docker_image: arm64v8/ubuntu + docker_platform: linux/arm64/v8 + - docker_image: arm32v7/ubuntu + docker_platform: linux/arm/v7 name: Test against latest release (ARM) runs-on: ubuntu-latest steps: @@ -25,7 +27,7 @@ jobs: - uses: actions/checkout@v4 - name: Run installation script in Docker run: | - docker run --rm -v $(pwd):$(pwd) -e "RUST_LOG=fnm=debug" --workdir $(pwd) ${{matrix.docker_image}} bash -c ' + docker run --rm --platform ${{ matrix.docker_platform }} -v $(pwd):$(pwd) -e "RUST_LOG=fnm=debug" --workdir $(pwd) ${{matrix.docker_image}} bash -c ' set -e apt update && apt install -y unzip curl libatomic1 @@ -53,13 +55,13 @@ jobs: strategy: matrix: shell: [fish, zsh, bash] - setup: + setup: - os: ubuntu - script_arguments: '' + script_arguments: "" - os: macos - script_arguments: '' + script_arguments: "" - os: macos - script_arguments: '--force-no-brew' + script_arguments: "--force-no-brew" runs-on: ${{ matrix.setup.os }}-latest steps: - uses: actions/checkout@v4 @@ -84,4 +86,4 @@ jobs: - run: "env SHELL=$(which ${{ matrix.shell }}) bash ./.ci/install.sh ${{ matrix.setup.script_arguments }}" name: Run the installation script - run: ./.ci/test_installation_script.sh ${{ matrix.shell }} - name: 'Test installation script' + name: "Test installation script" diff --git a/README.md b/README.md index eda8d797a..f5dbf9320 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,9 @@ Please follow your shell instructions to install them. Environment variables need to be setup before you can start using fnm. This is done by evaluating the output of `fnm env`. +> [!TIP] +> Prefer passing an explicit shell (for example `fnm env --shell bash`) instead of relying on shell inference at runtime. It is a bit faster and avoids process tree detection. + > [!NOTE] > Check out the [Configuration](./docs/configuration.md) section to enable highly > recommended features, like automatic version switching. @@ -197,7 +200,7 @@ fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression #### Windows Command Prompt aka Batch aka WinCMD -fnm is also supported but is not entirely covered. You can set up a startup script for [cmd.exe]( https://superuser.com/a/144348) or [Windows Terminal](https://superuser.com/a/1855283) and append the following lines: +fnm is also supported but is not entirely covered. You can set up a startup script for [cmd.exe](https://superuser.com/a/144348) or [Windows Terminal](https://superuser.com/a/1855283) and append the following lines: ```batch @echo off diff --git a/docs/commands.md b/docs/commands.md index c5340a5e5..3e2b4855c 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -380,7 +380,7 @@ Print and set up required environment variables for fnm This command generates a series of shell commands that should be evaluated by your shell to create a fnm-ready environment. -Each shell has its own syntax of evaluating a dynamic expression. For example, evaluating fnm on Bash and Zsh would look like `eval "$(fnm env)"`. In Fish, evaluating would look like `fnm env | source` +Each shell has its own syntax of evaluating a dynamic expression. For example, evaluating fnm on Bash and Zsh would look like `eval "$(fnm env --shell bash)"`. In Fish, evaluating would look like `fnm env --shell fish | source` Usage: fnm env [OPTIONS] diff --git a/src/cli.rs b/src/cli.rs index ec626005f..e670ff4aa 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -27,8 +27,8 @@ pub enum SubCommand { /// should be evaluated by your shell to create a fnm-ready environment. /// /// Each shell has its own syntax of evaluating a dynamic expression. - /// For example, evaluating fnm on Bash and Zsh would look like `eval "$(fnm env)"`. - /// In Fish, evaluating would look like `fnm env | source` + /// For example, evaluating fnm on Bash and Zsh would look like `eval "$(fnm env --shell bash)"`. + /// In Fish, evaluating would look like `fnm env --shell fish | source` #[clap(name = "env", bin_name = "env")] Env(commands::env::Env), From 50b7baf83d52ebe300bf3758033c9f7fee03041e Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Tue, 10 Mar 2026 09:47:05 -0400 Subject: [PATCH 4/4] test: verify cowsay exists before uninstall in issue #1527 repro --- e2e/repros/issue-1527.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/e2e/repros/issue-1527.test.ts b/e2e/repros/issue-1527.test.ts index 7f76bb001..80da639fe 100644 --- a/e2e/repros/issue-1527.test.ts +++ b/e2e/repros/issue-1527.test.ts @@ -46,6 +46,12 @@ for (const shell of [Bash, Zsh, Fish, PowerShell]) { .then(shell.call("fnm", ["use", "18"])) .then(shell.call("npm", ["install", "-g", "cowsay"])) .then(shell.call("fnm", ["use", "16"])) + .then( + shell.scriptOutputContains( + shell.call("node", ["check-cowsay-in-path.js"]), + "found", + ), + ) .then(shell.call("npm", ["uninstall", "-g", "cowsay"])) .then( shell.scriptOutputContains(