blob: 37fed0aeba5de46854ab90e9abdfde3e621442cb [file] [log] [blame]
// Copyright 2019 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.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.devtools.build.lib.actions.Artifact;
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.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;
import com.google.devtools.build.lib.bazel.rules.ninja.pipeline.NinjaPipeline;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.concurrent.ExecutorUtil;
import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.repository.ExternalPackageUtil;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
import com.google.devtools.build.skyframe.SkyKey;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;
/** Configured target factory for {@link NinjaGraphRule}. */
public class NinjaGraph implements RuleConfiguredTargetFactory {
/**
* Use a thread pool with the heuristically determined maximum number of threads, but the policy
* to not consume threads when there are no active tasks.
*/
private static final ThreadPoolExecutor NINJA_POOL =
ExecutorUtil.newSlackPool(
Math.max(2, Runtime.getRuntime().availableProcessors() / 2),
NinjaGraph.class.getSimpleName());
@Override
public ConfiguredTarget create(RuleContext ruleContext)
throws InterruptedException, RuleErrorException, ActionConflictException {
if (!ruleContext.getAnalysisEnvironment().getSkylarkSemantics().experimentalNinjaActions()) {
throw new RuleErrorException(
"Usage of ninja_graph is only allowed with --experimental_ninja_actions flag");
}
Artifact mainArtifact = ruleContext.getPrerequisiteArtifact("main", Mode.TARGET);
ImmutableList<Artifact> ninjaSrcs =
ruleContext.getPrerequisiteArtifacts("ninja_srcs", Mode.TARGET).list();
Environment env = ruleContext.getAnalysisEnvironment().getSkyframeEnv();
establishDependencyOnNinjaFiles(env, mainArtifact, ninjaSrcs);
ImmutableSortedSet<String> notSymlinkedDirs =
ExternalPackageUtil.getNotSymlinkedInExecrootDirectories(env);
if (env.valuesMissing()) {
return null;
}
List<Path> childNinjaFiles =
ninjaSrcs.stream().map(Artifact::getPath).collect(Collectors.toList());
PathFragment outputRoot =
PathFragment.create(ruleContext.attributes().get("output_root", Type.STRING));
// We can compare strings because notSymlinkedDirs contains normalized directory names
if (!notSymlinkedDirs.contains(outputRoot.getPathString())) {
ruleContext.attributeError(
"output_root",
String.format(
"Ninja output root directory '%s' must be declared"
+ " using global workspace function dont_symlink_directories_in_execroot().",
outputRoot.getPathString()));
}
PathFragment workingDirectory =
PathFragment.create(ruleContext.attributes().get("working_directory", Type.STRING));
if (!workingDirectory.isEmpty() && !workingDirectory.equals(outputRoot)) {
ruleContext.attributeError(
"working_directory",
String.format(
"Ninja working directory '%s' is restricted to be either empty (or not defined),"
+ " or be the same as output root '%s'.",
workingDirectory.getPathString(), outputRoot.getPathString()));
}
if (ruleContext.hasErrors()) {
return null;
}
List<String> outputRootInputs =
ruleContext.attributes().get("output_root_inputs", Type.STRING_LIST);
Path workspace =
Preconditions.checkNotNull(ruleContext.getConfiguration()).getDirectories().getWorkspace();
String ownerTargetName = ruleContext.getLabel().getName();
List<NinjaTarget> ninjaTargets =
readNinjaGraph(mainArtifact, childNinjaFiles, workingDirectory, workspace, ownerTargetName);
ImmutableSortedMap.Builder<PathFragment, NinjaTarget> usualTargetsBuilder =
ImmutableSortedMap.naturalOrder();
ImmutableSortedMap.Builder<PathFragment, NinjaTarget> phonyTargetsBuilder =
ImmutableSortedMap.naturalOrder();
separatePhonyTargets(ninjaTargets, usualTargetsBuilder, phonyTargetsBuilder);
ImmutableSortedMap<PathFragment, NinjaTarget> usualTargets = usualTargetsBuilder.build();
ImmutableSortedMap<PathFragment, NestedSet<Artifact>> phonyTargetsMap;
Root sourceRoot = mainArtifact.getRoot().getRoot();
ImmutableSortedMap<PathFragment, Artifact> depsMap = createDepsMap(ruleContext);
if (ruleContext.hasErrors()) {
return null;
}
NinjaGraphArtifactsHelper artifactsHelper =
new NinjaGraphArtifactsHelper(
ruleContext,
sourceRoot,
outputRoot,
workingDirectory,
createSrcsMap(ruleContext),
depsMap);
try {
phonyTargetsMap =
NinjaPhonyTargetsUtil.getPhonyPathsMap(
phonyTargetsBuilder.build(), artifactsHelper::getInputArtifact);
NinjaActionsHelper ninjaActionsHelper =
new NinjaActionsHelper(
ruleContext, artifactsHelper, outputRootInputs, usualTargets, phonyTargetsMap);
ninjaActionsHelper.process();
} catch (GenericParsingException e) {
throw new RuleErrorException(e.getMessage());
}
NinjaGraphProvider graphProvider =
new NinjaGraphProvider(
outputRoot, workingDirectory, phonyTargetsMap, artifactsHelper.getOutputsMap());
RuleConfiguredTargetBuilder builder = new RuleConfiguredTargetBuilder(ruleContext);
builder
.addProvider(NinjaGraphProvider.class, graphProvider)
.addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY);
return builder.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
* related FileValue SkyValues.
*/
private static void establishDependencyOnNinjaFiles(
Environment env, Artifact mainFile, ImmutableList<Artifact> ninjaSrcs)
throws InterruptedException {
ArrayList<SkyKey> depKeys = Lists.newArrayList();
depKeys.add(getArtifactRootedPath(mainFile));
for (Artifact artifact : ninjaSrcs) {
depKeys.add(getArtifactRootedPath(artifact));
}
env.getValues(depKeys);
}
private static SkyKey getArtifactRootedPath(Artifact artifact) {
return FileValue.key(
RootedPath.toRootedPath(artifact.getRoot().getRoot(), artifact.getRootRelativePath()));
}
private static void separatePhonyTargets(
List<NinjaTarget> ninjaTargets,
ImmutableSortedMap.Builder<PathFragment, NinjaTarget> usualTargetsBuilder,
ImmutableSortedMap.Builder<PathFragment, NinjaTarget> phonyTargetsBuilder)
throws RuleErrorException {
for (NinjaTarget target : ninjaTargets) {
if ("phony".equals(target.getRuleName())) {
if (target.getAllOutputs().size() != 1) {
String allOutputs =
target.getAllOutputs().stream()
.map(PathFragment::getPathString)
.collect(Collectors.joining(" "));
throw new RuleErrorException(
String.format(
"Ninja phony alias can only be used for single output, but found '%s'.",
allOutputs));
}
phonyTargetsBuilder.put(Iterables.getOnlyElement(target.getAllOutputs()), target);
} else {
for (PathFragment output : target.getAllOutputs()) {
usualTargetsBuilder.put(output, target);
}
}
}
}
private static List<NinjaTarget> readNinjaGraph(
Artifact mainArtifact,
Collection<Path> childNinjaFiles,
PathFragment workingDirectory,
Path workspace,
String ownerTargetName)
throws InterruptedException, RuleErrorException {
ListeningExecutorService service = MoreExecutors.listeningDecorator(NINJA_POOL);
try {
NinjaPipeline pipeline =
new NinjaPipeline(
workspace.getRelative(workingDirectory), service, childNinjaFiles, ownerTargetName);
return pipeline.pipeline(mainArtifact.getPath());
} catch (GenericParsingException | IOException e) {
throw new RuleErrorException(e.getMessage());
}
}
}