// 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.rules.repository;
import com.google.common.annotations.VisibleForTesting;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.RuleDefinition;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.skyframe.ActionEnvironmentFunction;
import com.google.devtools.build.lib.skyframe.FileSymlinkException;
import com.google.devtools.build.lib.skyframe.FileValue;
import com.google.devtools.build.lib.skyframe.InconsistentFilesystemException;
import com.google.devtools.build.lib.skyframe.PackageLookupValue;
import com.google.devtools.build.lib.skyframe.PackageLookupValue.BuildFileName;
import com.google.devtools.build.lib.skyframe.WorkspaceFileValue;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
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.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
import com.google.devtools.build.skyframe.SkyKey;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
/**
* Implementation of fetching various external repository types.
*
* <p>These objects are called from {@link RepositoryDelegatorFunction}.
*
* <p>External repositories come in two flavors: local and non-local.
*
* <p>Local ones are those whose fetching does not require access to any external resources
* (e.g. network). These are always re-fetched on Bazel server restarts. This operation is fast
* (usually just a few symlinks and maybe writing a BUILD file). {@code --nofetch} does not apply
* to local repositories.
*
* <p>The up-to-dateness of non-local repositories is checked using a marker file under the
* output base. When such a repository is fetched, data from the rule in the WORKSPACE file is
* written to the marker file which is consulted on next server startup. If the rule hasn't changed,
* the repository is not re-fetched.
*
* <p>Fetching repositories can be disabled using the {@code --nofetch} command line option. If a
* repository is on the file system, Bazel just tries to use it and hopes for the best. If the
* repository has never been fetched, Bazel errors out for lack of a better option. This is
* implemented using
* {@link com.google.devtools.build.lib.bazel.BazelRepositoryModule#REPOSITORY_VALUE_CHECKER} and
* a flag in {@link RepositoryDirectoryValue} that tells Bazel whether the value in Skyframe is
* stale according to the value of {@code --nofetch} or not.
*
* <p>When a rule in the WORKSPACE file is changed, the corresponding
* {@link RepositoryDirectoryValue} is invalidated using the usual Skyframe route.
*/
public abstract class RepositoryFunction {
protected Map<String, String> clientEnvironment;
/**
* Exception thrown when something goes wrong accessing a remote repository.
*
* <p>This exception should be used by child classes to limit the types of exceptions
* {@link RepositoryDelegatorFunction} has to know how to catch.</p>
*/
public static class RepositoryFunctionException extends SkyFunctionException {
public RepositoryFunctionException(NoSuchPackageException cause, Transience transience) {
super(cause, transience);
}
/**
* Error reading or writing to the filesystem.
*/
public RepositoryFunctionException(IOException cause, Transience transience) {
super(cause, transience);
}
/**
* For errors in WORKSPACE file rules (e.g., malformed paths or URLs).
*/
public RepositoryFunctionException(EvalException cause, Transience transience) {
super(cause, transience);
}
}
/**
* Exception thrown when something a repository rule cannot be found.
*/
public static final class RepositoryNotFoundException extends RepositoryFunctionException {
public RepositoryNotFoundException(String repositoryName) {
super(
new BuildFileContainsErrorsException(
Label.EXTERNAL_PACKAGE_IDENTIFIER,
"The repository named '" + repositoryName + "' could not be resolved"),
Transience.PERSISTENT);
}
}
/**
* Fetch the remote repository represented by the given rule.
*
* <p>When this method is called, it has already been determined that the repository is stale and
* that it needs to be re-fetched.
*
* <p>The {@code env} argument can be used to fetch Skyframe dependencies the repository
* implementation needs on the following conditions:
*
* <ul>
* <li>When a Skyframe value is missing, fetching must be restarted, thus, in order to avoid doing
* duplicate work, it's better to first request the Skyframe dependencies you need and only
* then start doing anything costly.
* <li>The output directory must be populated from within this method (and not from within another
* SkyFunction). This is because if it was populated in another SkyFunction, the repository
* function would be restarted <b>after</b> that SkyFunction has been run, and it would wipe
* the output directory clean.
* </ul>
*
* <p>The {@code markerData} argument can be mutated to augment the data to write to the
* repository marker file. If any data in the {@code markerData} change between 2 execute of
* the {@link RepositoryDelegatorFunction} then this should be a reason to invalidate the
* repository. The {@link #verifyMarkerData(Map<String, String>)} method is responsible for
* checking the value added to that map when checking the content of a marker file.
*/
@ThreadSafe
@Nullable
public abstract RepositoryDirectoryValue.Builder fetch(Rule rule, Path outputDirectory,
BlazeDirectories directories, Environment env, Map<String, String> markerData)
throws SkyFunctionException, InterruptedException;
/**
* Verify the data provided by the marker file to check if a refetch is needed. Returns true if
* the data is up to date and no refetch is needed and false if the data is obsolete and a refetch
* is needed.
*/
@Nullable
public boolean verifyMarkerData(Rule rule, Map<String, String> markerData, Environment env)
throws InterruptedException {
return true;
}
/**
* A method that can be called from a implementation of
* {@link #fetch(Rule, Path, BlazeDirectories, Environment, Map)} to declare a list of Skyframe
* dependencies on environment variable. It also add the information to the marker file. It
* returns the list of environment variable on which the function depends, or null if the skyframe
* function needs to be restarted.
*/
protected Map<String, String> declareEnvironmentDependencies(Map<String, String> markerData,
Environment env, Iterable<String> keys) throws InterruptedException {
Map<String, String> environ = ActionEnvironmentFunction.getEnvironmentView(env, keys);
// Returns true if there is a null value and we need to wait for some dependencies.
if (environ == null) {
return null;
}
// Add the dependencies to the marker file
for (Map.Entry<String, String> value : environ.entrySet()) {
markerData.put("ENV:" + value.getKey(), value.getValue());
}
return environ;
}
/**
* Verify marker data previously saved by
* {@link #declareEnvironmentDependencies(Map, Environment, Iterable)}. This function is to be
* called from a {@link #verifyMarkerData(Rule, Map, Environment)} function to verify the values
* for environment variables.
*/
protected boolean verifyEnvironMarkerData(Map<String, String> markerData, Environment env,
Iterable<String> keys) throws InterruptedException {
Map<String, String> environ = ActionEnvironmentFunction.getEnvironmentView(env, keys);
if (env.valuesMissing()) {
return false; // Returns false so caller knows to return immediately
}
// Verify that all environment variable in the marker file are also in keys
for (String key : markerData.keySet()) {
if (key.startsWith("ENV:") && !environ.containsKey(key.substring(4))) {
return false;
}
}
// Now verify the values of the marker data
for (Map.Entry<String, String> value : environ.entrySet()) {
if (!markerData.containsKey("ENV:" + value.getKey())) {
return false;
}
String markerValue = markerData.get("ENV:" + value.getKey());
if (!Objects.equals(markerValue, value.getValue())) {
return false;
}
}
return true;
}
/**
* Whether fetching is done using local operations only.
*
* <p>If this is false, Bazel may decide not to re-fetch the repository, for example when the
* {@code --nofetch} command line option is used.
*/
protected abstract boolean isLocal(Rule rule);
/**
* Returns a block of data that must be equal for two Rules for them to be considered the same.
*
* <p>This is used for the up-to-dateness check of fetched directory trees. The only reason for
* this to exist is the {@code maven_server} rule (which should go away, but until then, we need
* to keep it working somehow)
*/
protected byte[] getRuleSpecificMarkerData(Rule rule, Environment env)
throws RepositoryFunctionException, InterruptedException {
return new byte[] {};
}
protected Path prepareLocalRepositorySymlinkTree(Rule rule, Path repositoryDirectory)
throws RepositoryFunctionException {
try {
FileSystemUtils.createDirectoryAndParents(repositoryDirectory);
} catch (IOException e) {
throw new RepositoryFunctionException(e, Transience.TRANSIENT);
}
// Add x/WORKSPACE.
createWorkspaceFile(repositoryDirectory, rule.getTargetKind(), rule.getName());
return repositoryDirectory;
}
public static void createWorkspaceFile(
Path repositoryDirectory, String ruleKind, String ruleName)
throws RepositoryFunctionException {
try {
Path workspaceFile = repositoryDirectory.getRelative("WORKSPACE");
FileSystemUtils.writeContent(workspaceFile, Charset.forName("UTF-8"),
String.format("# DO NOT EDIT: automatically generated WORKSPACE file for %s\n"
+ "workspace(name = \"%s\")\n", ruleKind, ruleName));
} catch (IOException e) {
throw new RepositoryFunctionException(e, Transience.TRANSIENT);
}
}
protected static RepositoryDirectoryValue.Builder writeFile(
Path repositoryDirectory, String filename, String contents)
throws RepositoryFunctionException {
Path filePath = repositoryDirectory.getRelative(filename);
try {
// The repository could have an existing file that's either a regular file (for remote
// repositories) or a symlink (for local repositories). Either way, we want to remove it and
// write our own.
if (filePath.exists(Symlinks.NOFOLLOW)) {
filePath.delete();
}
FileSystemUtils.writeContentAsLatin1(filePath, contents);
} catch (IOException e) {
throw new RepositoryFunctionException(e, Transience.TRANSIENT);
}
return RepositoryDirectoryValue.builder().setPath(repositoryDirectory);
}
protected static RepositoryDirectoryValue.Builder writeBuildFile(
Path repositoryDirectory, String contents) throws RepositoryFunctionException {
return writeFile(repositoryDirectory, "BUILD.bazel", contents);
}
@VisibleForTesting
protected static PathFragment getTargetPath(Rule rule, Path workspace)
throws RepositoryFunctionException {
WorkspaceAttributeMapper mapper = WorkspaceAttributeMapper.of(rule);
String path;
try {
path = mapper.get("path", Type.STRING);
} catch (EvalException e) {
throw new RepositoryFunctionException(e, Transience.PERSISTENT);
}
PathFragment pathFragment = PathFragment.create(path);
return workspace.getRelative(pathFragment).asFragment();
}
/**
* Given a targetDirectory /some/path/to/y that contains files z, w, and v, create the following
* directory structure:
* <pre>
* .external-repository/
* x/
* WORKSPACE
* BUILD -> <build_root>/x.BUILD
* z -> /some/path/to/y/z
* w -> /some/path/to/y/w
* v -> /some/path/to/y/v
* </pre>
*/
public static boolean symlinkLocalRepositoryContents(
Path repositoryDirectory, Path targetDirectory)
throws RepositoryFunctionException {
try {
FileSystemUtils.createDirectoryAndParents(repositoryDirectory);
for (Path target : targetDirectory.getDirectoryEntries()) {
Path symlinkPath = repositoryDirectory.getRelative(target.getBaseName());
createSymbolicLink(symlinkPath, target);
}
} catch (IOException e) {
throw new RepositoryFunctionException(e, Transience.TRANSIENT);
}
return true;
}
static void createSymbolicLink(Path from, Path to)
throws RepositoryFunctionException {
try {
// Remove not-symlinks that are already there.
if (from.exists()) {
from.delete();
}
FileSystemUtils.ensureSymbolicLink(from, to);
} catch (IOException e) {
throw new RepositoryFunctionException(
new IOException(String.format("Error creating symbolic link from %s to %s: %s",
from, to, e.getMessage())), Transience.TRANSIENT);
}
}
/**
* Uses a remote repository name to fetch the corresponding Rule describing how to get it.
*
* <p>This should be the unique entry point for resolving a remote repository function.
*/
@Nullable
public static Rule getRule(String repository, Environment env)
throws RepositoryFunctionException, InterruptedException {
SkyKey packageLookupKey = PackageLookupValue.key(Label.EXTERNAL_PACKAGE_IDENTIFIER);
PackageLookupValue packageLookupValue = (PackageLookupValue) env.getValue(packageLookupKey);
if (packageLookupValue == null) {
return null;
}
RootedPath workspacePath = packageLookupValue.getRootedPath(Label.EXTERNAL_PACKAGE_IDENTIFIER);
SkyKey workspaceKey = WorkspaceFileValue.key(workspacePath);
do {
WorkspaceFileValue value = (WorkspaceFileValue) env.getValue(workspaceKey);
if (value == null) {
return null;
}
Package externalPackage = value.getPackage();
if (externalPackage.containsErrors()) {
Event.replayEventsOn(env.getListener(), externalPackage.getEvents());
throw new RepositoryFunctionException(
new BuildFileContainsErrorsException(
Label.EXTERNAL_PACKAGE_IDENTIFIER, "Could not load //external package"),
Transience.PERSISTENT);
}
Rule rule = externalPackage.getRule(repository);
if (rule != null) {
return rule;
}
workspaceKey = value.next();
} while (workspaceKey != null);
throw new RepositoryNotFoundException(repository);
}
@Nullable
public static Rule getRule(String ruleName, @Nullable String ruleClassName, Environment env)
throws RepositoryFunctionException, InterruptedException {
try {
return getRule(RepositoryName.create("@" + ruleName), ruleClassName, env);
} catch (LabelSyntaxException e) {
throw new RepositoryFunctionException(
new IOException("Invalid rule name " + ruleName), Transience.PERSISTENT);
}
}
/**
* Uses a remote repository name to fetch the corresponding Rule describing how to get it. This
* should be called from {@link SkyFunction#compute} functions, which should return null if this
* returns null. If {@code ruleClassName} is set, the rule found must have a matching rule class
* name.
*/
@Nullable
public static Rule getRule(
RepositoryName repositoryName, @Nullable String ruleClassName, Environment env)
throws RepositoryFunctionException, InterruptedException {
Rule rule = getRule(repositoryName.strippedName(), env);
Preconditions.checkState(
rule == null || ruleClassName == null || rule.getRuleClass().equals(ruleClassName),
"Got %s, was expecting a %s", rule, ruleClassName);
return rule;
}
/**
* Adds the repository's directory to the graph and, if it's a symlink, resolves it to an actual
* directory.
*/
@Nullable
protected static FileValue getRepositoryDirectory(Path repositoryDirectory, Environment env)
throws RepositoryFunctionException, InterruptedException {
SkyKey outputDirectoryKey = FileValue.key(RootedPath.toRootedPath(
repositoryDirectory, PathFragment.EMPTY_FRAGMENT));
FileValue value;
try {
value = (FileValue) env.getValueOrThrow(outputDirectoryKey, IOException.class,
FileSymlinkException.class, InconsistentFilesystemException.class);
} catch (IOException | FileSymlinkException | InconsistentFilesystemException e) {
throw new RepositoryFunctionException(
new IOException("Could not access " + repositoryDirectory + ": " + e.getMessage()),
Transience.PERSISTENT);
}
return value;
}
protected static Path getExternalRepositoryDirectory(BlazeDirectories directories) {
return directories.getOutputBase().getRelative(Label.EXTERNAL_PACKAGE_NAME);
}
/**
* For files that are under $OUTPUT_BASE/external, add a dependency on the corresponding rule so
* that if the WORKSPACE file changes, the File/DirectoryStateValue will be re-evaluated.
*
* <p>Note that: - We don't add a dependency on the parent directory at the package root boundary,
* so the only transitive dependencies from files inside the package roots to external files are
* through symlinks. So the upwards transitive closure of external files is small. - The only way
* other than external repositories for external source files to get into the skyframe graph in
* the first place is through symlinks outside the package roots, which we neither want to
* encourage nor optimize for since it is not common. So the set of external files is small.
*/
public static void addExternalFilesDependencies(
RootedPath rootedPath, BlazeDirectories directories, Environment env)
throws IOException, InterruptedException {
Path externalRepoDir = getExternalRepositoryDirectory(directories);
PathFragment repositoryPath = rootedPath.asPath().relativeTo(externalRepoDir);
if (repositoryPath.segmentCount() == 0) {
// We are the top of the repository path (<outputBase>/external), not in an actual external
// repository path.
return;
}
String repositoryName = repositoryPath.getSegment(0);
try {
// Add a dependency to the repository rule. RepositoryDirectoryValue does add this
// dependency already but we want to catch RepositoryNotFoundException, so invoke #getRule
// first.
Rule rule = RepositoryFunction.getRule(repositoryName, env);
if (rule == null) {
return;
}
if (repositoryPath.segmentCount() > 1) {
if (rule.getRuleClass().equals(LocalRepositoryRule.NAME)
&& repositoryPath.endsWith(BuildFileName.WORKSPACE.getFilenameFragment())) {
// Ignore this, there is a dependency from LocalRepositoryFunction->WORKSPACE file already
return;
}
// For all files under the repository directory, depend on the actual RepositoryDirectory
// function so we get invalidation when the repository is fetched.
// For the repository directory itself, we cannot depends on the RepositoryDirectoryValue
// (cycle).
env.getValue(
RepositoryDirectoryValue.key(
RepositoryName.createFromValidStrippedName(repositoryName)));
} else {
// Invalidate external/<repo> if the repository overrides change.
RepositoryDelegatorFunction.REPOSITORY_OVERRIDES.get(env);
}
} catch (RepositoryFunction.RepositoryNotFoundException ex) {
// The repository we are looking for does not exist so we should depend on the whole
// WORKSPACE file. In that case, the call to RepositoryFunction#getRule(String, Environment)
// already requested all repository functions from the WORKSPACE file from Skyframe as part
// of the resolution. Therefore we are safe to ignore that Exception.
return;
} catch (RepositoryFunctionException ex) {
// This should never happen.
throw new IllegalStateException(
"Repository " + repositoryName + " cannot be resolved for path " + rootedPath, ex);
}
}
/**
* Sets up a mapping of environment variables to use.
*/
public void setClientEnvironment(Map<String, String> clientEnvironment) {
this.clientEnvironment = clientEnvironment;
}
/**
* Returns the RuleDefinition class for this type of repository.
*/
public abstract Class<? extends RuleDefinition> getRuleDefinition();
}