diff options
author | Han-Wen Nienhuys <hanwen@google.com> | 2015-02-25 16:45:20 +0100 |
---|---|---|
committer | Han-Wen Nienhuys <hanwen@google.com> | 2015-02-25 16:45:20 +0100 |
commit | d08b27fa9701fecfdb69e1b0d1ac2459efc2129b (patch) | |
tree | 5d50963026239ca5aebfb47ea5b8db7e814e57c8 /src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java |
Update from Google.
--
MOE_MIGRATED_REVID=85702957
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java')
-rw-r--r-- | src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java | 454 |
1 files changed, 454 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java new file mode 100644 index 0000000000..d54e98fee0 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java @@ -0,0 +1,454 @@ +// Copyright 2014 Google Inc. 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.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; +import com.google.common.collect.Collections2; +import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; +import com.google.devtools.build.lib.events.Event; +import com.google.devtools.build.lib.events.EventKind; +import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFile; +import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.TraversalRequest; +import com.google.devtools.build.lib.vfs.Dirent; +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; +import com.google.devtools.build.skyframe.SkyFunctionException; +import com.google.devtools.build.skyframe.SkyKey; +import com.google.devtools.build.skyframe.SkyValue; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +/** A {@link SkyFunction} to build {@link RecursiveFilesystemTraversalValue}s. */ +public final class RecursiveFilesystemTraversalFunction implements SkyFunction { + + private static final class MissingDepException extends Exception {} + + /** Base class for exceptions that {@link RecursiveFilesystemTraversalFunctionException} wraps. */ + public abstract static class RecursiveFilesystemTraversalException extends Exception { + protected RecursiveFilesystemTraversalException(String message) { + super(message); + } + } + + /** Thrown when a generated directory's root-relative path conflicts with a package's path. */ + public static final class GeneratedPathConflictException extends + RecursiveFilesystemTraversalException { + GeneratedPathConflictException(TraversalRequest traversal) { + super(String.format( + "Generated directory %s conflicts with package under the same path. Additional info: %s", + traversal.path.getRelativePath().getPathString(), + traversal.errorInfo != null ? traversal.errorInfo : traversal.toString())); + } + } + + /** + * Thrown when the traversal encounters a subdirectory with a BUILD file but is not allowed to + * recurse into it. + */ + public static final class CannotCrossPackageBoundaryException extends + RecursiveFilesystemTraversalException { + CannotCrossPackageBoundaryException(String message) { + super(message); + } + } + + /** + * Thrown when a dangling symlink is attempted to be dereferenced. + * + * <p>Note: this class is not identical to the one in com.google.devtools.build.lib.view.fileset + * and it's not easy to merge the two because of the dependency structure. The other one will + * probably be removed along with the rest of the legacy Fileset code. + */ + public static final class DanglingSymlinkException extends RecursiveFilesystemTraversalException { + public final String path; + public final String unresolvedLink; + + public DanglingSymlinkException(String path, String unresolvedLink) { + super("Found dangling symlink: " + path + ", unresolved path: "); + Preconditions.checkArgument(path != null && !path.isEmpty()); + Preconditions.checkArgument(unresolvedLink != null && !unresolvedLink.isEmpty()); + this.path = path; + this.unresolvedLink = unresolvedLink; + } + + public String getPath() { + return path; + } + } + + /** Exception type thrown by {@link RecursiveFilesystemTraversalFunction#compute}. */ + private static final class RecursiveFilesystemTraversalFunctionException extends + SkyFunctionException { + RecursiveFilesystemTraversalFunctionException(RecursiveFilesystemTraversalException e) { + super(e, Transience.PERSISTENT); + } + } + + @Override + public SkyValue compute(SkyKey skyKey, Environment env) + throws RecursiveFilesystemTraversalFunctionException { + TraversalRequest traversal = (TraversalRequest) skyKey.argument(); + try { + // Stat the traversal root. + FileInfo rootInfo = lookUpFileInfo(env, traversal); + + if (!rootInfo.type.exists()) { + // May be a dangling symlink or a non-existent file. Handle gracefully. + if (rootInfo.type.isSymlink()) { + return resultForDanglingSymlink(traversal.path, rootInfo); + } else { + return RecursiveFilesystemTraversalValue.EMPTY; + } + } + + if (rootInfo.type.isFile()) { + // The root is a file or a symlink to one. + return resultForFileRoot(traversal.path, rootInfo); + } + + // Otherwise the root is a directory or a symlink to one. + PkgLookupResult pkgLookupResult = checkIfPackage(env, traversal, rootInfo); + traversal = pkgLookupResult.traversal; + + if (pkgLookupResult.isConflicting()) { + // The traversal was requested for an output directory whose root-relative path conflicts + // with a source package. We can't handle that, bail out. + throw new RecursiveFilesystemTraversalFunctionException( + new GeneratedPathConflictException(traversal)); + } else if (pkgLookupResult.isPackage() && !traversal.skipTestingForSubpackage) { + // The traversal was requested for a directory that defines a package. + if (traversal.crossPkgBoundaries) { + // We are free to traverse the subpackage but we need to display a warning. + String msg = traversal.errorInfo + " crosses package boundary into package rooted at " + + traversal.path.getRelativePath().getPathString(); + env.getListener().handle(new Event(EventKind.WARNING, null, msg)); + } else { + // We cannot traverse the subpackage and should skip it silently. Return empty results. + return RecursiveFilesystemTraversalValue.EMPTY; + } + } + + // We are free to traverse this directory. + Collection<SkyKey> dependentKeys = createRecursiveTraversalKeys(env, traversal); + return resultForDirectory(traversal, rootInfo, traverseChildren(env, dependentKeys)); + } catch (MissingDepException e) { + return null; + } + } + + @Override + public String extractTag(SkyKey skyKey) { + return null; + } + + private static final class FileInfo { + final FileType type; + final FileStateValue metadata; + @Nullable final RootedPath realPath; + @Nullable final PathFragment unresolvedSymlinkTarget; + + FileInfo(FileType type, FileStateValue metadata, @Nullable RootedPath realPath, + @Nullable PathFragment unresolvedSymlinkTarget) { + this.type = Preconditions.checkNotNull(type); + this.metadata = Preconditions.checkNotNull(metadata); + this.realPath = realPath; + this.unresolvedSymlinkTarget = unresolvedSymlinkTarget; + } + + @Override + public String toString() { + if (type.isSymlink()) { + return String.format("(%s: link_value=%s, real_path=%s)", type, + unresolvedSymlinkTarget.getPathString(), realPath); + } else { + return String.format("(%s: real_path=%s)", type, realPath); + } + } + } + + private static FileInfo lookUpFileInfo(Environment env, TraversalRequest traversal) + throws MissingDepException { + // Stat the file. + FileValue fileValue = (FileValue) getDependentSkyValue(env, FileValue.key(traversal.path)); + if (fileValue.exists()) { + // If it exists, it may either be a symlink or a file/directory. + PathFragment unresolvedLinkTarget = null; + FileType type = null; + if (fileValue.isSymlink()) { + unresolvedLinkTarget = fileValue.getUnresolvedLinkTarget(); + type = fileValue.isDirectory() ? FileType.SYMLINK_TO_DIRECTORY : FileType.SYMLINK_TO_FILE; + } else { + type = fileValue.isDirectory() ? FileType.DIRECTORY : FileType.FILE; + } + return new FileInfo(type, fileValue.realFileStateValue(), + fileValue.realRootedPath(), unresolvedLinkTarget); + } else { + // If it doesn't exist, or it's a dangling symlink, we still want to handle that gracefully. + return new FileInfo( + fileValue.isSymlink() ? FileType.DANGLING_SYMLINK : FileType.NONEXISTENT, + fileValue.realFileStateValue(), null, + fileValue.isSymlink() ? fileValue.getUnresolvedLinkTarget() : null); + } + } + + private static final class PkgLookupResult { + private enum Type { + CONFLICT, DIRECTORY, PKG + } + + private final Type type; + final TraversalRequest traversal; + final FileInfo rootInfo; + + /** Result for a generated directory that conflicts with a source package. */ + static PkgLookupResult conflict(TraversalRequest traversal, FileInfo rootInfo) { + return new PkgLookupResult(Type.CONFLICT, traversal, rootInfo); + } + + /** Result for a source or generated directory (not a package). */ + static PkgLookupResult directory(TraversalRequest traversal, FileInfo rootInfo) { + return new PkgLookupResult(Type.DIRECTORY, traversal, rootInfo); + } + + /** Result for a package, i.e. a directory with a BUILD file. */ + static PkgLookupResult pkg(TraversalRequest traversal, FileInfo rootInfo) { + return new PkgLookupResult(Type.PKG, traversal, rootInfo); + } + + private PkgLookupResult(Type type, TraversalRequest traversal, FileInfo rootInfo) { + this.type = Preconditions.checkNotNull(type); + this.traversal = Preconditions.checkNotNull(traversal); + this.rootInfo = Preconditions.checkNotNull(rootInfo); + } + + boolean isPackage() { + return type == Type.PKG; + } + + boolean isConflicting() { + return type == Type.CONFLICT; + } + + @Override + public String toString() { + return String.format("(%s: info=%s, traversal=%s)", type, rootInfo, traversal); + } + } + + /** + * Checks whether the {@code traversal}'s path refers to a package directory. + * + * @return the result of the lookup; it contains potentially new {@link TraversalRequest} and + * {@link FileInfo} so the caller should use these instead of the old ones (this happens when + * a package is found, but under a different root than expected) + */ + private static PkgLookupResult checkIfPackage(Environment env, TraversalRequest traversal, + FileInfo rootInfo) throws MissingDepException { + Preconditions.checkArgument(rootInfo.type.exists() && !rootInfo.type.isFile(), + "{%s} {%s}", traversal, rootInfo); + PackageLookupValue pkgLookup = (PackageLookupValue) getDependentSkyValue(env, + PackageLookupValue.key(traversal.path.getRelativePath())); + + if (pkgLookup.packageExists()) { + if (traversal.isGenerated) { + // The traversal's root was a generated directory, but its root-relative path conflicts with + // an existing package. + return PkgLookupResult.conflict(traversal, rootInfo); + } else { + // The traversal's root was a source directory and it defines a package. + Path pkgRoot = pkgLookup.getRoot(); + if (!pkgRoot.equals(traversal.path.getRoot())) { + // However the root of this package is different from what we expected. stat() the real + // BUILD file of that package. + traversal = traversal.forChangedRootPath(pkgRoot); + rootInfo = lookUpFileInfo(env, traversal); + Verify.verify(rootInfo.type.exists(), "{%s} {%s}", traversal, rootInfo); + } + return PkgLookupResult.pkg(traversal, rootInfo); + } + } else { + // The traversal's root was a directory (source or generated one), no package exists under the + // same root-relative path. + return PkgLookupResult.directory(traversal, rootInfo); + } + } + + /** + * List the directory and create {@code SkyKey}s to request contents of its children recursively. + * + * <p>The returned keys are of type {@link SkyFunctions#RECURSIVE_FILESYSTEM_TRAVERSAL}. + */ + private static Collection<SkyKey> createRecursiveTraversalKeys(Environment env, + TraversalRequest traversal) throws MissingDepException { + // Use the traversal's path, even if it's a symlink. The contents of the directory, as listed + // in the result, must be relative to it. + DirectoryListingValue dirListing = (DirectoryListingValue) getDependentSkyValue(env, + DirectoryListingValue.key(traversal.path)); + + List<SkyKey> result = new ArrayList<>(); + for (Dirent dirent : dirListing.getDirents()) { + RootedPath childPath = RootedPath.toRootedPath(traversal.path.getRoot(), + traversal.path.getRelativePath().getRelative(dirent.getName())); + TraversalRequest childTraversal = traversal.forChildEntry(childPath); + result.add(RecursiveFilesystemTraversalValue.key(childTraversal)); + } + return result; + } + + /** + * Creates result for a dangling symlink. + * + * @param linkName path to the symbolic link + * @param info the {@link FileInfo} associated with the link file + */ + private static RecursiveFilesystemTraversalValue resultForDanglingSymlink(RootedPath linkName, + FileInfo info) { + Preconditions.checkState(info.type.isSymlink() && !info.type.exists(), "{%s} {%s}", linkName, + info.type); + return RecursiveFilesystemTraversalValue.of( + ResolvedFile.danglingSymlink(linkName, info.unresolvedSymlinkTarget, info.metadata)); + } + + /** + * Creates results for a file or for a symlink that points to one. + * + * <p>A symlink may be direct (points to a file) or transitive (points at a direct or transitive + * symlink). + */ + private static RecursiveFilesystemTraversalValue resultForFileRoot(RootedPath path, + FileInfo info) { + Preconditions.checkState(info.type.isFile() && info.type.exists(), "{%s} {%s}", path, + info.type); + if (info.type.isSymlink()) { + return RecursiveFilesystemTraversalValue.of(ResolvedFile.symlinkToFile(info.realPath, path, + info.unresolvedSymlinkTarget, info.metadata)); + } else { + return RecursiveFilesystemTraversalValue.of(ResolvedFile.regularFile(path, info.metadata)); + } + } + + private static RecursiveFilesystemTraversalValue resultForDirectory(TraversalRequest traversal, + FileInfo rootInfo, Collection<RecursiveFilesystemTraversalValue> subdirTraversals) { + // Collect transitive closure of files in subdirectories. + NestedSetBuilder<ResolvedFile> paths = NestedSetBuilder.stableOrder(); + for (RecursiveFilesystemTraversalValue child : subdirTraversals) { + paths.addTransitive(child.getTransitiveFiles()); + } + ResolvedFile root; + if (rootInfo.type.isSymlink()) { + root = ResolvedFile.symlinkToDirectory(rootInfo.realPath, traversal.path, + rootInfo.unresolvedSymlinkTarget, rootInfo.metadata); + paths.add(root); + } else { + root = ResolvedFile.directory(rootInfo.realPath); + } + return RecursiveFilesystemTraversalValue.of(root, paths.build()); + } + + private static SkyValue getDependentSkyValue(Environment env, SkyKey key) + throws MissingDepException { + SkyValue value = env.getValue(key); + if (env.valuesMissing()) { + throw new MissingDepException(); + } + return value; + } + + /** + * Requests Skyframe to compute the dependent values and returns them. + * + * <p>The keys must all be {@link SkyFunctions#RECURSIVE_FILESYSTEM_TRAVERSAL} keys. + */ + private static Collection<RecursiveFilesystemTraversalValue> traverseChildren( + Environment env, Iterable<SkyKey> keys) + throws MissingDepException { + Map<SkyKey, SkyValue> values = env.getValues(keys); + if (env.valuesMissing()) { + throw new MissingDepException(); + } + return Collections2.transform(values.values(), + new Function<SkyValue, RecursiveFilesystemTraversalValue>() { + @Override + public RecursiveFilesystemTraversalValue apply(SkyValue input) { + return (RecursiveFilesystemTraversalValue) input; + } + }); + } + + /** Type information about the filesystem entry residing at a path. */ + enum FileType { + /** A regular file. */ + FILE { + @Override boolean isFile() { return true; } + @Override boolean exists() { return true; } + @Override public String toString() { return "<f>"; } + }, + /** + * A symlink to a regular file. + * + * <p>The symlink may be direct (points to a non-symlink (here a file)) or it may be transitive + * (points to a direct or transitive symlink). + */ + SYMLINK_TO_FILE { + @Override boolean isFile() { return true; } + @Override boolean isSymlink() { return true; } + @Override boolean exists() { return true; } + @Override public String toString() { return "<lf>"; } + }, + /** A directory. */ + DIRECTORY { + @Override boolean isDirectory() { return true; } + @Override boolean exists() { return true; } + @Override public String toString() { return "<d>"; } + }, + /** + * A symlink to a directory. + * + * <p>The symlink may be direct (points to a non-symlink (here a directory)) or it may be + * transitive (points to a direct or transitive symlink). + */ + SYMLINK_TO_DIRECTORY { + @Override boolean isDirectory() { return true; } + @Override boolean isSymlink() { return true; } + @Override boolean exists() { return true; } + @Override public String toString() { return "<ld>"; } + }, + /** A dangling symlink, i.e. one whose target is known not to exist. */ + DANGLING_SYMLINK { + @Override boolean isFile() { throw new UnsupportedOperationException(); } + @Override boolean isDirectory() { throw new UnsupportedOperationException(); } + @Override boolean isSymlink() { return true; } + @Override public String toString() { return "<l?>"; } + }, + /** A path that does not exist or should be ignored. */ + NONEXISTENT { + @Override public String toString() { return "<?>"; } + }; + + boolean isFile() { return false; } + boolean isDirectory() { return false; } + boolean isSymlink() { return false; } + boolean exists() { return false; } + @Override public abstract String toString(); + } +} |