Purpose
Reported in the Tuist Community Slack: a user wired up foreignBuild() for a KMP module, but editing the Kotlin sources didn’t rebuild the framework — the iOS app kept linking the stale xcframework.
The root cause is a silent failure in input expansion. When a .glob or .folder input resolves to zero files at generation time (most commonly because .relativeToRoot("../...") aims at the wrong path), the Xcode shell script build phase is written with an empty inputPaths. Combined with an existing outputPaths xcframework, Xcode’s dependency analysis decides the script is up-to-date forever — it runs once on the first build, then never again. Source changes on the non-Xcode side are dropped on the floor without any feedback.
Reproduced statically by inspecting the generated project.pbxproj against a fixture whose .glob aims at a non-existent path:
inputPaths = (
);
outputPaths = (
../../../../myAwesomeKmpProject/build/SharedKMP.xcframework,
);
No alwaysOutOfDate, no warning, no way for the user to notice.
Fix
- Warn when a
.glob input matches zero files (in the manifest mapper).
- Warn when a
.folder input expands to no files (in ForeignBuildGraphMapper).
- When the final
inputPaths for a foreign build script is empty, set basedOnDependencyAnalysis = false, which the build-phase generator translates to alwaysOutOfDate = 1 in the pbxproj. The script will then re-run on every build instead of caching the stale output indefinitely.
- Document the failure mode on
ForeignBuild.Input so it’s discoverable from autocomplete.
When inputs do resolve correctly, behavior is unchanged: inputPaths is populated, alwaysOutOfDate is not set, and Xcode does its usual incremental analysis.
How to test locally
- Set up a fixture where the foreign build’s
.glob / .folder input resolves to no files (e.g. a wrong .. count in .relativeToRoot):
.foreignBuild(
name: "SharedKMP",
destinations: .iOS,
script: "...",
inputs: [.glob(.relativeToRoot("does-not-exist/**/*.kt"))],
output: .xcframework(path: ..., linking: .dynamic)
)
tuist generate — observe the warning:
Foreign build glob input '.../does-not-exist/**/*.kt' matched no files. ...
-
Inspect the generated project.pbxproj and confirm the script phase has alwaysOutOfDate = 1.
-
Build, then build again without changing anything — the script should re-run (whereas before this fix it would have been silently skipped).
-
Run ForeignBuildGraphMapperTests for unit coverage.