diff --git a/Docs/ProjectSpec.md b/Docs/ProjectSpec.md index 83166e9c4..5a4aa0dde 100644 --- a/Docs/ProjectSpec.md +++ b/Docs/ProjectSpec.md @@ -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` diff --git a/Package.resolved b/Package.resolved index 237f873fa..671959c8b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -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" } }, { diff --git a/Package.swift b/Package.swift index 99c7649c0..ab86ad008 100644 --- a/Package.swift +++ b/Package.swift @@ -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") diff --git a/Sources/ProjectSpec/CacheFile.swift b/Sources/ProjectSpec/CacheFile.swift index c9c64692b..95e87014f 100644 --- a/Sources/ProjectSpec/CacheFile.swift +++ b/Sources/ProjectSpec/CacheFile.swift @@ -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") diff --git a/Sources/ProjectSpec/Project.swift b/Sources/ProjectSpec/Project.swift index 70fb9bc33..e872a27bf 100644 --- a/Sources/ProjectSpec/Project.swift +++ b/Sources/ProjectSpec/Project.swift @@ -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] { diff --git a/Sources/ProjectSpec/SourceType.swift b/Sources/ProjectSpec/SourceType.swift index 77ce4ffe7..9fe009321 100644 --- a/Sources/ProjectSpec/SourceType.swift +++ b/Sources/ProjectSpec/SourceType.swift @@ -11,4 +11,5 @@ public enum SourceType: String { case group case file case folder + case syncedFolder } diff --git a/Sources/ProjectSpec/SpecOptions.swift b/Sources/ProjectSpec/SpecOptions.swift index a5df4e28d..c10c34698 100644 --- a/Sources/ProjectSpec/SpecOptions.swift +++ b/Sources/ProjectSpec/SpecOptions.swift @@ -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 { diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index c7f5f2d1f..771e5b21c 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -686,8 +686,14 @@ public class PBXProjGenerator { let infoPlistFiles: [Config: String] = getInfoPlists(for: target) let sourceFileBuildPhaseOverrideSequence: [(Path, BuildPhaseSpec)] = Set(infoPlistFiles.values).map({ (project.basePath + $0, .none) }) let sourceFileBuildPhaseOverrides = Dictionary(uniqueKeysWithValues: sourceFileBuildPhaseOverrideSequence) - let sourceFiles = try sourceGenerator.getAllSourceFiles(targetType: target.type, sources: target.sources, buildPhases: sourceFileBuildPhaseOverrides) - .sorted { $0.path.lastComponent < $1.path.lastComponent } + let targetObject = targetObjects[target.name]! + let sourceFiles = try sourceGenerator.getAllSourceFiles( + target: targetObject, + targetType: target.type, + sources: target.sources, + buildPhases: sourceFileBuildPhaseOverrides + ) + .sorted { $0.path.lastComponent < $1.path.lastComponent } var anyDependencyRequiresObjCLinking = false @@ -1439,8 +1445,6 @@ public class PBXProjGenerator { defaultConfigurationName: defaultConfigurationName )) - let targetObject = targetObjects[target.name]! - let targetFileReference = targetFileReferences[target.name] targetObject.name = target.name @@ -1454,6 +1458,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? { diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index 0b8f93b70..5c8a20ab3 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -86,13 +86,13 @@ class SourceGenerator { /// - targetType: The type of target that the source files should belong to. /// - sources: The array of sources defined as part of the targets spec. /// - buildPhases: A dictionary containing any build phases that should be applied to source files at specific paths in the event that the associated `TargetSource` didn't already define a `buildPhase`. Values from this dictionary are used in cases where the project generator knows more about a file than the spec/filesystem does (i.e if the file should be treated as the targets Info.plist and so on). - func getAllSourceFiles(targetType: PBXProductType, sources: [TargetSource], buildPhases: [Path : BuildPhaseSpec]) throws -> [SourceFile] { - try sources.flatMap { try getSourceFiles(targetType: targetType, targetSource: $0, buildPhases: buildPhases) } + func getAllSourceFiles(target: PBXTarget, targetType: PBXProductType, sources: [TargetSource], buildPhases: [Path : BuildPhaseSpec]) throws -> [SourceFile] { + try sources.flatMap { try getSourceFiles(target: target, targetType: targetType, targetSource: $0, buildPhases: buildPhases) } } // get groups without build files. Use for Project.fileGroups func getFileGroups(path: String) throws { - _ = try getSourceFiles(targetType: .none, targetSource: TargetSource(path: path), buildPhases: [:]) + _ = try getSourceFiles(target: nil, targetType: .none, targetSource: TargetSource(path: path), buildPhases: [:]) } func getFileType(path: Path) -> FileType? { @@ -601,7 +601,7 @@ class SourceGenerator { } /// creates source files - private func getSourceFiles(targetType: PBXProductType, targetSource: TargetSource, buildPhases: [Path: BuildPhaseSpec]) throws -> [SourceFile] { + private func getSourceFiles(target: PBXTarget?, targetType: PBXProductType, targetSource: TargetSource, buildPhases: [Path: BuildPhaseSpec]) throws -> [SourceFile] { // generate excluded paths let path = project.basePath + targetSource.path @@ -687,6 +687,42 @@ class SourceGenerator { sourceFiles += groupSourceFiles sourceReference = group + case .syncedFolder: + + let relativePath = (try? path.relativePath(from: project.basePath)) ?? path + + var exceptions: [PBXFileSystemSynchronizedExceptionSet] = [] + let ignoredFiles = Array(buildPhases.filter { $0.value == .none }.keys) + if let target, !ignoredFiles.isEmpty { + let memberShipExceptions = ignoredFiles + .map { (try? $0.relativePath(from: project.basePath)) ?? $0 } + .map(\.string) + let exception = PBXFileSystemSynchronizedBuildFileExceptionSet(target: target, membershipExceptions: memberShipExceptions, publicHeaders: nil, privateHeaders: nil, additionalCompilerFlagsByRelativePath: nil, attributesByRelativePath: nil) + addObject(exception) + exceptions.append(exception) + } + let syncedRootGroup = PBXFileSystemSynchronizedRootGroup( + sourceTree: .group, + path: relativePath.string, + name: targetSource.name, + explicitFileTypes: [:], + exceptions: 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 +739,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) { diff --git a/Sources/XcodeGenKit/Version.swift b/Sources/XcodeGenKit/Version.swift index 57618046d..007d26f64 100644 --- a/Sources/XcodeGenKit/Version.swift +++ b/Sources/XcodeGenKit/Version.swift @@ -16,7 +16,7 @@ extension Project { } var objectVersion: UInt { - 54 + 70 } var minimizedProjectReferenceProxies: Int { diff --git a/Sources/XcodeGenKit/XCProjExtensions.swift b/Sources/XcodeGenKit/XCProjExtensions.swift index ec3f2763b..33d3f5b67 100644 --- a/Sources/XcodeGenKit/XCProjExtensions.swift +++ b/Sources/XcodeGenKit/XCProjExtensions.swift @@ -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 diff --git a/Tests/Fixtures/CarthageProject/Project.xcodeproj/project.pbxproj b/Tests/Fixtures/CarthageProject/Project.xcodeproj/project.pbxproj index eb6cf67fe..3289e3bdd 100644 --- a/Tests/Fixtures/CarthageProject/Project.xcodeproj/project.pbxproj +++ b/Tests/Fixtures/CarthageProject/Project.xcodeproj/project.pbxproj @@ -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 = ( diff --git a/Tests/Fixtures/SPM/SPM.xcodeproj/project.pbxproj b/Tests/Fixtures/SPM/SPM.xcodeproj/project.pbxproj index 59311f725..7918465d3 100644 --- a/Tests/Fixtures/SPM/SPM.xcodeproj/project.pbxproj +++ b/Tests/Fixtures/SPM/SPM.xcodeproj/project.pbxproj @@ -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 = ( diff --git a/Tests/Fixtures/SPM/SPM.xcodeproj/xcshareddata/xcschemes/App.xcscheme b/Tests/Fixtures/SPM/SPM.xcodeproj/xcshareddata/xcschemes/App.xcscheme index 353d320e0..298045bf8 100644 --- a/Tests/Fixtures/SPM/SPM.xcodeproj/xcshareddata/xcschemes/App.xcscheme +++ b/Tests/Fixtures/SPM/SPM.xcodeproj/xcshareddata/xcschemes/App.xcscheme @@ -40,7 +40,8 @@ + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO"> 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 } } diff --git a/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj b/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj index 1898ccc66..fe8e8c5ab 100644 --- a/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj +++ b/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXAggregateTarget section */ @@ -830,6 +830,31 @@ FED40A89162E446494DDE7C7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + "TEMP_067E2B5F-0F9F-4B71-A34B-DB4FEB8A01AC" /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + App_iOS/Info.plist, + ); + target = 0867B0DACEF28C11442DE8F7 /* App_iOS */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + AE2AB2772F70DFFF402AA02B /* SyncedFolder */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + "TEMP_067E2B5F-0F9F-4B71-A34B-DB4FEB8A01AC" /* PBXFileSystemSynchronizedBuildFileExceptionSet */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = SyncedFolder; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 117840B4DBC04099F6779D00 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -1050,6 +1075,7 @@ 2E1E747C7BC434ADB80CC269 /* Headers */, 6B1603BA83AA0C7B94E45168 /* ResourceFolder */, 6BBE762F36D94AB6FFBFE834 /* SomeFile */, + AE2AB2772F70DFFF402AA02B /* SyncedFolder */, 79DC4A1E4D2E0D3A215179BC /* Bundles */, FC1515684236259C50A7747F /* Frameworks */, AC523591AC7BE9275003D2DB /* Products */, @@ -1686,6 +1712,9 @@ E8C078B0A2A2B0E1D35694D5 /* PBXTargetDependency */, 981D116D40DBA0407D0E0E94 /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + AE2AB2772F70DFFF402AA02B /* SyncedFolder */, + ); name = App_iOS; packageProductDependencies = ( D7917D10F77DA9D69937D493 /* Swinject */, @@ -2443,7 +2472,7 @@ packageReferences = ( 4EDA79334592CBBA0E507AD2 /* XCRemoteSwiftPackageReference "Swinject" */, ); - preferredProjectObjectVersion = 54; + preferredProjectObjectVersion = 70; projectDirPath = ""; projectReferences = ( { diff --git a/Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_Clip.xcscheme b/Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_Clip.xcscheme index b5a5eda3a..6959dff88 100644 --- a/Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_Clip.xcscheme +++ b/Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_Clip.xcscheme @@ -40,7 +40,8 @@ + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO">