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
6 changes: 3 additions & 3 deletions apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,10 @@ describe("DesktopBackendConfiguration", () => {
observedProbeRoots.push(root);
return { ok: true, resolvedPath };
},
// The staged runtime carries its own Node, so the preflight must not
// go looking for one in the distro.
// The staged runtime carries its own Node and node-pty, so it must
// not require the mounted server tree's native dependency check.
ensureNodePty: () => {
throw new Error("the staged runtime must not probe for Node");
throw new Error("the staged runtime must not probe for node-pty");
},
}),
},
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,8 @@ const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(f
if (input.runtimeArchive !== null) {
const runtime = yield* wslEnv.prepareRuntime(runningDistro, input.runtimeArchive);
if (runtime.ok) {
// The staged runtime is self-contained, so the only question is whether
// it runs here; there is no Node to find or node-pty to load.
// The staged runtime supplies its own Node and node-pty. Provider PATH
// discovery must not require either dependency for runtime readiness.
const stagedProbe = yield* wslEnv.probeRuntime(runningDistro, runtime.linuxAppRoot);
if (stagedProbe.ok) {
yield* wslServerTree.cleanupLegacy;
Expand Down
70 changes: 70 additions & 0 deletions apps/desktop/src/wsl/DesktopWslEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
buildWslRuntimeInstallScript,
buildWslRuntimeInvalidateScript,
buildWslRuntimePruneScript,
buildWslRuntimeProbeScript,
DesktopWslDistroListError,
formatMissingToolsReason,
parseNodePath,
Expand Down Expand Up @@ -446,6 +447,75 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed
};
};

const probeFixture = (fixture: ReturnType<typeof createFixture>) =>
runShell(
[
`export HOME=${sh(`${fixture.work}/home`)}`,
'export NVM_DIR="$HOME/.nvm" FNM_DIR="$HOME/.fnm" VOLTA_HOME="$HOME/.volta"',
// Isolate login profiles and hide the host's Node/version managers.
// The resolver must discover the fixture's installation itself.
"bash() { (",
" command() {",
' case "$*" in',
' "-v node"|"-v mise"|"-v fnm"|"-v nodenv") return 1 ;;',
' *) builtin command "$@" ;;',
" esac",
" }",
' eval "$2"',
"); }",
buildWslRuntimeProbeScript(fixture.runtimeRoot),
].join("\n"),
);

it("discovers version-managed Node for providers with a standalone runtime", () => {
const fixture = createFixture();
expect(fixture.install().status).toBe(0);
const nodeBin = `${fixture.work}/home/.nvm/versions/node/v24.15.0/bin`;
const setup = runShell(
[
"set -eu",
`mkdir -p ${sh(nodeBin)}`,
`printf '%s' ${sh('#!/bin/sh\nprintf "linux-node-provider\\n"\n')} > ${sh(`${nodeBin}/node`)}`,
`chmod +x ${sh(`${nodeBin}/node`)}`,
].join("\n"),
);
expect(setup.status, setup.stderr).toBe(0);

const probe = probeFixture(fixture);

expect(probe.status, probe.stderr).toBe(0);
const resolvedPath = parseResolvedPath(probe.stdout);
expect(resolvedPath?.split(":")).toContain(nodeBin);
const provider = runShell(`export PATH=${sh(resolvedPath ?? "")}\nnode provider.js`);
expect(provider.status, provider.stderr).toBe(0);
expect(provider.stdout).toBe("linux-node-provider\n");
});

it("keeps standalone runtime readiness independent of Node availability", () => {
const fixture = createFixture();
expect(fixture.install().status).toBe(0);

const probe = probeFixture(fixture);

expect(probe.status, probe.stderr).toBe(0);
expect(parseResolvedPath(probe.stdout)).not.toBeNull();
});

it("keeps the inherited PATH when bash is unavailable", () => {
const fixture = createFixture();
expect(fixture.install().status).toBe(0);
const probe = runShell(
[
"bash() { return 127; }",
'export PATH="/fixture/bin:/usr/bin:/bin"',
buildWslRuntimeProbeScript(fixture.runtimeRoot),
].join("\n"),
);

expect(probe.status, probe.stderr).toBe(0);
expect(parseResolvedPath(probe.stdout)).toBe("/fixture/bin:/usr/bin:/bin");
});

it("reuses a warm cache without touching the archive", () => {
const fixture = createFixture();
expect(fixture.install().status).toBe(0);
Expand Down
26 changes: 15 additions & 11 deletions apps/desktop/src/wsl/DesktopWslEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ export class DesktopWslEnvironment extends Context.Service<
// Marks a staged runtime as unusable so the next launch reinstalls it.
readonly invalidateRuntime: (distro: string | null, runtimeId: string) => Effect.Effect<void>;
// Proves a staged self-contained runtime can run (`<root>/t3 --version`)
// and captures the user's login-shell PATH for the launch. Needs no Node
// in the distro; the mounted server tree still goes through ensureNodePty.
// and resolves the user's PATH, including version-managed Node for provider
// CLIs. Node is optional; the mounted tree still requires ensureNodePty.
readonly probeRuntime: (
distro: string | null,
linuxAppRoot: string,
Expand Down Expand Up @@ -545,13 +545,12 @@ require("node-pty");
NODE`;

// Readiness proof for a staged self-contained runtime: the executable runs and
// reports its version, and the login shell's PATH is captured for the launch.
// This runs under plain `sh` (no Node resolver preamble, since the runtime
// needs no Node), so the login shell is entered explicitly for the PATH
// capture; a distro without bash falls back to the PATH sh was started with.
const RUNTIME_PROBE_SCRIPT = (linuxAppRoot: string) =>
// reports its version. Provider CLIs may still need version-managed Node, so
// resolve it before capturing PATH without requiring it for runtime readiness.
// A distro without bash falls back to the PATH sh was started with.
export const buildWslRuntimeProbeScript = (linuxAppRoot: string) =>
[
`bash -lc ${shellQuote(RESOLVED_PATH_LINE)} 2>/dev/null || ${RESOLVED_PATH_LINE}`,
`bash -lc ${shellQuote(`${buildWslNodeEnvPreamble()}${RESOLVED_PATH_LINE}`)} 2>/dev/null || ${RESOLVED_PATH_LINE}`,
Comment thread
akj marked this conversation as resolved.
`${shellQuote(`${linuxAppRoot}/t3`)} --version >/dev/null 2>&1`,
].join("\n");

Expand Down Expand Up @@ -676,9 +675,14 @@ const probeWslRuntimeImpl = (
linuxAppRoot: string,
): Effect.Effect<ProbeWslRuntimeResult, never, ChildProcessSpawner.ChildProcessSpawner> =>
Effect.gen(function* () {
const probe = yield* runWslShell(distro, RUNTIME_PROBE_SCRIPT(linuxAppRoot), PROBE_TIMEOUT, {
resolveNode: false,
});
const probe = yield* runWslShell(
distro,
buildWslRuntimeProbeScript(linuxAppRoot),
PROBE_TIMEOUT,
{
resolveNode: false,
},
);
const transportFailureReason = formatWslShellTransportFailureReason(
probe.transportFailure,
"the staged runtime",
Expand Down
Loading