Introduce ninja_build for the correct composition of targets.

ninja_graph is responsible now only for parsing the graph and symlinking inputs under output_root. This way it will always be a leaf in the build graph, and all combinations of non-cyclic dependencies between Bazel-built and Ninja-built targets are possible.
ninja_build absorbs parsed NinjaTarget objects through NinjaGraphProvider.
ninja_build creates actions for the subgraph for the targets from output_groups.
Bazel-built dependencies are passed to ninja_build with deps_mapping attribute.
There is an interoperability with Bazel test.

! this CL additionally contains a fix for deps_mapping to also replace the path to bazel-built file in the constructed command line.

Closes #10804.

PiperOrigin-RevId: 295731311
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaActionsHelper.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaActionsHelper.java
index e11511b..af007a3 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaActionsHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaActionsHelper.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.Artifact.DerivedArtifact;
@@ -25,7 +26,6 @@
 import com.google.devtools.build.lib.analysis.RuleContext;
 import com.google.devtools.build.lib.analysis.ShToolchain;
 import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
-import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
 import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
 import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaRule;
 import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaRuleVariable;
@@ -41,6 +41,8 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
@@ -51,7 +53,6 @@
  */
 public class NinjaActionsHelper {
   private final RuleContext ruleContext;
-  private final List<String> outputRootInputs;
   private final ImmutableSortedMap<PathFragment, NinjaTarget> allUsualTargets;
   private final ImmutableSortedMap<PathFragment, PhonyTarget> phonyTargets;
 
@@ -67,8 +68,6 @@
    *
    * @param ruleContext parent NinjaGraphRule rule context
    * @param artifactsHelper helper object to create artifacts
-   * @param outputRootInputs inputs under output_root directory. Should be symlinked by absolute
-   *     paths under execroot/output_root.
    * @param allUsualTargets mapping of outputs to all non-phony Ninja targets from Ninja file
    * @param phonyTargets mapping of names to all phony Ninja actions from Ninja file
    * @param phonyTargetArtifacts helper class for computing transitively included artifacts of phony
@@ -78,14 +77,12 @@
   NinjaActionsHelper(
       RuleContext ruleContext,
       NinjaGraphArtifactsHelper artifactsHelper,
-      List<String> outputRootInputs,
       ImmutableSortedMap<PathFragment, NinjaTarget> allUsualTargets,
       ImmutableSortedMap<PathFragment, PhonyTarget> phonyTargets,
       PhonyTargetArtifacts phonyTargetArtifacts,
       List<PathFragment> pathsToBuild) {
     this.ruleContext = ruleContext;
     this.artifactsHelper = artifactsHelper;
-    this.outputRootInputs = outputRootInputs;
     this.allUsualTargets = allUsualTargets;
     this.phonyTargets = phonyTargets;
     this.shellExecutable = ShToolchain.getPathOrError(ruleContext);
@@ -94,39 +91,7 @@
     this.pathsToBuild = pathsToBuild;
   }
 
-  void process() throws GenericParsingException {
-    createSymlinkActions();
-    createNinjaActions();
-  }
-
-  private void createSymlinkActions() throws GenericParsingException {
-    if (this.outputRootInputs.isEmpty()) {
-      return;
-    }
-    for (String input : this.outputRootInputs) {
-      // output_root_inputs are relative to the output_root directory, and we should
-      // pass inside createOutputArtifact() paths, relative to working directory.
-      DerivedArtifact derivedArtifact =
-          artifactsHelper.createOutputArtifact(
-              artifactsHelper
-                  .getOutputRootPath()
-                  .getRelative(input)
-                  .relativeTo(artifactsHelper.getWorkingDirectory()));
-      // This method already expects the path relative to output_root.
-      PathFragment absolutePath =
-          artifactsHelper.createAbsolutePathUnderOutputRoot(PathFragment.create(input));
-      SymlinkAction symlinkAction =
-          SymlinkAction.toAbsolutePath(
-              ruleContext.getActionOwner(),
-              absolutePath,
-              derivedArtifact,
-              String.format(
-                  "Symlinking %s under <execroot>/%s", input, artifactsHelper.getOutputRootPath()));
-      ruleContext.registerAction(symlinkAction);
-    }
-  }
-
-  private void createNinjaActions() throws GenericParsingException {
+  void createNinjaActions() throws GenericParsingException {
     // Traverse the action graph starting from the targets, specified by the user.
     // Only create the required actions.
     Set<PathFragment> visitedPaths = Sets.newHashSet();
@@ -165,7 +130,8 @@
 
     NestedSetBuilder<Artifact> inputsBuilder = NestedSetBuilder.stableOrder();
     ImmutableList.Builder<Artifact> outputsBuilder = ImmutableList.builder();
-    boolean isAlwaysDirty = fillArtifacts(target, inputsBuilder, outputsBuilder);
+    TreeMap<PathFragment, Artifact> depsReplacements = Maps.newTreeMap();
+    boolean isAlwaysDirty = fillArtifacts(target, inputsBuilder, outputsBuilder, depsReplacements);
 
     NinjaScope targetScope = createTargetScope(target);
     int targetOffset = target.getOffset();
@@ -196,7 +162,8 @@
   private boolean fillArtifacts(
       NinjaTarget target,
       NestedSetBuilder<Artifact> inputsBuilder,
-      ImmutableList.Builder<Artifact> outputsBuilder)
+      ImmutableList.Builder<Artifact> outputsBuilder,
+      SortedMap<PathFragment, Artifact> depsReplacements)
       throws GenericParsingException {
     boolean isAlwaysDirty = false;
     for (PathFragment input : target.getAllInputs()) {
@@ -205,7 +172,8 @@
         inputsBuilder.addTransitive(phonyTargetArtifacts.getPhonyTargetArtifacts(input));
         isAlwaysDirty |= phonyTarget.isAlwaysDirty();
       } else {
-        inputsBuilder.add(artifactsHelper.getInputArtifact(input));
+        Artifact artifact = artifactsHelper.getInputArtifact(input);
+        inputsBuilder.add(artifact);
       }
     }
 
@@ -244,7 +212,7 @@
     }
   }
 
-  private static NinjaScope createTargetScope(NinjaTarget target) {
+  private NinjaScope createTargetScope(NinjaTarget target) {
     ImmutableSortedMap.Builder<String, List<Pair<Integer, String>>> builder =
         ImmutableSortedMap.naturalOrder();
     target
@@ -252,7 +220,7 @@
         .forEach((key, value) -> builder.put(key, ImmutableList.of(Pair.of(0, value))));
     String inNewline =
         target.getUsualInputs().stream()
-            .map(PathFragment::getPathString)
+            .map(this::getInputPathWithDepsMappingReplacement)
             .collect(Collectors.joining("\n"));
     String out =
         target.getOutputs().stream()
@@ -284,4 +252,12 @@
         .modifyExecutionInfo(map, "NinjaRule");
     return map;
   }
+
+  private String getInputPathWithDepsMappingReplacement(PathFragment fragment) {
+    Artifact bazelArtifact = artifactsHelper.getDepsMappingArtifact(fragment);
+    if (bazelArtifact != null) {
+      return bazelArtifact.getPath().getPathString();
+    }
+    return fragment.getPathString();
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaBuild.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaBuild.java
new file mode 100644
index 0000000..f52aae9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaBuild.java
@@ -0,0 +1,204 @@
+// Copyright 2020 The Bazel 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.
+
+package com.google.devtools.build.lib.bazel.rules.ninja.actions;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
+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.NestedSetVisitor;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetVisitor.VisitedState;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+/** Configured target factory for {@link NinjaBuildRule}. */
+public class NinjaBuild implements RuleConfiguredTargetFactory {
+
+  @Override
+  public ConfiguredTarget create(RuleContext ruleContext)
+      throws InterruptedException, RuleErrorException, ActionConflictException {
+    Map<String, List<String>> outputGroupsMap =
+        ruleContext.attributes().get("output_groups", Type.STRING_LIST_DICT);
+    NinjaGraphProvider graphProvider =
+        ruleContext.getPrerequisite("ninja_graph", Mode.TARGET, NinjaGraphProvider.class);
+    Preconditions.checkNotNull(graphProvider);
+    List<PathFragment> pathsToBuild =
+        outputGroupsMap.values().stream()
+            .flatMap(List::stream)
+            .map(PathFragment::create)
+            .collect(Collectors.toList());
+    ImmutableSortedMap.Builder<PathFragment, Artifact> depsMapBuilder =
+        ImmutableSortedMap.naturalOrder();
+    ImmutableSortedMap.Builder<PathFragment, Artifact> symlinksMapBuilder =
+        ImmutableSortedMap.naturalOrder();
+    createDepsMap(
+        ruleContext, graphProvider.getWorkingDirectory(), depsMapBuilder, symlinksMapBuilder);
+    NinjaGraphArtifactsHelper artifactsHelper =
+        new NinjaGraphArtifactsHelper(
+            ruleContext,
+            graphProvider.getOutputRoot(),
+            graphProvider.getWorkingDirectory(),
+            createSrcsMap(ruleContext),
+            depsMapBuilder.build(),
+            symlinksMapBuilder.build());
+    if (ruleContext.hasErrors()) {
+      return null;
+    }
+
+    try {
+      PhonyTargetArtifacts phonyTargetArtifacts =
+          new PhonyTargetArtifacts(graphProvider.getPhonyTargetsMap(), artifactsHelper);
+      new NinjaActionsHelper(
+              ruleContext,
+              artifactsHelper,
+              graphProvider.getUsualTargets(),
+              graphProvider.getPhonyTargetsMap(),
+              phonyTargetArtifacts,
+              pathsToBuild)
+          .createNinjaActions();
+
+      if (!checkOrphanArtifacts(ruleContext)) {
+        return null;
+      }
+
+      NestedSetBuilder<Artifact> filesToBuild = NestedSetBuilder.stableOrder();
+      TreeMap<String, NestedSet<Artifact>> groups = Maps.newTreeMap();
+      for (Map.Entry<String, List<String>> entry : outputGroupsMap.entrySet()) {
+        NestedSet<Artifact> artifacts =
+            getGroupArtifacts(
+                ruleContext,
+                entry.getValue(),
+                graphProvider.getPhonyTargetsMap(),
+                phonyTargetArtifacts,
+                artifactsHelper);
+        groups.put(entry.getKey(), artifacts);
+        filesToBuild.addTransitive(artifacts);
+      }
+
+      if (ruleContext.hasErrors()) {
+        return null;
+      }
+
+      return new RuleConfiguredTargetBuilder(ruleContext)
+          .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
+          .setFilesToBuild(filesToBuild.build())
+          .addOutputGroups(groups)
+          .build();
+    } catch (GenericParsingException e) {
+      ruleContext.ruleError(e.getMessage());
+      return null;
+    }
+  }
+
+  private static boolean checkOrphanArtifacts(RuleContext ruleContext) {
+    ImmutableSet<Artifact> orphanArtifacts =
+        ruleContext.getAnalysisEnvironment().getOrphanArtifacts();
+    if (!orphanArtifacts.isEmpty()) {
+      List<String> paths =
+          orphanArtifacts.stream().map(Artifact::getExecPathString).collect(Collectors.toList());
+      ruleContext.ruleError(
+          "The following artifacts do not have a generating action in Ninja file: "
+              + String.join(", ", paths));
+      return false;
+    }
+    return true;
+  }
+
+  private static NestedSet<Artifact> getGroupArtifacts(
+      RuleContext ruleContext,
+      List<String> targets,
+      ImmutableSortedMap<PathFragment, PhonyTarget> phonyTargetsMap,
+      PhonyTargetArtifacts phonyTargetsArtifacts,
+      NinjaGraphArtifactsHelper artifactsHelper)
+      throws GenericParsingException {
+    NestedSetBuilder<Artifact> nestedSetBuilder = NestedSetBuilder.stableOrder();
+    for (String target : targets) {
+      PathFragment path = PathFragment.create(target);
+      if (phonyTargetsMap.containsKey(path)) {
+        NestedSet<Artifact> artifacts = phonyTargetsArtifacts.getPhonyTargetArtifacts(path);
+        nestedSetBuilder.addTransitive(artifacts);
+      } else {
+        Artifact usualArtifact = artifactsHelper.createOutputArtifact(path);
+        if (usualArtifact == null) {
+          ruleContext.ruleError(
+              String.format("Required target '%s' is not created in ninja_graph.", path));
+          return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+        }
+        nestedSetBuilder.add(usualArtifact);
+      }
+    }
+    return nestedSetBuilder.build();
+  }
+
+  private static ImmutableSortedMap<PathFragment, Artifact> createSrcsMap(RuleContext ruleContext) {
+    ImmutableList<Artifact> srcs = ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list();
+    ImmutableSortedMap.Builder<PathFragment, Artifact> inputsMapBuilder =
+        ImmutableSortedMap.naturalOrder();
+    srcs.forEach(a -> inputsMapBuilder.put(a.getRootRelativePath(), a));
+    return inputsMapBuilder.build();
+  }
+
+  private static void createDepsMap(
+      RuleContext ruleContext,
+      PathFragment workingDirectory,
+      ImmutableSortedMap.Builder<PathFragment, Artifact> depsMapBuilder,
+      ImmutableSortedMap.Builder<PathFragment, Artifact> symlinksMapBuilder)
+      throws InterruptedException {
+    FileProvider fileProvider =
+        ruleContext.getPrerequisite("ninja_graph", Mode.TARGET, FileProvider.class);
+    Preconditions.checkNotNull(fileProvider);
+    new NestedSetVisitor<Artifact>(
+            a -> {
+              symlinksMapBuilder.put(a.getExecPath().relativeTo(workingDirectory), a);
+            },
+            new VisitedState<>())
+        .visit(fileProvider.getFilesToBuild());
+
+    Map<String, TransitiveInfoCollection> mapping = ruleContext.getPrerequisiteMap("deps_mapping");
+    for (Map.Entry<String, TransitiveInfoCollection> entry : mapping.entrySet()) {
+      NestedSet<Artifact> filesToBuild =
+          entry.getValue().getProvider(FileProvider.class).getFilesToBuild();
+      if (!filesToBuild.isSingleton()) {
+        ruleContext.attributeError(
+            "deps_mapping",
+            String.format(
+                "'%s' contains more than one output. "
+                    + "deps_mapping should only contain targets, producing a single output file.",
+                entry.getValue().getLabel().getCanonicalForm()));
+        return;
+      }
+      depsMapBuilder.put(PathFragment.create(entry.getKey()), filesToBuild.getSingleton());
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaBuildRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaBuildRule.java
new file mode 100644
index 0000000..76e727b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaBuildRule.java
@@ -0,0 +1,83 @@
+// Copyright 2020 The Bazel 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.
+
+package com.google.devtools.build.lib.bazel.rules.ninja.actions;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.BuildType.LABEL;
+import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.BuildType;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+/**
+ * The rule creates the action subgraph from graph of {@link
+ * com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaTarget}, parsed by {@link
+ * NinjaGraphRule} and passed in the form of {@link NinjaGraphProvider}.
+ *
+ * <p>The subgraph is computed as all actions needed to build targets from 'output_groups' (phony
+ * targets can also be passed there). Bazel-built inputs should be passed with 'deps_mapping'
+ * attribute. Currently, if there are two ninja_build targets which refer to intersecting subgraphs
+ * in ninja_graph, all the actions will be created by each of ninja_build targets, i.e. duplicates.
+ * Bazel will determine that those are duplicates and only execute each action once. Future
+ * improvements are planned to avoid creation of duplicate actions, probably with the help of some
+ * coordinating registry structure.
+ */
+public class NinjaBuildRule implements RuleDefinition {
+  @Override
+  public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment environment) {
+    return builder
+        .add(
+            attr("ninja_graph", LABEL)
+                .allowedFileTypes(FileTypeSet.ANY_FILE)
+                .allowedRuleClasses("ninja_graph")
+                .setDoc("ninja_graph that parses all Ninja files that compose a graph of actions."))
+        .add(
+            attr("srcs", LABEL_LIST)
+                .allowedFileTypes(FileTypeSet.ANY_FILE)
+                .setDoc("Source files requested by Ninja graph actions."))
+        .add(
+            attr("deps_mapping", BuildType.LABEL_DICT_UNARY)
+                .allowedFileTypes(FileTypeSet.ANY_FILE)
+                .setDoc(
+                    "Mapping of paths in the Ninja file to the Bazel-built dependencies. Main"
+                        + " output of each dependency will be used as an input to the Ninja"
+                        + " action which refers to the corresponding path.")
+                .value(ImmutableMap.of()))
+        .add(
+            attr("output_groups", Type.STRING_LIST_DICT)
+                .setDoc(
+                    "Mapping of output groups to the list of output paths in the Ninja file. "
+                        + "Only the output paths mentioned in this attribute will be built."
+                        + " Phony target names may be specified as the output paths."))
+        .build();
+  }
+
+  @Override
+  public Metadata getMetadata() {
+    return RuleDefinition.Metadata.builder()
+        .name("ninja_build")
+        .type(RuleClassType.NORMAL)
+        .ancestors(BaseRuleClasses.BaseRule.class)
+        .factoryClass(NinjaBuild.class)
+        .build();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraph.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraph.java
index 1555f9d..930ba34 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraph.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraph.java
@@ -17,23 +17,21 @@
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Artifact.DerivedArtifact;
 import com.google.devtools.build.lib.actions.FileValue;
 import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
-import com.google.devtools.build.lib.analysis.FileProvider;
 import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
 import com.google.devtools.build.lib.analysis.RuleConfiguredTargetFactory;
 import com.google.devtools.build.lib.analysis.RuleContext;
 import com.google.devtools.build.lib.analysis.RunfilesProvider;
-import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
 import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
 import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
 import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaTarget;
@@ -53,8 +51,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.stream.Collectors;
 
@@ -85,8 +81,6 @@
         PathFragment.create(ruleContext.attributes().get("working_directory", Type.STRING));
     List<String> outputRootInputs =
         ruleContext.attributes().get("output_root_inputs", Type.STRING_LIST);
-    Map<String, List<String>> outputGroupsMap =
-        ruleContext.attributes().get("output_groups", Type.STRING_LIST_DICT);
 
     Environment env = ruleContext.getAnalysisEnvironment().getSkyframeEnv();
     establishDependencyOnNinjaFiles(env, mainArtifact, ninjaSrcs);
@@ -97,19 +91,14 @@
     }
 
     Root sourceRoot = mainArtifact.getRoot().getRoot();
-    List<PathFragment> pathsToBuild =
-        outputGroupsMap.values().stream()
-            .flatMap(List::stream)
-            .map(PathFragment::create)
-            .collect(Collectors.toList());
     NinjaGraphArtifactsHelper artifactsHelper =
         new NinjaGraphArtifactsHelper(
             ruleContext,
-            sourceRoot,
             outputRoot,
             workingDirectory,
-            createSrcsMap(ruleContext),
-            createDepsMap(ruleContext));
+            ImmutableSortedMap.of(),
+            ImmutableSortedMap.of(),
+            ImmutableSortedMap.of());
     if (ruleContext.hasErrors()) {
       return null;
     }
@@ -131,44 +120,25 @@
                   ownerTargetName)
               .pipeline(mainArtifact.getPath());
       targetsPreparer.process(ninjaTargets);
-      PhonyTargetArtifacts phonyTargetArtifacts =
-          new PhonyTargetArtifacts(targetsPreparer.getPhonyTargetsMap(), artifactsHelper);
-      new NinjaActionsHelper(
-              ruleContext,
-              artifactsHelper,
-              outputRootInputs,
+
+      NinjaGraphProvider ninjaGraphProvider =
+          new NinjaGraphProvider(
+              outputRoot,
+              workingDirectory,
               targetsPreparer.getUsualTargets(),
-              targetsPreparer.getPhonyTargetsMap(),
-              phonyTargetArtifacts,
-              pathsToBuild)
-          .process();
+              targetsPreparer.getPhonyTargetsMap());
 
-      if (!checkOrphanArtifacts(ruleContext)) {
-        return null;
-      }
-
-      NestedSetBuilder<Artifact> filesToBuild = NestedSetBuilder.stableOrder();
-      TreeMap<String, NestedSet<Artifact>> groups = Maps.newTreeMap();
-      for (Map.Entry<String, List<String>> entry : outputGroupsMap.entrySet()) {
-        NestedSet<Artifact> artifacts =
-            getGroupArtifacts(
-                ruleContext,
-                entry.getValue(),
-                targetsPreparer.getPhonyTargetsMap(),
-                phonyTargetArtifacts,
-                artifactsHelper);
-        groups.put(entry.getKey(), artifacts);
-        filesToBuild.addTransitive(artifacts);
-      }
-
+      NestedSet<Artifact> filesToBuild =
+          createSymlinkActions(
+              ruleContext, sourceRoot, outputRoot, outputRootInputs, artifactsHelper);
       if (ruleContext.hasErrors()) {
         return null;
       }
 
       return new RuleConfiguredTargetBuilder(ruleContext)
           .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
-          .setFilesToBuild(filesToBuild.build())
-          .addOutputGroups(groups)
+          .addProvider(NinjaGraphProvider.class, ninjaGraphProvider)
+          .setFilesToBuild(filesToBuild)
           .build();
     } catch (GenericParsingException | IOException e) {
       // IOException is possible with reading Ninja file, describing the action graph.
@@ -177,18 +147,42 @@
     }
   }
 
-  private static boolean checkOrphanArtifacts(RuleContext ruleContext) {
-    ImmutableSet<Artifact> orphanArtifacts =
-        ruleContext.getAnalysisEnvironment().getOrphanArtifacts();
-    if (!orphanArtifacts.isEmpty()) {
-      List<String> paths =
-          orphanArtifacts.stream().map(Artifact::getExecPathString).collect(Collectors.toList());
-      ruleContext.ruleError(
-          "The following artifacts do not have a generating action in Ninja file: "
-              + String.join(", ", paths));
-      return false;
+  private NestedSet<Artifact> createSymlinkActions(
+      RuleContext ruleContext,
+      Root sourceRoot,
+      PathFragment outputRootPath,
+      List<String> outputRootInputs,
+      NinjaGraphArtifactsHelper artifactsHelper)
+      throws GenericParsingException {
+    if (outputRootInputs.isEmpty()) {
+      return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
     }
-    return true;
+    NestedSetBuilder<Artifact> filesToBuild = NestedSetBuilder.stableOrder();
+    Path outputRootInSources =
+        Preconditions.checkNotNull(sourceRoot.asPath()).getRelative(outputRootPath);
+    for (String input : outputRootInputs) {
+      // output_root_inputs are relative to the output_root directory, and we should
+      // pass inside createOutputArtifact() paths, relative to working directory.
+      DerivedArtifact derivedArtifact =
+          artifactsHelper.createOutputArtifact(
+              artifactsHelper
+                  .getOutputRootPath()
+                  .getRelative(input)
+                  .relativeTo(artifactsHelper.getWorkingDirectory()));
+      filesToBuild.add(derivedArtifact);
+      // This method already expects the path relative to output_root.
+      PathFragment absolutePath =
+          outputRootInSources.getRelative(PathFragment.create(input)).asFragment();
+      SymlinkAction symlinkAction =
+          SymlinkAction.toAbsolutePath(
+              ruleContext.getActionOwner(),
+              absolutePath,
+              derivedArtifact,
+              String.format(
+                  "Symlinking %s under <execroot>/%s", input, artifactsHelper.getOutputRootPath()));
+      ruleContext.registerAction(symlinkAction);
+    }
+    return filesToBuild.build();
   }
 
   private static class TargetsPreparer {
@@ -266,60 +260,6 @@
     }
   }
 
-  private static NestedSet<Artifact> getGroupArtifacts(
-      RuleContext ruleContext,
-      List<String> targets,
-      ImmutableSortedMap<PathFragment, PhonyTarget> phonyTargetsMap,
-      PhonyTargetArtifacts phonyTargetsArtifacts,
-      NinjaGraphArtifactsHelper artifactsHelper)
-      throws GenericParsingException {
-    NestedSetBuilder<Artifact> nestedSetBuilder = NestedSetBuilder.stableOrder();
-    for (String target : targets) {
-      PathFragment path = PathFragment.create(target);
-      if (phonyTargetsMap.containsKey(path)) {
-        NestedSet<Artifact> artifacts = phonyTargetsArtifacts.getPhonyTargetArtifacts(path);
-        nestedSetBuilder.addTransitive(artifacts);
-      } else {
-        Artifact usualArtifact = artifactsHelper.createOutputArtifact(path);
-        if (usualArtifact == null) {
-          ruleContext.ruleError(
-              String.format("Required target '%s' is not created in ninja_graph.", path));
-          return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
-        }
-        nestedSetBuilder.add(usualArtifact);
-      }
-    }
-    return nestedSetBuilder.build();
-  }
-
-  private static ImmutableSortedMap<PathFragment, Artifact> createSrcsMap(RuleContext ruleContext) {
-    ImmutableList<Artifact> srcs = ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list();
-    ImmutableSortedMap.Builder<PathFragment, Artifact> inputsMapBuilder =
-        ImmutableSortedMap.naturalOrder();
-    srcs.forEach(a -> inputsMapBuilder.put(a.getRootRelativePath(), a));
-    return inputsMapBuilder.build();
-  }
-
-  private static ImmutableSortedMap<PathFragment, Artifact> createDepsMap(RuleContext ruleContext) {
-    Map<String, TransitiveInfoCollection> mapping = ruleContext.getPrerequisiteMap("deps_mapping");
-    ImmutableSortedMap.Builder<PathFragment, Artifact> builder = ImmutableSortedMap.naturalOrder();
-    for (Map.Entry<String, TransitiveInfoCollection> entry : mapping.entrySet()) {
-      NestedSet<Artifact> filesToBuild =
-          entry.getValue().getProvider(FileProvider.class).getFilesToBuild();
-      if (!filesToBuild.isSingleton()) {
-        ruleContext.attributeError(
-            "deps_mapping",
-            String.format(
-                "'%s' contains more than one output. "
-                    + "deps_mapping should only contain targets, producing a single output file.",
-                entry.getValue().getLabel().getCanonicalForm()));
-        return ImmutableSortedMap.of();
-      }
-      builder.put(PathFragment.create(entry.getKey()), filesToBuild.getSingleton());
-    }
-    return builder.build();
-  }
-
   /**
    * As Ninja files describe the action graph, we must establish the dependency between Ninja files
    * and the Ninja graph configured target for the SkyFrame. We are doing it by computing all
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraphArtifactsHelper.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraphArtifactsHelper.java
index 51a2674..4c99e69 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraphArtifactsHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraphArtifactsHelper.java
@@ -24,7 +24,6 @@
 import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
-import com.google.devtools.build.lib.vfs.Root;
 
 /**
  * Helper class to create artifacts for {@link NinjaAction} to be used from {@link NinjaGraphRule}.
@@ -37,40 +36,39 @@
  */
 class NinjaGraphArtifactsHelper {
   private final RuleContext ruleContext;
-  private final Path outputRootInSources;
   private final PathFragment outputRootPath;
   private final PathFragment workingDirectory;
   private final ArtifactRoot derivedOutputRoot;
 
   private final ImmutableSortedMap<PathFragment, Artifact> depsNameToArtifact;
+  private final ImmutableSortedMap<PathFragment, Artifact> symlinkPathToArtifact;
   private final ImmutableSortedMap<PathFragment, Artifact> srcsMap;
 
   /**
    * Constructor
    *
    * @param ruleContext parent NinjaGraphRule rule context
-   * @param sourceRoot the source root, under which the main Ninja file resides.
    * @param outputRootPath name of output directory for Ninja actions under execroot
    * @param workingDirectory relative path under execroot, the root for interpreting all paths in
    *     Ninja file
    * @param srcsMap mapping between the path fragment and artifact for the files passed in 'srcs'
    *     attribute
    * @param depsNameToArtifact mapping between the path fragment in the Ninja file and prebuilt
+   * @param symlinkPathToArtifact
    */
   NinjaGraphArtifactsHelper(
       RuleContext ruleContext,
-      Root sourceRoot,
       PathFragment outputRootPath,
       PathFragment workingDirectory,
       ImmutableSortedMap<PathFragment, Artifact> srcsMap,
-      ImmutableSortedMap<PathFragment, Artifact> depsNameToArtifact) {
+      ImmutableSortedMap<PathFragment, Artifact> depsNameToArtifact,
+      ImmutableSortedMap<PathFragment, Artifact> symlinkPathToArtifact) {
     this.ruleContext = ruleContext;
-    this.outputRootInSources =
-        Preconditions.checkNotNull(sourceRoot.asPath()).getRelative(outputRootPath);
     this.outputRootPath = outputRootPath;
     this.workingDirectory = workingDirectory;
     this.srcsMap = srcsMap;
     this.depsNameToArtifact = depsNameToArtifact;
+    this.symlinkPathToArtifact = symlinkPathToArtifact;
     Path execRoot =
         Preconditions.checkNotNull(ruleContext.getConfiguration())
             .getDirectories()
@@ -78,10 +76,6 @@
     this.derivedOutputRoot = ArtifactRoot.asDerivedRoot(execRoot, outputRootPath);
   }
 
-  PathFragment createAbsolutePathUnderOutputRoot(PathFragment pathUnderOutputRoot) {
-    return outputRootInSources.getRelative(pathUnderOutputRoot).asFragment();
-  }
-
   DerivedArtifact createOutputArtifact(PathFragment pathRelativeToWorkingDirectory)
       throws GenericParsingException {
     PathFragment pathRelativeToWorkspaceRoot =
@@ -106,6 +100,8 @@
         workingDirectory.getRelative(pathRelativeToWorkingDirectory);
     Artifact asInput = srcsMap.get(pathRelativeToWorkspaceRoot);
     Artifact depsMappingArtifact = depsNameToArtifact.get(pathRelativeToWorkingDirectory);
+    Artifact symlinkMappingArtifact = symlinkPathToArtifact.get(pathRelativeToWorkingDirectory);
+    // Symlinked artifact is by definition outside of sources, in the output directory.
     if (asInput != null && depsMappingArtifact != null) {
       throw new GenericParsingException(
           String.format(
@@ -118,9 +114,16 @@
     if (depsMappingArtifact != null) {
       return depsMappingArtifact;
     }
+    if (symlinkMappingArtifact != null) {
+      return symlinkMappingArtifact;
+    }
     return createOutputArtifact(pathRelativeToWorkingDirectory);
   }
 
+  public Artifact getDepsMappingArtifact(PathFragment fragment) {
+    return depsNameToArtifact.get(fragment);
+  }
+
   public PathFragment getOutputRootPath() {
     return outputRootPath;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraphProvider.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraphProvider.java
new file mode 100644
index 0000000..22ab3ae
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraphProvider.java
@@ -0,0 +1,60 @@
+// Copyright 2020 The Bazel 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.
+
+package com.google.devtools.build.lib.bazel.rules.ninja.actions;
+
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaTarget;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Provider for passing information between {@link NinjaGraphRule} and {@link NinjaBuildRule}.
+ * Represents all usual and phony {@link NinjaTarget}s from the Ninja graph.
+ */
+@Immutable
+public final class NinjaGraphProvider implements TransitiveInfoProvider {
+  private final PathFragment outputRoot;
+  private final PathFragment workingDirectory;
+  private final ImmutableSortedMap<PathFragment, NinjaTarget> usualTargets;
+  private final ImmutableSortedMap<PathFragment, PhonyTarget> phonyTargetsMap;
+
+  public NinjaGraphProvider(
+      PathFragment outputRoot,
+      PathFragment workingDirectory,
+      ImmutableSortedMap<PathFragment, NinjaTarget> usualTargets,
+      ImmutableSortedMap<PathFragment, PhonyTarget> phonyTargetsMap) {
+    this.outputRoot = outputRoot;
+    this.workingDirectory = workingDirectory;
+    this.usualTargets = usualTargets;
+    this.phonyTargetsMap = phonyTargetsMap;
+  }
+
+  public PathFragment getOutputRoot() {
+    return outputRoot;
+  }
+
+  public PathFragment getWorkingDirectory() {
+    return workingDirectory;
+  }
+
+  public ImmutableSortedMap<PathFragment, NinjaTarget> getUsualTargets() {
+    return usualTargets;
+  }
+
+  public ImmutableSortedMap<PathFragment, PhonyTarget> getPhonyTargetsMap() {
+    return phonyTargetsMap;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraphRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraphRule.java
index fa25d43..b95fac2 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraphRule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaGraphRule.java
@@ -21,20 +21,24 @@
 import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.analysis.BaseRuleClasses;
 import com.google.devtools.build.lib.analysis.RuleDefinition;
 import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
-import com.google.devtools.build.lib.packages.BuildType;
 import com.google.devtools.build.lib.packages.RuleClass;
 import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
-import com.google.devtools.build.lib.packages.Type;
 import com.google.devtools.build.lib.syntax.Sequence;
 import com.google.devtools.build.lib.syntax.StarlarkThread;
 import com.google.devtools.build.lib.util.FileTypeSet;
 
 /**
- * The rule that parses the Ninja graph and creates {@link NinjaAction} actions.
+ * The rule that parses the Ninja graph and symlinks inputs into output_root.
+ *
+ * <p>The rule exposes {@link NinjaGraphProvider} with maps of usual and phony {@link
+ * com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaTarget} for {@link NinjaBuildRule} to
+ * use for action creation.
+ *
+ * <p>The rules establishes Skyframe dependency on input Ninja files, as each time they change, the
+ * action graph changes.
  *
  * <p>Important aspect is relation to non-symlinked-under-execroot-directories: {@link
  * com.google.devtools.build.lib.skylarkbuildapi.WorkspaceGlobalsApi#dontSymlinkDirectoriesInExecroot(Sequence,
@@ -52,10 +56,6 @@
                 .allowedFileTypes(FileTypeSet.ANY_FILE)
                 .setDoc("All included or subninja Ninja files describing the action graph."))
         .add(
-            attr("srcs", LABEL_LIST)
-                .allowedFileTypes(FileTypeSet.ANY_FILE)
-                .setDoc("Source files requested by Ninja graph actions."))
-        .add(
             attr("main", LABEL)
                 .allowedFileTypes(FileTypeSet.ANY_FILE)
                 .mandatory()
@@ -84,20 +84,6 @@
                     "Directory under workspace's exec root to be the root for relative paths and "
                         + "working directory for all Ninja actions. "
                         + "Must be empty or set to the value or output_root."))
-        .add(
-            attr("deps_mapping", BuildType.LABEL_DICT_UNARY)
-                .allowedFileTypes(FileTypeSet.ANY_FILE)
-                .setDoc(
-                    "Mapping of paths in the Ninja file to the Bazel-built dependencies. Main"
-                        + " output of each dependency will be used as an input to the Ninja"
-                        + " action,which refers to the corresponding path.")
-                .value(ImmutableMap.of()))
-        .add(
-            attr("output_groups", Type.STRING_LIST_DICT)
-                .setDoc(
-                    "Mapping of output groups to the list of output paths in the Ninja file. "
-                        + "Only the output paths mentioned in this attribute will be built."
-                        + " Phony target names may be specified as the output paths."))
         .build();
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaRulesModule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaRulesModule.java
index e688452..4e48b20 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaRulesModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/actions/NinjaRulesModule.java
@@ -22,5 +22,6 @@
   @Override
   public void initializeRuleClasses(ConfiguredRuleClassProvider.Builder builder) {
     builder.addRuleDefinition(new NinjaGraphRule());
+    builder.addRuleDefinition(new NinjaBuildRule());
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/AutoRegistry.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/AutoRegistry.java
index 5824bfa..a63c212 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/AutoRegistry.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/AutoRegistry.java
@@ -43,6 +43,7 @@
       ImmutableList.of(
           "com.google.devtools.build.lib.google",
           "com.google.devtools.build.lib.vfs",
+          "com.google.devtools.build.lib.bazel.rules.ninja",
           "com.google.devtools.build.lib.actions.ArtifactFactory",
           "com.google.devtools.build.lib.packages.PackageFactory$BuiltInRuleFunction",
           "com.google.devtools.build.skyframe.SkyFunctionEnvironment");
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaBuildTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaBuildTest.java
new file mode 100644
index 0000000..666f709
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaBuildTest.java
@@ -0,0 +1,330 @@
+// Copyright 2020 The Bazel 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.
+
+package com.google.devtools.build.lib.bazel.rules.ninja;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.CommandLines.CommandLineAndParamFileInfo;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.bazel.rules.ninja.actions.NinjaAction;
+import com.google.devtools.build.lib.bazel.rules.ninja.actions.NinjaBuildRule;
+import com.google.devtools.build.lib.bazel.rules.ninja.actions.NinjaGraphRule;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link NinjaBuild} configured target factory. */
+@RunWith(JUnit4.class)
+public class NinjaBuildTest extends BuildViewTestCase {
+
+  @Override
+  protected ConfiguredRuleClassProvider getRuleClassProvider() {
+    ConfiguredRuleClassProvider.Builder builder = new ConfiguredRuleClassProvider.Builder();
+    TestRuleClassProvider.addStandardRules(builder);
+    builder.addRuleDefinition(new NinjaGraphRule());
+    builder.addRuleDefinition(new NinjaBuildRule());
+    return builder.build();
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    setSkylarkSemanticsOptions("--experimental_ninja_actions");
+  }
+
+  @Test
+  public void testNinjaBuildRule() throws Exception {
+    rewriteWorkspace(
+        "workspace(name = 'test')",
+        "dont_symlink_directories_in_execroot(paths = ['build_config'])");
+
+    scratch.file("build_config/input.txt", "World");
+    scratch.file(
+        "build_config/build.ninja",
+        "rule echo",
+        "  command = echo \"Hello $$(cat ${in})!\" > ${out}",
+        "build build_config/hello.txt: echo build_config/input.txt");
+
+    // Working directory is workspace root.
+    ConfiguredTarget configuredTarget =
+        scratchConfiguredTarget(
+            "",
+            "ninja_target",
+            "ninja_graph(name = 'graph', output_root = 'build_config',",
+            " main = 'build_config/build.ninja',",
+            " output_root_inputs = ['input.txt'])",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
+            " output_groups= {'main': ['build_config/hello.txt']})");
+    assertThat(configuredTarget).isInstanceOf(RuleConfiguredTarget.class);
+    RuleConfiguredTarget ninjaConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
+    ImmutableList<ActionAnalysisMetadata> actions = ninjaConfiguredTarget.getActions();
+    assertThat(actions).hasSize(1);
+    ActionAnalysisMetadata action = Iterables.getOnlyElement(actions);
+    assertThat(action).isInstanceOf(NinjaAction.class);
+    NinjaAction ninjaAction = (NinjaAction) action;
+    List<CommandLineAndParamFileInfo> commandLines =
+        ninjaAction.getCommandLines().getCommandLines();
+    assertThat(commandLines).hasSize(1);
+    assertThat(commandLines.get(0).commandLine.toString())
+        .endsWith("echo \"Hello $(cat build_config/input.txt)!\" > build_config/hello.txt");
+    assertThat(ninjaAction.getPrimaryInput().getExecPathString())
+        .isEqualTo("build_config/input.txt");
+    assertThat(ninjaAction.getPrimaryOutput().getExecPathString())
+        .isEqualTo("build_config/hello.txt");
+  }
+
+  @Test
+  public void testNinjaGraphRuleWithPhonyTarget() throws Exception {
+    rewriteWorkspace(
+        "workspace(name = 'test')",
+        "dont_symlink_directories_in_execroot(paths = ['build_config'])");
+
+    scratch.file("build_config/input.txt", "World");
+    scratch.file(
+        "build_config/build.ninja",
+        "rule echo",
+        "  command = echo \"Hello $$(cat ${in})!\" > ${out}",
+        "build hello.txt: echo input.txt",
+        "build alias: phony hello.txt");
+
+    ConfiguredTarget configuredTarget =
+        scratchConfiguredTarget(
+            "",
+            "ninja_target",
+            "ninja_graph(name = 'graph', output_root = 'build_config',",
+            " working_directory = 'build_config',",
+            " main = 'build_config/build.ninja',",
+            " output_root_inputs = ['input.txt'])",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
+            " output_groups= {'main': ['alias']})");
+    assertThat(configuredTarget).isInstanceOf(RuleConfiguredTarget.class);
+    RuleConfiguredTarget ninjaConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
+    ImmutableList<ActionAnalysisMetadata> actions = ninjaConfiguredTarget.getActions();
+    ActionAnalysisMetadata action = Iterables.getOnlyElement(actions);
+
+    assertThat(actions).hasSize(1);
+    assertThat(action).isInstanceOf(NinjaAction.class);
+    NinjaAction ninjaAction = (NinjaAction) action;
+    List<CommandLineAndParamFileInfo> commandLines =
+        ninjaAction.getCommandLines().getCommandLines();
+    assertThat(commandLines).hasSize(1);
+    assertThat(commandLines.get(0).commandLine.toString())
+        .endsWith("cd build_config && echo \"Hello $(cat input.txt)!\" > hello.txt");
+    assertThat(ninjaAction.getPrimaryInput().getExecPathString())
+        .isEqualTo("build_config/input.txt");
+    assertThat(ninjaAction.getPrimaryOutput().getExecPathString())
+        .isEqualTo("build_config/hello.txt");
+  }
+
+  @Test
+  public void testNinjaGraphRuleWithPhonyTree() throws Exception {
+    rewriteWorkspace(
+        "workspace(name = 'test')",
+        "dont_symlink_directories_in_execroot(paths = ['build_config'])");
+
+    scratch.file("build_config/a.txt", "A");
+    scratch.file("build_config/b.txt", "B");
+    scratch.file("build_config/c.txt", "C");
+    scratch.file("build_config/d.txt", "D");
+    scratch.file("build_config/e.txt", "E");
+
+    scratch.file(
+        "build_config/build.ninja",
+        "rule cat",
+        "  command = cat ${in} > ${out}",
+        "rule echo",
+        "  command = echo \"Hello $$(cat ${in} | tr '\\r\\n' ' ')!\" > ${out}",
+        "build a: cat a.txt",
+        "build b: cat b.txt",
+        "build c: cat c.txt",
+        "build d: cat d.txt",
+        // e should be executed unconditionally as it depends on always-dirty phony action
+        "build e: cat e.txt always_dirty",
+        "build always_dirty: phony",
+        "build group1: phony a b c",
+        "build group2: phony d e",
+        "build inputs_alias: phony group1 group2",
+        "build hello.txt: echo inputs_alias",
+        "build alias: phony hello.txt");
+
+    ConfiguredTarget configuredTarget =
+        scratchConfiguredTarget(
+            "",
+            "ninja_target",
+            "ninja_graph(name = 'graph', output_root = 'build_config',",
+            " working_directory = 'build_config',",
+            " main = 'build_config/build.ninja',",
+            " output_root_inputs = ['a.txt', 'b.txt', 'c.txt', 'd.txt', 'e.txt'])",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
+            " output_groups= {'main': ['alias']})");
+    assertThat(configuredTarget).isInstanceOf(RuleConfiguredTarget.class);
+    RuleConfiguredTarget ninjaConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
+    ImmutableList<ActionAnalysisMetadata> actions = ninjaConfiguredTarget.getActions();
+    assertThat(actions).hasSize(6);
+    List<String> outputs = Lists.newArrayList();
+    actions.forEach(a -> outputs.add(Iterables.getOnlyElement(a.getOutputs()).getExecPathString()));
+    assertThat(outputs)
+        .containsExactlyElementsIn(
+            new String[] {
+              "build_config/hello.txt",
+              "build_config/a",
+              "build_config/b",
+              "build_config/c",
+              "build_config/d",
+              "build_config/e"
+            });
+
+    for (ActionAnalysisMetadata action : actions) {
+      Artifact artifact = action.getPrimaryOutput();
+      if ("hello.txt".equals(artifact.getFilename())) {
+        assertThat(action).isInstanceOf(NinjaAction.class);
+        NinjaAction ninjaAction = (NinjaAction) action;
+        List<CommandLineAndParamFileInfo> commandLines =
+            ninjaAction.getCommandLines().getCommandLines();
+        assertThat(commandLines).hasSize(1);
+        assertThat(commandLines.get(0).commandLine.toString())
+            .contains(
+                "cd build_config && echo \"Hello $(cat inputs_alias | tr '\\r\\n' ' ')!\""
+                    + " > hello.txt");
+        List<String> inputPaths =
+            ninjaAction.getInputs().toList().stream()
+                .map(Artifact::getExecPathString)
+                .collect(Collectors.toList());
+        assertThat(inputPaths)
+            .containsExactly(
+                "build_config/a",
+                "build_config/b",
+                "build_config/c",
+                "build_config/d",
+                "build_config/e");
+        assertThat(ninjaAction.getPrimaryOutput().getExecPathString())
+            .isEqualTo("build_config/hello.txt");
+      } else if ("e".equals(artifact.getFilename())) {
+        assertThat(action).isInstanceOf(NinjaAction.class);
+        NinjaAction ninjaAction = (NinjaAction) action;
+        List<CommandLineAndParamFileInfo> commandLines =
+            ninjaAction.getCommandLines().getCommandLines();
+        assertThat(commandLines).hasSize(1);
+        assertThat(commandLines.get(0).commandLine.toString())
+            .endsWith("cd build_config && cat e.txt always_dirty > e");
+        assertThat(ninjaAction.executeUnconditionally()).isTrue();
+      }
+    }
+  }
+
+  @Test
+  public void testDepsMapping() throws Exception {
+    rewriteWorkspace(
+        "workspace(name = 'test')",
+        "dont_symlink_directories_in_execroot(paths = ['build_config'])");
+
+    scratch.file("input.txt", "World");
+    scratch.file(
+        "build_config/build.ninja",
+        "rule echo",
+        "  command = echo \"Hello $$(cat ${in})!\" > ${out}",
+        "build hello.txt: echo placeholder");
+
+    ConfiguredTarget configuredTarget =
+        scratchConfiguredTarget(
+            "",
+            "ninja_target",
+            "ninja_graph(name = 'graph', output_root = 'build_config',",
+            " working_directory = 'build_config',",
+            " main = 'build_config/build.ninja')",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
+            " output_groups= {'main': ['hello.txt']},",
+            " deps_mapping = {'placeholder': ':input.txt'})");
+    assertThat(configuredTarget).isInstanceOf(RuleConfiguredTarget.class);
+    RuleConfiguredTarget ninjaConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
+    ImmutableList<ActionAnalysisMetadata> actions = ninjaConfiguredTarget.getActions();
+    assertThat(actions).hasSize(1);
+
+    ActionAnalysisMetadata action = Iterables.getOnlyElement(actions);
+    assertThat(action).isInstanceOf(NinjaAction.class);
+    NinjaAction ninjaAction = (NinjaAction) action;
+    List<CommandLineAndParamFileInfo> commandLines =
+        ninjaAction.getCommandLines().getCommandLines();
+    assertThat(commandLines).hasSize(1);
+    assertThat(commandLines.get(0).commandLine.toString())
+        .endsWith("cd build_config && echo \"Hello $(cat /workspace/input.txt)!\" > hello.txt");
+    assertThat(ninjaAction.getPrimaryInput().getExecPathString()).isEqualTo("input.txt");
+    assertThat(ninjaAction.getPrimaryOutput().getExecPathString())
+        .isEqualTo("build_config/hello.txt");
+  }
+
+  @Test
+  public void testOnlySubGraphIsCreated() throws Exception {
+    rewriteWorkspace(
+        "workspace(name = 'test')",
+        "dont_symlink_directories_in_execroot(paths = ['build_config'])");
+
+    scratch.file("build_config/a.txt", "A");
+    scratch.file("build_config/b.txt", "B");
+    scratch.file("build_config/c.txt", "C");
+    scratch.file("build_config/d.txt", "D");
+    scratch.file("build_config/e.txt", "E");
+
+    scratch.file(
+        "build_config/build.ninja",
+        "rule cat",
+        "  command = cat ${in} > ${out}",
+        "rule echo",
+        "  command = echo \"Hello $$(cat ${in} | tr '\\r\\n' ' ')!\" > ${out}",
+        "build a: cat a.txt",
+        "build b: cat b.txt",
+        "build c: cat c.txt",
+        "build d: cat d.txt",
+        "build e: cat e.txt",
+        "build group1: phony a b c",
+        "build group2: phony d e",
+        "build inputs_alias: phony group1 group2",
+        "build hello.txt: echo inputs_alias",
+        "build alias: phony hello.txt");
+
+    ConfiguredTarget configuredTarget =
+        scratchConfiguredTarget(
+            "",
+            "ninja_target",
+            "ninja_graph(name = 'graph', output_root = 'build_config',",
+            " working_directory = 'build_config',",
+            " main = 'build_config/build.ninja',",
+            " output_root_inputs = ['a.txt', 'b.txt', 'c.txt', 'd.txt', 'e.txt'])",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
+            " output_groups= {'main': ['group1']})");
+    assertThat(configuredTarget).isInstanceOf(RuleConfiguredTarget.class);
+    RuleConfiguredTarget ninjaConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
+    ImmutableList<ActionAnalysisMetadata> actions = ninjaConfiguredTarget.getActions();
+    assertThat(actions).hasSize(3);
+    List<String> outputs = Lists.newArrayList();
+    actions.forEach(a -> outputs.add(Iterables.getOnlyElement(a.getOutputs()).getExecPathString()));
+    assertThat(outputs)
+        .containsExactlyElementsIn(
+            new String[] {
+              "build_config/a", "build_config/b", "build_config/c",
+            });
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaGraphTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaGraphTest.java
index f4c805e..1336785 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaGraphTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaGraphTest.java
@@ -20,19 +20,18 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
-import com.google.devtools.build.lib.actions.Artifact;
-import com.google.devtools.build.lib.actions.CommandLines.CommandLineAndParamFileInfo;
 import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
 import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
 import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
-import com.google.devtools.build.lib.bazel.rules.ninja.actions.NinjaAction;
+import com.google.devtools.build.lib.bazel.rules.ninja.actions.NinjaGraphProvider;
 import com.google.devtools.build.lib.bazel.rules.ninja.actions.NinjaGraphRule;
+import com.google.devtools.build.lib.bazel.rules.ninja.actions.PhonyTarget;
+import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaTarget;
 import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.util.List;
-import java.util.stream.Collectors;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -75,37 +74,34 @@
             "graph",
             "ninja_graph(name = 'graph', output_root = 'build_config',",
             " main = 'build_config/build.ninja',",
-            " output_root_inputs = ['input.txt'],",
-            " output_groups= {'main': ['build_config/hello.txt']})");
+            " output_root_inputs = ['input.txt'])");
     assertThat(configuredTarget).isInstanceOf(RuleConfiguredTarget.class);
     RuleConfiguredTarget ninjaConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
     ImmutableList<ActionAnalysisMetadata> actions = ninjaConfiguredTarget.getActions();
-    assertThat(actions).hasSize(2);
+    assertThat(actions).hasSize(1);
 
-    for (ActionAnalysisMetadata action : actions) {
-      Artifact artifact = action.getPrimaryOutput();
-      if ("hello.txt".equals(artifact.getFilename())) {
-        assertThat(action).isInstanceOf(NinjaAction.class);
-        NinjaAction ninjaAction = (NinjaAction) action;
-        List<CommandLineAndParamFileInfo> commandLines =
-            ninjaAction.getCommandLines().getCommandLines();
-        assertThat(commandLines).hasSize(1);
-        assertThat(commandLines.get(0).commandLine.toString())
-            .endsWith("echo \"Hello $(cat build_config/input.txt)!\" > build_config/hello.txt");
-        assertThat(ninjaAction.getPrimaryInput().getExecPathString())
-            .isEqualTo("build_config/input.txt");
-        assertThat(ninjaAction.getPrimaryOutput().getExecPathString())
-            .isEqualTo("build_config/hello.txt");
-      } else {
-        assertThat(action).isInstanceOf(SymlinkAction.class);
-        SymlinkAction symlinkAction = (SymlinkAction) action;
-        assertThat(symlinkAction.executeUnconditionally()).isTrue();
-        assertThat(symlinkAction.getInputPath())
-            .isEqualTo(PathFragment.create("/workspace/build_config/input.txt"));
-        assertThat(symlinkAction.getPrimaryOutput().getExecPathString())
-            .isEqualTo("build_config/input.txt");
-      }
-    }
+    ActionAnalysisMetadata action = Iterables.getOnlyElement(actions);
+    assertThat(action).isInstanceOf(SymlinkAction.class);
+    SymlinkAction symlinkAction = (SymlinkAction) action;
+    assertThat(symlinkAction.executeUnconditionally()).isTrue();
+    assertThat(symlinkAction.getInputPath())
+        .isEqualTo(PathFragment.create("/workspace/build_config/input.txt"));
+    assertThat(symlinkAction.getPrimaryOutput().getExecPathString())
+        .isEqualTo("build_config/input.txt");
+
+    NinjaGraphProvider provider = configuredTarget.getProvider(NinjaGraphProvider.class);
+    assertThat(provider).isNotNull();
+    assertThat(provider.getOutputRoot()).isEqualTo(PathFragment.create("build_config"));
+    assertThat(provider.getWorkingDirectory()).isEqualTo(PathFragment.EMPTY_FRAGMENT);
+    assertThat(provider.getPhonyTargetsMap()).isEmpty();
+    assertThat(provider.getUsualTargets()).hasSize(1);
+
+    NinjaTarget target = Iterables.getOnlyElement(provider.getUsualTargets().values());
+    assertThat(target.getRuleName()).isEqualTo("echo");
+    assertThat(target.getAllInputs())
+        .containsExactly(PathFragment.create("build_config/input.txt"));
+    assertThat(target.getAllOutputs())
+        .containsExactly(PathFragment.create("build_config/hello.txt"));
   }
 
   @Test
@@ -114,7 +110,8 @@
         "workspace(name = 'test')",
         "dont_symlink_directories_in_execroot(paths = ['build_config'])");
 
-    scratch.file("build_config/input.txt", "World");
+    // We do not have to have the real files in place, the rule only reads
+    // the contents of Ninja files.
     scratch.file(
         "build_config/build.ninja",
         "rule echo",
@@ -129,36 +126,40 @@
             "ninja_graph(name = 'graph', output_root = 'build_config',",
             " working_directory = 'build_config',",
             " main = 'build_config/build.ninja',",
-            " output_root_inputs = ['input.txt'], output_groups= {'main': ['alias']})");
+            " output_root_inputs = ['input.txt'])");
     assertThat(configuredTarget).isInstanceOf(RuleConfiguredTarget.class);
     RuleConfiguredTarget ninjaConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
     ImmutableList<ActionAnalysisMetadata> actions = ninjaConfiguredTarget.getActions();
-    assertThat(actions).hasSize(2);
+    assertThat(configuredTarget).isInstanceOf(RuleConfiguredTarget.class);
 
-    for (ActionAnalysisMetadata action : actions) {
-      Artifact artifact = action.getPrimaryOutput();
-      if ("hello.txt".equals(artifact.getFilename())) {
-        assertThat(action).isInstanceOf(NinjaAction.class);
-        NinjaAction ninjaAction = (NinjaAction) action;
-        List<CommandLineAndParamFileInfo> commandLines =
-            ninjaAction.getCommandLines().getCommandLines();
-        assertThat(commandLines).hasSize(1);
-        assertThat(commandLines.get(0).commandLine.toString())
-            .endsWith("cd build_config && echo \"Hello $(cat input.txt)!\" > hello.txt");
-        assertThat(ninjaAction.getPrimaryInput().getExecPathString())
-            .isEqualTo("build_config/input.txt");
-        assertThat(ninjaAction.getPrimaryOutput().getExecPathString())
-            .isEqualTo("build_config/hello.txt");
-      } else {
-        assertThat(action).isInstanceOf(SymlinkAction.class);
-        SymlinkAction symlinkAction = (SymlinkAction) action;
-        assertThat(symlinkAction.executeUnconditionally()).isTrue();
-        assertThat(symlinkAction.getInputPath())
-            .isEqualTo(PathFragment.create("/workspace/build_config/input.txt"));
-        assertThat(symlinkAction.getPrimaryOutput().getExecPathString())
-            .isEqualTo("build_config/input.txt");
-      }
-    }
+    assertThat(actions).hasSize(1);
+    ActionAnalysisMetadata action = Iterables.getOnlyElement(actions);
+    assertThat(action).isInstanceOf(SymlinkAction.class);
+    SymlinkAction symlinkAction = (SymlinkAction) action;
+    assertThat(symlinkAction.executeUnconditionally()).isTrue();
+    assertThat(symlinkAction.getInputPath())
+        .isEqualTo(PathFragment.create("/workspace/build_config/input.txt"));
+    assertThat(symlinkAction.getPrimaryOutput().getExecPathString())
+        .isEqualTo("build_config/input.txt");
+
+    NinjaGraphProvider provider = configuredTarget.getProvider(NinjaGraphProvider.class);
+    assertThat(provider).isNotNull();
+    assertThat(provider.getOutputRoot()).isEqualTo(PathFragment.create("build_config"));
+    assertThat(provider.getWorkingDirectory()).isEqualTo(PathFragment.create("build_config"));
+    assertThat(provider.getUsualTargets()).hasSize(1);
+
+    NinjaTarget target = Iterables.getOnlyElement(provider.getUsualTargets().values());
+    assertThat(target.getRuleName()).isEqualTo("echo");
+    assertThat(target.getAllInputs()).containsExactly(PathFragment.create("input.txt"));
+    assertThat(target.getAllOutputs()).containsExactly(PathFragment.create("hello.txt"));
+
+    PathFragment alias = PathFragment.create("alias");
+    assertThat(provider.getPhonyTargetsMap().keySet()).containsExactly(alias);
+    PhonyTarget phonyTarget = provider.getPhonyTargetsMap().get(alias);
+    assertThat(phonyTarget.isAlwaysDirty()).isFalse();
+    assertThat(phonyTarget.getPhonyNames()).isEmpty();
+    assertThat(phonyTarget.getDirectUsualInputs())
+        .containsExactly(PathFragment.create("hello.txt"));
   }
 
   @Test
@@ -167,12 +168,8 @@
         "workspace(name = 'test')",
         "dont_symlink_directories_in_execroot(paths = ['build_config'])");
 
-    scratch.file("build_config/a.txt", "A");
-    scratch.file("build_config/b.txt", "B");
-    scratch.file("build_config/c.txt", "C");
-    scratch.file("build_config/d.txt", "D");
-    scratch.file("build_config/e.txt", "E");
-
+    // We do not have to have the real files in place, the rule only reads
+    // the contents of Ninja files.
     scratch.file(
         "build_config/build.ninja",
         "rule cat",
@@ -199,174 +196,33 @@
             "ninja_graph(name = 'graph', output_root = 'build_config',",
             " working_directory = 'build_config',",
             " main = 'build_config/build.ninja',",
-            " output_root_inputs = ['a.txt', 'b.txt', 'c.txt', 'd.txt', 'e.txt'],",
-            " output_groups= {'main': ['alias']})");
+            " output_root_inputs = ['a.txt', 'b.txt', 'c.txt', 'd.txt', 'e.txt'])");
     assertThat(configuredTarget).isInstanceOf(RuleConfiguredTarget.class);
     RuleConfiguredTarget ninjaConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
     ImmutableList<ActionAnalysisMetadata> actions = ninjaConfiguredTarget.getActions();
-    assertThat(actions).hasSize(11);
+    assertThat(actions).hasSize(5);
     List<String> outputs = Lists.newArrayList();
     actions.forEach(a -> outputs.add(Iterables.getOnlyElement(a.getOutputs()).getExecPathString()));
     assertThat(outputs)
         .containsExactlyElementsIn(
             new String[] {
-              "build_config/hello.txt",
               "build_config/a.txt",
               "build_config/b.txt",
               "build_config/c.txt",
               "build_config/d.txt",
-              "build_config/e.txt",
-              "build_config/a",
-              "build_config/b",
-              "build_config/c",
-              "build_config/d",
-              "build_config/e"
+              "build_config/e.txt"
             });
 
     for (ActionAnalysisMetadata action : actions) {
-      Artifact artifact = action.getPrimaryOutput();
-      if ("hello.txt".equals(artifact.getFilename())) {
-        assertThat(action).isInstanceOf(NinjaAction.class);
-        NinjaAction ninjaAction = (NinjaAction) action;
-        List<CommandLineAndParamFileInfo> commandLines =
-            ninjaAction.getCommandLines().getCommandLines();
-        assertThat(commandLines).hasSize(1);
-        assertThat(commandLines.get(0).commandLine.toString())
-            .contains(
-                "cd build_config && echo \"Hello $(cat inputs_alias | tr '\\r\\n' ' ')!\""
-                    + " > hello.txt");
-        List<String> inputPaths =
-            ninjaAction.getInputs().toList().stream()
-                .map(Artifact::getExecPathString)
-                .collect(Collectors.toList());
-        assertThat(inputPaths)
-            .containsExactly(
-                "build_config/a",
-                "build_config/b",
-                "build_config/c",
-                "build_config/d",
-                "build_config/e");
-        assertThat(ninjaAction.getPrimaryOutput().getExecPathString())
-            .isEqualTo("build_config/hello.txt");
-      } else if (artifact.getFilename().endsWith(".txt")) {
-        assertThat(action).isInstanceOf(SymlinkAction.class);
-        SymlinkAction symlinkAction = (SymlinkAction) action;
-        assertThat(symlinkAction.executeUnconditionally()).isTrue();
-        assertThat(symlinkAction.getInputPath().getParentDirectory())
-            .isEqualTo(PathFragment.create("/workspace/build_config"));
-        assertThat(symlinkAction.getInputPath().getFileExtension()).isEqualTo("txt");
-        PathFragment execRootPath = symlinkAction.getPrimaryOutput().getExecPath();
-        assertThat(execRootPath.getParentDirectory())
-            .isEqualTo(PathFragment.create("build_config"));
-        assertThat(execRootPath.getFileExtension()).isEqualTo("txt");
-      } else if ("e".equals(artifact.getFilename())) {
-        assertThat(action).isInstanceOf(NinjaAction.class);
-        NinjaAction ninjaAction = (NinjaAction) action;
-        List<CommandLineAndParamFileInfo> commandLines =
-            ninjaAction.getCommandLines().getCommandLines();
-        assertThat(commandLines).hasSize(1);
-        assertThat(commandLines.get(0).commandLine.toString())
-            .endsWith("cd build_config && cat e.txt always_dirty > e");
-        assertThat(ninjaAction.executeUnconditionally()).isTrue();
-      }
+      assertThat(action).isInstanceOf(SymlinkAction.class);
+      SymlinkAction symlinkAction = (SymlinkAction) action;
+      assertThat(symlinkAction.executeUnconditionally()).isTrue();
+      assertThat(symlinkAction.getInputPath().getParentDirectory())
+          .isEqualTo(PathFragment.create("/workspace/build_config"));
+      assertThat(symlinkAction.getInputPath().getFileExtension()).isEqualTo("txt");
+      PathFragment execRootPath = symlinkAction.getPrimaryOutput().getExecPath();
+      assertThat(execRootPath.getParentDirectory()).isEqualTo(PathFragment.create("build_config"));
+      assertThat(execRootPath.getFileExtension()).isEqualTo("txt");
     }
   }
-
-  @Test
-  public void testDepsMapping() throws Exception {
-    rewriteWorkspace(
-        "workspace(name = 'test')",
-        "dont_symlink_directories_in_execroot(paths = ['build_config'])");
-
-    scratch.file("input.txt", "World");
-    scratch.file(
-        "build_config/build.ninja",
-        "rule echo",
-        "  command = echo \"Hello $$(cat ${in})!\" > ${out}",
-        "build hello.txt: echo placeholder");
-
-    ConfiguredTarget configuredTarget =
-        scratchConfiguredTarget(
-            "",
-            "graph",
-            "ninja_graph(name = 'graph', output_root = 'build_config',",
-            " working_directory = 'build_config',",
-            " main = 'build_config/build.ninja',",
-            " deps_mapping = {'placeholder': ':input.txt'},",
-            " output_groups= {'main': ['hello.txt']})");
-    assertThat(configuredTarget).isInstanceOf(RuleConfiguredTarget.class);
-    RuleConfiguredTarget ninjaConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
-    ImmutableList<ActionAnalysisMetadata> actions = ninjaConfiguredTarget.getActions();
-    assertThat(actions).hasSize(1);
-
-    ActionAnalysisMetadata action = Iterables.getOnlyElement(actions);
-    assertThat(action).isInstanceOf(NinjaAction.class);
-    NinjaAction ninjaAction = (NinjaAction) action;
-    List<CommandLineAndParamFileInfo> commandLines =
-        ninjaAction.getCommandLines().getCommandLines();
-    assertThat(commandLines).hasSize(1);
-    assertThat(commandLines.get(0).commandLine.toString())
-        .endsWith("cd build_config && echo \"Hello $(cat placeholder)!\" > hello.txt");
-    assertThat(ninjaAction.getPrimaryInput().getExecPathString()).isEqualTo("input.txt");
-    assertThat(ninjaAction.getPrimaryOutput().getExecPathString())
-        .isEqualTo("build_config/hello.txt");
-  }
-
-  @Test
-  public void testOnlySubGraphIsCreated() throws Exception {
-    rewriteWorkspace(
-        "workspace(name = 'test')",
-        "dont_symlink_directories_in_execroot(paths = ['build_config'])");
-
-    scratch.file("build_config/a.txt", "A");
-    scratch.file("build_config/b.txt", "B");
-    scratch.file("build_config/c.txt", "C");
-    scratch.file("build_config/d.txt", "D");
-    scratch.file("build_config/e.txt", "E");
-
-    scratch.file(
-        "build_config/build.ninja",
-        "rule cat",
-        "  command = cat ${in} > ${out}",
-        "rule echo",
-        "  command = echo \"Hello $$(cat ${in} | tr '\\r\\n' ' ')!\" > ${out}",
-        "build a: cat a.txt",
-        "build b: cat b.txt",
-        "build c: cat c.txt",
-        "build d: cat d.txt",
-        "build e: cat e.txt",
-        "build group1: phony a b c",
-        "build group2: phony d e",
-        "build inputs_alias: phony group1 group2",
-        "build hello.txt: echo inputs_alias",
-        "build alias: phony hello.txt");
-
-    ConfiguredTarget configuredTarget =
-        scratchConfiguredTarget(
-            "",
-            "graph",
-            "ninja_graph(name = 'graph', output_root = 'build_config',",
-            " working_directory = 'build_config',",
-            " main = 'build_config/build.ninja',",
-            " output_root_inputs = ['a.txt', 'b.txt', 'c.txt', 'd.txt', 'e.txt'],",
-            " output_groups= {'main': ['group1']})");
-    assertThat(configuredTarget).isInstanceOf(RuleConfiguredTarget.class);
-    RuleConfiguredTarget ninjaConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
-    ImmutableList<ActionAnalysisMetadata> actions = ninjaConfiguredTarget.getActions();
-    assertThat(actions).hasSize(8);
-    List<String> outputs = Lists.newArrayList();
-    actions.forEach(a -> outputs.add(Iterables.getOnlyElement(a.getOutputs()).getExecPathString()));
-    assertThat(outputs)
-        .containsExactlyElementsIn(
-            new String[] {
-              "build_config/a.txt",
-              "build_config/b.txt",
-              "build_config/c.txt",
-              "build_config/d.txt",
-              "build_config/e.txt",
-              "build_config/a",
-              "build_config/b",
-              "build_config/c",
-            });
-  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/blackbox/tests/NinjaBlackBoxTest.java b/src/test/java/com/google/devtools/build/lib/blackbox/tests/NinjaBlackBoxTest.java
index 59d38d6..dbfd912 100644
--- a/src/test/java/com/google/devtools/build/lib/blackbox/tests/NinjaBlackBoxTest.java
+++ b/src/test/java/com/google/devtools/build/lib/blackbox/tests/NinjaBlackBoxTest.java
@@ -15,6 +15,7 @@
 package com.google.devtools.build.lib.blackbox.tests;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
 
 import com.google.devtools.build.lib.blackbox.framework.BuilderRunner;
 import com.google.devtools.build.lib.blackbox.framework.ProcessResult;
@@ -22,17 +23,25 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Integration test for Ninja execution functionality. */
 public class NinjaBlackBoxTest extends AbstractBlackBoxTest {
-  @Test
-  public void testOneTarget() throws Exception {
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    context().write(".bazelignore", "build_dir");
     context()
         .write(
             WORKSPACE,
-            "workspace(name = 'test')",
+            String.format("workspace(name = '%s')", testName.getMethodName()),
             "dont_symlink_directories_in_execroot(paths = ['build_dir'])");
+  }
+
+  @Test
+  public void testOneTarget() throws Exception {
     context().write("build_dir/input.txt", "World");
     context()
         .write(
@@ -46,18 +55,19 @@
             "ninja_graph(name = 'graph', output_root = 'build_dir',",
             " working_directory = 'build_dir',",
             " main = 'build_dir/build.ninja',",
-            " output_root_inputs = ['input.txt'],",
+            " output_root_inputs = ['input.txt'])",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
             " output_groups = {'group': ['hello.txt']})");
 
     BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
-    assertConfigured(bazel.build("//:graph"));
+    assertConfigured(bazel.build("//:ninja_target"));
     Path path = context().resolveExecRootPath(bazel, "build_dir/hello.txt");
     assertThat(path.toFile().exists()).isTrue();
     assertThat(Files.readAllLines(path)).containsExactly("Hello World!");
 
     // React to input change.
     context().write("build_dir/input.txt", "Sun");
-    assertNothingConfigured(bazel.build("//:graph"));
+    assertNothingConfigured(bazel.build("//:ninja_target"));
     assertThat(Files.readAllLines(path)).containsExactly("Hello Sun!");
 
     // React to Ninja file change.
@@ -67,17 +77,12 @@
             "rule echo",
             "  command = echo \"Hello $$(cat ${in}):)\" > ${out}",
             "build hello.txt: echo input.txt");
-    assertConfigured(bazel.build("//:graph"));
+    assertConfigured(bazel.build("//:ninja_target"));
     assertThat(Files.readAllLines(path)).containsExactly("Hello Sun:)");
   }
 
   @Test
   public void testWithoutExperimentalFlag() throws Exception {
-    context()
-        .write(
-            WORKSPACE,
-            "workspace(name = 'test')",
-            "dont_symlink_directories_in_execroot(paths = ['build_dir'])");
     context().write("build_dir/input.txt", "World");
     context()
         .write(
@@ -91,11 +96,12 @@
             "ninja_graph(name = 'graph', output_root = 'build_dir',",
             " working_directory = 'build_dir',",
             " main = 'build_dir/build.ninja',",
-            " output_root_inputs = ['input.txt'],",
+            " output_root_inputs = ['input.txt'])",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
             " output_groups = {'group': ['hello.txt']})");
 
     BuilderRunner bazel = context().bazel();
-    ProcessResult result = bazel.shouldFail().build("//:graph");
+    ProcessResult result = bazel.shouldFail().build("//:ninja_target");
     assertThat(result.errString())
         .contains("name 'dont_symlink_directories_in_execroot' is not defined");
     assertThat(result.errString()).contains("FAILED: Build did NOT complete successfully");
@@ -103,11 +109,6 @@
 
   @Test
   public void testWithoutMainNinja() throws Exception {
-    context()
-        .write(
-            WORKSPACE,
-            "workspace(name = 'test')",
-            "dont_symlink_directories_in_execroot(paths = ['build_dir'])");
     context().write("build_dir/input.txt", "World");
     context()
         .write(
@@ -120,11 +121,12 @@
             "BUILD",
             "ninja_graph(name = 'graph', output_root = 'build_dir',",
             " working_directory = 'build_dir',",
-            " output_root_inputs = ['input.txt'],",
+            " output_root_inputs = ['input.txt'])",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
             " output_groups = {'group': ['hello.txt']})");
 
     BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
-    ProcessResult result = bazel.shouldFail().build("//:graph");
+    ProcessResult result = bazel.shouldFail().build("//:ninja_target");
     assertThat(result.errString())
         .contains("//:graph: missing value for mandatory attribute 'main' in 'ninja_graph' rule");
     assertThat(result.errString()).contains("FAILED: Build did NOT complete successfully");
@@ -132,11 +134,6 @@
 
   @Test
   public void testSourceFileIsMissing() throws Exception {
-    context()
-        .write(
-            WORKSPACE,
-            "workspace(name = 'test')",
-            "dont_symlink_directories_in_execroot(paths = ['build_dir'])");
     context().write("input.txt", "World");
     context()
         .write(
@@ -149,25 +146,21 @@
             "BUILD",
             "ninja_graph(name = 'graph', output_root = 'build_dir',",
             " working_directory = 'build_dir',",
-            " main = 'build_dir/build.ninja',",
+            " main = 'build_dir/build.ninja')",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
             " output_groups = {'group': ['hello.txt']})");
 
     BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
-    ProcessResult result = bazel.shouldFail().build("//:graph");
+    ProcessResult result = bazel.shouldFail().build("//:ninja_target");
     assertThat(result.errString())
         .contains(
-            "in ninja_graph rule //:graph: Ninja actions are allowed to create outputs only "
+            "in ninja_build rule //:ninja_target: Ninja actions are allowed to create outputs only "
                 + "under output_root, path '../input.txt' is not allowed.");
     assertThat(result.errString()).contains("FAILED: Build did NOT complete successfully");
   }
 
   @Test
   public void testSourceFileIsMissingUnderOutputRoot() throws Exception {
-    context()
-        .write(
-            WORKSPACE,
-            "workspace(name = 'test')",
-            "dont_symlink_directories_in_execroot(paths = ['build_dir'])");
     context().write("input.txt", "World");
     context()
         .write(
@@ -180,41 +173,37 @@
             "BUILD",
             "ninja_graph(name = 'graph', output_root = 'build_dir',",
             " working_directory = 'build_dir',",
-            " main = 'build_dir/build.ninja',",
+            " main = 'build_dir/build.ninja')",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
             " output_groups = {'group': ['hello.txt']})");
 
     BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
-    ProcessResult result = bazel.shouldFail().build("//:graph");
+    ProcessResult result = bazel.shouldFail().build("//:ninja_target");
     assertThat(result.errString())
         .contains(
-            "in ninja_graph rule //:graph: The following artifacts do not have a generating "
+            "in ninja_build rule //:ninja_target: The following artifacts do not have a generating "
                 + "action in Ninja file: build_dir/build_dir/input.txt");
     assertThat(result.errString()).contains("FAILED: Build did NOT complete successfully");
   }
 
   private static void assertNothingConfigured(ProcessResult result) {
     assertThat(result.errString())
-        .contains("INFO: Analyzed target //:graph (0 packages loaded, 0 targets configured).");
+        .contains(
+            "INFO: Analyzed target //:ninja_target (0 packages loaded, 0 targets configured).");
   }
 
   private static void assertConfigured(ProcessResult result) {
     assertThat(result.errString())
         .doesNotContain(
-            "INFO: Analyzed target //:graph (0 packages loaded, 0 targets configured).");
+            "INFO: Analyzed target //:ninja_target (0 packages loaded, 0 targets configured).");
   }
 
   @Test
   public void testNullBuild() throws Exception {
-    context().write(".bazelignore", "build_config");
-    context()
-        .write(
-            WORKSPACE,
-            "workspace(name = 'test')",
-            "dont_symlink_directories_in_execroot(paths = ['build_config'])");
     // Print nanoseconds fraction of the current time into the output file.
     context()
         .write(
-            "build_config/build.ninja",
+            "build_dir/build.ninja",
             "rule echo_time",
             "  command = date +%N >> ${out}",
             "build nano.txt: echo_time");
@@ -222,22 +211,398 @@
         .write(
             "BUILD",
             "ninja_graph(name = 'graph', ",
-            "output_root = 'build_config',",
-            " working_directory = 'build_config',",
-            " main = 'build_config/build.ninja',",
-            " output_groups = {'main': ['nano.txt']})");
+            "output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build.ninja')",
+            "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
+            " output_groups = {'group': ['nano.txt']})");
 
     BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
-    assertConfigured(bazel.build("//:graph"));
-    Path path = context().resolveExecRootPath(bazel, "build_config/nano.txt");
+    assertConfigured(bazel.build("//:ninja_target"));
+    Path path = context().resolveExecRootPath(bazel, "build_dir/nano.txt");
     assertThat(path.toFile().exists()).isTrue();
     List<String> text = Files.readAllLines(path);
     assertThat(text).isNotEmpty();
     long lastModified = path.toFile().lastModified();
 
     // Should be null build, as nothing changed.
-    assertNothingConfigured(bazel.build("//:graph"));
+    assertNothingConfigured(bazel.build("//:ninja_target"));
     assertThat(Files.readAllLines(path)).containsExactly(text.get(0));
     assertThat(path.toFile().lastModified()).isEqualTo(lastModified);
   }
+
+  @Test
+  public void testInteroperabilityWithBazel() throws Exception {
+    context().write("bazel_input.txt", "World");
+    context()
+        .write(
+            "build_dir/build.ninja",
+            "rule echo",
+            "  command = echo \"Hello $$(cat ${in})!\" > ${out}",
+            "build hello.txt: echo placeholder",
+            "build hello2.txt: echo placeholder2");
+    context()
+        .write(
+            "BUILD",
+            "ninja_graph(name = 'graph', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build.ninja')",
+            "filegroup(name = 'bazel_built_input', srcs = [':bazel_input.txt'])",
+            "ninja_build(name = 'ninja_target1', ninja_graph = 'graph',",
+            " deps_mapping = {'placeholder': ':bazel_built_input'},",
+            " output_groups = {'group': ['hello.txt']})",
+            "filegroup(name = 'bazel_middle', srcs = [':ninja_target1'])",
+            "ninja_build(name = 'ninja_target2', ninja_graph = 'graph',",
+            " deps_mapping = {'placeholder2': ':bazel_middle'},",
+            " output_groups = {'group': ['hello2.txt']})");
+
+    BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
+    assertConfigured(bazel.build("//..."));
+    Path path1 = context().resolveExecRootPath(bazel, "build_dir/hello.txt");
+    Path path2 = context().resolveExecRootPath(bazel, "build_dir/hello2.txt");
+
+    assertThat(Files.readAllLines(path1)).containsExactly("Hello World!");
+    assertThat(Files.readAllLines(path2)).containsExactly("Hello Hello World!!");
+  }
+
+  @Test
+  public void testInteroperabilityWithBazelCycle() throws Exception {
+    context().write("bazel_input.txt", "World");
+    context()
+        .write(
+            "build_dir/build.ninja",
+            "rule echo",
+            "  command = echo \"Hello $$(cat ${in})!\" > ${out}",
+            "build hello.txt: echo placeholder",
+            "build hello2.txt: echo placeholder2");
+    context()
+        .write(
+            "BUILD",
+            "ninja_graph(name = 'graph', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build.ninja')",
+
+            // Cycle here with bazel_middle.
+            "filegroup(name = 'bazel_built_input', srcs = [':bazel_input.txt', ':bazel_middle'])",
+            "ninja_build(name = 'ninja_target1', ninja_graph = 'graph',",
+            " deps_mapping = {'placeholder': ':bazel_built_input'},",
+            " output_groups = {'group': ['hello.txt']})",
+            "filegroup(name = 'bazel_middle', srcs = [':ninja_target1'])",
+            "ninja_build(name = 'ninja_target2', ninja_graph = 'graph',",
+            " deps_mapping = {'placeholder2': ':bazel_middle'},",
+            " output_groups = {'group': ['hello2.txt']})");
+
+    BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
+    Exception exception = assertThrows(Exception.class, () -> bazel.build("//..."));
+    assertThat(exception).hasMessageThat().contains("cycle in dependency graph");
+  }
+
+  @Test
+  public void testDisjointPhonyNinjaParts() throws Exception {
+    context().write("build_dir/a.txt", "A");
+    context().write("build_dir/b.txt", "B");
+    context().write("build_dir/c.txt", "C");
+    context().write("build_dir/d.txt", "D");
+    context().write("build_dir/e.txt", "E");
+
+    context()
+        .write(
+            "build_dir/build.ninja",
+            "rule cat",
+            "  command = echo '<<' $$(cat ${in}) '>>' > ${out}",
+            "rule echo",
+            "  command = echo \"Hello $$(cat ${in} | tr '\\r\\n' ' ')!\" > ${out}",
+            "build a: cat a.txt",
+            "build b: cat b.txt",
+            "build c: cat c.txt",
+            "build d: cat d.txt",
+            "build e: cat e.txt",
+            "build group1: phony a b c",
+            "build group2: phony d e",
+            "build inputs_alias: phony group1 group2",
+            "build hello.txt: echo inputs_alias",
+            "build alias: phony hello.txt");
+
+    context()
+        .write(
+            "BUILD",
+            "ninja_graph(name = 'graph', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build.ninja',",
+            " output_root_inputs = ['a.txt', 'b.txt', 'c.txt', 'd.txt', 'e.txt'])",
+            "ninja_build(name = 'ninja_target1', ninja_graph = 'graph',",
+            " output_groups= {'main': ['group1']})",
+            "ninja_build(name = 'ninja_target2', ninja_graph = 'graph',",
+            " output_groups= {'main': ['group2']})");
+
+    BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
+    assertConfigured(bazel.build("//..."));
+    Path pathA = context().resolveExecRootPath(bazel, "build_dir/a");
+    Path pathB = context().resolveExecRootPath(bazel, "build_dir/b");
+    Path pathC = context().resolveExecRootPath(bazel, "build_dir/c");
+    Path pathD = context().resolveExecRootPath(bazel, "build_dir/d");
+    Path pathE = context().resolveExecRootPath(bazel, "build_dir/e");
+
+    assertThat(Files.readAllLines(pathA)).containsExactly("<< A >>");
+    assertThat(Files.readAllLines(pathB)).containsExactly("<< B >>");
+    assertThat(Files.readAllLines(pathC)).containsExactly("<< C >>");
+    assertThat(Files.readAllLines(pathD)).containsExactly("<< D >>");
+    assertThat(Files.readAllLines(pathE)).containsExactly("<< E >>");
+  }
+
+  @Test
+  public void testPhonyNinjaPartsWithSharedPart() throws Exception {
+    context().write("build_dir/a.txt", "A");
+    context().write("build_dir/b.txt", "B");
+    context().write("build_dir/c.txt", "C");
+    context().write("build_dir/d.txt", "D");
+    context().write("build_dir/e.txt", "E");
+
+    context()
+        .write(
+            "build_dir/build.ninja",
+            "rule cat",
+            "  command = echo '<<' $$(cat ${in}) '>>' > ${out}",
+            "build a: cat a.txt",
+            "build b: cat b.txt",
+            "build c: cat c.txt",
+            "build d: cat d.txt",
+            "build e: cat e.txt",
+            // 'a' is present in both groups, built by Bazel since file 'a' is produced by
+            // equal-without-owner actions.
+            "build group1: phony a b c",
+            "build group2: phony a d e");
+
+    context()
+        .write(
+            "BUILD",
+            "ninja_graph(name = 'graph', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build.ninja',",
+            " output_root_inputs = ['a.txt', 'b.txt', 'c.txt', 'd.txt', 'e.txt'])",
+            "ninja_build(name = 'ninja_target1', ninja_graph = 'graph',",
+            " output_groups= {'main': ['group1']})",
+            "ninja_build(name = 'ninja_target2', ninja_graph = 'graph',",
+            " output_groups= {'main': ['group2']})");
+
+    BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
+    assertConfigured(bazel.build("//..."));
+    Path pathA = context().resolveExecRootPath(bazel, "build_dir/a");
+    Path pathB = context().resolveExecRootPath(bazel, "build_dir/b");
+    Path pathC = context().resolveExecRootPath(bazel, "build_dir/c");
+    Path pathD = context().resolveExecRootPath(bazel, "build_dir/d");
+    Path pathE = context().resolveExecRootPath(bazel, "build_dir/e");
+
+    assertThat(Files.readAllLines(pathA)).containsExactly("<< A >>");
+    assertThat(Files.readAllLines(pathB)).containsExactly("<< B >>");
+    assertThat(Files.readAllLines(pathC)).containsExactly("<< C >>");
+    assertThat(Files.readAllLines(pathD)).containsExactly("<< D >>");
+    assertThat(Files.readAllLines(pathE)).containsExactly("<< E >>");
+  }
+
+  @Test
+  public void testDisjointUsualNinjaParts() throws Exception {
+    context().write("build_dir/a.txt", "A");
+    context().write("build_dir/b.txt", "B");
+    context().write("build_dir/c.txt", "C");
+    context().write("build_dir/d.txt", "D");
+    context().write("build_dir/e.txt", "E");
+
+    context()
+        .write(
+            "build_dir/build.ninja",
+            "rule cat",
+            "  command = echo '<<' $$(cat ${in}) '>>' > ${out}",
+            "build a: cat a.txt",
+            "build b: cat b.txt",
+            "build c: cat c.txt",
+            "build d: cat d.txt",
+            "build e: cat e.txt",
+            "build group1: phony a b c",
+            "build group2: phony d e",
+            "build inputs_alias: phony group1 group2",
+            "build hello.txt: echo inputs_alias",
+            "build alias: phony hello.txt");
+
+    context()
+        .write(
+            "BUILD",
+            "ninja_graph(name = 'graph', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build.ninja',",
+            " output_root_inputs = ['a.txt', 'b.txt', 'c.txt', 'd.txt', 'e.txt'])",
+            "ninja_build(name = 'ninja_target1', ninja_graph = 'graph',",
+            " output_groups= {'main': ['a']})",
+            "ninja_build(name = 'ninja_target2', ninja_graph = 'graph',",
+            " output_groups= {'main': ['e']})");
+
+    BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
+    assertConfigured(bazel.build("//..."));
+    Path pathA = context().resolveExecRootPath(bazel, "build_dir/a");
+    Path pathE = context().resolveExecRootPath(bazel, "build_dir/e");
+    assertThat(Files.readAllLines(pathA)).containsExactly("<< A >>");
+    assertThat(Files.readAllLines(pathE)).containsExactly("<< E >>");
+
+    Path pathB = context().resolveExecRootPath(bazel, "build_dir/b");
+    Path pathC = context().resolveExecRootPath(bazel, "build_dir/c");
+    Path pathD = context().resolveExecRootPath(bazel, "build_dir/d");
+    assertThat(Files.exists(pathB)).isFalse();
+    assertThat(Files.exists(pathC)).isFalse();
+    assertThat(Files.exists(pathD)).isFalse();
+  }
+
+  @Test
+  public void testDuplicateUsualNinjaParts() throws Exception {
+    context().write("build_dir/a.txt", "A");
+    context().write("build_dir/b.txt", "B");
+    context().write("build_dir/c.txt", "C");
+    context().write("build_dir/d.txt", "D");
+    context().write("build_dir/e.txt", "E");
+
+    context()
+        .write(
+            "build_dir/build.ninja",
+            "rule cat",
+            "  command = echo '<<' $$(cat ${in}) '>>' > ${out}",
+            "build a: cat a.txt",
+            "build b: cat b.txt");
+
+    context()
+        .write(
+            "BUILD",
+            "ninja_graph(name = 'graph', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build.ninja',",
+            " output_root_inputs = ['a.txt', 'b.txt', 'c.txt', 'd.txt', 'e.txt'])",
+            // 'a' is present in both ninja_build targets, built by Bazel since file 'a' is produced
+            // by
+            // equal-without-owner actions.
+            "ninja_build(name = 'ninja_target1', ninja_graph = 'graph',",
+            " output_groups= {'main': ['a']})",
+            "ninja_build(name = 'ninja_target2', ninja_graph = 'graph',",
+            " output_groups= {'main': ['a']})");
+
+    BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
+    assertConfigured(bazel.build("//..."));
+    Path pathA = context().resolveExecRootPath(bazel, "build_dir/a");
+    assertThat(Files.readAllLines(pathA)).containsExactly("<< A >>");
+
+    Path pathB = context().resolveExecRootPath(bazel, "build_dir/b");
+    assertThat(Files.exists(pathB)).isFalse();
+  }
+
+  @Test
+  public void testDuplicateUsualNinjaPartsDifferentMappings() throws Exception {
+    context().write("variant1.txt", "variant1");
+    context().write("variant2.txt", "variant2");
+
+    context()
+        .write(
+            "build_dir/build.ninja",
+            "rule append",
+            "  command = echo '<<' $$(cat ${in}) '>>' >> ${out}",
+            "build a: append a.txt");
+
+    context()
+        .write(
+            "BUILD",
+            "ninja_graph(name = 'graph', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build.ninja')",
+            "ninja_build(name = 'ninja_target1', ninja_graph = 'graph',",
+            " output_groups= {'main': ['a']}, deps_mapping = {'a.txt': ':variant1.txt'})",
+            "ninja_build(name = 'ninja_target2', ninja_graph = 'graph',",
+            " output_groups= {'main': ['a']}, deps_mapping = {'a.txt': ':variant2.txt'})");
+
+    BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
+
+    // Exception to do not tow additional dependencies into current test. (ActionConflictException)
+    Exception exception = assertThrows(Exception.class, () -> bazel.build("//..."));
+    assertThat(exception)
+        .hasMessageThat()
+        .contains("ERROR: file 'a' is generated by these conflicting actions:");
+    assertThat(exception)
+        .hasMessageThat()
+        .contains(
+            "for a, previous action: action 'running Ninja targets: 'a'', "
+                + "attempted action: action 'running Ninja targets: 'a''");
+  }
+
+  @Test
+  public void testDependentNinjaActions() throws Exception {
+    context().write("build_dir/a.txt", "A");
+
+    context()
+        .write(
+            "build_dir/build1.ninja",
+            "rule cat",
+            "  command = echo '<<' $$(cat ${in}) '>>' > ${out}",
+            "build first.txt: cat a.txt");
+    context()
+        .write(
+            "build_dir/build2.ninja",
+            "rule cat",
+            "  command = echo '<<' $$(cat ${in}) '>>' > ${out}",
+            "build second.txt: cat input");
+
+    // For the dependent Ninja actions from the same Ninja graph, Ninja mechanisms should be used.
+    context()
+        .write(
+            "BUILD",
+            "ninja_graph(name = 'graph1', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build1.ninja',",
+            " output_root_inputs = ['a.txt'])",
+            "ninja_build(name = 'ninja_target1', ninja_graph = 'graph1',",
+            " output_groups= {'main': ['first.txt']})",
+            "ninja_graph(name = 'graph2', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build2.ninja')",
+            "ninja_build(name = 'ninja_target2', ninja_graph = 'graph2',",
+            " output_groups= {'main': ['second.txt']}, deps_mapping = {'input':"
+                + " ':ninja_target1'})");
+
+    BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
+    assertConfigured(bazel.build("//..."));
+    Path pathFirst = context().resolveExecRootPath(bazel, "build_dir/first.txt");
+    assertThat(Files.readAllLines(pathFirst)).containsExactly("<< A >>");
+    Path pathSecond = context().resolveExecRootPath(bazel, "build_dir/second.txt");
+    assertThat(Files.readAllLines(pathSecond)).containsExactly("<< << A >> >>");
+  }
+
+  @Test
+  public void testDependentNinjaActionsCycle() throws Exception {
+    context()
+        .write(
+            "build_dir/build1.ninja",
+            "rule cat",
+            "  command = echo '<<' $$(cat ${in}) '>>' > ${out}",
+            "build first.txt: cat input");
+    context()
+        .write(
+            "build_dir/build2.ninja",
+            "rule cat",
+            "  command = echo '<<' $$(cat ${in}) '>>' > ${out}",
+            "build second.txt: cat input");
+
+    // For the dependent Ninja actions from the same Ninja graph, Ninja mechanisms should be used.
+    context()
+        .write(
+            "BUILD",
+            "ninja_graph(name = 'graph1', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build1.ninja')",
+            "ninja_build(name = 'ninja_target1', ninja_graph = 'graph1',",
+            " output_groups= {'main': ['first.txt']}, deps_mapping = {'input': ':ninja_target2'})",
+            "ninja_graph(name = 'graph2', output_root = 'build_dir',",
+            " working_directory = 'build_dir',",
+            " main = 'build_dir/build2.ninja')",
+            "ninja_build(name = 'ninja_target2', ninja_graph = 'graph2',",
+            " output_groups= {'main': ['second.txt']}, deps_mapping = {'input':"
+                + " ':ninja_target1'})");
+
+    BuilderRunner bazel = context().bazel().withFlags("--experimental_ninja_actions");
+    Exception exception = assertThrows(Exception.class, () -> bazel.build("//..."));
+    assertThat(exception).hasMessageThat().contains("cycle in dependency graph");
+  }
 }