blob: f6f497a92625e16d7ce0d714c1aa31e8bc968a07 [file] [log] [blame]
/*
* Copyright 2013-present Facebook, Inc.
*
* 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.facebook.buck.rules;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.ConsoleEvent;
import com.facebook.buck.io.DefaultDirectoryTraverser;
import com.facebook.buck.io.DirectoryTraversal;
import com.facebook.buck.io.DirectoryTraverser;
import com.facebook.buck.io.MoreFiles;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildId;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.timing.Clock;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gson.JsonArray;
import com.google.gson.JsonPrimitive;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
/**
* Utility for recording the paths to the output files generated by a build rule, as well as any
* metadata about those output files. This data will be packaged up into an artifact that will be
* stored in the cache. The metadata will also be written to disk so it can be read on a subsequent
* build by an {@link OnDiskBuildInfo}.
*/
public class BuildInfoRecorder {
@VisibleForTesting
static final String ABSOLUTE_PATH_ERROR_FORMAT =
"Error! '%s' is trying to record artifacts with absolute path: '%s'.";
private static final DirectoryTraverser DEFAULT_DIRECTORY_TRAVERSER =
new DefaultDirectoryTraverser();
private static final Path PATH_TO_ARTIFACT_INFO = Paths.get("buck-out/log/cache_artifact.txt");
private static final String BUCK_CACHE_DATA_ENV_VAR = "BUCK_CACHE_DATA";
private final BuildTarget buildTarget;
private final Path pathToMetadataDirectory;
private final ProjectFilesystem projectFilesystem;
private final Clock clock;
private final BuildId buildId;
private final String artifactExtraData;
private final Map<String, String> metadataToWrite;
private final RuleKey ruleKey;
/**
* Every value in this set is a path relative to the project root.
*/
private final Set<Path> pathsToOutputFiles;
private final Set<Path> pathsToOutputDirectories;
private final DirectoryTraverser directoryTraverser;
BuildInfoRecorder(BuildTarget buildTarget,
ProjectFilesystem projectFilesystem,
Clock clock,
BuildId buildId,
ImmutableMap<String, String> environment,
RuleKey ruleKey,
RuleKey rukeKeyWithoutDeps) {
this(
buildTarget,
projectFilesystem,
clock,
buildId,
environment,
ruleKey,
rukeKeyWithoutDeps,
DEFAULT_DIRECTORY_TRAVERSER);
}
BuildInfoRecorder(BuildTarget buildTarget,
ProjectFilesystem projectFilesystem,
Clock clock,
BuildId buildId,
ImmutableMap<String, String> environment,
RuleKey ruleKey,
RuleKey rukeKeyWithoutDeps,
DirectoryTraverser directoryTraverser) {
this.buildTarget = buildTarget;
this.pathToMetadataDirectory = BuildInfo.getPathToMetadataDirectory(buildTarget);
this.projectFilesystem = projectFilesystem;
this.clock = clock;
this.buildId = buildId;
this.artifactExtraData =
String.format("artifact_data=%s", environment.get(BUCK_CACHE_DATA_ENV_VAR));
this.metadataToWrite = Maps.newHashMap();
metadataToWrite.put(BuildInfo.METADATA_KEY_FOR_RULE_KEY,
ruleKey.toString());
metadataToWrite.put(BuildInfo.METADATA_KEY_FOR_RULE_KEY_WITHOUT_DEPS,
rukeKeyWithoutDeps.toString());
this.ruleKey = ruleKey;
this.pathsToOutputFiles = Sets.newHashSet();
this.pathsToOutputDirectories = Sets.newHashSet();
this.directoryTraverser = directoryTraverser;
}
/**
* Writes the metadata currently stored in memory to the directory returned by
* {@link BuildInfo#getPathToMetadataDirectory(BuildTarget)}.
*/
public void writeMetadataToDisk(boolean clearExistingMetadata) throws IOException {
if (clearExistingMetadata) {
projectFilesystem.rmdir(pathToMetadataDirectory);
}
projectFilesystem.mkdirs(pathToMetadataDirectory);
for (Map.Entry<String, String> entry : metadataToWrite.entrySet()) {
projectFilesystem.writeContentsToPath(
entry.getValue(),
pathToMetadataDirectory.resolve(entry.getKey()));
}
}
/**
* This key/value pair is stored in memory until {@link #writeMetadataToDisk(boolean)} is invoked.
*/
public void addMetadata(String key, String value) {
metadataToWrite.put(key, value);
}
public void addMetadata(String key, Iterable<String> value) {
JsonArray values = new JsonArray();
for (String str : value) {
values.add(new JsonPrimitive(str));
}
addMetadata(key, values.toString());
}
/**
* Creates a zip file of the metadata and recorded artifacts and stores it in the artifact cache.
*/
public void performUploadToArtifactCache(ArtifactCache artifactCache, BuckEventBus eventBus)
throws InterruptedException {
// Skip all of this if caching is disabled. Although artifactCache.store() will be a noop,
// building up the zip is wasted I/O.
if (!artifactCache.isStoreSupported()) {
return;
}
ImmutableSet.Builder<Path> pathsToIncludeInZipBuilder = ImmutableSet.<Path>builder()
.addAll(Iterables.transform(metadataToWrite.keySet(),
new Function<String, Path>() {
@Override
public Path apply(String key) {
return pathToMetadataDirectory.resolve(key);
}
}))
.addAll(pathsToOutputFiles);
try {
for (Path outputDirectory : pathsToOutputDirectories) {
pathsToIncludeInZipBuilder.addAll(getEntries(outputDirectory));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
ImmutableSet<Path> pathsToIncludeInZip = pathsToIncludeInZipBuilder.build();
File zip;
try {
zip = File.createTempFile(
MoreFiles.sanitize(buildTarget.getFullyQualifiedName()),
".zip");
long time = TimeUnit.MILLISECONDS.toSeconds(clock.currentTimeMillis());
String additionalArtifactInfo = String.format(
"build_id=%s\ntimestamp=%d\n%s\n",
buildId,
time,
artifactExtraData);
projectFilesystem.createZip(
pathsToIncludeInZip,
zip,
ImmutableMap.of(PATH_TO_ARTIFACT_INFO, additionalArtifactInfo));
} catch (IOException e) {
eventBus.post(ConsoleEvent.info("Failed to create zip for %s containing:\n%s",
buildTarget,
Joiner.on('\n').join(ImmutableSortedSet.copyOf(pathsToIncludeInZip))));
e.printStackTrace();
return;
}
artifactCache.store(ruleKey, zip);
zip.delete();
}
private List<Path> getEntries(final Path outputDirectory) throws IOException {
final ImmutableList.Builder<Path> entries = ImmutableList.builder();
DirectoryTraversal traversal = new DirectoryTraversal(
projectFilesystem.getFileForRelativePath(outputDirectory)) {
@Override
public void visit(File file, String relativePath) throws IOException {
entries.add(outputDirectory.resolve(relativePath));
}
@Override
public void visitDirectory(File directory, String relativePath) throws IOException {
entries.add(outputDirectory.resolve(relativePath));
}
};
directoryTraverser.traverse(traversal);
return entries.build();
}
/**
* Fetches the artifact associated with the {@link #buildTarget} for this class and writes it to
* the specified {@code outputFile}.
*/
public CacheResult fetchArtifactForBuildable(File outputFile, ArtifactCache artifactCache)
throws InterruptedException {
return artifactCache.fetch(ruleKey, outputFile);
}
/**
* @param pathToArtifact Relative path to the project root.
*/
public void recordArtifact(Path pathToArtifact) {
Preconditions.checkArgument(
!pathToArtifact.isAbsolute(),
ABSOLUTE_PATH_ERROR_FORMAT,
buildTarget,
pathToArtifact);
pathsToOutputFiles.add(pathToArtifact);
}
public void recordArtifactsInDirectory(Path pathToArtifactsDirectory) {
Preconditions.checkArgument(
!pathToArtifactsDirectory.isAbsolute(),
ABSOLUTE_PATH_ERROR_FORMAT,
buildTarget,
pathToArtifactsDirectory);
pathsToOutputDirectories.add(pathToArtifactsDirectory);
}
@Nullable
@VisibleForTesting
String getMetadataFor(String key) {
return metadataToWrite.get(key);
}
}