Setup for improvements to Bazel caching

- Add new TulsiOptionValueType: stringEnum for an enum-style option
- No longer set the sdk_version flags as they aren't needed
- No longer use the tulsigen- symlink; instead tell Bazel to not
  create/modify any symlinks.
- Only set the --xcode_version flag in Xcode if the version does
  not match the xcode_version used when the generation was run.

PiperOrigin-RevId: 204795989
diff --git a/src/Tulsi/OptionsEditorController.swift b/src/Tulsi/OptionsEditorController.swift
index 937c3cb..6010c96 100644
--- a/src/Tulsi/OptionsEditorController.swift
+++ b/src/Tulsi/OptionsEditorController.swift
@@ -249,7 +249,12 @@
         case .bool:
           newNode = OptionsEditorBooleanNode(key: key, option: option, model: model, target: target)
         case .string:
-          newNode = OptionsEditorStringNode(key: key, option: option, model: model, target: target)
+          fallthrough
+        case .stringEnum:
+          newNode = OptionsEditorConstrainedStringNode(key: key,
+                                                       option: option,
+                                                       model: model,
+                                                       target: target)
       }
 
       if let (group, displayName, description) = optionSet?.groupInfoForOptionKey(key) {
@@ -363,6 +368,8 @@
                                editable: editable)
         }
 
+      case .stringEnum:
+        fallthrough
       case .bool:
         let identifier: String
         if explicit {
diff --git a/src/Tulsi/OptionsEditorNode.swift b/src/Tulsi/OptionsEditorNode.swift
index d16f14e..59e7356 100644
--- a/src/Tulsi/OptionsEditorNode.swift
+++ b/src/Tulsi/OptionsEditorNode.swift
@@ -345,6 +345,20 @@
   }
 }
 
+/// An editor node that provides multiple string options.
+class OptionsEditorConstrainedStringNode: OptionsEditorStringNode {
+
+  override var supportsMultilineEditor: Bool {
+    return false
+  }
+
+  override var multiSelectItems: [String] {
+    if case .stringEnum(let values) = valueType {
+      return values
+    }
+    return []
+  }
+}
 
 /// An editor node that provides multiple boolean options and maps between display strings and
 /// serialization strings.
diff --git a/src/TulsiGenerator/Bazel/tulsi/tulsi_aspects.bzl b/src/TulsiGenerator/Bazel/tulsi/tulsi_aspects.bzl
index 7802138..e726cfb 100644
--- a/src/TulsiGenerator/Bazel/tulsi/tulsi_aspects.bzl
+++ b/src/TulsiGenerator/Bazel/tulsi/tulsi_aspects.bzl
@@ -478,6 +478,10 @@
         return (platform_type, minimum_os_version)
     return (platform_type, _minimum_os_for_platform(ctx, platform_type))
 
+def _get_xcode_version(ctx):
+    """Returns the current Xcode version as a string."""
+    return str(ctx.attr._tulsi_xcode_config[apple_common.XcodeVersionConfig].xcode_version())
+
 def _get_platform_type(ctx):
     """Return the current apple_common.platform_type as a string."""
     current_platform = (_get_opt_attr(ctx, "rule.attr.platform_type") or
@@ -665,7 +669,11 @@
         if watch_app:
             extensions.append(watch_app)
 
-    # Collect bundle related information.
+    # Record the Xcode version used for all targets, although it will only be used by bazel_build.py
+    # for targets that are buildable in the xcodeproj.
+    xcode_version = _get_xcode_version(ctx)
+
+    # Collect bundle related information and Xcode version only for runnable targets.
     if AppleBundleInfo in target:
         apple_bundle_provider = target[AppleBundleInfo]
 
@@ -677,13 +685,17 @@
         infoplist = apple_bundle_provider.infoplist if IosExtensionBundleInfo in target else None
     else:
         bundle_name = None
-
-        # For macos_command_line_application, which does not have a AppleBundleInfo
-        # provider but does have a bundle_id attribute for use in the Info.plist.
-        bundle_id = _get_opt_attr(rule_attr, "bundle_id")
         product_type = None
         infoplist = None
 
+        # For macos_command_line_application, which does not have a
+        # AppleBundleInfo provider but does have a bundle_id attribute for use
+        # in the Info.plist.
+        if target_kind == "macos_command_line_application":
+            bundle_id = _get_opt_attr(rule_attr, "bundle_id")
+        else:
+            bundle_id = None
+
     # Collect Swift related attributes.
     swift_info = None
     if SwiftInfo in target:
@@ -752,6 +764,7 @@
         infoplist = infoplist.basename if infoplist else None,
         platform_type = platform_type,
         product_type = product_type,
+        xcode_version = xcode_version,
     )
 
     # Create an action to write out this target's info.
diff --git a/src/TulsiGenerator/BazelAspectInfoExtractor.swift b/src/TulsiGenerator/BazelAspectInfoExtractor.swift
index 6a8e015..bb459df 100644
--- a/src/TulsiGenerator/BazelAspectInfoExtractor.swift
+++ b/src/TulsiGenerator/BazelAspectInfoExtractor.swift
@@ -24,12 +24,6 @@
     case parsingFailed(String)
   }
 
-  /// Prefix to be used by Bazel for the output of the Tulsi aspect.
-  private static let SymlinkPrefix = "tulsigen-"
-  /// Suffixes used by Bazel when creating output symlinks.
-  private static let BazelOutputSymlinks = [
-      "bin", "genfiles", "out", "testlogs"].map({ SymlinkPrefix + $0 })
-
   /// The location of the bazel binary.
   var bazelURL: URL
   /// The location of the Bazel workspace to be examined.
@@ -72,6 +66,7 @@
     guard !targets.isEmpty else {
       return RuleEntryMap()
     }
+
     return try extractRuleEntriesUsingBEP(targets,
                                           startupOptions: startupOptions,
                                           buildOptions: buildOptions,
@@ -176,25 +171,37 @@
     var arguments = startupOptions
     arguments.append(contentsOf: [
         "build",
-        "-c",
-        "dbg",  // The aspect is run in debug mode to match the default Xcode build configuration.
-        "--symlink_prefix",  // Generate artifacts without overwriting the normal build symlinks.
-        BazelAspectInfoExtractor.SymlinkPrefix,
+        // The aspect is run in debug mode to match the default Xcode build configuration.
+        // This does indeed affect Bazel analysis caching.
+        "--compilation_mode=dbg",
+        "--symlink_prefix=/",  // Generate artifacts without overwriting the normal build symlinks.
+        // The following flags control Bazel console output and should not affect Bazel analysis
+        // caching.
         "--announce_rc",  // Print the RC files used by this operation.
-        "--nocheck_visibility",  // Don't do package visibility enforcement during aspect runs.
         "--show_result=0",  // Don't bother printing the build results.
         "--noshow_loading_progress",  // Don't show Bazel's loading progress.
         "--noshow_progress",  // Don't show Bazel's build progress.
-        "--override_repository=tulsi=\(aspectWorkspacePath)",
-        "--aspects",
-        "@tulsi//tulsi:tulsi_aspects.bzl%\(aspect)",
-        "--output_groups=tulsi-info,-_,-default",  // Build only the aspect artifacts.
-        "--tool_tag=tulsi_v\(tulsiVersion):generator", // Add a tag for tracking.
+        // The following flags are used by Tulsi to identify itself and read build information from
+        // Bazel. They should not affect Bazel analysis caching.
+        "--tool_tag=tulsi_v\(tulsiVersion):generator",  // Add a tag for tracking.
         "--build_event_json_file=\(self.buildEventsFilePath)",
         "--noexperimental_build_event_json_file_path_conversion",
         // Don't replace test_suites with their tests. This allows the Aspect to discover the
         // structure of test_suites instead of just the tests they resolve to.
         "--noexpand_test_suites",
+        // The following flags WILL affect Bazel analysis caching.
+        // Keep this consistent with bazel_build.py.
+        "--nocheck_visibility",  // Don't do package visibility enforcement during aspect runs.
+        "--override_repository=tulsi=\(aspectWorkspacePath)",
+        "--aspects",
+        "@tulsi//tulsi:tulsi_aspects.bzl%\(aspect)",
+        "--output_groups=tulsi-info,-_,-default",  // Build only the aspect artifacts.
+    ])
+    // Extra flags added by bazel_build.py.
+    arguments.append(contentsOf: [
+        "--features=debug_prefix_map_pwd_is_dot",
+        "--define=apple.add_debugger_entitlement=1",
+        "--define=apple.propagate_embedded_extra_outputs=1",
     ])
     arguments.append(contentsOf: projectGenerationOptions)
     arguments.append(contentsOf: buildOptions)
@@ -213,37 +220,12 @@
                                completionInfo.commandlineString,
                                completionInfo.terminationStatus,
                                stderr)
