blob: 07c1c516984b15b4d5c51408536cc430adea01aa [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.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 java.nio.charset.StandardCharsets.ISO_8859_1;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.hash.HashFunction;
import com.google.common.io.BaseEncoding;
import com.google.devtools.build.lib.actions.Artifact.SourceArtifact;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
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.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
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 java.io.ByteArrayInputStream;
import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
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>
*/
@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();
// TODO(lberki): This is only used by FileArtifactValue itself. It seems possible to remove this.
public abstract FileContentsProxy getContentsProxy();
@Nullable
public byte[] getValueFingerprint() {
// TODO(janakr): return fingerprint in other cases: symlink, directory.
return getDigest();
}
/**
* Index used to resolve remote files.
*
* <p>0 indicates that no such information is available which can mean that it's either a local
* file, empty, or an omitted output.
*/
public int getLocationIndex() {
return 0;
}
/** Returns {@code true} if the file only exists remotely. */
public boolean isRemote() {
return false;
}
/**
* 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 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;
}
/**
* Optional materialization path.
*
* <p>If present, this artifact is a copy of another artifact. It is still tracked as a
* non-symlink by Bazel, but materialized in the local filesystem as a symlink to the original
* artifact, whose contents live at this location. This is used by {@link
* com.google.devtools.build.lib.remote.AbstractActionInputPrefetcher} to implement zero-cost
* copies of remotely stored artifacts.
*/
public Optional<PathFragment> getMaterializationExecPath() {
return Optional.empty();
}
/**
* 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),
/* digest= */ 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);
}
/**
* 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 FileContentsProxy getContentsProxy() {
throw new UnsupportedOperationException();
}
@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 FileContentsProxy getContentsProxy() {
throw new UnsupportedOperationException();
}
@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", 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 Objects.hash(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;
}
FileStatus stat = path.statIfFound(Symlinks.FOLLOW);
return stat == null || !stat.isFile() || !proxy.equals(FileContentsProxy.create(stat));
}
@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 o) {
if (o instanceof SymlinkToSourceFileArtifactValue symlinkToSource) {
o = symlinkToSource.sourceFileMetadata; // "Dereference" the symlink.
}
if (!(o instanceof RegularFileArtifactValue lastKnown)) {
return true;
}
return size != lastKnown.size || !Objects.equals(proxy, lastKnown.proxy);
}
}
/** Metadata for remotely stored files. */
public static class RemoteFileArtifactValue extends FileArtifactValue {
private final byte[] digest;
private final long size;
private final int locationIndex;
@Nullable private final PathFragment materializationExecPath;
private RemoteFileArtifactValue(
byte[] digest,
long size,
int locationIndex,
@Nullable PathFragment materializationExecPath) {
this.digest = checkNotNull(digest);
this.size = size;
this.locationIndex = locationIndex;
this.materializationExecPath = materializationExecPath;
}
public static RemoteFileArtifactValue create(
byte[] digest, long size, int locationIndex, long expireAtEpochMilli) {
return create(
digest, size, locationIndex, expireAtEpochMilli, /* materializationExecPath= */ null);
}
@VisibleForTesting
public static RemoteFileArtifactValue create(
byte[] digest,
long size,
int locationIndex,
long expireAtEpochMilli,
@Nullable PathFragment materializationExecPath) {
return expireAtEpochMilli < 0
? new RemoteFileArtifactValue(digest, size, locationIndex, materializationExecPath)
: new RemoteFileArtifactValueWithExpiration(
digest, size, locationIndex, materializationExecPath, expireAtEpochMilli);
}
/**
* Returns a {@link RemoteFileArtifactValue} identical to the given one, except that its
* materialization path is set to the given value unless already present.
*/
public static RemoteFileArtifactValue createFromExistingWithMaterializationPath(
RemoteFileArtifactValue metadata, PathFragment materializationExecPath) {
checkNotNull(materializationExecPath);
if (metadata.materializationExecPath != null) {
return metadata;
}
return create(
metadata.getDigest(),
metadata.getSize(),
metadata.getLocationIndex(),
metadata.getExpireAtEpochMilli(),
metadata.getMaterializationExecPath().orElse(materializationExecPath));
}
@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
&& Objects.equals(materializationExecPath, that.materializationExecPath);
}
@Override
public final int hashCode() {
return Objects.hash(Arrays.hashCode(digest), size, locationIndex, materializationExecPath);
}
@Override
public final FileStateType getType() {
return FileStateType.REGULAR_FILE;
}
@Override
public final byte[] getDigest() {
return digest;
}
@Override
public final FileContentsProxy getContentsProxy() {
throw new UnsupportedOperationException();
}
@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 Optional<PathFragment> getMaterializationExecPath() {
return Optional.ofNullable(materializationExecPath);
}
/**
* Returns the time when the remote file expires in milliseconds since epoch. A negative value
* means the remote is not known to expire.
*
* <p>Expiration time does not contribute to equality of remote files.
*/
public long getExpireAtEpochMilli() {
return -1;
}
/**
* Extends the expiration time for this metadata. If it was constructed without known expiration
* time (i.e. expireAtEpochMilli < 0), this extension does nothing.
*/
public void extendExpireAtEpochMilli(long expireAtEpochMilli) {}
public boolean isAlive(Instant now) {
return true;
}
@Override
public final boolean wasModifiedSinceDigest(Path path) {
return false;
}
@Override
public final boolean isRemote() {
return true;
}
@Override
public final String toString() {
return MoreObjects.toStringHelper(this)
.add("digest", bytesToString(digest))
.add("size", size)
.add("locationIndex", locationIndex)
.add("materializationExecPath", materializationExecPath)
.add("expireAtEpochMilli", getExpireAtEpochMilli())
.toString();
}
}
/** A remote artifact that expires at a particular time. */
private static final class RemoteFileArtifactValueWithExpiration extends RemoteFileArtifactValue {
private long expireAtEpochMilli;
private RemoteFileArtifactValueWithExpiration(
byte[] digest,
long size,
int locationIndex,
PathFragment materializationExecPath,
long expireAtEpochMilli) {
super(digest, size, locationIndex, materializationExecPath);
this.expireAtEpochMilli = expireAtEpochMilli;
}
@Override
public long getExpireAtEpochMilli() {
return expireAtEpochMilli;
}
@Override
public void extendExpireAtEpochMilli(long expireAtEpochMilli) {
checkState(expireAtEpochMilli > this.expireAtEpochMilli);
this.expireAtEpochMilli = expireAtEpochMilli;
}
@Override
public boolean isAlive(Instant now) {
return now.toEpochMilli() < expireAtEpochMilli;
}
}
/** A {@link FileArtifactValue} representing a symlink that is not to be resolved. */
public 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;
}
public String getSymlinkTarget() {
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 FileContentsProxy getContentsProxy() {
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;
}
}
}
/** File stored inline in metadata. */
public static final class InlineFileArtifactValue extends FileArtifactValue {
public static InlineFileArtifactValue create(byte[] bytes, HashFunction hashFunction) {
return new InlineFileArtifactValue(bytes, hashFunction.hashBytes(bytes).asBytes());
}
private final byte[] data;
private final byte[] digest;
private InlineFileArtifactValue(byte[] data, byte[] digest) {
this.data = checkNotNull(data);
this.digest = checkNotNull(digest);
}
@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);
}
public ByteArrayInputStream getInputStream() {
return new ByteArrayInputStream(data);
}
@Override
public FileStateType getType() {
return FileStateType.REGULAR_FILE;
}
@Override
public byte[] getDigest() {
return digest;
}
@Override
public FileContentsProxy getContentsProxy() {
throw new UnsupportedOperationException();
}
@Override
public long getSize() {
return data.length;
}
@Override
public long getModifiedTime() {
throw new UnsupportedOperationException();
}
@Override
public boolean wasModifiedSinceDigest(Path path) {
throw new UnsupportedOperationException();
}
}
/**
* Metadata for an output of {@link com.google.devtools.build.lib.analysis.actions.SymlinkAction}
* that resolves to a source file.
*/
public static final class SymlinkToSourceFileArtifactValue extends FileArtifactValue {
/** Creates metadata for a symlink pointing to a known {@link SourceArtifact}. */
public static SymlinkToSourceFileArtifactValue toSourceArtifact(
SourceArtifact sourceArtifact, FileArtifactValue sourceFileMetadata) {
return new SymlinkToSourceFileArtifactValue(
sourceArtifact.getPath().asFragment(), sourceFileMetadata, sourceArtifact.getExecPath());
}
/**
* Creates metadata for a symlink pointing to a source file that is not a known {@link
* SourceArtifact}.
*
* <p>This is only expected to happen for a symlink to an FDO profile file when {@code
* --fdo_profile} is specified as an absolute path.
*/
public static SymlinkToSourceFileArtifactValue toUnknownSourceFile(
PathFragment resolvedPath, FileArtifactValue sourceFileMetadata) {
return new SymlinkToSourceFileArtifactValue(
resolvedPath, sourceFileMetadata, /* sourceArtifactExecPath= */ null);
}
private final PathFragment resolvedPath;
private final FileArtifactValue sourceFileMetadata;
@Nullable private final PathFragment sourceArtifactExecPath;
private SymlinkToSourceFileArtifactValue(
PathFragment resolvedPath,
FileArtifactValue sourceFileMetadata,
@Nullable PathFragment sourceArtifactExecPath) {
checkArgument(resolvedPath.isAbsolute(), "Resolved path must be absolute: %s", resolvedPath);
this.resolvedPath = resolvedPath;
this.sourceFileMetadata = checkNotNull(sourceFileMetadata);
this.sourceArtifactExecPath = sourceArtifactExecPath;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SymlinkToSourceFileArtifactValue that)) {
return false;
}
return resolvedPath.equals(that.resolvedPath)
&& sourceFileMetadata.equals(that.sourceFileMetadata)
&& Objects.equals(sourceArtifactExecPath, that.sourceArtifactExecPath);
}
@Override
public int hashCode() {
return HashCodes.hashObjects(resolvedPath, sourceFileMetadata, sourceArtifactExecPath);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("resolvedPath", resolvedPath)
.add("sourceFileMetadata", sourceFileMetadata)
.add("sourceArtifactExecPath", sourceArtifactExecPath)
.toString();
}
/** Returns the absolute path to which the symlink resolves. */
public PathFragment getResolvedPath() {
return resolvedPath;
}
/**
* If the symlink resolves to a {@link SourceArtifact}, returns that artifact's exec path.
*
* <p>Returns {@code null} when the symlink does not resolve to a known {@link SourceArtifact}.
* See {@link #toUnknownSourceFile}.
*/
@Nullable
public PathFragment getSourceArtifactExecPath() {
return sourceArtifactExecPath;
}
@Override
public FileStateType getType() {
return sourceFileMetadata.getType();
}
@Nullable
@Override
public byte[] getDigest() {
return sourceFileMetadata.getDigest();
}
@Override
public FileContentsProxy getContentsProxy() {
return sourceFileMetadata.getContentsProxy();
}
@Override
public long getSize() {
return sourceFileMetadata.getSize();
}
@Override
public long getModifiedTime() {
return sourceFileMetadata.getModifiedTime();
}
@Override
public boolean wasModifiedSinceDigest(Path path) throws IOException {
return sourceFileMetadata.wasModifiedSinceDigest(path);
}
}
/** Metadata for an artifact obtained via a path proxy. */
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
public FileContentsProxy getContentsProxy() {
return delegate.getContentsProxy();
}
@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 FileContentsProxy getContentsProxy() {
throw new UnsupportedOperationException();
}
@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 FileContentsProxy getContentsProxy() {
throw new UnsupportedOperationException();
}
@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);
}
}
}