import Foundation import JSONUtilities import PathKit import ProjectSpec import XcodeProj import Yams extension Project { public func getProjectBuildSettings(config: Config) -> BuildSettings { var buildSettings: BuildSettings = [:] // set project SDKROOT is a single platform if let firstPlatform = targets.first?.platform, targets.allSatisfy({ $0.platform == firstPlatform }) { buildSettings["SDKROOT"] = firstPlatform.sdkRoot } if let type = config.type, options.settingPresets.applyProject { buildSettings += SettingsPresetFile.base.getBuildSettings() buildSettings += SettingsPresetFile.config(type).getBuildSettings() } // apply custom platform version for platform in Platform.allCases { if let version = options.deploymentTarget.version(for: platform) { buildSettings[platform.deploymentTargetSetting] = version.deploymentTarget } } // Prevent setting presets from overwriting settings in project xcconfig files if let configPath = configFiles[config.name] { buildSettings = removeConfigFileSettings(from: buildSettings, configPath: configPath) } buildSettings += getBuildSettings(settings: settings, config: config) return buildSettings } public func getTargetBuildSettings(target: Target, config: Config) -> BuildSettings { var buildSettings = BuildSettings() // list of supported destination sorted by priority let specSupportedDestinations = target.supportedDestinations?.sorted(by: { $0.priority < $1.priority }) ?? [] if options.settingPresets.applyTarget { let platform: Platform if target.platform == .auto, let firstDestination = specSupportedDestinations.first, let firstDestinationPlatform = Platform(rawValue: firstDestination.rawValue) { platform = firstDestinationPlatform } else { platform = target.platform } buildSettings += SettingsPresetFile.platform(platform).getBuildSettings() buildSettings += SettingsPresetFile.product(target.type).getBuildSettings() buildSettings += SettingsPresetFile.productPlatform(target.type, platform).getBuildSettings() if target.platform == .auto { // this fix is necessary because the platform preset overrides the original value buildSettings["SDKROOT"] = Platform.auto.rawValue } } if !specSupportedDestinations.isEmpty { var supportedPlatforms: [String] = [] var targetedDeviceFamily: [String] = [] for supportedDestination in specSupportedDestinations { let supportedPlatformBuildSettings = SettingsPresetFile.supportedDestination(supportedDestination).getBuildSettings() buildSettings += supportedPlatformBuildSettings if let value = supportedPlatformBuildSettings?["SUPPORTED_PLATFORMS"] as? String { supportedPlatforms += value.components(separatedBy: " ") } if let value = supportedPlatformBuildSettings?["TARGETED_DEVICE_FAMILY"] as? String { targetedDeviceFamily += value.components(separatedBy: ",") } } buildSettings["SUPPORTED_PLATFORMS"] = supportedPlatforms.joined(separator: " ") buildSettings["TARGETED_DEVICE_FAMILY"] = targetedDeviceFamily.joined(separator: ",") } // apply custom platform version if let version = target.deploymentTarget { if !specSupportedDestinations.isEmpty { for supportedDestination in specSupportedDestinations { if let platform = Platform(rawValue: supportedDestination.rawValue) { buildSettings[platform.deploymentTargetSetting] = version.deploymentTarget } } } else { buildSettings[target.platform.deploymentTargetSetting] = version.deploymentTarget } } // Prevent setting presets from overrwriting settings in target xcconfig files if let configPath = target.configFiles[config.name] { buildSettings = removeConfigFileSettings(from: buildSettings, configPath: configPath) } // Prevent setting presets from overrwriting settings in project xcconfig files if let configPath = configFiles[config.name] { buildSettings = removeConfigFileSettings(from: buildSettings, configPath: configPath) } buildSettings += getBuildSettings(settings: target.settings, config: config) return buildSettings } public func getBuildSettings(settings: Settings, config: Config) -> BuildSettings { var buildSettings: BuildSettings = [:] for group in settings.groups { if let settings = settingGroups[group] { buildSettings += getBuildSettings(settings: settings, config: config) } } buildSettings += settings.buildSettings for (configVariant, settings) in settings.configSettings { let isPartialMatch = config.name.lowercased().contains(configVariant.lowercased()) if isPartialMatch { let exactConfig = getConfig(configVariant) let matchesExactlyToOtherConfig = exactConfig != nil && exactConfig?.name != config.name if !matchesExactlyToOtherConfig { buildSettings += getBuildSettings(settings: settings, config: config) } } } return buildSettings } // combines all levels of a target's settings: target, target config, project, project config public func getCombinedBuildSetting(_ setting: String, target: ProjectTarget, config: Config) -> Any? { if let target = target as? Target, let value = getTargetBuildSettings(target: target, config: config)[setting] { return value } if let configFilePath = target.configFiles[config.name], let value = loadConfigFileBuildSettings(path: configFilePath)?[setting] { return value } if let value = getProjectBuildSettings(config: config)[setting] { return value } if let configFilePath = configFiles[config.name], let value = loadConfigFileBuildSettings(path: configFilePath)?[setting] { return value } return nil } public func getBoolBuildSetting(_ setting: String, target: ProjectTarget, config: Config) -> Bool? { guard let value = getCombinedBuildSetting(setting, target: target, config: config) else { return nil } if let boolValue = value as? Bool { return boolValue } else if let stringValue = value as? String { return stringValue == "YES" } return nil } public func targetHasBuildSetting(_ setting: String, target: Target, config: Config) -> Bool { getCombinedBuildSetting(setting, target: target, config: config) != nil } /// Removes values from build settings if they are defined in an xcconfig file private func removeConfigFileSettings(from buildSettings: BuildSettings, configPath: String) -> BuildSettings { var buildSettings = buildSettings if let configSettings = loadConfigFileBuildSettings(path: configPath) { for key in configSettings.keys { // FIXME: Catch platform specifier. e.g. LD_RUNPATH_SEARCH_PATHS[sdk=iphone*] buildSettings.removeValue(forKey: key) buildSettings.removeValue(forKey: key.quoted) } } return buildSettings } /// Returns cached build settings from a config file private func loadConfigFileBuildSettings(path: String) -> BuildSettings? { let configFilePath = basePath + path if let cached = configFileSettings[configFilePath.string] { return cached.value } else { guard let configFile = try? XCConfig(path: configFilePath) else { configFileSettings[configFilePath.string] = .nothing return nil } let settings = configFile.flattenedBuildSettings() configFileSettings[configFilePath.string] = .cached(settings) return settings } } } private enum Cached { case cached(T) case nothing var value: T? { switch self { case let .cached(value): return value case .nothing: return nil } } } // cached flattened xcconfig file settings private var configFileSettings: [String: Cached] = [:] // cached setting preset settings private var settingPresetSettings: [String: Cached] = [:] extension SettingsPresetFile { public func getBuildSettings() -> BuildSettings? { if let cached = settingPresetSettings[path] { return cached.value } let bundlePath = Path(Bundle.main.bundlePath) let relativePath = Path("SettingPresets/\(path).yml") var possibleSettingsPaths: [Path] = [ relativePath, bundlePath + relativePath, bundlePath + "../share/xcodegen/\(relativePath)", Path(#file).parent().parent().parent() + relativePath, ] if let resourcePath = Bundle.main.resourcePath { possibleSettingsPaths.append(Path(resourcePath) + relativePath) } if let symlink = try? (bundlePath + "xcodegen").symlinkDestination() { possibleSettingsPaths = [ symlink.parent() + relativePath, ] + possibleSettingsPaths } if let moduleResourcePath = Bundle.availableModule?.path(forResource: "SettingPresets", ofType: nil) { possibleSettingsPaths.append(Path(moduleResourcePath) + "\(path).yml") } guard let settingsPath = possibleSettingsPaths.first(where: { $0.exists }) else { switch self { case .base, .config, .platform, .supportedDestination: print("No \"\(name)\" settings found") case .product, .productPlatform: break } settingPresetSettings[path] = .nothing return nil } guard let buildSettings = try? loadYamlDictionary(path: settingsPath) else { print("Error parsing \"\(name)\" settings") return nil } settingPresetSettings[path] = .cached(buildSettings) return buildSettings } } private class BundleFinder {} /// The default SPM generated `Bundle.module` crashes on runtime if there is no .bundle file. /// Below implementation modified from generated `Bundle.module` code which call `fatalError` if .bundle file not found. private extension Bundle { /// Returns the resource bundle associated with the current Swift module. static let availableModule: Bundle? = { let bundleName = "XcodeGen_XcodeGenKit" let overrides: [URL] #if DEBUG // The 'PACKAGE_RESOURCE_BUNDLE_PATH' name is preferred since the expected value is a path. The // check for 'PACKAGE_RESOURCE_BUNDLE_URL' will be removed when all clients have switched over. // This removal is tracked by rdar://107766372. if let override = ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_PATH"] ?? ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_URL"] { overrides = [URL(fileURLWithPath: override)] } else { overrides = [] } #else overrides = [] #endif let candidates = overrides + [ // Bundle should be present here when the package is linked into an App. Bundle.main.resourceURL, // Bundle should be present here when the package is linked into a framework. Bundle(for: BundleFinder.self).resourceURL, // For command-line tools. Bundle.main.bundleURL, ] for candidate in candidates { let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle") if let bundle = bundlePath.flatMap(Bundle.init(url:)) { return bundle } } return nil }() }