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 supportedDestinations: [SupportedDestination]? 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 buildToolPlugins: [BuildToolPlugin] 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 putResourcesBeforeSourcesBuildPhase: 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, supportedDestinations: [SupportedDestination]? = nil, 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] = [], buildToolPlugins: [BuildToolPlugin] = [], postCompileScripts: [BuildScript] = [], postBuildScripts: [BuildScript] = [], buildRules: [BuildRule] = [], scheme: TargetScheme? = nil, legacy: LegacyTarget? = nil, attributes: [String: Any] = [:], onlyCopyFilesOnInstall: Bool = false, putResourcesBeforeSourcesBuildPhase: Bool = false ) { self.name = name self.type = type self.platform = platform self.supportedDestinations = supportedDestinations 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.buildToolPlugins = buildToolPlugins self.postCompileScripts = postCompileScripts self.postBuildScripts = postBuildScripts self.buildRules = buildRules self.scheme = scheme self.legacy = legacy self.attributes = attributes self.onlyCopyFilesOnInstall = onlyCopyFilesOnInstall self.putResourcesBeforeSourcesBuildPhase = putResourcesBeforeSourcesBuildPhase } } 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), .object("scheme", TargetScheme.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 /// 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 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.buildToolPlugins == rhs.buildToolPlugins && 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 = jsonDictionary.json(atKeyPath: "type") ?? "" if let type = PBXProductType(string: typeString) { self.type = type } else { throw SpecParsingError.unknownTargetType(typeString) } if let supportedDestinations: [SupportedDestination] = jsonDictionary.json(atKeyPath: "supportedDestinations") { self.supportedDestinations = supportedDestinations } let isResolved = jsonDictionary.json(atKeyPath: "isMultiPlatformTarget") ?? false if isResolved, supportedDestinations != nil { throw SpecParsingError.invalidTargetPlatformAsArray } var platformString: String = jsonDictionary.json(atKeyPath: "platform") ?? "" // platform defaults to 'auto' if it is empty and we are using supported destinations if supportedDestinations != nil, platformString.isEmpty { platformString = Platform.auto.rawValue } // we add 'iOS' in supported destinations if it contains only 'macCatalyst' if supportedDestinations?.contains(.macCatalyst) == true, supportedDestinations?.contains(.iOS) == false { supportedDestinations?.append(.iOS) } 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 = try BuildSettingsParser(jsonDictionary: jsonDictionary).parse() 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["buildToolPlugins"] == nil { buildToolPlugins = [] } else { self.buildToolPlugins = try jsonDictionary.json(atKeyPath: "buildToolPlugins", invalidItemBehaviour: .fail) } 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 putResourcesBeforeSourcesBuildPhase = jsonDictionary.json(atKeyPath: "putResourcesBeforeSourcesBuildPhase") ?? false } } extension Target: JSONEncodable { public func toJSONValue() -> Any { var dict: [String: Any?] = [ "type": type.name, "platform": platform.rawValue, "supportedDestinations": supportedDestinations?.map { $0.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() }, "buildToolPlugins": buildToolPlugins.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 } if putResourcesBeforeSourcesBuildPhase { dict["putResourcesBeforeSourcesBuildPhase"] = true } return dict } }