Skip to content

Commit

Permalink
Vendor registry files
Browse files Browse the repository at this point in the history
- Vendor registry files needed for Bazel module resolution to achieve
offline build with vendor mode.
- Also refactored bazel_vendor_test to avoid vendoring dependencies of
  bazel_tools, which speeds up the test significantly.

Fixes #22554
  • Loading branch information
meteorcloudy committed Jun 3, 2024
1 parent ee68688 commit 0747210
Show file tree
Hide file tree
Showing 17 changed files with 405 additions and 138 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ public void serverInit(OptionsParsingResult startupOptions, ServerBuilder builde
builder.addCommands(new FetchCommand());
builder.addCommands(new ModCommand());
builder.addCommands(new SyncCommand());
builder.addCommands(new VendorCommand());
builder.addCommands(new VendorCommand(downloadManager, clientEnvironmentSupplier));
builder.addInfoItems(new RepositoryCacheInfoItem(repositoryCache));
}

Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ java_library(
],
)

java_library(
name = "vendor",
srcs = ["VendorUtil.java"],
deps = [
"//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
"//src/main/java/com/google/devtools/build/lib/cmdline",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
"//third_party:guava",
],
)

java_library(
name = "module_extension",
srcs = [
Expand Down Expand Up @@ -80,13 +92,15 @@ java_library(
],
deps = [
":common",
":vendor",
":yanked_versions_value",
"//src/main/java/com/google/devtools/build/lib/bazel/repository:repository_options",
"//src/main/java/com/google/devtools/build/lib/bazel/repository/cache",
"//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
"//src/main/java/com/google/devtools/build/lib/events",
"//src/main/java/com/google/devtools/build/lib/profiler",
"//src/main/java/com/google/devtools/build/lib/util:os",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
"//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
"//third_party:gson",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public abstract class BazelModuleResolutionValue implements SkyValue {
/**
* Hashes of files obtained (or known to be missing) from registries while performing resolution.
*/
abstract ImmutableMap<String, Optional<Checksum>> getRegistryFileHashes();
public abstract ImmutableMap<String, Optional<Checksum>> getRegistryFileHashes();

/**
* Selected module versions that are known to be yanked (and hence must have been explicitly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@
import com.google.devtools.build.lib.profiler.ProfilerTask;
import com.google.devtools.build.lib.profiler.SilentCloseable;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
Expand Down Expand Up @@ -88,6 +90,7 @@ public enum KnownFileHashesMode {
private final Gson gson;
private final ImmutableMap<String, Optional<Checksum>> knownFileHashes;
private final ImmutableMap<ModuleKey, String> previouslySelectedYankedVersions;
private final Optional<Path> vendorDir;
private final KnownFileHashesMode knownFileHashesMode;
private volatile Optional<BazelRegistryJson> bazelRegistryJson;
private volatile StoredEventHandler bazelRegistryJsonEvents;
Expand All @@ -100,7 +103,8 @@ public IndexRegistry(
Map<String, String> clientEnv,
ImmutableMap<String, Optional<Checksum>> knownFileHashes,
KnownFileHashesMode knownFileHashesMode,
ImmutableMap<ModuleKey, String> previouslySelectedYankedVersions) {
ImmutableMap<ModuleKey, String> previouslySelectedYankedVersions,
Optional<Path> vendorDir) {
this.uri = uri;
this.downloadManager = downloadManager;
this.clientEnv = clientEnv;
Expand All @@ -111,6 +115,7 @@ public IndexRegistry(
this.knownFileHashes = knownFileHashes;
this.knownFileHashesMode = knownFileHashesMode;
this.previouslySelectedYankedVersions = previouslySelectedYankedVersions;
this.vendorDir = vendorDir;
}

@Override
Expand Down Expand Up @@ -143,19 +148,19 @@ private Optional<byte[]> grabFile(
}

private Optional<byte[]> doGrabFile(
String url, ExtendedEventHandler eventHandler, boolean useChecksum)
String rawURL, ExtendedEventHandler eventHandler, boolean useChecksum)
throws IOException, InterruptedException {
Optional<Checksum> checksum;
if (knownFileHashesMode != KnownFileHashesMode.IGNORE && useChecksum) {
Optional<Checksum> knownChecksum = knownFileHashes.get(url);
Optional<Checksum> knownChecksum = knownFileHashes.get(rawURL);
if (knownChecksum == null) {
if (knownFileHashesMode == KnownFileHashesMode.ENFORCE) {
throw new MissingChecksumException(
String.format(
"Missing checksum for registry file %s not permitted with --lockfile_mode=error."
+ " Please run `bazel mod deps --lockfile_mode=update` to update your"
+ " lockfile.",
url));
rawURL));
}
// This is a new file, download without providing a checksum.
checksum = Optional.empty();
Expand All @@ -182,17 +187,36 @@ private Optional<byte[]> doGrabFile(
"Cannot fetch a file without a checksum in ENFORCE mode. This is a bug in Bazel, please "
+ "report at https://github.com/bazelbuild/bazel/issues/new/choose.");
}

URL url = new URL(rawURL);
// Don't try to read the registry URL from the vendor directory in the following cases:
// 1. The vendor directory is not set, which means vendor mode is disabled.
// 2. The checksum is not present, which means the URL is not vendored or the vendored content is out-dated.
// 3. The URL starts with "file:", which means it's a local file and isn't vendored.
// Otherwise, check if the URL is vendored and read the registry file from the vendor directory.
if (vendorDir.isPresent() && checksum.isPresent() && !url.getProtocol().equals("file")) {
VendorUtil vendorUtil = new VendorUtil(vendorDir.get());
if (vendorUtil.isUrlVendored(url)) {
try {
return Optional.of(vendorUtil.readRegistryURL(url, checksum.get()));
} catch (IOException e) {
throw new IOException(
"Failed to read vendored registry file %s at %s: %s. Please rerun the bazel vendor command.".formatted(rawURL, vendorUtil.getVendorPathForURL(url), e.getMessage()), e);
}
}
}

try (SilentCloseable c =
Profiler.instance().profile(ProfilerTask.BZLMOD, () -> "download file: " + url)) {
Profiler.instance().profile(ProfilerTask.BZLMOD, () -> "download file: " + rawURL)) {
return Optional.of(
downloadManager.downloadAndReadOneUrlForBzlmod(
new URL(url), eventHandler, clientEnv, checksum));
url, eventHandler, clientEnv, checksum));
} catch (FileNotFoundException e) {
return Optional.empty();
} catch (IOException e) {
// Include the URL in the exception message for easier debugging.
throw new IOException(
"Failed to fetch registry file %s: %s".formatted(url, e.getMessage()), e);
"Failed to fetch registry file %s: %s".formatted(rawURL, e.getMessage()), e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.bazel.repository.RepositoryOptions;
import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
import com.google.devtools.build.lib.vfs.Path;

import java.net.URISyntaxException;
import java.util.Optional;

Expand All @@ -33,6 +35,7 @@ Registry createRegistry(
String url,
RepositoryOptions.LockfileMode lockfileMode,
ImmutableMap<String, Optional<Checksum>> fileHashes,
ImmutableMap<ModuleKey, String> previouslySelectedYankedVersions)
ImmutableMap<ModuleKey, String> previouslySelectedYankedVersions,
Optional<Path> vendorDir)
throws URISyntaxException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.LockfileMode;
import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
import com.google.devtools.build.lib.bazel.repository.downloader.DownloadManager;
import com.google.devtools.build.lib.vfs.Path;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
Expand All @@ -42,7 +44,8 @@ public Registry createRegistry(
String url,
LockfileMode lockfileMode,
ImmutableMap<String, Optional<Checksum>> knownFileHashes,
ImmutableMap<ModuleKey, String> previouslySelectedYankedVersions)
ImmutableMap<ModuleKey, String> previouslySelectedYankedVersions,
Optional<Path> vendorDir)
throws URISyntaxException {
URI uri = new URI(url);
if (uri.getScheme() == null) {
Expand Down Expand Up @@ -75,6 +78,7 @@ public Registry createRegistry(
clientEnvironmentSupplier.get(),
knownFileHashes,
knownFileHashesMode,
previouslySelectedYankedVersions);
previouslySelectedYankedVersions,
vendorDir);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.google.devtools.build.lib.bazel.bzlmod;

import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.LockfileMode;
import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.skyframe.PrecomputedValue.Precomputed;
import com.google.devtools.build.lib.vfs.Path;
Expand All @@ -26,6 +27,8 @@
import java.net.URISyntaxException;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;

import javax.annotation.Nullable;

/** A simple SkyFunction that creates a {@link Registry} with a given URL. */
Expand Down Expand Up @@ -56,6 +59,7 @@ public RegistryFunction(RegistryFactory registryFactory, Path workspaceRoot) {
public SkyValue compute(SkyKey skyKey, Environment env)
throws InterruptedException, RegistryException {
LockfileMode lockfileMode = BazelLockFileFunction.LOCKFILE_MODE.get(env);
Optional<Path> vendorDir = RepositoryDelegatorFunction.VENDOR_DIRECTORY.get(env);

if (lockfileMode == LockfileMode.REFRESH) {
RegistryFunction.LAST_INVALIDATION.get(env);
Expand All @@ -72,7 +76,8 @@ public SkyValue compute(SkyKey skyKey, Environment env)
key.getUrl().replace("%workspace%", workspaceRoot.getPathString()),
lockfileMode,
lockfile.getRegistryFileHashes(),
lockfile.getSelectedYankedVersions());
lockfile.getSelectedYankedVersions(),
vendorDir);
} catch (URISyntaxException e) {
throw new RegistryException(
ExternalDepsException.withCauseAndMessage(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package com.google.devtools.build.lib.bazel.bzlmod;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.ImmutableList;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hasher;
import com.google.devtools.build.lib.bazel.repository.downloader.Checksum;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.Symlinks;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Objects;
import java.util.Optional;


/**
* Utility class for vendoring external repositories.
*/
public class VendorUtil {

private final Path vendorDirectory;

public VendorUtil(Path vendorDirectory) {
this.vendorDirectory = vendorDirectory;
}

/**
* Vendors the specified repositories under the vendor directory.
*
* @param externalRepoRoot The root directory of the external repositories.
* @param reposToVendor The list of repositories to vendor.
* @throws IOException if an I/O error occurs.
*/
public void vendorRepos(
Path externalRepoRoot,
ImmutableList<RepositoryName> reposToVendor)
throws IOException {
if (!vendorDirectory.exists()) {
vendorDirectory.createDirectoryAndParents();
}

for (RepositoryName repo : reposToVendor) {
// Only re-vendor the repository if it is not up-to-date.
if (!isRepoUpToDate(repo.getName(), externalRepoRoot)) {
Path repoUnderVendor = vendorDirectory.getRelative(repo.getName());
if (!repoUnderVendor.exists()) {
repoUnderVendor.createDirectory();
}
FileSystemUtils.copyTreesBelow(
externalRepoRoot.getRelative(repo.getName()), repoUnderVendor, Symlinks.NOFOLLOW);
FileSystemUtils.copyFile(
externalRepoRoot.getChild("@" + repo.getName() + ".marker"),
vendorDirectory.getChild("@" + repo.getName() + ".marker"));
}
}
}

/**
* Checks if the given URL is vendored.
*
* @param url The URL to check.
* @return true if the URL is vendored, false otherwise.
* @throws UnsupportedEncodingException if the URL decoding fails.
*/
public boolean isUrlVendored(URL url) throws UnsupportedEncodingException {
return getVendorPathForURL(url).isFile();
}

/**
* Vendors the registry URL with the specified content.
*
* @param url The registry URL to vendor.
* @param content The content to write.
* @throws IOException if an I/O error occurs.
*/
public void vendorRegistryURL(URL url, byte[] content)
throws IOException {
Path outputPath = getVendorPathForURL(url);
Objects.requireNonNull(outputPath.getParentDirectory()).createDirectoryAndParents();
FileSystemUtils.writeContent(outputPath, content);
}

/**
* Reads the content of the registry URL and verifies its checksum.
*
* @param url The registry URL to read.
* @param checksum The checksum to verify.
* @return The content of the registry URL.
* @throws IOException if an I/O error occurs or the checksum verification fails.
*/
public byte[] readRegistryURL(URL url, Checksum checksum)
throws IOException {
byte[] content = FileSystemUtils.readContent(getVendorPathForURL(url));
Hasher hasher = checksum.getKeyType().newHasher();
hasher.putBytes(content);
HashCode actual = hasher.hash();
if (!checksum.getHashCode().equals(actual)) {
throw new IOException(
String.format(
"Checksum was %s but wanted %s",
checksum.emitOtherHashInSameFormat(actual),
checksum.emitOtherHashInSameFormat(checksum.getHashCode())));
}
return content;
}

/**
* Checks if the repository under vendor dir needs to be updated by comparing its marker file with the
* one under <output_base>/external. This function assumes the marker file under <output_base>/external exists
* and is up-to-date.
*
* @param repoName The name of the repository.
* @param externalPath The root directory of the external repositories.
* @return true if the repository is up-to-date, false otherwise.
* @throws IOException if an I/O error occurs.
*/
private boolean isRepoUpToDate(String repoName, Path externalPath)
throws IOException {
Path vendorMarkerFile = vendorDirectory.getChild("@" + repoName + ".marker");
if (!vendorMarkerFile.exists()) {
return false;
}

Path externalMarkerFile = externalPath.getChild("@" + repoName + ".marker");
String vendorMarkerContent = FileSystemUtils.readContent(vendorMarkerFile, UTF_8);
String externalMarkerContent = FileSystemUtils.readContent(externalMarkerFile, UTF_8);
return Objects.equals(vendorMarkerContent, externalMarkerContent);
}

/**
* Returns the vendor path for the given URL.
*
* The vendor path is constructed as follows:
* <vendor_directory>/registry_cache/<host>/<path>
*
* The host name is case-insensitive, so it is converted to lowercase.
* The path is case-sensitive, so it is left as is.
* The port number is not included in the vendor path.
*
* Note that the vendor path may conflicts if two URLs only differ by the case or port number.
* But this is unlikely to happen in practice, and conflicts are checked in VendorCommand.java.
*
* @param url The URL to get the vendor path for.
* @return The vendor path.
* @throws UnsupportedEncodingException if the URL decoding fails.
*/
public Path getVendorPathForURL(URL url) throws UnsupportedEncodingException {
String host = url.getHost().toLowerCase(); // Host names are case-insensitive
String path = url.getPath();
path = URLDecoder.decode(path, "UTF-8");
if (path.startsWith("/")) {
path = path.substring(1);
}
return vendorDirectory.getRelative("registry_cache").getRelative(host).getRelative(path);
}
}
Loading

0 comments on commit 0747210

Please sign in to comment.