Add Bazel feature flags functionality

This allows Tulsi to define flags conditional on Swift/non-Swift,
certain project configurations, etc. without impacting the Bazel
analysis cache.

Previously we would directly set Build Settings on the generated
xcodeproj in order to pass down these flags; however, this is
not a valid solution for caching reasons.

PiperOrigin-RevId: 204796507
diff --git a/src/Tulsi.xcodeproj/project.pbxproj b/src/Tulsi.xcodeproj/project.pbxproj
index 7af3111..8f59a39 100644
--- a/src/Tulsi.xcodeproj/project.pbxproj
+++ b/src/Tulsi.xcodeproj/project.pbxproj
@@ -116,6 +116,7 @@
 		54D8453F20CB121D004F6CF2 /* BazelSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8453E20CB121D004F6CF2 /* BazelSettingsProvider.swift */; };
 		54EA05C81F62E3A700472AB6 /* RuleEntryMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EA05C71F62E3A700472AB6 /* RuleEntryMap.swift */; };
 		54EC201820D1A8270050AF12 /* TulsiApplicationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EC201720D1A8270050AF12 /* TulsiApplicationSupport.swift */; };
+		54EDD24520D9BC27001A1B35 /* BuildSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EDD24420D9BC26001A1B35 /* BuildSettingsTests.swift */; };
 		54EF320A1F3E0804009E9C7F /* bazel_build_events.py in Resources */ = {isa = PBXBuildFile; fileRef = 54EF32091F3E0804009E9C7F /* bazel_build_events.py */; };
 		774F6E9720A2400E00572B76 /* bazel_build_flags.py in Resources */ = {isa = PBXBuildFile; fileRef = 774F6E9620A2400E00572B76 /* bazel_build_flags.py */; };
 		8B0F78C81BE5BC7E00357561 /* ConfigEditorSourceFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B0F78C71BE5BC7E00357561 /* ConfigEditorSourceFilterViewController.swift */; };
@@ -302,6 +303,7 @@
 		54D8453E20CB121D004F6CF2 /* BazelSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BazelSettingsProvider.swift; sourceTree = "<group>"; };
 		54EA05C71F62E3A700472AB6 /* RuleEntryMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleEntryMap.swift; sourceTree = "<group>"; };
 		54EC201720D1A8270050AF12 /* TulsiApplicationSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TulsiApplicationSupport.swift; sourceTree = "<group>"; };
+		54EDD24420D9BC26001A1B35 /* BuildSettingsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildSettingsTests.swift; sourceTree = "<group>"; };
 		54EF32091F3E0804009E9C7F /* bazel_build_events.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = bazel_build_events.py; sourceTree = "<group>"; };
 		774F6E9620A2400E00572B76 /* bazel_build_flags.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; name = bazel_build_flags.py; path = TulsiGenerator/Scripts/bazel_build_flags.py; sourceTree = "<group>"; };
 		8B0F78C71BE5BC7E00357561 /* ConfigEditorSourceFilterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigEditorSourceFilterViewController.swift; sourceTree = "<group>"; };
@@ -414,6 +416,7 @@
 			isa = PBXGroup;
 			children = (
 				3D9DAF541C23604100955CD5 /* BuildLabelTests.swift */,
+				54EDD24420D9BC26001A1B35 /* BuildSettingsTests.swift */,
 				3DFB7C4E1C835EFB00376760 /* CommandLineSplitterTests.swift */,
 				3D9926751C29F0A30094E098 /* Info.plist */,
 				3DA65B411C68539F0055448E /* Mocks */,
@@ -910,6 +913,7 @@
 				3D9926811C29F0CC0094E098 /* PBXTargetGeneratorTests.swift in Sources */,
 				3D9926821C29F0CC0094E098 /* BuildLabelTests.swift in Sources */,
 				3D9926841C29F0CC0094E098 /* PBXObjectsTests.swift in Sources */,
+				54EDD24520D9BC27001A1B35 /* BuildSettingsTests.swift in Sources */,
 				3DA65B621C693B570055448E /* TulsiGeneratorConfigTests.swift in Sources */,
 				3DA65B3E1C6849140055448E /* XcodeProjectGeneratorTests.swift in Sources */,
 				54D17A5220D94C4B0028D377 /* PythonableTests.swift in Sources */,
diff --git a/src/Tulsi/TulsiGeneratorConfigDocument.swift b/src/Tulsi/TulsiGeneratorConfigDocument.swift
index 78782f0..2f78247 100644
--- a/src/Tulsi/TulsiGeneratorConfigDocument.swift
+++ b/src/Tulsi/TulsiGeneratorConfigDocument.swift
@@ -441,6 +441,14 @@
     }
   }
 
+  private func enabledFeatures(options: TulsiOptionSet) -> Set<BazelSettingFeature> {
+    let workspaceRoot = infoExtractor.workspaceRootURL.path
+    let bazelExecroot = infoExtractor.bazelExecutionRoot
+    return BazelBuildSettingsFeatures.enabledFeatures(options: options,
+                                                      workspaceRoot: workspaceRoot,
+                                                      bazelExecRoot: bazelExecroot)
+  }
+
   // Regenerates the sourcePaths array based on the currently selected ruleEntries.
   func updateSourcePaths(_ callback: @escaping ([UISourcePath]) -> Void) {
     var sourcePathMap = [String: UISourcePath]()
@@ -467,7 +475,8 @@
                                                                    startupOptions: startupOptions,
                                                                    buildOptions: buildOptions,
                                                                    projectGenBuildOptions: projectGenBuildOptions,
-                                                                   prioritizeSwiftOption: prioritizeSwiftOption)
+                                                                   prioritizeSwiftOption: prioritizeSwiftOption,
+                                                                   features: self.enabledFeatures(options: optionSet))
       } catch TulsiProjectInfoExtractor.ExtractorError.ruleEntriesFailed(let info) {
         LogMessage.postError("Label resolution failed: \(info)")
         return
@@ -787,11 +796,13 @@
       return
     }
 
+    let options = optionSet!
     let ruleEntryMap = try infoExtractor.ruleEntriesForLabels(concreteBuildTargetLabels,
-                                                              startupOptions: optionSet![.BazelBuildStartupOptionsDebug],
-                                                              buildOptions: optionSet![.BazelBuildOptionsDebug],
-                                                              projectGenBuildOptions: optionSet![.BazelBuildOptionsProjectGenerationOnly],
-                                                              prioritizeSwiftOption: optionSet![.ProjectPrioritizesSwift])
+                                                              startupOptions: options[.BazelBuildStartupOptionsDebug],
+                                                              buildOptions: options[.BazelBuildOptionsDebug],
+                                                              projectGenBuildOptions: options[.BazelBuildOptionsProjectGenerationOnly],
+                                                              prioritizeSwiftOption: options[.ProjectPrioritizesSwift],
+                                                              features: enabledFeatures(options: options))
     var unresolvedLabels = Set<BuildLabel>()
     var ruleInfos = [UIRuleInfo]()
     for label in concreteBuildTargetLabels {
diff --git a/src/TulsiGenerator/BazelAspectInfoExtractor.swift b/src/TulsiGenerator/BazelAspectInfoExtractor.swift
index 49f66c1..d253ab2 100644
--- a/src/TulsiGenerator/BazelAspectInfoExtractor.swift
+++ b/src/TulsiGenerator/BazelAspectInfoExtractor.swift
@@ -67,7 +67,8 @@
                                    startupOptions: [String] = [],
                                    buildOptions: [String] = [],
                                    projectGenerationOptions: [String] = [],
-                                   prioritizeSwift: Bool = false) throws -> RuleEntryMap {
+                                   prioritizeSwift: Bool = false,
+                                   features: Set<BazelSettingFeature> = []) throws -> RuleEntryMap {
     guard !targets.isEmpty else {
       return RuleEntryMap()
     }
@@ -76,7 +77,8 @@
                                           startupOptions: startupOptions,
                                           buildOptions: buildOptions,
                                           projectGenerationOptions: projectGenerationOptions,
-                                          prioritizeSwift: prioritizeSwift)
+                                          prioritizeSwift: prioritizeSwift,
+                                          features: features)
   }
 
   // MARK: - Private methods
