Summary
- Teach
PackageInfoMapper to recognize SE-0482 staticLibrary .artifactbundle artifacts instead of treating every binary artifact path as an .xcframework.
- Materialize a lightweight XCFramework-compatible wrapper for Apple static-library variants under SwiftPM scratch derived state:
tuist-derived/XCFrameworks/<package>/<target>-<source-fingerprint>.xcframework.
- Symlink static archives, headers, and module maps from the derived wrapper back to the immutable artifact bundle contents, and emit the XCFramework plist keys Xcode expects.
Fixes #11322.
Why
swift-hf-api ships its Rust backend as an SE-0482 staticLibrary artifact bundle. SwiftPM resolves that artifact correctly, but Tuist previously passed the resolved .artifactbundle path into the existing XCFramework path, which made generation fail before the project could be produced.
This matters for packages that adopt static library artifact bundles instead of wrapping their Apple binaries as XCFrameworks. They should be usable from Tuist projects without package authors having to republish binaries in a Tuist-specific layout.
Root Cause
Tuist’s SwiftPM mapping code assumed binary artifact paths resolved from the workspace state were XCFrameworks. That was true for the older binary target shape, but SE-0482 adds artifact bundles whose info.json can describe static libraries for multiple triples.
When one of those artifact bundle paths reached XCFrameworkMetadataProvider, Tuist looked for Info.plist inside the .artifactbundle and failed with the reported error because the artifact bundle uses info.json instead.
Solution
The mapper now detects static-library artifact bundles by reading info.json, filters their variants down to Apple triples, maps those triples to XCFramework library metadata, and returns a generated .xcframework path to the rest of the graph.
The generated wrapper is intentionally only a compatibility layer for Xcode and the existing Tuist graph model. Xcode still expects a physical .xcframework root with LibraryIdentifier directories and relative LibraryPath entries, so modeling this purely as arbitrary external paths would bypass the XCFramework contract.
The wrapper is treated as Tuist derived state rather than mutating SwiftPM-owned package or artifact folders. Production mapping passes the SwiftPM scratch-derived location into the mapper, and test-only fallback infers that same location for artifacts under .../artifacts/.... The wrapper name includes a source fingerprint derived from the artifact bundle path and info.json contents, so artifact updates for the same package and target do not reuse stale generated wrappers. To avoid duplicating potentially large static archives, the wrapper symlinks the archive, headers, and module map back to the artifact bundle contents.
I also updated XCFrameworkInfoPlist encoding to include the plist fields produced by xcodebuild -create-xcframework. Without those top-level keys, a minimal plist can be decoded by Tuist but rejected by Xcode’s ProcessXCFramework step.
User Impact
Projects depending on packages like swift-hf-api can run tuist generate successfully when the package contains Apple-compatible static library artifact bundle variants.
Existing XCFramework binary targets keep using the previous flow. Non-Apple artifact variants are ignored because they cannot be represented in an Xcode project.
Validation
swift build --replace-scm-with-registry --target TuistLoader
xcodebuild test -workspace Tuist.xcworkspace -scheme Tuist-Workspace -only-testing TuistLoaderTests/PackageInfoMapperTests -only-testing TuistLoaderTests/SwiftPackageManagerGraphLoaderTests CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" COMPILATION_CACHE_ENABLE_CACHING=NO
xcodebuild -scheme Probe -destination 'generic/platform=macOS' -derivedDataPath DerivedData ARCHS=arm64 ONLY_ACTIVE_ARCH=YES build on a throwaway Swift package whose XCFramework wrapper symlinked its library and headers to an artifact-bundle-like layout
mise run cli:lint --fix
git diff --check