blob: 50627877fa309758cc5e04645c516b501eb45311 [file] [log] [blame]
// Copyright 2016 The Tulsi Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// Keys for all user modifiable Tulsi options.
// Style note: Entries that map directly to Xcode Build Settings values are all caps (matching
// Xcode's use) while entries handled in Tulsi are camel case.
public enum TulsiOptionKey: String {
case
// Whether or not to search user header paths first when resolving angle bracket includes.
ALWAYS_SEARCH_USER_PATHS,
// What C++ language standard to use for the project.
CLANG_CXX_LANGUAGE_STANDARD,
// The path to the Bazel binary.
BazelPath,
// Whether or not to claim Swift code was created at the same version as Tulsi itself.
// Suppresses the Xcode warning and automated update on first opening of the generated
// project.
SuppressSwiftUpdateCheck,
// The path from a config file to its associated workspace root.
WorkspaceRootPath,
// Commandline Arguments used by the run phase of the generated scheme.
CommandlineArguments,
// Environment Variables used by the run phase of the generated scheme.
EnvironmentVariables,
// Option to enable compilation after error.
BazelContinueBuildingAfterError,
// Include all .bzl files related to the build in the generated Xcodeproj.
IncludeBuildSources,
// Compilation mode to use during project generation.
ProjectGenerationCompilationMode,
// Platform configuration to use during project generation.
ProjectGenerationPlatformConfiguration,
// Improve auto-completion for include/import statements.
ImprovedImportAutocompletionFix,
// Generate .runfiles directory, as referenced by TEST_SRCDIR in bazel tests.
GenerateRunfiles,
// Used by Tulsi to improve Bazel-caching of build flags.
ProjectPrioritizesSwift,
// Custom build phase run script that runs before bazel build.
PreBuildPhaseRunScript,
// Custom build phase run script that runs after bazel build.
PostBuildPhaseRunScript,
// When building an iOS app with a companion watchOS app, the default architecture for the
// watchOS app will be armv7k. This option overrides the default and uses arm64_32 instead.
UseArm64_32
// Options for build invocations.
case BazelBuildOptionsDebug,
BazelBuildOptionsRelease
// Startup options for build invocations.
case BazelBuildStartupOptionsDebug,
BazelBuildStartupOptionsRelease
// Pre action scripts for build, launch, and test actions.
case BuildActionPreActionScript,
LaunchActionPreActionScript,
TestActionPreActionScript
// Post action scripts for build, launch, and test actions.
case BuildActionPostActionScript,
LaunchActionPostActionScript,
TestActionPostActionScript
}
/// Logical groupings for TulsiOptionKeys.
public enum TulsiOptionKeyGroup: String {
case BazelBuildOptions,
BazelBuildStartupOptions,
PreActionScriptOptions,
PostActionScriptOptions
}
/// Models the set of all user-modifiable options supported by Tulsi.
public class TulsiOptionSet: Equatable {
/// Suffix added to string keys in order to resolve an option's description.
static let DescriptionStringKeySuffix = "_DESC"
/// The key under which option sets are serialized.
static let PersistenceKey = "optionSet"
typealias PersistenceType = [String: TulsiOption.PersistenceType]
static let OptionKeyGroups: [TulsiOptionKey: TulsiOptionKeyGroup] = [
.BazelBuildOptionsDebug: .BazelBuildOptions,
.BazelBuildOptionsRelease: .BazelBuildOptions,
.BazelBuildStartupOptionsDebug: .BazelBuildStartupOptions,
.BazelBuildStartupOptionsRelease: .BazelBuildStartupOptions,
.BuildActionPreActionScript: .PreActionScriptOptions,
.LaunchActionPreActionScript: .PreActionScriptOptions,
.TestActionPreActionScript: .PreActionScriptOptions,
.BuildActionPostActionScript: .PostActionScriptOptions,
.LaunchActionPostActionScript: .PostActionScriptOptions,
.TestActionPostActionScript: .PostActionScriptOptions
]
public var allVisibleOptions = [TulsiOptionKey: TulsiOption]()
var options = [TulsiOptionKey: TulsiOption]() {
didSet {
allVisibleOptions = [TulsiOptionKey: TulsiOption]()
for (key, option) in options {
if !option.optionType.contains(.Hidden) {
allVisibleOptions[key] = option
}
}
}
}
var optionKeyGroupInfo = [TulsiOptionKeyGroup: (displayName: String, description: String)]()
public subscript(optionKey: TulsiOptionKey) -> TulsiOption {
return options[optionKey]!
}
public subscript(optionKey: TulsiOptionKey, target: String) -> String? {
return options[optionKey]?.valueForTarget(target)
}
static func getOptionsFromContainerDictionary(_ dict: [String: Any]) -> PersistenceType? {
return dict[TulsiOptionSet.PersistenceKey] as? PersistenceType
}
public init(withInheritanceEnabled inherit: Bool = false) {
let bundle = Bundle(for: type(of: self))
populateOptionsWithBundle(bundle, withInheritAsDefault: inherit)
populateOptionGroupInfoWithBundle(bundle)
}
public convenience init(fromDictionary dict: [String: Any]) {
self.init()
guard let persistedOptions = dict as? PersistenceType else {
assertionFailure("Options dictionary is not of the expected type")
return
}
for (key, option) in options {
if let value = persistedOptions[key.rawValue] {
option.deserialize(value)
}
}
}
/// Returns a new TulsiOptionSet by using the given parent as a base and applying this option
/// set's options as overrides.
public func optionSetByInheritingFrom(_ parent: TulsiOptionSet) -> TulsiOptionSet {
var resolvedOptions = [TulsiOptionKey: TulsiOption]()
for (key, opt) in options {
guard let parentOption = parent.options[key] else {
resolvedOptions[key] = opt
continue
}
resolvedOptions[key] = TulsiOption(resolvingValuesFrom: opt, byInheritingFrom: parentOption)
}
let resolvedSet = TulsiOptionSet()
resolvedSet.options = resolvedOptions
return resolvedSet
}
func saveShareableOptionsIntoDictionary(_ dict: inout [String: Any]) {
let serialized = saveToDictionary() {
!$1.optionType.contains(.PerUserOnly)
}
dict[TulsiOptionSet.PersistenceKey] = serialized
}
func savePerUserOptionsIntoDictionary(_ dict: inout [String: Any]) {
let serialized = saveToDictionary() {
return $1.optionType.contains(.PerUserOnly)
}
dict[TulsiOptionSet.PersistenceKey] = serialized
}
func saveAllOptionsIntoDictionary(_ dict: inout [String: AnyObject]) {
let serialized = saveToDictionary() { (_, _) in return true }
dict[TulsiOptionSet.PersistenceKey] = serialized as AnyObject?
}
public func groupInfoForOptionKey(_ key: TulsiOptionKey) -> (TulsiOptionKeyGroup, displayName: String, description: String)? {
guard let keyGroup = TulsiOptionSet.OptionKeyGroups[key] else { return nil }
guard let (displayName, description) = optionKeyGroupInfo[keyGroup] else {
assertionFailure("Missing group information for group key \(keyGroup)")
return (keyGroup, "\(keyGroup)", "")
}
return (keyGroup, displayName, description)
}
/// Returns a dictionary of build settings without applying any specializations.
func commonBuildSettings() -> [String: String] {
// These values come from AppleToolchain.java in Bazel
// https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/rules/apple/AppleToolchain.java
var buildSettings = [
"GCC_WARN_64_TO_32_BIT_CONVERSION": "YES",
"CLANG_WARN_BOOL_CONVERSION": "YES",
"CLANG_WARN_CONSTANT_CONVERSION": "YES",
"CLANG_WARN__DUPLICATE_METHOD_MATCH": "YES",
"CLANG_WARN_EMPTY_BODY": "YES",
"CLANG_WARN_ENUM_CONVERSION": "YES",
"CLANG_WARN_INT_CONVERSION": "YES",
"CLANG_WARN_UNREACHABLE_CODE": "YES",
"GCC_WARN_ABOUT_RETURN_TYPE": "YES",
"GCC_WARN_UNDECLARED_SELECTOR": "YES",
"GCC_WARN_UNINITIALIZED_AUTOS": "YES",
"GCC_WARN_UNUSED_FUNCTION": "YES",
"GCC_WARN_UNUSED_VARIABLE": "YES",
]
for (key, opt) in options.filter({ $1.optionType.contains(.BuildSetting) }) {
buildSettings[key.rawValue] = opt.commonValue!
}
return buildSettings
}
/// Returns a dictionary of build settings specialized for the given target without inheriting any
/// defaults.
func buildSettingsForTarget(_ target: String) -> [String: String] {
var buildSettings = [String: String]()
for (key, opt) in options.filter({ $1.optionType.contains(.TargetSpecializableBuildSetting) }) {
if let val = opt.valueForTarget(target, inherit: false) {
buildSettings[key.rawValue] = val
}
}
return buildSettings
}
// MARK: - Private methods.
private func saveToDictionary(_ filter: (TulsiOptionKey, TulsiOption) -> Bool) -> PersistenceType {
var serialized = PersistenceType()
for (key, option) in options.filter(filter) {
if let value = option.serialize() {
serialized[key.rawValue] = value
}
}
return serialized
}
private func populateOptionsWithBundle(_ bundle: Bundle, withInheritAsDefault inherit: Bool) {
func addOption(_ optionKey: TulsiOptionKey, valueType: TulsiOption.ValueType, optionType: TulsiOption.OptionType, defaultValue: String?) {
let key = optionKey.rawValue
let displayName = bundle.localizedString(forKey: key, value: nil, table: "Options")
let descriptionKey = key + TulsiOptionSet.DescriptionStringKeySuffix
var description = bundle.localizedString(forKey: descriptionKey, value: nil, table: "Options")
if description == descriptionKey { description = "" }
let opt = TulsiOption(displayName: displayName,
userDescription: description,
valueType: valueType,
optionType: optionType,
defaultValue: defaultValue)
if inherit && optionType.contains(.SupportsInheritKeyword) {
opt.projectValue = TulsiOption.InheritKeyword
}
options[optionKey] = opt
}
func addBoolOption(_ optionKey: TulsiOptionKey, _ optionType: TulsiOption.OptionType, _ defaultValue: Bool = false) {
let val = defaultValue ? TulsiOption.BooleanTrueValue : TulsiOption.BooleanFalseValue
addOption(optionKey, valueType: .bool, optionType: optionType, defaultValue: val)
}
func addStringOption(_ optionKey: TulsiOptionKey, _ optionType: TulsiOption.OptionType, _ defaultValue: String? = nil) {
addOption(optionKey, valueType: .string, optionType: optionType, defaultValue: defaultValue)
}
func addStringEnumOption(_ optionKey: TulsiOptionKey,
_ optionType: TulsiOption.OptionType,
_ defaultValue: String,
_ values: [String]) {
assert(values.contains(defaultValue), "Invalid enum for \(optionKey.rawValue): " +
"defaultValue of \"\(defaultValue)\" is not present in enum values: \(values).")
addOption(optionKey, valueType: .stringEnum(values),
optionType: optionType, defaultValue: defaultValue)
}
addBoolOption(.ALWAYS_SEARCH_USER_PATHS, .BuildSetting, false)
addBoolOption(.BazelContinueBuildingAfterError, .Generic, false)
addStringOption(.BazelBuildOptionsDebug, [.TargetSpecializable, .SupportsInheritKeyword])
addStringOption(.BazelBuildOptionsRelease, [.TargetSpecializable, .SupportsInheritKeyword])
addStringOption(.BazelBuildStartupOptionsDebug, [.TargetSpecializable, .SupportsInheritKeyword])
addStringOption(.BazelBuildStartupOptionsRelease, [.TargetSpecializable, .SupportsInheritKeyword])
addBoolOption(.SuppressSwiftUpdateCheck, .Generic, true)
addBoolOption(.IncludeBuildSources, .Generic, false)
addBoolOption(.ImprovedImportAutocompletionFix, .Generic, true)
addBoolOption(.GenerateRunfiles, .Generic, false)
addBoolOption(.ProjectPrioritizesSwift, .Generic, false)
addBoolOption(.UseArm64_32, .Generic, false)
let defaultIdentifier = PlatformConfiguration.defaultConfiguration.identifier
let platformCPUIdentifiers = PlatformConfiguration.allValidConfigurations.map { $0.identifier }
addStringEnumOption(.ProjectGenerationPlatformConfiguration, .Generic,
defaultIdentifier, platformCPUIdentifiers)
addStringEnumOption(.ProjectGenerationCompilationMode, .Generic, "dbg", ["dbg", "opt"])
addStringOption(.CommandlineArguments, [.TargetSpecializable, .SupportsInheritKeyword])
addStringOption(.EnvironmentVariables, [.TargetSpecializable, .SupportsInheritKeyword])
// List matches the available options for the 'C++ Language Dialect' setting in XCode 9.2.
let cppLanguageStandards = ["compiler-default", "c++98", "gnu++98", "c++0x", "gnu++0x", "c++14", "gnu++14", "c++17", "gnu++17"]
addStringEnumOption(.CLANG_CXX_LANGUAGE_STANDARD, .BuildSetting, "compiler-default", cppLanguageStandards)
addStringOption(.PreBuildPhaseRunScript, [.TargetSpecializable])
addStringOption(.PostBuildPhaseRunScript, [.TargetSpecializable])
addStringOption(.BuildActionPreActionScript, [.TargetSpecializable, .SupportsInheritKeyword])
addStringOption(.LaunchActionPreActionScript, [.TargetSpecializable, .SupportsInheritKeyword])
addStringOption(.TestActionPreActionScript, [.TargetSpecializable, .SupportsInheritKeyword])
addStringOption(.BuildActionPostActionScript, [.TargetSpecializable, .SupportsInheritKeyword])
addStringOption(.LaunchActionPostActionScript, [.TargetSpecializable, .SupportsInheritKeyword])
addStringOption(.TestActionPostActionScript, [.TargetSpecializable, .SupportsInheritKeyword])
addStringOption(.BazelPath, [.Hidden, .PerUserOnly])
addStringOption(.WorkspaceRootPath, [.Hidden, .PerUserOnly])
}
private func populateOptionGroupInfoWithBundle(_ bundle: Bundle) {
for (_, keyGroup) in TulsiOptionSet.OptionKeyGroups {
if optionKeyGroupInfo[keyGroup] == nil {
let key = keyGroup.rawValue
let displayName = NSLocalizedString(key, tableName: "Options", bundle: bundle, comment: "")
let descriptionKey = key + TulsiOptionSet.DescriptionStringKeySuffix
let description = NSLocalizedString(descriptionKey, tableName: "Options", bundle: bundle, comment: "")
optionKeyGroupInfo[keyGroup] = (displayName, description)
}
}
}
}
public func ==(lhs: TulsiOptionSet, rhs: TulsiOptionSet) -> Bool {
for (key, option) in lhs.options {
if rhs[key] != option {
return false
}
}
return true
}