diff --git a/CHANGELOG.md b/CHANGELOG.md index f876da2e..657a2b59 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. @mirkokg + ## 2.45.4 ### Fixed diff --git a/Sources/ProjectSpec/Project.swift b/Sources/ProjectSpec/Project.swift index e872a27b..1ed768b3 100644 --- a/Sources/ProjectSpec/Project.swift +++ b/Sources/ProjectSpec/Project.swift @@ -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) @@ -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") @@ -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 diff --git a/Sources/ProjectSpec/SpecFile.swift b/Sources/ProjectSpec/SpecFile.swift index 18ea1562..e4633bfa 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,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 @@ -99,21 +103,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 +124,14 @@ public struct SpecFile { resolvedDictionaryWithUniqueTargets() } + public func resolvedTargetDeclarationOrder() -> [String] { + var cachedSpecFiles: [Path: SpecFile] = [:] + let resolvedSpec = resolvingPaths(cachedSpecFiles: &cachedSpecFiles) + + var mergedSpecPaths = Set() + return resolvedSpec.mergedTargetDeclarationOrder(set: &mergedSpecPaths) + } + private func resolvedDictionaryWithUniqueTargets() -> JSONDictionary { var cachedSpecFiles: [Path: SpecFile] = [:] let resolvedSpec = resolvingPaths(cachedSpecFiles: &cachedSpecFiles) @@ -140,6 +151,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 +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 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..2043ba29 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -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 { @@ -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 { diff --git a/Sources/ProjectSpec/Yaml.swift b/Sources/ProjectSpec/Yaml.swift index 89014f66..b1a8384b 100644 --- a/Sources/ProjectSpec/Yaml.swift +++ b/Sources/ProjectSpec/Yaml.swift @@ -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) diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index 1f20e850..851e8339 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/CarthageProject/Project.xcodeproj/project.pbxproj b/Tests/Fixtures/CarthageProject/Project.xcodeproj/project.pbxproj index b1cbbabc..17d1d720 100644 --- a/Tests/Fixtures/CarthageProject/Project.xcodeproj/project.pbxproj +++ b/Tests/Fixtures/CarthageProject/Project.xcodeproj/project.pbxproj @@ -340,9 +340,9 @@ projectRoot = ""; targets = ( AE3F93DB94E7208F2F1D9A78 /* Framework_iOS */, - 53A3B531E3947D8A8722745E /* Framework_macOS */, 536ACF18E4603B59207D43CE /* Framework_tvOS */, 71B5187E710718C1A205D4DC /* Framework_watchOS */, + 53A3B531E3947D8A8722745E /* Framework_macOS */, ); }; /* End PBXProject section */ 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..419fe2e9 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 */, + 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 */ 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/ProjectSpecTests/SpecLoadingTests.swift b/Tests/ProjectSpecTests/SpecLoadingTests.swift index d3db0d9c..a0eb0185 100644 --- a/Tests/ProjectSpecTests/SpecLoadingTests.swift +++ b/Tests/ProjectSpecTests/SpecLoadingTests.swift @@ -25,6 +25,24 @@ 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"] + } + } + } + func testSpecLoader() { describe { $0.it("merges includes") { @@ -116,6 +134,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 +202,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]