Skip to content

Commit 167d119

Browse files
authored
Various synced folder enhancements (#1596)
* Add explicitFolders support to syncedFolder Adds an `explicitFolders` property to `TargetSource` that is expanded from Glob patterns and passed through to `PBXFileSystemSynchronizedRootGroup`. * Fix syncedFolder sources ignoring createIntermediateGroups When createIntermediateGroups was enabled and a syncedFolder source had a multi-component path (e.g. SyncedParent/SyncedChild), two things went wrong: 1. The synced folder was unconditionally added to rootGroups, causing it to appear both at the project root and inside the correct intermediate parent group. 2. The synced folder kept its full project-relative path instead of being made relative to its parent group, so Xcode concatenated them into a wrong path (e.g. SyncedParent/SyncedParent/SyncedChild). * Enhance PBXFileElement to recognize synced folders as groups that can be sorted * Fix membership exceptions for nested synced folder with intermediate groups * Update Changelog
1 parent 4c3d558 commit 167d119

13 files changed

Lines changed: 211 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Next Version
44

5+
- Adds an `explicitFolders` property to `TargetSource` that is passed through to `PBXFileSystemSynchronizedRootGroup`, to turn entire subfolders into Resources. #1596 @macguru
6+
- Allow synced folders to be sorted using `groupOrdering`. #1596 @macguru
7+
- Fixed synced folders ignoring `createIntermediateGroups=YES` and always beging created at the root level. #1596 @macguru
8+
- Fix membership exceptions not working for nested synced folders with intermediate groups enabled. #1596 @macguru
9+
510
## 2.44.1
611

712
### Fixed

Docs/ProjectSpec.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ A source can be provided via a string (the path) or an object of the form:
520520
- [ ] **compilerFlags**: **[String]** or **String** - A list of compilerFlags to add to files under this specific path provided as a list or a space delimited string. Defaults to empty.
521521
- [ ] **excludes**: **[String]** - A list of [global patterns](https://en.wikipedia.org/wiki/Glob_(programming)) representing the files to exclude. These rules are relative to `path` and _not the directory where `project.yml` resides_. XcodeGen uses Bash 4's Glob behaviors where globstar (**) is enabled.
522522
- [ ] **includes**: **[String]** - A list of global patterns in the same format as `excludes` representing the files to include. These rules are relative to `path` and _not the directory where `project.yml` resides_. If **excludes** is present and file conflicts with **includes**, **excludes** will override the **includes** behavior.
523+
- [ ] **explicitFolders**: **[String]** - Only valid for `syncedFolder` type. A list of global patterns in the same format as `excludes` to child folders that Xcode should treat as folder references.
523524
- [ ] **destinationFilters**: **[[Supported Destinations](#supported-destinations)]** - List of supported platform destinations the files should filter to. Defaults to all supported destinations.
524525
- [ ] **inferDestinationFiltersByPath**: **Bool** - This is a convenience filter that helps you to filter the files if their paths match these patterns `**/<supportedDestination>/*` or `*_<supportedDestination>.swift`. Note, if you use `destinationFilters` this flag will be ignored.
525526
- [ ] **createIntermediateGroups**: **Bool** - This overrides the value in [Options](#options).

Sources/ProjectSpec/TargetSource.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public struct TargetSource: Equatable {
1616
public var compilerFlags: [String]
1717
public var excludes: [String]
1818
public var includes: [String]
19+
public var explicitFolders: [String]
1920
public var type: SourceType?
2021
public var optional: Bool
2122
public var buildPhase: BuildPhaseSpec?
@@ -47,6 +48,7 @@ public struct TargetSource: Equatable {
4748
compilerFlags: [String] = [],
4849
excludes: [String] = [],
4950
includes: [String] = [],
51+
explicitFolders: [String] = [],
5052
type: SourceType? = nil,
5153
optional: Bool = optionalDefault,
5254
buildPhase: BuildPhaseSpec? = nil,
@@ -63,6 +65,7 @@ public struct TargetSource: Equatable {
6365
self.compilerFlags = compilerFlags
6466
self.excludes = excludes
6567
self.includes = includes
68+
self.explicitFolders = explicitFolders
6669
self.type = type
6770
self.optional = optional
6871
self.buildPhase = buildPhase
@@ -106,6 +109,7 @@ extension TargetSource: JSONObjectConvertible {
106109
headerVisibility = jsonDictionary.json(atKeyPath: "headerVisibility")
107110
excludes = jsonDictionary.json(atKeyPath: "excludes") ?? []
108111
includes = jsonDictionary.json(atKeyPath: "includes") ?? []
112+
explicitFolders = jsonDictionary.json(atKeyPath: "explicitFolders") ?? []
109113
type = jsonDictionary.json(atKeyPath: "type")
110114
optional = jsonDictionary.json(atKeyPath: "optional") ?? TargetSource.optionalDefault
111115

@@ -133,6 +137,7 @@ extension TargetSource: JSONEncodable {
133137
"compilerFlags": compilerFlags,
134138
"excludes": excludes,
135139
"includes": includes,
140+
"explicitFolders": explicitFolders,
136141
"name": name,
137142
"group": group,
138143
"headerVisibility": headerVisibility?.rawValue,

Sources/XcodeGenKit/PBXProjGenerator.swift

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,29 +1459,30 @@ public class PBXProjGenerator {
14591459
}
14601460

14611461
// add fileSystemSynchronizedGroups
1462-
let synchronizedRootGroups = sourceFiles.compactMap { $0.fileReference as? PBXFileSystemSynchronizedRootGroup }
1462+
let synchronizedRootGroups: [PBXFileSystemSynchronizedRootGroup] = sourceFiles.compactMap { sourceFile in
1463+
guard let syncedGroup = sourceFile.fileReference as? PBXFileSystemSynchronizedRootGroup else { return nil }
1464+
1465+
configureMembershipExceptions(
1466+
for: syncedGroup,
1467+
path: sourceFile.path,
1468+
target: target,
1469+
targetObject: targetObject,
1470+
infoPlistFiles: infoPlistFiles
1471+
)
1472+
return syncedGroup
1473+
}
14631474
if !synchronizedRootGroups.isEmpty {
1464-
for syncedGroup in synchronizedRootGroups {
1465-
configureMembershipExceptions(
1466-
for: syncedGroup,
1467-
target: target,
1468-
targetObject: targetObject,
1469-
infoPlistFiles: infoPlistFiles
1470-
)
1471-
}
14721475
targetObject.fileSystemSynchronizedGroups = synchronizedRootGroups
14731476
}
14741477
}
14751478

14761479
private func configureMembershipExceptions(
14771480
for syncedGroup: PBXFileSystemSynchronizedRootGroup,
1481+
path syncedPath: Path,
14781482
target: Target,
14791483
targetObject: PBXTarget,
14801484
infoPlistFiles: [Config: String]
14811485
) {
1482-
guard let syncedGroupPath = syncedGroup.path else { return }
1483-
let syncedPath = (project.basePath + Path(syncedGroupPath)).normalize()
1484-
14851486
guard let targetSource = target.sources.first(where: {
14861487
(project.basePath + $0.path).normalize() == syncedPath
14871488
}) else { return }
@@ -1692,13 +1693,13 @@ extension Platform {
16921693
}
16931694

16941695
extension PBXFileElement {
1695-
/// - returns: `true` if the element is a group or a folder reference. Likely an SPM package.
1696+
/// - returns: `true` if the element is a group, a folder reference (likely an SPM package), or a synced folder.
16961697
var isGroupOrFolder: Bool {
1697-
self is PBXGroup || (self as? PBXFileReference)?.lastKnownFileType == "folder"
1698+
self is PBXGroup || self is PBXFileSystemSynchronizedRootGroup || (self as? PBXFileReference)?.lastKnownFileType == "folder"
16981699
}
16991700

17001701
public func getSortOrder(groupSortPosition: SpecOptions.GroupSortPosition) -> Int {
1701-
if type(of: self).isa == "PBXGroup" {
1702+
if self is PBXGroup || self is PBXFileSystemSynchronizedRootGroup {
17021703
switch groupSortPosition {
17031704
case .top: return -1
17041705
case .bottom: return 1

Sources/XcodeGenKit/SourceGenerator.swift

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,20 @@ class SourceGenerator {
399399
)
400400
}
401401

402+
/// Expands glob patterns in `explicitFolders` relative to the synced root path.
403+
private func resolveExplicitFolders(targetSource: TargetSource) -> [String] {
404+
let rootSourcePath = project.basePath + targetSource.path
405+
406+
return targetSource.explicitFolders.flatMap { pattern in
407+
let matches = Glob(pattern: "\(rootSourcePath)/\(pattern)")
408+
.map { Path($0) }
409+
.filter { $0.isDirectory }
410+
.compactMap { try? $0.relativePath(from: rootSourcePath).string }
411+
.sorted()
412+
return matches.isEmpty ? [pattern] : matches
413+
}
414+
}
415+
402416
/// Checks whether the path is not in any default or TargetSource excludes
403417
func isIncludedPath(_ path: Path, excludePaths: Set<Path>, includePaths: SortedArray<Path>?) -> Bool {
404418
return !defaultExcludedFiles.contains(where: { path.lastComponent == $0 })
@@ -695,20 +709,22 @@ class SourceGenerator {
695709
case .syncedFolder:
696710

697711
let relativePath = (try? path.relativePath(from: project.basePath)) ?? path
712+
let resolvedExplicitFolders = resolveExplicitFolders(targetSource: targetSource)
698713

699714
let syncedRootGroup = PBXFileSystemSynchronizedRootGroup(
700715
sourceTree: .group,
701716
path: relativePath.string,
702717
name: targetSource.name,
703718
explicitFileTypes: [:],
704719
exceptions: [],
705-
explicitFolders: []
720+
explicitFolders: resolvedExplicitFolders
706721
)
707722
addObject(syncedRootGroup)
708723
sourceReference = syncedRootGroup
709724

710-
// TODO: adjust if hasCustomParent == true
711-
rootGroups.insert(syncedRootGroup)
725+
if !(createIntermediateGroups || hasCustomParent) || path.parent() == project.basePath {
726+
rootGroups.insert(syncedRootGroup)
727+
}
712728

713729
let sourceFile = generateSourceFile(
714730
targetType: targetType,
@@ -725,6 +741,7 @@ class SourceGenerator {
725741
try makePathRelative(for: sourceReference, at: path)
726742
} else if createIntermediateGroups {
727743
createIntermediaGroups(for: sourceReference, at: sourcePath)
744+
try makePathRelative(for: sourceReference, at: sourcePath)
728745
}
729746

730747
return sourceFiles

Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,16 @@
842842
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
843843

844844
/* Begin PBXFileSystemSynchronizedRootGroup section */
845+
A2F1B5386E15A261AC8A4DEE /* SyncedChild */ = {
846+
isa = PBXFileSystemSynchronizedRootGroup;
847+
explicitFileTypes = {
848+
};
849+
explicitFolders = (
850+
);
851+
name = SyncedChild;
852+
path = SyncedChild;
853+
sourceTree = "<group>";
854+
};
845855
AE2AB2772F70DFFF402AA02B /* SyncedFolder */ = {
846856
isa = PBXFileSystemSynchronizedRootGroup;
847857
exceptions = (
@@ -850,6 +860,9 @@
850860
explicitFileTypes = {
851861
};
852862
explicitFolders = (
863+
Resources,
864+
FeatureATests,
865+
FeatureBTests,
853866
);
854867
path = SyncedFolder;
855868
sourceTree = "<group>";
@@ -1012,6 +1025,12 @@
10121025
isa = PBXGroup;
10131026
children = (
10141027
2F80635127D17ECB7748067B /* FolderWithDot2.0 */,
1028+
CE1F06D99242F4223D081F0D /* LaunchScreen.storyboard */,
1029+
9E17D598D98065767A04740F /* Localizable.strings */,
1030+
65C8D6D1DDC1512D396C07B7 /* Localizable.stringsdict */,
1031+
0C6BA0D12467A13EC012C728 /* LocalizedStoryboard.storyboard */,
1032+
814D72C2B921F60B759C2D4B /* Main.storyboard */,
1033+
306796628DD52FA55E833B65 /* Model.xcdatamodeld */,
10151034
AEBCA8CFF769189C0D52031E /* App_iOS.xctestplan */,
10161035
F0D48A913C087D049C8EDDD7 /* App.entitlements */,
10171036
7F1A2F579A6F79C62DDA0571 /* AppDelegate.swift */,
@@ -1020,12 +1039,6 @@
10201039
B5C943D39DD7812CAB94B614 /* Documentation.docc */,
10211040
C9DDE1B06BCC1CDE0ECF1589 /* Info.plist */,
10221041
AAA49985DFFE797EE8416887 /* inputList.xcfilelist */,
1023-
CE1F06D99242F4223D081F0D /* LaunchScreen.storyboard */,
1024-
9E17D598D98065767A04740F /* Localizable.strings */,
1025-
65C8D6D1DDC1512D396C07B7 /* Localizable.stringsdict */,
1026-
0C6BA0D12467A13EC012C728 /* LocalizedStoryboard.storyboard */,
1027-
814D72C2B921F60B759C2D4B /* Main.storyboard */,
1028-
306796628DD52FA55E833B65 /* Model.xcdatamodeld */,
10291042
BF59AC868D227C92CA8B1B57 /* Model.xcmappingmodel */,
10301043
C7809CE9FE9852C2AA87ACE5 /* module.modulemap */,
10311044
553D289724905857912C7A1D /* outputList.xcfilelist */,
@@ -1068,6 +1081,8 @@
10681081
BDA839814AF73F01F7710518 /* StaticLibrary_ObjC */,
10691082
CBDAC144248EE9D3838C6AAA /* StaticLibrary_Swift */,
10701083
6E0D17C5B4E6F01B89254309 /* String Catalogs */,
1084+
AE2AB2772F70DFFF402AA02B /* SyncedFolder */,
1085+
AB527E0D553CE53AF54C39CD /* SyncedParent */,
10711086
8CFD8AD4820FAB9265663F92 /* Tool */,
10721087
4C7F5EB7D6F3E0E9B426AB4A /* Utilities */,
10731088
3FEA12CF227D41EF50E5C2DB /* Vendor */,
@@ -1076,7 +1091,6 @@
10761091
2E1E747C7BC434ADB80CC269 /* Headers */,
10771092
6B1603BA83AA0C7B94E45168 /* ResourceFolder */,
10781093
6BBE762F36D94AB6FFBFE834 /* SomeFile */,
1079-
AE2AB2772F70DFFF402AA02B /* SyncedFolder */,
10801094
79DC4A1E4D2E0D3A215179BC /* Bundles */,
10811095
FC1515684236259C50A7747F /* Frameworks */,
10821096
AC523591AC7BE9275003D2DB /* Products */,
@@ -1317,6 +1331,14 @@
13171331
path = iMessageApp;
13181332
sourceTree = "<group>";
13191333
};
1334+
AB527E0D553CE53AF54C39CD /* SyncedParent */ = {
1335+
isa = PBXGroup;
1336+
children = (
1337+
A2F1B5386E15A261AC8A4DEE /* SyncedChild */,
1338+
);
1339+
path = SyncedParent;
1340+
sourceTree = "<group>";
1341+
};
13201342
AC523591AC7BE9275003D2DB /* Products */ = {
13211343
isa = PBXGroup;
13221344
children = (
@@ -1368,9 +1390,9 @@
13681390
BAE6C12745737019DC9E98BF /* App_watchOS */ = {
13691391
isa = PBXGroup;
13701392
children = (
1393+
C872631362DDBAFCE71E5C66 /* Interface.storyboard */,
13711394
D8A016580A3B8F72B820BFBF /* Assets.xcassets */,
13721395
FED40A89162E446494DDE7C7 /* Info.plist */,
1373-
C872631362DDBAFCE71E5C66 /* Interface.storyboard */,
13741396
);
13751397
path = App_watchOS;
13761398
sourceTree = "<group>";
@@ -1388,9 +1410,9 @@
13881410
BF58996786F85CB77BEE72EF /* iMessageExtension */ = {
13891411
isa = PBXGroup;
13901412
children = (
1413+
753001CDCEAA4C4E1AFF8E87 /* MainInterface.storyboard */,
13911414
1BC32A813B80A53962A1F365 /* Assets.xcassets */,
13921415
40863AE6202CFCD0529D8438 /* Info.plist */,
1393-
753001CDCEAA4C4E1AFF8E87 /* MainInterface.storyboard */,
13941416
B198242976C3395E31FE000A /* MessagesViewController.swift */,
13951417
);
13961418
path = iMessageExtension;
@@ -1399,12 +1421,12 @@
13991421
C81493FAD71E9A9A19E00AD5 /* App_Clip */ = {
14001422
isa = PBXGroup;
14011423
children = (
1424+
79325B44B19B83EC6CEDBCC5 /* LaunchScreen.storyboard */,
1425+
2FC2A8A829CE71B1CF415FF7 /* Main.storyboard */,
14021426
23A2F16890ECF2EE3FED72AE /* AppDelegate.swift */,
14031427
59DA55A04FA2366B5D0BEEFF /* Assets.xcassets */,
14041428
1FA5E208EC184E3030D2A21D /* Clip.entitlements */,
14051429
6F165CDD5BCC13AFF50B65E2 /* Info.plist */,
1406-
79325B44B19B83EC6CEDBCC5 /* LaunchScreen.storyboard */,
1407-
2FC2A8A829CE71B1CF415FF7 /* Main.storyboard */,
14081430
DFE6A6FAAFF701FE729293DE /* ViewController.swift */,
14091431
);
14101432
path = App_Clip;
@@ -1449,10 +1471,10 @@
14491471
EE78B4FBD0137D1975C47D76 /* App_macOS */ = {
14501472
isa = PBXGroup;
14511473
children = (
1474+
74FBDFA5CB063F6001AD8ACD /* Main.storyboard */,
14521475
9528528C989D24FE3E6C533E /* App-Info.plist */,
14531476
09B82F603D981398F38D762E /* AppDelegate.swift */,
14541477
E55F45EACB0F382722D61C8D /* Assets.xcassets */,
1455-
74FBDFA5CB063F6001AD8ACD /* Main.storyboard */,
14561478
A4C3FE6B986506724DAB5D0F /* ViewController.swift */,
14571479
);
14581480
path = App_macOS;
@@ -1714,6 +1736,7 @@
17141736
981D116D40DBA0407D0E0E94 /* PBXTargetDependency */,
17151737
);
17161738
fileSystemSynchronizedGroups = (
1739+
A2F1B5386E15A261AC8A4DEE /* SyncedChild */,
17171740
AE2AB2772F70DFFF402AA02B /* SyncedFolder */,
17181741
);
17191742
name = App_iOS;

Tests/Fixtures/TestProject/SyncedFolder/FeatureATests/__Snapshots__/.gitkeep

Whitespace-only changes.

Tests/Fixtures/TestProject/SyncedFolder/FeatureBTests/__Snapshots__/.gitkeep

Whitespace-only changes.

Tests/Fixtures/TestProject/SyncedFolder/Resources/.gitkeep

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import Foundation

0 commit comments

Comments
 (0)