Files
XcodeGen/Sources/ProjectSpec/Target.swift
T
Ryu 53cb43cb66 Add validation to ensure that settings.configs values are dictionaries, in order to prevent misuse (#1547)
* Add validation to ensure settings.configs values are dictionaries to prevent misuse

* Add tests for invalid settings.configs value formats

* Replaced with filter and split into a function

* Rename invalidConfigsFormat to invalidConfigsMappingFormat

* Add comments to explain invalid  fixture

* Rename test fixture

* Update CHANGELOG.md

* Correct grammer

* Use KeyPath instead of closure

* Rename validateMappingStyleInConfig to extractValidConfigs

* Add a document comment for extractValidConfigs(from:)

* Use old testing api and remove EquatableErrorBox

* Rename test case to use "mapping" instead of "dictionary"

* Add ValidSettingsExtractor to encapsulate the logic for converting a dictionary to Settings

* Add settings validation for both Target and AggregateTarget

* Add tests for invalid settings.configs in Target and AggregateTarget

* Add document comments for ValidSettingsExtractor

* Rename ValidSettingsExtractor to BuildSettingsExtractor

* Add settings validation for settingGroups

* Add tests for settingGroups

* Rename extract to parse

* Refactor

* Update Tests/ProjectSpecTests/InvalidConfigsFormatTests.swift

---------

Co-authored-by: Yonas Kolb <yonaskolb@users.noreply.github.com>
2025-06-07 00:23:21 +10:00

417 lines
17 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 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
}
}