-
-        self.removeGeneratedSymlinks()
         terminationHandler(completionInfo.process, debugInfo)
     }
 
     return process
   }
 
-  private func removeGeneratedSymlinks() {
-    let fileManager = FileManager.default
-    for outputSymlink in BazelAspectInfoExtractor.BazelOutputSymlinks {
-
-      let symlinkURL = workspaceRootURL.appendingPathComponent(outputSymlink, isDirectory: true)
-      do {
-        let attributes = try fileManager.attributesOfItem(atPath: symlinkURL.path)
-        guard let type = attributes[FileAttributeKey.type] as? String, type == FileAttributeType.typeSymbolicLink.rawValue else {
-          continue
-        }
-      } catch {
-        // Any exceptions are expected to indicate that the file does not exist.
-        continue
-      }
-
-      do {
-        try fileManager.removeItem(at: symlinkURL)
-      } catch let e as NSError {
-        localizedMessageLogger.infoMessage("Failed to remove symlink at \(symlinkURL). \(e)")
-      }
-    }
-  }
-
   /// Builds a list of RuleEntry instances using the data in the given set of .tulsiinfo files.
   private func extractRuleEntriesFromArtifacts(_ files: Set<String>,
                                                progressNotifier: ProgressNotifier? = nil) -> RuleEntryMap {
@@ -347,6 +329,7 @@
       let productType = dict["product_type"] as? String
 
       let platformType = dict["platform_type"] as? String
+      let xcodeVersion = dict["xcode_version"] as? String
 
       let targetProductType: PBXTarget.ProductType?
 
@@ -403,7 +386,8 @@
                                 swiftToolchain: swiftToolchain,
                                 swiftTransitiveModules: swiftTransitiveModules,
                                 objCModuleMaps: objCModuleMaps,
-                                extensionType: extensionType)
+                                extensionType: extensionType,
+                                xcodeVersion: xcodeVersion)
       progressNotifier?.incrementValue()
       return ruleEntry
     }
diff --git a/src/TulsiGenerator/PBXTargetGenerator.swift b/src/TulsiGenerator/PBXTargetGenerator.swift
index a1d9199..38ec877 100644
--- a/src/TulsiGenerator/PBXTargetGenerator.swift
+++ b/src/TulsiGenerator/PBXTargetGenerator.swift
@@ -1526,6 +1526,13 @@
     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.
+    if let xcodeVersion = entry.xcodeVersion {
+      buildSettings["TULSI_XCODE_VERSION"] = xcodeVersion
+    }
+
     // Disable Xcode's attempts at generating dSYM bundles as it conflicts with the operation of the
     // special test runner build configurations (which have associated sources but don't actually
     // compile anything).
diff --git a/src/TulsiGenerator/RuleEntry.swift b/src/TulsiGenerator/RuleEntry.swift
index 6a98a84..9bbaca4 100644
--- a/src/TulsiGenerator/RuleEntry.swift
+++ b/src/TulsiGenerator/RuleEntry.swift
@@ -261,6 +261,9 @@
   /// The NSExtensionPointIdentifier of the extension associated with this rule, if any.
   public let extensionType: String?
 