@@ -85,7 +87,8 @@
                                           startupOptions: [String],
                                           buildOptions: [String],
                                           projectGenerationOptions: [String],
-                                          prioritizeSwift: Bool) throws -> RuleEntryMap {
+                                          prioritizeSwift: Bool,
+                                          features: Set<BazelSettingFeature>) throws -> RuleEntryMap {
     localizedMessageLogger.infoMessage("Build Events JSON file at \"\(buildEventsFilePath)\"")
 
     let progressNotifier = ProgressNotifier(name: SourceFileExtraction,
@@ -105,6 +108,7 @@
                                                buildOptions: buildOptions,
                                                projectGenerationOptions: projectGenerationOptions,
                                                prioritizeSwift: prioritizeSwift,
+                                               features: features,
                                                progressNotifier: progressNotifier) {
                                                 (process: Process, debugInfo: String) -> Void in
        defer { semaphore.signal() }
@@ -162,6 +166,7 @@
                                             buildOptions: [String] = [],
                                             projectGenerationOptions: [String] = [],
                                             prioritizeSwift: Bool,
+                                            features: Set<BazelSettingFeature>,
                                             progressNotifier: ProgressNotifier? = nil,
                                             terminationHandler: @escaping CompletionHandler) -> Process? {
 
@@ -177,7 +182,8 @@
 
     let tulsiVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "UNKNOWN"
 
-    let tulsiFlags = bazelSettingsProvider.tulsiFlags(hasSwift: prioritizeSwift).getFlags()
+    let tulsiFlags = bazelSettingsProvider.tulsiFlags(hasSwift: prioritizeSwift,
+                                                      features: features).getFlags()
     var arguments = startupOptions
     arguments.append(contentsOf: tulsiFlags.startup)
     arguments.append("build")
diff --git a/src/TulsiGenerator/BazelBuildSettings.swift b/src/TulsiGenerator/BazelBuildSettings.swift
index 4743124..47e7d21 100644
--- a/src/TulsiGenerator/BazelBuildSettings.swift
+++ b/src/TulsiGenerator/BazelBuildSettings.swift
@@ -185,6 +185,9 @@
   public let tulsiSwiftFlagSet: BazelFlagsSet
   public let tulsiNonSwiftFlagSet: BazelFlagsSet
 
+  public let swiftFeatures: [String]
+  public let nonSwiftFeatures: [String]
+
   /// Set of targets which depend (in some fashion) on Swift.
   public let swiftTargets: Set<String>
 
@@ -198,6 +201,8 @@
               tulsiCacheSafeFlagSet: BazelFlagsSet,
               tulsiSwiftFlagSet: BazelFlagsSet,
               tulsiNonSwiftFlagSet: BazelFlagsSet,
+              swiftFeatures: [String],
+              nonSwiftFeatures: [String],
               projDefaultFlagSet: BazelFlagsSet,
               projTargetFlagSets: [String: BazelFlagsSet]) {
     self.bazel = bazel
@@ -207,6 +212,8 @@
     self.tulsiCacheSafeFlagSet = tulsiCacheSafeFlagSet
     self.tulsiSwiftFlagSet = tulsiSwiftFlagSet
     self.tulsiNonSwiftFlagSet = tulsiNonSwiftFlagSet
+    self.swiftFeatures = swiftFeatures
+    self.nonSwiftFeatures = nonSwiftFeatures
     self.projDefaultFlagSet = projDefaultFlagSet
     self.projTargetFlagSets = projTargetFlagSets
   }
@@ -222,6 +229,8 @@
 \(nestedIndentation)\(tulsiCacheSafeFlagSet.toPython(nestedIndentation)),
 \(nestedIndentation)\(tulsiSwiftFlagSet.toPython(nestedIndentation)),
 \(nestedIndentation)\(tulsiNonSwiftFlagSet.toPython(nestedIndentation)),
+\(nestedIndentation)\(swiftFeatures.toPython(nestedIndentation)),
+\(nestedIndentation)\(nonSwiftFeatures.toPython(nestedIndentation)),
 \(nestedIndentation)\(projDefaultFlagSet.toPython(nestedIndentation)),
 \(nestedIndentation)\(projTargetFlagSets.toPython(nestedIndentation)),
 \(indentation))
diff --git a/src/TulsiGenerator/BazelBuildSettingsFeatures.swift b/src/TulsiGenerator/BazelBuildSettingsFeatures.swift
index 1bb7437..7b405f0 100644
--- a/src/TulsiGenerator/BazelBuildSettingsFeatures.swift
+++ b/src/TulsiGenerator/BazelBuildSettingsFeatures.swift
@@ -12,9 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-let bazelBuildSettingsFeatures = [
-  // For non-distributed builds.
-  // TODO(b/69857078): Replace with normalized debug info when wrapped_clang
-  // is updated.
-  "TULSI_DIRECT_DBG_PREFIX_MAP",
-]
+public class BazelBuildSettingsFeatures {
+  public static func enabledFeatures(
+      options: TulsiOptionSet,
+      workspaceRoot: String,
+      bazelExecRoot: String
+  ) -> Set<BazelSettingFeature> {
+    return [.DirectDebugPrefixMap(bazelExecRoot, workspaceRoot)]
+  }
+}
diff --git a/src/TulsiGenerator/BazelSettingsProvider.swift b/src/TulsiGenerator/BazelSettingsProvider.swift
index c9524bd..2bb3d61 100644
--- a/src/TulsiGenerator/BazelSettingsProvider.swift
+++ b/src/TulsiGenerator/BazelSettingsProvider.swift
@@ -14,18 +14,100 @@
 
 import Foundation
 
+/// Bazel feature settings that map to Bazel flags (start up or build options). These flags may
+/// affect Bazel analysis/action caching and therefore should be kept consistent between all
+/// invocations from Tulsi.
+///
+/// If adding a flag that does not impact Bazel caching, it can be added directly to
+/// BazelSettingsProvider directly (either as a cacheableFlag or a configBasedFlag).
+public enum BazelSettingFeature: Hashable, Pythonable {
+
+  /// Feature flag to normalize paths present in debug information via a Clang flag for distributed
+  /// builds (e.g. multiple distinct paths).
+  /// - Mutually exclusive with DirectDebugPrefixMap feature
+  ///
+  /// The use of this flag does not affect any sources built by swiftc. At present time, all Swift
+  /// compiled sources will be built with uncacheable, absolute paths, as the Swift compiler does
+  /// not provide an easy means of similarly normalizing all debug information.
+  case DebugPathNormalization
+
+  /// Feature flag to normalize paths present in debug information via a Clang flag for local
+  /// builds.
+  /// - Mutually exclusive with DebugPathNormalization feature
+  ///
+  /// NOTE: Use of -fdebug-prefix-map leads to producing binaries that cannot be
+  /// reused across multiple machines by a distributed build system, unless the
+  /// absolute paths to files visible to Xcode match perfectly between all of
+  /// those machines.
+  ///
+  /// For this reason, -fdebug-prefix-map is provided as a default for non-distributed purposes.
+  case DirectDebugPrefixMap(String, String)
+
+  public var stringValue: String {
+    switch self {
+      case .DebugPathNormalization:
+        return "DebugPathNormalization"
+      case .DirectDebugPrefixMap:
+        return "DirectDebugPrefixMap"
+    }
+  }
+
+  public var hashValue: Int {
+    return stringValue.hashValue
+  }
+
+  public static func ==(lhs: BazelSettingFeature, rhs: BazelSettingFeature) -> Bool {
+    return lhs.stringValue == rhs.stringValue
+  }
+
+  public var supportsSwift: Bool {
+    switch self {
+      case .DebugPathNormalization:
+        return false
+      case .DirectDebugPrefixMap:
+        return true
+    }
+  }
+
+  public var supportsNonSwift: Bool {
+    switch self {
+      case .DebugPathNormalization:
+        return true
+      case .DirectDebugPrefixMap:
+        return true
+    }
+  }
+
+  /// Start up flags for this feature.
+  public var startupFlags: [String] {
+    return []
+  }
+
+  /// Build flags for this feature.
+  public var buildFlags: [String] {
+    switch self {
+      case .DebugPathNormalization: return ["--features=debug_prefix_map_pwd_is_dot"]
+      case .DirectDebugPrefixMap(let execRoot, let workspaceRoot): return [
+          String(format: "--copt=-fdebug-prefix-map=%@=%@", execRoot, workspaceRoot)
+      ]
+    }
+  }
+
+  func toPython(_ indentation: String) -> String {
+    return stringValue.toPython(indentation)
+  }
+}
+
 /// Defines an object that provides flags for Bazel invocations.
 protocol BazelSettingsProviderProtocol {
   /// All general-Tulsi flags, varying based on whether the project has Swift or not.
-  func tulsiFlags(hasSwift: Bool) -> BazelFlagsSet
-
-  /// Cache-able Bazel flags based off TulsiOptions, used to generate BazelBuildSettings.
-  func optionsBasedFlags(_ options: TulsiOptionSet) -> BazelFlagsSet
+  func tulsiFlags(hasSwift: Bool, features: Set<BazelSettingFeature>) -> BazelFlagsSet
 
   /// Bazel build settings, used during Xcode/user Bazel builds.
   func buildSettings(bazel: String,
                      bazelExecRoot: String,
                      options: TulsiOptionSet,
+                     features: Set<BazelSettingFeature>,
                      buildRuleEntries: Set<RuleEntry>) -> BazelBuildSettings
 }
 
@@ -57,11 +139,8 @@
 
   /// Flags added by Tulsi for builds which do not contain Swift.
   /// - Enable dSYMs only for Release builds.
-  /// - Flag for remapping debug symbols.
   static let tulsiNonSwiftFlags = BazelFlagsSet(
-      release: BazelFlags(build: ["--apple_generate_dsym"]),
-      // TODO: Somehow support the open-source version of this which varies based on the exec-root.
-      common: BazelFlags(build: ["--features=debug_prefix_map_pwd_is_dot"]))
+      release: BazelFlags(build: ["--apple_generate_dsym"]))
 
   let universalFlags: BazelFlags
   let cacheableFlags: BazelFlagsSet
@@ -90,11 +169,40 @@
     self.nonSwiftFlags = nonSwiftFlags
   }
 
-  func tulsiFlags(hasSwift: Bool) -> BazelFlagsSet {
-    let languageFlags = hasSwift ? swiftFlags : nonSwiftFlags
+  func tulsiFlags(hasSwift: Bool, features: Set<BazelSettingFeature>) -> BazelFlagsSet {
+    let languageFlags = (hasSwift ? swiftFlags : nonSwiftFlags) + featureFlags(features,
+                                                                               hasSwift: hasSwift)
     return BazelFlagsSet(common: universalFlags) + cacheableFlags + nonCacheableFlags + languageFlags
   }
 
+  /// Non-cacheable Bazel flags based off of BazelSettingFeatures for the project.
+  func featureFlags(_ features: Set<BazelSettingFeature>, hasSwift: Bool) -> BazelFlagsSet {
+    let validFeatures = features.filter { return hasSwift ? $0.supportsSwift : $0.supportsNonSwift }
+    let sortedFeatures = validFeatures.sorted { (a, b) -> Bool in
+      return a.stringValue > b.stringValue
+    }
+
+    let startupFlags = sortedFeatures.reduce(into: []) { (arr, feature) in
+      arr.append(contentsOf: feature.startupFlags)
+    }
+    let buildFlags = sortedFeatures.reduce(into: []) { (arr, feature) in
+      arr.append(contentsOf: feature.buildFlags)
+    }
+    return BazelFlagsSet(startupFlags: startupFlags, buildFlags: buildFlags)
+  }
+
+  /// Returns an array of the enabled features' names.
+  func featureNames(_ features: Set<BazelSettingFeature>, hasSwift: Bool) -> [String] {
+    let validFeatures = features.filter { return hasSwift ? $0.supportsSwift : $0.supportsNonSwift }
+    return validFeatures.sorted { (a, b) -> Bool in
+      return a.stringValue > b.stringValue
+    }.map { $0.stringValue }
+  }
+
+  /// Cache-able Bazel flags based off TulsiOptions, used to generate BazelBuildSettings. This
+  /// should only add flags that do not affect Bazel analysis/action caching; flags that are based
+  /// off of TulsiOptions but do affect Bazel caching should instead be added to as
+  /// BazelSettingFeatures.
   func optionsBasedFlags(_ options: TulsiOptionSet) -> BazelFlagsSet {
     var configBasedTulsiFlags = [String]()
     if let continueBuildingAfterError = options[.BazelContinueBuildingAfterError].commonValueAsBool,
@@ -107,6 +215,7 @@
   func buildSettings(bazel: String,
                      bazelExecRoot: String,
                      options: TulsiOptionSet,
+                     features: Set<BazelSettingFeature>,
                      buildRuleEntries: Set<RuleEntry>) -> BazelBuildSettings {
     let projDefaultSettings = getProjDefaultSettings(options)
     var targetSettings = [String: BazelFlagsSet]()
@@ -130,13 +239,20 @@
     }
     let swiftTargets = Set(swiftRuleEntries.map { $0.label.value })
 
+    let tulsiSwiftFlags = swiftFlags + featureFlags(features, hasSwift: true)
+    let tulsiNonSwiftFlagSet = nonSwiftFlags + featureFlags(features, hasSwift: false)
+    let swiftFeatures = featureNames(features, hasSwift: true)
+    let nonSwiftFeatures = featureNames(features, hasSwift: false)
+
     return BazelBuildSettings(bazel: bazel,
                               bazelExecRoot: bazelExecRoot,
                               swiftTargets: swiftTargets,
                               tulsiCacheAffectingFlagsSet: BazelFlagsSet(common: universalFlags) + nonCacheableFlags,
                               tulsiCacheSafeFlagSet: cacheableFlags + optionsBasedFlags(options),
-                              tulsiSwiftFlagSet: swiftFlags,
-                              tulsiNonSwiftFlagSet: nonSwiftFlags,
+                              tulsiSwiftFlagSet: tulsiSwiftFlags,
+                              tulsiNonSwiftFlagSet: tulsiNonSwiftFlagSet,
+                              swiftFeatures: swiftFeatures,
+                              nonSwiftFeatures: nonSwiftFeatures,
                               projDefaultFlagSet: projDefaultSettings,
                               projTargetFlagSets: targetSettings)
   }
diff --git a/src/TulsiGenerator/BazelWorkspaceInfoExtractor.swift b/src/TulsiGenerator/BazelWorkspaceInfoExtractor.swift
index d7f35d7..29623a3 100644
--- a/src/TulsiGenerator/BazelWorkspaceInfoExtractor.swift
+++ b/src/TulsiGenerator/BazelWorkspaceInfoExtractor.swift
@@ -39,6 +39,9 @@
   /// Bazel settings provider for all invocations.
   let bazelSettingsProvider: BazelSettingsProviderProtocol
 
+  /// Bazel workspace root URL.
+  let workspaceRootURL: URL
+
   /// Fetcher object from which a workspace's path info may be obtained.
   private let workspacePathInfoFetcher: BazelWorkspacePathInfoFetcher
 
@@ -74,6 +77,7 @@
                                              workspaceRootURL: workspaceRootURL,
                                              bazelUniversalFlags: universalFlags,
                                              localizedMessageLogger: localizedMessageLogger)
+    self.workspaceRootURL = workspaceRootURL
   }
 
   // MARK: - BazelWorkspaceInfoExtractorProtocol
@@ -86,7 +90,8 @@
                             startupOptions: TulsiOption,
                             buildOptions: TulsiOption,
                             projectGenBuildOptions: TulsiOption,
-                            prioritizeSwiftOption: TulsiOption) throws -> RuleEntryMap {
+                            prioritizeSwiftOption: TulsiOption,
+                            features: Set<BazelSettingFeature>) throws -> RuleEntryMap {
     func isLabelMissing(_ label: BuildLabel) -> Bool {
       return !ruleEntryCache.hasAnyRuleEntry(withBuildLabel: label)
     }
@@ -111,7 +116,8 @@
                                                         startupOptions: startupOptions,
                                                         buildOptions: buildOptions,
                                                         projectGenerationOptions: projectGenerationOptions,
-                                                        prioritizeSwift: prioritizeSwift)
+                                                        prioritizeSwift: prioritizeSwift,
+                                                        features: features)
       ruleEntryCache = RuleEntryMap(ruleEntryMap)
     } catch BazelAspectInfoExtractor.ExtractorError.buildFailed {
       throw BazelWorkspaceInfoExtractorError.aspectExtractorFailed("Bazel aspects could not be built.")
diff --git a/src/TulsiGenerator/BazelWorkspaceInfoExtractorProtocol.swift b/src/TulsiGenerator/BazelWorkspaceInfoExtractorProtocol.swift
index 1d26444..04233ab 100644
--- a/src/TulsiGenerator/BazelWorkspaceInfoExtractorProtocol.swift
+++ b/src/TulsiGenerator/BazelWorkspaceInfoExtractorProtocol.swift
@@ -30,7 +30,8 @@
                             startupOptions: TulsiOption,
                             buildOptions: TulsiOption,
                             projectGenBuildOptions: TulsiOption,
-                            prioritizeSwiftOption: TulsiOption) throws -> RuleEntryMap
+                            prioritizeSwiftOption: TulsiOption,
+                            features: Set<BazelSettingFeature>) throws -> RuleEntryMap
 
   /// Extracts labels for the files referenced by the build infrastructure for the given set of
   /// BUILD targets.
@@ -51,6 +52,9 @@
   /// Absolute path to the Bazel execution root.
   var bazelExecutionRoot: String {get}
 
+  /// The location of the Bazel workspace to be examined.
+  var workspaceRootURL: URL {get}
+
   /// Bazel flag provider for all invocations.
   var bazelSettingsProvider: BazelSettingsProviderProtocol {get}
 }
diff --git a/src/TulsiGenerator/PBXTargetGenerator.swift b/src/TulsiGenerator/PBXTargetGenerator.swift
index b3195bd..f96bb21 100644
--- a/src/TulsiGenerator/PBXTargetGenerator.swift
+++ b/src/TulsiGenerator/PBXTargetGenerator.swift
@@ -1515,15 +1515,6 @@
       buildSettings["TARGETED_DEVICE_FAMILY[sdk=iphonesimulator*]"] = "1,4"
     }
 
-    // Tell our build script when a Swift dependency has been found. This could be discovered
-    // through the aspect instead, but we keep this information here for the sake of our debug info
-    // remapping story (for now).
-    //
-    // TODO(b/77916902): See if we can base this directly on if we have any Swift files indexed.
-    // The state of has_swift_dependency is currently computed through our aspect.
-    let swiftDependency = entry.attributes[.has_swift_dependency] as? Bool ?? false
-    buildSettings["TULSI_SWIFT_DEPENDENCY"] = swiftDependency ? "YES" : "NO"
-
     // bazel_build.py uses this to determine if it needs to pass the --xcode_version flag, as the
     // flag can have implications for caching even if the user's active Xcode version is the same
     // as the flag.
diff --git a/src/TulsiGenerator/Scripts/bazel_build.py b/src/TulsiGenerator/Scripts/bazel_build.py
index 621207d..d3c1beb 100755
--- a/src/TulsiGenerator/Scripts/bazel_build.py
+++ b/src/TulsiGenerator/Scripts/bazel_build.py
@@ -21,6 +21,7 @@
 import io
 import json
 import os
+import pipes
 import re
 import shutil
 import signal
@@ -256,6 +257,10 @@
         self.targets[0],
         is_debug)
 
+  def GetEnabledFeatures(self):
+    """Returns a list of enabled Bazel features for the active target."""
+    return self.build_settings.features_for_target(self.targets[0])
+
   def GetBazelOptions(self, config):
     """Returns the full set of build options for the given config."""
     bazel, start_up, build = self.GetBaseFlagsForTargets(config)
@@ -351,7 +356,8 @@
 
   BUILD_EVENTS_FILE = 'build_events.json'
 
-  def __init__(self):
+  def __init__(self, build_settings):
+    self.build_settings = build_settings
     self.verbose = 0
     self.build_path = None
     self.bazel_bin_path = None
@@ -371,21 +377,10 @@
                          'earlier Xcode, build %s.' % xcode_build_version)
 
     self.tulsi_version = os.environ.get('TULSI_VERSION', 'UNKNOWN')
-    self.swift_dependency = os.environ.get('TULSI_SWIFT_DEPENDENCY',
-                                           'NO') == 'YES'
 
     # TODO(b/69857078): Remove this when wrapped_clang is updated.
-    self.direct_debug_prefix_map = os.environ.get('TULSI_DIRECT_DBG_PREFIX_MAP',
-                                                  'NO') == 'YES'
-
-    if self.swift_dependency or self.direct_debug_prefix_map:
-      # Disable the normalized debug prefix map as swiftc doesn't support it.
-      #
-      # In addition, use of the direct_debug_prefix_map preempts the usage of
-      # the normalized debug prefix map.
-      self.normalized_prefix_map = False
-    else:
-      self.normalized_prefix_map = True
+    self.direct_debug_prefix_map = False
+    self.normalized_prefix_map = False
 
     self.update_symbol_cache = UpdateSymbolCache()
 
@@ -472,13 +467,7 @@
       sys.stderr.write('Xcode action is %s, ignoring.' % self.xcode_action)
       return 0
 
