diff options
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/actions/FileStateValue.java')
-rw-r--r-- | src/main/java/com/google/devtools/build/lib/actions/FileStateValue.java | 438 |
1 files changed, 438 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/actions/FileStateValue.java b/src/main/java/com/google/devtools/build/lib/actions/FileStateValue.java new file mode 100644 index 0000000000..bc4b51ad71 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/actions/FileStateValue.java @@ -0,0 +1,438 @@ +// 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.actions; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import com.google.common.collect.Interner; +import com.google.devtools.build.lib.concurrent.BlazeInterners; +import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; +import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; +import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor; +import com.google.devtools.build.lib.vfs.FileStatus; +import com.google.devtools.build.lib.vfs.FileStatusWithDigest; +import com.google.devtools.build.lib.vfs.FileStatusWithDigestAdapter; +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.lib.vfs.Symlinks; +import com.google.devtools.build.skyframe.AbstractSkyKey; +import com.google.devtools.build.skyframe.SkyFunctionName; +import com.google.devtools.build.skyframe.SkyValue; +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; +import javax.annotation.Nullable; + +/** + * Encapsulates the filesystem operations needed to get state for a path. This is equivalent to an + * 'lstat' that does not follow symlinks to determine what type of file the path is. + * <ul> + * <li> For a non-existent file, the non-existence is noted. + * <li> For a symlink, the symlink target is noted. + * <li> For a directory, the existence is noted. + * <li> For a file, the existence is noted, along with metadata about the file (e.g. + * file digest). See {@link RegularFileStateValue}. + * <ul> + * + * <p>This class is an implementation detail of {@link FileValue} and should not be used by + * {@link com.google.devtools.build.skyframe.SkyFunction}s other than {@link FileFunction}. Instead, + * {@link FileValue} should be used by {@link com.google.devtools.build.skyframe.SkyFunction} + * consumers that care about files. + * + * <p>All subclasses must implement {@link #equals} and {@link #hashCode} properly. + */ +@VisibleForTesting +public abstract class FileStateValue implements SkyValue { + public static final SkyFunctionName FILE_STATE = SkyFunctionName.create("FILE_STATE"); + + @AutoCodec + public static final DirectoryFileStateValue DIRECTORY_FILE_STATE_NODE = + new DirectoryFileStateValue(); + + @AutoCodec + public static final NonexistentFileStateValue NONEXISTENT_FILE_STATE_NODE = + new NonexistentFileStateValue(); + + protected FileStateValue() { + } + + public static FileStateValue create(RootedPath rootedPath, + @Nullable TimestampGranularityMonitor tsgm) throws InconsistentFilesystemException, + IOException { + Path path = rootedPath.asPath(); + // Stat, but don't throw an exception for the common case of a nonexistent file. This still + // throws an IOException in case any other IO error is encountered. + FileStatus stat = path.statIfFound(Symlinks.NOFOLLOW); + if (stat == null) { + return NONEXISTENT_FILE_STATE_NODE; + } + return createWithStatNoFollow(rootedPath, FileStatusWithDigestAdapter.adapt(stat), tsgm); + } + + public static FileStateValue createWithStatNoFollow( + RootedPath rootedPath, + FileStatusWithDigest statNoFollow, + @Nullable TimestampGranularityMonitor tsgm) + throws InconsistentFilesystemException, IOException { + Path path = rootedPath.asPath(); + if (statNoFollow.isFile()) { + return statNoFollow.isSpecialFile() + ? SpecialFileStateValue.fromStat(path.asFragment(), statNoFollow, tsgm) + : RegularFileStateValue.fromPath(path, statNoFollow, tsgm); + } else if (statNoFollow.isDirectory()) { + return DIRECTORY_FILE_STATE_NODE; + } else if (statNoFollow.isSymbolicLink()) { + return new SymlinkFileStateValue(path.readSymbolicLinkUnchecked()); + } + throw new InconsistentFilesystemException("according to stat, existing path " + path + " is " + + "neither a file nor directory nor symlink."); + } + + @VisibleForTesting + @ThreadSafe + public static Key key(RootedPath rootedPath) { + return Key.create(rootedPath); + } + + @AutoCodec.VisibleForSerialization + @AutoCodec + static class Key extends AbstractSkyKey<RootedPath> { + private static final Interner<Key> interner = BlazeInterners.newWeakInterner(); + + private Key(RootedPath arg) { + super(arg); + } + + @AutoCodec.VisibleForSerialization + @AutoCodec.Instantiator + static Key create(RootedPath arg) { + return interner.intern(new Key(arg)); + } + + @Override + public SkyFunctionName functionName() { + return FILE_STATE; + } + } + + public abstract FileStateType getType(); + + /** Returns the target of the symlink, or throws an exception if this is not a symlink. */ + public PathFragment getSymlinkTarget() { + throw new IllegalStateException(); + } + + long getSize() { + throw new IllegalStateException(); + } + + @Nullable + byte[] getDigest() { + throw new IllegalStateException(); + } + + @Override + public String toString() { + return prettyPrint(); + } + + abstract String prettyPrint(); + + /** + * Implementation of {@link FileStateValue} for regular files that exist. + * + * <p>A union of (digest, mtime). We use digests only if a fast digest lookup is available from + * the filesystem. If not, we fall back to mtime-based digests. This avoids the case where Blaze + * must read all files involved in the build in order to check for modifications in the case where + * fast digest lookups are not available. + */ + @ThreadSafe + @AutoCodec + public static final class RegularFileStateValue extends FileStateValue { + private final long size; + @Nullable private final byte[] digest; + @Nullable private final FileContentsProxy contentsProxy; + + public RegularFileStateValue(long size, byte[] digest, FileContentsProxy contentsProxy) { + Preconditions.checkState((digest == null) != (contentsProxy == null)); + this.size = size; + this.digest = digest; + this.contentsProxy = contentsProxy; + } + + /** + * Create a FileFileStateValue instance corresponding to the given existing file. + * @param stat must be of type "File". (Not a symlink). + */ + private static RegularFileStateValue fromPath(Path path, FileStatusWithDigest stat, + @Nullable TimestampGranularityMonitor tsgm) + throws InconsistentFilesystemException { + Preconditions.checkState(stat.isFile(), path); + + try { + byte[] digest = tryGetDigest(path, stat); + if (digest == null) { + // Note that TimestampGranularityMonitor#notifyDependenceOnFileTime is a thread-safe + // method. + if (tsgm != null) { + tsgm.notifyDependenceOnFileTime(path.asFragment(), stat.getLastChangeTime()); + } + return new RegularFileStateValue(stat.getSize(), null, FileContentsProxy.create(stat)); + } else { + // We are careful here to avoid putting the value ID into FileMetadata if we already have + // a digest. Arbitrary filesystems may do weird things with the value ID; a digest is more + // robust. + return new RegularFileStateValue(stat.getSize(), digest, null); + } + } catch (IOException e) { + String errorMessage = e.getMessage() != null + ? "error '" + e.getMessage() + "'" : "an error"; + throw new InconsistentFilesystemException("'stat' said " + path + " is a file but then we " + + "later encountered " + errorMessage + " which indicates that " + path + " is no " + + "longer a file. Did you delete it during the build?"); + } + } + + @Nullable + private static byte[] tryGetDigest(Path path, FileStatusWithDigest stat) throws IOException { + try { + byte[] digest = stat.getDigest(); + return digest != null ? digest : path.getFastDigest(); + } catch (IOException ioe) { + if (!path.isReadable()) { + return null; + } + throw ioe; + } + } + + @Override + public FileStateType getType() { + return FileStateType.REGULAR_FILE; + } + + @Override + public long getSize() { + return size; + } + + @Override + @Nullable + public byte[] getDigest() { + return digest; + } + + public FileContentsProxy getContentsProxy() { + return contentsProxy; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof RegularFileStateValue)) { + return false; + } + RegularFileStateValue other = (RegularFileStateValue) obj; + return size == other.size + && Arrays.equals(digest, other.digest) + && Objects.equals(contentsProxy, other.contentsProxy); + } + + @Override + public int hashCode() { + return Objects.hash(size, Arrays.hashCode(digest), contentsProxy); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("digest", digest) + .add("size", size) + .add("contentsProxy", contentsProxy).toString(); + } + + @Override + public String prettyPrint() { + String contents = digest != null + ? String.format("digest of %s", Arrays.toString(digest)) + : contentsProxy.prettyPrint(); + return String.format("regular file with size of %d and %s", size, contents); + } + } + + /** Implementation of {@link FileStateValue} for special files that exist. */ + @AutoCodec + public static final class SpecialFileStateValue extends FileStateValue { + private final FileContentsProxy contentsProxy; + + public SpecialFileStateValue(FileContentsProxy contentsProxy) { + this.contentsProxy = contentsProxy; + } + + static SpecialFileStateValue fromStat(PathFragment path, FileStatus stat, + @Nullable TimestampGranularityMonitor tsgm) throws IOException { + // Note that TimestampGranularityMonitor#notifyDependenceOnFileTime is a thread-safe method. + if (tsgm != null) { + tsgm.notifyDependenceOnFileTime(path, stat.getLastChangeTime()); + } + return new SpecialFileStateValue(FileContentsProxy.create(stat)); + } + + @Override + public FileStateType getType() { + return FileStateType.SPECIAL_FILE; + } + + @Override + long getSize() { + return 0; + } + + @Override + @Nullable + byte[] getDigest() { + return null; + } + + public FileContentsProxy getContentsProxy() { + return contentsProxy; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SpecialFileStateValue)) { + return false; + } + SpecialFileStateValue other = (SpecialFileStateValue) obj; + return Objects.equals(contentsProxy, other.contentsProxy); + } + + @Override + public int hashCode() { + return contentsProxy.hashCode(); + } + + @Override + public String prettyPrint() { + return String.format("special file with %s", contentsProxy.prettyPrint()); + } + } + + /** Implementation of {@link FileStateValue} for directories that exist. */ + public static final class DirectoryFileStateValue extends FileStateValue { + + private DirectoryFileStateValue() { + } + + @Override + public FileStateType getType() { + return FileStateType.DIRECTORY; + } + + @Override + public String prettyPrint() { + return "directory"; + } + + // This object is normally a singleton, but deserialization produces copies. + @Override + public boolean equals(Object obj) { + return obj instanceof DirectoryFileStateValue; + } + + @Override + public int hashCode() { + return 7654321; + } + } + + /** Implementation of {@link FileStateValue} for symlinks. */ + @AutoCodec + public static final class SymlinkFileStateValue extends FileStateValue { + + private final PathFragment symlinkTarget; + + public SymlinkFileStateValue(PathFragment symlinkTarget) { + this.symlinkTarget = symlinkTarget; + } + + @Override + public FileStateType getType() { + return FileStateType.SYMLINK; + } + + @Override + public PathFragment getSymlinkTarget() { + return symlinkTarget; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SymlinkFileStateValue)) { + return false; + } + SymlinkFileStateValue other = (SymlinkFileStateValue) obj; + return symlinkTarget.equals(other.symlinkTarget); + } + + @Override + public int hashCode() { + return symlinkTarget.hashCode(); + } + + @Override + public String prettyPrint() { + return "symlink to " + symlinkTarget; + } + } + + /** Implementation of {@link FileStateValue} for nonexistent files. */ + @AutoCodec.VisibleForSerialization + static final class NonexistentFileStateValue extends FileStateValue { + + private NonexistentFileStateValue() { + } + + @Override + public FileStateType getType() { + return FileStateType.NONEXISTENT; + } + + @Override + public String prettyPrint() { + return "nonexistent path"; + } + + // This object is normally a singleton, but deserialization produces copies. + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + return obj instanceof NonexistentFileStateValue; + } + + @Override + public int hashCode() { + return 8765432; + } + } +} |