From e4f21d13d6963a0bf20421215525c71f206ee3dc Mon Sep 17 00:00:00 2001 From: Vahan Babayan Date: Fri, 17 Jul 2026 18:42:04 -0400 Subject: [PATCH] Add options.createLocalPackageGroups to suppress local package folder references --- CHANGELOG.md | 5 +++ Docs/ProjectSpec.md | 3 +- Sources/ProjectSpec/SpecOptions.swift | 8 ++++ Sources/XcodeGenKit/PBXProjGenerator.swift | 4 +- Tests/ProjectSpecTests/ProjectSpecTests.swift | 3 +- Tests/ProjectSpecTests/SpecLoadingTests.swift | 12 ++++++ .../PBXProjGeneratorTests.swift | 43 +++++++++++++++++++ 7 files changed, 75 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4886713..1f859dd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## Next + +### Added +- Added `options.createLocalPackageGroups` (default `true`). When set to `false`, local Swift packages are referenced only via `XCLocalSwiftPackageReference` (shown under "Package Dependencies") without an additional folder/group in the navigator. # @vahanbabayan-bloom + ## 2.46.0 ### Added diff --git a/Docs/ProjectSpec.md b/Docs/ProjectSpec.md index d8c42780..56727b79 100644 --- a/Docs/ProjectSpec.md +++ b/Docs/ProjectSpec.md @@ -155,6 +155,7 @@ Note that target names can also be changed by adding a `name` property to a targ - [ ] **generateEmptyDirectories**: **Bool** - If this is `true` then empty directories will be added to project too else will be missed. Defaults to `false`. - [ ] **findCarthageFrameworks**: **Bool** - When this is set to `true`, all the individual frameworks for Carthage framework dependencies will automatically be found. This property can be overridden individually for each carthage dependency - for more details see See **findFrameworks** in the [Dependency](#dependency) section. Defaults to `false`. - [ ] **localPackagesGroup**: **String** - The group name that local packages are put into. This defaults to `Packages`. Use `""` to specify the project root. +- [ ] **createLocalPackageGroups**: **Bool** - Whether local packages get a browsable folder/group in the project navigator in addition to the Package Dependencies entry. Defaults to `true`. Set to `false` to show local packages only under "Package Dependencies" (matching Xcode's "Add Local…" behavior). - [ ] **fileTypes**: **[String: [FileType](#filetype)]** - A list of default file options for specific file extensions across the project. Values in [Sources](#sources) will overwrite these settings. - [ ] **preGenCommand**: **String** - A bash command to run before 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 generating resources files before the project is regenerated. - [ ] **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. @@ -1261,7 +1262,7 @@ Swift packages are defined at a project level, and then linked to individual tar ### Local Package - [x] **path**: **String** - the path to the package in local. The path must be directory with a `Package.swift`. -- [ ] **group** : **String**- Optional path that specifies the location where the package will live in your xcode project. Use `""` to specify the project root. +- [ ] **group** : **String**- Optional path that specifies the location where the package will live in your xcode project. Use `""` to specify the project root. To not create any group for local packages, set [Options](#options).createLocalPackageGroups to `false`. - [ ] **excludeFromProject** : **String**- Optional flag to exclude the package from the generated project (useful if the package is already added via xcworkspace and the project is not intended for standalone use), defaults to `false` - [ ] **traits**: **[String]** - Optional Swift package traits to enable for this package reference. Use the top-level `packages` mapping because the legacy `localPackages` syntax cannot specify traits. diff --git a/Sources/ProjectSpec/SpecOptions.swift b/Sources/ProjectSpec/SpecOptions.swift index 07582d42..ed75675f 100644 --- a/Sources/ProjectSpec/SpecOptions.swift +++ b/Sources/ProjectSpec/SpecOptions.swift @@ -10,6 +10,7 @@ public struct SpecOptions: Equatable { public static let generateEmptyDirectoriesDefault = false public static let findCarthageFrameworksDefault = false public static let useBaseInternationalizationDefault = true + public static let createLocalPackageGroupsDefault = true public static let schemePathPrefixDefault = "../../" public var minimumXcodeGenVersion: Version? @@ -34,6 +35,7 @@ public struct SpecOptions: Equatable { public var generateEmptyDirectories: Bool public var findCarthageFrameworks: Bool public var localPackagesGroup: String? + public var createLocalPackageGroups: Bool public var preGenCommand: String? public var postGenCommand: String? public var useBaseInternationalization: Bool @@ -100,6 +102,7 @@ public struct SpecOptions: Equatable { generateEmptyDirectories: Bool = generateEmptyDirectoriesDefault, findCarthageFrameworks: Bool = findCarthageFrameworksDefault, localPackagesGroup: String? = nil, + createLocalPackageGroups: Bool = createLocalPackageGroupsDefault, preGenCommand: String? = nil, postGenCommand: String? = nil, useBaseInternationalization: Bool = useBaseInternationalizationDefault, @@ -128,6 +131,7 @@ public struct SpecOptions: Equatable { self.generateEmptyDirectories = generateEmptyDirectories self.findCarthageFrameworks = findCarthageFrameworks self.localPackagesGroup = localPackagesGroup + self.createLocalPackageGroups = createLocalPackageGroups self.preGenCommand = preGenCommand self.postGenCommand = postGenCommand self.useBaseInternationalization = useBaseInternationalization @@ -163,6 +167,7 @@ extension SpecOptions: JSONObjectConvertible { generateEmptyDirectories = jsonDictionary.json(atKeyPath: "generateEmptyDirectories") ?? SpecOptions.generateEmptyDirectoriesDefault findCarthageFrameworks = jsonDictionary.json(atKeyPath: "findCarthageFrameworks") ?? SpecOptions.findCarthageFrameworksDefault localPackagesGroup = jsonDictionary.json(atKeyPath: "localPackagesGroup") + createLocalPackageGroups = jsonDictionary.json(atKeyPath: "createLocalPackageGroups") ?? SpecOptions.createLocalPackageGroupsDefault preGenCommand = jsonDictionary.json(atKeyPath: "preGenCommand") postGenCommand = jsonDictionary.json(atKeyPath: "postGenCommand") useBaseInternationalization = jsonDictionary.json(atKeyPath: "useBaseInternationalization") ?? SpecOptions.useBaseInternationalizationDefault @@ -218,6 +223,9 @@ extension SpecOptions: JSONEncodable { if schemePathPrefix != SpecOptions.schemePathPrefixDefault { dict["schemePathPrefix"] = schemePathPrefix } + if createLocalPackageGroups != SpecOptions.createLocalPackageGroupsDefault { + dict["createLocalPackageGroups"] = createLocalPackageGroups + } return dict } diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index 604ff473..7df4973c 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -177,7 +177,9 @@ public class PBXProjGenerator { if !excludeFromProject { addObject(packageReference) - try sourceGenerator.createLocalPackage(path: Path(path), group: group.map { Path($0) }) + if project.options.createLocalPackageGroups { + try sourceGenerator.createLocalPackage(path: Path(path), group: group.map { Path($0) }) + } } } } diff --git a/Tests/ProjectSpecTests/ProjectSpecTests.swift b/Tests/ProjectSpecTests/ProjectSpecTests.swift index c308df96..8234534b 100644 --- a/Tests/ProjectSpecTests/ProjectSpecTests.swift +++ b/Tests/ProjectSpecTests/ProjectSpecTests.swift @@ -854,7 +854,8 @@ class ProjectSpecTests: XCTestCase { transitivelyLinkDependencies: true, groupSortPosition: .top, generateEmptyDirectories: true, - findCarthageFrameworks: false), + findCarthageFrameworks: false, + createLocalPackageGroups: false), fileGroups: ["foo", "bar"], configFiles: ["configFiles": "bar"], attributes: ["attributes": "bar"]) diff --git a/Tests/ProjectSpecTests/SpecLoadingTests.swift b/Tests/ProjectSpecTests/SpecLoadingTests.swift index 54875e75..169361c6 100644 --- a/Tests/ProjectSpecTests/SpecLoadingTests.swift +++ b/Tests/ProjectSpecTests/SpecLoadingTests.swift @@ -1526,6 +1526,18 @@ class SpecLoadingTests: XCTestCase { try expect(parsedSpec) == expected } + $0.it("parses createLocalPackageGroups option") { + let defaultSpec = try getProjectSpec(["options": [String: Any]()]) + try expect(defaultSpec.options.createLocalPackageGroups) == true + + let expected = Project(name: "test", options: .init(createLocalPackageGroups: false)) + let dictionary: [String: Any] = ["options": [ + "createLocalPackageGroups": false, + ]] + let parsedSpec = try getProjectSpec(dictionary) + try expect(parsedSpec) == expected + } + $0.it("parses packages") { let project = Project(name: "spm", packages: [ "package1": .remote(url: "package.git", versionRequirement: .exact("1.2.2"), traits: ["FeatureA", "FeatureB"]), diff --git a/Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift b/Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift index 80b083e1..6d8e8b6e 100644 --- a/Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift @@ -350,6 +350,49 @@ class PBXProjGeneratorTests: XCTestCase { try expect(packages) == ["FeatureA", "FeatureB", "Common"] } + $0.it("doesn't create groups for local packages when createLocalPackageGroups is disabled") { + var options = SpecOptions() + options.createLocalPackageGroups = false + + let directories = """ + Sources: + - file.swift + Packages: + - Common: + - Package.swift + - FeatureA: + - Package.swift + - FeatureB: + - Package.swift + """ + try createDirectories(directories) + + let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"]) + let project = Project( + basePath: directoryPath, + name: "Test", + targets: [target], + packages: [ + "Common": .local(path: "Packages/Common", group: nil, excludeFromProject: false), + "FeatureA": .local(path: "Packages/FeatureA", group: nil, excludeFromProject: false), + "FeatureB": .local(path: "Packages/FeatureB", group: nil, excludeFromProject: false), + ], + options: options + ) + + let pbxProj = try project.generatePbxProj() + let group = try pbxProj.getMainGroup() + + let mainGroups = group.children.map { $0.nameOrPath } + try expect(mainGroups.contains("Packages")) == false + + let folderReferences = pbxProj.fileReferences.filter { $0.lastKnownFileType == "folder" } + try expect(folderReferences.isEmpty) == true + + let localPackages = pbxProj.rootObject?.localPackages ?? [] + try expect(localPackages.map(\.relativePath).sorted()) == ["Packages/Common", "Packages/FeatureA", "Packages/FeatureB"] + } + $0.it("sorts synced folders alongside groups") { var options = SpecOptions() options.groupSortPosition = .top