-    build_settings = bazel_build_settings.BUILD_SETTINGS
-    if build_settings is None:
-      _PrintXcodeError('Unable to resolve build settings. '
-                       'Please report a Tulsi bug.')
-      return 1
-
-    parser = _OptionsParser(build_settings,
+    parser = _OptionsParser(self.build_settings,
                             self.sdk_version,
                             self.platform_name,
                             self.arch)
@@ -492,23 +481,12 @@
     self.verbose = parser.verbose
     self.bazel_bin_path = os.path.abspath(parser.bazel_bin_path)
     self.bazel_executable = parser.bazel_executable
-    self.bazel_exec_root = build_settings.bazelExecRoot
+    self.bazel_exec_root = self.build_settings.bazelExecRoot
 
-    # Until wrapped_clang is updated, use -fdebug-prefix-map to have debug
-    # symbols match Xcode-visible sources.
-    #
-    # NOTE: Use of -fdebug-prefix-map leads to producing binaries that cannot be
-    # reused across multiple machines by a distributed build system, unless the
-    # absolute paths to files visible to Xcode match perfectly between all of
-    # those machines.
-    #
-    # For this reason, -fdebug-prefix-map is provided as a default for non-
-    # distributed purposes.
-    if self.direct_debug_prefix_map:
-      source_map = self._ExtractTargetSourceMap(False)
-      if source_map:
-        prefix_map = '--copt=-fdebug-prefix-map=%s=%s' % source_map
-        parser.common_build_options.append(prefix_map)
+    # Update feature flags.
+    features = parser.GetEnabledFeatures()
+    self.direct_debug_prefix_map = 'DirectDebugPrefixMap' in features
+    self.normalized_prefix_map = 'DebugPathNormalization' in features
 
     self.build_path = os.path.join(self.bazel_bin_path,
                                    os.environ.get('TULSI_BUILD_PATH', ''))
@@ -653,19 +631,6 @@
     else:
       bazel_command.append('--output_groups=tulsi-outputs,default')
 
-    # A normalized path for -fdebug-prefix-map exists for keeping all debug
-    # information as built by Clang consistent for the sake of caching within
-    # a distributed build system.
-    #
-    # This is handled through a wrapped_clang feature flag via the CROSSTOOL.
-    #
-    # The use of this flag does not affect any sources built by swiftc. At
-    # present time, all Swift compiled sources will be built with uncacheable,
-    # absolute paths, as the Swift compiler does not present an easy means of
-    # similarly normalizing all debug information.
-    if self.normalized_prefix_map:
-      bazel_command.append('--features=debug_prefix_map_pwd_is_dot')
-
     bazel_command.extend(options.targets)
 
     extra_options = bazel_options.BazelOptions(os.environ)
@@ -677,7 +642,7 @@
     """Runs subprocess command, patching output as it's received."""
     self._PrintVerbose('Running "%s", patching output for workspace root at '
                        '"%s" with project path at "%s".' %
-                       (' '.join(command),
+                       (' '.join([pipes.quote(x) for x in command]),
                         self.workspace_root,
                         self.project_dir))
     # Xcode translates anything that looks like ""<path>:<line>:" that is not
@@ -1663,11 +1628,20 @@
       sys.stdout.flush()
 
 
+def main(argv):
+  build_settings = bazel_build_settings.BUILD_SETTINGS
+  if build_settings is None:
+    _PrintXcodeError('Unable to resolve build settings. '
+                     'Please report a Tulsi bug.')
+    return 1
+  return BazelBuildBridge(build_settings).Run(argv)
+
+
 if __name__ == '__main__':
   _LockFileAcquire('/tmp/tulsi_bazel_build.lock')
   _logger = tulsi_logging.Logger()
   _timer = Timer('Everything', 'complete_build').Start()
   signal.signal(signal.SIGINT, _InterruptHandler)
-  _exit_code = BazelBuildBridge().Run(sys.argv)
+  _exit_code = main(sys.argv)
   _timer.End()
   sys.exit(_exit_code)
diff --git a/src/TulsiGenerator/Scripts/bazel_build_settings.py.template b/src/TulsiGenerator/Scripts/bazel_build_settings.py.template
index 1465f51..7536d0b 100644
--- a/src/TulsiGenerator/Scripts/bazel_build_settings.py.template
+++ b/src/TulsiGenerator/Scripts/bazel_build_settings.py.template
@@ -63,6 +63,7 @@
   def __init__(self, bazel, bazelExecRoot, swiftTargets,
                cacheAffecting, cacheSafe,
                swiftOnly, nonSwiftOnly,
+               swiftFeatures, nonSwiftFeatures,
                projDefault, projTargetMap):
     self.bazel = bazel
     self.bazelExecRoot = bazelExecRoot
@@ -71,10 +72,22 @@
     self.cacheSafe = cacheSafe
     self.swiftOnly = swiftOnly
     self.nonSwiftOnly = nonSwiftOnly
+    self.swiftFeatures = swiftFeatures
+    self.nonSwiftFeatures = nonSwiftFeatures
     self.projDefault = projDefault
     self.projTargetMap = projTargetMap
 
-  def flags_for_target(self, target, is_debug):
+  def features_for_target(self, target, is_swift_override=None):
+    """Returns an array of enabled features for the given target."""
+
+    target = _StandardizeTargetLabel(target)
+    is_swift = target in self.swiftTargets
+    if is_swift_override is not None:
+      is_swift = is_swift_override
+
+    return self.swiftFeatures if is_swift else self.nonSwiftFeatures
+
+  def flags_for_target(self, target, is_debug, is_swift_override=None):
     """Returns (bazel, startup flags, build flags) for the given target."""
 
     target = _StandardizeTargetLabel(target)
@@ -82,7 +95,10 @@
     if not target_flag_set:
       target_flag_set = self.projDefault
 
-    lang = self.swiftOnly if target in self.swiftTargets else self.nonSwiftOnly
+    is_swift = target in self.swiftTargets
+    if is_swift_override is not None:
+      is_swift = is_swift_override
+    lang = self.swiftOnly if is_swift else self.nonSwiftOnly
 
     cache_affecting = self.cacheAffecting.flags(is_debug)
     cache_safe = self.cacheSafe.flags(is_debug)
diff --git a/src/TulsiGenerator/Scripts/user_build.py b/src/TulsiGenerator/Scripts/user_build.py
index cb0a817..244f7f4 100755
--- a/src/TulsiGenerator/Scripts/user_build.py
+++ b/src/TulsiGenerator/Scripts/user_build.py
@@ -17,6 +17,7 @@
 
 
 import argparse
+import pipes
 import subprocess
 import sys
 from bazel_build_settings import BUILD_SETTINGS
@@ -34,10 +35,11 @@
 
 
 def _CreateCommand(targets, build_settings, test, release,
-                   config, xcode_version):
+                   config, xcode_version, force_swift):
   """Creates a Bazel command for targets with the specified settings."""
   target = _BuildSettingsTargetForTargets(targets)
-  bazel, startup, flags = build_settings.flags_for_target(target, not release)
+  bazel, startup, flags = build_settings.flags_for_target(
+      target, not release, is_swift_override=force_swift)
   bazel_action = 'test' if test else 'build'
 
   command = [bazel]
@@ -53,6 +55,11 @@
   return command
 
 
+def _QuoteCommandForShell(cmd):
+  cmd = [pipes.quote(x) for x in cmd]
+  return ' '.join(cmd)
+
+
 def _InterruptSafeCall(cmd):
   p = subprocess.Popen(cmd)
   try:
@@ -75,13 +82,20 @@
   parser.add_argument('--norun', dest='run', action='store_false', default=True)
   parser.add_argument('--config', help='Bazel --config flag.')
   parser.add_argument('--xcode_version', help='Bazel --xcode_version flag.')
+  parser.add_argument('--force_swift', dest='swift', action='store_true',
+                      default=None, help='Forcibly treat the given targets '
+                                         'as containing Swift.')
+  parser.add_argument('--force_noswift', dest='swift', action='store_false',
+                      default=None, help='Forcibly treat the given targets '
+                                         'as not containing Swift.')
   parser.add_argument('targets', nargs='+')
 
   args = parser.parse_args()
   command = _CreateCommand(args.targets, BUILD_SETTINGS, args.test,
-                           args.release, args.config, args.xcode_version)
+                           args.release, args.config, args.xcode_version,
+                           args.swift)
   if args.print_cmd:
-    print ' '.join(command)
+    print _QuoteCommandForShell(command)
 
   if args.run:
     return _InterruptSafeCall(command)
diff --git a/src/TulsiGenerator/TulsiProjectInfoExtractor.swift b/src/TulsiGenerator/TulsiProjectInfoExtractor.swift
index 198f261..8373e25 100644
--- a/src/TulsiGenerator/TulsiProjectInfoExtractor.swift
+++ b/src/TulsiGenerator/TulsiProjectInfoExtractor.swift
@@ -29,6 +29,14 @@
     set { workspaceInfoExtractor.bazelURL = newValue }
   }
 
