| 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,10 +399,10 @@ describe("DesktopBackendConfiguration", () => { |
| 399 | observedProbeRoots.push(root); | 399 | observedProbeRoots.push(root); |
| 400 | return { ok: true, resolvedPath }; | 400 | return { ok: true, resolvedPath }; |
| 401 | }, | 401 | }, |
| 402 | // The staged runtime carries its own Node, so the preflight must not | 402 | // The staged runtime carries its own Node and node-pty, so it must |
| 403 | // go looking for one in the distro. | 403 | // not require the mounted server tree's native dependency check. |
| 404 | ensureNodePty: () => { | 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,8 +384,8 @@ const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(f |
| 384 | if (input.runtimeArchive !== null) { | 384 | if (input.runtimeArchive !== null) { |
| 385 | const runtime = yield* wslEnv.prepareRuntime(runningDistro, input.runtimeArchive); | 385 | const runtime = yield* wslEnv.prepareRuntime(runningDistro, input.runtimeArchive); |
| 386 | if (runtime.ok) { | 386 | if (runtime.ok) { |
| 387 | // The staged runtime is self-contained, so the only question is whether | 387 | // The staged runtime supplies its own Node and node-pty. Provider PATH |
| 388 | // it runs here; there is no Node to find or node-pty to load. | 388 | // discovery must not require either dependency for runtime readiness. |
| 389 | const stagedProbe = yield* wslEnv.probeRuntime(runningDistro, runtime.linuxAppRoot); | 389 | const stagedProbe = yield* wslEnv.probeRuntime(runningDistro, runtime.linuxAppRoot); |
| 390 | if (stagedProbe.ok) { | 390 | if (stagedProbe.ok) { |
| 391 | yield* wslServerTree.cleanupLegacy; | 391 | yield* wslServerTree.cleanupLegacy; |
apps/desktop/src/wsl/DesktopWslEnvironment.test.ts+70| ... | @@ -15,6 +15,7 @@ import { | ... | @@ -15,6 +15,7 @@ import { |
| 15 | buildWslRuntimeInstallScript, | 15 | buildWslRuntimeInstallScript, |
| 16 | buildWslRuntimeInvalidateScript, | 16 | buildWslRuntimeInvalidateScript, |
| 17 | buildWslRuntimePruneScript, | 17 | buildWslRuntimePruneScript, |
| 18 | buildWslRuntimeProbeScript, | ||
| 18 | DesktopWslDistroListError, | 19 | DesktopWslDistroListError, |
| 19 | formatMissingToolsReason, | 20 | formatMissingToolsReason, |
| 20 | parseNodePath, | 21 | parseNodePath, |
| ... | @@ -446,6 +447,75 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed | ... | @@ -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 | it("reuses a warm cache without touching the archive", () => { | 519 | it("reuses a warm cache without touching the archive", () => { |
| 450 | const fixture = createFixture(); | 520 | const fixture = createFixture(); |
| 451 | expect(fixture.install().status).toBe(0); | 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,8 +122,8 @@ export class DesktopWslEnvironment extends Context.Service< |
| 122 | // Marks a staged runtime as unusable so the next launch reinstalls it. | 122 | // Marks a staged runtime as unusable so the next launch reinstalls it. |
| 123 | readonly invalidateRuntime: (distro: string | null, runtimeId: string) => Effect.Effect<void>; | 123 | readonly invalidateRuntime: (distro: string | null, runtimeId: string) => Effect.Effect<void>; |
| 124 | // Proves a staged self-contained runtime can run (`<root>/t3 --version`) | 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 | 125 | // and resolves the user's PATH, including version-managed Node for provider |
| 126 | // in the distro; the mounted server tree still goes through ensureNodePty. | 126 | // CLIs. Node is optional; the mounted tree still requires ensureNodePty. |
| 127 | readonly probeRuntime: ( | 127 | readonly probeRuntime: ( |
| 128 | distro: string | null, | 128 | distro: string | null, |
| 129 | linuxAppRoot: string, | 129 | linuxAppRoot: string, |
| ... | @@ -545,13 +545,12 @@ require("node-pty"); | ... | @@ -545,13 +545,12 @@ require("node-pty"); |
| 545 | NODE`; | 545 | NODE`; |
| 546 | 546 | ||
| 547 | // Readiness proof for a staged self-contained runtime: the executable runs and | 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. | 548 | // reports its version. Provider CLIs may still need version-managed Node, so |
| 549 | // This runs under plain `sh` (no Node resolver preamble, since the runtime | 549 | // resolve it before capturing PATH without requiring it for runtime readiness. |
| 550 | // needs no Node), so the login shell is entered explicitly for the PATH | 550 | // A distro without bash falls back to the PATH sh was started with. |
| 551 | // capture; a distro without bash falls back to the PATH sh was started with. | 551 | export const buildWslRuntimeProbeScript = (linuxAppRoot: string) => |
| 552 | const RUNTIME_PROBE_SCRIPT = (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 | `${shellQuote(`${linuxAppRoot}/t3`)} --version >/dev/null 2>&1`, | 554 | `${shellQuote(`${linuxAppRoot}/t3`)} --version >/dev/null 2>&1`, |
| 556 | ].join("\n"); | 555 | ].join("\n"); |
| 557 | 556 | ||
| ... | @@ -676,9 +675,14 @@ const probeWslRuntimeImpl = ( | ... | @@ -676,9 +675,14 @@ const probeWslRuntimeImpl = ( |
| 676 | linuxAppRoot: string, | 675 | linuxAppRoot: string, |
| 677 | ): Effect.Effect<ProbeWslRuntimeResult, never, ChildProcessSpawner.ChildProcessSpawner> => | 676 | ): Effect.Effect<ProbeWslRuntimeResult, never, ChildProcessSpawner.ChildProcessSpawner> => |
| 678 | Effect.gen(function* () { | 677 | Effect.gen(function* () { |
| 679 | const probe = yield* runWslShell(distro, RUNTIME_PROBE_SCRIPT(linuxAppRoot), PROBE_TIMEOUT, { | 678 | const probe = yield* runWslShell( |
| 680 | resolveNode: false, | 679 | distro, |
| 681 | }); | 680 | buildWslRuntimeProbeScript(linuxAppRoot), |
| 681 | PROBE_TIMEOUT, | ||
| 682 | { | ||
| 683 | resolveNode: false, | ||
| 684 | }, | ||
| 685 | ); | ||
| 682 | const transportFailureReason = formatWslShellTransportFailureReason( | 686 | const transportFailureReason = formatWslShellTransportFailureReason( |
| 683 | probe.transportFailure, | 687 | probe.transportFailure, |
| 684 | "the staged runtime", | 688 | "the staged runtime", |