blob: 5c4a4349dd0d3744c2b1d1e196fb7bfed912f913 [file] [log] [blame]
// Copyright 2014 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 com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.PackageIdentifier.RepositoryName;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
import com.google.devtools.build.lib.skyframe.RecursivePkgValue.RecursivePkgKey;
import com.google.devtools.build.lib.vfs.Dirent;
import com.google.devtools.build.lib.vfs.Dirent.Type;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.build.skyframe.ValueOrException4;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* RecursiveDirectoryTraversalFunction traverses the subdirectories of a directory, looking for
* and loading packages, and builds up a value from these packages in a manner customized by
* classes that derive from it.
*/
abstract class RecursiveDirectoryTraversalFunction
<TVisitor extends RecursiveDirectoryTraversalFunction.Visitor, TReturn> {
private static final String SENTINEL_FILE_NAME_FOR_NOT_TRAVERSING_SYMLINKS =
"DONT_FOLLOW_SYMLINKS_WHEN_TRAVERSING_THIS_DIRECTORY_VIA_A_RECURSIVE_TARGET_PATTERN";
/**
* Returned from {@link #visitDirectory} if its {@code recursivePkgKey} is a symlink or not a
* directory, or if a dependency value lookup returns an error.
*/
protected abstract TReturn getEmptyReturn();
/**
* Called by {@link #visitDirectory}, which will next call {@link Visitor#visitPackageValue} if
* the {@code recursivePkgKey} specifies a directory with a package, and which will lastly be
* provided to {@link #aggregateWithSubdirectorySkyValues} to compute the {@code TReturn} value
* returned by {@link #visitDirectory}.
*/
protected abstract TVisitor getInitialVisitor();
/**
* Called by {@link #visitDirectory} to get the {@link SkyKey}s associated with recursive
* computation in subdirectories of {@code subdirectory}, excluding directories in
* {@code excludedSubdirectoriesBeneathSubdirectory}, all of which must be proper subdirectories
* of {@code subdirectory}.
*/
protected abstract SkyKey getSkyKeyForSubdirectory(
RepositoryName repository, RootedPath subdirectory,
ImmutableSet<PathFragment> excludedSubdirectoriesBeneathSubdirectory);
/**
* Called by {@link #visitDirectory} to compute the {@code TReturn} value it returns, as a
* function of {@code visitor} and the {@link SkyValue}s computed for subdirectories
* of the directory specified by {@code recursivePkgKey}, contained in
* {@code subdirectorySkyValues}.
*/
protected abstract TReturn aggregateWithSubdirectorySkyValues(
TVisitor visitor, Map<SkyKey, SkyValue> subdirectorySkyValues);
/**
* A type of value used by {@link #visitDirectory} as it checks for a package in the directory
* specified by {@code recursivePkgKey}; if such a package exists, {@link #visitPackageValue}
* is called.
*
* <p>The value is then provided to {@link #aggregateWithSubdirectorySkyValues} to compute the
* value returned by {@link #visitDirectory}.
*/
interface Visitor {
/**
* Called iff the directory contains a package. Provides an {@link Environment} {@code env}
* so that the visitor may do additional lookups. {@link Environment#valuesMissing} will be
* checked afterwards.
*/
void visitPackageValue(Package pkg, Environment env);
}
/**
* Looks in the directory specified by {@code recursivePkgKey} for a package, does some work
* as specified by {@link Visitor} if such a package exists, then recursively does work in each
* non-excluded subdirectory as specified by {@link #getSkyKeyForSubdirectory}, and finally
* aggregates the {@link Visitor} value along with values from each subdirectory as specified
* by {@link #aggregateWithSubdirectorySkyValues}, and returns that aggregation.
*
* <p>Returns null if {@code env.valuesMissing()} is true, checked after each call to one of
* {@link RecursiveDirectoryTraversalFunction}'s abstract methods that were given {@code env}.
* (And after each of {@code visitDirectory}'s own uses of {@code env}, of course.)
*/
TReturn visitDirectory(RecursivePkgKey recursivePkgKey, Environment env) {
RootedPath rootedPath = recursivePkgKey.getRootedPath();
BlacklistedPackagePrefixesValue blacklist =
(BlacklistedPackagePrefixesValue) env.getValue(BlacklistedPackagePrefixesValue.key());
if (blacklist == null) {
return null;
}
Set<PathFragment> excludedPaths =
Sets.union(recursivePkgKey.getExcludedPaths(), blacklist.getPatterns());
Path root = rootedPath.getRoot();
PathFragment rootRelativePath = rootedPath.getRelativePath();
SkyKey fileKey = FileValue.key(rootedPath);
FileValue fileValue;
try {
fileValue = (FileValue) env.getValueOrThrow(fileKey, InconsistentFilesystemException.class,
FileSymlinkException.class, IOException.class);
} catch (InconsistentFilesystemException | FileSymlinkException | IOException e) {
return reportErrorAndReturn("Failed to get information about path", e, rootRelativePath,
env.getListener());
}
if (fileValue == null) {
return null;
}
if (!fileValue.isDirectory()) {
return getEmptyReturn();
}
PackageIdentifier packageId = PackageIdentifier.create(
recursivePkgKey.getRepository(), rootRelativePath);
SkyKey pkgLookupKey = PackageLookupValue.key(packageId);
SkyKey dirListingKey = DirectoryListingValue.key(rootedPath);
Map<SkyKey,
ValueOrException4<
NoSuchPackageException,
InconsistentFilesystemException,
FileSymlinkException,
IOException>> pkgLookupAndDirectoryListingDeps = env.getValuesOrThrow(
ImmutableList.of(pkgLookupKey, dirListingKey),
NoSuchPackageException.class,
InconsistentFilesystemException.class,
FileSymlinkException.class,
IOException.class);
if (env.valuesMissing()) {
return null;
}
PackageLookupValue pkgLookupValue;
try {
pkgLookupValue = (PackageLookupValue) Preconditions.checkNotNull(
pkgLookupAndDirectoryListingDeps.get(pkgLookupKey).get(), "%s %s", recursivePkgKey,
pkgLookupKey);
} catch (NoSuchPackageException | InconsistentFilesystemException e) {
return reportErrorAndReturn("Failed to load package", e, rootRelativePath,
env.getListener());
} catch (IOException | FileSymlinkException e) {
throw new IllegalStateException(e);
}
TVisitor visitor = getInitialVisitor();
if (pkgLookupValue.packageExists()) {
if (pkgLookupValue.getRoot().equals(root)) {
Package pkg = null;
try {
PackageValue pkgValue = (PackageValue)
env.getValueOrThrow(PackageValue.key(packageId), NoSuchPackageException.class);
if (pkgValue == null) {
return null;
}
pkg = pkgValue.getPackage();
if (pkg.containsErrors()) {
env
.getListener()
.handle(
Event.error("package contains errors: " + rootRelativePath.getPathString()));
}
} catch (NoSuchPackageException e) {
// The package had errors, but don't fail-fast as there might be subpackages below the
// current directory.
env
.getListener()
.handle(Event.error("package contains errors: " + rootRelativePath.getPathString()));
}
if (pkg != null) {
visitor.visitPackageValue(pkg, env);
if (env.valuesMissing()) {
return null;
}
}
}
// The package lookup succeeded, but was under a different root. We still, however, need to
// recursively consider subdirectories. For example:
//
// Pretend --package_path=rootA/workspace:rootB/workspace and these are the only files:
// rootA/workspace/foo/
// rootA/workspace/foo/bar/BUILD
// rootB/workspace/foo/BUILD
// If we're doing a recursive package lookup under 'rootA/workspace' starting at 'foo', note
// that even though the package 'foo' is under 'rootB/workspace', there is still a package
// 'foo/bar' under 'rootA/workspace'.
}
DirectoryListingValue dirListingValue;
try {
dirListingValue = (DirectoryListingValue) Preconditions.checkNotNull(
pkgLookupAndDirectoryListingDeps.get(dirListingKey).get(), "%s %s", recursivePkgKey,
dirListingKey);
} catch (InconsistentFilesystemException | IOException e) {
return reportErrorAndReturn("Failed to list directory contents", e, rootRelativePath,
env.getListener());
} 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 + "\"");
} catch (NoSuchPackageException e) {
throw new IllegalStateException(e);
}
List<SkyKey> childDeps = Lists.newArrayList();
boolean followSymlinks = shouldFollowSymlinksWhenTraversing(dirListingValue.getDirents());
for (Dirent dirent : dirListingValue.getDirents()) {
Type type = dirent.getType();
if (type != Type.DIRECTORY
&& (type != Type.SYMLINK || (type == 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();
if (rootRelativePath.equals(PathFragment.EMPTY_FRAGMENT)
&& PathPackageLocator.DEFAULT_TOP_LEVEL_EXCLUDES.contains(basename)) {
continue;
}
PathFragment subdirectory = rootRelativePath.getRelative(basename);
// If this subdirectory is one of the excluded paths, don't recurse into it.
if (excludedPaths.contains(subdirectory)) {
continue;
}
// 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.
//
// 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. Consider the example above. A
// subsequent request of "a/b/...", without any excluded paths, will be a cache hit.
//
// TODO(bazel-team): Replace the excludedPaths set with a trie or a SortedSet for better
// efficiency.
ImmutableSet<PathFragment> excludedSubdirectoriesBeneathThisSubdirectory =
PathFragment.filterPathsStartingWith(excludedPaths, subdirectory);
RootedPath subdirectoryRootedPath = RootedPath.toRootedPath(root, subdirectory);
childDeps.add(getSkyKeyForSubdirectory(recursivePkgKey.getRepository(),
subdirectoryRootedPath, excludedSubdirectoriesBeneathThisSubdirectory));
}
Map<SkyKey, SkyValue> subdirectorySkyValues = env.getValues(childDeps);
if (env.valuesMissing()) {
return null;
}
return aggregateWithSubdirectorySkyValues(visitor, subdirectorySkyValues);
}
private static boolean shouldFollowSymlinksWhenTraversing(Dirents dirents) {
for (Dirent dirent : dirents) {
// This is a specical 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;
}
// Ignore all errors in traversal and return an empty value.
private TReturn reportErrorAndReturn(String errorPrefix, Exception e,
PathFragment rootRelativePath, EventHandler handler) {
handler.handle(Event.warn(errorPrefix + ", for " + rootRelativePath
+ ", skipping: " + e.getMessage()));
return getEmptyReturn();
}
}