Repository navigation
Support destinationFilters for synced folders - #1647
anandghegde wants to merge 5 commits into
Conversation
yonaskolb
left a comment
There was a problem hiding this comment.
Thanks @anandghegde.
The approach matches what Xcode reads. Built with Xcode 27.0 for iOS Simulator and macOS, files and resources filtered with destinationFilters and inferDestinationFiltersByPath only compile/copy on their platform, asset catalogs are filtered correctly, excluded files stay out of the filters, and output is deterministic. Expanding folders to individual files is also necessary, as Xcode ignores filter keys on plain folders. A few cases still need handling:
1. Explicit folders aren't filtered
The walk in SourceGenerator.syncedFolderPlatformFilters recurses into folders listed in explicitFolders and writes keys for the files inside them (e.g. Bundle/data.txt). Xcode treats an explicit folder as a single item, so those keys are ignored and the folder is still copied on every platform. With destinationFilters: [iOS] and explicitFolders: [Bundle], the macOS build still copies Bundle/data.txt. A key for the folder itself (Bundle = (ios, )) is honoured. Suggested fix: match the source's explicitFolders during the walk, emit the folder's relative path as the key and don't recurse into it.
2. Localized files aren't filtered
Files in *.lproj folders get keys like en.lproj/Localizable.strings and de.lproj/Localizable.strings, which Xcode ignores, so both languages are still copied on the filtered-out platform. Xcode's key for a localized file is the variant group form, "/Localized: Localizable.strings", and a single such key filters all languages correctly. #1633 uses the same /Localized: form for membership exceptions, so a shared helper that converts a synced relative path to Xcode's key (collapsing *.lproj/X to /Localized: X and de-duplicating languages) and is used for both membershipExceptions and platformFiltersByRelativePath would keep the two consistent. Without it, the .filter { !exceptions.contains($0.key) } de-duplication in configureMembershipExceptions will also stop matching for localized files once #1633 lands.
3. Test assertion that doesn't run
In SourceGeneratorTests.swift, try expect(exceptionSet.membershipExceptions) == nil only produces compiler warnings ("result of operator '==' is unused" / "comparing non-optional value ... to 'nil' always returns false"), so nothing is asserted. try expect(exceptionSet.membershipExceptions).to.beNil() works, as used in SchemeGeneratorTests.
4. The #1646 example is still broken
The example in #1646 lists Sources twice in one target with different includes/excludes/filters. With this PR it generates two identical exception sets for the target, puts VisionOSMain.swift in membershipExceptions in both (so the visionOS build has no @main), and lists the synced group twice in fileSystemSynchronizedGroups. That duplication predates this PR, so it doesn't need to be solved here, but the PR shouldn't close #1646. Merging multiple sources for the same synced path, or a validation error for duplicate synced paths in a target, could be a follow-up.
5. Docs
Docs/ProjectSpec.md isn't updated. For synced folders the filters are resolved from the files present at generation time, so a file added to a filtered folder later is built for all platforms until the project is regenerated (confirmed: a UIKit file added to an iOS-only folder breaks the macOS build until regeneration). That differs from the usual synced folder behaviour and is worth a sentence on destinationFilters and inferDestinationFiltersByPath.
6. inferDestinationFiltersByPath matches the absolute path (existing issue)
makeDestinationFilters matches the regexes against the absolute path, so a project inside any directory named iOS (e.g. ~/Code/iOS/MyApp) gets every file filtered to ios. This affects regular sources too and isn't introduced here, but it now also applies to synced folders. Matching against the path relative to the source root would fix it, here or in a follow-up.
Also note CI's Tests/Fixtures/TestProject/build.sh only builds App_iOS Test and App_macOS, so the new App_supportedDestinations/SyncedSources fixture isn't built in CI.
Xcode treats an explicit folder as a single item, so give it one platform
filter key instead of keys for the files inside it. Localized files are
keyed by their variant group ("/Localized: Resources/Localizable.strings"),
which Xcode honors for every language, through a helper that the
membership exceptions now share.
Also assert that membershipExceptions is nil instead of comparing it.
|
Thanks for testing this so thoroughly.
There are new tests for 1 and 2. The |
Description
destinationFiltersandinferDestinationFiltersByPathwere ignored fortype: syncedFoldersources. Regular sources get the filters on eachPBXBuildFile, but synced folders have no build files, so every file in the folder was compiled for every supported destination.Xcode stores per-file platform filters for synced folders in
platformFiltersByRelativePathon thePBXFileSystemSynchronizedBuildFileExceptionSet, which XcodeProj already supports. This PR fills it in:SourceGenerator.syncedFolderPlatformFilterswalks the synced folder (same traversal assyncedFolderExceptions: recurse into plain directories, treat file wrappers like.xcassetsas files, skip hidden files) and uses the existingmakeDestinationFiltersfor each file. Xcode only honors file paths here, not folder paths, so folders are expanded into files.configureMembershipExceptionsadds those filters to the exception set, leaving out files that are already membership exceptions. An exception set is now also created when there are only platform filters.Note: the example in the issue lists the same synced folder path twice in one target. That already doesn't work on master, separately from filters: only the first source's settings are used, and the group is added to
fileSystemSynchronizedGroupstwice. I left that out to keep this PR small. Using separate folders, or one folder withinferDestinationFiltersByPath, works with this change.Testing
SourceGeneratorTests(destinationFilterswith excludes and a nested folder, andinferDestinationFiltersByPath). Both failed before the change.inferDestinationFiltersByPathtoApp_supportedDestinationsin the TestProject fixture and regenerated it. It hasSyncedFile_iOS.swiftandSyncedFile_tvOS.swift, which declare the same symbol, so the target only builds if the filters are applied.swift testpasses, andscripts/gen-fixtures.sh+scripts/diff-fixtures.shshow no changes after the commit.App_supportedDestinationswith Xcode 26.6 for the iOS Simulator and the tvOS Simulator SDK. Each build compiled only its ownSyncedFile_*.swift. WithplatformFiltersByRelativePathremoved from the generated project by hand, the iOS build fails withinvalid redeclaration of 'syncedPlatformName'.destinationFilterson synced folders (like the example above) and built it foriphonesimulator,xrosandxrsimulator. Each build compiled only the files for its platform plus the unfiltered shared folder.Related to #1646. This doesn't fix the example in that issue, because it lists the same synced folder twice in one target (see the note above).