// 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.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.Dirent;
import com.google.devtools.build.lib.vfs.Dirent.Type;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.lib.vfs.UnixGlob;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import java.util.Map;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* A {@link SkyFunction} for {@link GlobValue}s.
*
*
This code drives the glob matching process.
*/
public final class GlobFunction implements SkyFunction {
private final Cache regexPatternCache =
CacheBuilder.newBuilder().maximumSize(10000).concurrencyLevel(4).build();
private final boolean alwaysUseDirListing;
public GlobFunction(boolean alwaysUseDirListing) {
this.alwaysUseDirListing = alwaysUseDirListing;
}
@Override
public SkyValue compute(SkyKey skyKey, Environment env)
throws GlobFunctionException, InterruptedException {
GlobDescriptor glob = (GlobDescriptor) skyKey.argument();
// Note that the glob's package is assumed to exist which implies that the package's BUILD file
// exists which implies that the package's directory exists.
PathFragment globSubdir = glob.getSubdir();
if (!globSubdir.equals(PathFragment.EMPTY_FRAGMENT)) {
PackageLookupValue globSubdirPkgLookupValue =
(PackageLookupValue)
env.getValue(
PackageLookupValue.key(
PackageIdentifier.create(
glob.getPackageId().getRepository(),
glob.getPackageId().getPackageFragment().getRelative(globSubdir))));
if (globSubdirPkgLookupValue == null) {
return null;
}
if (globSubdirPkgLookupValue.packageExists()) {
// We crossed the package boundary, that is, pkg/subdir contains a BUILD file and thus
// defines another package, so glob expansion should not descend into that subdir.
return GlobValue.EMPTY;
}
}
String pattern = glob.getPattern();
// Split off the first path component of the pattern.
int slashPos = pattern.indexOf('/');
String patternHead;
String patternTail;
if (slashPos == -1) {
patternHead = pattern;
patternTail = null;
} else {
// Substrings will share the backing array of the original glob string. That should be fine.
patternHead = pattern.substring(0, slashPos);
patternTail = pattern.substring(slashPos + 1);
}
NestedSetBuilder matches = NestedSetBuilder.stableOrder();
boolean globMatchesBareFile = patternTail == null;
// "**" also matches an empty segment, so try the case where it is not present.
if ("**".equals(patternHead)) {
if (globMatchesBareFile) {
// Recursive globs aren't supposed to match the package's directory.
if (!glob.excludeDirs() && !globSubdir.equals(PathFragment.EMPTY_FRAGMENT)) {
matches.add(globSubdir);
}
} else {
SkyKey globKey =
GlobValue.internalKey(
glob.getPackageId(),
glob.getPackageRoot(),
globSubdir,
patternTail,
glob.excludeDirs());
GlobValue globValue = (GlobValue) env.getValue(globKey);
if (globValue == null) {
return null;
}
matches.addTransitive(globValue.getMatches());
}
}
PathFragment dirPathFragment = glob.getPackageId().getPackageFragment().getRelative(globSubdir);
RootedPath dirRootedPath = RootedPath.toRootedPath(glob.getPackageRoot(), dirPathFragment);
if (alwaysUseDirListing || containsGlobs(patternHead)) {
String subdirPattern = "**".equals(patternHead) ? glob.getPattern() : patternTail;
// Pattern contains globs, so a directory listing is required.
//
// Note that we have good reason to believe the directory exists: if this is the
// top-level directory of the package, the package's existence implies the directory's
// existence; if this is a lower-level directory in the package, then we got here from
// previous directory listings. Filesystem operations concurrent with build could mean the
// directory no longer exists, but DirectoryListingFunction handles that gracefully.
DirectoryListingValue listingValue = (DirectoryListingValue)
env.getValue(DirectoryListingValue.key(dirRootedPath));
if (listingValue == null) {
return null;
}
// In order to batch Skyframe requests, we do three passes over the directory:
// (1) Process every dirent, keeping track of values we need to request if the dirent cannot
// be processed with current information (symlink targets and subdirectory globs/package
// lookups for some subdirectories).
// (2) Get those values and process the symlinks, keeping track of subdirectory globs/package
// lookups we may need to request in case the symlink's target is a directory.
// (3) Process the necessary subdirectories.
int direntsSize = listingValue.getDirents().size();
Map symlinkFileMap = Maps.newHashMapWithExpectedSize(direntsSize);
Map subdirMap = Maps.newHashMapWithExpectedSize(direntsSize);
Map sortedResultMap = Maps.newTreeMap();
// First pass: do normal files and collect SkyKeys to request for subdirectories and symlinks.
for (Dirent dirent : listingValue.getDirents()) {
Type direntType = dirent.getType();
String fileName = dirent.getName();
if (!UnixGlob.matches(patternHead, fileName, regexPatternCache)) {
continue;
}
if (direntType == Dirent.Type.SYMLINK) {
// TODO(bazel-team): Consider extracting the symlink resolution logic.
// For symlinks, look up the corresponding FileValue. This ensures that if the symlink
// changes and "switches types" (say, from a file to a directory), this value will be
// invalidated. We also need the target's type to properly process the symlink.
symlinkFileMap.put(
FileValue.key(
RootedPath.toRootedPath(
glob.getPackageRoot(), dirPathFragment.getRelative(fileName))),
dirent);
continue;
}
if (direntType == Dirent.Type.DIRECTORY) {
SkyKey keyToRequest = getSkyKeyForSubdir(fileName, glob, subdirPattern);
if (keyToRequest != null) {
subdirMap.put(keyToRequest, dirent);
}
} else if (globMatchesBareFile) {
sortedResultMap.put(dirent, glob.getSubdir().getRelative(fileName));
}
}
Map subdirAndSymlinksResult =
env.getValues(Sets.union(subdirMap.keySet(), symlinkFileMap.keySet()));
if (env.valuesMissing()) {
return null;
}
Map symlinkSubdirMap = Maps.newHashMapWithExpectedSize(symlinkFileMap.size());
// Second pass: process the symlinks and subdirectories from the first pass, and maybe
// collect further SkyKeys if fully resolved symlink targets are themselves directories.
// Also process any known directories.
for (Map.Entry lookedUpKeyAndValue : subdirAndSymlinksResult.entrySet()) {
if (symlinkFileMap.containsKey(lookedUpKeyAndValue.getKey())) {
FileValue symlinkFileValue = (FileValue) lookedUpKeyAndValue.getValue();
if (!symlinkFileValue.isSymlink()) {
throw new GlobFunctionException(
new InconsistentFilesystemException(
"readdir and stat disagree about whether "
+ ((RootedPath) lookedUpKeyAndValue.getKey().argument()).asPath()
+ " is a symlink."),
Transience.TRANSIENT);
}
if (!symlinkFileValue.exists()) {
continue;
}
Dirent dirent = symlinkFileMap.get(lookedUpKeyAndValue.getKey());
String fileName = dirent.getName();
if (symlinkFileValue.isDirectory()) {
SkyKey keyToRequest = getSkyKeyForSubdir(fileName, glob, subdirPattern);
if (keyToRequest != null) {
symlinkSubdirMap.put(keyToRequest, dirent);
}
} else if (globMatchesBareFile) {
sortedResultMap.put(dirent, glob.getSubdir().getRelative(fileName));
}
} else {
processSubdir(lookedUpKeyAndValue, subdirMap, glob, sortedResultMap);
}
}
Map symlinkSubdirResult = env.getValues(symlinkSubdirMap.keySet());
if (env.valuesMissing()) {
return null;
}
// Third pass: do needed subdirectories of symlinked directories discovered during the second
// pass.
for (Map.Entry lookedUpKeyAndValue : symlinkSubdirResult.entrySet()) {
processSubdir(lookedUpKeyAndValue, symlinkSubdirMap, glob, sortedResultMap);
}
for (Map.Entry fileMatches : sortedResultMap.entrySet()) {
addToMatches(fileMatches.getValue(), matches);
}
} else {
// Pattern does not contain globs, so a direct stat is enough.
String fileName = patternHead;
RootedPath fileRootedPath =
RootedPath.toRootedPath(glob.getPackageRoot(), dirPathFragment.getRelative(fileName));
FileValue fileValue = (FileValue) env.getValue(FileValue.key(fileRootedPath));
if (fileValue == null) {
return null;
}
if (fileValue.exists()) {
if (fileValue.isDirectory()) {
SkyKey keyToRequest = getSkyKeyForSubdir(fileName, glob, patternTail);
if (keyToRequest != null) {
SkyValue valueRequested = env.getValue(keyToRequest);
if (env.valuesMissing()) {
return null;
}
Object fileMatches = getSubdirMatchesFromSkyValue(fileName, glob, valueRequested);
if (fileMatches != null) {
addToMatches(fileMatches, matches);
}
}
} else if (globMatchesBareFile) {
matches.add(glob.getSubdir().getRelative(fileName));
}
}
}
Preconditions.checkState(!env.valuesMissing(), skyKey);
NestedSet matchesBuilt = matches.build();
// Use the same value to represent that we did not match anything.
if (matchesBuilt.isEmpty()) {
return GlobValue.EMPTY;
}
return new GlobValue(matchesBuilt);
}
private static void processSubdir(
Map.Entry keyAndValue,
Map subdirMap,
GlobDescriptor glob,
Map sortedResultMap) {
Dirent dirent = Preconditions.checkNotNull(subdirMap.get(keyAndValue.getKey()), keyAndValue);
String fileName = dirent.getName();
Object dirMatches = getSubdirMatchesFromSkyValue(fileName, glob, keyAndValue.getValue());
if (dirMatches != null) {
sortedResultMap.put(dirent, dirMatches);
}
}
/** Returns true if the given pattern contains globs. */
private static boolean containsGlobs(String pattern) {
return pattern.contains("*") || pattern.contains("?");
}
@SuppressWarnings("unchecked") // cast to NestedSet
private static void addToMatches(Object toAdd, NestedSetBuilder matches) {
if (toAdd instanceof PathFragment) {
matches.add((PathFragment) toAdd);
} else {
matches.addTransitive((NestedSet) toAdd);
}
}
/**
* Includes the given file/directory in the glob.
*
* {@code fileName} must exist.
*
*
{@code isDirectory} must be true iff the file is a directory.
*
*
Returns a {@link SkyKey} for a value that is needed to compute the files that will be added
* to {@code matches}, or {@code null} if no additional value is needed. The returned value should
* be opaquely passed to {@link #getSubdirMatchesFromSkyValue}.
*/
private static SkyKey getSkyKeyForSubdir(
String fileName, GlobDescriptor glob, String subdirPattern) {
if (subdirPattern == null) {
if (glob.excludeDirs()) {
return null;
} else {
return PackageLookupValue.key(
PackageIdentifier.create(
glob.getPackageId().getRepository(),
glob.getPackageId()
.getPackageFragment()
.getRelative(glob.getSubdir())
.getRelative(fileName)));
}
} else {
// There is some more pattern to match. Get the glob for the subdirectory. Note that this
// directory may also match directly in the case of a pattern that starts with "**", but that
// match will be found in the subdirectory glob.
return GlobValue.internalKey(
glob.getPackageId(),
glob.getPackageRoot(),
glob.getSubdir().getRelative(fileName),
subdirPattern,
glob.excludeDirs());
}
}
/**
* Returns matches coming from the directory {@code fileName} if appropriate, either an individual
* file or a nested set of files.
*
*
{@code valueRequested} must be the SkyValue whose key was returned by
* {@link #getSkyKeyForSubdir} for these parameters.
*/
@Nullable
private static Object getSubdirMatchesFromSkyValue(
String fileName,
GlobDescriptor glob,
SkyValue valueRequested) {
if (valueRequested instanceof GlobValue) {
return ((GlobValue) valueRequested).getMatches();
} else {
Preconditions.checkState(
valueRequested instanceof PackageLookupValue,
"%s is not a GlobValue or PackageLookupValue (%s %s)",
valueRequested,
fileName,
glob);
if (!((PackageLookupValue) valueRequested).packageExists()) {
return glob.getSubdir().getRelative(fileName);
}
}
return null;
}
@Nullable
@Override
public String extractTag(SkyKey skyKey) {
return null;
}
/**
* Used to declare all the exception types that can be wrapped in the exception thrown by
* {@link GlobFunction#compute}.
*/
private static final class GlobFunctionException extends SkyFunctionException {
public GlobFunctionException(InconsistentFilesystemException e, Transience transience) {
super(e, transience);
}
}
}