Files
XcodeGen/Sources/ProjectSpec/Target.swift
T
Dalton Claybrook 81bd52be4b Add support for conditionally linking dependencies based on platform (#1087)
* Add initial support for conditional platform dependencies

* Add tests for conditional platforms

* Update docs and changelog

* Respond to PR feedback

* Change name of field from 'conditionalPlatforms' to 'platforms'

Co-authored-by: Yonas Kolb <yonaskolb@users.noreply.github.com>
2021-06-20 20:19:02 +10:00

369 lines
14 KiB
Swift

import Foundation
import JSONUtilities
import XcodeProj
import Version
public struct LegacyTarget: Equatable {
public static let passSettingsDefault = false
public var toolPath: String
public var arguments: String?
public var passSettings: Bool
public var workingDirectory: String?
public init(
toolPath: String,
passSettings: Bool = passSettingsDefault,
arguments: String? = nil,
workingDirectory: String? = nil
) {
self.toolPath = toolPath
self.arguments = arguments
self.passSettings = passSettings
self.workingDirectory = workingDirectory
}
}
extension LegacyTarget: PathContainer {
static var pathProperties: [PathProperty] {
[
.string("workingDirectory"),
]
}
}
public struct Target: ProjectTarget {
public var name: String
public var type: PBXProductType
public var platform: Platform
public var settings: Settings
public var sources: [TargetSource]
public var dependencies: [Dependency]
public var info: Plist?
public var entitlements: Plist?
public var transitivelyLinkDependencies: Bool?
public var directlyEmbedCarthageDependencies: Bool?
public var requiresObjCLinking: Bool?
public var preBuildScripts: [BuildScript]
public var postCompileScripts: [BuildScript]
public var postBuildScripts: [BuildScript]
public var buildRules: [BuildRule]
public var configFiles: [String: String]
public var scheme: TargetScheme?
public var legacy: LegacyTarget?
public var deploymentTarget: Version?
public var attributes: [String: Any]
public var productName: String
public var onlyCopyFilesOnInstall: Bool
public var isLegacy: Bool {
legacy != nil
}
public var filename: String {
var filename = productName
if let fileExtension = type.fileExtension {
filename += ".\(fileExtension)"
}
if type == .staticLibrary {
filename = "lib\(filename)"
}
return filename
}
public init(
name: String,
type: PBXProductType,
platform: Platform,
productName: String? = nil,
deploymentTarget: Version? = nil,
settings: Settings = .empty,
configFiles: [String: String] = [:],
sources: [TargetSource] = [],
dependencies: [Dependency] = [],
info: Plist? = nil,
entitlements: Plist? = nil,
transitivelyLinkDependencies: Bool? = nil,
directlyEmbedCarthageDependencies: Bool? = nil,
requiresObjCLinking: Bool? = nil,
preBuildScripts: [BuildScript] = [],
postCompileScripts: [BuildScript] = [],
postBuildScripts: [BuildScript] = [],
buildRules: [BuildRule] = [],
scheme: TargetScheme? = nil,
legacy: LegacyTarget? = nil,
attributes: [String: Any] = [:],
onlyCopyFilesOnInstall: Bool = false
) {
self.name = name
self.type = type
self.platform = platform
self.deploymentTarget = deploymentTarget
self.productName = productName ?? name
self.settings = settings
self.configFiles = configFiles
self.sources = sources
self.dependencies = dependencies
self.info = info
self.entitlements = entitlements
self.transitivelyLinkDependencies = transitivelyLinkDependencies
self.directlyEmbedCarthageDependencies = directlyEmbedCarthageDependencies
self.requiresObjCLinking = requiresObjCLinking
self.preBuildScripts = preBuildScripts
self.postCompileScripts = postCompileScripts
self.postBuildScripts = postBuildScripts
self.buildRules = buildRules
self.scheme = scheme
self.legacy = legacy
self.attributes = attributes
self.onlyCopyFilesOnInstall = onlyCopyFilesOnInstall
}
}
extension Target: CustomStringConvertible {
public var description: String {
"\(name): \(platform.rawValue) \(type)"
}
}
extension Target: PathContainer {
static var pathProperties: [PathProperty] {
[
.dictionary([
.string("sources"),
.object("sources", TargetSource.pathProperties),
.string("configFiles"),
.object("dependencies", Dependency.pathProperties),
.object("info", Plist.pathProperties),
.object("entitlements", Plist.pathProperties),
.object("preBuildScripts", BuildScript.pathProperties),
.object("prebuildScripts", BuildScript.pathProperties),
.object("postCompileScripts", BuildScript.pathProperties),
.object("postBuildScripts", BuildScript.pathProperties),
.object("legacy", LegacyTarget.pathProperties),
]),
]
}
}
extension Target {
static func resolveMultiplatformTargets(jsonDictionary: JSONDictionary) -> JSONDictionary {
guard let targetsDictionary: [String: JSONDictionary] = jsonDictionary["targets"] as? [String: JSONDictionary] else {
return jsonDictionary
}
var crossPlatformTargets: [String: JSONDictionary] = [:]
for (targetName, target) in targetsDictionary {
if let platforms = target["platform"] as? [String] {
for platform in platforms {
var platformTarget = 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
var settings = platformTarget["settings"] as? JSONDictionary ?? [:]
if settings["configs"] != nil || settings["groups"] != nil || settings["base"] != nil {
var base = settings["base"] as? JSONDictionary ?? [:]
if base["PRODUCT_NAME"] == nil {
base["PRODUCT_NAME"] = targetName
}
settings["base"] = base
} else {
if settings["PRODUCT_NAME"] == nil {
settings["PRODUCT_NAME"] = targetName
}
}
platformTarget["productName"] = targetName
platformTarget["settings"] = settings
if let deploymentTargets = target["deploymentTarget"] as? [String: Any] {
platformTarget["deploymentTarget"] = deploymentTargets[platform]
}
crossPlatformTargets[newTargetName] = platformTarget
}
} else {
crossPlatformTargets[targetName] = target
}
}
var merged = jsonDictionary
merged["targets"] = crossPlatformTargets
return merged
}
}
extension Target: Equatable {
public static func == (lhs: Target, rhs: Target) -> Bool {
lhs.name == rhs.name &&
lhs.type == rhs.type &&
lhs.platform == rhs.platform &&
lhs.deploymentTarget == rhs.deploymentTarget &&
lhs.transitivelyLinkDependencies == rhs.transitivelyLinkDependencies &&
lhs.requiresObjCLinking == rhs.requiresObjCLinking &&
lhs.directlyEmbedCarthageDependencies == rhs.directlyEmbedCarthageDependencies &&
lhs.settings == rhs.settings &&
lhs.configFiles == rhs.configFiles &&
lhs.sources == rhs.sources &&
lhs.info == rhs.info &&
lhs.entitlements == rhs.entitlements &&
lhs.dependencies == rhs.dependencies &&
lhs.preBuildScripts == rhs.preBuildScripts &&
lhs.postCompileScripts == rhs.postCompileScripts &&
lhs.postBuildScripts == rhs.postBuildScripts &&
lhs.buildRules == rhs.buildRules &&
lhs.scheme == rhs.scheme &&
lhs.legacy == rhs.legacy &&
NSDictionary(dictionary: lhs.attributes).isEqual(to: rhs.attributes)
}
}
extension LegacyTarget: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
toolPath = try jsonDictionary.json(atKeyPath: "toolPath")
arguments = jsonDictionary.json(atKeyPath: "arguments")
passSettings = jsonDictionary.json(atKeyPath: "passSettings") ?? LegacyTarget.passSettingsDefault
workingDirectory = jsonDictionary.json(atKeyPath: "workingDirectory")
}
}
extension LegacyTarget: JSONEncodable {
public func toJSONValue() -> Any {
var dict: [String: Any?] = [
"toolPath": toolPath,
"arguments": arguments,
"workingDirectory": workingDirectory,
]
if passSettings != LegacyTarget.passSettingsDefault {
dict["passSettings"] = passSettings
}
return dict
}
}
extension Target: NamedJSONDictionaryConvertible {
public init(name: String, jsonDictionary: JSONDictionary) throws {
let resolvedName: String = jsonDictionary.json(atKeyPath: "name") ?? name
self.name = resolvedName
productName = jsonDictionary.json(atKeyPath: "productName") ?? resolvedName
let typeString: String = try jsonDictionary.json(atKeyPath: "type")
if let type = PBXProductType(string: typeString) {
self.type = type
} else {
throw SpecParsingError.unknownTargetType(typeString)
}
let platformString: String = try jsonDictionary.json(atKeyPath: "platform")
if let platform = Platform(rawValue: platformString) {
self.platform = platform
} else {
throw SpecParsingError.unknownTargetPlatform(platformString)
}
if let string: String = jsonDictionary.json(atKeyPath: "deploymentTarget") {
deploymentTarget = try Version.parse(string)
} else if let double: Double = jsonDictionary.json(atKeyPath: "deploymentTarget") {
deploymentTarget = try Version.parse(String(double))
} else {
deploymentTarget = nil
}
settings = jsonDictionary.json(atKeyPath: "settings") ?? .empty
configFiles = jsonDictionary.json(atKeyPath: "configFiles") ?? [:]
if let source: String = jsonDictionary.json(atKeyPath: "sources") {
sources = [TargetSource(path: source)]
} else if let array = jsonDictionary["sources"] as? [Any] {
sources = try array.compactMap { source in
if let string = source as? String {
return TargetSource(path: string)
} else if let dictionary = source as? [String: Any] {
return try TargetSource(jsonDictionary: dictionary)
} else {
return nil
}
}
} else {
sources = []
}
if jsonDictionary["dependencies"] == nil {
dependencies = []
} else {
let dependencies: [Dependency] = try jsonDictionary.json(atKeyPath: "dependencies", invalidItemBehaviour: .fail)
self.dependencies = dependencies.filter { [platform] dependency -> Bool in
// If unspecified, all platforms are supported
guard let platforms = dependency.platforms else { return true }
return platforms.contains(platform)
}
}
if jsonDictionary["info"] != nil {
info = try jsonDictionary.json(atKeyPath: "info") as Plist
}
if jsonDictionary["entitlements"] != nil {
entitlements = try jsonDictionary.json(atKeyPath: "entitlements") as Plist
}
transitivelyLinkDependencies = jsonDictionary.json(atKeyPath: "transitivelyLinkDependencies")
directlyEmbedCarthageDependencies = jsonDictionary.json(atKeyPath: "directlyEmbedCarthageDependencies")
requiresObjCLinking = jsonDictionary.json(atKeyPath: "requiresObjCLinking")
preBuildScripts = jsonDictionary.json(atKeyPath: "preBuildScripts") ?? jsonDictionary.json(atKeyPath: "prebuildScripts") ?? []
postCompileScripts = jsonDictionary.json(atKeyPath: "postCompileScripts") ?? []
postBuildScripts = jsonDictionary.json(atKeyPath: "postBuildScripts") ?? jsonDictionary.json(atKeyPath: "postbuildScripts") ?? []
buildRules = jsonDictionary.json(atKeyPath: "buildRules") ?? []
scheme = jsonDictionary.json(atKeyPath: "scheme")
legacy = jsonDictionary.json(atKeyPath: "legacy")
attributes = jsonDictionary.json(atKeyPath: "attributes") ?? [:]
onlyCopyFilesOnInstall = jsonDictionary.json(atKeyPath: "onlyCopyFilesOnInstall") ?? false
}
}
extension Target: JSONEncodable {
public func toJSONValue() -> Any {
var dict: [String: Any?] = [
"type": type.name,
"platform": platform.rawValue,
"settings": settings.toJSONValue(),
"configFiles": configFiles,
"attributes": attributes,
"sources": sources.map { $0.toJSONValue() },
"dependencies": dependencies.map { $0.toJSONValue() },
"postCompileScripts": postCompileScripts.map { $0.toJSONValue() },
"prebuildScripts": preBuildScripts.map { $0.toJSONValue() },
"postbuildScripts": postBuildScripts.map { $0.toJSONValue() },
"buildRules": buildRules.map { $0.toJSONValue() },
"deploymentTarget": deploymentTarget?.deploymentTarget,
"info": info?.toJSONValue(),
"entitlements": entitlements?.toJSONValue(),
"transitivelyLinkDependencies": transitivelyLinkDependencies,
"directlyEmbedCarthageDependencies": directlyEmbedCarthageDependencies,
"requiresObjCLinking": requiresObjCLinking,
"scheme": scheme?.toJSONValue(),
"legacy": legacy?.toJSONValue(),
]
if productName != name {
dict["productName"] = productName
}
if onlyCopyFilesOnInstall {
dict["onlyCopyFilesOnInstall"] = true
}
return dict
}
}