Skip to content

fix(xcframework): namespace public headers to avoid include/module.modulemap collision - #87

Merged
Alex-Wengg merged 1 commit into
FluidInference:mainfrom
ComicBit:fix/namespace-xcframework-headers
Aug 27, 2026
Merged

fix(xcframework): namespace public headers to avoid include/module.modulemap collision#87
Alex-Wengg merged 1 commit into
FluidInference:mainfrom
ComicBit:fix/namespace-xcframework-headers

Conversation

@ComicBit

Copy link
Copy Markdown
Contributor

Problem

NemoTextProcessing.xcframework ships its module map at the top level of each slice's Headers/ directory:

NemoTextProcessing.xcframework/macos-arm64_x86_64/Headers/module.modulemap
NemoTextProcessing.xcframework/macos-arm64_x86_64/Headers/nemo_text_processing.h

When Xcode builds a target that consumes an xcframework, ProcessXCFramework copies that slice's Headers/ into the shared $BUILT_PRODUCTS_DIR/include directory. That directory is shared by every xcframework in the build, so any two xcframeworks that both ship a top-level include/module.modulemap claim the same output path and the build fails before compiling anything:

error: Multiple commands produce '.../Build/Products/Debug/include/module.modulemap'
    note: Command: ProcessXCFramework .../litert-lm/CLiteRTLM_mac.xcframework .../Debug/libCLiteRTLM_mac.dylib macos
    note: Command: ProcessXCFramework .../fluidaudio/NemoTextProcessing/NemoTextProcessing.xcframework .../Debug/libtext_processing_rs.a macos

This is not hypothetical. It reproduces for any app that links FluidAudio (which vendors this xcframework as a .binaryTarget) alongside google-ai-edge/LiteRT-LM, whose CLiteRTLM_mac.xcframework also ships Headers/module.modulemap. Neither consumer can fix it: the colliding path is baked into the published artifacts.

Why CI does not catch this

swift build does not use ProcessXCFramework. SwiftPM points the compiler at each binary target's own Headers/ directory in place, so per-target module maps never share an output directory and never collide. Only the Xcode build system funnels them into one include/. Both halves were reproduced to confirm:

  • Two-binaryTarget package under xcodebuild → the exact error above.
  • The identical package under swift buildBuild complete!

Fix

Namespace the public headers so the copied path is unique:

Headers/CNemoTextProcessing/module.modulemap
Headers/CNemoTextProcessing/nemo_text_processing.h

which lands as include/CNemoTextProcessing/module.modulemap in $BUILT_PRODUCTS_DIR and no longer contends with another artifact's top-level module.modulemap.

This is source-compatible. import CNemoTextProcessing keeps working with no flag changes, because when Clang resolves a module it also searches each header search path for a module map in a subdirectory named after the module. That is why the directory is named CNemoTextProcessing (the module name) rather than NemoTextProcessing (the xcframework name) — naming it after the xcframework produces error: no such module 'CNemoTextProcessing'.

build-xcframework.sh still passes -headers swift/include; the nesting lives inside that directory and xcodebuild -create-xcframework preserves subdirectory structure. The only script change is a comment recording why the nesting matters, so a future tidy-up does not silently reintroduce the bug.

Verification

  • xcodebuild -create-xcframework -headers swift/include against the patched tree produces Headers/CNemoTextProcessing/{module.modulemap,nemo_text_processing.h}.
  • Restructuring the published v0.3.0 artifact the same way and building the two-xcframework package under xcodebuild** BUILD SUCCEEDED **, with include/CNemoTextProcessing/module.modulemap and LiteRT's include/module.modulemap coexisting.
  • A SwiftPM target doing import CNemoTextProcessing compiles and runs against the restructured artifact: nemo_normalize("two hundred")200.
  • Header contents are unchanged; nemo_text_processing.h only includes <stdint.h>, so no include paths break.

Consumer impact

This changes the layout of the published artifact, so it needs a new tagged release. Consumers pinning the .binaryTarget must bump both the URL and the checksum: — SwiftPM rejects the new zip against the old checksum. The existing workflow already emits NemoTextProcessing.xcframework.zip.checksum alongside the zip, so the new value is published automatically.

One behavioral caveat: anyone doing a bare #include <nemo_text_processing.h> rather than import CNemoTextProcessing must switch to #include <CNemoTextProcessing/nemo_text_processing.h>. No known consumer does this — FluidAudio's two call sites (ITN/TextNormalizer.swift, TTS/Shared/NemoTextNormalizer.swift) both use the Swift module import.

Note for LiteRT-LM

CLiteRTLM_mac.xcframework has the same latent packaging issue and would collide with any other xcframework shipping a top-level module map. Fixing either side unblocks the current pairing; fixing both is the durable answer.

…dulemap collision

Ship the module map under Headers/CNemoTextProcessing/ instead of at the top
level of each slice. Xcode's ProcessXCFramework copies every xcframework's
Headers/ into the shared $BUILT_PRODUCTS_DIR/include, so two artifacts that
both ship a top-level module.modulemap claim the same output path and the
build fails before compiling anything.

The directory is named after the MODULE (CNemoTextProcessing), not the
xcframework, because that is the subdirectory Clang searches when resolving a
module — naming it after the xcframework yields 'no such module'.

swift build does not use ProcessXCFramework, which is why CI never saw this.
@Alex-Wengg
Alex-Wengg merged commit d54aa1f into FluidInference:main Aug 27, 2026
8 checks passed
@ComicBit
ComicBit deleted the fix/namespace-xcframework-headers branch August 30, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants