Skip to content

Commit 5f3fea4

Browse files
committed
Preserve target order
1 parent c0f5b32 commit 5f3fea4

17 files changed

Lines changed: 218 additions & 176 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
## Next Version
44

5-
### Added
6-
- Added `targetOrdering` option to control the order of targets in the generated project (sidebar in Xcode, `xcodebuild -list` output). Unlisted targets fall back to the existing alphabetical order, so behavior is unchanged when the option is not set. @mirkokg
5+
### Changed
6+
- Targets in the generated project now follow the declaration order from the YAML spec (Xcode sidebar, `xcodebuild -list` output). Previously they were always sorted alphabetically. JSON specs and projects built programmatically continue to sort alphabetically, since key order isn't recoverable. @mirkokg
77

88
## 2.45.4
99

Docs/ProjectSpec.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ Note that target names can also be changed by adding a `name` property to a targ
151151
- `top` - at the top, before files
152152
- `bottom` (default) - at the bottom, after other files
153153
- [ ] **groupOrdering**: **[[GroupOrdering]](#groupOrdering)** - An order of groups.
154-
- [ ] **targetOrdering**: **[String]** - An ordered list of target names. Listed targets appear in this order in the generated project (sidebar in Xcode, `xcodebuild -list` output); unlisted targets follow, sorted alphabetically. If a name in this list doesn't match any target or aggregate target, a validation error is raised. Defaults to an empty list, in which case all targets are sorted alphabetically.
155154
- [ ] **transitivelyLinkDependencies**: **Bool** - If this is `true` then targets will link to the dependencies of their target dependencies. If a target should embed its dependencies, such as application and test bundles, it will embed these transitive dependencies as well. Some complex setups might want to set this to `false` and explicitly specify dependencies at every level. Targets can override this with [Target](#target).transitivelyLinkDependencies. Defaults to `false`.
156155
- [ ] **generateEmptyDirectories**: **Bool** - If this is `true` then empty directories will be added to project too else will be missed. Defaults to `false`.
157156
- [ ] **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`.

Sources/ProjectSpec/Project.swift

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,19 @@ extension Project {
164164
}
165165

166166
public init(spec: SpecFile) throws {
167-
try self.init(basePath: spec.basePath, jsonDictionary: spec.resolvedDictionary())
167+
try self.init(
168+
basePath: spec.basePath,
169+
jsonDictionary: spec.resolvedDictionary(),
170+
targetDeclarationOrder: spec.resolvedTargetDeclarationOrder()
171+
)
168172
}
169173

170-
public init(basePath: Path = "", jsonDictionary: JSONDictionary) throws {
174+
public init(basePath: Path = "", jsonDictionary: JSONDictionary, targetDeclarationOrder: [String] = []) throws {
171175
self.basePath = basePath
172176

177+
let rawTargets = jsonDictionary["targets"] as? JSONDictionary ?? [:]
178+
let expandedTargetOrder = Project.expandedTargetOrder(targetDeclarationOrder, rawTargets: rawTargets)
179+
173180
let jsonDictionary = Project.resolveProject(jsonDictionary: jsonDictionary)
174181
let buildSettingsParser = BuildSettingsParser(jsonDictionary: jsonDictionary)
175182

@@ -181,7 +188,16 @@ extension Project {
181188
let configs: [String: String] = jsonDictionary.json(atKeyPath: "configs") ?? [:]
182189
self.configs = configs.isEmpty ? Config.defaultConfigs :
183190
configs.map { Config(name: $0, type: ConfigType(rawValue: $1)) }.sorted { $0.name < $1.name }
184-
targets = try jsonDictionary.json(atKeyPath: "targets", parallel: true).sorted { $0.name < $1.name }
191+
let parsedTargets: [Target] = try jsonDictionary.json(atKeyPath: "targets", parallel: true)
192+
let targetOrderIndex = Dictionary(uniqueKeysWithValues: expandedTargetOrder.enumerated().map { ($1, $0) })
193+
targets = parsedTargets.sorted { lhs, rhs in
194+
let lhsIndex = targetOrderIndex[lhs.name] ?? Int.max
195+
let rhsIndex = targetOrderIndex[rhs.name] ?? Int.max
196+
if lhsIndex != rhsIndex {
197+
return lhsIndex < rhsIndex
198+
}
199+
return lhs.name < rhs.name
200+
}
185201
aggregateTargets = try jsonDictionary.json(atKeyPath: "aggregateTargets").sorted { $0.name < $1.name }
186202
projectReferences = try jsonDictionary.json(atKeyPath: "projectReferences").sorted { $0.name < $1.name }
187203
schemes = try jsonDictionary.json(atKeyPath: "schemes")
@@ -218,6 +234,24 @@ extension Project {
218234
projectReferencesMap = Dictionary(uniqueKeysWithValues: projectReferences.map { ($0.name, $0) })
219235
}
220236

237+
static func expandedTargetOrder(_ declarationOrder: [String], rawTargets: JSONDictionary) -> [String] {
238+
var result: [String] = []
239+
for key in declarationOrder {
240+
guard let target = rawTargets[key] as? JSONDictionary else { continue }
241+
if let platforms = target["platform"] as? [String] {
242+
for platform in platforms {
243+
let prefix = target["platformPrefix"] as? String ?? ""
244+
let suffix = target["platformSuffix"] as? String ?? "_\(platform)"
245+
result.append(prefix + key + suffix)
246+
}
247+
} else {
248+
let resolvedName = target["name"] as? String ?? key
249+
result.append(resolvedName)
250+
}
251+
}
252+
return result
253+
}
254+
221255
static func resolveProject(jsonDictionary: JSONDictionary) -> JSONDictionary {
222256
var jsonDictionary = jsonDictionary
223257

Sources/ProjectSpec/SpecFile.swift

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public struct SpecFile {
99
public let basePath: Path
1010
public let jsonDictionary: JSONDictionary
1111
public let subSpecs: [SpecFile]
12+
public let targetDeclarationOrder: [String]
1213

1314
/// The relative path to use when resolving paths in the json dictionary. Is an empty path when
1415
/// included with relativePaths disabled.
@@ -68,12 +69,13 @@ public struct SpecFile {
6869
}
6970

7071
/// Memberwise initializer for SpecFile
71-
public init(filePath: Path, jsonDictionary: JSONDictionary, basePath: Path = "", relativePath: Path = "", subSpecs: [SpecFile] = []) {
72+
public init(filePath: Path, jsonDictionary: JSONDictionary, basePath: Path = "", relativePath: Path = "", subSpecs: [SpecFile] = [], targetDeclarationOrder: [String] = []) {
7273
self.basePath = basePath
7374
self.relativePath = relativePath
7475
self.jsonDictionary = jsonDictionary
7576
self.subSpecs = subSpecs
7677
self.filePath = filePath
78+
self.targetDeclarationOrder = targetDeclarationOrder
7779
}
7880

7981
private init(include: Include, basePath: Path, relativePath: Path, cachedSpecFiles: inout [Path: SpecFile], variables: [String: String]) throws {
@@ -91,6 +93,7 @@ public struct SpecFile {
9193
}
9294

9395
let jsonDictionary = try SpecFile.loadDictionary(path: path).expand(variables: variables)
96+
let targetDeclarationOrder = try SpecFile.loadTargetDeclarationOrder(path: path)
9497

9598
let includes = Include.parse(json: jsonDictionary["include"])
9699
let subSpecs: [SpecFile] = try includes
@@ -99,10 +102,17 @@ public struct SpecFile {
99102
return try SpecFile(include: include, basePath: basePath, relativePath: relativePath, cachedSpecFiles: &cachedSpecFiles, variables: variables)
100103
}
101104

102-
self.init(filePath: filePath, jsonDictionary: jsonDictionary, basePath: basePath, relativePath: relativePath, subSpecs: subSpecs)
105+
self.init(filePath: filePath, jsonDictionary: jsonDictionary, basePath: basePath, relativePath: relativePath, subSpecs: subSpecs, targetDeclarationOrder: targetDeclarationOrder)
103106
cachedSpecFiles[path] = self
104107
}
105108

109+
static func loadTargetDeclarationOrder(path: Path) throws -> [String] {
110+
if path.extension?.lowercased() == "json" {
111+
return []
112+
}
113+
return try loadOrderedTargetNames(path: path)
114+
}
115+
106116
static func loadDictionary(path: Path) throws -> JSONDictionary {
107117
// Depending on the extension we will either load the file as YAML or JSON
108118
if path.extension?.lowercased() == "json" {
@@ -121,6 +131,14 @@ public struct SpecFile {
121131
resolvedDictionaryWithUniqueTargets()
122132
}
123133

134+
public func resolvedTargetDeclarationOrder() -> [String] {
135+
var cachedSpecFiles: [Path: SpecFile] = [:]
136+
let resolvedSpec = resolvingPaths(cachedSpecFiles: &cachedSpecFiles)
137+
138+
var mergedSpecPaths = Set<Path>()
139+
return resolvedSpec.mergedTargetDeclarationOrder(set: &mergedSpecPaths)
140+
}
141+
124142
private func resolvedDictionaryWithUniqueTargets() -> JSONDictionary {
125143
var cachedSpecFiles: [Path: SpecFile] = [:]
126144
let resolvedSpec = resolvingPaths(cachedSpecFiles: &cachedSpecFiles)
@@ -140,6 +158,28 @@ public struct SpecFile {
140158
.reduce([:]) { $1.merged(onto: $0) })
141159
}
142160

161+
private func mergedTargetDeclarationOrder(set mergedSpecPaths: inout Set<Path>) -> [String] {
162+
let path = basePath + filePath
163+
164+
guard mergedSpecPaths.insert(path).inserted else { return [] }
165+
166+
var order: [String] = []
167+
var seen = Set<String>()
168+
for subSpec in subSpecs {
169+
for name in subSpec.mergedTargetDeclarationOrder(set: &mergedSpecPaths) {
170+
if seen.insert(name).inserted {
171+
order.append(name)
172+
}
173+
}
174+
}
175+
for name in targetDeclarationOrder {
176+
if seen.insert(name).inserted {
177+
order.append(name)
178+
}
179+
}
180+
return order
181+
}
182+
143183
private func resolvingPaths(cachedSpecFiles: inout [Path: SpecFile], relativeTo basePath: Path = Path()) -> SpecFile {
144184
let path = basePath + filePath
145185
if let cachedSpecFile = cachedSpecFiles[path] {
@@ -157,7 +197,8 @@ public struct SpecFile {
157197
jsonDictionary: jsonDictionary,
158198
basePath: self.basePath,
159199
relativePath: self.relativePath,
160-
subSpecs: subSpecs.map { $0.resolvingPaths(cachedSpecFiles: &cachedSpecFiles, relativeTo: relativePath) }
200+
subSpecs: subSpecs.map { $0.resolvingPaths(cachedSpecFiles: &cachedSpecFiles, relativeTo: relativePath) },
201+
targetDeclarationOrder: targetDeclarationOrder
161202
)
162203
cachedSpecFiles[path] = specFile
163204
return specFile

Sources/ProjectSpec/SpecLoader.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ public class SpecLoader {
1919
let projectRoot = projectRoot?.absolute()
2020
let spec = try SpecFile(path: path, projectRoot: projectRoot, variables: variables)
2121
let resolvedDictionary = spec.resolvedDictionary()
22-
let project = try Project(basePath: projectRoot ?? spec.basePath, jsonDictionary: resolvedDictionary)
22+
let project = try Project(
23+
basePath: projectRoot ?? spec.basePath,
24+
jsonDictionary: resolvedDictionary,
25+
targetDeclarationOrder: spec.resolvedTargetDeclarationOrder()
26+
)
2327

2428
self.project = project
2529
projectDictionary = resolvedDictionary

Sources/ProjectSpec/SpecOptions.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ public struct SpecOptions: Equatable {
3030
public var transitivelyLinkDependencies: Bool
3131
public var groupSortPosition: GroupSortPosition
3232
public var groupOrdering: [GroupOrdering]
33-
public var targetOrdering: [String]
3433
public var fileTypes: [String: FileType]
3534
public var generateEmptyDirectories: Bool
3635
public var findCarthageFrameworks: Bool
@@ -97,7 +96,6 @@ public struct SpecOptions: Equatable {
9796
transitivelyLinkDependencies: Bool = transitivelyLinkDependenciesDefault,
9897
groupSortPosition: GroupSortPosition = groupSortPositionDefault,
9998
groupOrdering: [GroupOrdering] = [],
100-
targetOrdering: [String] = [],
10199
fileTypes: [String: FileType] = [:],
102100
generateEmptyDirectories: Bool = generateEmptyDirectoriesDefault,
103101
findCarthageFrameworks: Bool = findCarthageFrameworksDefault,
@@ -126,7 +124,6 @@ public struct SpecOptions: Equatable {
126124
self.transitivelyLinkDependencies = transitivelyLinkDependencies
127125
self.groupSortPosition = groupSortPosition
128126
self.groupOrdering = groupOrdering
129-
self.targetOrdering = targetOrdering
130127
self.fileTypes = fileTypes
131128
self.generateEmptyDirectories = generateEmptyDirectories
132129
self.findCarthageFrameworks = findCarthageFrameworks
@@ -163,7 +160,6 @@ extension SpecOptions: JSONObjectConvertible {
163160
transitivelyLinkDependencies = jsonDictionary.json(atKeyPath: "transitivelyLinkDependencies") ?? SpecOptions.transitivelyLinkDependenciesDefault
164161
groupSortPosition = jsonDictionary.json(atKeyPath: "groupSortPosition") ?? SpecOptions.groupSortPositionDefault
165162
groupOrdering = jsonDictionary.json(atKeyPath: "groupOrdering") ?? []
166-
targetOrdering = jsonDictionary.json(atKeyPath: "targetOrdering") ?? []
167163
generateEmptyDirectories = jsonDictionary.json(atKeyPath: "generateEmptyDirectories") ?? SpecOptions.generateEmptyDirectoriesDefault
168164
findCarthageFrameworks = jsonDictionary.json(atKeyPath: "findCarthageFrameworks") ?? SpecOptions.findCarthageFrameworksDefault
169165
localPackagesGroup = jsonDictionary.json(atKeyPath: "localPackagesGroup")
@@ -222,9 +218,6 @@ extension SpecOptions: JSONEncodable {
222218
if schemePathPrefix != SpecOptions.schemePathPrefixDefault {
223219
dict["schemePathPrefix"] = schemePathPrefix
224220
}
225-
if !targetOrdering.isEmpty {
226-
dict["targetOrdering"] = targetOrdering
227-
}
228221

229222
return dict
230223
}

Sources/ProjectSpec/SpecValidation.swift

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,6 @@ extension Project {
7777
}
7878
}
7979

80-
if !options.targetOrdering.isEmpty {
81-
let knownTargetNames = Set(projectTargets.map { $0.name })
82-
var seen = Set<String>()
83-
for name in options.targetOrdering {
84-
if !seen.insert(name).inserted {
85-
errors.append(.duplicateTargetOrderingEntry(name))
86-
}
87-
if !knownTargetNames.contains(name) {
88-
errors.append(.invalidTargetOrderingReference(name))
89-
}
90-
}
91-
}
92-
9380
for settings in settingGroups.values {
9481
errors += validateSettings(settings)
9582
}

Sources/ProjectSpec/SpecValidationError.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ public struct SpecValidationError: Error, CustomStringConvertible {
4343
case duplicateDependencies(target: String, dependencyReference: String)
4444
case invalidPluginPackageReference(plugin: String, package: String)
4545
case emptySourcePath(target: String)
46-
case invalidTargetOrderingReference(String)
47-
case duplicateTargetOrderingEntry(String)
4846

4947
public var description: String {
5048
switch self {
@@ -114,10 +112,6 @@ public struct SpecValidationError: Error, CustomStringConvertible {
114112
return "Plugin \(plugin) has invalid package reference \(package)"
115113
case let .emptySourcePath(target):
116114
return "Target \(target.quoted) has an empty source path entry"
117-
case let .invalidTargetOrderingReference(name):
118-
return "Target ordering includes \(name.quoted) which is not a known target or aggregate target"
119-
case let .duplicateTargetOrderingEntry(name):
120-
return "Target ordering contains \(name.quoted) more than once"
121115
}
122116
}
123117
}

Sources/ProjectSpec/Yaml.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ public func loadYamlDictionary(path: Path) throws -> [String: Any] {
1717
return yaml as? [String: Any] ?? [:]
1818
}
1919

20+
public func loadOrderedTargetNames(path: Path) throws -> [String] {
21+
guard path.extension?.lowercased() != "json" else { return [] }
22+
let string: String = try path.read()
23+
guard !string.isEmpty else { return [] }
24+
guard let node = try Yams.compose(yaml: string),
25+
let rootMapping = node.mapping,
26+
let targetsNode = rootMapping["targets"],
27+
let targetsMapping = targetsNode.mapping else {
28+
return []
29+
}
30+
return targetsMapping.compactMap { key, _ in key.string }
31+
}
32+
2033
public func dumpYamlDictionary(_ dictionary: [String: Any], path: Path) throws {
2134
let uncluttered = (dictionary as [String: Any?]).removingEmptyArraysDictionariesAndNils()
2235
let string: String = try Yams.dump(object: uncluttered)

Sources/XcodeGenKit/PBXProjGenerator.swift

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -322,16 +322,9 @@ public class PBXProjGenerator {
322322
pbxProject.remotePackages = packageReferences.sorted { $0.key < $1.key }.map { $1 }
323323
pbxProject.localPackages = localPackageReferences.sorted { $0.key < $1.key }.map { $1 }
324324

325-
let allTargets: [PBXTarget] = targetObjects.valueArray + targetAggregateObjects.valueArray
326-
let targetOrdering = project.options.targetOrdering
327-
pbxProject.targets = allTargets.sorted { lhs, rhs in
328-
let lhsIndex = targetOrdering.firstIndex(of: lhs.name) ?? Int.max
329-
let rhsIndex = targetOrdering.firstIndex(of: rhs.name) ?? Int.max
330-
if lhsIndex != rhsIndex {
331-
return lhsIndex < rhsIndex
332-
}
333-
return lhs.name < rhs.name
334-
}
325+
let orderedNativeTargets: [PBXTarget] = project.targets.compactMap { targetObjects[$0.name] }
326+
let orderedAggregateTargets: [PBXTarget] = project.aggregateTargets.compactMap { targetAggregateObjects[$0.name] }
327+
pbxProject.targets = orderedNativeTargets + orderedAggregateTargets
335328
pbxProject.attributes = projectAttributes
336329
pbxProject.targetAttributes = generateTargetAttributes()
337330
return pbxProj

0 commit comments

Comments
 (0)