| // 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()); | |
| } | |
| } | |
| } | |
| } |