blob: 5bd0c8e7c6ee5bbb7ffaed028ab13e2391331473 [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.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
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.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionExecutionMetadata;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactFactory;
import com.google.devtools.build.lib.actions.ArtifactRoot;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.MissingDepException;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor;
import com.google.devtools.build.lib.concurrent.ErrorClassifier;
import com.google.devtools.build.lib.concurrent.ThreadSafety;
import com.google.devtools.build.lib.includescanning.IncludeParser.Hints;
import com.google.devtools.build.lib.includescanning.IncludeParser.Inclusion;
import com.google.devtools.build.lib.includescanning.IncludeParser.Inclusion.Kind;
import com.google.devtools.build.lib.rules.cpp.IncludeScanner;
import com.google.devtools.build.lib.vfs.IORuntimeException;
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.skyframe.SkyFunction;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
/**
* C include scanner. Quickly scans C/C++ source files to determine the bounding set of transitively
* referenced include files.
*
* <p>Maintains caches for parses and search-matches for performance.
*
* <pre>
* TODO(bazel-team): (2009) Currently does not evaluate preprocessor symbols, so computed includes
* are ignored.
* TODO(bazel-team): (2009) Does not handle multiline block comments preceding or around an #include
* </pre>
*/
public class LegacyIncludeScanner implements IncludeScanner {
private static final class ArtifactWithInclusionContext {
private final Artifact artifact;
private final Kind contextKind;
private final int contextPathPos;
private ArtifactWithInclusionContext(Artifact artifact, Kind contextKind, int contextPathPos) {
this.artifact = artifact;
this.contextKind = contextKind;
this.contextPathPos = contextPathPos;
}
@Override
public int hashCode() {
return contextPathPos + 37 * Objects.hash(contextKind, artifact);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ArtifactWithInclusionContext)) {
return false;
}
ArtifactWithInclusionContext that = (ArtifactWithInclusionContext) obj;
return this.contextKind == that.contextKind
&& this.contextPathPos == that.contextPathPos
&& this.artifact.equals(that.artifact);
}
}
/**
* A cache of inclusion lookups, taking care to avoid spurious caching related to generated
* headers / source files.
*/
@ThreadSafety.ThreadSafe
private abstract class InclusionCache {
/**
* Locates an included file along the search paths. The result is cacheable.
*
* @param inclusion the inclusion to locate
* @param pathToLegalOutputArtifact generated files which may be reached during scanning
* @param onlyCheckGenerated if true, only search for generated output files
* @return a tuple of the found file, the position of the respective include path entry on the
* search path (or null if no matching file was found), and whether the scan touched illegal
* output files
*/
protected LocateOnPathResult locateOnPaths(
InclusionWithContext inclusion,
Map<PathFragment, Artifact> pathToLegalOutputArtifact,
boolean onlyCheckGenerated) {
PathFragment name = inclusion.getInclusion().pathFragment;
// A framework header must begin with a framework name, followed by a path separator, followed
// by the rest of the header path. We do not currently support include_next of framework
// headers.
boolean searchFrameworkIncludePaths =
!frameworkIncludePaths.isEmpty()
&& !inclusion.getInclusion().kind.isNext()
&& !name.containsUplevelReferences()
&& PathFragment.containsSeparator(name.getPathString());
// For #include_next directives we start searching on the include path where
// we found the previous inclusion.
int searchStart = inclusion.getInclusion().kind.isNext() ? inclusion.getContextPathPos() : 0;
// Search the header on the remaining paths.
List<PathFragment> paths =
inclusion.getContextKind() == Kind.QUOTE ? quoteIncludePaths : includePaths;
int alsoSearchFrameworkAtIndex =
inclusion.getContextKind() == Kind.QUOTE ? quoteIncludePathsFrameworkIndex : 0;
alsoSearchFrameworkAtIndex = Math.max(alsoSearchFrameworkAtIndex, searchStart);
boolean viewedIllegalOutput = false;
for (int i = searchStart; i < paths.size(); ++i) {
if (i == alsoSearchFrameworkAtIndex && searchFrameworkIncludePaths) {
String frameworkName = name.subFragment(0, 1).getPathString() + ".framework";
PathFragment relHeaderPath = name.subFragment(1);
LocateOnPathResult result =
locateOnFrameworkPaths(
frameworkName,
relHeaderPath,
pathToLegalOutputArtifact,
onlyCheckGenerated,
viewedIllegalOutput);
if (result.path != null) {
return result;
}
viewedIllegalOutput = viewedIllegalOutput || result.viewedIllegalOutputFile;
}
PathFragment fileFragment = paths.get(i).getRelative(name);
if (fileFragment.containsUplevelReferences()) {
// TODO(janakr): This branch shouldn't be necessary: we should be able to filter such
// inclusions out unconditionally.
// Deal with fragments that escape the execroot. They most likely come right back in.
Path execRootRelativePath = execRoot.getRelative(fileFragment);
if (execRootRelativePath.startsWith(execRoot)) {
// Common case: transform #include "../execroot/foo.h" into #include "foo.h"
fileFragment = execRootRelativePath.relativeTo(execRoot);
} else {
// Ugh: successfully escaped the exec root. It's their funeral.
fileFragment = execRootRelativePath.asFragment();
}
// This can happen when we are processing Windows paths with backslashes on Unix,
// since we do not do any #ifdef processing.
// We can safely discard these here.
if (fileFragment.containsUplevelReferences()) {
continue;
}
}
if (onlyCheckGenerated && !isRealOutputFile(fileFragment)) {
continue;
}
viewedIllegalOutput =
viewedIllegalOutput
|| isIllegalOutputFile(fileFragment, pathToLegalOutputArtifact.keySet());
boolean isOutputDirectory = fileFragment.startsWith(outputPathFragment);
if (!isFile(fileFragment, name, !isOutputDirectory, pathToLegalOutputArtifact.keySet())) {
continue;
}
Artifact artifact;
if (isOutputDirectory) {
// May be a normal output file or an inc_library header.
artifact = pathToLegalOutputArtifact.get(fileFragment);
if (artifact == null) {
// This happens if an included file exists in a cc_inc_library's output directory,
// but is not an output of the cc_inc_library. This can happen if, for instance, the
// definition of the cc_inc_library is changed to output different files, but the
// source file's includes don't change.
// Often, such an include is conditional, and so failing to find it here will not
// lead to problems. If this include is actually needed for compilation, then we will
// emit a somewhat unhelpful error message of a missing file, rather than the more
// helpful one of an illegal include, but it's hard to emit the illegal include
// message consistently, and this is a rare occurrence in any case.
return LocateOnPathResult.createNotFound(viewedIllegalOutput);
}
} else if (!fileFragment.isAbsolute()) {
artifact = artifactFactory.resolveSourceArtifact(fileFragment, RepositoryName.MAIN);
if (artifact == null) {
// There was a real file, but we couldn't resolve it, probably because it belonged to
// a package that wasn't actually loaded this build, so user cannot refer to files in
// that package.
continue;
}
} else {
// This file is given with an absolute path. We will error out after transitive scanning
// of the top-level source is finished unless this corresponds to a built-in include
// directory, and will ignore this artifact in any case, but track it here so that its
// includes can be processed.
artifact = artifactFactory.getSourceArtifact(fileFragment, absoluteRoot);
}
// +1 to account for the virtual entry for relative includes.
return LocateOnPathResult.create(artifact, i + 1, viewedIllegalOutput);
}
// Not found.
return LocateOnPathResult.createNotFound(viewedIllegalOutput);
}
/**
* Locates an included file along the framework search paths. The result is cacheable.
*
* @param frameworkName the name of the framework, including the ".framework" suffix
* @param relHeaderPath the path of the framework header, relative to the framework
* @param pathToLegalOutputArtifact generated files which may be reached during scanning
* @param onlyCheckGenerated if true, only search for generated output files
* @param viewedIllegalOutput whether the scanner has viewed an illegal output file.
* @return a tuple of the found file, the context path position of the input inclusion, and
* whether the scan touched illegal output files
*/
private LocateOnPathResult locateOnFrameworkPaths(
String frameworkName,
PathFragment relHeaderPath,
Map<PathFragment, Artifact> pathToLegalOutputArtifact,
boolean onlyCheckGenerated,
boolean viewedIllegalOutput) {
Set<PathFragment> outputArtifactPaths = pathToLegalOutputArtifact.keySet();
for (int i = 0; i < frameworkIncludePaths.size(); ++i) {
PathFragment includePath = frameworkIncludePaths.get(i);
// Construct the full framework path path/to/foo.framework.
PathFragment fullFrameworkPath = includePath.getRelative(frameworkName);
if (onlyCheckGenerated && !isRealOutputFile(fullFrameworkPath)) {
return LocateOnPathResult.createNotFound(viewedIllegalOutput);
}
// Look for header in path/to/foo.framework/Headers/
PathFragment foundHeaderPath = null;
PathFragment fullHeaderPath =
fullFrameworkPath.getRelative("Headers").getRelative(relHeaderPath);
viewedIllegalOutput =
viewedIllegalOutput || isIllegalOutputFile(fullHeaderPath, outputArtifactPaths);
boolean isOutputDirectory = fullHeaderPath.startsWith(outputPathFragment);
if (isFile(fullHeaderPath, relHeaderPath, isOutputDirectory, outputArtifactPaths)) {
foundHeaderPath = fullHeaderPath;
} else {
// Look for header in path/to/foo.framework/PrivateHeaders/
fullHeaderPath =
fullFrameworkPath.getRelative("PrivateHeaders").getRelative(relHeaderPath);
viewedIllegalOutput =
viewedIllegalOutput || isIllegalOutputFile(fullHeaderPath, outputArtifactPaths);
if (isFile(fullHeaderPath, relHeaderPath, isOutputDirectory, outputArtifactPaths)) {
foundHeaderPath = fullHeaderPath;
} else {
continue;
}
}
Artifact artifact;
if (isOutputDirectory) {
artifact = pathToLegalOutputArtifact.get(foundHeaderPath);
if (artifact == null) {
// This happens if an included file exists in a framework directory but is not but is
// not an output of the framework rule.
// Such an include may be conditional, and so failing to find it here will not lead to
// problems. If this include is actually needed for compilation, then we will emit a
// somewhat unhelpful error message of a missing file, rather than the more helpful one
// of an illegal include, but it's hard to emit the illegal include message
// consistently, and this is a rare occurrence in any case.
// Note that the corresponding case for non-framework paths aborts the search here, but
// for framdwork paths, we keep going like in other cases where we can't find a header
// we have access to.
continue;
}
} else if (!foundHeaderPath.isAbsolute()) {
artifact = artifactFactory.resolveSourceArtifact(foundHeaderPath, RepositoryName.MAIN);
if (artifact == null) {
// There was a real file, but we couldn't resolve it, probably because it belonged to
// a package that wasn't actually loaded this build, so user cannot refer to files in
// that package.
continue;
}
} else {
// This file is given with an absolute path. We will error out after transitive scanning
// of the top-level source is finished unless this corresponds to a built-in include
// directory, and will ignore this artifact in any case, but track it here so that its
// includes can be processed.
artifact = artifactFactory.getSourceArtifact(foundHeaderPath, absoluteRoot);
}
// Reset contextPathPos to 0 so that include_next in a framework header searches the include
// paths from the beginning.
return LocateOnPathResult.create(artifact, 0, viewedIllegalOutput);
}
// Not found.
return LocateOnPathResult.createNotFound(viewedIllegalOutput);
}
/**
* Locates an included file along the search paths.
*
* @param inclusion the inclusion to locate
* @param pathToLegalOutputArtifact generated files which may be reached during scanning
* @return a LocateOnPathResult
*/
protected abstract LocateOnPathResult lookup(
InclusionWithContext inclusion, Map<PathFragment, Artifact> pathToLegalOutputArtifact);
/**
* Locates an included file along the search paths.
*
* @param inclusion the inclusion to locate
* @param pathToLegalOutputArtifact generated files which may be reached during scanning
* @return a LocateOnPathResult
*/
protected abstract ListenableFuture<LocateOnPathResult> lookupAsync(
InclusionWithContext inclusion, Map<PathFragment, Artifact> pathToLegalOutputArtifact);
}
/**
* A cache of inclusion lookups, taking care to avoid spurious caching related to generated
* headers / source files.
*/
@ThreadSafety.ThreadSafe
private class LegacyInclusionCache extends InclusionCache {
private final ConcurrentMap<InclusionWithContext, LocateOnPathResult> cache =
new ConcurrentHashMap<>();
@Override
public LocateOnPathResult lookup(
InclusionWithContext inclusion, Map<PathFragment, Artifact> pathToLegalOutputArtifact) {
LocateOnPathResult result =
cache.computeIfAbsent(
inclusion, key -> locateOnPaths(key, pathToLegalOutputArtifact, false));
// If the previous computation for this inclusion had a different pathToLegalOutputArtifact
// map, result may not be valid for this lookup. Because this is a hot spot, we tolerate a
// known correctness bug but try to catch most issues.
// (1) [correct]: The prior computation found an output file, but that file is not in the
// current lookup's inputs. We don't reuse the computation. b/149935208.
// (2) [correct]: The prior computation checked an output path not in its legal outputs, and
// then didn't find a file anywhere. However, that output file is a legal input for this
// lookup. We don't reuse the computation. b/2097998.
// (3) [INCORRECT]: Same as (2), except that the prior computation found a file after checking
// the output path not in its legal inputs. We incorrectly cache this computation, assuming it
// is very rare. b/150307245.
if (result.path != null) {
if (result.path.isSourceArtifact()
|| result.path.equals(pathToLegalOutputArtifact.get(result.path.getExecPath()))) {
return result;
}
} else if (!result.viewedIllegalOutputFile) {
return result;
}
result = locateOnPaths(inclusion, pathToLegalOutputArtifact, true);
if (result.path != null || !result.viewedIllegalOutputFile) {
// In this case, the result is now cachable either because a file has been found or
// because there are no more illegal output files. This is rare in practice. Avoid
// creating a future and modifying the cache in the common case.
cache.put(inclusion, result);
}
return result;
}
@Override
protected ListenableFuture<LocateOnPathResult> lookupAsync(
InclusionWithContext inclusion, Map<PathFragment, Artifact> pathToLegalOutputArtifact) {
throw new UnsupportedOperationException();
}
}
/**
* A cache of inclusion lookups, taking care to avoid spurious caching related to generated
* headers / source files.
*/
@ThreadSafety.ThreadSafe
private class AsyncInclusionCache extends InclusionCache {
private final ConcurrentMap<InclusionWithContext, ListenableFuture<LocateOnPathResult>> cache =
new ConcurrentHashMap<>();
@Override
protected LocateOnPathResult lookup(
InclusionWithContext inclusion, Map<PathFragment, Artifact> pathToLegalOutputArtifact) {
throw new UnsupportedOperationException();
}
@Override
public ListenableFuture<LocateOnPathResult> lookupAsync(
InclusionWithContext inclusion, Map<PathFragment, Artifact> pathToLegalOutputArtifact) {
SettableFuture<LocateOnPathResult> future = SettableFuture.create();
ListenableFuture<LocateOnPathResult> previous = cache.putIfAbsent(inclusion, future);
ListenableFuture<LocateOnPathResult> actualFuture;
if (previous == null) {
includePool.execute(
() -> {
LocateOnPathResult result =
locateOnPaths(inclusion, pathToLegalOutputArtifact, false);
future.set(result);
});
actualFuture = future;
} else {
actualFuture = previous;
}
// It is not safe to cache lookups which viewed illegal output files. Their nonexistence do
// not imply nonexistence for actions using the same include scanner, but executed later on.
// See bug 2097998. For performance reasons, take a small shortcut: only avoid caching when
// the path lookup result from locateOnPaths() is empty.
return Futures.transformAsync(
actualFuture,
(result) -> {
if (result.path != null || !result.viewedIllegalOutputFile) {
return Futures.immediateFuture(result);
}
result = locateOnPaths(inclusion, pathToLegalOutputArtifact, true);
if (result.path != null || !result.viewedIllegalOutputFile) {
// In this case, the result is now cachable either because a file has been found or
// because there are no more illegal output files. This is rare in practice. Avoid
// creating a future and modifying the cache in the common case.
ListenableFuture<LocateOnPathResult> replacement = Futures.immediateFuture(result);
cache.put(inclusion, replacement);
return replacement;
}
return Futures.immediateFuture(result);
},
MoreExecutors.directExecutor());
}
}
private static class LocateOnPathResult {
private static final LocateOnPathResult NOT_FOUND_VIEWED_ILLEGAL =
new LocateOnPathResult(null, -1, true);
private static final LocateOnPathResult NOT_FOUND_NO_VIEWED_ILLEGAL =
new LocateOnPathResult(null, -1, false);
private final Artifact path;
private final int includePosition;
private final boolean viewedIllegalOutputFile;
private LocateOnPathResult(
Artifact path, int includePosition, boolean viewedIllegalOutputFile) {
this.path = path;
this.includePosition = includePosition;
this.viewedIllegalOutputFile = viewedIllegalOutputFile;
}
static LocateOnPathResult create(
Artifact path, int includePosition, boolean viewedIllegalOutputFile) {
return new LocateOnPathResult(
Preconditions.checkNotNull(path), includePosition, viewedIllegalOutputFile);
}
static LocateOnPathResult createNotFound(boolean viewedIllegalOutputFile) {
return viewedIllegalOutputFile ? NOT_FOUND_VIEWED_ILLEGAL : NOT_FOUND_NO_VIEWED_ILLEGAL;
}
}
private final Path execRoot;
private final ArtifactFactory artifactFactory;
private final Supplier<SpawnIncludeScanner> spawnIncludeScannerSupplier;
/**
* Externally-scoped cache of file path => parsed inclusion set mappings. Saves us from having to
* parse files more than once, and can be shared by scanners with different search paths.
*/
private final ConcurrentMap<Artifact, ListenableFuture<Collection<Inclusion>>> fileParseCache;
private final IncludeParser parser;
/**
* Search path for searching for all quoted "xyz.h" includes, composed of all the -iquote, -I and
* -isystem paths (in this order).
*/
private final ImmutableList<PathFragment> quoteIncludePaths;
/**
* The index position within quoteIncludePaths at which framework paths (-F) should be searched.
*/
private final int quoteIncludePathsFrameworkIndex;
/**
* Search path for searching for all includes, composed of all the -I and -isystem paths (in this
* order).
*/
private final List<PathFragment> includePaths;
/** Search path for searching for all includes from frameworks. */
private final ImmutableList<PathFragment> frameworkIncludePaths;
private final PathFragment includeRootFragment;
private final PathFragment outputPathFragment;
private final Root absoluteRoot;
/**
* Scanner-scoped cache of inclusions with their resolved files and include path entries. This
* cache is specific to a given pair of search paths, and is thus scanner-local.
*
* <p>Each inclusion (name+type+context) is associated with its resolved file here, thus saving
* redundant path searches. The second entry of the pair is the include path entry on which the
* file was found.
*/
private final InclusionCache inclusionCache;
private final PathExistenceCache pathCache;
private final ExecutorService includePool;
private final boolean useAsyncIncludeScanner;
// We are using this Random just for shuffling, so keep the order deterministic by hardcoding
// the seed.
private static final Random CONSTANT_SEED_RANDOM = new Random(88);
/**
* Constructs a new IncludeScanner
*
* @param cache externally scoped cache of file-path to inclusion-set mappings
* @param pathCache include path existence cache
* @param quoteIncludePaths the list of quote search path dirs (-iquote)
* @param includePaths the list of all other non-framework search path dirs (-I and -isystem)
* @param frameworkIncludePaths the list of framework other search path dirs (-F)
*/
LegacyIncludeScanner(
IncludeParser parser,
ExecutorService includePool,
ConcurrentMap<Artifact, ListenableFuture<Collection<Inclusion>>> cache,
PathExistenceCache pathCache,
List<PathFragment> quoteIncludePaths,
List<PathFragment> includePaths,
List<PathFragment> frameworkIncludePaths,
Path outputPath,
Path execRoot,
ArtifactFactory artifactFactory,
Supplier<SpawnIncludeScanner> spawnIncludeScannerSupplier,
boolean useAsyncIncludeScanner) {
this.parser = parser;
this.includePool = includePool;
this.fileParseCache = cache;
this.pathCache = pathCache;
this.artifactFactory = Preconditions.checkNotNull(artifactFactory);
this.spawnIncludeScannerSupplier = spawnIncludeScannerSupplier;
this.quoteIncludePaths = ImmutableList.<PathFragment>builder()
.addAll(quoteIncludePaths)
.addAll(includePaths)
.build();
this.quoteIncludePathsFrameworkIndex = quoteIncludePaths.size();
this.includePaths = ImmutableList.copyOf(includePaths);
this.frameworkIncludePaths = ImmutableList.copyOf(frameworkIncludePaths);
this.inclusionCache =
useAsyncIncludeScanner ? new AsyncInclusionCache() : new LegacyInclusionCache();
this.execRoot = execRoot;
this.outputPathFragment = outputPath.relativeTo(execRoot);
this.includeRootFragment =
outputPathFragment.getRelative(BlazeDirectories.RELATIVE_INCLUDE_DIR);
this.absoluteRoot = Root.absoluteRoot(execRoot.getFileSystem());
this.useAsyncIncludeScanner = useAsyncIncludeScanner;
}
/**
* Locates an included file relative to the including file. The result is not cacheable.
*
* @param inclusion the inclusion to locate
* @param includer the including file
* @return the resolved Path, or null if no file could be found
*/
private Artifact locateRelative(
Inclusion inclusion, Map<PathFragment, Artifact> legalOutputFiles, Artifact includer) {
if (inclusion.kind != Kind.QUOTE) {
return null;
}
PathFragment name = inclusion.pathFragment;
PathFragment execPath = includer.getExecPath().getParentDirectory().getRelative(name);
if (!isFile(execPath, name, includer.isSourceArtifact(), legalOutputFiles.keySet())) {
return null;
}
PathFragment parentDirectory = includer.getRootRelativePath().getParentDirectory();
PathFragment rootRelativePath = parentDirectory.getRelative(name);
if (rootRelativePath.containsUplevelReferences()) {
// An include cannot break out of a (package path) root via a relative inclusion. It should
// also not break out of the root and then come back into it -- who knows what hardcoded
// directory names there are in it.
return null;
}
if (legalOutputFiles.containsKey(execPath)) {
return legalOutputFiles.get(execPath);
}
ArtifactRoot root = includer.getRoot();
Artifact sourceArtifact =
artifactFactory.resolveSourceArtifactWithAncestor(
name, parentDirectory, root, RepositoryName.MAIN);
if (sourceArtifact == null) {
// If the name had up-level references, this path may not be under any package. Otherwise,
// we must have gotten an artifact, since it should be under the same package as the
// including artifact.
Preconditions.checkState(
name.containsUplevelReferences(),
"%s %s %s %s",
name,
parentDirectory,
rootRelativePath,
root);
}
return sourceArtifact;
}
/** Returns whether the given path exists in the filesystem. */
private boolean isFile(
PathFragment execPath, PathFragment includeAsWritten, boolean isSource,
Collection<PathFragment> legalOutputFiles) {
if (isRealOutputFile(execPath)) {
return legalOutputFiles.contains(execPath);
}
// TODO(djasper): This code path cannot be hit with isSource being false. Verify and add
// Preconditions check.
if (isSource && !execPath.isAbsolute() && execPath.endsWith(includeAsWritten)) {
// Verify that the directory of execPath exists as an optimization. Most includes are relative
// to the workspace and we'd like to avoid stat'ing every such include relative to every
// include path. If testing whether "a/b/c.h" is a file beneath the include path "e/f/",
// verify that "e/f/a" and "e/f/a/b" are valid directories (and cache the result).
int execPathSegments = execPath.segmentCount();
int nameSegments = includeAsWritten.segmentCount();
for (int i = execPathSegments - nameSegments + 1; i < execPathSegments; i++) {
if (!pathCache.directoryExists(execPath.subFragment(0, i))) {
return false;
}
}
}
return pathCache.fileExists(execPath, isSource);
}
@Override
public ListenableFuture<?> processAsync(
Artifact mainSource,
Collection<Artifact> sources,
IncludeScanningHeaderData includeScanningHeaderData,
List<String> cmdlineIncludes,
Set<Artifact> includes,
ActionExecutionMetadata actionExecutionMetadata,
ActionExecutionContext actionExecutionContext,
Artifact grepIncludes)
throws IOException, ExecException, InterruptedException {
ImmutableSet<Artifact> pathHints =
prepare(actionExecutionContext.getEnvironmentForDiscoveringInputs());
IncludeVisitor visitor;
visitor =
useAsyncIncludeScanner
? new AsyncIncludeVisitor(
actionExecutionMetadata,
actionExecutionContext,
grepIncludes,
includeScanningHeaderData.getPathToLegalOutputArtifact(),
includeScanningHeaderData.getModularHeaders())
: new LegacyIncludeVisitor(
actionExecutionMetadata,
actionExecutionContext,
grepIncludes,
includeScanningHeaderData.getPathToLegalOutputArtifact(),
includeScanningHeaderData.getModularHeaders());
return visitor.processInternal(
mainSource,
sources,
cmdlineIncludes,
includes,
pathHints);
}
private ImmutableSet<Artifact> prepare(SkyFunction.Environment env) throws InterruptedException {
if (parser.getHints() != null) {
Collection<Artifact> artifacts =
parser.getHints().getPathLevelHintedInclusions(quoteIncludePaths, env);
if (env.valuesMissing()) {
throw new MissingDepException();
}
ImmutableSet.Builder<Artifact> pathHints;
pathHints = ImmutableSet.builderWithExpectedSize(quoteIncludePaths.size());
pathHints.addAll(Preconditions.checkNotNull(artifacts, quoteIncludePaths));
return pathHints.build();
}
return ImmutableSet.of();
}
private static void checkForInterrupt(String operation, Object source)
throws InterruptedException {
// We require passing in the operation and the source Path / Artifact to avoid intermediate
// String operations. The include scanner is performance critical and this showed up in a
// profiler.
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException(
"Include scanning interrupted while " + operation + " " + source);
}
}
private boolean isIllegalOutputFile(
PathFragment includeFile, Collection<PathFragment> legalOutputFiles) {
return isRealOutputFile(includeFile) && !legalOutputFiles.contains(includeFile);
}
private boolean isRealOutputFile(PathFragment path) {
return path.startsWith(outputPathFragment) && !isIncPath(path);
}
private boolean isIncPath(PathFragment path) {
// See CreateIncSymlinkAction and where it's used: The symlink trees
// are always rooted at locations that fit the logic here.
return path.startsWith(includeRootFragment) && !path.equals(includeRootFragment);
}
private interface IncludeVisitor {
ListenableFuture<?> processInternal(
Artifact mainSource,
Collection<Artifact> sources,
List<String> cmdlineIncludes,
Set<Artifact> includes,
ImmutableSet<Artifact> pathHints)
throws InterruptedException, IOException, ExecException;
}
/**
* Implements a potentially parallel traversal over source files using a thread pool shared across
* different IncludeScanner instances.
*/
private class LegacyIncludeVisitor extends AbstractQueueVisitor implements IncludeVisitor {
private final ActionExecutionMetadata actionExecutionMetadata;
private final ActionExecutionContext actionExecutionContext;
private final Artifact grepIncludes;
private final Map<PathFragment, Artifact> pathToLegalOutputArtifact;
/** The set of headers known to be part of a C++ module. Scanning can stop here. */
private final Set<Artifact> modularHeaders;
/** The set of all processed inclusions, to avoid processing duplicate inclusions. */
private final Set<ArtifactWithInclusionContext> visitedInclusions = Sets.newConcurrentHashSet();
LegacyIncludeVisitor(
ActionExecutionMetadata actionExecutionMetadata,
ActionExecutionContext actionExecutionContext,
Artifact grepIncludes,
final Map<PathFragment, Artifact> pathToLegalOutputArtifact,
Set<Artifact> modularHeaders) {
super(
includePool,
/*shutdownOnCompletion=*/ false,
/*failFastOnException=*/ true,
ErrorClassifier.DEFAULT);
this.actionExecutionMetadata = actionExecutionMetadata;
this.actionExecutionContext = actionExecutionContext;
this.grepIncludes = grepIncludes;
this.pathToLegalOutputArtifact = pathToLegalOutputArtifact;
this.modularHeaders = modularHeaders;
}
@Override
public ListenableFuture<?> processInternal(
Artifact mainSource,
Collection<Artifact> sources,
List<String> cmdlineIncludes,
Set<Artifact> includes,
ImmutableSet<Artifact> pathHints)
throws InterruptedException, IOException, ExecException {
try {
// Process cmd line includes, if specified.
if (mainSource != null && !cmdlineIncludes.isEmpty()) {
processCmdlineIncludes(mainSource, cmdlineIncludes, includes);
sync();
}
processBulkAsync(sources, includes);
sync();
// Process include hints
// TODO(ulfjack): Make this code go away. Use the new hinted inclusions instead.
Hints hints = parser.getHints();
if (hints != null) {
// Follow "path" hints.
processBulkAsync(pathHints, includes);
// Follow "file" hints for the primary sources.
for (Artifact source : sources) {
processFileLevelHintsAsync(hints, source, includes);
}
sync();
// Follow "file" hints for all included headers, transitively.
Set<Artifact> frontier = includes;
while (!frontier.isEmpty()) {
Set<Artifact> adjacent = Sets.newConcurrentHashSet();
for (Artifact include : frontier) {
processFileLevelHintsAsync(hints, include, adjacent);
}
sync();
// Keep novel nodes as the next frontier.
for (Iterator<Artifact> iter = adjacent.iterator(); iter.hasNext(); ) {
if (!includes.add(iter.next())) {
iter.remove();
}
}
frontier = adjacent;
}
}
} catch (IOException | InterruptedException | ExecException | MissingDepException e) {
// Careful: Do not leak visitation threads if we have an exception in the initial thread.
sync();
throw e;
}
return Futures.immediateFuture(null);
}
/** Block for the completion of all outstanding visitations. */
private void sync() throws IOException, ExecException, InterruptedException {
try {
super.awaitQuiescence(true);
} catch (InterruptedException e) {
throw new InterruptedException("Interrupted during include visitation");
} catch (IORuntimeException e) {
throw e.getCauseIOException();
} catch (ExecRuntimeException e) {
throw e.getRealCause();
} catch (InterruptedRuntimeException e) {
throw e.getRealCause();
}
}
/**
* Processes a given file for includes and populates the provided set with the visited includes.
*
* @param source the file to process
* @param contextPathPos the position on the include path where the containing file was found,
* or <code>-1</code> for top-level inclusions
* @param contextKind the kind how the containing file was included, or null for top-level
* inclusions
* @param visited the set to receive the files that are transitively included by {@code source}
*/
private void process(
final Artifact source, int contextPathPos, Kind contextKind, Set<Artifact> visited)
throws IOException, ExecException, InterruptedException {
checkForInterrupt("processing", source);
Collection<Inclusion> inclusions;
try {
inclusions =
fileParseCache
.computeIfAbsent(
source,
file -> {
try {
return Futures.immediateFuture(
parser.extractInclusions(
file,
actionExecutionMetadata,
actionExecutionContext,
grepIncludes,
spawnIncludeScannerSupplier.get(),
isRealOutputFile(source.getExecPath())));
} catch (IOException e) {
throw new IORuntimeException(e);
} catch (ExecException e) {
throw new ExecRuntimeException(e);
} catch (InterruptedException e) {
throw new InterruptedRuntimeException(e);
}
})
.get();
} catch (ExecutionException ee) {
try {
Throwables.throwIfInstanceOf(ee.getCause(), RuntimeException.class);
throw new IllegalStateException(ee.getCause());
} catch (IORuntimeException e) {
throw e.getCauseIOException();
} catch (ExecRuntimeException e) {
throw e.getRealCause();
} catch (InterruptedRuntimeException e) {
throw e.getRealCause();
}
}
Preconditions.checkNotNull(inclusions, source);
// Shuffle the inclusions to get better parallelism. See b/62200470.
List<Inclusion> shuffledInclusions = new ArrayList<>(inclusions);
Collections.shuffle(shuffledInclusions, CONSTANT_SEED_RANDOM);
// For each inclusion: get or locate its target file & recursively process
IncludeScannerHelper helper =
new IncludeScannerHelper(includePaths, quoteIncludePaths, source);
for (Inclusion inclusion : shuffledInclusions) {
findAndProcess(
helper.createInclusionWithContext(inclusion, contextPathPos, contextKind),
source,
visited);
}
}
/**
* Same as {@link #process}, but executes asynchronously if the #include lines of {@code source}
* haven't been extracted yet. For sources that have already been extracted, just continue
* walking them in the current thread. The overhead of scheduling this on other threads is
* larger than the gain in concurrency. The only really slow operation is the (possibly remote)
* extraction of includes.
*/
private void processAsyncIfNotExtracted(
final Artifact source, int contextPathPos, Kind contextKind, Set<Artifact> visited)
throws IOException, ExecException, InterruptedException {
ListenableFuture<Collection<Inclusion>> cacheResult = fileParseCache.get(source);
if (cacheResult != null) {
process(source, contextPathPos, contextKind, visited);
} else {
super.execute(
() -> {
try {
process(source, contextPathPos, contextKind, visited);
} catch (IOException e) {
throw new IORuntimeException(e);
} catch (ExecException e) {
throw new ExecRuntimeException(e);
} catch (InterruptedException e) {
throw new InterruptedRuntimeException(e);
}
});
}
}
/** Visits an inclusion starting from a source file. */
private void findAndProcess(
InclusionWithContext inclusion, Artifact source, Set<Artifact> visited)
throws IOException, ExecException, InterruptedException {
// Try to find the included file relative to the file that contains the inclusion. Relative
// inclusions are handled like the first entry on the quote include path
Artifact includeFile =
locateRelative(inclusion.getInclusion(), pathToLegalOutputArtifact, source);
int contextPathPos = 0;
Kind contextKind = null;
checkForInterrupt("visiting", source);
// If nothing has been found, get an inclusion from the cache. This will automatically search
// on the include paths and populate the cache if necessary.
if (includeFile == null) {
LocateOnPathResult result = inclusionCache.lookup(inclusion, pathToLegalOutputArtifact);
includeFile = result.path;
contextPathPos = result.includePosition;
contextKind = inclusion.getContextKind();
}
// Recursively process the found file (if not yet done).
if (includeFile != null
&& !isIllegalOutputFile(includeFile.getExecPath(), pathToLegalOutputArtifact.keySet())
&& visitedInclusions.add(
new ArtifactWithInclusionContext(includeFile, contextKind, contextPathPos))) {
visited.add(includeFile);
if (modularHeaders.contains(includeFile)) {
return;
}
processAsyncIfNotExtracted(includeFile, contextPathPos, contextKind, visited);
}
}
/**
* Processes a given list of includes for a given base file and populates the provided set with
* the visited includes
*
* @param source the source file used as a reference for finding includes
* @param includes the list of -include option strings to locate and process
* @param visited the set of files that are transitively included by {@code includes} to
* populate
*/
private void processCmdlineIncludes(
Artifact source, List<String> includes, Set<Artifact> visited)
throws IOException, ExecException, InterruptedException {
for (String incl : includes) {
InclusionWithContext inclusion = new InclusionWithContext(incl, Kind.QUOTE);
findAndProcess(inclusion, source, visited);
}
}
/**
* Processes a bunch sources asynchronously and adds them and their included files to the
* provided set.
*
* @param sources the files to process and add to the set
* @param visited the set to receive the files that are transitively included by {@code sources}
*/
private void processBulkAsync(Collection<Artifact> sources, final Set<Artifact> visited)
throws IOException, ExecException, InterruptedException {
for (final Artifact source : sources) {
// TODO(djasper): This looks suspicious. We should only stop based on visitedInclusions.
if (!visited.add(source)) {
continue;
}
processAsyncIfNotExtracted(source, /*contextPathPos=*/ -1, /*contextKind=*/ null, visited);
}
}
private void processFileLevelHintsAsync(
final Hints hints, final Artifact include, final Set<Artifact> alsoVisited) {
Collection<Artifact> sources = hints.getFileLevelHintedInclusionsLegacy(include);
// Early-out if there's nothing to do to avoid enqueuing a closure
if (sources.isEmpty()) {
return;
}
super.execute(
() -> {
try {
processBulkAsync(sources, alsoVisited);
} catch (IOException e) {
throw new IORuntimeException(e);
} catch (ExecException e) {
throw new ExecRuntimeException(e);
} catch (InterruptedException e) {
throw new InterruptedRuntimeException(e);
}
});
}
}
/**
* Implements a potentially parallel traversal over source files using a thread pool shared across
* different IncludeScanner instances.
*/
private class AsyncIncludeVisitor implements IncludeVisitor {
private final ActionExecutionMetadata actionExecutionMetadata;
private final ActionExecutionContext actionExecutionContext;
private final Artifact grepIncludes;
private final Map<PathFragment, Artifact> pathToLegalOutputArtifact;
/** The set of headers known to be part of a C++ module. Scanning can stop here. */
private final Set<Artifact> modularHeaders;
/** The set of all processed inclusions, to avoid processing duplicate inclusions. */
private final Set<ArtifactWithInclusionContext> visitedInclusions = Sets.newConcurrentHashSet();
public AsyncIncludeVisitor(
ActionExecutionMetadata actionExecutionMetadata,
ActionExecutionContext actionExecutionContext,
Artifact grepIncludes,
Map<PathFragment, Artifact> pathToLegalOutputArtifact,
Set<Artifact> modularHeaders) {
this.actionExecutionMetadata = actionExecutionMetadata;
this.actionExecutionContext = actionExecutionContext;
this.grepIncludes = grepIncludes;
this.pathToLegalOutputArtifact = pathToLegalOutputArtifact;
this.modularHeaders = modularHeaders;
}
@Override
public ListenableFuture<?> processInternal(
Artifact mainSource,
Collection<Artifact> sources,
List<String> cmdlineIncludes,
Set<Artifact> includes,
ImmutableSet<Artifact> pathHints)
throws InterruptedException, IOException, ExecException {
try {
ListenableFuture<?> result = Futures.immediateFuture(null);
// Process cmd line includes, if specified.
if (mainSource != null && !cmdlineIncludes.isEmpty()) {
result = processCmdlineIncludesAsync(mainSource, cmdlineIncludes, includes);
}
result =
Futures.transformAsync(result, (v) -> processBulkAsync(sources, includes), includePool);
// Process include hints
// TODO(ulfjack): Make this code go away. Use the new hinted inclusions instead.
Hints hints = parser.getHints();
if (hints != null) {
// Follow "path" hints.
if (!pathHints.isEmpty()) {
result =
Futures.transformAsync(
result, (v) -> processBulkAsync(pathHints, includes), includePool);
}
Collection<Artifact> allHintedIncludes = allHintedIncludes(hints, sources);
if (!allHintedIncludes.isEmpty()) {
result =
Futures.transformAsync(
result, (v) -> processBulkAsync(allHintedIncludes, includes), includePool);
}
// Follow "file" hints for all included headers, transitively.
result =
Futures.transformAsync(
result, (v) -> processOnePass(hints, includes, includes), includePool);
}
return result;
} catch (IOException | InterruptedException | ExecException | MissingDepException e) {
// Careful: Do not leak visitation threads if we have an exception in the initial thread.
throw e;
}
}
private ListenableFuture<Collection<Artifact>> processOnePass(
Hints hints, Collection<Artifact> before, Set<Artifact> includes)
throws InterruptedException, IOException, ExecException {
Set<Artifact> adjacent = Sets.newConcurrentHashSet();
ListenableFuture<?> future = processAllFileLevelHintsAsync(hints, before, adjacent);
return Futures.transformAsync(
future,
(v) -> {
// Keep novel nodes as the next frontier.
for (Iterator<Artifact> iter = adjacent.iterator(); iter.hasNext(); ) {
if (!includes.add(iter.next())) {
iter.remove();
}
}
if (adjacent.isEmpty()) {
return Futures.immediateFuture(null);
}
return processOnePass(hints, adjacent, includes);
},
includePool);
}
/**
* Processes a given file for includes and populates the provided set with the visited includes.
*
* @param source the file to process
* @param contextPathPos the position on the include path where the containing file was found,
* or <code>-1</code> for top-level inclusions
* @param contextKind the kind how the containing file was included, or null for top-level
* inclusions
* @param visited the set to receive the files that are transitively included by {@code source}
*/
private ListenableFuture<?> process(
final Artifact source, int contextPathPos, Kind contextKind, Set<Artifact> visited)
throws IOException, InterruptedException {
checkForInterrupt("processing", source);
ListenableFuture<Collection<Inclusion>> actualFuture;
SettableFuture<Collection<Inclusion>> future = SettableFuture.create();
ListenableFuture<Collection<Inclusion>> previous = fileParseCache.putIfAbsent(source, future);
if (previous == null) {
actualFuture = future;
future.setFuture(
parser.extractInclusionsAsync(
includePool,
source,
actionExecutionMetadata,
actionExecutionContext,
grepIncludes,
spawnIncludeScannerSupplier.get(),
isRealOutputFile(source.getExecPath())));
// When rewinding, we may need to rerun a spawn that previously failed rather than cache the
// failure here, so we remove the cache entry if the future throws. Unfortunately, we can
// only detect that case by actually calling get().
future.addListener(
() -> {
try {
future.get();
} catch (ExecutionException | InterruptedException e) {
fileParseCache.remove(source);
}
},
MoreExecutors.directExecutor());
} else {
actualFuture = previous;
}
if (actualFuture.isDone()) {
Collection<Inclusion> inclusions;
try {
inclusions = actualFuture.get();
} catch (ExecutionException e) {
return actualFuture;
}
return processInclusions(inclusions, source, contextPathPos, contextKind, visited);
} else {
return Futures.transformAsync(
actualFuture,
(inclusions) ->
processInclusions(inclusions, source, contextPathPos, contextKind, visited),
MoreExecutors.directExecutor());
}
}
private ListenableFuture<?> processInclusions(
Collection<Inclusion> inclusions,
Artifact source,
int contextPathPos,
Kind contextKind,
Set<Artifact> visited)
throws IOException, InterruptedException {
Preconditions.checkNotNull(inclusions, source);
// Shuffle the inclusions to get better parallelism. See b/62200470.
// Be careful not to modify the original collection! It's shared between any number of
// threads.
// TODO: Maybe we should shuffle before returning it to avoid the copy?
List<Inclusion> shuffledInclusions = new ArrayList<>(inclusions);
Collections.shuffle(shuffledInclusions, CONSTANT_SEED_RANDOM);
// For each inclusion: get or locate its target file & recursively process
IncludeScannerHelper helper =
new IncludeScannerHelper(includePaths, quoteIncludePaths, source);
List<ListenableFuture<?>> allFutures = new ArrayList<>(shuffledInclusions.size());
for (Inclusion inclusion : shuffledInclusions) {
allFutures.add(
findAndProcess(
helper.createInclusionWithContext(inclusion, contextPathPos, contextKind),
source,
visited));
}
return Futures.allAsList(allFutures);
}
/** Visits an inclusion starting from a source file. */
private ListenableFuture<?> findAndProcess(
InclusionWithContext inclusion, Artifact source, Set<Artifact> visited)
throws IOException, InterruptedException {
// Try to find the included file relative to the file that contains the inclusion. Relative
// inclusions are handled like the first entry on the quote include path
Artifact includeFile =
locateRelative(inclusion.getInclusion(), pathToLegalOutputArtifact, source);
checkForInterrupt("visiting", source);
// If nothing has been found, get an inclusion from the cache. This will automatically search
// on the include paths and populate the cache if necessary.
if (includeFile == null) {
ListenableFuture<LocateOnPathResult> lookupFuture =
inclusionCache.lookupAsync(inclusion, pathToLegalOutputArtifact);
return Futures.transformAsync(
lookupFuture,
(locateOnPathResult) ->
processFound(
locateOnPathResult,
inclusion.getContextKind(),
visited,
pathToLegalOutputArtifact,
visitedInclusions),
MoreExecutors.directExecutor());
} else {
LocateOnPathResult locateOnPathResult = new LocateOnPathResult(includeFile, 0, false);
Kind contextKind = null;
// Recursively process the found file (if not yet done).
return processFound(
locateOnPathResult, contextKind, visited, pathToLegalOutputArtifact, visitedInclusions);
}
}
/** Visits an inclusion starting from a source file. */
private ListenableFuture<?> processFound(
LocateOnPathResult locateOnPathResult,
Kind contextKind,
Set<Artifact> visited,
Map<PathFragment, Artifact> pathToLegalOutputArtifact,
Set<ArtifactWithInclusionContext> visitedInclusions)
throws IOException, InterruptedException {
// Try to find the included file relative to the file that contains the inclusion. Relative
// inclusions are handled like the first entry on the quote include path
Artifact includeFile = locateOnPathResult.path;
int contextPathPos = locateOnPathResult.includePosition;
// Recursively process the found file (if not yet done).
if (includeFile != null
&& !isIllegalOutputFile(includeFile.getExecPath(), pathToLegalOutputArtifact.keySet())
&& visitedInclusions.add(
new ArtifactWithInclusionContext(includeFile, contextKind, contextPathPos))) {
visited.add(includeFile);
if (modularHeaders.contains(includeFile)) {
return Futures.immediateFuture(null);
}
return process(includeFile, contextPathPos, contextKind, visited);
}
return Futures.immediateFuture(null);
}
/**
* Processes a given list of includes for a given base file and populates the provided set with
* the visited includes
*
* @param source the source file used as a reference for finding includes
* @param includes the list of -include option strings to locate and process
* @param visited the set of files that are transitively included by {@code includes} to
* populate
*/
private ListenableFuture<?> processCmdlineIncludesAsync(
Artifact source, List<String> includes, Set<Artifact> visited)
throws IOException, ExecException, InterruptedException {
List<ListenableFuture<?>> allFutures = new ArrayList<>(includes.size());
for (String incl : includes) {
InclusionWithContext inclusion = new InclusionWithContext(incl, Kind.QUOTE);
allFutures.add(findAndProcess(inclusion, source, visited));
}
return Futures.allAsList(allFutures);
}
/**
* Processes a bunch sources asynchronously and adds them and their included files to the
* provided set.
*
* @param sources the files to process and add to the set
* @param visited the set to receive the files that are transitively included by {@code sources}
*/
private ListenableFuture<?> processBulkAsync(
Collection<Artifact> sources, Set<Artifact> visited)
throws IOException, InterruptedException {
if (sources.isEmpty()) {
// Early-out if there's nothing to do.
return Futures.immediateFuture(null);
}
List<ListenableFuture<?>> allFutures = new ArrayList<>(sources.size());
for (final Artifact source : sources) {
// TODO(djasper): This looks suspicious. We should only stop based on visitedInclusions.
if (!visited.add(source)) {
continue;
}
allFutures.add(process(source, /*contextPathPos=*/ -1, /*contextKind=*/ null, visited));
}
return Futures.allAsList(allFutures);
}
private ListenableFuture<?> processAllFileLevelHintsAsync(
Hints hints, Collection<Artifact> sources, Set<Artifact> alsoVisited)
throws InterruptedException, IOException, ExecException {
return processBulkAsync(allHintedIncludes(hints, sources), alsoVisited);
}
private Collection<Artifact> allHintedIncludes(Hints hints, Collection<Artifact> sources) {
List<Artifact> result = new ArrayList<>();
for (Artifact source : sources) {
result.addAll(hints.getFileLevelHintedInclusionsLegacy(source));
}
return result;
}
}
private static class ExecRuntimeException extends RuntimeException {
private final ExecException cause;
public ExecRuntimeException(ExecException e) {
super(e);
this.cause = e;
}
public ExecException getRealCause() {
return cause;
}
}
private static class InterruptedRuntimeException extends RuntimeException {
private final InterruptedException cause;
public InterruptedRuntimeException(InterruptedException e) {
super(e);
this.cause = e;
}
public InterruptedException getRealCause() {
return cause;
}
}
}