| author | |
| committer | |
| log | 8b1ea4dd2465f3cf3cfa02d6878beaa1ec22e71a |
| tree | fcf23e7d3312c86c7d6cd848f38e117b3369c8ac |
| parent | 9375c779707fb95c06670db6da87441720b2d2e2 |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 90 insertions(+), 16 deletions(-)
apps/desktop/src/backend/DesktopBackendConfiguration.test.ts+3-3| ... | ... | @@ -399,10 +399,10 @@ describe("DesktopBackendConfiguration", () => { |
| 399 | 399 | observedProbeRoots.push(root); |
| 400 | 400 | return { ok: true, resolvedPath }; |
| 401 | 401 | }, |
| 402 | // The staged runtime carries its own Node, so the preflight must not | |
| 403 | // go looking for one in the distro. | |
| 402 | // The staged runtime carries its own Node and node-pty, so it must | |
| 403 | // not require the mounted server tree's native dependency check. | |
| 404 | 404 | ensureNodePty: () => { |
| 405 | throw new Error("the staged runtime must not probe for Node"); | |
| 405 | throw new Error("the staged runtime must not probe for node-pty"); | |
| 406 | 406 | }, |
| 407 | 407 | }), |
| 408 | 408 | }, |
apps/desktop/src/backend/DesktopBackendConfiguration.ts+2-2| ... | ... | @@ -384,8 +384,8 @@ const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(f |
| 384 | 384 | if (input.runtimeArchive !== null) { |
| 385 | 385 | const runtime = yield* wslEnv.prepareRuntime(runningDistro, input.runtimeArchive); |
| 386 | 386 | if (runtime.ok) { |
| 387 | // The staged runtime is self-contained, so the only question is whether | |
| 388 | // it runs here; there is no Node to find or node-pty to load. | |
| 387 | // The staged runtime supplies its own Node and node-pty. Provider PATH | |
| 388 | // discovery must not require either dependency for runtime readiness. | |
| 389 | 389 | const stagedProbe = yield* wslEnv.probeRuntime(runningDistro, runtime.linuxAppRoot); |
| 390 | 390 | if (stagedProbe.ok) { |
| 391 | 391 | yield* wslServerTree.cleanupLegacy; |
apps/desktop/src/wsl/DesktopWslEnvironment.test.ts+70| ... | ... | @@ -15,6 +15,7 @@ import { |
| 15 | 15 | buildWslRuntimeInstallScript, |
| 16 | 16 | buildWslRuntimeInvalidateScript, |
| 17 | 17 | buildWslRuntimePruneScript, |
| 18 | buildWslRuntimeProbeScript, | |
| 18 | 19 | DesktopWslDistroListError, |
| 19 | 20 | formatMissingToolsReason, |
| 20 | 21 | parseNodePath, |
| ... | ... | @@ -446,6 +447,75 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed |
| 446 | 447 | }; |
| 447 | 448 | }; |
| 448 | 449 | |
| 450 | const probeFixture = (fixture: ReturnType<typeof createFixture>) => | |
| 451 | runShell( | |
| 452 | [ | |
| 453 | `export HOME=${sh(`${fixture.work}/home`)}`, | |
| 454 | 'export NVM_DIR="$HOME/.nvm" FNM_DIR="$HOME/.fnm" VOLTA_HOME="$HOME/.volta"', | |
| 455 | // Isolate login profiles and hide the host's Node/version managers. | |
| 456 | // The resolver must discover the fixture's installation itself. | |
| 457 | "bash() { (", | |
| 458 | " command() {", | |
| 459 | ' case "$*" in', | |
| 460 | ' "-v node"|"-v mise"|"-v fnm"|"-v nodenv") return 1 ;;', | |
| 461 | ' *) builtin command "$@" ;;', | |
| 462 | " esac", | |
| 463 | " }", | |
| 464 | ' eval "$2"', | |
| 465 | "); }", | |
| 466 | buildWslRuntimeProbeScript(fixture.runtimeRoot), | |
| 467 | ].join("\n"), | |
| 468 | ); | |
| 469 | ||
| 470 | it("discovers version-managed Node for providers with a standalone runtime", () => { | |
| 471 | const fixture = createFixture(); | |
| 472 | expect(fixture.install().status).toBe(0); | |
| 473 | const nodeBin = `${fixture.work}/home/.nvm/versions/node/v24.15.0/bin`; | |
| 474 | const setup = runShell( | |
| 475 | [ | |
| 476 | "set -eu", | |
| 477 | `mkdir -p ${sh(nodeBin)}`, | |
| 478 | `printf '%s' ${sh('#!/bin/sh\nprintf "linux-node-provider\\n"\n')} > ${sh(`${nodeBin}/node`)}`, | |
| 479 | `chmod +x ${sh(`${nodeBin}/node`)}`, | |
| 480 | ].join("\n"), | |
| 481 | ); | |
| 482 | expect(setup.status, setup.stderr).toBe(0); | |
| 483 | ||
| 484 | const probe = probeFixture(fixture); | |
| 485 | ||
| 486 | expect(probe.status, probe.stderr).toBe(0); | |
| 487 | const resolvedPath = parseResolvedPath(probe.stdout); | |
| 488 | expect(resolvedPath?.split(":")).toContain(nodeBin); | |
| 489 | const provider = runShell(`export PATH=${sh(resolvedPath ?? "")}\nnode provider.js`); | |
| 490 | expect(provider.status, provider.stderr).toBe(0); | |
| 491 | expect(provider.stdout).toBe("linux-node-provider\n"); | |
| 492 | }); | |
| 493 | ||
| 494 | it("keeps standalone runtime readiness independent of Node availability", () => { | |
| 495 | const fixture = createFixture(); | |
| 496 | expect(fixture.install().status).toBe(0); | |
| 497 | ||
| 498 | const probe = probeFixture(fixture); | |
| 499 | ||
| 500 | expect(probe.status, probe.stderr).toBe(0); | |
| 501 | expect(parseResolvedPath(probe.stdout)).not.toBeNull(); | |
| 502 | }); | |
| 503 | ||
| 504 | it("keeps the inherited PATH when bash is unavailable", () => { | |
| 505 | const fixture = createFixture(); | |
| 506 | expect(fixture.install().status).toBe(0); | |
| 507 | const probe = runShell( | |
| 508 | [ | |
| 509 | "bash() { return 127; }", | |
| 510 | 'export PATH="/fixture/bin:/usr/bin:/bin"', | |
| 511 | buildWslRuntimeProbeScript(fixture.runtimeRoot), | |
| 512 | ].join("\n"), | |
| 513 | ); | |
| 514 | ||
| 515 | expect(probe.status, probe.stderr).toBe(0); | |
| 516 | expect(parseResolvedPath(probe.stdout)).toBe("/fixture/bin:/usr/bin:/bin"); | |
| 517 | }); | |
| 518 | ||
| 449 | 519 | it("reuses a warm cache without touching the archive", () => { |
| 450 | 520 | const fixture = createFixture(); |
| 451 | 521 | expect(fixture.install().status).toBe(0); |
apps/desktop/src/wsl/DesktopWslEnvironment.ts+15-11| ... | ... | @@ -122,8 +122,8 @@ export class DesktopWslEnvironment extends Context.Service< |
| 122 | 122 | // Marks a staged runtime as unusable so the next launch reinstalls it. |
| 123 | 123 | readonly invalidateRuntime: (distro: string | null, runtimeId: string) => Effect.Effect<void>; |
| 124 | 124 | // Proves a staged self-contained runtime can run (`<root>/t3 --version`) |
| 125 | // and captures the user's login-shell PATH for the launch. Needs no Node | |
| 126 | // in the distro; the mounted server tree still goes through ensureNodePty. | |
| 125 | // and resolves the user's PATH, including version-managed Node for provider | |
| 126 | // CLIs. Node is optional; the mounted tree still requires ensureNodePty. | |
| 127 | 127 | readonly probeRuntime: ( |
| 128 | 128 | distro: string | null, |
| 129 | 129 | linuxAppRoot: string, |
| ... | ... | @@ -545,13 +545,12 @@ require("node-pty"); |
| 545 | 545 | NODE`; |
| 546 | 546 | |
| 547 | 547 | // Readiness proof for a staged self-contained runtime: the executable runs and |
| 548 | // reports its version, and the login shell's PATH is captured for the launch. | |
| 549 | // This runs under plain `sh` (no Node resolver preamble, since the runtime | |
| 550 | // needs no Node), so the login shell is entered explicitly for the PATH | |
| 551 | // capture; a distro without bash falls back to the PATH sh was started with. | |
| 552 | const RUNTIME_PROBE_SCRIPT = (linuxAppRoot: string) => | |
| 548 | // reports its version. Provider CLIs may still need version-managed Node, so | |
| 549 | // resolve it before capturing PATH without requiring it for runtime readiness. | |
| 550 | // A distro without bash falls back to the PATH sh was started with. | |
| 551 | export const buildWslRuntimeProbeScript = (linuxAppRoot: string) => | |
| 553 | 552 | [ |
| 554 | `bash -lc ${shellQuote(RESOLVED_PATH_LINE)} 2>/dev/null || ${RESOLVED_PATH_LINE}`, | |
| 553 | `bash -lc ${shellQuote(`${buildWslNodeEnvPreamble()}${RESOLVED_PATH_LINE}`)} 2>/dev/null || ${RESOLVED_PATH_LINE}`, | |
| 555 | 554 | `${shellQuote(`${linuxAppRoot}/t3`)} --version >/dev/null 2>&1`, |
| 556 | 555 | ].join("\n"); |
| 557 | 556 | |
| ... | ... | @@ -676,9 +675,14 @@ const probeWslRuntimeImpl = ( |
| 676 | 675 | linuxAppRoot: string, |
| 677 | 676 | ): Effect.Effect<ProbeWslRuntimeResult, never, ChildProcessSpawner.ChildProcessSpawner> => |
| 678 | 677 | Effect.gen(function* () { |
| 679 | const probe = yield* runWslShell(distro, RUNTIME_PROBE_SCRIPT(linuxAppRoot), PROBE_TIMEOUT, { | |
| 680 | resolveNode: false, | |
| 681 | }); | |
| 678 | const probe = yield* runWslShell( | |
| 679 | distro, | |
| 680 | buildWslRuntimeProbeScript(linuxAppRoot), | |
| 681 | PROBE_TIMEOUT, | |
| 682 | { | |
| 683 | resolveNode: false, | |
| 684 | }, | |
| 685 | ); | |
| 682 | 686 | const transportFailureReason = formatWslShellTransportFailureReason( |
| 683 | 687 | probe.transportFailure, |
| 684 | 688 | "the staged runtime", |