diff --git a/CHANGELOG.md b/CHANGELOG.md index f876da2e..772e380c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/ProjectSpec/Project.swift b/Sources/ProjectSpec/Project.swift index d9bd7682..5244d28d 100644 --- a/Sources/ProjectSpec/Project.swift +++ b/Sources/ProjectSpec/Project.swift @@ -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") @@ -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") diff --git a/Sources/ProjectSpec/SpecFile.swift b/Sources/ProjectSpec/SpecFile.swift index 18ea1562..43764526 100644 --- a/Sources/ProjectSpec/SpecFile.swift +++ b/Sources/ProjectSpec/SpecFile.swift @@ -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. @@ -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 { @@ -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 @@ -99,21 +104,20 @@ 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) } } @@ -121,6 +125,11 @@ public struct SpecFile { resolvedDictionaryWithUniqueTargets() } + public func resolvedTargetDeclarationOrder() -> [String] { + var mergedSpecPaths = Set() + return mergedTargetDeclarationOrder(set: &mergedSpecPaths) + } + private func resolvedDictionaryWithUniqueTargets() -> JSONDictionary { var cachedSpecFiles: [Path: SpecFile] = [:] let resolvedSpec = resolvingPaths(cachedSpecFiles: &cachedSpecFiles) @@ -140,6 +149,16 @@ public struct SpecFile { .reduce([:]) { $1.merged(onto: $0) }) } + private func mergedTargetDeclarationOrder(set mergedSpecPaths: inout Set) -> [String] { + let path = basePath + filePath + + guard mergedSpecPaths.insert(path).inserted else { return [] } + + let fromSubSpecs = subSpecs.flatMap { $0.mergedTargetDeclarationOrder(set: &mergedSpecPaths) } + var seen = Set() + 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] { @@ -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 diff --git a/Sources/ProjectSpec/SpecLoader.swift b/Sources/ProjectSpec/SpecLoader.swift index fe93e840..56c31a5c 100644 --- a/Sources/ProjectSpec/SpecLoader.swift +++ b/Sources/ProjectSpec/SpecLoader.swift @@ -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 diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index 781994f4..9d94c7d8 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -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 @@ -172,7 +174,7 @@ 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 @@ -180,9 +182,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 { @@ -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 { diff --git a/Sources/ProjectSpec/Yaml.swift b/Sources/ProjectSpec/Yaml.swift index 89014f66..6dcb5966 100644 --- a/Sources/ProjectSpec/Yaml.swift +++ b/Sources/ProjectSpec/Yaml.swift @@ -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) diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index d561aeaf..604ff473 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -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 diff --git a/Tests/Fixtures/SPM/SPM.xcodeproj/project.pbxproj b/Tests/Fixtures/SPM/SPM.xcodeproj/project.pbxproj index 2799afcc..8eb4f2d5 100644 --- a/Tests/Fixtures/SPM/SPM.xcodeproj/project.pbxproj +++ b/Tests/Fixtures/SPM/SPM.xcodeproj/project.pbxproj @@ -267,10 +267,10 @@ projectDirPath = ""; projectRoot = ""; targets = ( - ADD3CE771A0D5E996031A193 /* AggTarget */, C99E3C420D63D5219CE57E33 /* App */, - 3F8D94C4EFC431F646AAFB28 /* StaticLibrary */, 339863E54E2D955C00B56802 /* Tests */, + 3F8D94C4EFC431F646AAFB28 /* StaticLibrary */, + ADD3CE771A0D5E996031A193 /* AggTarget */, ); }; /* End PBXProject section */ diff --git a/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj b/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj index 8a2c46fc..bbfb8e41 100644 --- a/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj +++ b/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj @@ -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 */ diff --git a/Tests/Fixtures/target_ordering_template_name_test.yml b/Tests/Fixtures/target_ordering_template_name_test.yml new file mode 100644 index 00000000..8eb971f1 --- /dev/null +++ b/Tests/Fixtures/target_ordering_template_name_test.yml @@ -0,0 +1,12 @@ +name: TargetOrderingTemplateName +targets: + Zebra: + type: framework + platform: iOS + templates: [Renamed] + Apple: + type: framework + platform: iOS +targetTemplates: + Renamed: + name: Zulu diff --git a/Tests/Fixtures/target_ordering_template_test.yml b/Tests/Fixtures/target_ordering_template_test.yml new file mode 100644 index 00000000..2b6b1cee --- /dev/null +++ b/Tests/Fixtures/target_ordering_template_test.yml @@ -0,0 +1,11 @@ +name: TargetOrderingTemplate +targets: + Zebra: + type: framework + templates: [CrossPlatform] + Apple: + type: framework + platform: iOS +targetTemplates: + CrossPlatform: + platform: [iOS, tvOS] diff --git a/Tests/Fixtures/target_ordering_test.json b/Tests/Fixtures/target_ordering_test.json new file mode 100644 index 00000000..42746f8f --- /dev/null +++ b/Tests/Fixtures/target_ordering_test.json @@ -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" + } + } +} diff --git a/Tests/Fixtures/target_ordering_test.yml b/Tests/Fixtures/target_ordering_test.yml new file mode 100644 index 00000000..55f67940 --- /dev/null +++ b/Tests/Fixtures/target_ordering_test.yml @@ -0,0 +1,14 @@ +name: TargetOrdering +targets: + Zebra: + type: framework + platform: iOS + Apple: + type: framework + platform: iOS + Mango: + type: framework + platform: iOS + Banana: + type: framework + platform: iOS diff --git a/Tests/Fixtures/target_ordering_variable_test.yml b/Tests/Fixtures/target_ordering_variable_test.yml new file mode 100644 index 00000000..02be5052 --- /dev/null +++ b/Tests/Fixtures/target_ordering_variable_test.yml @@ -0,0 +1,8 @@ +name: TargetOrderingVariable +targets: + ${FIRST_TARGET}: + type: framework + platform: iOS + Apple: + type: framework + platform: iOS diff --git a/Tests/ProjectSpecTests/SpecLoadingTests.swift b/Tests/ProjectSpecTests/SpecLoadingTests.swift index d45ce83d..54875e75 100644 --- a/Tests/ProjectSpecTests/SpecLoadingTests.swift +++ b/Tests/ProjectSpecTests/SpecLoadingTests.swift @@ -25,6 +25,49 @@ class SpecLoadingTests: XCTestCase { } } + func testTargetDeclarationOrder() { + describe { + $0.it("preserves target declaration order from YAML") { + let path = fixturePath + "target_ordering_test.yml" + let project = try loadSpec(path: path) + + try expect(project.targets.map { $0.name }) == ["Zebra", "Apple", "Mango", "Banana"] + } + + $0.it("preserves target declaration order from JSON") { + let path = fixturePath + "target_ordering_test.json" + let project = try loadSpec(path: path) + + try expect(project.targets.map { $0.name }) == ["Zebra", "Apple", "Mango", "Banana"] + } + + $0.it("preserves declaration order for a template-supplied platform array") { + let path = fixturePath + "target_ordering_template_test.yml" + let project = try loadSpec(path: path) + + try expect(project.targets.map { $0.name }) == ["Zebra_iOS", "Zebra_tvOS", "Apple"] + } + + $0.it("returns an empty order for contents that are not valid YAML") { + try expect(loadOrderedTargetNames(contents: "\ttargets:\n\tZebra:")) == [] + } + + $0.it("preserves declaration order for a template-supplied target name") { + let path = fixturePath + "target_ordering_template_name_test.yml" + let project = try loadSpec(path: path) + + try expect(project.targets.map { $0.name }) == ["Zulu", "Apple"] + } + + $0.it("preserves declaration order for a variable-expanded target key") { + let path = fixturePath + "target_ordering_variable_test.yml" + let project = try loadSpec(path: path, variables: ["FIRST_TARGET": "Zebra"]) + + try expect(project.targets.map { $0.name }) == ["Zebra", "Apple"] + } + } + } + func testSpecLoader() { describe { $0.it("merges includes") { @@ -116,6 +159,41 @@ class SpecLoadingTests: XCTestCase { ] try expect(project.targets) == [ + Target( + name: "RecursiveTarget", + type: .application, + platform: .macOS, + configFiles: ["Config": "paths_test/recursive_test/config"], + sources: ["paths_test/recursive_test/source"], + dependencies: [Dependency(type: .framework, reference: "paths_test/recursive_test/Framework")], + info: Plist(path: "paths_test/recursive_test/info"), + entitlements: Plist(path: "paths_test/recursive_test/entitlements"), + preBuildScripts: [BuildScript(script: .path("paths_test/recursive_test/prebuildScript"))], + postCompileScripts: [BuildScript(script: .path("paths_test/recursive_test/postCompileScript"))], + postBuildScripts: [BuildScript(script: .path("paths_test/recursive_test/postBuildScript"))] + ), + Target( + name: "target1", + type: .framework, + platform: .macOS, + sources: ["paths_test/same_relative_path_test/parent1/same/target1/source"] + ), + Target( + name: "target2", + type: .framework, + platform: .macOS, + sources: ["paths_test/same_relative_path_test/parent2/same/target2/source"] + ), + Target( + name: "app", + type: .application, + platform: .macOS, + sources: ["paths_test/same_relative_path_test/source"], + dependencies: [ + Dependency(type: .target, reference: "target1"), + Dependency(type: .target, reference: "target2") + ] + ), Target( name: "IncludedTarget", type: .application, @@ -149,41 +227,6 @@ class SpecLoadingTests: XCTestCase { postCompileScripts: [BuildScript(script: .path("postCompileScript"))], postBuildScripts: [BuildScript(script: .path("postBuildScript"))] ), - Target( - name: "RecursiveTarget", - type: .application, - platform: .macOS, - configFiles: ["Config": "paths_test/recursive_test/config"], - sources: ["paths_test/recursive_test/source"], - dependencies: [Dependency(type: .framework, reference: "paths_test/recursive_test/Framework")], - info: Plist(path: "paths_test/recursive_test/info"), - entitlements: Plist(path: "paths_test/recursive_test/entitlements"), - preBuildScripts: [BuildScript(script: .path("paths_test/recursive_test/prebuildScript"))], - postCompileScripts: [BuildScript(script: .path("paths_test/recursive_test/postCompileScript"))], - postBuildScripts: [BuildScript(script: .path("paths_test/recursive_test/postBuildScript"))] - ), - Target( - name: "app", - type: .application, - platform: .macOS, - sources: ["paths_test/same_relative_path_test/source"], - dependencies: [ - Dependency(type: .target, reference: "target1"), - Dependency(type: .target, reference: "target2") - ] - ), - Target( - name: "target1", - type: .framework, - platform: .macOS, - sources: ["paths_test/same_relative_path_test/parent1/same/target1/source"] - ), - Target( - name: "target2", - type: .framework, - platform: .macOS, - sources: ["paths_test/same_relative_path_test/parent2/same/target2/source"] - ) ] try expect(project.schemes) == [ diff --git a/Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift b/Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift index 842cb86c..80b083e1 100644 --- a/Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift @@ -396,7 +396,55 @@ class PBXProjGeneratorTests: XCTestCase { } } } - + + func testTargetOrdering() { + describe { + $0.it("honors declaration order in the generated pbxproj") { + let project = try Project( + jsonDictionary: [ + "name": "Test", + "targets": [ + "A": ["type": "framework", "platform": "iOS"], + "B": ["type": "framework", "platform": "iOS"], + "C": ["type": "framework", "platform": "iOS"], + ], + ], + targetDeclarationOrder: ["C", "A", "B"] + ) + + let pbxProj = try project.generatePbxProj() + let targetNames = pbxProj.projects.first?.targets.map { $0.name } + try expect(targetNames) == ["C", "A", "B"] + } + + $0.it("preserves the order of targets in a programmatically-built project") { + let c = Target(name: "C", type: .framework, platform: .iOS) + let a = Target(name: "A", type: .framework, platform: .iOS) + let b = Target(name: "B", type: .framework, platform: .iOS) + let project = Project(name: "Test", targets: [c, a, b]) + + let pbxProj = try project.generatePbxProj() + let targetNames = pbxProj.projects.first?.targets.map { $0.name } + try expect(targetNames) == ["C", "A", "B"] + } + + $0.it("places aggregate targets after native targets") { + let app = Target(name: "App", type: .application, platform: .iOS) + let framework = Target(name: "Framework", type: .framework, platform: .iOS) + let aggregate = AggregateTarget(name: "Agg", targets: ["App"]) + let project = Project( + name: "Test", + targets: [app, framework], + aggregateTargets: [aggregate] + ) + + let pbxProj = try project.generatePbxProj() + let targetNames = pbxProj.projects.first?.targets.map { $0.name } + try expect(targetNames) == ["App", "Framework", "Agg"] + } + } + } + func testDefaultLastUpgradeCheckWhenUserDidSpecifyInvalidValue() throws { let lastUpgradeKey = "LastUpgradeCheck" let attributes: [String: Any] = [lastUpgradeKey: 1234]