diff --git a/Sources/ProjectSpec/ProjectSpec.swift b/Sources/ProjectSpec/ProjectSpec.swift index d359dbb74..6c1b50bc5 100644 --- a/Sources/ProjectSpec/ProjectSpec.swift +++ b/Sources/ProjectSpec/ProjectSpec.swift @@ -89,11 +89,7 @@ extension ProjectSpec { settingPresets = jsonDictionary.json(atKeyPath: "settingPresets") ?? [:] let configs: [String: String] = jsonDictionary.json(atKeyPath: "configs") ?? [:] self.configs = configs.map { Config(name: $0, type: ConfigType(rawValue: $1)) } - if jsonDictionary["targets"] == nil { - targets = [] - } else { - targets = try jsonDictionary.json(atKeyPath: "targets", invalidItemBehaviour: .fail) - } + self.targets = try Target.decodeTargets(jsonDictionary: jsonDictionary) schemes = try jsonDictionary.json(atKeyPath: "schemes") if jsonDictionary["options"] != nil { options = try jsonDictionary.json(atKeyPath: "options") diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index 00b1e6aa4..ae5022800 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -46,6 +46,76 @@ public struct Target { } } +extension Target { + + static func decodeTargets(jsonDictionary: JSONDictionary) throws -> [Target] { + guard jsonDictionary["targets"] != nil else { + return [] + } + let array: [JSONDictionary] = try jsonDictionary.json(atKeyPath: "targets", invalidItemBehaviour: .fail) + + var targets: [JSONDictionary] = [] + + let platformReplacement = "$platform" + + for json in array { + + if let platforms = json["platform"] as? [String] { + + for platform in platforms { + var platformTarget = json + + func replacePlatform(_ dictionary: JSONDictionary) -> JSONDictionary { + var replaced = dictionary + for (key, value) in dictionary { + switch value { + case let dictionary as JSONDictionary: + replaced[key] = replacePlatform(dictionary) + case let string as String: + replaced[key] = string.replacingOccurrences(of: platformReplacement, with: platform) + case let array as [JSONDictionary]: + replaced[key] = array.map(replacePlatform) + case let array as [String]: + replaced[key] = array.map { $0.replacingOccurrences(of: platformReplacement, with: platform) } + default: break + } + } + return replaced + } + + platformTarget = replacePlatform(platformTarget) + + platformTarget["platform"] = platform + let platformSuffix = platformTarget["platformSuffix"] as? String ?? "_\(platform)" + let platformPrefix = platformTarget["platformPrefix"] as? String ?? "" + let name = platformTarget["name"] as? String ?? "" + platformTarget["name"] = platformPrefix + name + platformSuffix + + var settings = platformTarget["settings"] as? JSONDictionary ?? [:] + if settings["configs"] != nil || settings["presets"] != nil || settings["base"] != nil { + var base = settings["base"] as? JSONDictionary ?? [:] + if base["PRODUCT_NAME"] == nil { + base["PRODUCT_NAME"] = name + } + settings["base"] = base + } else { + if settings["PRODUCT_NAME"] == nil { + settings["PRODUCT_NAME"] = name + } + } + platformTarget["settings"] = settings + + targets.append(platformTarget) + } + } else { + targets.append(json) + } + } + + return try targets.map { try Target(jsonDictionary: $0) } + } +} + extension Target: Equatable { public static func ==(lhs: Target, rhs: Target) -> Bool { diff --git a/Tests/XcodeGenKitTests/SpecLoadingTests.swift b/Tests/XcodeGenKitTests/SpecLoadingTests.swift index b01c1e726..88d456eba 100644 --- a/Tests/XcodeGenKitTests/SpecLoadingTests.swift +++ b/Tests/XcodeGenKitTests/SpecLoadingTests.swift @@ -64,6 +64,28 @@ func specLoadingTests() { try expect(target.dependencies[2]) == .framework("path") } + $0.it("parsed cross platform targets") { + let targetDictionary: [String: Any] = [ + "name":"Framework", + "platform": ["iOS", "tvOS"], + "type": "framework", + "sources": ["Framework", "Framework $platform"], + "settings": ["SETTING": "value_$platform"], + ] + + let spec = try getProjectSpec(["targets":[targetDictionary]]) + var target_iOS = Target(name: "Framework_iOS", type: .framework, platform: .iOS) + var target_tvOS = Target(name: "Framework_tvOS", type: .framework, platform: .tvOS) + + target_iOS.sources = ["Framework","Framework iOS"] + target_tvOS.sources = ["Framework","Framework tvOS"] + target_iOS.settings = ["PRODUCT_NAME": "Framework", "SETTING": "value_iOS"] + target_tvOS.settings = ["PRODUCT_NAME": "Framework", "SETTING": "value_tvOS"] + + try expect(spec.targets.count) == 2 + try expect(spec.targets) == [target_iOS, target_tvOS] + } + $0.it("parses schemes") { let schemeDictionary: [String: Any] = [ "build": ["targets": [ @@ -138,5 +160,6 @@ func specLoadingTests() { let parsedSpec = try getProjectSpec(["options": ["carthageBuildPath": "../Carthage/Build"]]) try expect(parsedSpec) == expected } + } } diff --git a/docs/ProjectSpec.md b/docs/ProjectSpec.md index d7b7874d9..a2e2c71ff 100644 --- a/docs/ProjectSpec.md +++ b/docs/ProjectSpec.md @@ -133,6 +133,30 @@ This will provide default build settings for a certain platform. It can be any o - macOS - watchOS +**Multi Platform targets** + +You can also specify an array of platforms. This will generate a target for each platform. +If you reference the string `$platform` anywhere within the target spec, that will be replaced with the platform. + +The generated targets by default will have a suffix of `_$platform` applied, you can change this by specifying a `platformSuffix` or `platformPrefix`. + +If no `PRODUCT_NAME` build setting is specified for a target, this will be set to the target name, so that this target can be imported under a single name. + +``` +name: MyFramework +sources: MyFramework +platform: [iOS, tvOS] +type: framework +settings: + base: + INFOPLIST_FILE: MyApp/Info.plist + PRODUCT_BUNDLE_IDENTIFIER: com.myapp + MY_SETTING: platform $platform + presets: + - $platform +``` +The above will generate 2 targets named `MyFramework_iOS` and `MyFramework_tvOS`, with all the relevant platform build settings. They will both have a `PRODUCT_NAME` of `MyFramework` + ### Sources Specifies the source directories for a target. This can either be a single path or a list of paths. Applicable source files, resources, headers, and lproj files will be parsed appropriately