Hive Hive
Sign in

fix(cli): pass explicit working directory to manifest evaluation

GitHub issue · Closed

Metadata
Source
tuist/tuist #10996
Updated
Jun 24, 2026
Domains
CLI
Details

What changed

Manifest evaluation now passes an explicit working directory to the subprocess that compiles and runs Tuist manifests, instead of letting the command runner resolve it from getcwd.

  • ManifestLoader.loadDataForManifest (cli/Sources/TuistLoader/Loaders/ManifestLoader.swift) resolves the working directory via Environment.current.currentWorkingDirectory() and passes it as workingDirectory: to commandRunner.capture.
  • Environment.currentWorkingDirectory() (cli/Sources/TuistEnvironment/Environment.swift) falls back to the shell-provided PWD when the filesystem backend can’t resolve the working directory, so a transient getcwd failure no longer aborts the command when a valid PWD is present.

Why

A customer hit this on CI (CircleCI macOS) after upgrading, running tuist graph -f legacyJSON --no-open (and -f json) via fastlane:

✖ Error
Couldn't resolve the current working directory: FileManager returned an empty path (getcwd likely failed).

An earlier version failed harder with Command/CommandRunner.swift:155: Fatal error: 'try!' ... invalid absolute path ''. Both are the same condition: the Command package’s CommandRunner reads FileManager.default.currentDirectoryPath to determine the subprocess working directory, gets an empty string (because getcwd fails — the process’s working directory was deleted/replaced by an earlier CI step), and aborts.

Root cause

Confirmed via the customer’s version bisection: 4.191.5 good, 4.193.0 broken — which rules out the filesystem-backend switch (4.190.0) and points at the 4.192.x line.

Before #10628 (first shipped in 4.192.0), manifests were evaluated with System.shared.capture (TSCBasic), which launches the subprocess inheriting the parent’s working directory at the OS level — it never resolves getcwd into a validated path, so a dangling working directory didn’t matter. #10628 switched manifest evaluation to the Command package’s commandRunner.capture without passing a workingDirectory.

CommandRunner has two getcwd sites:

  1. run() resolves FileManager.default.currentDirectoryPath only when workingDirectory == nil, and throws invalidWorkingDirectory("") on an empty result — this is the fatal path that produces the error above.
  2. lookupExecutable() reads getcwd only when the executable argument is not an absolute path.

So the regression hits every manifest-evaluating command on CI environments where getcwd fails. (The tuist/Command change in 0.14.4 / 4.192.1 that moved the runner onto a detached task is in the same window and points at the same site; note, though, that the working directory is a per-process attribute on macOS/POSIX, so the thread the runner executes on does not change whether getcwd succeeds — the empty result reflects a genuinely unresolvable cwd, not thread context.)

(#10891 in 4.195.7 already added explicit working directories to swift package commands, but the manifest-evaluation path was still affected — which is why the customer still saw the failure on 4.195.9.)

Why this approach, and why it’s complete

Passing an explicit workingDirectory restores the exact pre-#10628 behavior and fixes the shared path for all manifest-evaluating commands (graph, generate, dump, config/plugin/package loading), not just graph. Resolving it through Environment.current.currentWorkingDirectory() routes the lookup through the new PWD fallback, so even when getcwd itself fails we still hand the subprocess a valid directory (the common case on CI, where PWD remains set).

The fix removes the manifest path’s dependence on CommandRunner resolving getcwd entirely:

  • The fatal site (run(), site 1 above) is skipped because workingDirectory is no longer nil.
  • The executable lookup (site 2) is never reached because arguments[0] is /usr/bin/xcrun (ManifestLoader.swift), an absolute path, so lookupExecutable returns early without touching getcwd.

This is the inverse of the #10966 situation: that PR fixed a case where a non-cwd directory (a package checkout) was wrongly passed as the working directory; here we pass the actual current working directory — the same directory the subprocess already ran in before #10628 — so there is no behavior change when the cwd resolves normally.

The PWD fallback in Environment is also a general safety net: any in-process caller of currentWorkingDirectory() (e.g. GraphCommand) stops aborting when getcwd transiently fails.

Impact

tuist graph and other manifest-evaluating commands no longer abort with “Couldn’t resolve the current working directory” when getcwd fails but PWD is valid. No behavior change when the working directory resolves normally.

Validation

  • Version bisection (customer): 4.191.5 good, 4.193.0 broken; #10628 first shipped in 4.192.0 and matches the window.
  • Read CommandRunner (Command 0.14.5): confirmed the two getcwd sites above and that the manifest path hits neither after the fix (explicit workingDirectory + absolute /usr/bin/xcrun executable).
  • Confirmed the pre-#10628 implementation used System.shared.capture and the post-#10628 implementation called commandRunner.capture without a workingDirectory.
  • Local SwiftPM build: Build of target: 'TuistKit' complete! (only pre-existing @retroactive warnings).
Comments
TA
tuist-atlas[bot] May 30, 2026

The fix for passing an explicit working directory to manifest evaluation is now available in 4.195.11. Update to this version to resolve manifest loading failures when getcwd fails but PWD is valid.