Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Next Version

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

## 2.44.1

### Fixed
Expand Down
1 change: 1 addition & 0 deletions Docs/ProjectSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ A source can be provided via a string (the path) or an object of the form:
- [ ] **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.
- [ ] **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.
- [ ] **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.
- [ ] **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.
- [ ] **destinationFilters**: **[[Supported Destinations](#supported-destinations)]** - List of supported platform destinations the files should filter to. Defaults to all supported destinations.
- [ ] **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.
- [ ] **createIntermediateGroups**: **Bool** - This overrides the value in [Options](#options).
Expand Down
5 changes: 5 additions & 0 deletions Sources/ProjectSpec/TargetSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public struct TargetSource: Equatable {
public var compilerFlags: [String]
public var excludes: [String]
public var includes: [String]
public var explicitFolders: [String]
public var type: SourceType?
public var optional: Bool
public var buildPhase: BuildPhaseSpec?
Expand Down Expand Up @@ -47,6 +48,7 @@ public struct TargetSource: Equatable {
compilerFlags: [String] = [],
excludes: [String] = [],
includes: [String] = [],
explicitFolders: [String] = [],
type: SourceType? = nil,
optional: Bool = optionalDefault,
buildPhase: BuildPhaseSpec? = nil,
Expand All @@ -63,6 +65,7 @@ public struct TargetSource: Equatable {
self.compilerFlags = compilerFlags
self.excludes = excludes
self.includes = includes
self.explicitFolders = explicitFolders
self.type = type
self.optional = optional
self.buildPhase = buildPhase
Expand Down Expand Up @@ -106,6 +109,7 @@ extension TargetSource: JSONObjectConvertible {
headerVisibility = jsonDictionary.json(atKeyPath: "headerVisibility")
excludes = jsonDictionary.json(atKeyPath: "excludes") ?? []
includes = jsonDictionary.json(atKeyPath: "includes") ?? []
explicitFolders = jsonDictionary.json(atKeyPath: "explicitFolders") ?? []
type = jsonDictionary.json(atKeyPath: "type")
optional = jsonDictionary.json(atKeyPath: "optional") ?? TargetSource.optionalDefault

Expand Down Expand Up @@ -133,6 +137,7 @@ extension TargetSource: JSONEncodable {
"compilerFlags": compilerFlags,
"excludes": excludes,
"includes": includes,
"explicitFolders": explicitFolders,
"name": name,
"group": group,
"headerVisibility": headerVisibility?.rawValue,
Expand Down
31 changes: 16 additions & 15 deletions Sources/XcodeGenKit/PBXProjGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1459,29 +1459,30 @@ public class PBXProjGenerator {
}

// add fileSystemSynchronizedGroups
let synchronizedRootGroups = sourceFiles.compactMap { $0.fileReference as? PBXFileSystemSynchronizedRootGroup }
let synchronizedRootGroups: [PBXFileSystemSynchronizedRootGroup] = sourceFiles.compactMap { sourceFile in
guard let syncedGroup = sourceFile.fileReference as? PBXFileSystemSynchronizedRootGroup else { return nil }

configureMembershipExceptions(
for: syncedGroup,
path: sourceFile.path,
target: target,
targetObject: targetObject,
infoPlistFiles: infoPlistFiles
)
return syncedGroup
}
if !synchronizedRootGroups.isEmpty {
for syncedGroup in synchronizedRootGroups {
configureMembershipExceptions(
for: syncedGroup,
target: target,
targetObject: targetObject,
infoPlistFiles: infoPlistFiles
)
}
targetObject.fileSystemSynchronizedGroups = synchronizedRootGroups
}
}

private func configureMembershipExceptions(
for syncedGroup: PBXFileSystemSynchronizedRootGroup,
path syncedPath: Path,
target: Target,
targetObject: PBXTarget,
infoPlistFiles: [Config: String]
) {
guard let syncedGroupPath = syncedGroup.path else { return }
let syncedPath = (project.basePath + Path(syncedGroupPath)).normalize()

guard let targetSource = target.sources.first(where: {
(project.basePath + $0.path).normalize() == syncedPath
}) else { return }
Expand Down Expand Up @@ -1692,13 +1693,13 @@ extension Platform {
}

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

public func getSortOrder(groupSortPosition: SpecOptions.GroupSortPosition) -> Int {
if type(of: self).isa == "PBXGroup" {
if self is PBXGroup || self is PBXFileSystemSynchronizedRootGroup {
switch groupSortPosition {
case .top: return -1
case .bottom: return 1
Expand Down
23 changes: 20 additions & 3 deletions Sources/XcodeGenKit/SourceGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,20 @@ class SourceGenerator {
)
}

/// Expands glob patterns in `explicitFolders` relative to the synced root path.
private func resolveExplicitFolders(targetSource: TargetSource) -> [String] {
let rootSourcePath = project.basePath + targetSource.path

return targetSource.explicitFolders.flatMap { pattern in
let matches = Glob(pattern: "\(rootSourcePath)/\(pattern)")
.map { Path($0) }
.filter { $0.isDirectory }
.compactMap { try? $0.relativePath(from: rootSourcePath).string }
.sorted()
return matches.isEmpty ? [pattern] : matches
}
}

/// Checks whether the path is not in any default or TargetSource excludes
func isIncludedPath(_ path: Path, excludePaths: Set<Path>, includePaths: SortedArray<Path>?) -> Bool {
return !defaultExcludedFiles.contains(where: { path.lastComponent == $0 })
Expand Down Expand Up @@ -695,20 +709,22 @@ class SourceGenerator {
case .syncedFolder:

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

let syncedRootGroup = PBXFileSystemSynchronizedRootGroup(
sourceTree: .group,
path: relativePath.string,
name: targetSource.name,
explicitFileTypes: [:],
exceptions: [],
explicitFolders: []
explicitFolders: resolvedExplicitFolders
)
addObject(syncedRootGroup)
sourceReference = syncedRootGroup

// TODO: adjust if hasCustomParent == true
rootGroups.insert(syncedRootGroup)
if !(createIntermediateGroups || hasCustomParent) || path.parent() == project.basePath {
rootGroups.insert(syncedRootGroup)
}

let sourceFile = generateSourceFile(
targetType: targetType,
Expand All @@ -725,6 +741,7 @@ class SourceGenerator {
try makePathRelative(for: sourceReference, at: path)
} else if createIntermediateGroups {
createIntermediaGroups(for: sourceReference, at: sourcePath)
try makePathRelative(for: sourceReference, at: sourcePath)
}

return sourceFiles
Expand Down
47 changes: 35 additions & 12 deletions Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,16 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
A2F1B5386E15A261AC8A4DEE /* SyncedChild */ = {
isa = PBXFileSystemSynchronizedRootGroup;
explicitFileTypes = {
};
explicitFolders = (
);
name = SyncedChild;
path = SyncedChild;
sourceTree = "<group>";
};
AE2AB2772F70DFFF402AA02B /* SyncedFolder */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
Expand All @@ -850,6 +860,9 @@
explicitFileTypes = {
};
explicitFolders = (
Resources,
FeatureATests,
FeatureBTests,
);
path = SyncedFolder;
sourceTree = "<group>";
Expand Down Expand Up @@ -1012,6 +1025,12 @@
isa = PBXGroup;
children = (
2F80635127D17ECB7748067B /* FolderWithDot2.0 */,
CE1F06D99242F4223D081F0D /* LaunchScreen.storyboard */,
9E17D598D98065767A04740F /* Localizable.strings */,
65C8D6D1DDC1512D396C07B7 /* Localizable.stringsdict */,
0C6BA0D12467A13EC012C728 /* LocalizedStoryboard.storyboard */,
814D72C2B921F60B759C2D4B /* Main.storyboard */,
306796628DD52FA55E833B65 /* Model.xcdatamodeld */,
AEBCA8CFF769189C0D52031E /* App_iOS.xctestplan */,
F0D48A913C087D049C8EDDD7 /* App.entitlements */,
7F1A2F579A6F79C62DDA0571 /* AppDelegate.swift */,
Expand All @@ -1020,12 +1039,6 @@
B5C943D39DD7812CAB94B614 /* Documentation.docc */,
C9DDE1B06BCC1CDE0ECF1589 /* Info.plist */,
AAA49985DFFE797EE8416887 /* inputList.xcfilelist */,
CE1F06D99242F4223D081F0D /* LaunchScreen.storyboard */,
9E17D598D98065767A04740F /* Localizable.strings */,
65C8D6D1DDC1512D396C07B7 /* Localizable.stringsdict */,
0C6BA0D12467A13EC012C728 /* LocalizedStoryboard.storyboard */,
814D72C2B921F60B759C2D4B /* Main.storyboard */,
306796628DD52FA55E833B65 /* Model.xcdatamodeld */,
BF59AC868D227C92CA8B1B57 /* Model.xcmappingmodel */,
C7809CE9FE9852C2AA87ACE5 /* module.modulemap */,
553D289724905857912C7A1D /* outputList.xcfilelist */,
Expand Down Expand Up @@ -1068,6 +1081,8 @@
BDA839814AF73F01F7710518 /* StaticLibrary_ObjC */,
CBDAC144248EE9D3838C6AAA /* StaticLibrary_Swift */,
6E0D17C5B4E6F01B89254309 /* String Catalogs */,
AE2AB2772F70DFFF402AA02B /* SyncedFolder */,
AB527E0D553CE53AF54C39CD /* SyncedParent */,
8CFD8AD4820FAB9265663F92 /* Tool */,
4C7F5EB7D6F3E0E9B426AB4A /* Utilities */,
3FEA12CF227D41EF50E5C2DB /* Vendor */,
Expand All @@ -1076,7 +1091,6 @@
2E1E747C7BC434ADB80CC269 /* Headers */,
6B1603BA83AA0C7B94E45168 /* ResourceFolder */,
6BBE762F36D94AB6FFBFE834 /* SomeFile */,
AE2AB2772F70DFFF402AA02B /* SyncedFolder */,
79DC4A1E4D2E0D3A215179BC /* Bundles */,
FC1515684236259C50A7747F /* Frameworks */,
AC523591AC7BE9275003D2DB /* Products */,
Expand Down Expand Up @@ -1317,6 +1331,14 @@
path = iMessageApp;
sourceTree = "<group>";
};
AB527E0D553CE53AF54C39CD /* SyncedParent */ = {
isa = PBXGroup;
children = (
A2F1B5386E15A261AC8A4DEE /* SyncedChild */,
);
path = SyncedParent;
sourceTree = "<group>";
};
AC523591AC7BE9275003D2DB /* Products */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1368,9 +1390,9 @@
BAE6C12745737019DC9E98BF /* App_watchOS */ = {
isa = PBXGroup;
children = (
C872631362DDBAFCE71E5C66 /* Interface.storyboard */,
D8A016580A3B8F72B820BFBF /* Assets.xcassets */,
FED40A89162E446494DDE7C7 /* Info.plist */,
C872631362DDBAFCE71E5C66 /* Interface.storyboard */,
);
path = App_watchOS;
sourceTree = "<group>";
Expand All @@ -1388,9 +1410,9 @@
BF58996786F85CB77BEE72EF /* iMessageExtension */ = {
isa = PBXGroup;
children = (
753001CDCEAA4C4E1AFF8E87 /* MainInterface.storyboard */,
1BC32A813B80A53962A1F365 /* Assets.xcassets */,
40863AE6202CFCD0529D8438 /* Info.plist */,
753001CDCEAA4C4E1AFF8E87 /* MainInterface.storyboard */,
B198242976C3395E31FE000A /* MessagesViewController.swift */,
);
path = iMessageExtension;
Expand All @@ -1399,12 +1421,12 @@
C81493FAD71E9A9A19E00AD5 /* App_Clip */ = {
isa = PBXGroup;
children = (
79325B44B19B83EC6CEDBCC5 /* LaunchScreen.storyboard */,
2FC2A8A829CE71B1CF415FF7 /* Main.storyboard */,
23A2F16890ECF2EE3FED72AE /* AppDelegate.swift */,
59DA55A04FA2366B5D0BEEFF /* Assets.xcassets */,
1FA5E208EC184E3030D2A21D /* Clip.entitlements */,
6F165CDD5BCC13AFF50B65E2 /* Info.plist */,
79325B44B19B83EC6CEDBCC5 /* LaunchScreen.storyboard */,
2FC2A8A829CE71B1CF415FF7 /* Main.storyboard */,
DFE6A6FAAFF701FE729293DE /* ViewController.swift */,
);
path = App_Clip;
Expand Down Expand Up @@ -1449,10 +1471,10 @@
EE78B4FBD0137D1975C47D76 /* App_macOS */ = {
isa = PBXGroup;
children = (
74FBDFA5CB063F6001AD8ACD /* Main.storyboard */,
9528528C989D24FE3E6C533E /* App-Info.plist */,
09B82F603D981398F38D762E /* AppDelegate.swift */,
E55F45EACB0F382722D61C8D /* Assets.xcassets */,
74FBDFA5CB063F6001AD8ACD /* Main.storyboard */,
A4C3FE6B986506724DAB5D0F /* ViewController.swift */,
);
path = App_macOS;
Expand Down Expand Up @@ -1714,6 +1736,7 @@
981D116D40DBA0407D0E0E94 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
A2F1B5386E15A261AC8A4DEE /* SyncedChild */,
AE2AB2772F70DFFF402AA02B /* SyncedFolder */,
);
name = App_iOS;
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import Foundation
6 changes: 6 additions & 0 deletions Tests/Fixtures/TestProject/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ targets:
excludes:
- ExcludedFile.swift
- Info.plist
explicitFolders:
- Resources
- "**/*Tests"
- path: SyncedParent/SyncedChild
type: syncedFolder
createIntermediateGroups: true
settings:
INFOPLIST_FILE: App_iOS/Info.plist
PRODUCT_BUNDLE_IDENTIFIER: com.project.app
Expand Down
45 changes: 45 additions & 0 deletions Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,51 @@ class PBXProjGeneratorTests: XCTestCase {

try expect(packages) == ["FeatureA", "FeatureB", "Common"]
}

$0.it("sorts synced folders alongside groups") {
var options = SpecOptions()
options.groupSortPosition = .top
options.groupOrdering = [
GroupOrdering(
order: [
"Sources",
"SyncedSources",
"Resources",
]
),
]

let directories = """
Resources:
- file.swift
Sources:
- file.swift
SyncedSources:
- file.swift
"""
try createDirectories(directories)

let target = Target(
name: "Test",
type: .application,
platform: .iOS,
sources: [
"Sources",
.init(path: "SyncedSources", type: .syncedFolder),
"Resources",
]
)
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: options)
let projGenerator = PBXProjGenerator(project: project)

let pbxProj = try project.generatePbxProj()
let group = try pbxProj.getMainGroup()

projGenerator.setupGroupOrdering(group: group)

let mainGroups = group.children.map { $0.nameOrPath }
try expect(mainGroups) == ["Sources", "SyncedSources", "Resources", "Products"]
}
}
}

Expand Down
Loading