Add an aspect to collect build outputs.

* WORKSPACE file and the "tulsi" package is now copied into the 
generated Xcode project. This ensures that the aspect can be used on 
every bazel_build.py invocation.

* The new aspect is disabled by default and is used for development 
purposes.

--
PiperOrigin-RevId: 150321088
MOS_MIGRATED_REVID=150321088
diff --git a/src/TulsiGenerator/Bazel/tulsi/tulsi_aspects.bzl b/src/TulsiGenerator/Bazel/tulsi/tulsi_aspects.bzl
index c76079a..f683c03 100644
--- a/src/TulsiGenerator/Bazel/tulsi/tulsi_aspects.bzl
+++ b/src/TulsiGenerator/Bazel/tulsi/tulsi_aspects.bzl
@@ -589,9 +589,37 @@
       transitive_attributes=transitive_attributes,
   )
 
+def _tulsi_outputs_aspect(target, ctx):
+  """Collects outputs of each build invocation."""
+
+  # TODO(b/35322727): Move apple_watch2_extension into _IPA_GENERATING_RULES
+  # when dynamic outputs is the default strategy and it does need to be
+  # special-cased above.
+  if ctx.rule.kind not in _IPA_GENERATING_RULES + ['apple_watch2_extension']:
+    return
+
+  # An IPA output is guaranteed to exist for rules in _IPA_GENERATING_RULES
+  ipa_output = [x.path for x in target.files if x.path.endswith('.ipa')][0]
+  info = _struct_omitting_none(ipa=ipa_output)
+
+  output = ctx.new_file(target.label.name + '.tulsiouts')
+  ctx.file_action(output, info.to_json())
+
+  return struct(
+      output_groups={
+          'tulsi-outputs': [output],
+      },
+  )
+
 
 tulsi_sources_aspect = aspect(
     implementation=_tulsi_sources_aspect,
-    attr_aspects=_TULSI_COMPILE_DEPS,
     fragments=['apple', 'cpp', 'objc'],
 )
+
+
+# This aspect does not propagate past the top-level target because we only need
+# the IPA, which is at top level.
+tulsi_outputs_aspect = aspect(
+    implementation=_tulsi_outputs_aspect,
+)
diff --git a/src/TulsiGenerator/Scripts/bazel_build.py b/src/TulsiGenerator/Scripts/bazel_build.py
index c200723..d15456c 100755
--- a/src/TulsiGenerator/Scripts/bazel_build.py
+++ b/src/TulsiGenerator/Scripts/bazel_build.py
@@ -31,6 +31,9 @@
 import time
 import zipfile
 
+# TOOD(b/35322727): Remove when this is the default behavior.
+USE_DYNAMIC_OUTPUTS = False
+
 
 def _PrintXcodeWarning(msg):
   sys.stdout.write(':: warning: %s\n' % msg)
@@ -635,6 +638,19 @@
     bazel_command.append('build')
     bazel_command.extend(options.GetBuildOptions(configuration))
 
