Skip to content

Commit

Permalink
Add module_ctx.extension_metadata and module_ctx.use_all_repos
Browse files Browse the repository at this point in the history
  • Loading branch information
fmeum committed Apr 12, 2023
1 parent 6de73af commit 7a86b6d
Show file tree
Hide file tree
Showing 15 changed files with 694 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ java_library(
"Discovery.java",
"GsonTypeAdapterUtil.java",
"ModuleExtensionContext.java",
"ModuleExtensionMetadata.java",
"ModuleFileFunction.java",
"ModuleFileGlobals.java",
"Selection.java",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
Expand All @@ -29,8 +30,10 @@
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import javax.annotation.Nullable;
import net.starlark.java.eval.Dict;
Expand Down Expand Up @@ -92,6 +95,14 @@ private DelegateTypeAdapterFactory(
raw -> new ArrayList<>((List<?>) raw),
delegate -> ImmutableList.copyOf((List<?>) delegate));

public static final TypeAdapterFactory IMMUTABLE_SET =
new DelegateTypeAdapterFactory<>(
ImmutableSet.class,
Set.class,
LinkedHashSet.class,
raw -> new LinkedHashSet<>((Set<?>) raw),
delegate -> ImmutableSet.copyOf((Set<?>) delegate));

@SuppressWarnings("unchecked")
@Override
@Nullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static com.google.devtools.build.lib.bazel.bzlmod.DelegateTypeAdapterFactory.IMMUTABLE_BIMAP;
import static com.google.devtools.build.lib.bazel.bzlmod.DelegateTypeAdapterFactory.IMMUTABLE_LIST;
import static com.google.devtools.build.lib.bazel.bzlmod.DelegateTypeAdapterFactory.IMMUTABLE_MAP;
import static com.google.devtools.build.lib.bazel.bzlmod.DelegateTypeAdapterFactory.IMMUTABLE_SET;

import com.google.common.base.Splitter;
import com.google.devtools.build.lib.bazel.bzlmod.Version.ParseException;
Expand Down Expand Up @@ -96,6 +97,7 @@ public ModuleKey read(JsonReader jsonReader) throws IOException {
.registerTypeAdapterFactory(IMMUTABLE_MAP)
.registerTypeAdapterFactory(IMMUTABLE_LIST)
.registerTypeAdapterFactory(IMMUTABLE_BIMAP)
.registerTypeAdapterFactory(IMMUTABLE_SET)
.registerTypeAdapter(Version.class, VERSION_TYPE_ADAPTER)
.registerTypeAdapter(ModuleKey.class, MODULE_KEY_TYPE_ADAPTER)
.registerTypeAdapter(AttributeValues.class, new AttributeValuesAdapter())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.annot.StarlarkMethod;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.NoneType;
import net.starlark.java.eval.Sequence;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkList;
import net.starlark.java.eval.StarlarkSemantics;

Expand Down Expand Up @@ -117,4 +120,35 @@ public StarlarkList<StarlarkBazelModule> getModules() {
public boolean isDevDependency(TypeCheckedTag tag) {
return tag.isDevDependency();
}

@StarlarkMethod(
name = "extension_metadata",
doc = "foo",
parameters = {
@Param(
name = "root_module_direct_deps",
doc = "foo",
positional = false,
named = true,
defaultValue = "None",
allowedTypes = {
@ParamType(type = Sequence.class, generic1 = String.class),
@ParamType(type = String.class),
@ParamType(type = NoneType.class)}),
@Param(
name = "root_module_direct_dev_deps",
doc = "foo",
positional = false,
named = true,
defaultValue = "None",
allowedTypes = {
@ParamType(type = Sequence.class, generic1 = String.class),
@ParamType(type = String.class),
@ParamType(type = NoneType.class)}),
})
public ModuleExtensionMetadata extensionMetadata(Object rootModuleDirectDepsUnchecked,
Object rootModuleDirectDevDepsUnchecked) throws EvalException {
return ModuleExtensionMetadata.create(rootModuleDirectDepsUnchecked,
rootModuleDirectDevDepsUnchecked);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
package com.google.devtools.build.lib.bazel.bzlmod;


import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;
import com.google.devtools.build.docgen.annot.DocCategory;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Sequence;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkList;
import net.starlark.java.eval.StarlarkValue;
import net.starlark.java.syntax.Location;

@StarlarkBuiltin(
name = "extension_metadata",
category = DocCategory.BUILTIN,
doc = "Return values of this type from a module extension's implementation function to "
+ "provide metadata about the repositories generated by the extension to Bazel.")
public class ModuleExtensionMetadata implements StarlarkValue {
@Nullable
private final ImmutableSet<String> explicitRootModuleDirectDeps;
@Nullable
private final ImmutableSet<String> explicitRootModuleDirectDevDeps;
private final UseAllRepos useAllRepos;

private ModuleExtensionMetadata(@Nullable Set<String> explicitRootModuleDirectDeps,
@Nullable Set<String> explicitRootModuleDirectDevDeps, UseAllRepos useAllRepos) {
this.explicitRootModuleDirectDeps = explicitRootModuleDirectDeps != null ? ImmutableSet
.copyOf(explicitRootModuleDirectDeps) : null;
this.explicitRootModuleDirectDevDeps = explicitRootModuleDirectDevDeps != null ? ImmutableSet
.copyOf(explicitRootModuleDirectDevDeps) : null;
this.useAllRepos = useAllRepos;
}

static ModuleExtensionMetadata create(Object rootModuleDirectDepsUnchecked,
Object rootModuleDirectDevDepsUnchecked) throws EvalException {
if (rootModuleDirectDepsUnchecked == Starlark.NONE
&& rootModuleDirectDevDepsUnchecked == Starlark.NONE) {
return new ModuleExtensionMetadata(null, null, UseAllRepos.NO);
}

// When root_module_direct_deps = "all", accept both root_module_direct_dev_deps = None and
// root_module_direct_dev_deps = [], but not root_module_direct_dev_deps = ["some_repo"].
if (rootModuleDirectDepsUnchecked.equals("all") && rootModuleDirectDevDepsUnchecked.equals(
StarlarkList.immutableOf())) {
return new ModuleExtensionMetadata(null, null, UseAllRepos.REGULAR);
}

if (rootModuleDirectDevDepsUnchecked.equals("all") && rootModuleDirectDepsUnchecked.equals(
StarlarkList.immutableOf())) {
return new ModuleExtensionMetadata(null, null, UseAllRepos.DEV);
}

if (rootModuleDirectDepsUnchecked.equals("all") || rootModuleDirectDevDepsUnchecked.equals(
"all")) {
throw Starlark.errorf("if one of root_module_direct_deps and root_module_direct_dev_deps is "
+ "\"all\", the other must be an empty list");
}

if (rootModuleDirectDepsUnchecked instanceof String
|| rootModuleDirectDevDepsUnchecked instanceof String) {
throw Starlark.errorf("root_module_direct_deps and root_module_direct_dev_deps must be "
+ "None, \"all\", or a list of strings");
}
if ((rootModuleDirectDepsUnchecked == Starlark.NONE) != (rootModuleDirectDevDepsUnchecked
== Starlark.NONE)) {
throw Starlark.errorf("root_module_direct_deps and root_module_direct_dev_deps must both be "
+ "specified or both be unspecified");
}

Sequence<String> rootModuleDirectDeps = Sequence.cast(rootModuleDirectDepsUnchecked,
String.class, "root_module_direct_deps");
Sequence<String> rootModuleDirectDevDeps =
Sequence.cast(rootModuleDirectDevDepsUnchecked, String.class,
"root_module_direct_dev_deps");

Set<String> explicitRootModuleDirectDeps = new LinkedHashSet<>();
for (String dep : rootModuleDirectDeps) {
try {
RepositoryName.validateUserProvidedRepoName(dep);
} catch (EvalException e) {
throw Starlark.errorf("in root_module_direct_deps: %s", e.getMessage());
}
if (!explicitRootModuleDirectDeps.add(dep)) {
throw Starlark.errorf("in root_module_direct_deps: duplicate entry '%s'", dep);
}
}

Set<String> explicitRootModuleDirectDevDeps = new LinkedHashSet<>();
for (String dep : rootModuleDirectDevDeps) {
try {
RepositoryName.validateUserProvidedRepoName(dep);
} catch (EvalException e) {
throw Starlark.errorf("in root_module_direct_dev_deps: %s", e.getMessage());
}
if (explicitRootModuleDirectDeps.contains(dep)) {
throw Starlark.errorf("in root_module_direct_dev_deps: entry '%s' is also in "
+ "root_module_direct_deps", dep);
}
if (!explicitRootModuleDirectDevDeps.add(dep)) {
throw Starlark.errorf("in root_module_direct_dev_deps: duplicate entry '%s'", dep);
}
}

return new ModuleExtensionMetadata(explicitRootModuleDirectDeps,
explicitRootModuleDirectDevDeps, UseAllRepos.NO);
}

public void evaluate(Collection<ModuleExtensionUsage> usages, Set<String> allRepos,
EventHandler handler) throws EvalException {
generateFixupMessage(usages, allRepos).ifPresent(handler::handle);
}

Optional<Event> generateFixupMessage(Collection<ModuleExtensionUsage> usages,
Set<String> allRepos) throws EvalException {
var rootUsages = usages.stream()
.filter(usage -> usage.getModuleKey().equals(ModuleKey.ROOT))
.collect(ImmutableList.toImmutableList());
if (rootUsages.isEmpty()) {
// The root module doesn't use the current extension. Do not suggest fixes as the user isn't
// expected to modify any other module's MODULE.bazel file.
return Optional.empty();
}

var rootModuleDirectDevDeps = getRootModuleDirectDevDeps(allRepos);
var rootModuleDirectDeps = getRootModuleDirectDeps(allRepos);
if (rootModuleDirectDevDeps.isEmpty() && rootModuleDirectDeps.isEmpty()) {
return Optional.empty();
}

Preconditions.checkState(
rootModuleDirectDevDeps.isPresent() && rootModuleDirectDeps.isPresent());
return generateFixupMessage(rootUsages, allRepos, rootModuleDirectDeps.get(),
rootModuleDirectDevDeps.get());
}

private static Optional<Event> generateFixupMessage(List<ModuleExtensionUsage> rootUsages,
Set<String> allRepos, Set<String> expectedImports, Set<String> expectedDevImports) {
var actualDevImports = rootUsages.stream()
.flatMap(usage -> usage.getDevImports().stream())
.collect(ImmutableSet.toImmutableSet());
var actualImports = rootUsages.stream()
.flatMap(usage -> usage.getImports().keySet().stream())
.filter(repo -> !actualDevImports.contains(repo))
.collect(ImmutableSet.toImmutableSet());

// All label strings that map to the same Label are equivalent for buildozer as it implements
// the same normalization of label strings with no or empty repo name.
ModuleExtensionUsage firstUsage = rootUsages.get(0);
String extensionBzlFile = firstUsage.getExtensionBzlFile();
String extensionName = firstUsage.getExtensionName();
Location location = firstUsage.getLocation();

var importsToAdd = ImmutableSortedSet.copyOf(Sets.difference(expectedImports, actualImports));
var importsToRemove = ImmutableSortedSet.copyOf(
Sets.difference(actualImports, expectedImports));
var devImportsToAdd = ImmutableSortedSet.copyOf(
Sets.difference(expectedDevImports, actualDevImports));
var devImportsToRemove = ImmutableSortedSet.copyOf(
Sets.difference(actualDevImports, expectedDevImports));

if (importsToAdd.isEmpty() && importsToRemove.isEmpty() && devImportsToAdd.isEmpty()
&& devImportsToRemove.isEmpty()) {
return Optional.empty();
}

var message = String.format("The module extension %s defined in %s reported incorrect imports "
+ "of repositories via use_repo():\n\n", extensionName, extensionBzlFile);

var allActualImports = ImmutableSortedSet.copyOf(Sets.union(actualImports, actualDevImports));
var allExpectedImports = ImmutableSortedSet.copyOf(
Sets.union(expectedImports, expectedDevImports));

var invalidImports = ImmutableSortedSet.copyOf(Sets.difference(allActualImports, allRepos));
if (!invalidImports.isEmpty()) {
message += String.format(
"Imported, but not created by the extension (will cause the build to fail):\n %s\n\n",
String.join(", ", invalidImports));
}

var missingImports = ImmutableSortedSet.copyOf(
Sets.difference(allExpectedImports, allActualImports));
if (!missingImports.isEmpty()) {
message += String.format(
"Not imported, but declared as direct dependencies (may cause the build to fail):\n %s\n\n",
String.join(", ", missingImports));
}

var indirectDepImports = ImmutableSortedSet.copyOf(
Sets.difference(Sets.intersection(allActualImports, allRepos), allExpectedImports));
if (!indirectDepImports.isEmpty()) {
message += String.format(
"Imported, but not declared as direct dependencies:\n %s\n\n",
String.join(", ", indirectDepImports));
}

var fixupCommands = Stream.of(
makeUseRepoCommand("use_repo_add", false, importsToAdd, extensionBzlFile, extensionName),
makeUseRepoCommand("use_repo_remove", false, importsToRemove, extensionBzlFile,
extensionName),
makeUseRepoCommand("use_repo_add", true, devImportsToAdd, extensionBzlFile,
extensionName),
makeUseRepoCommand("use_repo_remove", true, devImportsToRemove, extensionBzlFile,
extensionName)
).flatMap(Optional::stream);

return Optional.of(Event.warn(location, message + String.format(
"%s ** You can use the following buildozer command(s) to fix these issues:%s\n\n%s",
"\033[35m\033[1m", "\033[0m", fixupCommands.collect(Collectors.joining("\n")))));
}

private static Optional<String> makeUseRepoCommand(String cmd, boolean devDependency,
Collection<String> repos, String extensionBzlFile, String extensionName) {
if (repos.isEmpty()) {
return Optional.empty();
}
return Optional.of(String.format("buildozer '%s%s %s %s %s' //MODULE.bazel:all", cmd,
devDependency ? " dev" : "", extensionBzlFile, extensionName, String.join(" ", repos)));
}

private Optional<ImmutableSet<String>> getRootModuleDirectDeps(Set<String> allRepos)
throws EvalException {
switch (useAllRepos) {
case NO:
if (explicitRootModuleDirectDeps != null) {
Set<String> invalidRepos = Sets.difference(explicitRootModuleDirectDeps, allRepos);
if (!invalidRepos.isEmpty()) {
throw Starlark.errorf("root_module_direct_deps contained the following repositories "
+ "not generated by the extension: %s", String.join(", ", invalidRepos));
}
}
return Optional.ofNullable(explicitRootModuleDirectDeps);
case REGULAR:
return Optional.of(ImmutableSet.copyOf(allRepos));
case DEV:
return Optional.of(ImmutableSet.of());
default:
throw new IllegalStateException("not reached");
}
}

private Optional<ImmutableSet<String>> getRootModuleDirectDevDeps(Set<String> allRepos)
throws EvalException {
switch (useAllRepos) {
case NO:
if (explicitRootModuleDirectDevDeps != null) {
Set<String> invalidRepos = Sets.difference(explicitRootModuleDirectDevDeps, allRepos);
if (!invalidRepos.isEmpty()) {
throw Starlark.errorf("root_module_direct_dev_deps contained the following "
+ "repositories not generated by the extension: %s",
String.join(", ", invalidRepos));
}
}
return Optional.ofNullable(explicitRootModuleDirectDevDeps);
case REGULAR:
return Optional.of(ImmutableSet.of());
case DEV:
return Optional.of(ImmutableSet.copyOf(allRepos));
default:
throw new IllegalStateException("not reached");
}
}

private enum UseAllRepos {
NO,
REGULAR,
DEV,
}
}
Loading

0 comments on commit 7a86b6d

Please sign in to comment.