Framework Bundle Structure#
Tuist controls how the Modules/ directory is laid out inside .framework bundles through its graph mapping pipeline β specifically ModuleMapMapper, a GraphMapping pass inside TuistGenerator.
Canonical Path: Top-Level Modules/, Not Versioned#
When a framework target has a MODULEMAP_FILE build setting, ModuleMapMapper injects a post-build script that writes the module map to the top-level Modules/ directory of the bundle :
$TARGET_BUILD_DIR/$WRAPPER_NAME/Modules/module.modulemap
There is no Versions/A/Modules/ hierarchy. This is a deliberate design choice: Tuist mirrors the convention used by swift-build (SWBTaskConstruction), which models framework module maps at Modules/module.modulemap for its ExtractAPI pass . Aligning with this canonical path ensures that generated Xcode projects are compatible with ExtractAPI and related Apple toolchain workflows.
The script uses mkdir -p + cp -f rather than a plain copy because Xcode's compilation-caching (CAS) can materialise the destination as a read-only file; cp -f forces an overwrite .
Why the Modules/ Directory Is Stripped During Embedding#
The embed-frameworks script generated by Tuist explicitly excludes Modules/ when rsync-ing frameworks into the app bundle . This prevents non-portable paths (which may be absolute paths from the developer's machine) from leaking into the distributed product. If a module map is needed at runtime, it must be re-added by the framework consumer's build system, not carried inside the embedded copy.
When the Post-Build Script Is Added#
The "Copy Module Map" script is added only when both conditions hold :
- The target's
MODULEMAP_FILEsetting resolves to a path. - The target's product type is
.framework(not.staticFramework, not.xcframework).
Static frameworks (.staticFramework) are handled differently: rather than embedding a module map into a bundle, the mapper propagates -fmodule-map-file flags transitively to consuming targets.
XCFramework Bundles#
XCFrameworks are loaded and processed separately from regular frameworks. Their Info.plist is parsed to extract metadata including moduleMaps and swiftModules . For XCFrameworks that contain static libraries, the StaticXCFrameworkModuleMapGraphMapper handles additional rewriting β it distinguishes between flat layouts (headers next to the module map, passed via -fmodule-map-file) and nested layouts (headers in a Headers/<ModuleName>/ subdirectory, where clang discovers the module map automatically via header search paths) .
Key Source Files#
| File | Role |
|---|---|
ModuleMapMapper.swift | Injects the "Copy Module Map" post-build script; owns top-level Modules/ path logic |
EmbedScriptGenerator.swift | Generates the embed-frameworks bash script; excludes Modules/ via rsync filter |
Product.swift | Defines .framework, .staticFramework, and isDynamic/isStatic predicates |
Module.swift (ProjectDescriptionHelpers) | Enumerates all internal tuist modules and their product types β the project-level registry that drives which targets become .framework vs other product types |