+  public var bazelExecutionRoot: String {
+    return workspaceInfoExtractor.bazelExecutionRoot
+  }
+
+  public var workspaceRootURL: URL {
+    return workspaceInfoExtractor.workspaceRootURL
+  }
+
   public init(bazelURL: URL,
               project: TulsiProject) {
     self.project = project
@@ -48,25 +56,29 @@
                                   startupOptions: TulsiOption,
                                   buildOptions: TulsiOption,
                                   projectGenBuildOptions: TulsiOption,
-                                  prioritizeSwiftOption: TulsiOption) throws -> RuleEntryMap {
+                                  prioritizeSwiftOption: TulsiOption,
+                                  features: Set<BazelSettingFeature>) throws -> RuleEntryMap {
     return try ruleEntriesForLabels(infos.map({ $0.label }),
                                     startupOptions: startupOptions,
                                     buildOptions: buildOptions,
                                     projectGenBuildOptions: projectGenBuildOptions,
-                                    prioritizeSwiftOption: prioritizeSwiftOption)
+                                    prioritizeSwiftOption: prioritizeSwiftOption,
+                                    features: features)
   }
 
   public func ruleEntriesForLabels(_ labels: [BuildLabel],
                                    startupOptions: TulsiOption,
                                    buildOptions: TulsiOption,
                                    projectGenBuildOptions: TulsiOption,
-                                   prioritizeSwiftOption: TulsiOption) throws -> RuleEntryMap {
+                                   prioritizeSwiftOption: TulsiOption,
+                                   features: Set<BazelSettingFeature>) throws -> RuleEntryMap {
     do {
       return try workspaceInfoExtractor.ruleEntriesForLabels(labels,
                                                              startupOptions: startupOptions,
                                                              buildOptions: buildOptions,
                                                              projectGenBuildOptions: projectGenBuildOptions,
-                                                             prioritizeSwiftOption: prioritizeSwiftOption)
+                                                             prioritizeSwiftOption: prioritizeSwiftOption,
+                                                             features: features)
     } catch BazelWorkspaceInfoExtractorError.aspectExtractorFailed(let info) {
       throw ExtractorError.ruleEntriesFailed(info)
     }
diff --git a/src/TulsiGenerator/XcodeProjectGenerator.swift b/src/TulsiGenerator/XcodeProjectGenerator.swift
index a13fa8a..4fc6b88 100644
--- a/src/TulsiGenerator/XcodeProjectGenerator.swift
+++ b/src/TulsiGenerator/XcodeProjectGenerator.swift
@@ -495,11 +495,6 @@
         }
       }
 
-      // Update this project's build settings with the latest feature flags.
-      for featureFlag in bazelBuildSettingsFeatures {
-        buildSettings[featureFlag] = "YES"
-      }
-
       if let genRunfiles = config.options[.GenerateRunfiles].commonValueAsBool, genRunfiles {
         buildSettings["GENERATE_RUNFILES"] = "YES"
       }
@@ -578,11 +573,15 @@
 
   private func loadRuleEntryMap() throws -> RuleEntryMap {
     do {
+      let features = BazelBuildSettingsFeatures.enabledFeatures(options: config.options,
+                                                                workspaceRoot: workspaceRootURL.path,
+                                                                bazelExecRoot: workspaceInfoExtractor.bazelExecutionRoot)
       return try workspaceInfoExtractor.ruleEntriesForLabels(config.buildTargetLabels,
                                                              startupOptions: config.options[.BazelBuildStartupOptionsDebug],
                                                              buildOptions: config.options[.BazelBuildOptionsDebug],
                                                              projectGenBuildOptions: config.options[.BazelBuildOptionsProjectGenerationOnly],
-                                                             prioritizeSwiftOption: config.options[.ProjectPrioritizesSwift])
+                                                             prioritizeSwiftOption: config.options[.ProjectPrioritizesSwift],
+                                                             features: features)
     } catch BazelWorkspaceInfoExtractorError.aspectExtractorFailed(let info) {
       throw ProjectGeneratorError.labelAspectFailure(info)
     }
@@ -952,9 +951,13 @@
 
     let bazelSettingsProvider = workspaceInfoExtractor.bazelSettingsProvider
     let bazelExecRoot = workspaceInfoExtractor.bazelExecutionRoot
+    let features = BazelBuildSettingsFeatures.enabledFeatures(options: config.options,
+                                                              workspaceRoot: workspaceRootURL.path,
+                                                              bazelExecRoot: bazelExecRoot)
     let bazelBuildSettings = bazelSettingsProvider.buildSettings(bazel: config.bazelURL.path,
                                                                  bazelExecRoot: bazelExecRoot,
                                                                  options: config.options,
+                                                                 features: features,
                                                                  buildRuleEntries: buildRuleEntries)
 
     let bundle = Bundle(for: type(of: self))
