Skip to content

Commit 797bb0a

Browse files
committed
add syncedFolder source type
1 parent 9363be7 commit 797bb0a

File tree

11 files changed

+123
-3
lines changed

11 files changed

+123
-3
lines changed

Docs/ProjectSpec.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ Note that target names can also be changed by adding a `name` property to a targ
153153
- [ ] **postGenCommand**: **String** - A bash command to run after the project has been generated. If the project isn't generated due to no changes when using the cache then this won't run. This is useful for running things like `pod install` only if the project is actually regenerated.
154154
- [ ] **useBaseInternationalization**: **Bool** If this is `false` and your project does not include resources located in a **Base.lproj** directory then `Base` will not be included in the projects 'known regions'. The default value is `true`.
155155
- [ ] **schemePathPrefix**: **String** - A path prefix for relative paths in schemes, such as StoreKitConfiguration. The default is `"../../"`, which is suitable for non-workspace projects. For use in workspaces, use `"../"`.
156+
- [ ] **defaultSourceDirectoryType**: **String** - When a [Target source](#target-source) doesn't specify a type and is a directory, this is the type that will be used. If nothing is specified for either then `group` will be used.
157+
- `group` (default)
158+
- `folder`
159+
- `syncedFolder`
156160

157161
```yaml
158162
options:
@@ -542,6 +546,7 @@ A source can be provided via a string (the path) or an object of the form:
542546
- `file`: a file reference with a parent group will be created (Default for files or directories with extensions)
543547
- `group`: a group with all it's containing files. (Default for directories without extensions)
544548
- `folder`: a folder reference.
549+
- `syncedFolder`: Xcode 16's synchronized folders, also knows as buildable folders
545550
- [ ] **headerVisibility**: **String** - The visibility of any headers. This defaults to `public`, but can be either:
546551
- `public`
547552
- `private`

Sources/ProjectSpec/SourceType.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ public enum SourceType: String {
1111
case group
1212
case file
1313
case folder
14+
case syncedFolder
1415
}

Sources/ProjectSpec/SpecOptions.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public struct SpecOptions: Equatable {
3737
public var postGenCommand: String?
3838
public var useBaseInternationalization: Bool
3939
public var schemePathPrefix: String
40+
public var defaultSourceDirectoryType: SourceType?
4041

4142
public enum ValidationType: String {
4243
case missingConfigs
@@ -100,7 +101,8 @@ public struct SpecOptions: Equatable {
100101
preGenCommand: String? = nil,
101102
postGenCommand: String? = nil,
102103
useBaseInternationalization: Bool = useBaseInternationalizationDefault,
103-
schemePathPrefix: String = schemePathPrefixDefault
104+
schemePathPrefix: String = schemePathPrefixDefault,
105+
defaultSourceDirectoryType: SourceType? = nil
104106
) {
105107
self.minimumXcodeGenVersion = minimumXcodeGenVersion
106108
self.carthageBuildPath = carthageBuildPath
@@ -127,6 +129,7 @@ public struct SpecOptions: Equatable {
127129
self.postGenCommand = postGenCommand
128130
self.useBaseInternationalization = useBaseInternationalization
129131
self.schemePathPrefix = schemePathPrefix
132+
self.defaultSourceDirectoryType = defaultSourceDirectoryType
130133
}
131134
}
132135

@@ -160,6 +163,7 @@ extension SpecOptions: JSONObjectConvertible {
160163
postGenCommand = jsonDictionary.json(atKeyPath: "postGenCommand")
161164
useBaseInternationalization = jsonDictionary.json(atKeyPath: "useBaseInternationalization") ?? SpecOptions.useBaseInternationalizationDefault
162165
schemePathPrefix = jsonDictionary.json(atKeyPath: "schemePathPrefix") ?? SpecOptions.schemePathPrefixDefault
166+
defaultSourceDirectoryType = jsonDictionary.json(atKeyPath: "defaultSourceDirectoryType")
163167
if jsonDictionary["fileTypes"] != nil {
164168
fileTypes = try jsonDictionary.json(atKeyPath: "fileTypes")
165169
} else {

Sources/XcodeGenKit/PBXProjGenerator.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,12 @@ public class PBXProjGenerator {
14541454
if !target.isLegacy {
14551455
targetObject.productType = target.type
14561456
}
1457+
1458+
// add fileSystemSynchronizedGroups
1459+
let synchronizedRootGroups = sourceFiles.compactMap { $0.synchronizedRootGroup }
1460+
if !synchronizedRootGroups.isEmpty {
1461+
targetObject.fileSystemSynchronizedGroups = synchronizedRootGroups
1462+
}
14571463
}
14581464

14591465
private func makePlatformFilter(for filter: Dependency.PlatformFilter) -> String? {

Sources/XcodeGenKit/SourceGenerator.swift

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct SourceFile {
99
let fileReference: PBXFileElement
1010
let buildFile: PBXBuildFile
1111
let buildPhase: BuildPhaseSpec?
12+
var synchronizedRootGroup: PBXFileSystemSynchronizedRootGroup?
1213
}
1314

1415
class SourceGenerator {
@@ -687,6 +688,33 @@ class SourceGenerator {
687688

688689
sourceFiles += groupSourceFiles
689690
sourceReference = group
691+
case .syncedFolder:
692+
693+
let relativePath = (try? path.relativePath(from: project.basePath)) ?? path
694+
695+
let syncedRootGroup = PBXFileSystemSynchronizedRootGroup(
696+
sourceTree: .group,
697+
path: relativePath.string,
698+
name: targetSource.name,
699+
explicitFileTypes: [:],
700+
exceptions: [],
701+
explicitFolders: []
702+
)
703+
addObject(syncedRootGroup)
704+
sourceReference = syncedRootGroup
705+
706+
// TODO: adjust if hasCustomParent == true
707+
rootGroups.insert(syncedRootGroup)
708+
709+
var sourceFile = generateSourceFile(
710+
targetType: targetType,
711+
targetSource: targetSource,
712+
path: path,
713+
fileReference: syncedRootGroup,
714+
buildPhases: buildPhases
715+
)
716+
sourceFile.synchronizedRootGroup = syncedRootGroup
717+
sourceFiles.append(sourceFile)
690718
}
691719

692720
if hasCustomParent {
@@ -703,7 +731,17 @@ class SourceGenerator {
703731
///
704732
/// While `TargetSource` declares `type`, its optional and in the event that the value is not defined then we must resolve a sensible default based on the path of the source.
705733
private func resolvedTargetSourceType(for targetSource: TargetSource, at path: Path) -> SourceType {
706-
return targetSource.type ?? (path.isFile || path.extension != nil ? .file : .group)
734+
if let chosenType = targetSource.type {
735+
return chosenType
736+
} else {
737+
if path.isFile || path.extension != nil {
738+
return .file
739+
} else if let sourceType = project.options.defaultSourceDirectoryType {
740+
return sourceType
741+
} else {
742+
return .group
743+
}
744+
}
707745
}
708746

709747
private func createParentGroups(_ parentGroups: [String], for fileElement: PBXFileElement) {

Sources/XcodeGenKit/XCProjExtensions.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ extension PBXProj {
3838
string += "\n 🌎 " + variantGroup.nameOrPath
3939
} else if let versionGroup = child as? XCVersionGroup {
4040
string += "\n 🔢 " + versionGroup.nameOrPath
41+
} else if let syncedFolder = child as? PBXFileSystemSynchronizedRootGroup {
42+
string += "\n 📁 " + syncedFolder.nameOrPath
4143
}
4244
}
4345
return string

Tests/Fixtures/TestProject/App_iOS/AppDelegate.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
77
var window: UIWindow?
88

99
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
10-
// Override point for customization after application launch.
10+
11+
// file from a framework
1112
_ = FrameworkStruct()
13+
1214
// Standalone files added to project by path-to-file.
1315
_ = standaloneHello()
16+
17+
// file in a synced folder
18+
_ = SyncedStruct()
19+
1420
return true
1521
}
1622
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,18 @@
830830
FED40A89162E446494DDE7C7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
831831
/* End PBXFileReference section */
832832

833+
/* Begin PBXFileSystemSynchronizedRootGroup section */
834+
AE2AB2772F70DFFF402AA02B /* SyncedFolder */ = {
835+
isa = PBXFileSystemSynchronizedRootGroup;
836+
explicitFileTypes = {
837+
};
838+
explicitFolders = (
839+
);
840+
path = SyncedFolder;
841+
sourceTree = "<group>";
842+
};
843+
/* End PBXFileSystemSynchronizedRootGroup section */
844+
833845
/* Begin PBXFrameworksBuildPhase section */
834846
117840B4DBC04099F6779D00 /* Frameworks */ = {
835847
isa = PBXFrameworksBuildPhase;
@@ -1050,6 +1062,7 @@
10501062
2E1E747C7BC434ADB80CC269 /* Headers */,
10511063
6B1603BA83AA0C7B94E45168 /* ResourceFolder */,
10521064
6BBE762F36D94AB6FFBFE834 /* SomeFile */,
1065+
AE2AB2772F70DFFF402AA02B /* SyncedFolder */,
10531066
79DC4A1E4D2E0D3A215179BC /* Bundles */,
10541067
FC1515684236259C50A7747F /* Frameworks */,
10551068
AC523591AC7BE9275003D2DB /* Products */,
@@ -1686,6 +1699,9 @@
16861699
E8C078B0A2A2B0E1D35694D5 /* PBXTargetDependency */,
16871700
981D116D40DBA0407D0E0E94 /* PBXTargetDependency */,
16881701
);
1702+
fileSystemSynchronizedGroups = (
1703+
AE2AB2772F70DFFF402AA02B /* SyncedFolder */,
1704+
);
16891705
name = App_iOS;
16901706
packageProductDependencies = (
16911707
D7917D10F77DA9D69937D493 /* Swinject */,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
struct SyncedStruct {
3+
4+
}

Tests/Fixtures/TestProject/project.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ targets:
164164
- tag1
165165
- tag2
166166
- String Catalogs/LocalizableStrings.xcstrings
167+
- path: SyncedFolder
168+
type: syncedFolder
167169
settings:
168170
INFOPLIST_FILE: App_iOS/Info.plist
169171
PRODUCT_BUNDLE_IDENTIFIER: com.project.app

Tests/XcodeGenKitTests/SourceGeneratorTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,42 @@ class SourceGeneratorTests: XCTestCase {
8484
try pbxProj.expectFile(paths: ["Sources", "A", "C2.0", "c.swift"], buildPhase: .sources)
8585
}
8686

87+
$0.it("generates synced folder") {
88+
let directories = """
89+
Sources:
90+
A:
91+
- a.swift
92+
"""
93+
try createDirectories(directories)
94+
95+
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [.init(path: "Sources", type: .syncedFolder)])
96+
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
97+
98+
let pbxProj = try project.generatePbxProj()
99+
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
100+
let syncedFolder = try unwrap(syncedFolders.first)
101+
102+
try expect([syncedFolder]) == pbxProj.nativeTargets.first?.fileSystemSynchronizedGroups
103+
}
104+
105+
$0.it("respects defaultSourceDirectoryType") {
106+
let directories = """
107+
Sources:
108+
A:
109+
- a.swift
110+
"""
111+
try createDirectories(directories)
112+
113+
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
114+
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: .init(defaultSourceDirectoryType: .syncedFolder))
115+
116+
let pbxProj = try project.generatePbxProj()
117+
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
118+
let syncedFolder = try unwrap(syncedFolders.first)
119+
120+
try expect([syncedFolder]) == pbxProj.nativeTargets.first?.fileSystemSynchronizedGroups
121+
}
122+
87123
$0.it("supports frameworks in sources") {
88124
let directories = """
89125
Sources:

0 commit comments

Comments
 (0)