+  /// Xcode version used during the aspect run. Only set for bundled and runnable targets.
+  public let xcodeVersion: String?
+
   /// Returns the set of non-versioned artifacts that are not source files.
   public var normalNonSourceArtifacts: [BazelFileInfo] {
     var artifacts = [BazelFileInfo]()
@@ -336,7 +339,8 @@
        swiftToolchain: String? = nil,
        swiftTransitiveModules: [BazelFileInfo] = [],
        objCModuleMaps: [BazelFileInfo] = [],
-       extensionType: String? = nil) {
+       extensionType: String? = nil,
+       xcodeVersion: String? = nil) {
 
     var checkedAttributes = [Attribute: AnyObject]()
     for (key, value) in attributes {
@@ -386,6 +390,7 @@
     self.swiftLanguageVersion = swiftLanguageVersion
     self.swiftToolchain = swiftToolchain
     self.swiftTransitiveModules = swiftTransitiveModules
+    self.xcodeVersion = xcodeVersion
 
     // Swift targets may have a generated Objective-C module map for their Swift generated header.
     // Unfortunately, this breaks Xcode's indexing (it doesn't really make sense to ask SourceKit
@@ -432,7 +437,8 @@
                    swiftToolchain: String? = nil,
                    swiftTransitiveModules: [BazelFileInfo] = [],
                    objCModuleMaps: [BazelFileInfo] = [],
-                   extensionType: String? = nil) {
+                   extensionType: String? = nil,
+                   xcodeVersion: String? = nil) {
     self.init(label: BuildLabel(label),
               type: type,
               attributes: attributes,
@@ -458,7 +464,8 @@
               swiftToolchain: swiftToolchain,
               swiftTransitiveModules: swiftTransitiveModules,
               objCModuleMaps: objCModuleMaps,
-              extensionType: extensionType)
+              extensionType: extensionType,
+              xcodeVersion: xcodeVersion)
   }
 
   // MARK: Private methods
diff --git a/src/TulsiGenerator/Scripts/bazel_build.py b/src/TulsiGenerator/Scripts/bazel_build.py
index c544bd7..1b77015 100755
--- a/src/TulsiGenerator/Scripts/bazel_build.py
+++ b/src/TulsiGenerator/Scripts/bazel_build.py
@@ -206,6 +206,7 @@
             _OptionsParser.ALL_CONFIGS: [
                 '--verbose_failures',
                 '--announce_rc',
+                '--bes_outerr_buffer_size=0',  # Don't buffer Bazel output.
             ],
 
             'Debug': [
@@ -300,23 +301,8 @@
     options = self._GetOptions(self.build_options, config)
 
     version_string = self._GetXcodeVersionString()
-    if version_string:
+    if version_string and self._NeedsXcodeVersionFlag(version_string):
       self._AddDefaultOption(options, '--xcode_version', version_string)
-
-    if self.sdk_version:
-      if self.platform_name.startswith('watch'):
-        self._AddDefaultOption(options,
-                               '--watchos_sdk_version',
-                               self.sdk_version)
-      elif self.platform_name.startswith('iphone'):
-        self._AddDefaultOption(options, '--ios_sdk_version', self.sdk_version)
-      elif self.platform_name.startswith('macos'):
-        self._AddDefaultOption(options, '--macos_sdk_version', self.sdk_version)
-      elif self.platform_name.startswith('appletv'):
-        self._AddDefaultOption(options, '--tvos_sdk_version', self.sdk_version)
-      else:
-        self._WarnUnknownPlatform()
-        self._AddDefaultOption(options, '--ios_sdk_version', self.sdk_version)
     return options
 
   @staticmethod
@@ -475,6 +461,32 @@
       fix_version_string = '.%d' % fix_version
     return '%d.%d%s' % (major_version, minor_version, fix_version_string)
 
+  @staticmethod
+  def _NeedsXcodeVersionFlag(xcode_version):
+    """Returns True if the --xcode_version flag should be used for building.
+
+    The flag should be used if the active Xcode version was not the same one
+    used during project generation.
+
+    Note this a best-attempt only; this may not be accurate as Bazel itself
+    caches the active DEVELOPER_DIR path and the user may have changed their
+    installed Xcode version.
+
+    Args:
+      xcode_version: active Xcode version string.
+    """
+    tulsi_xcode_version = os.environ.get('TULSI_XCODE_VERSION')
+    if not tulsi_xcode_version:
+      return True
+
+    # xcode_version will be of the form Major.Minor(.Fix)? while
+    # TULSI_XCODE_VERSION will be of the form Major.Minor.Fix so we'll need to
+    # remove the trailing .0 if it exists.
+    if tulsi_xcode_version.endswith('.0'):
+      tulsi_xcode_version = tulsi_xcode_version[:-2]
+
+    return xcode_version != tulsi_xcode_version
+
 
 class BazelBuildBridge(object):
   """Handles invoking Bazel and unpacking generated binaries."""
@@ -775,18 +787,25 @@
         os.path.join(os.path.dirname(__file__), '..', 'Bazel'))
 
     bazel_command.extend([
+        # The following flags are used by Tulsi to identify itself and read
+        # build information from Bazel. They shold not affect Bazel anaylsis
+        # caching.
+        '--tool_tag=tulsi_v%s:bazel_build' % self.tulsi_version,
         '--build_event_json_file=%s' % self.build_events_file_path,
         '--noexperimental_build_event_json_file_path_conversion',
-        '--bes_outerr_buffer_size=0',  # Don't buffer Bazel output at all.
-        '--aspects', '@tulsi//tulsi:tulsi_aspects.bzl%tulsi_outputs_aspect',
-        '--override_repository=tulsi=%s' % tulsi_package_dir,
-        '--tool_tag=tulsi_v%s:bazel_build' % self.tulsi_version])
+        '--aspects', '@tulsi//tulsi:tulsi_aspects.bzl%tulsi_outputs_aspect'])
 
     if self.is_test and self.gen_runfiles:
       bazel_command.append('--output_groups=+tulsi-outputs')
     else:
       bazel_command.append('--output_groups=tulsi-outputs,default')
 
