blob: af007a3e16b39425ec3bde5fd26ea5e2388a30d1 [file] [log] [blame]
// 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.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;
import com.google.devtools.build.lib.actions.CommandLines;
import com.google.devtools.build.lib.actions.EmptyRunfilesSupplier;
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.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;
import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaScope;
import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaTarget;
import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaVariableValue;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.packages.TargetUtils;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayDeque;
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;
/**
* Helper class for creating {@link NinjaAction} for each {@link NinjaTarget}, and linking the file
* under the output_root into corresponding directory in execroot. See output_root property in the
* {@link NinjaGraphRule}.
*/
public class NinjaActionsHelper {
private final RuleContext ruleContext;
private final ImmutableSortedMap<PathFragment, NinjaTarget> allUsualTargets;
private final ImmutableSortedMap<PathFragment, PhonyTarget> phonyTargets;
private final NinjaGraphArtifactsHelper artifactsHelper;
private final PathFragment shellExecutable;
private final ImmutableSortedMap<String, String> executionInfo;
private final PhonyTargetArtifacts phonyTargetArtifacts;
private final List<PathFragment> pathsToBuild;
/**
* Constructor
*
* @param ruleContext parent NinjaGraphRule rule context
* @param artifactsHelper helper object to create artifacts
* @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
* targets
* @param pathsToBuild paths requested by the user to be build (in output_groups attribute)
*/
NinjaActionsHelper(
RuleContext ruleContext,
NinjaGraphArtifactsHelper artifactsHelper,
ImmutableSortedMap<PathFragment, NinjaTarget> allUsualTargets,
ImmutableSortedMap<PathFragment, PhonyTarget> phonyTargets,
PhonyTargetArtifacts phonyTargetArtifacts,
List<PathFragment> pathsToBuild) {
this.ruleContext = ruleContext;
this.artifactsHelper = artifactsHelper;
this.allUsualTargets = allUsualTargets;
this.phonyTargets = phonyTargets;
this.shellExecutable = ShToolchain.getPathOrError(ruleContext);
this.executionInfo = createExecutionInfo(ruleContext);
this.phonyTargetArtifacts = phonyTargetArtifacts;
this.pathsToBuild = pathsToBuild;
}
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();
Set<NinjaTarget> visitedTargets = Sets.newHashSet();
visitedPaths.addAll(pathsToBuild);
ArrayDeque<PathFragment> queue = new ArrayDeque<>(pathsToBuild);
Consumer<Collection<PathFragment>> enqueuer =
paths -> {
for (PathFragment input : paths) {
if (visitedPaths.add(input)) {
queue.add(input);
}
}
};
while (!queue.isEmpty()) {
PathFragment fragment = queue.remove();
NinjaTarget target = allUsualTargets.get(fragment);
if (target != null) {
if (visitedTargets.add(target)) {
createNinjaAction(target);
}
enqueuer.accept(target.getAllInputs());
} else {
PhonyTarget phonyTarget = phonyTargets.get(fragment);
// Phony target can be null, if the path in neither usual or phony target,
// but the source file.
if (phonyTarget != null) {
phonyTarget.visitUsualInputs(phonyTargets, enqueuer::accept);
}
}
}
}
private void createNinjaAction(NinjaTarget target) throws GenericParsingException {
NinjaRule rule = getNinjaRule(target);
NestedSetBuilder<Artifact> inputsBuilder = NestedSetBuilder.stableOrder();
ImmutableList.Builder<Artifact> outputsBuilder = ImmutableList.builder();
TreeMap<PathFragment, Artifact> depsReplacements = Maps.newTreeMap();
boolean isAlwaysDirty = fillArtifacts(target, inputsBuilder, outputsBuilder, depsReplacements);
NinjaScope targetScope = createTargetScope(target);
int targetOffset = target.getOffset();
maybeCreateRspFile(rule, targetScope, targetOffset, inputsBuilder);
String command =
targetScope.getExpandedValue(
targetOffset, rule.getVariables().get(NinjaRuleVariable.COMMAND));
if (!artifactsHelper.getWorkingDirectory().isEmpty()) {
command = String.format("cd %s && ", artifactsHelper.getWorkingDirectory()) + command;
}
CommandLines commandLines =
CommandLines.of(ImmutableList.of(shellExecutable.getPathString(), "-c", command));
ruleContext.registerAction(
new NinjaAction(
ruleContext.getActionOwner(),
NestedSetBuilder.emptySet(Order.STABLE_ORDER),
inputsBuilder.build(),
outputsBuilder.build(),
commandLines,
Preconditions.checkNotNull(ruleContext.getConfiguration()).getActionEnvironment(),
executionInfo,
EmptyRunfilesSupplier.INSTANCE,
isAlwaysDirty));
}
/** Returns true is the action shpould be marked as always dirty. */
private boolean fillArtifacts(
NinjaTarget target,
NestedSetBuilder<Artifact> inputsBuilder,
ImmutableList.Builder<Artifact> outputsBuilder,
SortedMap<PathFragment, Artifact> depsReplacements)
throws GenericParsingException {
boolean isAlwaysDirty = false;
for (PathFragment input : target.getAllInputs()) {
PhonyTarget phonyTarget = this.phonyTargets.get(input);
if (phonyTarget != null) {
inputsBuilder.addTransitive(phonyTargetArtifacts.getPhonyTargetArtifacts(input));
isAlwaysDirty |= phonyTarget.isAlwaysDirty();
} else {
Artifact artifact = artifactsHelper.getInputArtifact(input);
inputsBuilder.add(artifact);
}
}
for (PathFragment output : target.getAllOutputs()) {
outputsBuilder.add(artifactsHelper.createOutputArtifact(output));
}
return isAlwaysDirty;
}
private void maybeCreateRspFile(
NinjaRule rule,
NinjaScope targetScope,
int targetOffset,
NestedSetBuilder<Artifact> inputsBuilder)
throws GenericParsingException {
NinjaVariableValue value = rule.getVariables().get(NinjaRuleVariable.RSPFILE);
NinjaVariableValue content = rule.getVariables().get(NinjaRuleVariable.RSPFILE_CONTENT);
if (value == null && content == null) {
return;
}
if (value == null || content == null) {
ruleContext.ruleError(
String.format(
"Both rspfile and rspfile_content should be defined for rule '%s'.", rule.getName()));
return;
}
String fileName = targetScope.getExpandedValue(targetOffset, value);
String contentString = targetScope.getExpandedValue(targetOffset, content);
if (!fileName.trim().isEmpty()) {
DerivedArtifact rspArtifact =
artifactsHelper.createOutputArtifact(PathFragment.create(fileName));
FileWriteAction fileWriteAction =
FileWriteAction.create(ruleContext, rspArtifact, contentString, false);
ruleContext.registerAction(fileWriteAction);
inputsBuilder.add(rspArtifact);
}
}
private NinjaScope createTargetScope(NinjaTarget target) {
ImmutableSortedMap.Builder<String, List<Pair<Integer, String>>> builder =
ImmutableSortedMap.naturalOrder();
target
.getVariables()
.forEach((key, value) -> builder.put(key, ImmutableList.of(Pair.of(0, value))));
String inNewline =
target.getUsualInputs().stream()
.map(this::getInputPathWithDepsMappingReplacement)
.collect(Collectors.joining("\n"));
String out =
target.getOutputs().stream()
.map(PathFragment::getPathString)
.collect(Collectors.joining(" "));
builder.put("in", ImmutableList.of(Pair.of(0, inNewline.replace('\n', ' '))));
builder.put("in_newline", ImmutableList.of(Pair.of(0, inNewline)));
builder.put("out", ImmutableList.of(Pair.of(0, out)));
return target.getScope().createTargetsScope(builder.build());
}
private static NinjaRule getNinjaRule(NinjaTarget target) throws GenericParsingException {
String ruleName = target.getRuleName();
Preconditions.checkState(!"phony".equals(ruleName));
NinjaRule rule = target.getScope().findRule(target.getOffset(), ruleName);
if (rule == null) {
throw new GenericParsingException(String.format("Unknown Ninja rule: '%s'", ruleName));
}
return rule;
}
private static ImmutableSortedMap<String, String> createExecutionInfo(RuleContext ruleContext) {
ImmutableSortedMap.Builder<String, String> builder = ImmutableSortedMap.naturalOrder();
builder.putAll(TargetUtils.getExecutionInfo(ruleContext.getRule()));
builder.put("local", "");
ImmutableSortedMap<String, String> map = builder.build();
Preconditions.checkNotNull(ruleContext.getConfiguration())
.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();
}
}