From f02df45a4693433c268081c30c5f1e66a5c13eea Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 26 Oct 2026 22:42:39 +0800 Subject: [PATCH] fix(process): preserve selected Darwin helper environment through execution --- src/process/DarwinProcessRunTokenReader.ts | 12 ++- src/process/ProcessOwnershipObservation.ts | 3 +- .../darwinProcessRunTokenReader.test.ts | 80 ++++++++++++++++++- 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/src/process/DarwinProcessRunTokenReader.ts b/src/process/DarwinProcessRunTokenReader.ts index bd047077a7..f01d4fc2ae 100644 --- a/src/process/DarwinProcessRunTokenReader.ts +++ b/src/process/DarwinProcessRunTokenReader.ts @@ -19,6 +19,7 @@ import type { import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js"; import { safeParseJson } from "../domain/safeJson.js"; import { boundedUtf8Text } from "./boundedUtf8Text.js"; +import { snapshotEnvironment } from "./snapshotEnvironment.js"; import { execFileOutput, execFileOutputFailure, @@ -407,7 +408,7 @@ export const createDarwinProcessRunTokenReader = ( readonly compiler?: DarwinProcessReaderCompiler; /** Deadline for helper compilation, independent of provider execution. */ readonly compileTimeoutMs?: number; - /** Environment consulted when `compileTimeoutMs` is omitted. */ + /** Captured compiler/helper environment and default compile deadline. */ readonly environment?: Readonly>; /** * Caller-owned directory holding the compiler's module cache. Unset keeps @@ -421,7 +422,11 @@ export const createDarwinProcessRunTokenReader = ( kind: "xcrun", executable: "/usr/bin/xcrun", }; - const compilerTimeoutMs = resolveSwiftcCompileTimeoutMs(options); + const environment = snapshotEnvironment(options.environment ?? process.env); + const compilerTimeoutMs = resolveSwiftcCompileTimeoutMs({ + ...options, + environment, + }); const moduleCachePath = options.moduleCachePath ?? ""; let root: ProcessReaderRoot | undefined; let executable: string | undefined; @@ -496,7 +501,7 @@ export const createDarwinProcessRunTokenReader = ( const compilerTemporary = join(operationRoot.path, "tmp"); await mkdir(compilerTemporary); const compilerEnvironment: NodeJS.ProcessEnv = { - ...process.env, + ...environment, TMPDIR: compilerTemporary, }; const compilerArguments = @@ -640,6 +645,7 @@ export const createDarwinProcessRunTokenReader = ( const pending = (async () => { const binary = await compile(signal); const { stdout } = await execFileOutput(binary, arguments_, { + env: environment, timeout: 15_000, stopSignal: "SIGTERM", ...(signal === undefined ? {} : { signal }), diff --git a/src/process/ProcessOwnershipObservation.ts b/src/process/ProcessOwnershipObservation.ts index a50e161e36..d0180ae6fe 100644 --- a/src/process/ProcessOwnershipObservation.ts +++ b/src/process/ProcessOwnershipObservation.ts @@ -981,12 +981,13 @@ export const createSystemProcessOwnershipHost = ( const darwinTokens = platform === "darwin" ? createDarwinProcessRunTokenReader({ + environment: hostEnvironment, ...(darwinCompiler === undefined ? {} : { compiler: darwinCompiler }), ...(darwinModuleCachePath === undefined ? {} : { moduleCachePath: darwinModuleCachePath }), ...(options.compileTimeoutMs === undefined - ? { environment: hostEnvironment } + ? {} : { compileTimeoutMs: options.compileTimeoutMs }), }) : undefined; diff --git a/tests/boundary/process/darwinProcessRunTokenReader.test.ts b/tests/boundary/process/darwinProcessRunTokenReader.test.ts index dcdddc3afa..f3c5ccbd97 100644 --- a/tests/boundary/process/darwinProcessRunTokenReader.test.ts +++ b/tests/boundary/process/darwinProcessRunTokenReader.test.ts @@ -15,7 +15,7 @@ import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { _electron as electron } from "playwright-core"; -import { expect, it, vi } from "vitest"; +import { expect, it, onTestFinished, vi } from "vitest"; import { z } from "zod"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -457,6 +457,84 @@ const existsPath = async (target: string) => () => false, ); +it + .skipIf(process.platform === "win32") + .each([ + "selected environment", + "selected environment and deadline", + "default environment", + ] as const)( + "captures the compiler and helper environment from %s", + async (selection) => { + const directory = await createTestTempDirectory( + "rea-process-token-selected-environment-", + ); + const developerDirectory = join(directory, "selected Xcode.app"); + const compileState = join(directory, "compile-environment.json"); + const executable = join(directory, "fixture-xcrun"); + const helper = `#!${process.execPath} +console.log(JSON.stringify({ results: process.argv.slice(2).map(pid => ({ + pid: Number(pid), state: "readable", run_id: process.env.DEVELOPER_DIR, +})) })); +`; + await writeFile( + executable, + `#!${process.execPath} +const fs = process.getBuiltinModule("node:fs"); +fs.writeFileSync(${JSON.stringify(compileState)}, JSON.stringify({ + developerDirectory: process.env.DEVELOPER_DIR ?? null, +})); +const output = process.argv[process.argv.indexOf("-o") + 1]; +fs.writeFileSync(output, ${JSON.stringify(helper)}); +fs.chmodSync(output, 0o755); +`, + { mode: 0o755 }, + ); + onTestFinished(() => { + vi.unstubAllEnvs(); + }); + vi.stubEnv("DEVELOPER_DIR", developerDirectory); + const environment = { ...process.env }; + const compiler = { kind: "xcrun" as const, executable }; + const reader = + selection === "default environment" + ? createDarwinProcessRunTokenReader({ compiler }) + : createSystemProcessOwnershipHost("darwin", environment, { + darwinCompiler: compiler, + ...(selection === "selected environment and deadline" + ? { compileTimeoutMs: 15_000 } + : {}), + }); + environment.DEVELOPER_DIR = join(directory, "later caller selection"); + vi.stubEnv("DEVELOPER_DIR", join(directory, "later ambient selection")); + try { + await reader.prepare?.(AbortSignal.timeout(15_000)); + expect(JSON.parse(await readFile(compileState, "utf8"))).toEqual({ + developerDirectory, + }); + const processes = [ + { + pid: 123, + parentPid: 1, + processGroupId: 123, + state: "S", + command: "fixture", + }, + ]; + const observations = + "read" in reader + ? await reader.read(processes) + : await reader.runTokens?.(processes); + expect(observations?.get(123)).toEqual({ + state: "readable", + runId: developerDirectory, + }); + } finally { + await reader.close?.(); + } + }, +); + it.skipIf(process.platform === "win32")( "forwards a caller-owned module cache and keeps it after the helper root is removed", async () => {