Summary
Several CommandRunner.run call sites in the SwiftPM-driven graph-load path relied on the default workingDirectory resolution, which reads FileManager.default.currentDirectoryPath. When getcwd fails inside the running process (cwd deleted, unsearchable, transient state under heavy concurrent Process setup), Foundation returns an empty string and the underlying AbsolutePath validation traps via try! in tuist/Command — surfacing as a Trace/BPT trap: 5 with no stack in user reports.
These callers fire concurrently per Swift package (hundreds of Task.detached in large workspaces), which maximizes exposure to the failure mode. Pass the package or workspace directory as workingDirectory explicitly. The commands already take --package-path / -workspace arguments, so this does not change executed behavior — it only removes the implicit dependency on currentDirectoryPath.
Touched call sites:
cli/Sources/XcodeGraph/Sources/XcodeGraphMapper/Utilities/PackageInfoLoader.swift — swift package dump-package, fires concurrently per SPM dep during XcodeGraph mapping
cli/Sources/TuistLoader/Loaders/PackageInfoLoader.swift — swift package invocations for resolve/update/tools-version/dump-package, plus the swift build + lipo chain in buildFatReleaseBinary
cli/Sources/TuistGenerator/Utils/SwiftPackageManagerInteractor.swift — xcodebuild -resolvePackageDependencies
Scope: defensive, not causal
This is a defensive fix. It does not identify or fix the underlying reason currentDirectoryPath returns empty. Read on for the reasoning.
A reporter hit the trap during tuist generate with Trace/BPT trap: 5 and three identical Command/CommandRunner.swift:155: 'try!' expression unexpectedly raised an error: invalid absolute path '' lines. Notable observations:
- Idle
getcwd and FileManager.default.currentDirectoryPath returned the correct path. The empty string only appeared transiently inside the running Tuist process.
TUIST_CONFIG_TOKEN= tuist generate succeeded in ~22s. With the token set, it crashed.
- Running
tuist auth login also made the crash stop reproducing.
Project tokens skip the auth-refresh codepath entirely (tokenStatus returns .valid(.project) immediately, no refresh). So the auth-refresh code itself is not the trigger. What auth state controls is whether downstream binary-cache substitution runs. With a valid token, the cache layer spawns its own concurrent Process work on top of the already-concurrent SPM Task.detached storm. Without a token, that work short-circuits and the extra concurrency disappears.
The evidence points to a race or transient state under heavy concurrent Process setup that makes getcwd (and therefore Foundation’s currentDirectoryPath) return empty briefly. The crash surfaces at whichever CommandRunner.run call happens to be running at that moment — which, given call volume, is overwhelmingly the SPM callers patched here.
After this PR, those callers no longer touch currentDirectoryPath, so the visible crash for that user’s reproducer should disappear. But the underlying race condition is unchanged, and any other CommandRunner.run caller without an explicit workingDirectory could still trigger the same trap under similar conditions.
Companion change: tuist/Command#262 replaces the try! with proper error propagation. With both PRs landed, the next reproducer surfaces a regular Swift error pointing at the actual call site, which is what is needed to find the real root cause.
Test plan
-
swift build --replace-scm-with-registry --target TuistLoader succeeds
-
swift build --replace-scm-with-registry --target TuistGenerator succeeds
-
swift build --replace-scm-with-registry --target XcodeGraphMapper succeeds
- Existing unit tests still pass on CI (
MockCommandRunner matches on argument strings only, which are unchanged)