+    bazel_command.extend([
+        # The following flags WILL affect Bazel analysis caching.
+        # Keep this consistent with BazelAspectInfoExtractor.swift.
+        '--nocheck_visibility',  # Don't do package visibility enforcement.
+        '--override_repository=tulsi=%s' % tulsi_package_dir])
+
     if self.generate_dsym:
       bazel_command.append('--apple_generate_dsym')
 
diff --git a/src/TulsiGenerator/TulsiOption.swift b/src/TulsiGenerator/TulsiOption.swift
index 0005c1c..03b12a3 100644
--- a/src/TulsiGenerator/TulsiOption.swift
+++ b/src/TulsiGenerator/TulsiOption.swift
@@ -28,8 +28,18 @@
   public static let InheritKeyword = "$(inherited)"
 
   /// The valid value types for this option.
-  public enum ValueType {
+  public enum ValueType: Equatable {
     case bool, string
+    case stringEnum([String])
+
+    public static func ==(lhs: ValueType, rhs: ValueType) -> Bool {
+      switch (lhs, rhs) {
+        case (.bool, .bool): return true
+        case (.string, .string): return true
+        case (.stringEnum(let a), .stringEnum(let b)): return a == b
+        default: return false
+      }
+    }
   }
 
   /// How this option is intended to be used.
@@ -158,13 +168,19 @@
   }
 
   public func sanitizeValue(_ value: String?) -> String? {
-    if valueType == .bool {
-      if value != TulsiOption.BooleanTrueValue {
-        return TulsiOption.BooleanFalseValue
-      }
-      return value
+    switch (valueType) {
+      case .bool:
+        if value != TulsiOption.BooleanTrueValue {
+          return TulsiOption.BooleanFalseValue
+        }
+        return value
+      case .string:
+        return value?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
+      case .stringEnum(let values):
+        guard let curValue = value else { return defaultValue }
+        guard values.contains(curValue) else { return defaultValue }
+        return curValue
     }
-    return value?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
   }
 
   // Generates a serialized form of this option's user-defined values or nil if the value is
@@ -184,13 +200,19 @@
 
   func deserialize(_ serialized: PersistenceType) {
     if let value = serialized[TulsiOption.ProjectValueKey] as? String {
-      projectValue = value
+      projectValue = sanitizeValue(value)
     } else {
       projectValue = nil
     }
 
     if let values = serialized[TulsiOption.TargetValuesKey] as? [String: String] {
-      targetValues = values
+      var validValues = [String: String]()
+      for (key, value) in values {
+        if let sanitized = sanitizeValue(value) {
+          validValues[key] = sanitized
+        }
+      }
+      targetValues = validValues
     } else if optionType.contains(.TargetSpecializable) {
       self.targetValues = [String: String]()
     } else {
diff --git a/src/TulsiGenerator/TulsiOptionSet.swift b/src/TulsiGenerator/TulsiOptionSet.swift
index 2686e02..665c0c4 100644
--- a/src/TulsiGenerator/TulsiOptionSet.swift
+++ b/src/TulsiGenerator/TulsiOptionSet.swift
@@ -274,6 +274,16 @@
       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(.BazelBuildOptionsProjectGenerationOnly, .Generic)
diff --git a/src/TulsiGenerator/XcodeProjectGenerator.swift b/src/TulsiGenerator/XcodeProjectGenerator.swift
index e03f8ca..9327796 100644
--- a/src/TulsiGenerator/XcodeProjectGenerator.swift
+++ b/src/TulsiGenerator/XcodeProjectGenerator.swift
@@ -111,10 +111,6 @@
   /// write a stub value that will be the same regardless of the execution environment.
   var redactWorkspaceSymlink = false
 
-  /// Exposed for testing. Suppresses creating folders for artifacts that are expected to be
-  /// generated by Bazel.
-  var suppressGeneratedArtifactFolderCreation = false
-
   /// Exposed for testing. Do not modify user defaults.
   var suppressModifyingUserDefaults = false
 
@@ -181,7 +177,6 @@
                                                                        context: config.projectName)
     defer { localizedMessageLogger.logProfilingEnd(generateProfilingToken) }
     try validateXcodeProjectPath(outputFolderURL)
-    try resolveConfigReferences()
 
     let mainGroup = pbxTargetGeneratorType.mainGroupForOutputFolder(outputFolderURL,
                                                                     workspaceRootURL: workspaceRootURL)
@@ -331,9 +326,9 @@
     }
   }
 
