diff --git a/Fixtures/TestProject/NestedFiles/Foo/Nested.swift b/Fixtures/TestProject/NestedFiles/Foo/Nested.swift new file mode 100644 index 00000000..b5c22056 --- /dev/null +++ b/Fixtures/TestProject/NestedFiles/Foo/Nested.swift @@ -0,0 +1,3 @@ +func nested() -> String { + return "Nested" +} diff --git a/Fixtures/TestProject/Project.xcodeproj/project.pbxproj b/Fixtures/TestProject/Project.xcodeproj/project.pbxproj index 3af1b493..2a3c370c 100644 --- a/Fixtures/TestProject/Project.xcodeproj/project.pbxproj +++ b/Fixtures/TestProject/Project.xcodeproj/project.pbxproj @@ -165,6 +165,15 @@ FR7078510801 /* FrameworkFile.swift */, FR1345298503 /* Info.plist */, FR7740960501 /* MyFramework.h */, + FR7078510801 /* FrameworkFile.swift */, + FR1345298503 /* Info.plist */, + FR7740960501 /* MyFramework.h */, + FR7078510801 /* FrameworkFile.swift */, + FR1345298503 /* Info.plist */, + FR7740960501 /* MyFramework.h */, + FR7078510801 /* FrameworkFile.swift */, + FR1345298503 /* Info.plist */, + FR7740960501 /* MyFramework.h */, ); name = Framework; path = Framework; @@ -258,6 +267,9 @@ G82523211001 /* App_iOS */, G78312289901 /* App_iOS_Tests */, G46615002701 /* Framework */, + G46615002701 /* Framework */, + G46615002701 /* Framework */, + G46615002701 /* Framework */, G86202385201 /* Products */, G19527407101 /* Frameworks */, ); diff --git a/Sources/ProjectSpec/ProjectSpec.swift b/Sources/ProjectSpec/ProjectSpec.swift index 1cf01830..ad69f678 100644 --- a/Sources/ProjectSpec/ProjectSpec.swift +++ b/Sources/ProjectSpec/ProjectSpec.swift @@ -29,6 +29,7 @@ public struct ProjectSpec { public struct Options { public var carthageBuildPath: String? + public var createIntermediateGroups: Bool public var bundleIdPrefix: String? public var settingPresets: SettingPresets = .all @@ -54,6 +55,7 @@ public struct ProjectSpec { } public init() { + createIntermediateGroups = false } } @@ -160,5 +162,6 @@ extension ProjectSpec.Options: JSONObjectConvertible { carthageBuildPath = jsonDictionary.json(atKeyPath: "carthageBuildPath") bundleIdPrefix = jsonDictionary.json(atKeyPath: "bundleIdPrefix") settingPresets = jsonDictionary.json(atKeyPath: "settingPresets") ?? .all + createIntermediateGroups = jsonDictionary.json(atKeyPath: "createIntermediateGroups") ?? false } } diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index 4992666a..31b4bb01 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -512,6 +512,47 @@ public class PBXProjGenerator { return (fromFiles + fromDirs).flatMap { $0.sourceFiles } } + func getSingleGroup(path: Path, mergingChildren children: [String], depth: Int = 0) -> PBXGroup { + let group: PBXGroup + if let cachedGroup = groupsByPath[path] { + cachedGroup.children += children + group = cachedGroup + } else { + group = PBXGroup( + reference: generateUUID(PBXGroup.self, path.lastComponent), + children: children, + sourceTree: .group, + name: path.lastComponent, + path: depth == 0 && !spec.options.createIntermediateGroups ? + path.byRemovingBase(path: spec.basePath).string : + path.lastComponent + ) + addObject(group) + groupsByPath[path] = group + } + return group + } + + // Add groups for all parents recursively + // ex: path/foo/bar/baz/Hello.swift -> path:[foo:[bar:[baz:[Hello.swift]]]] + func getIntermediateGroups(path: Path, group: PBXGroup) -> PBXGroup { + // verify path is a subpath of spec.basePath + guard Path(components: zip(path.components, spec.basePath.components).map{ $0.0 }) == spec.basePath else { + return group + } + + // base case + if path == spec.basePath { + return group + } + + // recursive case + return getIntermediateGroups( + path: path.parent(), + group: getSingleGroup(path: path, mergingChildren: [group.reference]) + ) + } + func getSources(path: Path, children: [Path]? = nil, depth: Int = 0) throws -> (sourceFiles: [SourceFile], groups: [PBXGroup]) { let children = try children ?? (try path.children()) let excludedFiles: [String] = [".DS_Store"] @@ -605,17 +646,18 @@ public class PBXProjGenerator { } } - let groupPath: String = depth == 0 ? path.byRemovingBase(path: spec.basePath).string : path.lastComponent let group: PBXGroup - if let cachedGroup = groupsByPath[path] { - group = cachedGroup + if spec.options.createIntermediateGroups { + group = getIntermediateGroups( + path: path.parent(), + group: getSingleGroup(path: path, mergingChildren: groupChildren, depth: depth) + ) } else { - group = PBXGroup(reference: generateUUID(PBXGroup.self, path.lastComponent), children: groupChildren, sourceTree: .group, name: path.lastComponent, path: groupPath) - addObject(group) - if depth == 0 { - topLevelGroups.append(group) - } - groupsByPath[path] = group + group = getSingleGroup(path: path, mergingChildren: groupChildren, depth: depth) + } + + if depth == 0 { + topLevelGroups.append(group) } groups.insert(group, at: 0) return (allSourceFiles, groups) diff --git a/docs/ProjectSpec.md b/docs/ProjectSpec.md index 05001f7a..46001d34 100644 --- a/docs/ProjectSpec.md +++ b/docs/ProjectSpec.md @@ -63,6 +63,7 @@ Note that target names can also be changed by adding a `name` property to a targ ### Options - ⚪️ **carthageBuildPath**: `String` - The path to the carthage build directory. Defaults to `Carthage/Build`. This is used when specifying target carthage dependencies +- ⚪️ **createIntermediateGroups**: `String` - If this is specified and set to `true`, then intermediate groups will be created for every path component between the folder containing the source and the base path. For example, when enabled if a source path is specified as `Vendor/Foo/Hello.swift`, the group `Vendor` will created as a parent of the `Foo` group. - ⚪️ **bundleIdPrefix**: `String` - If this is specified then any target that doesn't have an `PRODUCT_BUNDLE_IDENTIFIER` (via all levels of build settings) will get an autogenerated one by combining `bundleIdPrefix` and the target name: `bundleIdPrefix.name`. The target name will be stripped of all characters that aren't alphanumerics, hyphens, or periods. Underscores will be replace with hyphens. - ⚪️ **settingPresets**: `String` - This controls the settings that are automatically applied to the project and its targets. These are the same build settings that Xcode would add when creating a new project. Project settings are applied by config type. Target settings are applied by the product type and platform. By default this is set to `all` - `all`: project and target settings