Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/init-rustup-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"playground-cli": patch
---

Fix `dot init` failing on a clean macOS/Linux machine where `rustup` isn't yet installed. The rustup installer writes its binaries to `~/.cargo/bin` and adds that directory to PATH only via shell rc files, which doesn't reach the running `dot` process. The next two steps (`Rust nightly`, `rust-src`) then fail with `bash: rustup: command not found`. After installing rustup we now prepend `$CARGO_HOME/bin` (default `~/.cargo/bin`) to `process.env.PATH` so the rest of `dot init` can resolve `rustup` immediately.
39 changes: 39 additions & 0 deletions src/utils/toolchain.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { prependPath } from "./toolchain.js";

describe("prependPath", () => {
let originalPath: string | undefined;

beforeEach(() => {
originalPath = process.env.PATH;
});

afterEach(() => {
if (originalPath === undefined) delete process.env.PATH;
else process.env.PATH = originalPath;
});

it("prepends the directory when not already present", () => {
process.env.PATH = "/usr/bin:/bin";
prependPath("/Users/me/.cargo/bin");
expect(process.env.PATH).toBe("/Users/me/.cargo/bin:/usr/bin:/bin");
});

it("is a no-op when the directory is already on PATH", () => {
process.env.PATH = "/Users/me/.cargo/bin:/usr/bin";
prependPath("/Users/me/.cargo/bin");
expect(process.env.PATH).toBe("/Users/me/.cargo/bin:/usr/bin");
});

it("handles an empty PATH", () => {
process.env.PATH = "";
prependPath("/Users/me/.cargo/bin");
expect(process.env.PATH).toBe("/Users/me/.cargo/bin");
});

it("handles an unset PATH", () => {
delete process.env.PATH;
prependPath("/Users/me/.cargo/bin");
expect(process.env.PATH).toBe("/Users/me/.cargo/bin");
});
});
23 changes: 20 additions & 3 deletions src/utils/toolchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ function run(cmd: string, opts?: { shell?: string }): Promise<string> {
});
}

/**
* Prepend `dir` to `process.env.PATH` if not already present. Lets a step that
* just installed a binary expose it to the rest of `dot init` without waiting
* for a shell restart.
*/
export function prependPath(dir: string): void {
const segments = (process.env.PATH ?? "").split(":").filter(Boolean);
if (segments.includes(dir)) return;
process.env.PATH = process.env.PATH ? `${dir}:${process.env.PATH}` : dir;
}

export async function commandExists(cmd: string): Promise<boolean> {
if (!/^[a-zA-Z0-9_-]+$/.test(cmd)) {
throw new Error(`Invalid command name: ${cmd}`);
Expand Down Expand Up @@ -80,11 +91,17 @@ export const TOOL_STEPS: ToolStep[] = [
{
name: "rustup",
check: () => commandExists("rustup"),
install: (onData) =>
runPiped(
install: async (onData) => {
await runPiped(
'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y',
onData,
),
);
// rustup-init writes binaries to $CARGO_HOME/bin (default ~/.cargo/bin)
// and updates shell rc files, but those edits don't reach the running
// dot process. Prepend the bin dir so the very next step in this same
// `dot init` can resolve `rustup`.
prependPath(resolve(process.env.CARGO_HOME ?? `${homedir()}/.cargo`, "bin"));
},
manualHint: "https://rustup.rs",
},
{
Expand Down
Loading