+    if USE_DYNAMIC_OUTPUTS:
+      # Do not follow symlinks on __file__ in case this script is linked during
+      # development.
+      tulsi_package_dir = os.path.abspath(
+          os.path.join(os.path.dirname(__file__), '..', 'Bazel'))
+      package_path = '%%workspace%%:%s' % tulsi_package_dir
+
+      bazel_command.extend([
+          '--experimental_show_artifacts',
+          '--output_groups=tulsi-outputs,default',
+          '--aspects', '/tulsi/tulsi_aspects.bzl%tulsi_outputs_aspect',
+          '--package_path=%s' % package_path])
+
     if self.code_coverage_enabled:
       self._PrintVerbose('Enabling code coverage information.')
       bazel_command.extend([
diff --git a/src/TulsiGenerator/TulsiXcodeProjectGenerator.swift b/src/TulsiGenerator/TulsiXcodeProjectGenerator.swift
index 7dd9862..4569163 100644
--- a/src/TulsiGenerator/TulsiXcodeProjectGenerator.swift
+++ b/src/TulsiGenerator/TulsiXcodeProjectGenerator.swift
@@ -56,7 +56,9 @@
         stubInfoPlist: bundle.url(forResource: "StubInfoPlist", withExtension: "plist")!,
         stubIOSAppExInfoPlist: bundle.url(forResource: "StubIOSAppExtensionInfoPlist", withExtension: "plist")!,
         stubWatchOS2InfoPlist: bundle.url(forResource: "StubWatchOS2InfoPlist", withExtension: "plist")!,
-        stubWatchOS2AppExInfoPlist: bundle.url(forResource: "StubWatchOS2AppExtensionInfoPlist", withExtension: "plist")!)
+        stubWatchOS2AppExInfoPlist: bundle.url(forResource: "StubWatchOS2AppExtensionInfoPlist", withExtension: "plist")!,
+        workspaceFile: bundle.url(forResource: "WORKSPACE", withExtension: nil)!,
+        packageFiles: bundle.urls(forResourcesWithExtension: nil, subdirectory: "tulsi")!)
 
     // Note: A new extractor is created on each generate in order to allow users to modify their
     // BUILD files (or add new files to glob's) and regenerate without restarting Tulsi.
diff --git a/src/TulsiGenerator/XcodeProjectGenerator.swift b/src/TulsiGenerator/XcodeProjectGenerator.swift
index 36d93a5..7b3fdb9 100644
--- a/src/TulsiGenerator/XcodeProjectGenerator.swift
+++ b/src/TulsiGenerator/XcodeProjectGenerator.swift
@@ -36,18 +36,33 @@
     let stubIOSAppExInfoPlist: URL  // Stub Info.plist (needed for app extension targets).
     let stubWatchOS2InfoPlist: URL  // Stub Info.plist (needed for watchOS2 app targets).
     let stubWatchOS2AppExInfoPlist: URL  // Stub Info.plist (needed for watchOS2 appex targets).
+
+    // In order to load tulsi_aspects, Tulsi constructs a Bazel repository inside of the generated
+    // Xcode project. Its structure looks like this:
+    // ├── Bazel
+    // │   ├── WORKSPACE
+    // │   └── tulsi
+    // │       ├── file1
+    // │       └── ...
+    // These two items define the content of this repository, including the WORKSPACE file and the
+    // "tulsi" package.
+    let bazelWorkspaceFile: URL // Stub WORKSPACE file.
+    let tulsiPackageFiles: [URL] // Files to copy into the "tulsi" package.
   }
 
   /// Path relative to PROJECT_FILE_PATH in which Tulsi generated files (scripts, artifacts, etc...)
   /// should be placed.
   private static let TulsiArtifactDirectory = ".tulsi"
   static let ScriptDirectorySubpath = "\(TulsiArtifactDirectory)/Scripts"
+  static let BazelDirectorySubpath = "\(TulsiArtifactDirectory)/Bazel"
+  static let TulsiPackageName = "tulsi"
   static let UtilDirectorySubpath = "\(TulsiArtifactDirectory)/Utils"
   static let ConfigDirectorySubpath = "\(TulsiArtifactDirectory)/Configs"
   static let ProjectResourcesDirectorySubpath = "\(TulsiArtifactDirectory)/Resources"
   static let ManifestFileSubpath = "\(TulsiArtifactDirectory)/generatorManifest.json"
   private static let BuildScript = "bazel_build.py"
   private static let CleanScript = "bazel_clean.sh"
+  private static let WorkspaceFile = "WORKSPACE"
   private static let PostProcessorUtil = "post_processor"
   private static let UIRunnerEntitlements = "XCTRunner.entitlements"
   private static let StubInfoPlistFilename = "StubInfoPlist.plist"
@@ -179,6 +194,7 @@
                                           projectURL: projectURL,
                                           projectBundleName: projectBundleName)
     installTulsiScripts(projectURL)
