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");
+ }
}