// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.skyframe;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.cache.DigestUtils;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.skyframe.SkyValue;
import java.io.IOException;
import java.util.Arrays;
import javax.annotation.Nullable;
/**
* Stores the actual metadata data of a file. We have the following cases:
*
*
* - an ordinary file, in which case we would expect to see a digest and size;
*
- a directory, in which case we would expect to see an mtime;
*
- an intentionally omitted file which the build system is aware of but doesn't actually exist,
* where all access methods are unsupported;
*
- a "middleman marker" object, which has a null digest, 0 size, and mtime of 0.
*
- The "self data" of a TreeArtifact, where we would expect to see a digest representing the
* artifact's contents, and a size of 0.
*
*/
// TODO(janakr): make this an interface once JDK8 allows us to have static methods on interfaces.
public abstract class FileArtifactValue implements SkyValue {
private static final class SingletonMarkerValue extends FileArtifactValue {
@Nullable
@Override
public byte[] getDigest() {
return null;
}
@Override
boolean isFile() {
return false;
}
@Override
public long getSize() {
return 0;
}
@Override
public long getModifiedTime() {
return 0;
}
@Override
public String toString() {
return "singleton marker artifact value (" + hashCode() + ")";
}
}
static final FileArtifactValue DEFAULT_MIDDLEMAN = new SingletonMarkerValue();
/** Data that marks that a file is not present on the filesystem. */
@VisibleForTesting
public static final FileArtifactValue MISSING_FILE_MARKER = new SingletonMarkerValue();
/**
* Represents an omitted file -- we are aware of it but it doesn't exist. All access methods are
* unsupported.
*/
static final FileArtifactValue OMITTED_FILE_MARKER =
new FileArtifactValue() {
@Override
public byte[] getDigest() {
throw new UnsupportedOperationException();
}
@Override
boolean isFile() {
throw new UnsupportedOperationException();
}
@Override
public long getSize() {
throw new UnsupportedOperationException();
}
@Override
public long getModifiedTime() {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
return "OMITTED_FILE_MARKER";
}
};
private static final class DirectoryArtifactValue extends FileArtifactValue {
private final long mtime;
private DirectoryArtifactValue(long mtime) {
this.mtime = mtime;
}
@Nullable
@Override
public byte[] getDigest() {
return null;
}
@Override
public long getModifiedTime() {
return mtime;
}
@Override
public long getSize() {
return 0;
}
@Override
public boolean isFile() {
return false;
}
@Override
public int hashCode() {
return (int) mtime;
}
@Override
public boolean equals(Object other) {
return (this == other)
|| ((other instanceof DirectoryArtifactValue)
&& this.mtime == ((DirectoryArtifactValue) other).mtime);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("mtime", mtime).toString();
}
}
private static final class RegularFileArtifactValue extends FileArtifactValue {
private final byte[] digest;
private final long size;
private RegularFileArtifactValue(byte[] digest, long size) {
this.digest = Preconditions.checkNotNull(digest);
this.size = size;
}
@Override
public byte[] getDigest() {
return digest;
}
@Override
boolean isFile() {
return true;
}
@Override
public long getSize() {
return size;
}
@Override
public long getModifiedTime() {
throw new UnsupportedOperationException(
"regular file's mtime should never be called. (" + this + ")");
}
@Override
public int hashCode() {
// Hash digest by content, not reference.
return 37 * (int) size + Arrays.hashCode(digest);
}
/**
* Two RegularFileArtifactValues will only compare equal if they have the same content. This
* differs from the {@code Metadata#equivalence} method, which allows for comparison using mtime
* if one object does not have a digest available.
*/
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof RegularFileArtifactValue)) {
return false;
}
RegularFileArtifactValue that = (RegularFileArtifactValue) other;
return this.size == that.size && Arrays.equals(this.digest, that.digest);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("digest", digest).add("size", size).toString();
}
}
@VisibleForTesting
public static FileArtifactValue create(Artifact artifact) throws IOException {
Path path = artifact.getPath();
FileStatus stat = path.stat();
boolean isFile = stat.isFile();
return create(artifact, isFile, isFile ? stat.getSize() : 0, null);
}
static FileArtifactValue create(Artifact artifact, FileValue fileValue) throws IOException {
boolean isFile = fileValue.isFile();
return create(artifact, isFile, isFile ? fileValue.getSize() : 0,
isFile ? fileValue.getDigest() : null);
}
static FileArtifactValue create(Artifact artifact, boolean isFile, long size,
@Nullable byte[] digest) throws IOException {
if (isFile && digest == null) {
digest = DigestUtils.getDigestOrFail(artifact.getPath(), size);
}
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 (maybe by implementing FileSet in Skyframe).
return new DirectoryArtifactValue(artifact.getPath().getLastModifiedTime());
}
Preconditions.checkState(digest != null, artifact);
return new RegularFileArtifactValue(digest, size);
}
/**
* Creates a FileArtifactValue used as a 'proxy' input for other ArtifactValues.
* These are used in {@link com.google.devtools.build.lib.actions.ActionCacheChecker}.
*/
static FileArtifactValue createProxy(byte[] digest) {
Preconditions.checkNotNull(digest);
return new RegularFileArtifactValue(digest, /*size=*/ 0);
}
/** Returns the digest of this value. Null for non-files, non-null for files. */
@Nullable
public abstract byte[] getDigest();
/** @return true if this is a file or a symlink to an existing file */
abstract boolean isFile();
/** Gets the size of the file. Non-files (including directories) have size 0. */
public abstract long getSize();
/** Gets last modified time of file. Should only be called if this is not a file. */
abstract long getModifiedTime();
}