Synced folders (#1541)

* update xcodeproj to 8.27.7

* add syncedFolder source type

* drop xcode 15 support

* Rely on fileReference instead of adding new synchronizedRootGroup (#1557)

* fix: don't include untracked children in cache

---------

Co-authored-by: Kirill Yakimovich <kirill.yakimovich@gmail.com>
This commit is contained in:
Yonas Kolb
2025-07-17 15:15:43 +10:00
committed by GitHub
parent 53cb43cb66
commit c32aa4cc94
26 changed files with 179 additions and 43 deletions
+5
View File
@@ -153,6 +153,10 @@ Note that target names can also be changed by adding a `name` property to a targ
- [ ] **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.
- [ ] **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`.
- [ ] **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 `"../"`.
- [ ] **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.
- `group` (default)
- `folder`
- `syncedFolder`
```yaml
options:
@@ -542,6 +546,7 @@ A source can be provided via a string (the path) or an object of the form:
- `file`: a file reference with a parent group will be created (Default for files or directories with extensions)
- `group`: a group with all it's containing files. (Default for directories without extensions)
- `folder`: a folder reference.
- `syncedFolder`: Xcode 16's synchronized folders, also knows as buildable folders
- [ ] **headerVisibility**: **String** - The visibility of any headers. This defaults to `public`, but can be either:
- `public`
- `private`
+4 -4
View File
@@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tadija/AEXML.git",
"state" : {
"revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3",
"version" : "4.6.1"
"revision" : "db806756c989760b35108146381535aec231092b",
"version" : "4.7.0"
}
},
{
@@ -77,8 +77,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tuist/XcodeProj.git",
"state" : {
"revision" : "dc3b87a4e69f9cd06c6cb16199f5d0472e57ef6b",
"version" : "8.24.3"
"revision" : "b1caa062d4aaab3e3d2bed5fe0ac5f8ce9bf84f4",
"version" : "8.27.7"
}
},
{
+1 -1
View File
@@ -16,7 +16,7 @@ let package = Package(
.package(url: "https://github.com/yonaskolb/JSONUtilities.git", from: "4.2.0"),
.package(url: "https://github.com/kylef/Spectre.git", from: "0.9.2"),
.package(url: "https://github.com/onevcat/Rainbow.git", from: "4.0.0"),
.package(url: "https://github.com/tuist/XcodeProj.git", exact: "8.24.3"),
.package(url: "https://github.com/tuist/XcodeProj.git", exact: "8.27.7"),
.package(url: "https://github.com/jakeheis/SwiftCLI.git", from: "6.0.3"),
.package(url: "https://github.com/mxcl/Version", from: "2.0.0"),
.package(url: "https://github.com/freddi-kit/ArtifactBundleGen", exact: "0.0.6")
+1 -1
View File
@@ -10,7 +10,7 @@ public class CacheFile {
guard #available(OSX 10.13, *) else { return nil }
let files = Set(project.allFiles)
let files = Set(project.allTrackedFiles)
.map { ((try? $0.relativePath(from: project.basePath)) ?? $0).string }
.sorted { $0.localizedStandardCompare($1) == .orderedAscending }
.joined(separator: "\n")
+19 -3
View File
@@ -252,7 +252,7 @@ extension Project: PathContainer {
extension Project {
public var allFiles: [Path] {
public var allTrackedFiles: [Path] {
var files: [Path] = []
files.append(contentsOf: configFilePaths)
for fileGroup in fileGroups {
@@ -270,8 +270,12 @@ extension Project {
files.append(contentsOf: target.configFilePaths)
for source in target.sources {
let sourcePath = basePath + source.path
let sourceChildren = (try? sourcePath.recursiveChildren()) ?? []
files.append(contentsOf: sourceChildren)
let type = source.type ?? options.defaultSourceDirectoryType ?? .group
if type.projectTracksChildren {
let sourceChildren = (try? sourcePath.recursiveChildren()) ?? []
files.append(contentsOf: sourceChildren)
}
files.append(sourcePath)
}
}
@@ -279,6 +283,18 @@ extension Project {
}
}
extension SourceType {
var projectTracksChildren: Bool {
switch self {
case .file: false
case .folder: false
case .group: true
case .syncedFolder: false
}
}
}
extension BuildSettingsContainer {
fileprivate var configFilePaths: [Path] {
+1
View File
@@ -11,4 +11,5 @@ public enum SourceType: String {
case group
case file
case folder
case syncedFolder
}
+5 -1
View File
@@ -37,6 +37,7 @@ public struct SpecOptions: Equatable {
public var postGenCommand: String?
public var useBaseInternationalization: Bool
public var schemePathPrefix: String
public var defaultSourceDirectoryType: SourceType?
public enum ValidationType: String {
case missingConfigs
@@ -100,7 +101,8 @@ public struct SpecOptions: Equatable {
preGenCommand: String? = nil,
postGenCommand: String? = nil,
useBaseInternationalization: Bool = useBaseInternationalizationDefault,
schemePathPrefix: String = schemePathPrefixDefault
schemePathPrefix: String = schemePathPrefixDefault,
defaultSourceDirectoryType: SourceType? = nil
) {
self.minimumXcodeGenVersion = minimumXcodeGenVersion
self.carthageBuildPath = carthageBuildPath
@@ -127,6 +129,7 @@ public struct SpecOptions: Equatable {
self.postGenCommand = postGenCommand
self.useBaseInternationalization = useBaseInternationalization
self.schemePathPrefix = schemePathPrefix
self.defaultSourceDirectoryType = defaultSourceDirectoryType
}
}
@@ -160,6 +163,7 @@ extension SpecOptions: JSONObjectConvertible {
postGenCommand = jsonDictionary.json(atKeyPath: "postGenCommand")
useBaseInternationalization = jsonDictionary.json(atKeyPath: "useBaseInternationalization") ?? SpecOptions.useBaseInternationalizationDefault
schemePathPrefix = jsonDictionary.json(atKeyPath: "schemePathPrefix") ?? SpecOptions.schemePathPrefixDefault
defaultSourceDirectoryType = jsonDictionary.json(atKeyPath: "defaultSourceDirectoryType")
if jsonDictionary["fileTypes"] != nil {
fileTypes = try jsonDictionary.json(atKeyPath: "fileTypes")
} else {
@@ -1454,6 +1454,12 @@ public class PBXProjGenerator {
if !target.isLegacy {
targetObject.productType = target.type
}
// add fileSystemSynchronizedGroups
let synchronizedRootGroups = sourceFiles.compactMap { $0.fileReference as? PBXFileSystemSynchronizedRootGroup }
if !synchronizedRootGroups.isEmpty {
targetObject.fileSystemSynchronizedGroups = synchronizedRootGroups
}
}
private func makePlatformFilter(for filter: Dependency.PlatformFilter) -> String? {
+37 -1
View File
@@ -687,6 +687,32 @@ class SourceGenerator {
sourceFiles += groupSourceFiles
sourceReference = group
case .syncedFolder:
let relativePath = (try? path.relativePath(from: project.basePath)) ?? path
let syncedRootGroup = PBXFileSystemSynchronizedRootGroup(
sourceTree: .group,
path: relativePath.string,
name: targetSource.name,
explicitFileTypes: [:],
exceptions: [],
explicitFolders: []
)
addObject(syncedRootGroup)
sourceReference = syncedRootGroup
// TODO: adjust if hasCustomParent == true
rootGroups.insert(syncedRootGroup)
let sourceFile = generateSourceFile(
targetType: targetType,
targetSource: targetSource,
path: path,
fileReference: syncedRootGroup,
buildPhases: buildPhases
)
sourceFiles.append(sourceFile)
}
if hasCustomParent {
@@ -703,7 +729,17 @@ class SourceGenerator {
///
/// 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.
private func resolvedTargetSourceType(for targetSource: TargetSource, at path: Path) -> SourceType {
return targetSource.type ?? (path.isFile || path.extension != nil ? .file : .group)
if let chosenType = targetSource.type {
return chosenType
} else {
if path.isFile || path.extension != nil {
return .file
} else if let sourceType = project.options.defaultSourceDirectoryType {
return sourceType
} else {
return .group
}
}
}
private func createParentGroups(_ parentGroups: [String], for fileElement: PBXFileElement) {
+1 -1
View File
@@ -16,7 +16,7 @@ extension Project {
}
var objectVersion: UInt {
54
70
}
var minimizedProjectReferenceProxies: Int {
@@ -38,6 +38,8 @@ extension PBXProj {
string += "\n 🌎 " + variantGroup.nameOrPath
} else if let versionGroup = child as? XCVersionGroup {
string += "\n 🔢 " + versionGroup.nameOrPath
} else if let syncedFolder = child as? PBXFileSystemSynchronizedRootGroup {
string += "\n 📁 " + syncedFolder.nameOrPath
}
}
return string
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -322,8 +322,6 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
TargetAttributes = {
};
};
buildConfigurationList = D91E14E36EC0B415578456F2 /* Build configuration list for PBXProject "Project" */;
compatibilityVersion = "Xcode 14.0";
@@ -335,7 +333,7 @@
);
mainGroup = 293D0FF827366B513839236A;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 54;
preferredProjectObjectVersion = 70;
projectDirPath = "";
projectRoot = "";
targets = (
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 70;
objects = {
/* Begin PBXAggregateTarget section */
@@ -239,8 +239,6 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
TargetAttributes = {
};
};
buildConfigurationList = 425866ADA259DB93FC4AF1E3 /* Build configuration list for PBXProject "SPM" */;
compatibilityVersion = "Xcode 14.0";
@@ -259,7 +257,7 @@
630A8CE9F2BE39704ED9D461 /* XCLocalSwiftPackageReference "FooFeature" */,
C6539B364583AE96C18CE377 /* XCLocalSwiftPackageReference "../../.." */,
);
preferredProjectObjectVersion = 54;
preferredProjectObjectVersion = 70;
projectDirPath = "";
projectRoot = "";
targets = (
@@ -40,7 +40,8 @@
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "XcodeGenKitTests"
@@ -50,7 +51,8 @@
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "339863E54E2D955C00B56802"
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 70;
objects = {
/* Begin PBXFileReference section */
@@ -120,8 +120,6 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
TargetAttributes = {
};
};
buildConfigurationList = 3DFC1105373EDB6483D4BC5D /* Build configuration list for PBXProject "AnotherProject" */;
compatibilityVersion = "Xcode 14.0";
@@ -132,7 +130,7 @@
);
mainGroup = 4E8CFA4275C972686621210C;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 54;
preferredProjectObjectVersion = 70;
projectDirPath = "";
projectRoot = "";
targets = (
@@ -7,10 +7,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// file from a framework
_ = FrameworkStruct()
// Standalone files added to project by path-to-file.
_ = standaloneHello()
// file in a synced folder
_ = SyncedStruct()
return true
}
}
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 70;
objects = {
/* Begin PBXAggregateTarget section */
@@ -830,6 +830,18 @@
FED40A89162E446494DDE7C7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
AE2AB2772F70DFFF402AA02B /* SyncedFolder */ = {
isa = PBXFileSystemSynchronizedRootGroup;
explicitFileTypes = {
};
explicitFolders = (
);
path = SyncedFolder;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
117840B4DBC04099F6779D00 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -1050,6 +1062,7 @@
2E1E747C7BC434ADB80CC269 /* Headers */,
6B1603BA83AA0C7B94E45168 /* ResourceFolder */,
6BBE762F36D94AB6FFBFE834 /* SomeFile */,
AE2AB2772F70DFFF402AA02B /* SyncedFolder */,
79DC4A1E4D2E0D3A215179BC /* Bundles */,
FC1515684236259C50A7747F /* Frameworks */,
AC523591AC7BE9275003D2DB /* Products */,
@@ -1686,6 +1699,9 @@
E8C078B0A2A2B0E1D35694D5 /* PBXTargetDependency */,
981D116D40DBA0407D0E0E94 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
AE2AB2772F70DFFF402AA02B /* SyncedFolder */,
);
name = App_iOS;
packageProductDependencies = (
D7917D10F77DA9D69937D493 /* Swinject */,
@@ -2443,7 +2459,7 @@
packageReferences = (
4EDA79334592CBBA0E507AD2 /* XCRemoteSwiftPackageReference "Swinject" */,
);
preferredProjectObjectVersion = 54;
preferredProjectObjectVersion = 70;
projectDirPath = "";
projectReferences = (
{
@@ -40,7 +40,8 @@
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "63BFF75AA22335E3DDD5E26A"
@@ -50,7 +51,8 @@
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "91C3E922A8482E07649971B9"
@@ -48,7 +48,8 @@
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F674B2CFC4738EEC49BAD0DA"
@@ -59,7 +60,6 @@
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
@@ -42,7 +42,8 @@
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC2F16BAA6E13B44AB62F888"
@@ -52,7 +53,8 @@
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F674B2CFC4738EEC49BAD0DA"
@@ -42,7 +42,8 @@
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC2F16BAA6E13B44AB62F888"
@@ -52,7 +53,8 @@
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F674B2CFC4738EEC49BAD0DA"
@@ -42,7 +42,8 @@
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC2F16BAA6E13B44AB62F888"
@@ -52,7 +53,8 @@
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F674B2CFC4738EEC49BAD0DA"
@@ -0,0 +1,4 @@
struct SyncedStruct {
}
+2
View File
@@ -164,6 +164,8 @@ targets:
- tag1
- tag2
- String Catalogs/LocalizableStrings.xcstrings
- path: SyncedFolder
type: syncedFolder
settings:
INFOPLIST_FILE: App_iOS/Info.plist
PRODUCT_BUNDLE_IDENTIFIER: com.project.app
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 70;
objects = {
/* Begin PBXFileReference section */
@@ -72,8 +72,6 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
TargetAttributes = {
};
};
buildConfigurationList = E903F6E8184E2A86CEC31778 /* Build configuration list for PBXProject "TestProject" */;
compatibilityVersion = "Xcode 14.0";
@@ -85,7 +83,7 @@
);
mainGroup = 2D08B11F4EE060D112B7BCA1;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 54;
preferredProjectObjectVersion = 70;
projectDirPath = "";
projectRoot = "";
targets = (
@@ -84,6 +84,42 @@ class SourceGeneratorTests: XCTestCase {
try pbxProj.expectFile(paths: ["Sources", "A", "C2.0", "c.swift"], buildPhase: .sources)
}
$0.it("generates synced folder") {
let directories = """
Sources:
A:
- a.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [.init(path: "Sources", type: .syncedFolder)])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
let syncedFolder = try unwrap(syncedFolders.first)
try expect([syncedFolder]) == pbxProj.nativeTargets.first?.fileSystemSynchronizedGroups
}
$0.it("respects defaultSourceDirectoryType") {
let directories = """
Sources:
A:
- a.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: .init(defaultSourceDirectoryType: .syncedFolder))
let pbxProj = try project.generatePbxProj()
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
let syncedFolder = try unwrap(syncedFolders.first)
try expect([syncedFolder]) == pbxProj.nativeTargets.first?.fileSystemSynchronizedGroups
}
$0.it("supports frameworks in sources") {
let directories = """
Sources: