Don't register a NinjaAction if the output of that NinjaAction is already
created by a symlink action created from that output being specified in the
output_root_inputs attribute of the ninja_graph rule.
RELNOTES: None.
PiperOrigin-RevId: 300134451
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 c6b33b2..6b33ebf 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
@@ -15,8 +15,10 @@
package com.google.devtools.build.lib.bazel.rules.ninja.actions;
import com.google.common.base.Ascii;
+import com.google.common.base.Joiner;
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.Sets;
import com.google.devtools.build.lib.actions.Artifact;
@@ -38,6 +40,7 @@
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayDeque;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -61,6 +64,7 @@
private final ImmutableSortedMap<String, String> executionInfo;
private final PhonyTargetArtifacts phonyTargetArtifacts;
private final List<PathFragment> pathsToBuild;
+ private final ImmutableSet<PathFragment> outputRootInputsSymlinks;
/**
* Constructor
@@ -79,7 +83,8 @@
ImmutableSortedMap<PathFragment, NinjaTarget> allUsualTargets,
ImmutableSortedMap<PathFragment, PhonyTarget> phonyTargets,
PhonyTargetArtifacts phonyTargetArtifacts,
- List<PathFragment> pathsToBuild) {
+ List<PathFragment> pathsToBuild,
+ ImmutableSet<PathFragment> outputRootInputsSymlinks) {
this.ruleContext = ruleContext;
this.artifactsHelper = artifactsHelper;
this.allUsualTargets = allUsualTargets;
@@ -88,6 +93,7 @@
this.executionInfo = createExecutionInfo(ruleContext);
this.phonyTargetArtifacts = phonyTargetArtifacts;
this.pathsToBuild = pathsToBuild;
+ this.outputRootInputsSymlinks = outputRootInputsSymlinks;
}
void createNinjaActions() throws GenericParsingException {
@@ -109,10 +115,39 @@
PathFragment fragment = queue.remove();
NinjaTarget target = allUsualTargets.get(fragment);
if (target != null) {
- if (visitedTargets.add(target)) {
- createNinjaAction(target);
+ // If the output is already created by a symlink action created from specifying that
+ // file in output_root_inputs attribute of the ninja_graph rule, do not create other
+ // actions that output that same file, since that will result in an action conflict.
+ if (!outputRootInputsSymlinks.contains(fragment)) {
+ if (visitedTargets.add(target)) {
+ createNinjaAction(target);
+ }
+ enqueuer.accept(target.getAllInputs());
+ } else {
+ // Sanity check that the Ninja action we're skipping (because its outputs are already
+ // being symlinked using output_root_inputs) has only symlink outputs specified in
+ // output_root_inputs. Otherwise we might skip some other outputs.
+ List<PathFragment> outputsInOutputRootInputsSymlinks = new ArrayList<>();
+ List<PathFragment> outputsNotInOutputRootInputsSymlinks = new ArrayList<>();
+ for (PathFragment output : target.getAllOutputs()) {
+ if (outputRootInputsSymlinks.contains(output)) {
+ outputsInOutputRootInputsSymlinks.add(output);
+ } else {
+ outputsNotInOutputRootInputsSymlinks.add(output);
+ }
+ }
+ if (!outputsNotInOutputRootInputsSymlinks.isEmpty()) {
+ throw new GenericParsingException(
+ "Ninja target "
+ + target.getRuleName()
+ + " has "
+ + "outputs in output_root_inputs and other outputs not in output_root_inputs:\n"
+ + "Outputs in output_root_inputs:\n "
+ + Joiner.on(" \n").join(outputsInOutputRootInputsSymlinks)
+ + "\nOutputs not in output_root_inputs:\n "
+ + Joiner.on(" \n").join(outputsNotInOutputRootInputsSymlinks));
+ }
}
- enqueuer.accept(target.getAllInputs());
} else {
PhonyTarget phonyTarget = phonyTargets.get(fragment);
// Phony target can be null, if the path in neither usual or phony target,
@@ -126,7 +161,6 @@
private void createNinjaAction(NinjaTarget target) throws GenericParsingException {
NinjaRule rule = getNinjaRule(target);
-
NestedSetBuilder<Artifact> inputsBuilder = NestedSetBuilder.stableOrder();
ImmutableList.Builder<Artifact> outputsBuilder = ImmutableList.builder();
boolean isAlwaysDirty = fillArtifacts(target, inputsBuilder, outputsBuilder);
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
index fe7ddea..11cb1db 100644
--- 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
@@ -84,7 +84,8 @@
graphProvider.getUsualTargets(),
graphProvider.getPhonyTargetsMap(),
phonyTargetArtifacts,
- pathsToBuild)
+ pathsToBuild,
+ graphProvider.getOutputRootInputsSymlinks())
.createNinjaActions();
if (!checkOrphanArtifacts(ruleContext)) {
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 6dfb5c3..bea9f17 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
@@ -14,10 +14,12 @@
package com.google.devtools.build.lib.bazel.rules.ninja.actions;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
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;
@@ -125,28 +127,35 @@
.pipeline(mainArtifact.getPath());
targetsPreparer.process(ninjaTargets);
+ NestedSet<Artifact> outputRootInputsSymlinks =
+ createSymlinkActions(ruleContext, sourceRoot, outputRootInputs, artifactsHelper);
+ if (ruleContext.hasErrors()) {
+ return null;
+ }
+
+ ImmutableSortedSet<PathFragment> outputRootSymlinksPathFragments =
+ outputRootSymlinks.stream()
+ .map(PathFragment::create)
+ .collect(toImmutableSortedSet(Comparator.comparing(PathFragment::getPathString)));
+
+ ImmutableSet<PathFragment> outputRootInputsSymlinksPathFragments =
+ outputRootInputsSymlinks.toList().stream()
+ .map(Artifact::getExecPath)
+ .collect(toImmutableSet());
+
NinjaGraphProvider ninjaGraphProvider =
new NinjaGraphProvider(
outputRoot,
workingDirectory,
targetsPreparer.getUsualTargets(),
targetsPreparer.getPhonyTargetsMap(),
- outputRootSymlinks.stream()
- .map(PathFragment::create)
- .collect(
- toImmutableSortedSet(Comparator.comparing(PathFragment::getPathString))));
-
- NestedSet<Artifact> filesToBuild =
- createSymlinkActions(
- ruleContext, sourceRoot, outputRoot, outputRootInputs, artifactsHelper);
- if (ruleContext.hasErrors()) {
- return null;
- }
+ outputRootSymlinksPathFragments,
+ outputRootInputsSymlinksPathFragments);
return new RuleConfiguredTargetBuilder(ruleContext)
.addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
.addProvider(NinjaGraphProvider.class, ninjaGraphProvider)
- .setFilesToBuild(filesToBuild)
+ .setFilesToBuild(outputRootInputsSymlinks)
.build();
} catch (GenericParsingException | IOException e) {
// IOException is possible with reading Ninja file, describing the action graph.
@@ -158,16 +167,19 @@
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);
}
- NestedSetBuilder<Artifact> filesToBuild = NestedSetBuilder.stableOrder();
+
+ NestedSetBuilder<Artifact> symlinks = NestedSetBuilder.stableOrder();
Path outputRootInSources =
- Preconditions.checkNotNull(sourceRoot.asPath()).getRelative(outputRootPath);
+ Preconditions.checkNotNull(sourceRoot.asPath())
+ .getRelative(artifactsHelper.getOutputRootPath());
+
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.
@@ -177,7 +189,7 @@
.getOutputRootPath()
.getRelative(input)
.relativeTo(artifactsHelper.getWorkingDirectory()));
- filesToBuild.add(derivedArtifact);
+ symlinks.add(derivedArtifact);
// This method already expects the path relative to output_root.
PathFragment absolutePath =
outputRootInSources.getRelative(PathFragment.create(input)).asFragment();
@@ -190,7 +202,7 @@
"Symlinking %s under <execroot>/%s", input, artifactsHelper.getOutputRootPath()));
ruleContext.registerAction(symlinkAction);
}
- return filesToBuild.build();
+ return symlinks.build();
}
private static class TargetsPreparer {
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
index 7bb50bb..deff5a5 100644
--- 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
@@ -14,6 +14,7 @@
package com.google.devtools.build.lib.bazel.rules.ninja.actions;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
@@ -32,18 +33,22 @@
private final ImmutableSortedMap<PathFragment, NinjaTarget> usualTargets;
private final ImmutableSortedMap<PathFragment, PhonyTarget> phonyTargetsMap;
private final ImmutableSortedSet<PathFragment> outputRootSymlinks;
+ private final ImmutableSet<PathFragment> outputRootInputsSymlinks;
public NinjaGraphProvider(
PathFragment outputRoot,
PathFragment workingDirectory,
ImmutableSortedMap<PathFragment, NinjaTarget> usualTargets,
ImmutableSortedMap<PathFragment, PhonyTarget> phonyTargetsMap,
- ImmutableSortedSet<PathFragment> outputRootSymlinks) {
+ ImmutableSortedSet<PathFragment> outputRootSymlinks,
+ ImmutableSet<PathFragment> outputRootInputsSymlinks) {
+
this.outputRoot = outputRoot;
this.workingDirectory = workingDirectory;
this.usualTargets = usualTargets;
this.phonyTargetsMap = phonyTargetsMap;
this.outputRootSymlinks = outputRootSymlinks;
+ this.outputRootInputsSymlinks = outputRootInputsSymlinks;
}
public PathFragment getOutputRoot() {
@@ -66,4 +71,8 @@
public ImmutableSortedSet<PathFragment> getOutputRootSymlinks() {
return outputRootSymlinks;
}
+
+ public ImmutableSet<PathFragment> getOutputRootInputsSymlinks() {
+ return outputRootInputsSymlinks;
+ }
}
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
index ef63388..4d7e5d3 100644
--- 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
@@ -15,6 +15,7 @@
package com.google.devtools.build.lib.bazel.rules.ninja;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
@@ -463,4 +464,88 @@
assertThat(commandLines.get(0).commandLine.toString())
.endsWith("cd build_config && ln -s fictive-file dangling_symlink");
}
+
+ @Test
+ public void testOutputRootInputsWithConflictingNinjaActionOutput() throws Exception {
+ rewriteWorkspace(
+ "workspace(name = 'test')",
+ "dont_symlink_directories_in_execroot(paths = ['build_config'])");
+
+ scratch.file("build_config/hello.txt", "hello");
+ scratch.file(
+ "build_config/build.ninja",
+ "rule hello_world",
+ " command = echo \"Hello World!\" > ${out}",
+ "build build_config/hello.txt: hello_world",
+ "rule echo",
+ " command = echo \"Hello $$(cat ${in})!\" > ${out}",
+ "build build_config/out.txt: echo build_config/hello.txt");
+
+ // Working directory is workspace root.
+ ConfiguredTarget configuredTarget =
+ scratchConfiguredTarget(
+ "",
+ "ninja_target",
+ "ninja_graph(name = 'graph', output_root = 'build_config',",
+ " main = 'build_config/build.ninja',",
+ // hello.txt will be symlinked using a symlink action, but will also be an output of
+ // the NinjaAction created from the ninja rule above.
+ " output_root_inputs = ['hello.txt'])",
+ "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
+ " output_groups= {'main': ['build_config/out.txt']})");
+
+ RuleConfiguredTarget ninjaConfiguredTarget = (RuleConfiguredTarget) configuredTarget;
+ ImmutableList<ActionAnalysisMetadata> actions = ninjaConfiguredTarget.getActions();
+ // The build.ninja file has rules for two actions, but only one of them should have been
+ // registered. Normally this would produce an ActionsConflictException, but we skip the action.
+ assertThat(actions).hasSize(1);
+ assertThat(actions.get(0).getOutputs().asList().get(0).getExecPathString())
+ .isEqualTo("build_config/out.txt");
+ }
+
+ @Test
+ public void testOutputRootInputsWithConflictingNinjaActionOutputThrowsErrorOnNonSymlinkOutput()
+ throws Exception {
+
+ rewriteWorkspace(
+ "workspace(name = 'test')",
+ "dont_symlink_directories_in_execroot(paths = ['build_config'])");
+
+ scratch.file("build_config/hello.txt", "hello");
+ scratch.file(
+ "build_config/build.ninja",
+ "rule hello_world",
+ " command = echo \"Hello World!\" > ${out}",
+ "build build_config/hello.txt build_config/not_an_input_symlink.txt: hello_world",
+ "rule echo",
+ " command = echo \"Hello $$(cat ${in})!\" > ${out}",
+ "build build_config/out.txt: echo build_config/hello.txt");
+
+ String message =
+ "in ninja_build rule //:ninja_target: Ninja target hello_world has outputs in "
+ + "output_root_inputs and other outputs not in output_root_inputs:\n"
+ + "Outputs in output_root_inputs:\n"
+ + " build_config/hello.txt\n"
+ + "Outputs not in output_root_inputs:\n"
+ + " build_config/not_an_input_symlink.txt";
+
+ // A GenericParsingException is what's actually created, however the rule code and the
+ // testing framework only relays the exception's messasge wrapped in an AssertionError.
+ Throwable throwable =
+ assertThrows(
+ AssertionError.class,
+ () ->
+ scratchConfiguredTarget(
+ "",
+ "ninja_target",
+ "ninja_graph(name = 'graph', output_root = 'build_config',",
+ " main = 'build_config/build.ninja',",
+ // hello.txt will be symlinked using a symlink action, but will also be an
+ // output of
+ // the NinjaAction created from the ninja rule above.
+ " output_root_inputs = ['hello.txt'])",
+ "ninja_build(name = 'ninja_target', ninja_graph = 'graph',",
+ " output_groups= {'main': ['build_config/out.txt']})"));
+ assertThat(throwable).hasMessageThat().contains(message);
+ }
}