A prototype implementation of top-level aspects.

--
MOS_MIGRATED_REVID=101033236
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/Aspect.java b/src/main/java/com/google/devtools/build/lib/analysis/Aspect.java
index 3f4a06e..95f0683 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/Aspect.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/Aspect.java
@@ -17,10 +17,16 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.UnmodifiableIterator;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.TreeMap;
+
+import javax.annotation.Nullable;
 
 /**
  * Extra information about a configured target computed on request of a dependent.
@@ -34,15 +40,25 @@
  */
 @Immutable
 public final class Aspect implements Iterable<TransitiveInfoProvider> {
-  private final
-      ImmutableMap<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers;
+  private final String name;
+  private final ImmutableMap<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider>
+      providers;
 
   private Aspect(
+      String name,
       ImmutableMap<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers) {
+    this.name = name;
     this.providers = providers;
   }
 
   /**
+   * Returns the aspect name.
+   */
+  public String getName() {
+    return name;
+  }
+
+  /**
    * Returns the providers created by the aspect.
    */
   public ImmutableMap<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider>
@@ -50,6 +66,14 @@
     return providers;
   }
 
+
+  @Nullable
+  <P extends TransitiveInfoProvider> P getProvider(Class<P> providerClass) {
+    AnalysisUtils.checkProvider(providerClass);
+
+    return providerClass.cast(providers.get(providerClass));
+  }
+
   @Override
   public UnmodifiableIterator<TransitiveInfoProvider> iterator() {
     return providers.values().iterator();
@@ -61,6 +85,12 @@
   public static class Builder {
     private final Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider>
         providers = new LinkedHashMap<>();
+    private final Map<String, NestedSetBuilder<Artifact>> outputGroupBuilders = new TreeMap<>();
+    private final String name;
+
+    public Builder(String name) {
+      this.name = name;
+    }
 
     /**
      * Adds a provider to the aspect.
@@ -75,8 +105,35 @@
       return this;
     }
 
+    /**
+     * Adds a set of files to an output group.
+     */
+    public Builder addOutputGroup(String name, NestedSet<Artifact> artifacts) {
+      NestedSetBuilder<Artifact> nestedSetBuilder = outputGroupBuilders.get(name);
+      if (nestedSetBuilder == null) {
+        nestedSetBuilder = NestedSetBuilder.<Artifact>stableOrder();
+        outputGroupBuilders.put(name, nestedSetBuilder);
+      }
+      nestedSetBuilder.addTransitive(artifacts);
+      return this;
+    }
+
+
     public Aspect build() {
-      return new Aspect(ImmutableMap.copyOf(providers));
+      if (!outputGroupBuilders.isEmpty()) {
+        ImmutableMap.Builder<String, NestedSet<Artifact>> outputGroups = ImmutableMap.builder();
+        for (Map.Entry<String, NestedSetBuilder<Artifact>> entry : outputGroupBuilders.entrySet()) {
+          outputGroups.put(entry.getKey(), entry.getValue().build());
+        }
+
+        if (providers.containsKey(OutputGroupProvider.class)) {
+          throw new IllegalStateException(
+              "OutputGroupProvider was provided explicitly; do not use addOutputGroup");
+        }
+        addProvider(OutputGroupProvider.class, new OutputGroupProvider(outputGroups.build()));
+      }
+
+      return new Aspect(name, ImmutableMap.copyOf(providers));
     }
   }
 }
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AspectCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/analysis/AspectCompleteEvent.java
new file mode 100644
index 0000000..faefb3c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AspectCompleteEvent.java
@@ -0,0 +1,73 @@
+// Copyright 2015 Google Inc. 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.
+package com.google.devtools.build.lib.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.skyframe.AspectValue;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * This event is fired as soon as a top-level aspect is either built or fails.
+ */
+public class AspectCompleteEvent implements SkyValue {
+  private final AspectValue aspectValue;
+  private final NestedSet<Label> rootCauses;
+
+  private AspectCompleteEvent(AspectValue aspectValue, NestedSet<Label> rootCauses) {
+    this.aspectValue = aspectValue;
+    this.rootCauses =
+        (rootCauses == null) ? NestedSetBuilder.<Label>emptySet(Order.STABLE_ORDER) : rootCauses;
+  }
+
+  /**
+   * Construct a successful target completion event.
+   */
+  public static AspectCompleteEvent createSuccessful(AspectValue value) {
+    return new AspectCompleteEvent(value, null);
+  }
+
+  /**
+   * Construct a target completion event for a failed target, with the given non-empty root causes.
+   */
+  public static AspectCompleteEvent createFailed(AspectValue value, NestedSet<Label> rootCauses) {
+    Preconditions.checkArgument(!Iterables.isEmpty(rootCauses));
+    return new AspectCompleteEvent(value, rootCauses);
+  }
+
+  /**
+   * Returns the target associated with the event.
+   */
+  public AspectValue getAspectValue() {
+    return aspectValue;
+  }
+
+  /**
+   * Determines whether the target has failed or succeeded.
+   */
+  public boolean failed() {
+    return !rootCauses.isEmpty();
+  }
+
+  /**
+   * Get the root causes of the target. May be empty.
+   */
+  public Iterable<Label> getRootCauses() {
+    return rootCauses;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java b/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
index 3baae74..7ce8c04 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
@@ -65,13 +65,15 @@
 import com.google.devtools.build.lib.rules.test.CoverageReportActionFactory;
 import com.google.devtools.build.lib.rules.test.CoverageReportActionFactory.CoverageReportActionsWrapper;
 import com.google.devtools.build.lib.skyframe.ActionLookupValue;
+import com.google.devtools.build.lib.skyframe.AspectValue;
+import com.google.devtools.build.lib.skyframe.AspectValue.AspectKey;
 import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
 import com.google.devtools.build.lib.skyframe.CoverageReportValue;
+import com.google.devtools.build.lib.skyframe.SkyframeAnalysisResult;
 import com.google.devtools.build.lib.skyframe.SkyframeBuildView;
 import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Label;
-import com.google.devtools.build.lib.util.Pair;
 import com.google.devtools.build.lib.util.RegexFilter;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.skyframe.SkyKey;
@@ -79,6 +81,7 @@
 import com.google.devtools.common.options.Option;
 import com.google.devtools.common.options.OptionsBase;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -434,12 +437,17 @@
    */
   public static final class AnalysisResult {
 
-    public static final AnalysisResult EMPTY = new AnalysisResult(
-        ImmutableList.<ConfiguredTarget>of(), null, null, null,
-        ImmutableList.<Artifact>of(),
-        ImmutableList.<ConfiguredTarget>of(),
-        ImmutableList.<ConfiguredTarget>of(),
-        null);
+    public static final AnalysisResult EMPTY =
+        new AnalysisResult(
+            ImmutableList.<ConfiguredTarget>of(),
+            ImmutableList.<AspectValue>of(),
+            null,
+            null,
+            null,
+            ImmutableList.<Artifact>of(),
+            ImmutableList.<ConfiguredTarget>of(),
+            ImmutableList.<ConfiguredTarget>of(),
+            null);
 
     private final ImmutableList<ConfiguredTarget> targetsToBuild;
     @Nullable private final ImmutableList<ConfiguredTarget> targetsToTest;
@@ -449,13 +457,20 @@
     private final ImmutableSet<ConfiguredTarget> parallelTests;
     private final ImmutableSet<ConfiguredTarget> exclusiveTests;
     @Nullable private final TopLevelArtifactContext topLevelContext;
+    private final ImmutableList<AspectValue> aspects;
 
     private AnalysisResult(
-        Collection<ConfiguredTarget> targetsToBuild, Collection<ConfiguredTarget> targetsToTest,
-        @Nullable String error, ActionGraph actionGraph,
-        Collection<Artifact> artifactsToBuild, Collection<ConfiguredTarget> parallelTests,
-        Collection<ConfiguredTarget> exclusiveTests, TopLevelArtifactContext topLevelContext) {
+        Collection<ConfiguredTarget> targetsToBuild,
+        Collection<AspectValue> aspects,
+        Collection<ConfiguredTarget> targetsToTest,
+        @Nullable String error,
+        ActionGraph actionGraph,
+        Collection<Artifact> artifactsToBuild,
+        Collection<ConfiguredTarget> parallelTests,
+        Collection<ConfiguredTarget> exclusiveTests,
+        TopLevelArtifactContext topLevelContext) {
       this.targetsToBuild = ImmutableList.copyOf(targetsToBuild);
+      this.aspects = ImmutableList.copyOf(aspects);
       this.targetsToTest = targetsToTest == null ? null : ImmutableList.copyOf(targetsToTest);
       this.error = error;
       this.actionGraph = actionGraph;
@@ -473,6 +488,16 @@
     }
 
     /**
+     * Returns aspects of configured targets to build.
+     *
+     * <p>If this list is empty, build the targets returned by {@code getTargetsToBuild()}.
+     * Otherwise, only build these aspects of the targets returned by {@code getTargetsToBuild()}.
+     */
+    public Collection<AspectValue> getAspects() {
+      return aspects;
+    }
+
+    /**
      * Returns the configured targets to run as tests, or {@code null} if testing was not
      * requested (e.g. "build" command rather than "test" command).
      */
@@ -520,10 +545,11 @@
   static Iterable<? extends ConfiguredTarget> filterTestsByTargets(
       Collection<? extends ConfiguredTarget> targets,
       final Set<? extends Target> allowedTargets) {
-    return Iterables.filter(targets,
+    return Iterables.filter(
+        targets,
         new Predicate<ConfiguredTarget>() {
           @Override
-              public boolean apply(ConfiguredTarget rule) {
+          public boolean apply(ConfiguredTarget rule) {
             return allowedTargets.contains(rule.getTarget());
           }
         });
@@ -536,10 +562,15 @@
   }
 
   @ThreadCompatible
-  public AnalysisResult update(LoadingResult loadingResult,
-      BuildConfigurationCollection configurations, Options viewOptions,
-      TopLevelArtifactContext topLevelOptions, EventHandler eventHandler, EventBus eventBus)
-          throws ViewCreationFailedException, InterruptedException {
+  public AnalysisResult update(
+      LoadingResult loadingResult,
+      BuildConfigurationCollection configurations,
+      List<String> aspects,
+      Options viewOptions,
+      TopLevelArtifactContext topLevelOptions,
+      EventHandler eventHandler,
+      EventBus eventBus)
+      throws ViewCreationFailedException, InterruptedException {
     LOG.info("Starting analysis");
     pollInterruptedStatus();
 
@@ -580,21 +611,36 @@
           }
         });
 
+    List<AspectKey> aspectKeys = new ArrayList<>();
+    for (String aspect : aspects) {
+      @SuppressWarnings("unchecked")
+      final Class<? extends ConfiguredAspectFactory> aspectFactoryClass =
+          (Class<? extends ConfiguredAspectFactory>)
+              ruleClassProvider.getAspectFactoryMap().get(aspect);
+      if (aspectFactoryClass != null) {
+        for (ConfiguredTargetKey targetSpec : targetSpecs) {
+          aspectKeys.add(
+              AspectValue.createAspectKey(
+                  targetSpec.getLabel(), targetSpec.getConfiguration(), aspectFactoryClass));
+        }
+      } else {
+        throw new ViewCreationFailedException("Aspect '" + aspect + "' is unknown");
+      }
+    }
+
     prepareToBuild(new SkyframePackageRootResolver(skyframeExecutor));
     skyframeExecutor.injectWorkspaceStatusData();
-    Collection<ConfiguredTarget> configuredTargets;
-    WalkableGraph graph;
+    SkyframeAnalysisResult skyframeAnalysisResult;
     try {
-      Pair<Collection<ConfiguredTarget>, WalkableGraph> configuredTargetsResult =
-          skyframeBuildView.configureTargets(targetSpecs, eventBus, viewOptions.keepGoing);
-      configuredTargets = configuredTargetsResult.getFirst();
-      graph = configuredTargetsResult.getSecond();
+      skyframeAnalysisResult =
+          skyframeBuildView.configureTargets(
+              targetSpecs, aspectKeys, eventBus, viewOptions.keepGoing);
     } finally {
       skyframeBuildView.clearInvalidatedConfiguredTargets();
     }
 
     int numTargetsToAnalyze = nodes.size();
-    int numSuccessful = configuredTargets.size();
+    int numSuccessful = skyframeAnalysisResult.getConfiguredTargets().size();
     boolean analysisSuccessful = (numSuccessful == numTargetsToAnalyze);
     if (0 < numSuccessful && numSuccessful < numTargetsToAnalyze) {
       String msg = String.format("Analysis succeeded for only %d of %d top-level targets",
@@ -603,17 +649,28 @@
       LOG.info(msg);
     }
 
-    AnalysisResult result = createResult(loadingResult, topLevelOptions,
-        viewOptions, configuredTargets, analysisSuccessful, graph);
+    AnalysisResult result =
+        createResult(
+            loadingResult,
+            topLevelOptions,
+            viewOptions,
+            skyframeAnalysisResult.getConfiguredTargets(),
+            skyframeAnalysisResult.getAspects(),
+            skyframeAnalysisResult.getWalkableGraph(),
+            analysisSuccessful);
     LOG.info("Finished analysis");
     return result;
   }
 
-  private AnalysisResult createResult(LoadingResult loadingResult,
-      TopLevelArtifactContext topLevelOptions, BuildView.Options viewOptions,
-      Collection<ConfiguredTarget> configuredTargets, boolean analysisSuccessful,
-      final WalkableGraph graph)
-          throws InterruptedException {
+  private AnalysisResult createResult(
+      LoadingResult loadingResult,
+      TopLevelArtifactContext topLevelOptions,
+      BuildView.Options viewOptions,
+      Collection<ConfiguredTarget> configuredTargets,
+      Collection<AspectValue> aspects,
+      final WalkableGraph graph,
+      boolean analysisSuccessful)
+      throws InterruptedException {
     Collection<Target> testsToRun = loadingResult.getTestsToRun();
     Collection<ConfiguredTarget> allTargetsToTest = null;
     if (testsToRun != null) {
@@ -667,8 +724,16 @@
         return null;
       }
     };
-    return new AnalysisResult(configuredTargets, allTargetsToTest, error, actionGraph,
-        artifactsToBuild, parallelTests, exclusiveTests, topLevelOptions);
+    return new AnalysisResult(
+        configuredTargets,
+        aspects,
+        allTargetsToTest,
+        error,
+        actionGraph,
+        artifactsToBuild,
+        parallelTests,
+        exclusiveTests,
+        topLevelOptions);
   }
 
   private static NestedSet<Artifact> getBaselineCoverageArtifacts(
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java
index 3b40300..543c71e 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java
@@ -31,6 +31,7 @@
 import com.google.devtools.build.lib.events.EventHandler;
 import com.google.devtools.build.lib.graph.Digraph;
 import com.google.devtools.build.lib.graph.Node;
+import com.google.devtools.build.lib.packages.AspectFactory;
 import com.google.devtools.build.lib.packages.Attribute;
 import com.google.devtools.build.lib.packages.RuleClass;
 import com.google.devtools.build.lib.packages.RuleClassProvider;
@@ -83,6 +84,8 @@
     private final Map<String, RuleClass> ruleClassMap = new HashMap<>();
     private final  Map<String, Class<? extends RuleDefinition>> ruleDefinitionMap =
         new HashMap<>();
+    private final Map<String, Class<? extends AspectFactory<?, ?, ?>>> aspectFactoryMap =
+        new HashMap<>();
     private final Map<Class<? extends RuleDefinition>, RuleClass> ruleMap = new HashMap<>();
     private final Map<Class<? extends RuleDefinition>, RuleDefinition> ruleDefinitionInstanceCache =
         new HashMap<>();
@@ -117,6 +120,13 @@
       return this;
     }
 
+    public Builder addAspectFactory(
+        String name, Class<? extends AspectFactory<?, ?, ?>> configuredAspectFactoryClass) {
+      aspectFactoryMap.put(name, configuredAspectFactoryClass);
+
+      return this;
+    }
+
     public Builder addConfigurationOptions(Class<? extends FragmentOptions> configurationOptions) {
       this.configurationOptions.add(configurationOptions);
       return this;
@@ -199,6 +209,7 @@
       return new ConfiguredRuleClassProvider(
           ImmutableMap.copyOf(ruleClassMap),
           ImmutableMap.copyOf(ruleDefinitionMap),
+          ImmutableMap.copyOf(aspectFactoryMap),
           defaultWorkspaceFile.toString(),
           ImmutableList.copyOf(buildInfoFactories),
           ImmutableList.copyOf(configurationOptions),
@@ -247,6 +258,11 @@
   private final ImmutableMap<String, Class<? extends RuleDefinition>> ruleDefinitionMap;
 
   /**
+   * Maps aspect name to the aspect factory meta class.
+   */
+  private final ImmutableMap<String, Class<? extends AspectFactory<?, ?, ?>>> aspectFactoryMap;
+
+  /**
    * The configuration options that affect the behavior of the rules.
    */
   private final ImmutableList<Class<? extends FragmentOptions>> configurationOptions;
@@ -272,6 +288,7 @@
   public ConfiguredRuleClassProvider(
       ImmutableMap<String, RuleClass> ruleClassMap,
       ImmutableMap<String, Class<? extends RuleDefinition>> ruleDefinitionMap,
+      ImmutableMap<String, Class<? extends AspectFactory<?, ?, ?>>> aspectFactoryMap,
       String defaultWorkspaceFile,
       ImmutableList<BuildInfoFactory> buildInfoFactories,
       ImmutableList<Class<? extends FragmentOptions>> configurationOptions,
@@ -282,6 +299,7 @@
 
     this.ruleClassMap = ruleClassMap;
     this.ruleDefinitionMap = ruleDefinitionMap;
+    this.aspectFactoryMap = aspectFactoryMap;
     this.defaultWorkspaceFile = defaultWorkspaceFile;
     this.buildInfoFactories = buildInfoFactories;
     this.configurationOptions = configurationOptions;
@@ -302,6 +320,11 @@
     return ruleClassMap;
   }
 
+  @Override
+  public Map<String, Class<? extends AspectFactory<?, ?, ?>>> getAspectFactoryMap() {
+    return aspectFactoryMap;
+  }
+
   /**
    * Returns a list of build info factories that are needed for the supported languages.
    */
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/OutputGroupProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/OutputGroupProvider.java
index 90b373f..5411f0f 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/OutputGroupProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/OutputGroupProvider.java
@@ -22,6 +22,12 @@
 import com.google.devtools.build.lib.collect.nestedset.Order;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
 /**
  * {@code ConfiguredTarget}s implementing this interface can provide artifacts that <b>can</b> be
  * built when the target is mentioned on the command line (as opposed to being always built, like
@@ -93,7 +99,7 @@
 
   private final ImmutableMap<String, NestedSet<Artifact>> outputGroups;
 
-  OutputGroupProvider(ImmutableMap<String, NestedSet<Artifact>> outputGroups) {
+  public OutputGroupProvider(ImmutableMap<String, NestedSet<Artifact>> outputGroups) {
     this.outputGroups = outputGroups;
   }
 
@@ -107,4 +113,32 @@
         ? outputGroups.get(outputGroupName)
         : NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER);
   }
+
+  /**
+   * Merges output groups from two output providers. The set of output groups must be disjoint.
+   *
+   * @param providers providers to merge {@code this} with.
+   */
+  @Nullable
+  public static OutputGroupProvider merge(List<OutputGroupProvider> providers) {
+    if (providers.size() == 0) {
+      return null;
+    }
+    if (providers.size() == 1) {
+      return providers.get(0);
+    }
+
+    ImmutableMap.Builder<String, NestedSet<Artifact>> resultBuilder = new ImmutableMap.Builder<>();
+    Set<String> seenGroups = new HashSet<>();
+    for (OutputGroupProvider provider : providers) {
+      for (String outputGroup : provider.outputGroups.keySet()) {
+        if (!seenGroups.add(outputGroup)) {
+          throw new IllegalStateException("Output group " + outputGroup + " provided twice");
+        }
+
+        resultBuilder.put(outputGroup, provider.getOutputGroup(outputGroup));
+      }
+    }
+    return new OutputGroupProvider(resultBuilder.build());
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTarget.java
index 6ba6f1e..fb99467 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTarget.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTarget.java
@@ -27,8 +27,10 @@
 import com.google.devtools.build.lib.packages.Rule;
 import com.google.devtools.build.lib.rules.SkylarkApiProvider;
 
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -127,15 +129,50 @@
     Set<Class<? extends TransitiveInfoProvider>> providers = new HashSet<>();
 
     providers.addAll(base.providers.keySet());
+
+    // Merge output group providers.
+    OutputGroupProvider baseOutputGroupProvider = base.getProvider(OutputGroupProvider.class);
+    List<OutputGroupProvider> outputGroupProviders = new ArrayList<>();
+    if (baseOutputGroupProvider != null) {
+      outputGroupProviders.add(baseOutputGroupProvider);
+    }
+
+    for (Aspect aspect : aspects) {
+      final OutputGroupProvider aspectProvider = aspect.getProvider(OutputGroupProvider.class);
+      if (aspectProvider == null) {
+        continue;
+      }
+      outputGroupProviders.add(aspectProvider);
+    }
+    OutputGroupProvider outputGroupProvider = OutputGroupProvider.merge(outputGroupProviders);
+
+    // Validate that all other providers are only provided once.
     for (Aspect aspect : aspects) {
       for (TransitiveInfoProvider aspectProvider : aspect) {
-        if (!providers.add(aspectProvider.getClass())) {
-          throw new IllegalStateException(
-              "Provider " + aspectProvider.getClass() + " provided twice");
+        Class<? extends TransitiveInfoProvider> aClass = aspectProvider.getClass();
+        if (OutputGroupProvider.class.equals(aClass)) {
+          continue;
+        }
+        if (!providers.add(aClass)) {
+          throw new IllegalStateException("Provider " + aClass + " provided twice");
         }
       }
     }
-    this.providers = base.providers;
+
+    if (baseOutputGroupProvider == outputGroupProvider) {
+      this.providers = base.providers;
+    } else {
+      ImmutableMap.Builder<Class<? extends TransitiveInfoProvider>, Object> builder =
+          new ImmutableMap.Builder<>();
+      for (Class<? extends TransitiveInfoProvider> aClass : base.providers.keySet()) {
+        if (OutputGroupProvider.class.equals(aClass)) {
+          continue;
+        }
+        builder.put(aClass, base.providers.get(aClass));
+      }
+      builder.put(OutputGroupProvider.class, outputGroupProvider);
+      this.providers = builder.build();
+    }
     this.mandatoryStampFiles = base.mandatoryStampFiles;
     this.configConditions = base.configConditions;
     this.aspects = ImmutableList.copyOf(aspects);
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelper.java b/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelper.java
index ddf3646..043f4e3 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelper.java
@@ -21,6 +21,7 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.rules.test.TestProvider;
+import com.google.devtools.build.lib.skyframe.AspectValue;
 
 /**
  * A small static class containing utility methods for handling the inclusion of
@@ -96,6 +97,22 @@
   }
 
   /**
+   * Utility function to form a NestedSet of all top-level Artifacts of the given targets.
+   */
+  public static ArtifactsToBuild getAllArtifactsToBuildFromAspects(
+      Iterable<AspectValue> aspects, TopLevelArtifactContext context) {
+    NestedSetBuilder<Artifact> allArtifacts = NestedSetBuilder.stableOrder();
+    NestedSetBuilder<Artifact> importantArtifacts = NestedSetBuilder.stableOrder();
+    for (AspectValue aspect : aspects) {
+      ArtifactsToBuild aspectArtifacts = getAllArtifactsToBuild(aspect, context);
+      allArtifacts.addTransitive(aspectArtifacts.getAllArtifacts());
+      importantArtifacts.addTransitive(aspectArtifacts.getImportantArtifacts());
+    }
+    return new ArtifactsToBuild(importantArtifacts.build(), allArtifacts.build());
+  }
+
+
+  /**
    * Returns all artifacts to build if this target is requested as a top-level target. The resulting
    * set includes the temps and either the files to compile, if
    * {@code context.compileOnly() == true}, or the files to run.
@@ -138,4 +155,39 @@
     allBuilder.addTransitive(importantArtifacts);
     return new ArtifactsToBuild(importantArtifacts, allBuilder.build());
   }
+
+  public static ArtifactsToBuild getAllArtifactsToBuild(
+      AspectValue aspectValue, TopLevelArtifactContext context) {
+    NestedSetBuilder<Artifact> importantBuilder = NestedSetBuilder.stableOrder();
+    NestedSetBuilder<Artifact> allBuilder = NestedSetBuilder.stableOrder();
+
+    OutputGroupProvider outputGroupProvider =
+        aspectValue.getAspect().getProvider(OutputGroupProvider.class);
+
+    for (String outputGroup : context.outputGroups()) {
+      NestedSetBuilder<Artifact> results = NestedSetBuilder.stableOrder();
+
+      if (outputGroup.equals(OutputGroupProvider.DEFAULT)) {
+        // For the default group, we also throw in filesToBuild
+        FileProvider fileProvider = aspectValue.getAspect().getProvider(FileProvider.class);
+        if (fileProvider != null) {
+          results.addTransitive(fileProvider.getFilesToBuild());
+        }
+      }
+
+      if (outputGroupProvider != null) {
+        results.addTransitive(outputGroupProvider.getOutputGroup(outputGroup));
+      }
+
+      if (outputGroup.startsWith(OutputGroupProvider.HIDDEN_OUTPUT_GROUP_PREFIX)) {
+        allBuilder.addTransitive(results.build());
+      } else {
+        importantBuilder.addTransitive(results.build());
+      }
+    }
+
+    NestedSet<Artifact> importantArtifacts = importantBuilder.build();
+    allBuilder.addTransitive(importantArtifacts);
+    return new ArtifactsToBuild(importantArtifacts, allBuilder.build());
+  }
 }