Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Next Version

### Changed
- Targets in the generated project now follow the declaration order from the source spec (Xcode sidebar, `xcodebuild -list` output). Previously they were always sorted alphabetically. Applies to both YAML and JSON specs. Declaration order is now also preserved for targets whose `platform`/`name` come from a target template and for targets whose key is a `${VARIABLE}`. @mirkokg

## 2.45.4

### Fixed
Expand Down
27 changes: 23 additions & 4 deletions Sources/ProjectSpec/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,25 @@ extension Project {
}

public init(spec: SpecFile) throws {
try self.init(basePath: spec.basePath, jsonDictionary: spec.resolvedDictionary())
try self.init(
basePath: spec.basePath,
jsonDictionary: spec.resolvedDictionary(),
targetDeclarationOrder: spec.resolvedTargetDeclarationOrder()
)
}

public init(basePath: Path = "", jsonDictionary: JSONDictionary) throws {
public init(basePath: Path = "", jsonDictionary: JSONDictionary, targetDeclarationOrder: [String] = []) throws {
self.basePath = basePath

let jsonDictionary = Project.resolveProject(jsonDictionary: jsonDictionary)
var jsonDictionary = jsonDictionary
if var rawTargets = jsonDictionary["targets"] as? [String: JSONDictionary] {
for (index, key) in targetDeclarationOrder.enumerated() where rawTargets[key] != nil {
rawTargets[key]?[Target.declarationIndexKey] = index
}
jsonDictionary["targets"] = rawTargets
}

jsonDictionary = Project.resolveProject(jsonDictionary: jsonDictionary)
let buildSettingsParser = BuildSettingsParser(jsonDictionary: jsonDictionary)

name = try jsonDictionary.json(atKeyPath: "name")
Expand All @@ -181,7 +193,14 @@ extension Project {
let configs: [String: String] = jsonDictionary.json(atKeyPath: "configs") ?? [:]
self.configs = configs.isEmpty ? Config.defaultConfigs :
configs.map { Config(name: $0, type: ConfigType(rawValue: $1)) }.sorted { $0.name < $1.name }
targets = try jsonDictionary.json(atKeyPath: "targets", parallel: true).sorted { $0.name < $1.name }
let parsedTargets: [Target] = try jsonDictionary.json(atKeyPath: "targets", parallel: true)
let resolvedTargets = jsonDictionary["targets"] as? [String: JSONDictionary] ?? [:]
var targetOrder: [String: Int] = [:]
for (key, target) in resolvedTargets {
let name = target["name"] as? String ?? key
targetOrder[name] = (target[Target.declarationIndexKey] as? Int) ?? .max
}
targets = parsedTargets.sorted { (targetOrder[$0.name] ?? .max, $0.name) < (targetOrder[$1.name] ?? .max, $1.name) }
aggregateTargets = try jsonDictionary.json(atKeyPath: "aggregateTargets").sorted { $0.name < $1.name }
projectReferences = try jsonDictionary.json(atKeyPath: "projectReferences").sorted { $0.name < $1.name }
schemes = try jsonDictionary.json(atKeyPath: "schemes")
Expand Down
36 changes: 28 additions & 8 deletions Sources/ProjectSpec/SpecFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public struct SpecFile {
public let basePath: Path
public let jsonDictionary: JSONDictionary
public let subSpecs: [SpecFile]
public let targetDeclarationOrder: [String]

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

/// Memberwise initializer for SpecFile
public init(filePath: Path, jsonDictionary: JSONDictionary, basePath: Path = "", relativePath: Path = "", subSpecs: [SpecFile] = []) {
public init(filePath: Path, jsonDictionary: JSONDictionary, basePath: Path = "", relativePath: Path = "", subSpecs: [SpecFile] = [], targetDeclarationOrder: [String] = []) {
self.basePath = basePath
self.relativePath = relativePath
self.jsonDictionary = jsonDictionary
self.subSpecs = subSpecs
self.filePath = filePath
self.targetDeclarationOrder = targetDeclarationOrder
}

private init(include: Include, basePath: Path, relativePath: Path, cachedSpecFiles: inout [Path: SpecFile], variables: [String: String]) throws {
Expand All @@ -90,7 +92,10 @@ public struct SpecFile {
return
}

let jsonDictionary = try SpecFile.loadDictionary(path: path).expand(variables: variables)
let contents: String = try path.read()
let jsonDictionary = try SpecFile.loadDictionary(path: path, contents: contents).expand(variables: variables)
let targetDeclarationOrder = loadOrderedTargetNames(contents: contents)
.map { [$0: ""].expand(variables: variables).keys.first ?? $0 }

let includes = Include.parse(json: jsonDictionary["include"])
let subSpecs: [SpecFile] = try includes
Expand All @@ -99,28 +104,32 @@ public struct SpecFile {
return try SpecFile(include: include, basePath: basePath, relativePath: relativePath, cachedSpecFiles: &cachedSpecFiles, variables: variables)
}

self.init(filePath: filePath, jsonDictionary: jsonDictionary, basePath: basePath, relativePath: relativePath, subSpecs: subSpecs)
self.init(filePath: filePath, jsonDictionary: jsonDictionary, basePath: basePath, relativePath: relativePath, subSpecs: subSpecs, targetDeclarationOrder: targetDeclarationOrder)
cachedSpecFiles[path] = self
}

static func loadDictionary(path: Path) throws -> JSONDictionary {
static func loadDictionary(path: Path, contents: String) throws -> JSONDictionary {
// Depending on the extension we will either load the file as YAML or JSON
if path.extension?.lowercased() == "json" {
let data: Data = try path.read()
let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
let jsonData = try JSONSerialization.jsonObject(with: Data(contents.utf8), options: .allowFragments)
guard let jsonDictionary = jsonData as? [String: Any] else {
fatalError("Invalid JSON at path \(path)")
}
return jsonDictionary
} else {
return try loadYamlDictionary(path: path)
return try loadYamlDictionary(contents: contents)
}
}

public func resolvedDictionary() -> JSONDictionary {
resolvedDictionaryWithUniqueTargets()
}

public func resolvedTargetDeclarationOrder() -> [String] {
var mergedSpecPaths = Set<Path>()
return mergedTargetDeclarationOrder(set: &mergedSpecPaths)
}

private func resolvedDictionaryWithUniqueTargets() -> JSONDictionary {
var cachedSpecFiles: [Path: SpecFile] = [:]
let resolvedSpec = resolvingPaths(cachedSpecFiles: &cachedSpecFiles)
Expand All @@ -140,6 +149,16 @@ public struct SpecFile {
.reduce([:]) { $1.merged(onto: $0) })
}

private func mergedTargetDeclarationOrder(set mergedSpecPaths: inout Set<Path>) -> [String] {
let path = basePath + filePath

guard mergedSpecPaths.insert(path).inserted else { return [] }

let fromSubSpecs = subSpecs.flatMap { $0.mergedTargetDeclarationOrder(set: &mergedSpecPaths) }
var seen = Set<String>()
return (fromSubSpecs + targetDeclarationOrder).filter { seen.insert($0).inserted }
}

private func resolvingPaths(cachedSpecFiles: inout [Path: SpecFile], relativeTo basePath: Path = Path()) -> SpecFile {
let path = basePath + filePath
if let cachedSpecFile = cachedSpecFiles[path] {
Expand All @@ -157,7 +176,8 @@ public struct SpecFile {
jsonDictionary: jsonDictionary,
basePath: self.basePath,
relativePath: self.relativePath,
subSpecs: subSpecs.map { $0.resolvingPaths(cachedSpecFiles: &cachedSpecFiles, relativeTo: relativePath) }
subSpecs: subSpecs.map { $0.resolvingPaths(cachedSpecFiles: &cachedSpecFiles, relativeTo: relativePath) },
targetDeclarationOrder: targetDeclarationOrder
)
cachedSpecFiles[path] = specFile
return specFile
Expand Down
6 changes: 5 additions & 1 deletion Sources/ProjectSpec/SpecLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ public class SpecLoader {
let projectRoot = projectRoot?.absolute()
let spec = try SpecFile(path: path, projectRoot: projectRoot, variables: variables)
let resolvedDictionary = spec.resolvedDictionary()
let project = try Project(basePath: projectRoot ?? spec.basePath, jsonDictionary: resolvedDictionary)
let project = try Project(
basePath: projectRoot ?? spec.basePath,
jsonDictionary: resolvedDictionary,
targetDeclarationOrder: spec.resolvedTargetDeclarationOrder()
)

self.project = project
projectDictionary = resolvedDictionary
Expand Down
14 changes: 10 additions & 4 deletions Sources/ProjectSpec/Target.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ extension Target: PathContainer {

extension Target {

static let declarationIndexKey = "__declarationIndex"

static func resolveMultiplatformTargets(jsonDictionary: JSONDictionary) -> JSONDictionary {
guard let targetsDictionary: [String: JSONDictionary] = jsonDictionary["targets"] as? [String: JSONDictionary] else {
return jsonDictionary
Expand All @@ -172,17 +174,15 @@ extension Target {
if let platforms = target["platform"] as? [String] {
for platform in platforms {
var platformTarget = target

/// This value is set to help us to check, in Target init, that there are no conflicts in the definition of the platforms. We want to ensure that the user didn't define, at the same time,
/// the new Xcode 14 supported destinations and the XcodeGen generation of Multiple Platform Targets (when you define the platform field as an array).
platformTarget["isMultiPlatformTarget"] = true

platformTarget = platformTarget.expand(variables: ["platform": platform])

platformTarget["platform"] = platform
let platformSuffix = platformTarget["platformSuffix"] as? String ?? "_\(platform)"
let platformPrefix = platformTarget["platformPrefix"] as? String ?? ""
let newTargetName = platformPrefix + targetName + platformSuffix
let newTargetName = multiplatformTargetName(fromExpanded: platformTarget, key: targetName, platform: platform)

var settings = platformTarget["settings"] as? JSONDictionary ?? [:]
if settings["configs"] != nil || settings["groups"] != nil || settings["base"] != nil {
Expand Down Expand Up @@ -212,6 +212,12 @@ extension Target {
merged["targets"] = crossPlatformTargets
return merged
}

static func multiplatformTargetName(fromExpanded expanded: JSONDictionary, key: String, platform: String) -> String {
let prefix = expanded["platformPrefix"] as? String ?? ""
let suffix = expanded["platformSuffix"] as? String ?? "_\(platform)"
return prefix + key + suffix
}
}

extension Target: Equatable {
Expand Down
19 changes: 17 additions & 2 deletions Sources/ProjectSpec/Yaml.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,34 @@ import Yams

public func loadYamlDictionary(path: Path) throws -> [String: Any] {
let string: String = try path.read()
if string == "" {
return try loadYamlDictionary(contents: string)
}

public func loadYamlDictionary(contents: String) throws -> [String: Any] {
if contents.isEmpty {
return [:]
}

let resolver = Resolver.default
.removing(.null) // remove rule so that empty quotes are treated as empty strings

guard let yaml = try Yams.load(yaml: string, resolver) else {
guard let yaml = try Yams.load(yaml: contents, resolver) else {
return [:]
}
return yaml as? [String: Any] ?? [:]
}

public func loadOrderedTargetNames(contents: String) -> [String] {
guard !contents.isEmpty else { return [] }
guard let node = try? Yams.compose(yaml: contents),
let rootMapping = node.mapping,
let targetsNode = rootMapping["targets"],
let targetsMapping = targetsNode.mapping else {
return []
}
return targetsMapping.compactMap { key, _ in key.string }
}

public func dumpYamlDictionary(_ dictionary: [String: Any], path: Path) throws {
let uncluttered = (dictionary as [String: Any?]).removingEmptyArraysDictionariesAndNils()
let string: String = try Yams.dump(object: uncluttered)
Expand Down
6 changes: 3 additions & 3 deletions Sources/XcodeGenKit/PBXProjGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,9 @@ public class PBXProjGenerator {
pbxProject.remotePackages = packageReferences.sorted { $0.key < $1.key }.map { $1 }
pbxProject.localPackages = localPackageReferences.sorted { $0.key < $1.key }.map { $1 }

let allTargets: [PBXTarget] = targetObjects.valueArray + targetAggregateObjects.valueArray
pbxProject.targets = allTargets
.sorted { $0.name < $1.name }
let orderedNativeTargets: [PBXTarget] = project.targets.compactMap { targetObjects[$0.name] }
let orderedAggregateTargets: [PBXTarget] = project.aggregateTargets.compactMap { targetAggregateObjects[$0.name] }
pbxProject.targets = orderedNativeTargets + orderedAggregateTargets
pbxProject.attributes = projectAttributes
pbxProject.targetAttributes = generateTargetAttributes()
return pbxProj
Expand Down
4 changes: 2 additions & 2 deletions Tests/Fixtures/SPM/SPM.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,10 @@
projectDirPath = "";
projectRoot = "";
targets = (
ADD3CE771A0D5E996031A193 /* AggTarget */,
C99E3C420D63D5219CE57E33 /* App */,
3F8D94C4EFC431F646AAFB28 /* StaticLibrary */,
339863E54E2D955C00B56802 /* Tests */,
3F8D94C4EFC431F646AAFB28 /* StaticLibrary */,
ADD3CE771A0D5E996031A193 /* AggTarget */,
);
};
/* End PBXProject section */
Expand Down
62 changes: 31 additions & 31 deletions Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2509,50 +2509,50 @@
);
projectRoot = "";
targets = (
D137C04B64B7052419A2DF4E /* App_Clip */,
63BFF75AA22335E3DDD5E26A /* App_Clip_Tests */,
91C3E922A8482E07649971B9 /* App_Clip_UITests */,
0867B0DACEF28C11442DE8F7 /* App_iOS */,
DC2F16BAA6E13B44AB62F888 /* App_iOS_Tests */,
F674B2CFC4738EEC49BAD0DA /* App_iOS_UITests */,
020A320BB3736FCDE6CC4E70 /* App_macOS */,
71E2BDAC4B8E8FC2BBF75C55 /* App_macOS_Tests */,
C0570E2FB50D830D8D423396 /* App_supportedDestinations */,
208179651927D1138D19B5AD /* App_watchOS */,
307AE3FA155FFD09B74AE351 /* App_watchOS Extension */,
DA40AB367B606CCE2FDD398D /* BundleX */,
271CAC331D24F4F7E12C819C /* BundleY */,
0CE8BE7C80651629EC056066 /* CrossOverlayFramework_iOS */,
2F4FEAEFD7EE82DC49D899FC /* CrossOverlayFramework_macOS */,
3BF66A4668DFAF9338791F60 /* CrossOverlayFramework_tvOS */,
C7F90FD0FAAF232B3E015D38 /* CrossOverlayFramework_watchOS */,
428715FBC1D86458DA70CBDE /* DriverKitDriver */,
9F551F66949B55E8328EB995 /* EndpointSecuritySystemExtension */,
B61ED4688789B071275E2B7A /* EntitledApp */,
E7454C10EA126A93537DD57E /* ExternalTarget */,
CE7D183D3752B5B35D2D8E6D /* Framework2_iOS */,
FC26AF2506D3B2B40DE8A5F8 /* Framework2_macOS */,
8B9A14DC280CCE013CC86440 /* Framework2_tvOS */,
6ED01BC471A8C3642258E178 /* Framework2_watchOS */,
AE3F93DB94E7208F2F1D9A78 /* Framework_iOS */,
53A3B531E3947D8A8722745E /* Framework_macOS */,
536ACF18E4603B59207D43CE /* Framework_tvOS */,
71B5187E710718C1A205D4DC /* Framework_watchOS */,
700328EE1570207608D6ADB3 /* IncludedLegacy */,
72C923899DE05F1281872160 /* Legacy */,
020A320BB3736FCDE6CC4E70 /* App_macOS */,
0867B0DACEF28C11442DE8F7 /* App_iOS */,
B61ED4688789B071275E2B7A /* EntitledApp */,
C0570E2FB50D830D8D423396 /* App_supportedDestinations */,
208179651927D1138D19B5AD /* App_watchOS */,
307AE3FA155FFD09B74AE351 /* App_watchOS Extension */,
834F55973F05AC8A18144DB0 /* iMessageApp */,
1C26A6A0BC446191F311D470 /* iMessageExtension */,
192C574D74079A99AF1AD0B1 /* iMessageStickersExtension */,
428715FBC1D86458DA70CBDE /* DriverKitDriver */,
AD28397BCC984F769EE8A937 /* NetworkSystemExtension */,
9F551F66949B55E8328EB995 /* EndpointSecuritySystemExtension */,
13E8C5AB873CEE21E18E552F /* StaticLibrary_ObjC_iOS */,
578C80E461E675508CED5DC3 /* StaticLibrary_ObjC_macOS */,
93542A75A613F00FDB5C9C63 /* StaticLibrary_ObjC_tvOS */,
7D3D92034F4F203C140574F0 /* StaticLibrary_ObjC_watchOS */,
19BFB84599B0AA1275A9662D /* StaticLibrary_Swift */,
BF3693DCA6182D7AEC410AFC /* SuperTarget */,
AE3F93DB94E7208F2F1D9A78 /* Framework_iOS */,
53A3B531E3947D8A8722745E /* Framework_macOS */,
536ACF18E4603B59207D43CE /* Framework_tvOS */,
71B5187E710718C1A205D4DC /* Framework_watchOS */,
CE7D183D3752B5B35D2D8E6D /* Framework2_iOS */,
FC26AF2506D3B2B40DE8A5F8 /* Framework2_macOS */,
8B9A14DC280CCE013CC86440 /* Framework2_tvOS */,
6ED01BC471A8C3642258E178 /* Framework2_watchOS */,
0636AAF06498C336E1CEEDE4 /* TestFramework */,
090315A24AC8E8B8F318A709 /* Tool */,
0CE8BE7C80651629EC056066 /* CrossOverlayFramework_iOS */,
2F4FEAEFD7EE82DC49D899FC /* CrossOverlayFramework_macOS */,
3BF66A4668DFAF9338791F60 /* CrossOverlayFramework_tvOS */,
C7F90FD0FAAF232B3E015D38 /* CrossOverlayFramework_watchOS */,
DC2F16BAA6E13B44AB62F888 /* App_iOS_Tests */,
F674B2CFC4738EEC49BAD0DA /* App_iOS_UITests */,
71E2BDAC4B8E8FC2BBF75C55 /* App_macOS_Tests */,
E7815F2F0D9CDECF9185AAF3 /* XPC Service */,
834F55973F05AC8A18144DB0 /* iMessageApp */,
1C26A6A0BC446191F311D470 /* iMessageExtension */,
192C574D74079A99AF1AD0B1 /* iMessageStickersExtension */,
090315A24AC8E8B8F318A709 /* Tool */,
D137C04B64B7052419A2DF4E /* App_Clip */,
63BFF75AA22335E3DDD5E26A /* App_Clip_Tests */,
91C3E922A8482E07649971B9 /* App_Clip_UITests */,
BF3693DCA6182D7AEC410AFC /* SuperTarget */,
);
};
/* End PBXProject section */
Expand Down
12 changes: 12 additions & 0 deletions Tests/Fixtures/target_ordering_template_name_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: TargetOrderingTemplateName
targets:
Zebra:
type: framework
platform: iOS
templates: [Renamed]
Apple:
type: framework
platform: iOS
targetTemplates:
Renamed:
name: Zulu
11 changes: 11 additions & 0 deletions Tests/Fixtures/target_ordering_template_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: TargetOrderingTemplate
targets:
Zebra:
type: framework
templates: [CrossPlatform]
Apple:
type: framework
platform: iOS
targetTemplates:
CrossPlatform:
platform: [iOS, tvOS]
Loading