fix(xcframework): namespace public headers to avoid include/module.modulemap collision - #87
Merged
Alex-Wengg merged 1 commit intoAug 27, 2026
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
NemoTextProcessing.xcframeworkships its module map at the top level of each slice'sHeaders/directory:When Xcode builds a target that consumes an xcframework,
ProcessXCFrameworkcopies that slice'sHeaders/into the shared$BUILT_PRODUCTS_DIR/includedirectory. That directory is shared by every xcframework in the build, so any two xcframeworks that both ship a top-levelinclude/module.modulemapclaim the same output path and the build fails before compiling anything:This is not hypothetical. It reproduces for any app that links FluidAudio (which vendors this xcframework as a
.binaryTarget) alongsidegoogle-ai-edge/LiteRT-LM, whoseCLiteRTLM_mac.xcframeworkalso shipsHeaders/module.modulemap. Neither consumer can fix it: the colliding path is baked into the published artifacts.Why CI does not catch this
swift builddoes not useProcessXCFramework. SwiftPM points the compiler at each binary target's ownHeaders/directory in place, so per-target module maps never share an output directory and never collide. Only the Xcode build system funnels them into oneinclude/. Both halves were reproduced to confirm:binaryTargetpackage underxcodebuild→ the exact error above.swift build→Build complete!Fix
Namespace the public headers so the copied path is unique:
which lands as
include/CNemoTextProcessing/module.modulemapin$BUILT_PRODUCTS_DIRand no longer contends with another artifact's top-levelmodule.modulemap.This is source-compatible.
import CNemoTextProcessingkeeps 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 namedCNemoTextProcessing(the module name) rather thanNemoTextProcessing(the xcframework name) — naming it after the xcframework produceserror: no such module 'CNemoTextProcessing'.build-xcframework.shstill passes-headers swift/include; the nesting lives inside that directory andxcodebuild -create-xcframeworkpreserves 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/includeagainst the patched tree producesHeaders/CNemoTextProcessing/{module.modulemap,nemo_text_processing.h}.xcodebuild→** BUILD SUCCEEDED **, withinclude/CNemoTextProcessing/module.modulemapand LiteRT'sinclude/module.modulemapcoexisting.import CNemoTextProcessingcompiles and runs against the restructured artifact:nemo_normalize("two hundred")→200.nemo_text_processing.honly 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
.binaryTargetmust bump both the URL and thechecksum:— SwiftPM rejects the new zip against the old checksum. The existing workflow already emitsNemoTextProcessing.xcframework.zip.checksumalongside the zip, so the new value is published automatically.One behavioral caveat: anyone doing a bare
#include <nemo_text_processing.h>rather thanimport CNemoTextProcessingmust 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.xcframeworkhas 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.