+    installTulsiBazelPackage(projectURL)
     installUtilities(projectURL)
     installGeneratorConfig(projectURL)
     installGeneratedProjectResources(projectURL)
@@ -723,6 +739,30 @@
                     (resourceURLs.cleanScript, XcodeProjectGenerator.CleanScript),
                    ],
                    toDirectory: scriptDirectoryURL)
+
+      localizedMessageLogger.logProfilingEnd(profilingToken)
+    }
+  }
+
+  private func installTulsiBazelPackage(_ projectURL: URL) {
+
+    let bazelWorkspaceURL = projectURL.appendingPathComponent(XcodeProjectGenerator.BazelDirectorySubpath,
+                                                              isDirectory: true)
+    let bazelPackageURL = bazelWorkspaceURL.appendingPathComponent(XcodeProjectGenerator.TulsiPackageName,
+                                                                   isDirectory: true)
+
+    if createDirectory(bazelPackageURL) {
+      let profilingToken = localizedMessageLogger.startProfiling("installing_package",
+                                                                 context: config.projectName)
+      let progressNotifier = ProgressNotifier(name: InstallingScripts, maxValue: 1)
+      defer { progressNotifier.incrementValue() }
+      localizedMessageLogger.infoMessage("Installing Bazel integration package")
+
+      installFiles([(resourceURLs.bazelWorkspaceFile, XcodeProjectGenerator.WorkspaceFile)],
+                   toDirectory: bazelWorkspaceURL)
+      installFiles(resourceURLs.tulsiPackageFiles.map { ($0, $0.lastPathComponent) },
+                   toDirectory: bazelPackageURL)
+
       localizedMessageLogger.logProfilingEnd(profilingToken)
     }
   }
diff --git a/src/TulsiGeneratorTests/XcodeProjectGeneratorTests.swift b/src/TulsiGeneratorTests/XcodeProjectGeneratorTests.swift
index a4e76c7..3b58504 100644
--- a/src/TulsiGeneratorTests/XcodeProjectGeneratorTests.swift
+++ b/src/TulsiGeneratorTests/XcodeProjectGeneratorTests.swift
@@ -41,7 +41,9 @@
       stubInfoPlist: URL(fileURLWithPath: "/generatedProjectResources/StubInfoPlist.plist"),
       stubIOSAppExInfoPlist: URL(fileURLWithPath: "/generatedProjectResources/stubIOSAppExInfoPlist.plist"),
       stubWatchOS2InfoPlist: URL(fileURLWithPath: "/generatedProjectResources/StubWatchOS2InfoPlist.plist"),
-      stubWatchOS2AppExInfoPlist: URL(fileURLWithPath: "/generatedProjectResources/StubWatchOS2AppExInfoPlist.plist"))
+      stubWatchOS2AppExInfoPlist: URL(fileURLWithPath: "/generatedProjectResources/StubWatchOS2AppExInfoPlist.plist"),
+      workspaceFile: URL(fileURLWithPath: "/WORKSPACE"),
+      packageFiles: [URL(fileURLWithPath: "/tulsi/tulsi_aspects.bzl")])
 
   var config: TulsiGeneratorConfig! = nil
   var mockLocalizedMessageLogger: MockLocalizedMessageLogger! = nil
@@ -228,6 +230,12 @@
     let resources = projectURL.appendingPathComponent(".tulsi/Resources")
     mockFileManager.allowedDirectoryCreates.insert(resources.path)
 
+    let tulsiBazelRoot = projectURL.appendingPathComponent(".tulsi/Bazel")
+    mockFileManager.allowedDirectoryCreates.insert(tulsiBazelRoot.path)
+
+    let tulsiBazelPackage = projectURL.appendingPathComponent(".tulsi/Bazel/tulsi")
+    mockFileManager.allowedDirectoryCreates.insert(tulsiBazelPackage.path)
+
     mockExtractor.labelToRuleEntry = ruleEntries
 
     generator = XcodeProjectGenerator(workspaceRootURL: workspaceRoot,