diff --git a/Sources/ProjectSpec/Project.swift b/Sources/ProjectSpec/Project.swift index adfdf029..ee97e2e6 100644 --- a/Sources/ProjectSpec/Project.swift +++ b/Sources/ProjectSpec/Project.swift @@ -28,9 +28,15 @@ public struct Project: BuildSettingsContainer { public var fileGroups: [String] public var configFiles: [String: String] public var include: [String] = [] + public var externalProjects: [ExternalProject] = [] { + didSet { + externalProjectsMap = Dictionary(uniqueKeysWithValues: externalProjects.map { ($0.name, $0) }) + } + } private var targetsMap: [String: Target] private var aggregateTargetsMap: [String: AggregateTarget] + private var externalProjectsMap: [String: ExternalProject] public init( basePath: Path = "", @@ -44,7 +50,8 @@ public struct Project: BuildSettingsContainer { options: SpecOptions = SpecOptions(), fileGroups: [String] = [], configFiles: [String: String] = [:], - attributes: [String: Any] = [:] + attributes: [String: Any] = [:], + externalProjects: [ExternalProject] = [] ) { self.basePath = basePath self.name = name @@ -60,6 +67,12 @@ public struct Project: BuildSettingsContainer { self.fileGroups = fileGroups self.configFiles = configFiles self.attributes = attributes + self.externalProjects = externalProjects + externalProjectsMap = Dictionary(uniqueKeysWithValues: self.externalProjects.map { ($0.name, $0) }) + } + + public func getExternalProject(_ projectName: String) -> ExternalProject? { + return externalProjectsMap[projectName] } public func getTarget(_ targetName: String) -> Target? { @@ -155,6 +168,7 @@ extension Project { configs.map { Config(name: $0, type: ConfigType(rawValue: $1)) }.sorted { $0.name < $1.name } targets = try jsonDictionary.json(atKeyPath: "targets").sorted { $0.name < $1.name } aggregateTargets = try jsonDictionary.json(atKeyPath: "aggregateTargets").sorted { $0.name < $1.name } + externalProjects = try jsonDictionary.json(atKeyPath: "externalProjects").sorted { $0.name < $1.name } schemes = try jsonDictionary.json(atKeyPath: "schemes") fileGroups = jsonDictionary.json(atKeyPath: "fileGroups") ?? [] configFiles = jsonDictionary.json(atKeyPath: "configFiles") ?? [:] @@ -167,6 +181,7 @@ extension Project { } targetsMap = Dictionary(uniqueKeysWithValues: targets.map { ($0.name, $0) }) aggregateTargetsMap = Dictionary(uniqueKeysWithValues: aggregateTargets.map { ($0.name, $0) }) + externalProjectsMap = Dictionary(uniqueKeysWithValues: externalProjects.map { ($0.name, $0) }) } static func resolveProject(jsonDictionary: JSONDictionary) throws -> JSONDictionary { @@ -241,6 +256,7 @@ extension Project: JSONEncodable { let configsPairs = configs.map { ($0.name, $0.type?.rawValue) } let aggregateTargetsPairs = aggregateTargets.map { ($0.name, $0.toJSONValue()) } let schemesPairs = schemes.map { ($0.name, $0.toJSONValue()) } + let externalProjectsPairs = externalProjects.map { ($0.name, $0.toJSONValue()) } return [ "name": name, @@ -255,6 +271,33 @@ extension Project: JSONEncodable { "aggregateTargets": Dictionary(uniqueKeysWithValues: aggregateTargetsPairs), "schemes": Dictionary(uniqueKeysWithValues: schemesPairs), "settingGroups": settingGroups.mapValues { $0.toJSONValue() }, + "externalProjects": externalProjectsPairs, + ] + } +} + + +public struct ExternalProject { + public let name: String + public let path: String + + public init(name: String, path: String) { + self.name = name + self.path = path + } +} + +extension ExternalProject: NamedJSONDictionaryConvertible { + public init(name: String, jsonDictionary: JSONDictionary) throws { + self.name = name + self.path = try jsonDictionary.json(atKeyPath: "path") + } +} + +extension ExternalProject: JSONEncodable { + public func toJSONValue() -> Any { + return [ + "path": path, ] } } diff --git a/Sources/ProjectSpec/Scheme.swift b/Sources/ProjectSpec/Scheme.swift index 4f859a52..243bd5da 100644 --- a/Sources/ProjectSpec/Scheme.swift +++ b/Sources/ProjectSpec/Scheme.swift @@ -119,32 +119,33 @@ public struct Scheme: Equatable { public static let randomExecutionOrderDefault = false public static let parallelizableDefault = false - public let name: String - public var externalProject: String? + public var name: String { return targetReference.name } + public let targetReference: TargetReference public var randomExecutionOrder: Bool public var parallelizable: Bool public var skippedTests: [String] public init( - name: String, - externalProject: String? = nil, + targetReference: TargetReference, randomExecutionOrder: Bool = randomExecutionOrderDefault, parallelizable: Bool = parallelizableDefault, skippedTests: [String] = [] ) { - self.name = name - self.externalProject = externalProject + self.targetReference = targetReference self.randomExecutionOrder = randomExecutionOrder self.parallelizable = parallelizable self.skippedTests = skippedTests } public init(stringLiteral value: String) { - name = value - externalProject = nil - randomExecutionOrder = false - parallelizable = false - skippedTests = [] + do { + targetReference = try TargetReference(string: value) + randomExecutionOrder = false + parallelizable = false + skippedTests = [] + } catch { + fatalError(SpecParsingError.invalidTargetReference(value).description) + } } } @@ -235,14 +236,56 @@ public struct Scheme: Equatable { } public struct BuildTarget: Equatable { - public var target: String - public var externalProject: String? + public var target: TargetReference public var buildTypes: [BuildType] - public init(target: String, externalProject: String? = nil, buildTypes: [BuildType] = BuildType.all) { + public init(target: TargetReference, buildTypes: [BuildType] = BuildType.all) { self.target = target self.buildTypes = buildTypes - self.externalProject = externalProject + } + } +} + + +public struct TargetReference: Equatable { + public let name: String + public let location: Location + + public enum Location: Equatable { + case local + case project(String) + } + + public init(name: String, location: Location = .local) { + self.name = name + self.location = location + } +} + +extension TargetReference { + public init(string: String) throws { + let paths = string.split(separator: "/") + guard paths.count <= 2 && !paths.isEmpty else { + throw SpecParsingError.invalidTargetReference(string) + } + switch paths.count { + case 2: + location = .project(String(paths[0])) + name = String(paths[1]) + case 1: + location = .local + name = String(paths[0]) + default: fatalError("unreachable") + } + } +} + +extension TargetReference { + public func toString() -> String { + switch location { + case .local: return name + case .project(let projectPath): + return "\(projectPath)/\(name)" } } } @@ -314,7 +357,7 @@ extension Scheme.Test: JSONObjectConvertible { if let targets = jsonDictionary["targets"] as? [Any] { self.targets = try targets.compactMap { target in if let string = target as? String { - return TestTarget(name: string) + return TestTarget(stringLiteral: string) } else if let dictionary = target as? JSONDictionary { return try TestTarget(jsonDictionary: dictionary) } else { @@ -360,8 +403,7 @@ extension Scheme.Test: JSONEncodable { extension Scheme.Test.TestTarget: JSONObjectConvertible { public init(jsonDictionary: JSONDictionary) throws { - name = try jsonDictionary.json(atKeyPath: "name") - externalProject = jsonDictionary.json(atKeyPath: "externalProject") + targetReference = try TargetReference(string: jsonDictionary.json(atKeyPath: "name")) randomExecutionOrder = jsonDictionary.json(atKeyPath: "randomExecutionOrder") ?? Scheme.Test.TestTarget.randomExecutionOrderDefault parallelizable = jsonDictionary.json(atKeyPath: "parallelizable") ?? Scheme.Test.TestTarget.parallelizableDefault skippedTests = jsonDictionary.json(atKeyPath: "skippedTests") ?? [] @@ -371,13 +413,12 @@ extension Scheme.Test.TestTarget: JSONObjectConvertible { extension Scheme.Test.TestTarget: JSONEncodable { public func toJSONValue() -> Any { if randomExecutionOrder == Scheme.Test.TestTarget.randomExecutionOrderDefault, - parallelizable == Scheme.Test.TestTarget.parallelizableDefault, - externalProject == nil { - return name + parallelizable == Scheme.Test.TestTarget.parallelizableDefault { + return targetReference.toString() } var dict: JSONDictionary = [ - "name": name, + "name": targetReference.toString(), ] if randomExecutionOrder != Scheme.Test.TestTarget.randomExecutionOrderDefault { @@ -386,9 +427,6 @@ extension Scheme.Test.TestTarget: JSONEncodable { if parallelizable != Scheme.Test.TestTarget.parallelizableDefault { dict["parallelizable"] = parallelizable } - if let externalProject = externalProject { - dict["externalProject"] = externalProject - } return dict } @@ -491,10 +529,9 @@ extension Scheme.Build: JSONObjectConvertible { public init(jsonDictionary: JSONDictionary) throws { let targetDictionary: JSONDictionary = try jsonDictionary.json(atKeyPath: "targets") var targets: [Scheme.BuildTarget] = [] - for (target, possibleBuildTypesOrDict) in targetDictionary { + for (targetRepr, possibleBuildTypes) in targetDictionary { let buildTypes: [BuildType] - var externalProject: String? = nil - if let string = possibleBuildTypesOrDict as? String { + if let string = possibleBuildTypes as? String { switch string { case "all": buildTypes = BuildType.all case "none": buildTypes = [] @@ -502,23 +539,17 @@ extension Scheme.Build: JSONObjectConvertible { case "indexing": buildTypes = [.testing, .analyzing, .archiving] default: buildTypes = BuildType.all } - } else if let enabledDictionary = possibleBuildTypesOrDict as? [String: Bool] { + } else if let enabledDictionary = possibleBuildTypes as? [String: Bool] { buildTypes = enabledDictionary.filter { $0.value }.compactMap { BuildType.from(jsonValue: $0.key) } - } else if let array = possibleBuildTypesOrDict as? [String] { + } else if let array = possibleBuildTypes as? [String] { buildTypes = array.compactMap(BuildType.from) - } else if let dict = possibleBuildTypesOrDict as? [String: Any] { - if let array = dict["types"] as? [String] { - buildTypes = array.compactMap(BuildType.from) - } else { - buildTypes = BuildType.all - } - externalProject = dict["externalProject"] as? String } else { buildTypes = BuildType.all } - targets.append(Scheme.BuildTarget(target: target, externalProject: externalProject, buildTypes: buildTypes)) + let target = try TargetReference(string: targetRepr) + targets.append(Scheme.BuildTarget(target: target, buildTypes: buildTypes)) } - self.targets = targets.sorted { $0.target < $1.target } + self.targets = targets.sorted { $0.target.name < $1.target.name } preActions = try jsonDictionary.json(atKeyPath: "preActions")?.map(Scheme.ExecutionAction.init) ?? [] postActions = try jsonDictionary.json(atKeyPath: "postActions")?.map(Scheme.ExecutionAction.init) ?? [] parallelizeBuild = jsonDictionary.json(atKeyPath: "parallelizeBuild") ?? Scheme.Build.parallelizeBuildDefault @@ -528,7 +559,7 @@ extension Scheme.Build: JSONObjectConvertible { extension Scheme.Build: JSONEncodable { public func toJSONValue() -> Any { - let targetPairs = targets.map { ($0.target, $0.buildTypes.map { $0.toJSONValue() }) } + let targetPairs = targets.map { ($0.target.toString(), $0.buildTypes.map { $0.toJSONValue() }) } var dict: JSONDictionary = [ "targets": Dictionary(uniqueKeysWithValues: targetPairs), diff --git a/Sources/ProjectSpec/SpecParsingError.swift b/Sources/ProjectSpec/SpecParsingError.swift index 5d48d4d0..9c252dca 100644 --- a/Sources/ProjectSpec/SpecParsingError.swift +++ b/Sources/ProjectSpec/SpecParsingError.swift @@ -5,6 +5,7 @@ public enum SpecParsingError: Error, CustomStringConvertible { case unknownTargetPlatform(String) case invalidDependency([String: Any]) case invalidSourceBuildPhase(String) + case invalidTargetReference(String) case invalidVersion(String) public var description: String { @@ -17,6 +18,8 @@ public enum SpecParsingError: Error, CustomStringConvertible { return "Unknown Target dependency: \(dependency)" case let .invalidSourceBuildPhase(error): return "Invalid Source Build Phase: \(error)" + case let .invalidTargetReference(targetReference): + return "Invalid Target Reference Syntax: \(targetReference)" case let .invalidVersion(version): return "Invalid version: \(version)" } diff --git a/Sources/ProjectSpec/SpecValidation.swift b/Sources/ProjectSpec/SpecValidation.swift index 8d081256..eae613a6 100644 --- a/Sources/ProjectSpec/SpecValidation.swift +++ b/Sources/ProjectSpec/SpecValidation.swift @@ -170,8 +170,9 @@ extension Project { for scheme in schemes { for buildTarget in scheme.build.targets { - if getProjectTarget(buildTarget.target) == nil && buildTarget.externalProject == nil { - errors.append(.invalidSchemeTarget(scheme: scheme.name, target: buildTarget.target)) + guard buildTarget.target.location == .local else { continue } + if getProjectTarget(buildTarget.target.name) == nil { + errors.append(.invalidSchemeTarget(scheme: scheme.name, target: buildTarget.target.name)) } } if let action = scheme.run, let config = action.config, getConfig(config) == nil { diff --git a/Sources/ProjectSpec/TargetScheme.swift b/Sources/ProjectSpec/TargetScheme.swift index c4c4673e..2bb18622 100644 --- a/Sources/ProjectSpec/TargetScheme.swift +++ b/Sources/ProjectSpec/TargetScheme.swift @@ -42,7 +42,7 @@ extension TargetScheme: JSONObjectConvertible { if let targets = jsonDictionary["testTargets"] as? [Any] { testTargets = try targets.compactMap { target in if let string = target as? String { - return .init(name: string) + return .init(stringLiteral: string) } else if let dictionary = target as? JSONDictionary { return try .init(jsonDictionary: dictionary) } else { diff --git a/Sources/XcodeGenKit/SchemeGenerator.swift b/Sources/XcodeGenKit/SchemeGenerator.swift index 8f214e88..e9f2bddf 100644 --- a/Sources/XcodeGenKit/SchemeGenerator.swift +++ b/Sources/XcodeGenKit/SchemeGenerator.swift @@ -77,32 +77,36 @@ public class SchemeGenerator { func getBuildEntry(_ buildTarget: Scheme.BuildTarget) throws -> XCScheme.BuildAction.Entry { let pbxProj: PBXProj - let projectFilename: String - if let externalProject = buildTarget.externalProject { - pbxProj = try XcodeProj(pathString: externalProject).pbxproj - projectFilename = externalProject - } else { + let projectFilePath: String + switch buildTarget.target.location { + case .project(let project): + guard let externalProject = self.project.getExternalProject(project) else { + fatalError("Unable to find external project named \"\(project)\" in project.yml") + } + pbxProj = try XcodeProj(pathString: externalProject.path).pbxproj + projectFilePath = externalProject.path + case .local: pbxProj = self.pbxProj - projectFilename = "\(self.project.name).xcodeproj" + projectFilePath = "\(self.project.name).xcodeproj" } - guard let pbxTarget = pbxProj.targets(named: buildTarget.target).first else { + guard let pbxTarget = pbxProj.targets(named: buildTarget.target.name).first else { fatalError("Unable to find target named \"\(buildTarget.target)\" in \"PBXProj.targets\"") } let buildableName = pbxTarget.productNameWithExtension() ?? pbxTarget.name let buildableReference = XCScheme.BuildableReference( - referencedContainer: "container:\(projectFilename)", + referencedContainer: "container:\(projectFilePath)", blueprint: pbxTarget, buildableName: buildableName, - blueprintName: buildTarget.target + blueprintName: buildTarget.target.name ) return XCScheme.BuildAction.Entry(buildableReference: buildableReference, buildFor: buildTarget.buildTypes) } let testTargets = scheme.test?.targets ?? [] let testBuildTargets = testTargets.map { - Scheme.BuildTarget(target: $0.name, externalProject: $0.externalProject, buildTypes: BuildType.testOnly) + Scheme.BuildTarget(target: TargetReference(name: $0.name, location: .local), buildTypes: BuildType.testOnly) } let testBuildTargetEntries = try testBuildTargets.map(getBuildEntry) @@ -119,7 +123,7 @@ public class SchemeGenerator { return XCScheme.ExecutionAction(scriptText: action.script, title: action.name, environmentBuildable: environmentBuildable) } - let target = project.getTarget(scheme.build.targets.first!.target) + let target = project.getTarget(scheme.build.targets.first!.target.name) let shouldExecuteOnLaunch = target?.type.isExecutable == true let buildableReference = buildActionEntries.first!.buildableReference @@ -217,7 +221,7 @@ extension Scheme { public init(name: String, target: Target, targetScheme: TargetScheme, debugConfig: String, releaseConfig: String) { self.init( name: name, - build: .init(targets: [Scheme.BuildTarget(target: target.name)]), + build: .init(targets: [Scheme.BuildTarget(target: TargetReference(name: target.name, location: .local))]), run: .init( config: debugConfig, commandLineArguments: targetScheme.commandLineArguments,