Files
XcodeGen/Sources/XcodeGenKit/SettingsBuilder.swift
T
JP Simard f65dad7625 Speed up SettingsBuilder (#1221)
* Speed up SettingsBuilder

It's unnecessary to build up a whole grouped dictionary only to check
if all platforms are identical and then immediately discard the
dictionary.

Instead we can check if all targets match the first platform, which
avoids creating a new dictionary but also allows bailing early as soon
as a non-matching platform is found.

Generating a large project (36MB json spec) on an M1 Max machine leads
to a ~6% total speedup: 28.48s vs 30.07s.

* Add changelog entry
2022-06-16 13:54:28 +10:00

221 lines
8.1 KiB
Swift

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()
if options.settingPresets.applyTarget {
buildSettings += SettingsPresetFile.platform(target.platform).getBuildSettings()
buildSettings += SettingsPresetFile.product(target.type).getBuildSettings()
buildSettings += SettingsPresetFile.productPlatform(target.type, target.platform).getBuildSettings()
}
// apply custom platform version
if let version = target.deploymentTarget {
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<T> {
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<BuildSettings>] = [:]
// cached setting preset settings
private var settingPresetSettings: [String: Cached<BuildSettings>] = [:]
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 symlink = try? (bundlePath + "xcodegen").symlinkDestination() {
possibleSettingsPaths = [
symlink.parent() + relativePath,
] + possibleSettingsPaths
}
guard let settingsPath = possibleSettingsPaths.first(where: { $0.exists }) else {
switch self {
case .base, .config, .platform:
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
}
}