blob: fd2bfddd42120a0d10a5d1a9ec15727b2e131932 [file] [log] [blame]
// Copyright 2023 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.bzlmod;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableMap;
import com.google.common.eventbus.Subscribe;
import com.google.common.flogger.GoogleLogger;
import com.google.devtools.build.lib.bazel.repository.RepositoryOptions;
import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.LockfileMode;
import com.google.devtools.build.lib.cmdline.LabelConstants;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.gson.JsonSyntaxException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Module collecting Bazel module and module extensions resolution results and updating the
* lockfile.
*/
public class BazelLockFileModule extends BlazeModule {
private Path workspaceRoot;
@Nullable private BazelModuleResolutionEvent moduleResolutionEvent;
private final List<ModuleExtensionResolutionEvent> extensionResolutionEvents = new ArrayList<>();
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
@Override
public void beforeCommand(CommandEnvironment env) {
workspaceRoot = env.getWorkspace();
RepositoryOptions options = env.getOptions().getOptions(RepositoryOptions.class);
if (options.lockfileMode.equals(LockfileMode.UPDATE)) {
env.getEventBus().register(this);
}
}
@Override
public void afterCommand() throws AbruptExitException {
if (moduleResolutionEvent == null && extensionResolutionEvents.isEmpty()) {
return; // nothing changed, do nothing!
}
RootedPath lockfilePath =
RootedPath.toRootedPath(Root.fromPath(workspaceRoot), LabelConstants.MODULE_LOCKFILE_NAME);
// Create an updated version of the lockfile with the events updates
BazelLockFileValue lockfile;
if (moduleResolutionEvent != null) {
lockfile = moduleResolutionEvent.getLockfileValue();
} else {
// Read the existing lockfile (if none exists, will get an empty lockfile value)
try {
lockfile = BazelLockFileFunction.getLockfileValue(lockfilePath);
} catch (IOException | JsonSyntaxException | NullPointerException e) {
logger.atSevere().withCause(e).log(
"Failed to read and parse the MODULE.bazel.lock file with error: %s."
+ " Try deleting it and rerun the build.",
e.getMessage());
return;
}
}
lockfile =
lockfile.toBuilder()
.setModuleExtensions(combineModuleExtensions(lockfile.getModuleExtensions()))
.build();
// Write the new value to the file
updateLockfile(lockfilePath, lockfile);
this.moduleResolutionEvent = null;
this.extensionResolutionEvents.clear();
}
/**
* Combines the old extensions stored in the lockfile -that are still used- with the new
* extensions from the events (if any)
*
* @param oldModuleExtensions Module extensions stored in the current lockfile
*/
private ImmutableMap<ModuleExtensionId, LockFileModuleExtension> combineModuleExtensions(
ImmutableMap<ModuleExtensionId, LockFileModuleExtension> oldModuleExtensions) {
ImmutableMap.Builder<ModuleExtensionId, LockFileModuleExtension> updatedExtensionMap =
ImmutableMap.builder();
// This event being null means that no changes occurred to the usages of the stored extensions,
// hence no changes to any module resulted in re-running resolution. So we can just add all the
// old stored extensions. Otherwise, check the usage of each one.
if (moduleResolutionEvent == null) {
updatedExtensionMap.putAll(oldModuleExtensions);
} else {
// Add the old extensions (stored in the lockfile) only if it still has a usage somewhere
for (Map.Entry<ModuleExtensionId, LockFileModuleExtension> extensionEntry :
oldModuleExtensions.entrySet()) {
if (moduleResolutionEvent.getExtensionUsagesById().containsRow(extensionEntry.getKey())) {
updatedExtensionMap.put(extensionEntry);
}
}
}
// Add the new resolved extensions
for (ModuleExtensionResolutionEvent extensionEvent : extensionResolutionEvents) {
updatedExtensionMap.put(extensionEvent.getExtensionId(), extensionEvent.getModuleExtension());
}
return updatedExtensionMap.buildKeepingLast();
}
/**
* Updates the data stored in the lockfile (MODULE.bazel.lock)
*
* @param lockfilePath Rooted path to lockfile
* @param updatedLockfile The updated lockfile data to save
*/
public static void updateLockfile(RootedPath lockfilePath, BazelLockFileValue updatedLockfile) {
try {
FileSystemUtils.writeContent(
lockfilePath.asPath(),
UTF_8,
GsonTypeAdapterUtil.createLockFileGson(
lockfilePath
.asPath()
.getParentDirectory()
.getRelative(LabelConstants.MODULE_DOT_BAZEL_FILE_NAME))
.toJson(updatedLockfile));
} catch (IOException e) {
logger.atSevere().withCause(e).log(
"Error while updating MODULE.bazel.lock file: %s", e.getMessage());
}
}
@Subscribe
public void bazelModuleResolved(BazelModuleResolutionEvent moduleResolutionEvent) {
this.moduleResolutionEvent = moduleResolutionEvent;
}
@Subscribe
public void moduleExtensionResolved(ModuleExtensionResolutionEvent extensionResolutionEvent) {
this.extensionResolutionEvents.add(extensionResolutionEvent);
}
}