blob: 70e950b221cc92e0cfab8a81101d5317133648ae [file] [log] [blame]
// Copyright 2019 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.bazel;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.RuleDefinition;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.bazel.commands.FetchCommand;
import com.google.devtools.build.lib.bazel.commands.SyncCommand;
import com.google.devtools.build.lib.bazel.repository.LocalConfigPlatformFunction;
import com.google.devtools.build.lib.bazel.repository.LocalConfigPlatformRule;
import com.google.devtools.build.lib.bazel.repository.RepositoryOptions;
import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.RepositoryOverride;
import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
import com.google.devtools.build.lib.bazel.repository.downloader.DelegatingDownloader;
import com.google.devtools.build.lib.bazel.repository.downloader.DownloadManager;
import com.google.devtools.build.lib.bazel.repository.downloader.HttpDownloader;
import com.google.devtools.build.lib.bazel.repository.skylark.SkylarkRepositoryFunction;
import com.google.devtools.build.lib.bazel.repository.skylark.SkylarkRepositoryModule;
import com.google.devtools.build.lib.bazel.rules.android.AndroidNdkRepositoryFunction;
import com.google.devtools.build.lib.bazel.rules.android.AndroidNdkRepositoryRule;
import com.google.devtools.build.lib.bazel.rules.android.AndroidSdkRepositoryFunction;
import com.google.devtools.build.lib.bazel.rules.android.AndroidSdkRepositoryRule;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
import com.google.devtools.build.lib.rules.repository.LocalRepositoryFunction;
import com.google.devtools.build.lib.rules.repository.LocalRepositoryRule;
import com.google.devtools.build.lib.rules.repository.ManagedDirectoriesKnowledgeImpl;
import com.google.devtools.build.lib.rules.repository.ManagedDirectoriesKnowledgeImpl.ManagedDirectoriesListener;
import com.google.devtools.build.lib.rules.repository.NewLocalRepositoryFunction;
import com.google.devtools.build.lib.rules.repository.NewLocalRepositoryRule;
import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction;
import com.google.devtools.build.lib.rules.repository.RepositoryDirectoryDirtinessChecker;
import com.google.devtools.build.lib.rules.repository.RepositoryFunction;
import com.google.devtools.build.lib.rules.repository.RepositoryLoaderFunction;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.RepositoryRemoteExecutor;
import com.google.devtools.build.lib.runtime.RepositoryRemoteExecutorFactory;
import com.google.devtools.build.lib.runtime.ServerBuilder;
import com.google.devtools.build.lib.runtime.WorkspaceBuilder;
import com.google.devtools.build.lib.runtime.commands.InfoItem;
import com.google.devtools.build.lib.server.FailureDetails.ExternalRepository;
import com.google.devtools.build.lib.server.FailureDetails.ExternalRepository.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.skyframe.MutableSupplier;
import com.google.devtools.build.lib.skyframe.PrecomputedValue;
import com.google.devtools.build.lib.skyframe.PrecomputedValue.Injected;
import com.google.devtools.build.lib.skyframe.SkyFunctions;
import com.google.devtools.build.lib.skylarkbuildapi.repository.RepositoryBootstrap;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.vfs.FileSystem;
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.Root;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParsingResult;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
/** Adds support for fetching external code. */
public class BazelRepositoryModule extends BlazeModule {
// Default location (relative to output user root) of the repository cache.
public static final String DEFAULT_CACHE_LOCATION = "cache/repos/v1";
// A map of repository handlers that can be looked up by rule class name.
private final ImmutableMap<String, RepositoryFunction> repositoryHandlers;
private final AtomicBoolean isFetch = new AtomicBoolean(false);
private final SkylarkRepositoryFunction skylarkRepositoryFunction;
private final RepositoryCache repositoryCache = new RepositoryCache();
private final HttpDownloader httpDownloader = new HttpDownloader();
private final DelegatingDownloader delegatingDownloader =
new DelegatingDownloader(httpDownloader);
private final DownloadManager downloadManager =
new DownloadManager(repositoryCache, delegatingDownloader);
private final MutableSupplier<Map<String, String>> clientEnvironmentSupplier =
new MutableSupplier<>();
private ImmutableMap<RepositoryName, PathFragment> overrides = ImmutableMap.of();
private Optional<RootedPath> resolvedFile = Optional.absent();
private Optional<RootedPath> resolvedFileReplacingWorkspace = Optional.absent();
private Set<String> outputVerificationRules = ImmutableSet.of();
private FileSystem filesystem;
// We hold the precomputed value of the managed directories here, so that the dependency
// on WorkspaceFileValue is not registered for each FileStateValue.
private final ManagedDirectoriesKnowledgeImpl managedDirectoriesKnowledge;
public BazelRepositoryModule() {
this.skylarkRepositoryFunction = new SkylarkRepositoryFunction(downloadManager);
this.repositoryHandlers = repositoryRules();
ManagedDirectoriesListener listener =
repositoryNamesWithManagedDirs -> {
Set<String> conflicting =
overrides.keySet().stream()
.filter(repositoryNamesWithManagedDirs::contains)
.map(RepositoryName::getName)
.collect(Collectors.toSet());
if (!conflicting.isEmpty()) {
String message =
"Overriding repositories is not allowed"
+ " for the repositories with managed directories.\n"
+ "The following overridden external repositories have managed directories: "
+ String.join(", ", conflicting.toArray(new String[0]));
throw new AbruptExitException(
detailedExitCode(message, Code.OVERRIDE_DISALLOWED_MANAGED_DIRECTORIES));
}
};
managedDirectoriesKnowledge = new ManagedDirectoriesKnowledgeImpl(listener);
}
private static DetailedExitCode detailedExitCode(String message, ExternalRepository.Code code) {
return DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(message)
.setExternalRepository(ExternalRepository.newBuilder().setCode(code))
.build());
}
public static ImmutableMap<String, RepositoryFunction> repositoryRules() {
return ImmutableMap.<String, RepositoryFunction>builder()
.put(LocalRepositoryRule.NAME, new LocalRepositoryFunction())
.put(NewLocalRepositoryRule.NAME, new NewLocalRepositoryFunction())
.put(AndroidSdkRepositoryRule.NAME, new AndroidSdkRepositoryFunction())
.put(AndroidNdkRepositoryRule.NAME, new AndroidNdkRepositoryFunction())
.put(LocalConfigPlatformRule.NAME, new LocalConfigPlatformFunction())
.build();
}
private static class RepositoryCacheInfoItem extends InfoItem {
private final RepositoryCache repositoryCache;
RepositoryCacheInfoItem(RepositoryCache repositoryCache) {
super("repository_cache", "The location of the repository download cache used");
this.repositoryCache = repositoryCache;
}
@Override
public byte[] get(Supplier<BuildConfiguration> configurationSupplier, CommandEnvironment env)
throws AbruptExitException, InterruptedException {
return print(repositoryCache.getRootPath());
}
}
@Override
public void serverInit(OptionsParsingResult startupOptions, ServerBuilder builder) {
builder.addCommands(new FetchCommand());
builder.addCommands(new SyncCommand());
builder.addInfoItems(new RepositoryCacheInfoItem(repositoryCache));
}
@Override
public void workspaceInit(
BlazeRuntime runtime, BlazeDirectories directories, WorkspaceBuilder builder) {
builder.setManagedDirectoriesKnowledge(managedDirectoriesKnowledge);
RepositoryDirectoryDirtinessChecker customDirtinessChecker =
new RepositoryDirectoryDirtinessChecker(
directories.getWorkspace(), managedDirectoriesKnowledge);
builder.addCustomDirtinessChecker(customDirtinessChecker);
// Create the repository function everything flows through.
builder.addSkyFunction(SkyFunctions.REPOSITORY, new RepositoryLoaderFunction());
RepositoryDelegatorFunction repositoryDelegatorFunction =
new RepositoryDelegatorFunction(
repositoryHandlers,
skylarkRepositoryFunction,
isFetch,
clientEnvironmentSupplier,
directories,
managedDirectoriesKnowledge);
builder.addSkyFunction(SkyFunctions.REPOSITORY_DIRECTORY, repositoryDelegatorFunction);
filesystem = runtime.getFileSystem();
}
@Override
public void initializeRuleClasses(ConfiguredRuleClassProvider.Builder builder) {
for (Map.Entry<String, RepositoryFunction> handler : repositoryHandlers.entrySet()) {
RuleDefinition ruleDefinition;
try {
ruleDefinition =
handler.getValue().getRuleDefinition().getDeclaredConstructor().newInstance();
} catch (IllegalAccessException
| InstantiationException
| NoSuchMethodException
| InvocationTargetException e) {
throw new IllegalStateException(e);
}
builder.addRuleDefinition(ruleDefinition);
}
builder.addSkylarkBootstrap(new RepositoryBootstrap(new SkylarkRepositoryModule()));
}
@Override
public void beforeCommand(CommandEnvironment env) {
clientEnvironmentSupplier.set(env.getRepoEnv());
PackageCacheOptions pkgOptions = env.getOptions().getOptions(PackageCacheOptions.class);
isFetch.set(pkgOptions != null && pkgOptions.fetch);
resolvedFile = Optional.<RootedPath>absent();
resolvedFileReplacingWorkspace = Optional.<RootedPath>absent();
outputVerificationRules = ImmutableSet.<String>of();
RepositoryOptions repoOptions = env.getOptions().getOptions(RepositoryOptions.class);
if (repoOptions != null) {
repositoryCache.setHardlink(repoOptions.useHardlinks);
if (repoOptions.experimentalScaleTimeouts > 0.0) {
skylarkRepositoryFunction.setTimeoutScaling(repoOptions.experimentalScaleTimeouts);
} else {
env.getReporter()
.handle(
Event.warn(
"Ignoring request to scale timeouts for repositories by a non-positive"
+ " factor"));
skylarkRepositoryFunction.setTimeoutScaling(1.0);
}
if (repoOptions.experimentalRepositoryCache != null) {
// A set but empty path indicates a request to disable the repository cache.
if (!repoOptions.experimentalRepositoryCache.isEmpty()) {
Path repositoryCachePath;
if (repoOptions.experimentalRepositoryCache.isAbsolute()) {
repositoryCachePath = filesystem.getPath(repoOptions.experimentalRepositoryCache);
} else {
repositoryCachePath =
env.getBlazeWorkspace()
.getWorkspace()
.getRelative(repoOptions.experimentalRepositoryCache);
}
repositoryCache.setRepositoryCachePath(repositoryCachePath);
}
} else {
Path repositoryCachePath =
env.getDirectories()
.getServerDirectories()
.getOutputUserRoot()
.getRelative(DEFAULT_CACHE_LOCATION);
try {
FileSystemUtils.createDirectoryAndParents(repositoryCachePath);
repositoryCache.setRepositoryCachePath(repositoryCachePath);
} catch (IOException e) {
env.getReporter()
.handle(
Event.warn(
"Failed to set up cache at "
+ repositoryCachePath.toString()
+ ": "
+ e.getMessage()));
}
}
if (repoOptions.experimentalDistdir != null) {
downloadManager.setDistdir(
repoOptions.experimentalDistdir.stream()
.map(
path ->
path.isAbsolute()
? filesystem.getPath(path)
: env.getBlazeWorkspace().getWorkspace().getRelative(path))
.collect(Collectors.toList()));
} else {
downloadManager.setDistdir(ImmutableList.<Path>of());
}
if (repoOptions.httpTimeoutScaling > 0) {
httpDownloader.setTimeoutScaling((float) repoOptions.httpTimeoutScaling);
} else {
env.getReporter()
.handle(Event.warn("Ingoring request to scale http timeouts by a non-positive factor"));
httpDownloader.setTimeoutScaling(1.0f);
}
if (repoOptions.repositoryOverrides != null) {
// To get the usual latest-wins semantics, we need a mutable map, as the builder
// of an immutable map does not allow redefining the values of existing keys.
// We use a LinkedHashMap to preserve the iteration order.
Map<RepositoryName, PathFragment> overrideMap = new LinkedHashMap<>();
for (RepositoryOverride override : repoOptions.repositoryOverrides) {
overrideMap.put(override.repositoryName(), override.path());
}
ImmutableMap<RepositoryName, PathFragment> newOverrides = ImmutableMap.copyOf(overrideMap);
if (!Maps.difference(overrides, newOverrides).areEqual()) {
overrides = newOverrides;
}
} else {
overrides = ImmutableMap.of();
}
if (!Strings.isNullOrEmpty(repoOptions.repositoryHashFile)) {
Path hashFile;
if (env.getWorkspace() != null) {
hashFile = env.getWorkspace().getRelative(repoOptions.repositoryHashFile);
} else {
hashFile = filesystem.getPath(repoOptions.repositoryHashFile);
}
resolvedFile =
Optional.of(RootedPath.toRootedPath(Root.absoluteRoot(filesystem), hashFile));
}
if (!Strings.isNullOrEmpty(repoOptions.experimentalResolvedFileInsteadOfWorkspace)) {
Path resolvedFile;
if (env.getWorkspace() != null) {
resolvedFile =
env.getWorkspace()
.getRelative(repoOptions.experimentalResolvedFileInsteadOfWorkspace);
} else {
resolvedFile = filesystem.getPath(repoOptions.experimentalResolvedFileInsteadOfWorkspace);
}
resolvedFileReplacingWorkspace =
Optional.of(RootedPath.toRootedPath(Root.absoluteRoot(filesystem), resolvedFile));
}
if (repoOptions.experimentalVerifyRepositoryRules != null) {
outputVerificationRules =
ImmutableSet.copyOf(repoOptions.experimentalVerifyRepositoryRules);
}
RepositoryRemoteExecutorFactory remoteExecutorFactory =
env.getRuntime().getRepositoryRemoteExecutorFactory();
RepositoryRemoteExecutor remoteExecutor = null;
if (remoteExecutorFactory != null) {
remoteExecutor = remoteExecutorFactory.create();
}
skylarkRepositoryFunction.setRepositoryRemoteExecutor(remoteExecutor);
delegatingDownloader.setDelegate(env.getRuntime().getDownloaderSupplier().get());
}
}
@Override
public ImmutableList<Injected> getPrecomputedValues() {
return ImmutableList.of(
PrecomputedValue.injected(RepositoryDelegatorFunction.REPOSITORY_OVERRIDES, overrides),
PrecomputedValue.injected(
RepositoryDelegatorFunction.RESOLVED_FILE_FOR_VERIFICATION, resolvedFile),
PrecomputedValue.injected(
RepositoryDelegatorFunction.OUTPUT_VERIFICATION_REPOSITORY_RULES,
outputVerificationRules),
PrecomputedValue.injected(
RepositoryDelegatorFunction.RESOLVED_FILE_INSTEAD_OF_WORKSPACE,
resolvedFileReplacingWorkspace),
// That key will be reinjected by the sync command with a universally unique identifier.
// Nevertheless, we need to provide a default value for other commands.
PrecomputedValue.injected(
RepositoryDelegatorFunction.DEPENDENCY_FOR_UNCONDITIONAL_FETCHING,
RepositoryDelegatorFunction.DONT_FETCH_UNCONDITIONALLY),
PrecomputedValue.injected(
RepositoryDelegatorFunction.DEPENDENCY_FOR_UNCONDITIONAL_CONFIGURING,
RepositoryDelegatorFunction.DONT_FETCH_UNCONDITIONALLY));
}
@Override
public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) {
return ImmutableList.<Class<? extends OptionsBase>>of(RepositoryOptions.class);
}
}