diff --git a/src/TulsiGeneratorTests/BuildSettingsTests.swift b/src/TulsiGeneratorTests/BuildSettingsTests.swift
new file mode 100644
index 0000000..1d342e3
--- /dev/null
+++ b/src/TulsiGeneratorTests/BuildSettingsTests.swift
@@ -0,0 +1,160 @@
+// Copyright 2018 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 XCTest
+@testable import TulsiGenerator
+
+class BuildSettingsTests: XCTestCase {
+
+  func testBazelFlagsEmpty() {
+    XCTAssert(BazelFlags().isEmpty)
+    XCTAssertEqual(BazelFlags().toPython(""), "BazelFlags()")
+  }
+  func testBazelFlagsOperators() {
+    let a = BazelFlags(startupStr: "a b", buildStr: "x y")
+    let b = BazelFlags(startupStr: "c d", buildStr: "z 1")
+    let aExplicit = BazelFlags(startup: ["a", "b"], build: ["x", "y"])
+    let bExplicit = BazelFlags(startup: ["c", "d"], build: ["z", "1"])
+
+    XCTAssertEqual(a, aExplicit)
+    XCTAssertEqual(b, bExplicit)
+
+    let c = a + b
+    XCTAssertEqual(c.startup, ["a", "b", "c", "d"])
+    XCTAssertEqual(c.build, ["x", "y", "z", "1"])
+    XCTAssertNotEqual(c, b + a)
+  }
+  func testBazelFlagsPythonable() {
+    let startup = ["--startup-flag"]
+    let build = ["--build-flag"]
+    XCTAssertEqual(BazelFlags(startup: startup, build: build).toPython(""), """
+BazelFlags(
+    startup = [
+        '--startup-flag',
+    ],
+    build = [
+        '--build-flag',
+    ],
+)
+""")
+  }
+
+  func testBazelFlagsSetEmpty() {
+    XCTAssert(BazelFlagsSet().isEmpty)
+    XCTAssertEqual(BazelFlagsSet().toPython(""), "BazelFlagsSet()")
+  }
+  func testBazelFlagsSetInitializers() {
+    let basicFlagsSet = BazelFlagsSet(startupFlags: ["a"], buildFlags: ["b"])
+    let basicFlags = BazelFlags(startup: ["a"], build: ["b"])
+    XCTAssertEqual(basicFlagsSet.release, basicFlags)
+    XCTAssertEqual(basicFlagsSet.debug, basicFlags)
+    XCTAssertEqual(basicFlagsSet.getFlags(forDebug: true), basicFlagsSet.debug)
+    XCTAssertEqual(basicFlagsSet.getFlags(forDebug: false), basicFlagsSet.release)
+
+    let complexFlagSet = BazelFlagsSet(debug: BazelFlags(startup: ["a"], build: ["b"]),
+                                       release: BazelFlags(startup: ["x"], build: ["y"]),
+                                       common: BazelFlags(startup: ["1"], build: ["2"]))
+    XCTAssertEqual(complexFlagSet.debug, BazelFlags(startup: ["a", "1"], build: ["b", "2"]))
+    XCTAssertEqual(complexFlagSet.release, BazelFlags(startup: ["x", "1"], build: ["y", "2"]))
+  }
+  func testBazelFlagsSetPythonable() {
+    let basicFlagsSet = BazelFlagsSet(startupFlags: ["a"], buildFlags: ["b"])
+    XCTAssertEqual(basicFlagsSet.toPython(""), """
+BazelFlagsSet(
+    flags = BazelFlags(
+        startup = [
+            'a',
+        ],
+        build = [
+            'b',
+        ],
+    ),
+)
+""")
+    let complexFlagSet = BazelFlagsSet(debug: BazelFlags(startup: ["a"], build: ["b"]),
+                                       release: BazelFlags(startup: ["x"], build: ["y"]),
+                                       common: BazelFlags(startup: ["1"], build: ["2"]))
+    XCTAssertEqual(complexFlagSet.toPython(""), """
+BazelFlagsSet(
+    debug = BazelFlags(
+        startup = [
+            'a',
+            '1',
+        ],
+        build = [
+            'b',
+            '2',
+        ],
+    ),
+    release = BazelFlags(
+        startup = [
+            'x',
+            '1',
+        ],
+        build = [
+            'y',
+            '2',
+        ],
+    ),
+)
+""")
+  }
+
+    func testBazelBuildSettingsPythonable() {
+      let bazel = "/path/to/bazel"
+      let bazelExecRoot = "__MOCK_EXEC_ROOT__"
+      let swiftTargets: Set<String> = [
+          "//dir/swiftTarget:swiftTarget",
+          "//dir/nested/depOnswift:depOnswift"
+      ]
+      let cacheAffecting = BazelFlagsSet(startupFlags: ["--nocacheStartup"],
+                                         buildFlags: ["--nocacheBuild"])
+      let cacheSafe = BazelFlagsSet(startupFlags: ["--cacheSafeStartup"],
+                                    buildFlags: ["--cacheSafeBuild"])
+      let swift = BazelFlagsSet(buildFlags: ["--swift-only"])
+      let nonSwift = BazelFlagsSet(startupFlags: ["--non-swift-only"])
+      let projDefaults = BazelFlagsSet()
+      let projTargetFlags = [
+          "//dir/some/customized:target": BazelFlagsSet(buildFlags: ["a", "b"]),
+      ]
+      let swiftFeatures = [BazelSettingFeature.DebugPathNormalization.stringValue]
+      let nonSwiftFeatures = [BazelSettingFeature.DirectDebugPrefixMap("", "").stringValue]
+      let settings = BazelBuildSettings(bazel: bazel,
+                                        bazelExecRoot: bazelExecRoot,
+                                        swiftTargets: swiftTargets,
+                                        tulsiCacheAffectingFlagsSet: cacheAffecting,
+                                        tulsiCacheSafeFlagSet: cacheSafe,
+                                        tulsiSwiftFlagSet: swift,
+                                        tulsiNonSwiftFlagSet: nonSwift,
+                                        swiftFeatures: swiftFeatures,
+                                        nonSwiftFeatures: nonSwiftFeatures,
+                                        projDefaultFlagSet: projDefaults,
+                                        projTargetFlagSets: projTargetFlags)
+      XCTAssertEqual(settings.toPython(""), """
+BazelBuildSettings(
+    '\(bazel)',
+    '\(bazelExecRoot)',
+    \(swiftTargets.toPython("    ")),
+    \(cacheAffecting.toPython("    ")),
+    \(cacheSafe.toPython("    ")),
+    \(swift.toPython("    ")),
+    \(nonSwift.toPython("    ")),
+    \(swiftFeatures.toPython("    ")),
+    \(nonSwiftFeatures.toPython("    ")),
+    \(projDefaults.toPython("    ")),
+    \(projTargetFlags.toPython("    ")),
+)
+""")
+  }
+}
diff --git a/src/TulsiGeneratorTests/MockWorkspaceInfoExtractor.swift b/src/TulsiGeneratorTests/MockWorkspaceInfoExtractor.swift
index 6e9e01b..fe5d25b 100644
--- a/src/TulsiGeneratorTests/MockWorkspaceInfoExtractor.swift
+++ b/src/TulsiGeneratorTests/MockWorkspaceInfoExtractor.swift
@@ -16,15 +16,15 @@
 @testable import TulsiGenerator
 
 class MockBazelSettingsProvider: BazelSettingsProviderProtocol {
-  func tulsiFlags(hasSwift: Bool) -> BazelFlagsSet {
+  func tulsiFlags(hasSwift: Bool, features: Set<BazelSettingFeature>) -> BazelFlagsSet {
     return BazelFlagsSet()
   }
 
-  func optionsBasedFlags(_ options: TulsiOptionSet) -> BazelFlagsSet {
-    return BazelFlagsSet()
-  }
-
-  func buildSettings(bazel: String, bazelExecRoot: String, options: TulsiOptionSet, buildRuleEntries: Set<RuleEntry>) -> BazelBuildSettings {
+  func buildSettings(bazel: String,
+                     bazelExecRoot: String,
+                     options: TulsiOptionSet,
+                     features: Set<BazelSettingFeature>,
+                     buildRuleEntries: Set<RuleEntry>) -> BazelBuildSettings {
     return BazelBuildSettings(bazel: bazel,
                               bazelExecRoot: bazelExecRoot,
                               swiftTargets: [],
@@ -32,6 +32,8 @@
                               tulsiCacheSafeFlagSet: BazelFlagsSet(),
                               tulsiSwiftFlagSet: BazelFlagsSet(),
                               tulsiNonSwiftFlagSet: BazelFlagsSet(),
+                              swiftFeatures: [],
+                              nonSwiftFeatures: [],
                               projDefaultFlagSet: BazelFlagsSet(),
                               projTargetFlagSets: [:])
   }
@@ -49,6 +51,7 @@
   var bazelURL = URL(fileURLWithPath: "")
   var bazelBinPath = "bazel-bin"
   var bazelExecutionRoot = "/private/var/tmp/_bazel_localhost/1234567890abcdef1234567890abcdef/execroot/workspace_dir"
+  var workspaceRootURL = URL(fileURLWithPath: "")
 
   func extractRuleInfoFromProject(_ project: TulsiProject) -> [RuleInfo] {
     return []
@@ -58,7 +61,8 @@
                             startupOptions: TulsiOption,
                             buildOptions: TulsiOption,
                             projectGenBuildOptions: TulsiOption,
-                            prioritizeSwiftOption: TulsiOption) throws -> RuleEntryMap {
+                            prioritizeSwiftOption: TulsiOption,
+                            features: Set<BazelSettingFeature>) throws -> RuleEntryMap {
     invalidLabels.removeAll(keepingCapacity: true)
     let ret = RuleEntryMap()
     for label in labels {
diff --git a/src/TulsiGeneratorTests/PBXTargetGeneratorTests.swift b/src/TulsiGeneratorTests/PBXTargetGeneratorTests.swift
index 2b2ae87..25b3242 100644
--- a/src/TulsiGeneratorTests/PBXTargetGeneratorTests.swift
+++ b/src/TulsiGeneratorTests/PBXTargetGeneratorTests.swift
@@ -335,7 +335,6 @@
           "PRODUCT_NAME": rule1TargetName,
           "SDKROOT": "iphoneos",
           "TULSI_BUILD_PATH": rule1BuildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
           name: rule1TargetName,
@@ -372,7 +371,6 @@
           "PRODUCT_NAME": rule2TargetName,
           "SDKROOT": "iphoneos",
           "TULSI_BUILD_PATH": rule2BuildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
           name: rule2TargetName,
@@ -450,7 +448,6 @@
           "PRODUCT_NAME": rule1TargetName,
           "SDKROOT": "iphoneos",
           "TULSI_BUILD_PATH": rule1BuildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
           name: rule1TargetName,
@@ -491,7 +488,6 @@
           "SDKROOT": "iphoneos",
           "TEST_HOST": "$(BUILT_PRODUCTS_DIR)/\(rule1TargetName).app/\(rule1TargetName)",
           "TULSI_BUILD_PATH": rule2BuildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
           "TULSI_TEST_RUNNER_ONLY": "YES",
       ]
       let expectedTarget = TargetDefinition(
@@ -564,7 +560,6 @@
         "PRODUCT_NAME": rule1TargetName,
         "SDKROOT": "iphoneos",
         "TULSI_BUILD_PATH": rule1BuildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
         ]
       let expectedTarget = TargetDefinition(
         name: rule1TargetName,
@@ -605,7 +600,6 @@
         "TEST_TARGET_NAME": rule1TargetName,
         "TULSI_BUILD_PATH": rule2BuildPath,
         "TULSI_TEST_RUNNER_ONLY": "YES",
-        "TULSI_SWIFT_DEPENDENCY": "NO",
         ]
       let expectedTarget = TargetDefinition(
         name: rule2TargetName,
@@ -682,7 +676,6 @@
         "PRODUCT_NAME": rule1TargetName,
         "SDKROOT": "macosx",
         "TULSI_BUILD_PATH": rule1BuildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
         ]
       let expectedTarget = TargetDefinition(
         name: rule1TargetName,
@@ -723,7 +716,6 @@
         "SDKROOT": "macosx",
         "TEST_HOST": "$(BUILT_PRODUCTS_DIR)/\(rule1TargetName).app/Contents/MacOS/\(rule1TargetName)",
         "TULSI_BUILD_PATH": rule2BuildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
         "TULSI_TEST_RUNNER_ONLY": "YES",
         ]
       let expectedTarget = TargetDefinition(
@@ -801,7 +793,6 @@
         "PRODUCT_NAME": rule1TargetName,
         "SDKROOT": "macosx",
         "TULSI_BUILD_PATH": rule1BuildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
         ]
       let expectedTarget = TargetDefinition(
         name: rule1TargetName,
@@ -841,7 +832,6 @@
         "SDKROOT": "macosx",
         "TEST_TARGET_NAME": rule1TargetName,
         "TULSI_BUILD_PATH": rule2BuildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
         "TULSI_TEST_RUNNER_ONLY": "YES",
         ]
       let expectedTarget = TargetDefinition(
@@ -909,7 +899,6 @@
         "PRODUCT_NAME": rule1TargetName,
         "SDKROOT": "macosx",
         "TULSI_BUILD_PATH": rule1BuildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
         "TULSI_TEST_RUNNER_ONLY": "YES",
         ]
       let expectedTarget = TargetDefinition(
@@ -979,7 +968,6 @@
         "PRODUCT_NAME": rule1TargetName,
         "SDKROOT": "macosx",
         "TULSI_BUILD_PATH": rule1BuildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
         "TULSI_TEST_RUNNER_ONLY": "YES",
         ]
       let expectedTarget = TargetDefinition(
@@ -1057,7 +1045,6 @@
           "PRODUCT_NAME": rule1TargetName,
           "SDKROOT": "iphoneos",
           "TULSI_BUILD_PATH": rule1BuildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
           name: rule1TargetName,
@@ -1099,7 +1086,6 @@
           "SDKROOT": "iphoneos",
           "TEST_HOST": "$(BUILT_PRODUCTS_DIR)/\(rule1TargetName).app/\(rule1TargetName)",
           "TULSI_BUILD_PATH": testRuleBuildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
           "TULSI_TEST_RUNNER_ONLY": "YES",
       ]
       let expectedTarget = TargetDefinition(
@@ -1187,7 +1173,6 @@
       "SDKROOT": "iphoneos",
       "TEST_HOST": "$(BUILT_PRODUCTS_DIR)/\(testHostTargetName).app/\(testHostTargetName)",
       "TULSI_BUILD_PATH": testRulePackage,
-      "TULSI_SWIFT_DEPENDENCY": "NO",
       "TULSI_TEST_RUNNER_ONLY": "YES",
       ]
     let expectedTarget = TargetDefinition(
@@ -1275,7 +1260,6 @@
       "SDKROOT": "iphoneos",
       "TEST_HOST": "$(BUILT_PRODUCTS_DIR)/\(testHostTargetName).app/\(testHostTargetName)",
       "TULSI_BUILD_PATH": testRulePackage,
-      "TULSI_SWIFT_DEPENDENCY": "YES",
       "TULSI_TEST_RUNNER_ONLY": "YES",
       ]
     let expectedTarget = TargetDefinition(
@@ -1357,7 +1341,6 @@
         "PRODUCT_NAME": rule1TargetName,
         "SDKROOT": "iphoneos",
         "TULSI_BUILD_PATH": rule1BuildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
         ]
       let expectedTarget = TargetDefinition(
         name: rule1TargetName,
@@ -1398,7 +1381,6 @@
         "SDKROOT": "iphoneos",
         "TEST_TARGET_NAME": rule1TargetName,
         "TULSI_BUILD_PATH": testRuleBuildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
         "TULSI_TEST_RUNNER_ONLY": "YES",
         ]
       let expectedTarget = TargetDefinition(
@@ -1523,7 +1505,6 @@
           "PRODUCT_NAME": testRuleTargetName,
           "SDKROOT": "iphoneos",
           "TULSI_BUILD_PATH": testRuleBuildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       var testRunnerExpectedBuildSettings = expectedBuildSettings
       testRunnerExpectedBuildSettings["DEBUG_INFORMATION_FORMAT"] = "dwarf"
@@ -1591,7 +1572,6 @@
           "PRODUCT_NAME": "test-test1-SameName",
           "SDKROOT": "iphoneos",
           "TULSI_BUILD_PATH": rule1BuildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
           name: "test-test1-SameName",
@@ -1628,7 +1608,6 @@
           "PRODUCT_NAME": "test-test2-SameName",
           "SDKROOT": "iphoneos",
           "TULSI_BUILD_PATH": rule2BuildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
           name: "test-test2-SameName",
@@ -1692,7 +1671,6 @@
           "PRODUCT_NAME": targetName,
           "SDKROOT": "iphoneos",
           "TULSI_BUILD_PATH": buildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
           name: targetName,
@@ -1755,7 +1733,6 @@
         "PRODUCT_NAME": bundleName,
         "SDKROOT": "iphoneos",
         "TULSI_BUILD_PATH": buildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
         name: targetName,
@@ -1847,7 +1824,6 @@
           "SDKROOT": "iphoneos",
           "IPHONEOS_DEPLOYMENT_TARGET": "9.0",
           "TULSI_BUILD_PATH": appBuildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
           name: appTargetName,
@@ -1886,7 +1862,6 @@
           "SDKROOT": "watchos",
           "WATCHOS_DEPLOYMENT_TARGET": "2.0",
           "TULSI_BUILD_PATH": watchAppBuildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
           name: watchAppTargetName,
@@ -1925,7 +1900,6 @@
           "SDKROOT": "watchos",
           "WATCHOS_DEPLOYMENT_TARGET": "2.0",
           "TULSI_BUILD_PATH": watchExtBuildPath,
-          "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
           name: watchExtTargetName,
@@ -2014,7 +1988,6 @@
         "SDKROOT": "macosx",
         "MACOSX_DEPLOYMENT_TARGET": "10.12",
         "TULSI_BUILD_PATH": appBuildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
         name: appTargetName,
@@ -2053,7 +2026,6 @@
         "SDKROOT": "macosx",
         "MACOSX_DEPLOYMENT_TARGET": "10.12",
         "TULSI_BUILD_PATH": macAppExtBuildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
         name: macAppExtTargetName,
@@ -2092,7 +2064,6 @@
         "SDKROOT": "macosx",
         "MACOSX_DEPLOYMENT_TARGET": "10.12",
         "TULSI_BUILD_PATH": macCLIAppBuildPath,
-        "TULSI_SWIFT_DEPENDENCY": "NO",
       ]
       let expectedTarget = TargetDefinition(
         name: macCLIAppTargetName,
@@ -2578,7 +2549,6 @@
         "PRODUCT_NAME": targetName,
         "SDKROOT": "iphoneos",
         "TULSI_BUILD_PATH": package,
-        "TULSI_SWIFT_DEPENDENCY": "YES",
     ]
     let expectedTarget = TargetDefinition(
         name: "TestTarget",