blob: e0f300fdce81c2a9258137cfd164570c631d320e [file] [log] [blame]
// Copyright 2016 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.skyframe;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
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.devtools.build.lib.actions.FileValue;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.cmdline.LabelConstants;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.io.FileSymlinkException;
import com.google.devtools.build.lib.io.FileSymlinkInfiniteExpansionException;
import com.google.devtools.build.lib.io.FileSymlinkInfiniteExpansionUniquenessFunction;
import com.google.devtools.build.lib.io.InconsistentFilesystemException;
import com.google.devtools.build.lib.io.ProcessPackageDirectoryException;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
import com.google.devtools.build.lib.vfs.Dirent;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyframeLookupResult;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import net.starlark.java.eval.StarlarkSemantics;
/**
* Processes a directory that may contain a package and subdirectories for the benefit of processes
* that traverse directories recursively, looking for packages.
*/
public final class ProcessPackageDirectory {
private static final String SENTINEL_FILE_NAME_FOR_NOT_TRAVERSING_SYMLINKS =
"DONT_FOLLOW_SYMLINKS_WHEN_TRAVERSING_THIS_DIRECTORY_VIA_A_RECURSIVE_TARGET_PATTERN";
/** Produces a {@link SkyKey} for the recursive traversal into the specified subdirectory. */
public interface SkyKeyTransformer {
SkyKey makeSkyKey(
RepositoryName repository,
RootedPath subdirectory,
ImmutableSet<PathFragment> excludedSubdirectoriesBeneathSubdirectory);
}
private final BlazeDirectories directories;
private final SkyKeyTransformer skyKeyTransformer;
public ProcessPackageDirectory(
BlazeDirectories directories, SkyKeyTransformer skyKeyTransformer) {
this.directories = directories;
this.skyKeyTransformer = skyKeyTransformer;
}
/**
* Examines {@code rootedPath} to see if it is the location of a package, and to see if it has any
* subdirectory children that should also be examined. Returns a {@link
* ProcessPackageDirectoryResult}, or {@code null} if required dependencies were missing.
*/
@Nullable
public ProcessPackageDirectoryResult getPackageExistenceAndSubdirDeps(
RootedPath rootedPath,
RepositoryName repositoryName,
ImmutableSet<PathFragment> excludedPaths,
SkyFunction.Environment env)
throws InterruptedException, ProcessPackageDirectorySkyFunctionException {
PathFragment rootRelativePath = rootedPath.getRootRelativePath();
SkyKey fileKey = FileValue.key(rootedPath);
FileValue fileValue;
try {
fileValue = (FileValue) env.getValueOrThrow(fileKey, IOException.class);
} catch (InconsistentFilesystemException e) {
throw new ProcessPackageDirectorySkyFunctionException(rootedPath, e);
} catch (IOException e) {
return reportErrorAndReturn(
"Failed to get information about path", e, rootRelativePath, env.getListener());
}
if (env.valuesMissing()) {
return null;
}
if (!fileValue.isDirectory()) {
return ProcessPackageDirectoryResult.EMPTY_RESULT;
}
if (fileValue.unboundedAncestorSymlinkExpansionChain() != null) {
SkyKey uniquenessKey =
FileSymlinkInfiniteExpansionUniquenessFunction.key(
fileValue.unboundedAncestorSymlinkExpansionChain());
env.getValue(uniquenessKey);
if (env.valuesMissing()) {
return null;
}
FileSymlinkInfiniteExpansionException symlinkException =
new FileSymlinkInfiniteExpansionException(
fileValue.pathToUnboundedAncestorSymlinkExpansionChain(),
fileValue.unboundedAncestorSymlinkExpansionChain());
return reportErrorAndReturn(
symlinkException.getMessage(), symlinkException, rootRelativePath, env.getListener());
}
PackageIdentifier packageId = PackageIdentifier.create(repositoryName, rootRelativePath);
if (packageId.getRepository().isMain() && isConvenienceSymlink(fileValue, rootedPath, env)) {
return ProcessPackageDirectoryResult.EMPTY_RESULT;
}
if (env.valuesMissing()) {
return null;
}
SkyKey pkgLookupKey = PackageLookupValue.key(packageId);
SkyKey dirListingKey = DirectoryListingValue.key(rootedPath);
SkyframeLookupResult pkgLookupAndDirectoryListingDeps =
env.getValuesAndExceptions(ImmutableList.of(pkgLookupKey, dirListingKey));
PackageLookupValue pkgLookupValue;
try {
pkgLookupValue =
(PackageLookupValue)
pkgLookupAndDirectoryListingDeps.getOrThrow(
pkgLookupKey,
NoSuchPackageException.class,
InconsistentFilesystemException.class);
} catch (NoSuchPackageException e) {
return reportErrorAndReturn("Failed to load package", e, rootRelativePath, env.getListener());
} catch (InconsistentFilesystemException e) {
throw new ProcessPackageDirectorySkyFunctionException(rootedPath, e);
}
DirectoryListingValue dirListingValue;
try {
dirListingValue =
(DirectoryListingValue)
pkgLookupAndDirectoryListingDeps.getOrThrow(dirListingKey, IOException.class);
} catch (FileSymlinkException e) {
// DirectoryListingFunction only throws FileSymlinkCycleException when FileFunction throws it,
// but FileFunction was evaluated for rootedPath above, and didn't throw there. It shouldn't
// be able to avoid throwing there but throw here.
throw new IllegalStateException(
"Symlink cycle found after not being found for \"" + rootedPath + "\"", e);
} catch (IOException e) {
return reportErrorAndReturn(
"Failed to list directory contents", e, rootRelativePath, env.getListener());
}
StarlarkSemantics starlarkSemantics = PrecomputedValue.STARLARK_SEMANTICS.get(env);
if (env.valuesMissing()) {
return null;
}
Preconditions.checkNotNull(
pkgLookupValue, "%s %s %s", rootedPath, repositoryName, pkgLookupKey);
Preconditions.checkNotNull(
dirListingValue, "%s %s %s", rootedPath, repositoryName, dirListingKey);
return new ProcessPackageDirectoryResult(
pkgLookupValue.packageExists() && pkgLookupValue.getRoot().equals(rootedPath.getRoot()),
getSubdirDeps(
dirListingValue,
rootedPath,
repositoryName,
excludedPaths,
starlarkSemantics.getBool(BuildLanguageOptions.EXPERIMENTAL_SIBLING_REPOSITORY_LAYOUT)),
/*additionalValuesToAggregate=*/ ImmutableMap.of());
}
// Note that it's not enough to just check for the convenience symlinks themselves,
// because if the value of --symlink_prefix changes, the old symlinks are left in place. It
// is also not sufficient to check whether the symlink points to a directory in the current
// exec root, since this can change between bazel invocations. Therefore we check if the
// suffix of the symlink source suggests it is a convenience symlink, then see if the symlink
// target is in a directory that looks like an execroot. This algorithm also covers more
// creative use cases where people create convenience symlinks somewhere in the directory
// tree manually.
private boolean isConvenienceSymlink(
FileValue fileValue, RootedPath rootedPath, SkyFunction.Environment env)
throws InterruptedException {
if (!fileValue.isSymlink()) {
return false;
}
PathFragment linkTarget = fileValue.getUnresolvedLinkTarget();
if (linkTarget.startsWith(directories.getExecRootBase().asFragment())) {
return true;
}
PathFragment rootRelativePath = rootedPath.getRootRelativePath();
Root root = rootedPath.getRoot();
if (rootRelativePath.getBaseName().endsWith("-bin") && isInExecRoot(linkTarget, root, 4, env)) {
return true;
}
if (rootRelativePath.getBaseName().endsWith("-genfiles")
&& isInExecRoot(linkTarget, root, 4, env)) {
return true;
}
if (rootRelativePath.getBaseName().endsWith("-out") && isInExecRoot(linkTarget, root, 2, env)) {
return true;
}
if (rootRelativePath.getBaseName().endsWith("-testlogs")
&& isInExecRoot(linkTarget, root, 4, env)) {
return true;
}
if (rootRelativePath
.getBaseName()
.endsWith("-" + directories.getWorkingDirectory().getBaseName())
&& isInExecRoot(linkTarget, root, 1, env)) {
return true;
}
return false;
}
private boolean isInExecRoot(PathFragment path, Root root, int depth, SkyFunction.Environment env)
throws InterruptedException {
int segmentCount = path.segmentCount();
if (segmentCount <= depth) {
return false;
}
PathFragment candidateExecRoot = path.subFragment(0, segmentCount - depth);
if (!candidateExecRoot.getBaseName().equals("execroot")) {
return false;
}
Root absoluteRoot = Root.absoluteRoot(root.getFileSystem());
RootedPath doNotBuildPath =
RootedPath.toRootedPath(absoluteRoot, candidateExecRoot.getChild("DO_NOT_BUILD_HERE"));
FileValue doNotBuildValue = (FileValue) env.getValue(FileValue.key(doNotBuildPath));
if (doNotBuildValue == null) {
return false;
}
return doNotBuildValue.exists();
}
private Iterable<SkyKey> getSubdirDeps(
DirectoryListingValue dirListingValue,
RootedPath rootedPath,
RepositoryName repositoryName,
ImmutableSet<PathFragment> excludedPaths,
boolean siblingRepositoryLayout) {
Root root = rootedPath.getRoot();
PathFragment rootRelativePath = rootedPath.getRootRelativePath();
boolean followSymlinks = shouldFollowSymlinksWhenTraversing(dirListingValue.getDirents());
List<SkyKey> childDeps = new ArrayList<>();
for (Dirent dirent : dirListingValue.getDirents()) {
Dirent.Type type = dirent.getType();
if (type != Dirent.Type.DIRECTORY && (type != Dirent.Type.SYMLINK || !followSymlinks)) {
// Non-directories can never host packages. Symlinks to non-directories are weeded out at
// the next level of recursion when we check if its FileValue is a directory. This is slower
// if there are a lot of symlinks in the tree, but faster if there are only a few, which is
// the case most of the time.
//
// We are not afraid of weird symlink structure here: both cyclical ones and ones that give
// rise to infinite directory trees are diagnosed by FileValue.
continue;
}
String basename = dirent.getName();
PathFragment subdirectory = rootRelativePath.getRelative(basename);
if (!siblingRepositoryLayout && subdirectory.equals(LabelConstants.EXTERNAL_PACKAGE_NAME)) {
// Subpackages under //external can be processed only when
// --experimental_sibling_repository_layout is set.
continue;
}
// If this subdirectory is one of the excluded paths, don't recurse into it.
if (excludedPaths.contains(subdirectory)) {
continue;
}
childDeps.add(
skyKeyTransformer.makeSkyKey(
repositoryName,
RootedPath.toRootedPath(root, subdirectory),
getExcludedSubdirectoriesBeneathSubdirectory(subdirectory, excludedPaths)));
}
return childDeps;
}
/**
* Returns the 'excludedPaths' set to use when recursing below this subdirectory. If we have an
* excluded path that isn't below this subdirectory, we shouldn't pass that excluded path to our
* evaluation of the subdirectory, because the exclusion can't possibly match anything beneath the
* subdirectory.
*
* <p>For example, if we're currently evaluating directory "a", are looking at its subdirectory
* "a/b", and we have an excluded path "a/c/d", there's no need to pass the excluded path "a/c/d"
* to our evaluation of "a/b". This strategy should help to get more skyframe sharing. In our
* example, a subsequent request of "a/b/...", without any excluded paths, will be a cache hit.
*
* <p>TODO(bazel-team): Replace the excludedPaths set with a trie or a SortedSet for better
* efficiency.
*/
public static ImmutableSet<PathFragment> getExcludedSubdirectoriesBeneathSubdirectory(
PathFragment subdirectory, ImmutableSet<PathFragment> excludedPaths) {
return excludedPaths.stream()
.filter(pathFragment -> pathFragment.startsWith(subdirectory))
.collect(toImmutableSet());
}
private static ProcessPackageDirectoryResult reportErrorAndReturn(
String errorPrefix, Exception e, PathFragment rootRelativePath, EventHandler handler) {
handler.handle(
Event.error(errorPrefix + ", for " + rootRelativePath + ", skipping: " + e.getMessage()));
return ProcessPackageDirectoryResult.EMPTY_RESULT;
}
private static boolean shouldFollowSymlinksWhenTraversing(Dirents dirents) {
for (Dirent dirent : dirents) {
// This is a special sentinel file whose existence tells Blaze not to follow symlinks when
// recursively traversing through this directory.
//
// This admittedly ugly feature is used to support workspaces with directories with weird
// symlink structures that aren't intended to be consumed by Blaze.
if (dirent.getName().equals(SENTINEL_FILE_NAME_FOR_NOT_TRAVERSING_SYMLINKS)) {
return false;
}
}
return true;
}
/** Wraps {@link InconsistentFilesystemException} in {@link ProcessPackageDirectoryException}. */
public static final class ProcessPackageDirectorySkyFunctionException
extends SkyFunctionException {
public ProcessPackageDirectorySkyFunctionException(
RootedPath directory, InconsistentFilesystemException e) {
super(new ProcessPackageDirectoryException(directory, e), Transience.PERSISTENT);
}
@Override
public boolean isCatastrophic() {
return true;
}
}
}