-  /// Invokes Bazel to load any missing information in the config file.
-  private func resolveConfigReferences() throws {
-    let ruleEntryMap = try loadRuleEntryMap()
+  /// Validates that the aspect output contains all targets listed in the config file and that
+  /// there are no ambiguous top-level targets.
+  private func validateConfigReferences(_ ruleEntryMap: RuleEntryMap) throws {
     let unresolvedLabels = config.buildTargetLabels.filter {
       !ruleEntryMap.hasAnyRuleEntry(withBuildLabel: $0)
     }
@@ -380,6 +375,8 @@
     }
 
     let ruleEntryMap = try loadRuleEntryMap()
+    try validateConfigReferences(ruleEntryMap)
+
     var expandedTargetLabels = Set<BuildLabel>()
     var testSuiteRules = [BuildLabel: RuleEntry]()
     func expandTargetLabels<T: Sequence>(_ labels: T) where T.Iterator.Element == BuildLabel {
@@ -1310,7 +1307,12 @@
 
       let errorInfo: String?
       do {
+        // Only over-write if needed.
         if fileManager.fileExists(atPath: targetURL.path) {
+          guard !fileManager.contentsEqual(atPath: sourceURL.path, andPath: targetURL.path) else {
+            print("Not overwriting \(targetURL.path) as its contents haven't changed.")
+            continue;
+          }
           try fileManager.removeItem(at: targetURL)
         }
         try fileManager.copyItem(at: sourceURL, to: targetURL)
diff --git a/src/TulsiGeneratorIntegrationTests/AspectTests.swift b/src/TulsiGeneratorIntegrationTests/AspectTests.swift
index 24d206d..c7477b6 100644
--- a/src/TulsiGeneratorIntegrationTests/AspectTests.swift
+++ b/src/TulsiGeneratorIntegrationTests/AspectTests.swift
@@ -28,6 +28,13 @@
                                                    localizedMessageLogger: localizedMessageLogger)
   }
 
+  // Utility function to fetch RuleEntries for the given labels.
+  func extractRuleEntriesForLabels(_ labels: [BuildLabel]) throws -> RuleEntryMap {
+    return try aspectInfoExtractor.extractRuleEntriesForLabels(labels,
+                                                               startupOptions: bazelStartupOptions,
+                                                               buildOptions: bazelBuildOptions)
+  }
+
   func testSimple() throws {
     installBUILDFile("Simple", intoSubdirectory: "tulsi_test")
     makeTestXCDataModel("SimpleDataModelsTestv1", inSubdirectory: "tulsi_test/SimpleTest.xcdatamodeld")
@@ -114,10 +121,8 @@
   func testExceptionThrown() {
     installBUILDFile("SimpleBad", intoSubdirectory: "tulsi_test")
     do {
-      let _ = try aspectInfoExtractor.extractRuleEntriesForLabels([BuildLabel("//tulsi_test:Application"),
-                                                                   BuildLabel("//tulsi_test:XCTest")],
-                                                                  startupOptions: bazelStartupOptions,
-                                                                  buildOptions: bazelBuildOptions)
+      let _ = try extractRuleEntriesForLabels([BuildLabel("//tulsi_test:Application"),
+                                               BuildLabel("//tulsi_test:XCTest")])
     } catch BazelAspectInfoExtractor.ExtractorError.buildFailed {
       // Expected failure on malformed BUILD file.
       XCTAssert(aspectInfoExtractor.hasQueuedInfoMessages)
@@ -141,9 +146,7 @@
                        withContent: ["NSExtension": ["NSExtensionPointIdentifier": "com.apple.extension-foo"]],
                        inSubdirectory: "(tulsi_test/TodayExtension")
 
-    let ruleEntryMap = try aspectInfoExtractor.extractRuleEntriesForLabels([BuildLabel("//tulsi_test:XCTest")],
-                                                                           startupOptions: bazelStartupOptions,
-                                                                           buildOptions: bazelBuildOptions)
+    let ruleEntryMap = try extractRuleEntriesForLabels([BuildLabel("//tulsi_test:XCTest")])
 
     let checker = InfoChecker(ruleEntryMap: ruleEntryMap)
 
@@ -326,9 +329,7 @@
                                  inSubdirectory: "tulsi_test/TodayExtension")
     XCTAssertNotNil(url)
 
-    let ruleEntryMap = try aspectInfoExtractor.extractRuleEntriesForLabels([BuildLabel("//tulsi_test:XCTest")],
-                                                                           startupOptions: bazelStartupOptions,
-                                                                           buildOptions: bazelBuildOptions)
+    let ruleEntryMap = try extractRuleEntriesForLabels([BuildLabel("//tulsi_test:XCTest")])
 
     let checker = InfoChecker(ruleEntryMap: ruleEntryMap)
 
@@ -368,9 +369,7 @@
 
   func testWatch() throws {
     installBUILDFile("Watch", intoSubdirectory: "tulsi_test")
-    let ruleEntryMap = try aspectInfoExtractor.extractRuleEntriesForLabels([BuildLabel("//tulsi_test:Application")],
-                                                                           startupOptions: bazelStartupOptions,
-                                                                           buildOptions: bazelBuildOptions)
+    let ruleEntryMap = try extractRuleEntriesForLabels([BuildLabel("//tulsi_test:Application")])
 
     let checker = InfoChecker(ruleEntryMap: ruleEntryMap)
 
@@ -403,11 +402,7 @@
 
   func testSwift() throws {
     installBUILDFile("Swift", intoSubdirectory: "tulsi_test")
-    let labels = [BuildLabel("//tulsi_test:Application")]
-    let ruleEntryMap =
-        try aspectInfoExtractor.extractRuleEntriesForLabels(labels,
-                                                            startupOptions: bazelStartupOptions,
-                                                            buildOptions: bazelBuildOptions)
+    let ruleEntryMap = try extractRuleEntriesForLabels([BuildLabel("//tulsi_test:Application")])
 
     let checker = InfoChecker(ruleEntryMap: ruleEntryMap)
 
@@ -475,10 +470,17 @@
                      fromResourceDirectory: "TestSuite/Three")
   }
 
+  // Utility function to fetch RuleEntries for the given labels.
+  func extractRuleEntriesForLabels(_ labels: [BuildLabel]) throws -> RuleEntryMap {
+    return try aspectInfoExtractor.extractRuleEntriesForLabels(
+      labels,
+      startupOptions: bazelStartupOptions,
+      buildOptions: bazelBuildOptions)
+  }
+
+
   func testTestSuite_ExplicitXCTests() throws {
-    let ruleEntryMap = try aspectInfoExtractor.extractRuleEntriesForLabels([BuildLabel("//\(testDir):explicit_XCTests")],
-                                                                           startupOptions: bazelStartupOptions,
-                                                                           buildOptions: bazelBuildOptions)
+    let ruleEntryMap = try extractRuleEntriesForLabels([BuildLabel("//\(testDir):explicit_XCTests")])
     let checker = InfoChecker(ruleEntryMap: ruleEntryMap)
 
     checker.assertThat("//\(testDir):explicit_XCTests")
@@ -495,14 +497,10 @@
         .hasTestHost("//\(testDir):TestApplication")
     checker.assertThat("//\(testDir)/Three:XCTest")
         .hasTestHost("//\(testDir):TestApplication")
-
-
   }
 
   func testTestSuite_TaggedTests() throws {
-    let ruleEntryMap = try aspectInfoExtractor.extractRuleEntriesForLabels([BuildLabel("//\(testDir):local_tagged_tests")],
-                                                                           startupOptions: bazelStartupOptions,
-                                                                           buildOptions: bazelBuildOptions)
+    let ruleEntryMap = try extractRuleEntriesForLabels([BuildLabel("//\(testDir):local_tagged_tests")])
     let checker = InfoChecker(ruleEntryMap: ruleEntryMap)
 
     checker.assertThat("//\(testDir):local_tagged_tests")
diff --git a/src/TulsiGeneratorIntegrationTests/EndToEndIntegrationTestCase.swift b/src/TulsiGeneratorIntegrationTests/EndToEndIntegrationTestCase.swift
index 55eadd5..21619e9 100644
--- a/src/TulsiGeneratorIntegrationTests/EndToEndIntegrationTestCase.swift
+++ b/src/TulsiGeneratorIntegrationTests/EndToEndIntegrationTestCase.swift
@@ -132,9 +132,6 @@
     // Bazel built-in preprocessor defines are suppressed in order to prevent any
     // environment-dependent variables from mismatching the golden data.
     projectGenerator.xcodeProjectGenerator.suppressCompilerDefines = true
-    // Output directory generation is suppressed in order to prevent having to whitelist diffs of
-    // empty directories.
-    projectGenerator.xcodeProjectGenerator.suppressGeneratedArtifactFolderCreation = true
     // Don't modify any user defaults.
     projectGenerator.xcodeProjectGenerator.suppressModifyingUserDefaults = true
     // The username is forced to a known value.
diff --git a/src/TulsiGeneratorTests/TulsiOptionSetTests.swift b/src/TulsiGeneratorTests/TulsiOptionSetTests.swift
index d039e2d..da78d97 100644
--- a/src/TulsiGeneratorTests/TulsiOptionSetTests.swift
+++ b/src/TulsiGeneratorTests/TulsiOptionSetTests.swift
@@ -49,10 +49,8 @@
 
   func testPerUserOptionsAreOmitted() {
     let optionSet = TulsiOptionSet()
-    var i = 0
     for (_, option) in optionSet.options {
-      option.projectValue = String(i)
-      i += 10
+      option.projectValue = option.defaultValue
     }
     var dict = [String: Any]()
     optionSet.saveShareableOptionsIntoDictionary(&dict)
@@ -64,6 +62,27 @@
     }
   }
 
+  func testValueSanitization() {
+    let optionSet = TulsiOptionSet()
+    let optionKey = TulsiOptionKey.ALWAYS_SEARCH_USER_PATHS
+    optionSet[optionKey].projectValue = "invalid"
+
+    var dict = [String: AnyObject]()
+    optionSet.saveAllOptionsIntoDictionary(&dict)
+
+    let optionsDict = TulsiOptionSet.getOptionsFromContainerDictionary(dict) ?? [:]
+    let deserializedSet = TulsiOptionSet(fromDictionary: optionsDict)
+
+    for (key, option) in optionSet.options {
+      if key == optionKey {
+        XCTAssertNotEqual(deserializedSet[key], option)
+        XCTAssertEqual(deserializedSet[key].projectValue, "NO")
+      } else {
+        XCTAssertEqual(deserializedSet[key], option)
+      }
+    }
+  }
+
   func testOnlyPerUserOptionsArePersisted() {
     let optionSet = TulsiOptionSet()
     var i = 0