blob: c666498b01a558d53ed0b7d6e2f8604b88b12ef8 [file] [log] [blame]
// Copyright 2018 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.includescanning;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import com.google.devtools.build.lib.actions.AbstractAction;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionExecutionMetadata;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.ActionInputHelper;
import com.google.devtools.build.lib.actions.ActionKeyContext;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactPathResolver;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.ExecutionRequirements;
import com.google.devtools.build.lib.actions.ResourceSet;
import com.google.devtools.build.lib.actions.RunfilesSupplier;
import com.google.devtools.build.lib.actions.SimpleSpawn;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.SpawnContinuation;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.SpawnStrategy;
import com.google.devtools.build.lib.analysis.platform.PlatformInfo;
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.Order;
import com.google.devtools.build.lib.includescanning.IncludeParser.GrepIncludesFileType;
import com.google.devtools.build.lib.includescanning.IncludeParser.Inclusion;
import com.google.devtools.build.lib.util.io.FileOutErr;
import com.google.devtools.build.lib.vfs.IORuntimeException;
import com.google.devtools.build.lib.vfs.OutputService;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
/**
* C include scanner. Scans C/C++ source files using spawns to determine the bounding set of
* transitively referenced include files.
*/
public class SpawnIncludeScanner {
/** The grep-includes tool is very lightweight, so don't use the default from AbstractAction. */
private static final ResourceSet LOCAL_RESOURCES =
ResourceSet.createWithRamCpu(/*memoryMb=*/ 10, /*cpuUsage=*/ 1);
private final Path execRoot;
private OutputService outputService;
private boolean inMemoryOutput;
private final int remoteExtractionThreshold;
/** Constructs a new SpawnIncludeScanner. */
public SpawnIncludeScanner(Path execRoot, int remoteExtractionThreshold) {
this.execRoot = execRoot;
this.remoteExtractionThreshold = remoteExtractionThreshold;
}
public void setOutputService(OutputService outputService) {
Preconditions.checkState(this.outputService == null);
this.outputService = outputService;
}
public void setInMemoryOutput(boolean inMemoryOutput) {
this.inMemoryOutput = inMemoryOutput;
}
@VisibleForTesting
Path getIncludesOutput(
Artifact src, ArtifactPathResolver resolver, GrepIncludesFileType fileType,
boolean placeNextToFile) {
if (placeNextToFile) {
// If this is an output file, just place the grepped-file next to it. The directory is bound
// to exist.
return resolver.toPath(src)
.getParentDirectory()
.getRelative(src.getFilename() + ".blaze-grepped_includes_" + fileType);
}
return resolver.convertPath(execRoot)
.getChild("blaze-grepped_includes_" + fileType.getFileType())
.getRelative(src.getExecPath());
}
private PathFragment execPath(Path path) {
return path.asFragment().relativeTo(execRoot.asFragment());
}
/** Returns whether "file" should be parsed using this include scanner. */
public boolean shouldParseRemotely(Artifact file, ActionExecutionContext ctx) throws IOException {
// We currently cannot remotely extract inclusions from files that aren't underneath a known
// Blaze root (e.g. that are in /usr/include). Likely, it's not a good idea to look at those in
// the first place as it means we have a non-hermetic build.
// TODO(b/115503807): Fix underlying issue and consider turning this into a precondition check.
if (file.getRoot().getRoot().isAbsolute()) {
return false;
}
// Files written remotely that are not locally available should be scanned remotely to avoid the
// bandwidth and disk space penalty of bringing them across. Also, enable include scanning
// remotely when explicitly directed to via a flag.
return remoteExtractionThreshold == 0
|| (outputService != null && outputService.isRemoteFile(file))
|| ctx.getPathResolver().toPath(file).getFileSize() > remoteExtractionThreshold;
}
/**
* Action for grepping. Is used basically just for ActionStatusMessages (displaying the action
* status to the user as it executes).
*/
private static class GrepIncludesAction implements ActionExecutionMetadata {
private static final String MNEMONIC = "GrepIncludes";
/**
* We don't use this object as the 'resource owner' of the spawn because we want to override the
* mnemonic (among other things, see additional comments below). However, we do delegate
* getOwner, and we may delegate other methods (e.g., getProgressMessage, describe) in the
* future.
*/
private final ActionExecutionMetadata actionExecutionMetadata;
private final String progressMessage;
GrepIncludesAction(ActionExecutionMetadata actionExecutionMetadata, PathFragment input) {
this.actionExecutionMetadata = Preconditions.checkNotNull(actionExecutionMetadata);
this.progressMessage = "Extracting include lines from " + input.getPathString();
}
@Override
public ActionOwner getOwner() {
return actionExecutionMetadata.getOwner();
}
@Override
public String getMnemonic() {
return MNEMONIC;
}
@Override
public boolean isShareable() {
return false;
}
@Override
public String getProgressMessage() {
return progressMessage;
}
@Override
public String describe() {
return getProgressMessage();
}
@Override
public boolean inputsDiscovered() {
throw new UnsupportedOperationException();
}
@Override
public boolean discoversInputs() {
throw new UnsupportedOperationException();
}
@Override
public NestedSet<Artifact> getTools() {
throw new UnsupportedOperationException();
}
@Override
public NestedSet<Artifact> getInputs() {
throw new UnsupportedOperationException();
}
@Override
public RunfilesSupplier getRunfilesSupplier() {
throw new UnsupportedOperationException();
}
@Override
public ImmutableMap<String, String> getExecProperties() {
return actionExecutionMetadata.getExecProperties();
}
@Override
@Nullable
public PlatformInfo getExecutionPlatform() {
return actionExecutionMetadata.getExecutionPlatform();
}
@Override
public ImmutableSet<Artifact> getOutputs() {
// We currently compute orphaned outputs from the Action's list of outputs rather than from
// the Spawn's list of outputs. If we return something here, we need to update that place as
// well.
return ImmutableSet.of();
}
@Override
public Iterable<String> getClientEnvironmentVariables() {
return ImmutableSet.of();
}
@Override
public Artifact getPrimaryInput() {
throw new UnsupportedOperationException();
}
@Override
public Artifact getPrimaryOutput() {
// This violates the contract of ActionExecutionMetadata. Classes that call here are working
// around this returning null. At least some subclasses of CriticalPathComputer are affected.
// TODO(ulfjack): Either fix this or change the contract. See b/111583707 for
// CriticalPathComputer.
return null;
}
@Override
public NestedSet<Artifact> getMandatoryInputs() {
throw new UnsupportedOperationException();
}
@Override
public String getKey(ActionKeyContext actionKeyContext) {
throw new UnsupportedOperationException();
}
@Override
public String describeKey() {
throw new UnsupportedOperationException();
}
@Override
public String prettyPrint() {
// This is called when running with -s (printing all subcommands).
return "(include scanning)";
}
@Override
public NestedSet<Artifact> getInputFilesForExtraAction(
ActionExecutionContext actionExecutionContext) {
throw new UnsupportedOperationException();
}
@Override
public ImmutableSet<Artifact> getMandatoryOutputs() {
// This is called to compute orphaned outputs. See getOutputs.
return ImmutableSet.of();
}
@Override
public MiddlemanType getActionType() {
throw new UnsupportedOperationException();
}
@Override
public boolean shouldReportPathPrefixConflict(ActionAnalysisMetadata action) {
throw new UnsupportedOperationException();
}
}
/** Extracts and returns inclusions from "file" using a spawn. */
public Collection<Inclusion> extractInclusions(
Artifact file,
ActionExecutionMetadata actionExecutionMetadata,
ActionExecutionContext actionExecutionContext,
Artifact grepIncludes,
GrepIncludesFileType fileType,
boolean isOutputFile)
throws IOException, ExecException, InterruptedException {
boolean placeNextToFile = isOutputFile && !file.hasParent();
Path output = getIncludesOutput(file, actionExecutionContext.getPathResolver(), fileType,
placeNextToFile);
if (!inMemoryOutput) {
AbstractAction.deleteOutput(output, placeNextToFile ? file.getRoot() : null);
if (!placeNextToFile) {
output.getParentDirectory().createDirectoryAndParents();
}
}
InputStream dotIncludeStream =
spawnGrep(
file,
execPath(output),
inMemoryOutput,
// We use {@link GrepIncludesAction} primarily to overwrite {@link Action#getMnemonic}.
// You might be tempted to use a custom mnemonic on the Spawn instead, but rest assured
// that _this does not work_. We call Spawn.getResourceOwner().getMnemonic() in a lot of
// places, some of which are downstream from here, and doing so would cause the Spawn
// and its owning ActionExecutionMetadata to be inconsistent with each other.
new GrepIncludesAction(actionExecutionMetadata, file.getExecPath()),
actionExecutionContext,
grepIncludes,
fileType);
return dotIncludeStream == null
? IncludeParser.processIncludes(output)
: IncludeParser.processIncludes(output, dotIncludeStream);
}
/**
* Executes grep-includes.
*
* @param input the file to parse
* @param outputExecPath the output file (exec path)
* @param inMemoryOutput if true, return the contents of the output in the return value instead of
* to the given Path
* @param resourceOwner the resource owner
* @param actionExecutionContext services in the scope of the action. Like the Err/Out stream
* outputs.
* @param fileType Either "c++" or "swig", passed verbatim to grep-includes.
* @return The InputStream of the .includes file if inMemoryOutput feature retrieved it directly.
* Otherwise "null"
* @throws ExecException if scanning fails
*/
// Visible only for CppIncludeExtractionContextImpl.
static InputStream spawnGrep(
Artifact input,
PathFragment outputExecPath,
boolean inMemoryOutput,
ActionExecutionMetadata resourceOwner,
ActionExecutionContext actionExecutionContext,
Artifact grepIncludes,
GrepIncludesFileType fileType)
throws ExecException, InterruptedException {
ActionInput output = ActionInputHelper.fromPath(outputExecPath);
NestedSet<? extends ActionInput> inputs =
NestedSetBuilder.create(Order.STABLE_ORDER, grepIncludes, input);
ImmutableSet<ActionInput> outputs = ImmutableSet.of(output);
ImmutableList<String> command =
ImmutableList.of(
grepIncludes.getExecPathString(),
input.getExecPath().getPathString(),
outputExecPath.getPathString(),
fileType.getFileType());
ImmutableMap.Builder<String, String> execInfoBuilder = ImmutableMap.<String, String>builder();
if (inMemoryOutput) {
execInfoBuilder.put(
ExecutionRequirements.REMOTE_EXECUTION_INLINE_OUTPUTS,
outputExecPath.getPathString());
}
execInfoBuilder.put(ExecutionRequirements.DO_NOT_REPORT, "");
Spawn spawn = new SimpleSpawn(
resourceOwner,
command,
ImmutableMap.of(),
execInfoBuilder.build(),
inputs,
outputs,
LOCAL_RESOURCES);
actionExecutionContext.maybeReportSubcommand(spawn);
// Don't share the originalOutErr across spawnGrep calls. Doing so would not be thread-safe.
FileOutErr originalOutErr = actionExecutionContext.getFileOutErr();
FileOutErr grepOutErr = originalOutErr.childOutErr();
SpawnStrategy strategy = actionExecutionContext.getContext(SpawnStrategy.class);
ActionExecutionContext spawnContext = actionExecutionContext.withFileOutErr(grepOutErr);
List<SpawnResult> results;
try {
results = strategy.exec(spawn, spawnContext);
dump(spawnContext, actionExecutionContext);
} catch (ExecException e) {
dump(spawnContext, actionExecutionContext);
throw e;
}
SpawnResult result = Iterables.getLast(results);
return result.getInMemoryOutput(output);
}
/** Extracts and returns inclusions from "file" using a spawn. */
public ListenableFuture<Collection<Inclusion>> extractInclusionsAsync(
Executor executor,
Artifact file,
ActionExecutionMetadata actionExecutionMetadata,
ActionExecutionContext actionExecutionContext,
Artifact grepIncludes,
GrepIncludesFileType fileType,
boolean isOutputFile)
throws IOException {
boolean placeNextToFile = isOutputFile && !file.hasParent();
Path output =
getIncludesOutput(
file, actionExecutionContext.getPathResolver(), fileType, placeNextToFile);
if (!inMemoryOutput) {
AbstractAction.deleteOutput(output, placeNextToFile ? file.getRoot() : null);
if (!placeNextToFile) {
output.getParentDirectory().createDirectoryAndParents();
}
}
ListenableFuture<InputStream> dotIncludeStreamFuture =
spawnGrepAsync(
executor,
file,
execPath(output),
inMemoryOutput,
// We use {@link GrepIncludesAction} primarily to overwrite {@link Action#getMnemonic}.
// You might be tempted to use a custom mnemonic on the Spawn instead, but rest assured
// that _this does not work_. We call Spawn.getResourceOwner().getMnemonic() in a lot of
// places, some of which are downstream from here, and doing so would cause the Spawn
// and its owning ActionExecutionMetadata to be inconsistent with each other.
new GrepIncludesAction(actionExecutionMetadata, file.getExecPath()),
actionExecutionContext,
grepIncludes,
fileType);
return Futures.transform(
dotIncludeStreamFuture,
(stream) -> {
try {
return IncludeParser.processIncludes(output, stream);
} catch (IOException e) {
throw new IORuntimeException(e);
}
},
MoreExecutors.directExecutor());
}
/**
* Executes grep-includes.
*
* @param input the file to parse
* @param outputExecPath the output file (exec path)
* @param inMemoryOutput if true, return the contents of the output in the return value instead of
* to the given Path
* @param resourceOwner the resource owner
* @param actionExecutionContext services in the scope of the action. Like the Err/Out stream
* outputs.
* @param fileType Either "c++" or "swig", passed verbatim to grep-includes.
* @return The InputStream of the .includes file if inMemoryOutput feature retrieved it directly.
* Otherwise "null"
* @throws ExecException if scanning fails
*/
private static ListenableFuture<InputStream> spawnGrepAsync(
Executor executor,
Artifact input,
PathFragment outputExecPath,
boolean inMemoryOutput,
ActionExecutionMetadata resourceOwner,
ActionExecutionContext actionExecutionContext,
Artifact grepIncludes,
GrepIncludesFileType fileType) {
ActionInput output = ActionInputHelper.fromPath(outputExecPath);
NestedSet<? extends ActionInput> inputs =
NestedSetBuilder.create(Order.STABLE_ORDER, grepIncludes, input);
ImmutableSet<ActionInput> outputs = ImmutableSet.of(output);
ImmutableList<String> command =
ImmutableList.of(
grepIncludes.getExecPathString(),
input.getExecPath().getPathString(),
outputExecPath.getPathString(),
fileType.getFileType());
ImmutableMap.Builder<String, String> execInfoBuilder = ImmutableMap.<String, String>builder();
if (inMemoryOutput) {
execInfoBuilder.put(
ExecutionRequirements.REMOTE_EXECUTION_INLINE_OUTPUTS, outputExecPath.getPathString());
}
execInfoBuilder.put(ExecutionRequirements.DO_NOT_REPORT, "");
Spawn spawn =
new SimpleSpawn(
resourceOwner,
command,
ImmutableMap.of(),
execInfoBuilder.build(),
inputs,
outputs,
LOCAL_RESOURCES);
actionExecutionContext.maybeReportSubcommand(spawn);
// Sharing the originalOutErr across spawnGrep calls would not be thread-safe. Instead, write
// outerr to a temporary location and copy it back to the original after execution, using the
// parent context as a lock to make it thread-safe (see dump() below).
FileOutErr originalOutErr = actionExecutionContext.getFileOutErr();
FileOutErr grepOutErr = originalOutErr.childOutErr();
ActionExecutionContext grepContext = actionExecutionContext.withFileOutErr(grepOutErr);
SpawnContinuation spawnContinuation;
try {
spawnContinuation =
grepContext.getContext(SpawnStrategy.class).beginExecution(spawn, grepContext);
} catch (InterruptedException e) {
dump(grepContext, actionExecutionContext);
return Futures.immediateCancelledFuture();
}
SettableFuture<InputStream> future = SettableFuture.create();
process(executor, future, spawnContinuation, output, grepContext, actionExecutionContext);
return future;
}
private static void process(
Executor executor,
SettableFuture<InputStream> future,
SpawnContinuation continuation,
ActionInput output,
ActionExecutionContext actionExecutionContext,
ActionExecutionContext originalActionExecutionContext) {
continuation
.getFuture()
.addListener(
() -> {
if (continuation.isDone()) {
List<SpawnResult> results = continuation.get();
dump(actionExecutionContext, originalActionExecutionContext);
SpawnResult result = Iterables.getLast(results);
InputStream stream = result.getInMemoryOutput(output);
try {
InputStream finalResult =
stream == null
? actionExecutionContext.getInputPath(output).getInputStream()
: stream;
future.set(finalResult);
} catch (IOException e) {
future.setException(e);
}
} else {
try {
SpawnContinuation next = continuation.execute();
process(
executor,
future,
next,
output,
actionExecutionContext,
originalActionExecutionContext);
} catch (ExecException e) {
dump(actionExecutionContext, originalActionExecutionContext);
future.setException(e);
} catch (InterruptedException e) {
dump(actionExecutionContext, originalActionExecutionContext);
future.cancel(false);
}
}
},
executor);
}
private static void dump(ActionExecutionContext fromContext, ActionExecutionContext toContext) {
if (fromContext.getFileOutErr().hasRecordedOutput()) {
synchronized (toContext) {
FileOutErr.dump(fromContext.getFileOutErr(), toContext.getFileOutErr());
}
}
}
}