| // 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 static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.base.Preconditions.checkState; |
| import static com.google.devtools.build.lib.vfs.PathFragment.pathFragmentCodec; |
| import static java.nio.charset.StandardCharsets.ISO_8859_1; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.hash.HashFunction; |
| import com.google.common.io.BaseEncoding; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; |
| import com.google.devtools.build.lib.pkgcache.PackagePathCodecDependencies; |
| import com.google.devtools.build.lib.skyframe.serialization.DeserializationContext; |
| import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec; |
| import com.google.devtools.build.lib.skyframe.serialization.SerializationContext; |
| import com.google.devtools.build.lib.skyframe.serialization.SerializationException; |
| import com.google.devtools.build.lib.skyframe.serialization.VisibleForSerialization; |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant; |
| import com.google.devtools.build.lib.util.Fingerprint; |
| import com.google.devtools.build.lib.util.HashCodes; |
| import com.google.devtools.build.lib.vfs.DigestUtils; |
| import com.google.devtools.build.lib.vfs.FileStatus; |
| import com.google.devtools.build.lib.vfs.FileStatusWithDigest; |
| import com.google.devtools.build.lib.vfs.Path; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import com.google.devtools.build.lib.vfs.Root; |
| import com.google.devtools.build.lib.vfs.Symlinks; |
| import com.google.devtools.build.lib.vfs.SyscallCache; |
| import com.google.devtools.build.lib.vfs.XattrProvider; |
| import com.google.devtools.build.skyframe.SkyValue; |
| import com.google.errorprone.annotations.Keep; |
| import com.google.protobuf.CodedInputStream; |
| import com.google.protobuf.CodedOutputStream; |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.time.Instant; |
| import java.util.Arrays; |
| import java.util.Objects; |
| import javax.annotation.Nullable; |
| |
| /** |
| * A value that represents a file for the purposes of up-to-dateness checks of actions. |
| * |
| * <p>It always stands for an actual file. In particular, tree artifacts and runfiles trees do not |
| * have a corresponding {@link FileArtifactValue}. However, the file is not necessarily present in |
| * the file system; this happens when intermediate build outputs are not downloaded (and maybe when |
| * an input artifact of an action is missing?) |
| * |
| * <p>It makes its main appearance in {@code ActionExecutionValue.artifactData}. It has two main |
| * uses: |
| * |
| * <ul> |
| * <li>This is how dependent actions get hold of the output metadata of their generated inputs. |
| * <li>This is how {@code FileSystemValueChecker} figures out which actions need to be invalidated |
| * (just propagating the invalidation up from leaf nodes is not enough, because the output |
| * tree may have been changed while Blaze was not looking) |
| * </ul> |
| * |
| * <p>{@link FileArtifactValue} instance equality should only be used for testing purposes. To |
| * determine whether a metadata is equivalent to another for invalidation purposes, use {@link |
| * #couldBeModifiedSince} or {@link #wasModifiedSinceDigest}. |
| */ |
| @Immutable |
| @ThreadSafe |
| public abstract class FileArtifactValue implements SkyValue, HasDigest { |
| /** |
| * The type of the underlying file system object. If it is a regular file, then it is guaranteed |
| * to have a digest. Otherwise it does not have a digest. |
| */ |
| public abstract FileStateType getType(); |
| |
| /** |
| * Returns a digest of the content of the underlying file system object; must always return a |
| * non-null value for instances of type {@link FileStateType#REGULAR_FILE} that are owned by an |
| * {@code ActionExecutionValue}. |
| * |
| * <p>All instances of this interface must either have a digest or return a last-modified time. |
| * Clients should prefer using the digest for content identification (e.g., for caching), and only |
| * fall back to the last-modified time if no digest is available. |
| * |
| * <p>The return value is owned by this object and must not be modified. |
| */ |
| @Override |
| public abstract byte[] getDigest(); |
| |
| /** Returns the file's size, or 0 if the underlying file system object is not a file. */ |
| // TODO(ulfjack): Throw an exception if it's not a file. |
| public abstract long getSize(); |
| |
| /** |
| * Returns the last modified time; see the documentation of {@link #getDigest} for when this can |
| * and should be called. |
| */ |
| public abstract long getModifiedTime(); |
| |
| /** |
| * Returns a contents proxy (typically, a subset of the file system object's inode properties) |
| * that can be used to detect modifications more cheaply (at the cost of increased chance of a |
| * false negative) in situations where a digest would be too expensive to compute. |
| * |
| * <p>If no proxy is available, returns null. |
| */ |
| @Nullable |
| public FileContentsProxy getContentsProxy() { |
| return null; |
| } |
| |
| /** |
| * Sets the contents proxy. If this metadata does not support setting the contents proxy, does |
| * nothing. |
| */ |
| public void setContentsProxy(FileContentsProxy proxy) {} |
| |
| @Nullable |
| public byte[] getValueFingerprint() { |
| // TODO(janakr): return fingerprint in other cases: symlink, directory. |
| return getDigest(); |
| } |
| |
| /** |
| * Returns the unresolved symlink target path. |
| * |
| * @throws UnsupportedOperationException if the metadata is not of symlink file type. |
| */ |
| public String getUnresolvedSymlinkTarget() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Returns whether the file contents are inline, i.e., can be obtained directly from this {@link |
| * FileArtifactValue} by calling {@link #getInputStream}. |
| */ |
| public boolean isInline() { |
| return false; |
| } |
| |
| /** |
| * Returns an input stream for the inline file contents. |
| * |
| * @throws UnsupportedOperationException if the file contents are not inline. |
| */ |
| public InputStream getInputStream() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** Returns whether the file contents exist remotely. */ |
| public boolean isRemote() { |
| return false; |
| } |
| |
| /** Returns the location index for remote files. For non-remote files, returns 0. */ |
| public int getLocationIndex() { |
| return 0; |
| } |
| |
| /** |
| * Returns the time when the remote file contents may expire. If the contents never expire, |
| * including when they're not remote, returns null. |
| * |
| * <p>The expiration time does not factor into equality, as it can be mutated by {@link |
| * #setExpirationTime}. |
| */ |
| @Nullable |
| public Instant getExpirationTime() { |
| return null; |
| } |
| |
| /** |
| * Sets the expiration time. If this metadata does not support setting the expiration time, does |
| * nothing. |
| */ |
| public void setExpirationTime(Instant newExpirationTime) {} |
| |
| /** |
| * Provides a best-effort determination whether the file was changed since the digest was |
| * computed. This method performs file system I/O, so may be expensive. It's primarily intended to |
| * avoid storing bad cache entries in an action cache. It should return true if there is a chance |
| * that the file was modified since the digest was computed. Better not upload if we are not sure |
| * that the cache entry is reliable. |
| */ |
| // TODO(lberki): This is very similar to couldBeModifiedSince(). Check if we can unify these. |
| public abstract boolean wasModifiedSinceDigest(Path path) throws IOException; |
| |
| /** |
| * Returns whether the two {@link FileArtifactValue} instances could be considered the same for |
| * purposes of action invalidation. |
| */ |
| // TODO(lberki): This is very similar to wasModifiedSinceDigest(). Check if we can unify these. |
| public final boolean couldBeModifiedSince(FileArtifactValue lastKnown) { |
| if (this instanceof Singleton || lastKnown instanceof Singleton) { |
| return true; |
| } |
| |
| if (getType() != lastKnown.getType()) { |
| return true; |
| } |
| |
| if (getDigest() != null && lastKnown.getDigest() != null) { |
| // If we know the digests, we can tell with certainty whether the file has changed. |
| return !Arrays.equals(getDigest(), lastKnown.getDigest()) || getSize() != lastKnown.getSize(); |
| } else { |
| // If not, we assume by default that the file has changed, but individual implementations |
| // might know better. For example, regular local files can be compared by ctime or mtime. |
| return couldBeModifiedByMetadata(lastKnown); |
| } |
| } |
| |
| /** Adds this file metadata to the given {@link Fingerprint}. */ |
| public final void addTo(Fingerprint fp) { |
| byte[] digest = getDigest(); |
| if (digest != null) { |
| fp.addBytes(digest); |
| } else { |
| // Use the timestamp if the digest is not present, but not both. Modifying a timestamp while |
| // keeping the contents of a file the same should not cause rebuilds. |
| fp.addLong(getModifiedTime()); |
| } |
| } |
| |
| protected boolean couldBeModifiedByMetadata(FileArtifactValue lastKnown) { |
| return true; |
| } |
| |
| /** |
| * Returns the real path at which the file contents this metadata refers to can be found. |
| * |
| * <p>If present, an artifact possessing this metadata is materialized in the filesystem as a |
| * symlink to another artifact, but acts as a copy of that artifact for invalidation purposes. |
| * Thus, all other metadata fields reflect the properties of the file system object found at the |
| * real path. In particular, this means that {@link #getType} doesn't necessarily return {@link |
| * FileStateType#SYMLINK}. |
| * |
| * <p>The path must be absolute and not contain any unresolved symlinks, i.e., calling {@link |
| * Path#resolveSymbolicLinks} on it should yield the same path. |
| * |
| * <p>This allows such an artifact to be created as a symlink to the real path when lazily |
| * materialized on disk, in situations where making a copy is undesirable (e.g. because it would |
| * result in redundant downloads of the same remote output file) or impossible (e.g. because the |
| * original is a source file or a local output file, and its contents cannot be obtained from the |
| * digest). An output service is free to ignore this hint and materialize the artifact in some |
| * other way (e.g. as a regular file backed by a FUSE filesystem). |
| */ |
| @Nullable |
| public PathFragment getResolvedPath() { |
| return null; |
| } |
| |
| /** |
| * Marker interface for singleton implementations of this class. |
| * |
| * <p>Needed for a correct implementation of {@code equals}. |
| */ |
| interface Singleton {} |
| |
| /** |
| * Metadata for runfiles trees. |
| * |
| * <p>This should really be more nuanced so that runfiles trees don't need to be special-cased in |
| * the local action cache, but it works well enough. The only downsides are that we don't detect |
| * when someone changed a runfiles tree like we do for other output artifacts and a number of |
| * extra branches. |
| * |
| * <p>In Skyframe, we check whether a runfiles tree changed based on {@link |
| * RunfilesArtifactValue}, which does contain data about its contents. |
| */ |
| @SerializationConstant |
| public static final FileArtifactValue RUNFILES_TREE_MARKER = new SingletonMarkerValue(); |
| |
| /** Data that marks that a file is not present on the filesystem. */ |
| @SerializationConstant |
| public static final FileArtifactValue MISSING_FILE_MARKER = new SingletonMarkerValue(); |
| |
| public static FileArtifactValue createForSourceArtifact( |
| Artifact artifact, FileValue fileValue, XattrProvider xattrProvider) throws IOException { |
| // Artifacts with known generating actions should obtain the derived artifact's SkyValue |
| // from the generating action, instead. |
| checkState(!artifact.hasKnownGeneratingAction()); |
| checkState(!artifact.isConstantMetadata()); |
| boolean isFile = fileValue.isFile(); |
| return create( |
| artifact.getPath(), |
| isFile, |
| isFile ? fileValue.getSize() : 0, |
| isFile ? fileValue.realFileStateValue().getContentsProxy() : null, |
| isFile ? fileValue.getDigest() : null, |
| xattrProvider); |
| } |
| |
| public static FileArtifactValue createFromInjectedDigest( |
| FileArtifactValue metadata, @Nullable byte[] digest) { |
| return createForNormalFile(digest, metadata.getContentsProxy(), metadata.getSize()); |
| } |
| |
| @VisibleForTesting |
| public static FileArtifactValue createForTesting(Artifact artifact) throws IOException { |
| return createForTesting(artifact.getPath()); |
| } |
| |
| @VisibleForTesting |
| public static FileArtifactValue createForTesting(Path path) throws IOException { |
| // Caution: there's a race condition between stating the file and computing the digest. We need |
| // to stat first, since we're using the stat to detect changes. We follow symlinks here to be |
| // consistent with getDigest. |
| return createFromStat(path, path.stat(Symlinks.FOLLOW), SyscallCache.NO_CACHE); |
| } |
| |
| public static FileArtifactValue createFromStat( |
| Path path, FileStatus stat, XattrProvider xattrProvider) throws IOException { |
| return create( |
| path, |
| stat.isFile(), |
| stat.getSize(), |
| FileContentsProxy.create(stat), |
| stat instanceof FileStatusWithDigest statWithDigest ? statWithDigest.getDigest() : null, |
| xattrProvider); |
| } |
| |
| private static FileArtifactValue create( |
| Path path, |
| boolean isFile, |
| long size, |
| FileContentsProxy proxy, |
| @Nullable byte[] digest, |
| XattrProvider xattrProvider) |
| throws IOException { |
| if (!isFile) { |
| // In this case, we need to store the mtime because the action cache uses mtime for |
| // directories to determine if this artifact has changed. We want this code path to go away |
| // somehow. |
| return new DirectoryArtifactValue(path.getLastModifiedTime()); |
| } |
| if (digest == null) { |
| digest = DigestUtils.getDigestWithManualFallback(path, xattrProvider); |
| } |
| checkState(digest != null, path); |
| return createForNormalFile(digest, proxy, size); |
| } |
| |
| public static FileArtifactValue createForVirtualActionInput(byte[] digest, long size) { |
| return new RegularFileArtifactValue(digest, /* proxy= */ null, size); |
| } |
| |
| public static FileArtifactValue createForUnresolvedSymlink(Artifact artifact) throws IOException { |
| checkArgument(artifact.isSymlink()); |
| return createForUnresolvedSymlink(artifact.getPath()); |
| } |
| |
| public static FileArtifactValue createForUnresolvedSymlink(Path symlink) throws IOException { |
| return new UnresolvedSymlinkArtifactValue(symlink); |
| } |
| |
| public static FileArtifactValue createForNormalFile( |
| byte[] digest, @Nullable FileContentsProxy proxy, long size) { |
| return new RegularFileArtifactValue(digest, proxy, size); |
| } |
| |
| /** |
| * Create a FileArtifactValue using the {@link Path} and size. FileArtifactValue#create will |
| * handle getting the digest using the Path and size values. |
| */ |
| public static FileArtifactValue createForNormalFileUsingPath( |
| Path path, long size, XattrProvider xattrProvider) throws IOException { |
| return create( |
| path, /* isFile= */ true, size, /* proxy= */ null, /* digest= */ null, xattrProvider); |
| } |
| |
| public static FileArtifactValue createForDirectoryWithHash(byte[] digest) { |
| return new HashedDirectoryArtifactValue(digest); |
| } |
| |
| public static FileArtifactValue createForDirectoryWithMtime(long mtime) { |
| return new DirectoryArtifactValue(mtime); |
| } |
| |
| public static FileArtifactValue createForInlineFile(byte[] bytes, HashFunction hashFunction) { |
| return new InlineFileArtifactValue(bytes, hashFunction.hashBytes(bytes).asBytes()); |
| } |
| |
| public static FileArtifactValue createForRemoteFile(byte[] digest, long size, int locationIndex) { |
| return new RemoteFileArtifactValue(digest, size, locationIndex); |
| } |
| |
| public static FileArtifactValue createForRemoteFileWithMaterializationData( |
| byte[] digest, long size, int locationIndex, @Nullable Instant expirationTime) { |
| return new RemoteFileArtifactValueWithMaterializationData( |
| digest, size, locationIndex, expirationTime); |
| } |
| |
| public static FileArtifactValue createFromExistingWithResolvedPath( |
| FileArtifactValue delegate, PathFragment resolvedPath) { |
| return new ResolvedSymlinkArtifactValue(delegate, resolvedPath); |
| } |
| |
| /** |
| * Creates a FileArtifactValue used as a 'proxy' input for other ArtifactValues. These are used in |
| * {@link ActionCacheChecker}. |
| */ |
| public static FileArtifactValue createProxy(byte[] digest) { |
| checkNotNull(digest); |
| return createForNormalFile(digest, /* proxy= */ null, /* size= */ 0); |
| } |
| |
| private static String bytesToString(@Nullable byte[] bytes) { |
| return bytes == null ? "null" : "0x" + BaseEncoding.base16().omitPadding().encode(bytes); |
| } |
| |
| private static final class DirectoryArtifactValue extends FileArtifactValue { |
| private final long mtime; |
| |
| private DirectoryArtifactValue(long mtime) { |
| this.mtime = mtime; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (!(o instanceof DirectoryArtifactValue that)) { |
| return false; |
| } |
| |
| return mtime == that.mtime; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Long.hashCode(mtime); |
| } |
| |
| @Override |
| public FileStateType getType() { |
| return FileStateType.DIRECTORY; |
| } |
| |
| @Nullable |
| @Override |
| public byte[] getDigest() { |
| return null; |
| } |
| |
| @Override |
| public byte[] getValueFingerprint() { |
| return new Fingerprint() |
| .addString(getClass().getCanonicalName()) |
| .addLong(mtime) |
| .digestAndReset(); |
| } |
| |
| @Override |
| public long getModifiedTime() { |
| return mtime; |
| } |
| |
| @Override |
| public long getSize() { |
| return 0; |
| } |
| |
| @Override |
| public boolean wasModifiedSinceDigest(Path path) { |
| return false; |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this).add("mtime", mtime).toString(); |
| } |
| } |
| |
| private static final class HashedDirectoryArtifactValue extends FileArtifactValue { |
| |
| private final byte[] digest; |
| |
| private HashedDirectoryArtifactValue(byte[] digest) { |
| this.digest = digest; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (!(o instanceof HashedDirectoryArtifactValue that)) { |
| return false; |
| } |
| |
| return Arrays.equals(digest, that.digest); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Arrays.hashCode(digest); |
| } |
| |
| @Override |
| public FileStateType getType() { |
| return FileStateType.DIRECTORY; |
| } |
| |
| @Nullable |
| @Override |
| public byte[] getDigest() { |
| return digest; |
| } |
| |
| @Override |
| public long getModifiedTime() { |
| return 0; |
| } |
| |
| @Override |
| public long getSize() { |
| return 0; |
| } |
| |
| @Override |
| public boolean wasModifiedSinceDigest(Path path) { |
| // TODO(ulfjack): Ideally, we'd attempt to detect intra-build modifications here. I'm |
| // consciously deferring work here as this code will most likely change again, and we're |
| // already doing better than before by detecting inter-build modifications. |
| return false; |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this).add("digest", bytesToString(digest)).toString(); |
| } |
| } |
| |
| private static final class RegularFileArtifactValue extends FileArtifactValue { |
| private final byte[] digest; |
| @Nullable private final FileContentsProxy proxy; |
| private final long size; |
| |
| private RegularFileArtifactValue( |
| @Nullable byte[] digest, @Nullable FileContentsProxy proxy, long size) { |
| this.digest = digest; |
| this.proxy = proxy; |
| this.size = size; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof RegularFileArtifactValue that)) { |
| return false; |
| } |
| return Arrays.equals(digest, that.digest) |
| && Objects.equals(proxy, that.proxy) |
| && size == that.size; |
| } |
| |
| @Override |
| public int hashCode() { |
| return HashCodes.hashObjects(Arrays.hashCode(digest), proxy, size); |
| } |
| |
| @Override |
| public FileStateType getType() { |
| return FileStateType.REGULAR_FILE; |
| } |
| |
| @Override |
| public byte[] getDigest() { |
| return digest; |
| } |
| |
| @Override |
| public FileContentsProxy getContentsProxy() { |
| return proxy; |
| } |
| |
| @Override |
| public long getSize() { |
| return size; |
| } |
| |
| @Override |
| public boolean wasModifiedSinceDigest(Path path) throws IOException { |
| if (proxy == null) { |
| return false; |
| } |
| var stat = path.statIfFound(Symlinks.FOLLOW); |
| if (stat == null || !stat.isFile()) { |
| // The file no longer exists or changed type, so it certainly has changed. |
| return true; |
| } |
| var newProxy = FileContentsProxy.create(stat); |
| if (proxy.equals(newProxy)) { |
| // If the proxy is the same, then the file certainly hasn't been modified. This is the |
| // common case, so we check it first. |
| return false; |
| } |
| if (proxy.isModified(newProxy)) { |
| // If the non-ctime information in the proxy changed, the file has certainly been modified |
| // between the time the digest was computed and now. |
| return true; |
| } |
| // At this point the ctime changed, so some of the file's metadata has changed since we |
| // computed the digest. Returning true here would allow us to cautiously report modification |
| // even in complex ABA scenarios (file modified, then modified back with its mtime reset). |
| // However, we would also report modification in case a hardlink to the file was created or |
| // removed, such as by the hermetic Linux sandbox or certain optimized copy actions. |
| // As a compromise, we check whether the current state of the file differs from the previous |
| // one, ignoring any inbetween modifications that may have happened. |
| // |
| // Note that this path is always taken when using the hermetic Linux sandbox, but the |
| // associated cost should amortize over the next build as the digest will be cached under the |
| // new stat. |
| byte[] newDigest = DigestUtils.getDigestWithManualFallback(path, SyscallCache.NO_CACHE, stat); |
| return !Arrays.equals(digest, newDigest); |
| } |
| |
| @Override |
| public long getModifiedTime() { |
| throw new UnsupportedOperationException( |
| "regular file's mtime should never be called. (" + this + ")"); |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("digest", bytesToString(digest)) |
| .add("size", size) |
| .add("proxy", proxy) |
| .toString(); |
| } |
| |
| @Override |
| protected boolean couldBeModifiedByMetadata(FileArtifactValue lastKnown) { |
| return size != lastKnown.getSize() || !Objects.equals(proxy, lastKnown.getContentsProxy()); |
| } |
| } |
| |
| /** Metadata for remotely stored files. */ |
| private static class RemoteFileArtifactValue extends FileArtifactValue { |
| private final byte[] digest; |
| private final long size; |
| private final int locationIndex; |
| |
| private RemoteFileArtifactValue(byte[] digest, long size, int locationIndex) { |
| this.digest = checkNotNull(digest); |
| this.size = size; |
| this.locationIndex = locationIndex; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof RemoteFileArtifactValue that)) { |
| return false; |
| } |
| |
| return Arrays.equals(digest, that.digest) |
| && size == that.size |
| && locationIndex == that.locationIndex; |
| } |
| |
| @Override |
| public int hashCode() { |
| return HashCodes.hashObjects(Arrays.hashCode(digest), size, locationIndex); |
| } |
| |
| @Override |
| public final FileStateType getType() { |
| return FileStateType.REGULAR_FILE; |
| } |
| |
| @Override |
| public final byte[] getDigest() { |
| return digest; |
| } |
| |
| @Override |
| public final long getSize() { |
| return size; |
| } |
| |
| @Override |
| public final long getModifiedTime() { |
| throw new UnsupportedOperationException( |
| "RemoteFileArtifactValue doesn't support getModifiedTime"); |
| } |
| |
| @Override |
| public final int getLocationIndex() { |
| return locationIndex; |
| } |
| |
| @Override |
| public final boolean wasModifiedSinceDigest(Path path) { |
| return false; |
| } |
| |
| @Override |
| public final boolean isRemote() { |
| return true; |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("digest", bytesToString(digest)) |
| .add("size", size) |
| .add("locationIndex", locationIndex) |
| .toString(); |
| } |
| } |
| |
| /** |
| * Metadata for remotely stored files, with the additional ability to store a {@link |
| * #getExpirationTime} modifiable via {@link #setExpirationTime}, and a {@link #getContentsProxy} |
| * modifiable via {@link #setContentsProxy}. |
| * |
| * <p>This is used when the output mode allows for late materialization of remote outputs in the |
| * local filesystem. |
| */ |
| private static final class RemoteFileArtifactValueWithMaterializationData |
| extends RemoteFileArtifactValue { |
| private long expirationTime; |
| @Nullable private FileContentsProxy proxy; |
| |
| private RemoteFileArtifactValueWithMaterializationData( |
| byte[] digest, long size, int locationIndex, @Nullable Instant expirationTime) { |
| super(digest, size, locationIndex); |
| this.expirationTime = toEpochMilli(expirationTime); |
| } |
| |
| private static long toEpochMilli(@Nullable Instant expirationTime) { |
| return expirationTime != null ? expirationTime.toEpochMilli() : -1; |
| } |
| |
| @Nullable |
| private static Instant fromEpochMilli(long expirationTime) { |
| return expirationTime >= 0 ? Instant.ofEpochMilli(expirationTime) : null; |
| } |
| |
| @Override |
| @Nullable |
| public Instant getExpirationTime() { |
| return fromEpochMilli(expirationTime); |
| } |
| |
| @Override |
| public void setExpirationTime(Instant expirationTime) { |
| this.expirationTime = toEpochMilli(expirationTime); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p>Returns non-null if the file contents have been materialized in the local filesystem. |
| */ |
| @Override |
| @Nullable |
| public FileContentsProxy getContentsProxy() { |
| return proxy; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p>Called when the file contents are materialized in the local filesystem. |
| */ |
| @Override |
| public void setContentsProxy(FileContentsProxy proxy) { |
| this.proxy = proxy; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof RemoteFileArtifactValueWithMaterializationData that)) { |
| return false; |
| } |
| |
| return Arrays.equals(getDigest(), that.getDigest()) |
| && getSize() == that.getSize() |
| && getLocationIndex() == that.getLocationIndex(); |
| } |
| |
| @Override |
| public int hashCode() { |
| return HashCodes.hashObjects(Arrays.hashCode(getDigest()), getSize(), getLocationIndex()); |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("digest", bytesToString(getDigest())) |
| .add("size", getSize()) |
| .add("locationIndex", getLocationIndex()) |
| .add("expirationTime", fromEpochMilli(expirationTime)) |
| .add("proxy", proxy) |
| .toString(); |
| } |
| } |
| |
| /** |
| * Metadata for an artifact that is materialized in the filesystem as a symlink to another |
| * artifact, but acts as a copy of that artifact for invalidation purposes. See the documentation |
| * of {@link #getResolvedPath} for when this is useful. |
| * |
| * <p>Other than {@link #getResolvedPath}, all methods delegate to the {@link FileArtifactValue} |
| * of the artifact pointed to, which must itself have a null {@link #getResolvedPath}). |
| */ |
| private static final class ResolvedSymlinkArtifactValue extends FileArtifactValue { |
| private final FileArtifactValue delegate; |
| private final PathFragment resolvedPath; |
| |
| // TODO(b/329460099): Store just the execpath once multiple source roots are no longer |
| // supported. At that point it becomes possible to reliably compute the absolute path from the |
| // execpath. |
| |
| private ResolvedSymlinkArtifactValue(FileArtifactValue delegate, PathFragment resolvedPath) { |
| checkArgument(!(delegate instanceof Singleton), "delegate is a singleton: %s", delegate); |
| checkArgument(resolvedPath.isAbsolute(), "resolved path is not absolute: %s", resolvedPath); |
| checkArgument( |
| delegate.getResolvedPath() == null || delegate.getResolvedPath().equals(resolvedPath), |
| "delegate has a different resolved path: %s", |
| delegate); |
| this.delegate = |
| delegate instanceof ResolvedSymlinkArtifactValue resolvedDelegate |
| ? resolvedDelegate.delegate |
| : delegate; |
| this.resolvedPath = resolvedPath; |
| } |
| |
| @Override |
| public PathFragment getResolvedPath() { |
| return resolvedPath; |
| } |
| |
| @Override |
| public FileStateType getType() { |
| return delegate.getType(); |
| } |
| |
| @Nullable |
| @Override |
| public byte[] getDigest() { |
| return delegate.getDigest(); |
| } |
| |
| @Override |
| public FileContentsProxy getContentsProxy() { |
| return delegate.getContentsProxy(); |
| } |
| |
| @Override |
| public void setContentsProxy(FileContentsProxy proxy) { |
| delegate.setContentsProxy(proxy); |
| } |
| |
| @Override |
| public long getSize() { |
| return delegate.getSize(); |
| } |
| |
| @Override |
| public long getModifiedTime() { |
| return delegate.getModifiedTime(); |
| } |
| |
| @Override |
| public boolean wasModifiedSinceDigest(Path path) throws IOException { |
| return delegate.wasModifiedSinceDigest(path); |
| } |
| |
| @Override |
| protected boolean couldBeModifiedByMetadata(FileArtifactValue lastKnown) { |
| return delegate.couldBeModifiedByMetadata(lastKnown); |
| } |
| |
| @Override |
| public byte[] getValueFingerprint() { |
| return delegate.getValueFingerprint(); |
| } |
| |
| @Override |
| public boolean isInline() { |
| return delegate.isInline(); |
| } |
| |
| @Override |
| public InputStream getInputStream() { |
| return delegate.getInputStream(); |
| } |
| |
| @Override |
| public boolean isRemote() { |
| return delegate.isRemote(); |
| } |
| |
| @Override |
| public int getLocationIndex() { |
| return delegate.getLocationIndex(); |
| } |
| |
| @Override |
| @Nullable |
| public Instant getExpirationTime() { |
| return delegate.getExpirationTime(); |
| } |
| |
| @Override |
| public void setExpirationTime(Instant newExpirationTime) { |
| delegate.setExpirationTime(newExpirationTime); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof ResolvedSymlinkArtifactValue that)) { |
| return false; |
| } |
| return delegate.equals(that.delegate) && resolvedPath.equals(that.resolvedPath); |
| } |
| |
| @Override |
| public int hashCode() { |
| return HashCodes.hashObjects(delegate, resolvedPath); |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("delegate", delegate) |
| .add("resolvedPath", resolvedPath) |
| .toString(); |
| } |
| } |
| |
| /** |
| * Codec that serializes the absolute {@link ResolvedSymlinkArtifactValue#resolvedPath} by finding |
| * its root in {@link PackagePathCodecDependencies} and relativizing. |
| */ |
| // TODO: b/329460099 - This would not be necessary if we could store a source root relative path. |
| @Keep // Used reflectively. |
| private static final class ResolvedSymlinkArtifactValueCodec |
| implements ObjectCodec<ResolvedSymlinkArtifactValue> { |
| |
| @Override |
| public Class<? extends ResolvedSymlinkArtifactValue> getEncodedClass() { |
| return ResolvedSymlinkArtifactValue.class; |
| } |
| |
| @Override |
| public void serialize( |
| SerializationContext context, ResolvedSymlinkArtifactValue obj, CodedOutputStream codedOut) |
| throws SerializationException, IOException { |
| context.serialize(obj.delegate, codedOut); |
| |
| PathFragment resolvedPath = obj.resolvedPath; |
| ImmutableList<Root> roots = |
| context.getDependency(PackagePathCodecDependencies.class).getPackageRoots(); |
| for (int i = 0; i < roots.size(); i++) { |
| Root root = roots.get(i); |
| if (root.contains(resolvedPath)) { |
| PathFragment relativePath = root.relativize(resolvedPath); |
| context.serializeLeaf(relativePath, pathFragmentCodec(), codedOut); |
| codedOut.write((byte) i); |
| return; |
| } |
| } |
| throw new SerializationException(resolvedPath + " is not under any package roots: " + roots); |
| } |
| |
| @Override |
| public ResolvedSymlinkArtifactValue deserialize( |
| DeserializationContext context, CodedInputStream codedIn) |
| throws SerializationException, IOException { |
| FileArtifactValue delegate = context.deserialize(codedIn); |
| PathFragment relativePath = context.deserializeLeaf(codedIn, pathFragmentCodec()); |
| int rootIndex = codedIn.readRawByte(); |
| Root root = |
| context |
| .getDependency(PackagePathCodecDependencies.class) |
| .getPackageRoots() |
| .get(rootIndex); |
| PathFragment resolvedPath = root.getRelative(relativePath).asFragment(); |
| return new ResolvedSymlinkArtifactValue(delegate, resolvedPath); |
| } |
| } |
| |
| /** |
| * Metadata for a symlink that is not to be resolved. |
| * |
| * <p>Unlike {@link ResolvedSymlinkArtifactValue}, only the textual contents of the symlink matter |
| * for invalidation purposes. |
| */ |
| private static final class UnresolvedSymlinkArtifactValue extends FileArtifactValue { |
| private final String symlinkTarget; |
| private final byte[] digest; |
| |
| private UnresolvedSymlinkArtifactValue(Path symlink) throws IOException { |
| String symlinkTarget = symlink.readSymbolicLink().getPathString(); |
| |
| byte[] digest = |
| symlink |
| .getFileSystem() |
| .getDigestFunction() |
| .getHashFunction() |
| .hashString(symlinkTarget, ISO_8859_1) |
| .asBytes(); |
| |
| // We need to be able to tell the difference between a symlink and a file containing the same |
| // text. So we transform the digest a bit. This works because if one wants to craft a file |
| // with the same digest as a symlink, one would need to mount a preimage attack on the digest |
| // function (this would be different if we tweaked the data before applying the hash function) |
| digest[0] = (byte) (digest[0] ^ 0xff); |
| |
| this.symlinkTarget = symlinkTarget; |
| this.digest = digest; |
| } |
| |
| @Override |
| public String getUnresolvedSymlinkTarget() { |
| return symlinkTarget; |
| } |
| |
| @Override |
| public FileStateType getType() { |
| return FileStateType.SYMLINK; |
| } |
| |
| @Override |
| public byte[] getDigest() { |
| return digest; |
| } |
| |
| @Override |
| public long getSize() { |
| return 0; |
| } |
| |
| @Override |
| public long getModifiedTime() { |
| throw new IllegalStateException(); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof UnresolvedSymlinkArtifactValue)) { |
| return false; |
| } |
| UnresolvedSymlinkArtifactValue that = (UnresolvedSymlinkArtifactValue) o; |
| return Arrays.equals(digest, that.digest); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Arrays.hashCode(digest); |
| } |
| |
| @Override |
| public boolean wasModifiedSinceDigest(Path path) { |
| try { |
| var newMetadata = FileArtifactValue.createForUnresolvedSymlink(path); |
| return !Arrays.equals(digest, newMetadata.getDigest()); |
| } catch (IOException e) { |
| return true; |
| } |
| } |
| } |
| |
| /** Metadata for files whose contents are available in memory. */ |
| @AutoCodec |
| @VisibleForSerialization |
| public static final class InlineFileArtifactValue extends FileArtifactValue { |
| private final byte[] data; |
| private final byte[] digest; |
| |
| InlineFileArtifactValue(byte[] data, byte[] digest) { |
| this.data = checkNotNull(data); |
| this.digest = checkNotNull(digest); |
| } |
| |
| @Override |
| public boolean isInline() { |
| return true; |
| } |
| |
| @Override |
| public ByteArrayInputStream getInputStream() { |
| return new ByteArrayInputStream(data); |
| } |
| |
| @Override |
| public FileStateType getType() { |
| return FileStateType.REGULAR_FILE; |
| } |
| |
| @Override |
| public byte[] getDigest() { |
| return digest; |
| } |
| |
| @Override |
| public long getSize() { |
| return data.length; |
| } |
| |
| @Override |
| public long getModifiedTime() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public boolean wasModifiedSinceDigest(Path path) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof InlineFileArtifactValue that)) { |
| return false; |
| } |
| return Arrays.equals(digest, that.digest); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Arrays.hashCode(digest); |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("digest", bytesToString(digest)) |
| .add("size", getSize()) |
| .toString(); |
| } |
| } |
| |
| /** |
| * Metadata for an artifact obtained via a path proxy. |
| * |
| * <p>This is used to inform action file systems which would otherwise not read local disk that |
| * the source of truth for an output is at {@link #getTargetPath}. |
| */ |
| public static final class ProxyFileArtifactValue extends FileArtifactValue { |
| private final FileArtifactValue delegate; |
| private final Path path; |
| |
| public ProxyFileArtifactValue(FileArtifactValue delegate, Path path) { |
| this.delegate = checkNotNull(delegate); |
| this.path = checkNotNull(path); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof ProxyFileArtifactValue that)) { |
| return false; |
| } |
| return this.delegate.equals(that.delegate) && this.path.equals(that.path); |
| } |
| |
| @Override |
| public int hashCode() { |
| return HashCodes.hashObjects(delegate, path); |
| } |
| |
| public Path getTargetPath() { |
| return path; |
| } |
| |
| @Override |
| public FileStateType getType() { |
| return delegate.getType(); |
| } |
| |
| @Override |
| public byte[] getDigest() { |
| return delegate.getDigest(); |
| } |
| |
| @Override |
| @Nullable |
| public FileContentsProxy getContentsProxy() { |
| return delegate.getContentsProxy(); |
| } |
| |
| @Override |
| public void setContentsProxy(FileContentsProxy proxy) { |
| delegate.setContentsProxy(proxy); |
| } |
| |
| @Override |
| public long getSize() { |
| return delegate.getSize(); |
| } |
| |
| @Override |
| public long getModifiedTime() { |
| return delegate.getModifiedTime(); |
| } |
| |
| @Override |
| public boolean wasModifiedSinceDigest(Path path) throws IOException { |
| return delegate.wasModifiedSinceDigest(path); |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("delegate", delegate) |
| .add("path", path) |
| .toString(); |
| } |
| } |
| |
| private static final class SingletonMarkerValue extends FileArtifactValue implements Singleton { |
| private static final byte[] FINGERPRINT = new byte[] {0x10}; |
| |
| @Override |
| public FileStateType getType() { |
| return FileStateType.NONEXISTENT; |
| } |
| |
| @Nullable |
| @Override |
| public byte[] getDigest() { |
| return null; |
| } |
| |
| @Override |
| public long getSize() { |
| return 0; |
| } |
| |
| @Override |
| public long getModifiedTime() { |
| return 0; |
| } |
| |
| @Override |
| public boolean wasModifiedSinceDigest(Path path) { |
| return false; |
| } |
| |
| @Override |
| public byte[] getValueFingerprint() { |
| return FINGERPRINT; |
| } |
| |
| @Override |
| public String toString() { |
| return "singleton marker artifact value (" + hashCode() + ")"; |
| } |
| } |
| |
| /** {@link FileArtifactValue} subclass for artifacts with constant metadata. A singleton. */ |
| public static final class ConstantMetadataValue extends FileArtifactValue |
| implements FileArtifactValue.Singleton { |
| static final ConstantMetadataValue INSTANCE = new ConstantMetadataValue(); |
| // This needs to not be of length 0, so it is distinguishable from a missing digest when written |
| // into a Fingerprint. |
| private static final byte[] DIGEST = new byte[1]; |
| |
| private ConstantMetadataValue() {} |
| |
| @Override |
| public FileStateType getType() { |
| return FileStateType.REGULAR_FILE; |
| } |
| |
| @Override |
| public byte[] getDigest() { |
| return DIGEST; |
| } |
| |
| @Override |
| public long getSize() { |
| return 0; |
| } |
| |
| @Override |
| public long getModifiedTime() { |
| return -1; |
| } |
| |
| @Override |
| public boolean wasModifiedSinceDigest(Path path) { |
| throw new UnsupportedOperationException( |
| "ConstantMetadataValue doesn't support wasModifiedSinceDigest " + path); |
| } |
| } |
| } |