Skip to content
Open
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. @mirkokg

## 2.45.4

### Fixed
Expand Down
22 changes: 19 additions & 3 deletions Sources/ProjectSpec/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,19 @@ 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 rawTargets = jsonDictionary["targets"] as? JSONDictionary ?? [:]
let expandedTargetOrder = Project.expandedTargetOrder(targetDeclarationOrder, rawTargets: rawTargets)

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

Expand All @@ -181,7 +188,9 @@ 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 targetOrderIndex = Dictionary(uniqueKeysWithValues: expandedTargetOrder.enumerated().map { ($1, $0) })
targets = parsedTargets.sorted { (targetOrderIndex[$0.name] ?? .max, $0.name) < (targetOrderIndex[$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 Expand Up @@ -218,6 +227,13 @@ extension Project {
projectReferencesMap = Dictionary(uniqueKeysWithValues: projectReferences.map { ($0.name, $0) })
}

static func expandedTargetOrder(_ declarationOrder: [String], rawTargets: JSONDictionary) -> [String] {
declarationOrder.flatMap { key -> [String] in
guard let target = rawTargets[key] as? JSONDictionary else { return [] }
return Target.resolvedNames(forRawTarget: target, key: key)
}
}

static func resolveProject(jsonDictionary: JSONDictionary) -> JSONDictionary {
var jsonDictionary = jsonDictionary

Expand Down
38 changes: 30 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,9 @@ 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 = try loadOrderedTargetNames(contents: contents)

let includes = Include.parse(json: jsonDictionary["include"])
let subSpecs: [SpecFile] = try includes
Expand All @@ -99,28 +103,35 @@ 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 cachedSpecFiles: [Path: SpecFile] = [:]
let resolvedSpec = resolvingPaths(cachedSpecFiles: &cachedSpecFiles)

var mergedSpecPaths = Set<Path>()
return resolvedSpec.mergedTargetDeclarationOrder(set: &mergedSpecPaths)
}

private func resolvedDictionaryWithUniqueTargets() -> JSONDictionary {
var cachedSpecFiles: [Path: SpecFile] = [:]
let resolvedSpec = resolvingPaths(cachedSpecFiles: &cachedSpecFiles)
Expand All @@ -140,6 +151,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 +178,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
20 changes: 17 additions & 3 deletions Sources/ProjectSpec/Target.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,7 @@ extension Target {
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 +210,22 @@ extension Target {
merged["targets"] = crossPlatformTargets
return merged
}

static func resolvedNames(forRawTarget dict: JSONDictionary, key: String) -> [String] {
if let platforms = dict["platform"] as? [String] {
return platforms.map { platform in
let expanded = dict.expand(variables: ["platform": platform])
return multiplatformTargetName(fromExpanded: expanded, key: key, platform: platform)
}
}
return [dict["name"] as? String ?? key]
}

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
24 changes: 22 additions & 2 deletions Sources/ProjectSpec/Yaml.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,39 @@ 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(path: Path) throws -> [String] {
let string: String = try path.read()
return try loadOrderedTargetNames(contents: string)
}

public func loadOrderedTargetNames(contents: String) throws -> [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
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,9 @@
projectRoot = "";
targets = (
AE3F93DB94E7208F2F1D9A78 /* Framework_iOS */,
53A3B531E3947D8A8722745E /* Framework_macOS */,
536ACF18E4603B59207D43CE /* Framework_tvOS */,
71B5187E710718C1A205D4DC /* Framework_watchOS */,
53A3B531E3947D8A8722745E /* Framework_macOS */,
);
};
/* End PBXProject section */
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
64 changes: 32 additions & 32 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 */,
578C80E461E675508CED5DC3 /* StaticLibrary_ObjC_macOS */,
19BFB84599B0AA1275A9662D /* StaticLibrary_Swift */,
BF3693DCA6182D7AEC410AFC /* SuperTarget */,
AE3F93DB94E7208F2F1D9A78 /* Framework_iOS */,
536ACF18E4603B59207D43CE /* Framework_tvOS */,
71B5187E710718C1A205D4DC /* Framework_watchOS */,
53A3B531E3947D8A8722745E /* Framework_macOS */,
CE7D183D3752B5B35D2D8E6D /* Framework2_iOS */,
8B9A14DC280CCE013CC86440 /* Framework2_tvOS */,
6ED01BC471A8C3642258E178 /* Framework2_watchOS */,
FC26AF2506D3B2B40DE8A5F8 /* Framework2_macOS */,
0636AAF06498C336E1CEEDE4 /* TestFramework */,
090315A24AC8E8B8F318A709 /* Tool */,
0CE8BE7C80651629EC056066 /* CrossOverlayFramework_iOS */,
3BF66A4668DFAF9338791F60 /* CrossOverlayFramework_tvOS */,
C7F90FD0FAAF232B3E015D38 /* CrossOverlayFramework_watchOS */,
2F4FEAEFD7EE82DC49D899FC /* CrossOverlayFramework_macOS */,
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
21 changes: 21 additions & 0 deletions Tests/Fixtures/target_ordering_test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "TargetOrdering",
"targets": {
"Zebra": {
"type": "framework",
"platform": "iOS"
},
"Apple": {
"type": "framework",
"platform": "iOS"
},
"Mango": {
"type": "framework",
"platform": "iOS"
},
"Banana": {
"type": "framework",
"platform": "iOS"
}
}
}
Loading