| // Copyright 2021 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 com.google.common.collect.ImmutableList.toImmutableList; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableCollection; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.devtools.build.docgen.annot.GlobalMethods; |
| import com.google.devtools.build.docgen.annot.GlobalMethods.Environment; |
| import com.google.devtools.build.lib.bazel.bzlmod.InterimModule.DepSpec; |
| import com.google.devtools.build.lib.bazel.bzlmod.ModuleThreadContext.ModuleExtensionUsageBuilder; |
| import com.google.devtools.build.lib.bazel.bzlmod.Version.ParseException; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.cmdline.LabelSyntaxException; |
| import com.google.devtools.build.lib.cmdline.PackageIdentifier; |
| import com.google.devtools.build.lib.cmdline.RepositoryMapping; |
| import com.google.devtools.build.lib.cmdline.RepositoryName; |
| import com.google.devtools.build.lib.events.EventHandler; |
| import com.google.devtools.build.lib.packages.StarlarkExportable; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| import javax.annotation.Nullable; |
| import net.starlark.java.annot.Param; |
| import net.starlark.java.annot.ParamType; |
| import net.starlark.java.annot.StarlarkBuiltin; |
| import net.starlark.java.annot.StarlarkMethod; |
| import net.starlark.java.eval.Dict; |
| import net.starlark.java.eval.EvalException; |
| import net.starlark.java.eval.Sequence; |
| import net.starlark.java.eval.Starlark; |
| import net.starlark.java.eval.StarlarkInt; |
| import net.starlark.java.eval.StarlarkThread; |
| import net.starlark.java.eval.StarlarkValue; |
| import net.starlark.java.eval.Structure; |
| import net.starlark.java.eval.Tuple; |
| import net.starlark.java.syntax.Location; |
| |
| /** A collection of global Starlark build API functions that apply to MODULE.bazel files. */ |
| @GlobalMethods(environment = Environment.MODULE) |
| public class ModuleFileGlobals { |
| |
| /* Valid bazel compatibility argument must 1) start with (<,<=,>,>=,-); |
| 2) then contain a version number in form of X.X.X where X has one or two digits |
| */ |
| private static final Pattern VALID_BAZEL_COMPATIBILITY_VERSION = |
| Pattern.compile("(>|<|-|<=|>=)(\\d+\\.){2}\\d+"); |
| |
| @VisibleForTesting |
| static void validateModuleName(String moduleName) throws EvalException { |
| if (!RepositoryName.VALID_MODULE_NAME.matcher(moduleName).matches()) { |
| throw Starlark.errorf( |
| "invalid module name '%s': valid names must 1) only contain lowercase letters (a-z)," |
| + " digits (0-9), dots (.), hyphens (-), and underscores (_); 2) begin with a" |
| + " lowercase letter; 3) end with a lowercase letter or digit.", |
| moduleName); |
| } |
| } |
| |
| @StarlarkMethod( |
| name = "module", |
| doc = |
| "Declares certain properties of the Bazel module represented by the current Bazel repo." |
| + " These properties are either essential metadata of the module (such as the name" |
| + " and version), or affect behavior of the current module and its dependents. <p>It" |
| + " should be called at most once, and if called, it must be the very first directive" |
| + " in the MODULE.bazel file. It can be omitted only if this module is the root" |
| + " module (as in, if it's not going to be depended on by another module).", |
| parameters = { |
| @Param( |
| name = "name", |
| doc = |
| "The name of the module. Can be omitted only if this module is the root module (as" |
| + " in, if it's not going to be depended on by another module). A valid module" |
| + " name must: 1) only contain lowercase letters (a-z), digits (0-9), dots (.)," |
| + " hyphens (-), and underscores (_); 2) begin with a lowercase letter; 3) end" |
| + " with a lowercase letter or digit.", |
| named = true, |
| positional = false, |
| defaultValue = "''"), |
| @Param( |
| name = "version", |
| doc = |
| "The version of the module. Can be omitted only if this module is the root module" |
| + " (as in, if it's not going to be depended on by another module). The version" |
| + " must be in a relaxed SemVer format; see <a" |
| + " href=\"/external/module#version_format\">the documentation</a> for more" |
| + " details.", |
| named = true, |
| positional = false, |
| defaultValue = "''"), |
| @Param( |
| name = "compatibility_level", |
| doc = |
| "The compatibility level of the module; this should be changed every time a major" |
| + " incompatible change is introduced. This is essentially the \"major" |
| + " version\" of the module in terms of SemVer, except that it's not embedded" |
| + " in the version string itself, but exists as a separate field. Modules with" |
| + " different compatibility levels participate in version resolution as if" |
| + " they're modules with different names, but the final dependency graph cannot" |
| + " contain multiple modules with the same name but different compatibility" |
| + " levels (unless <code>multiple_version_override</code> is in effect). See <a" |
| + " href=\"/external/module#compatibility_level\">the documentation</a> for" |
| + " more details.", |
| named = true, |
| positional = false, |
| defaultValue = "0"), |
| @Param( |
| name = "repo_name", |
| doc = |
| "The name of the repository representing this module, as seen by the module itself." |
| + " By default, the name of the repo is the name of the module. This can be" |
| + " specified to ease migration for projects that have been using a repo name" |
| + " for itself that differs from its module name.", |
| named = true, |
| positional = false, |
| defaultValue = "''"), |
| @Param( |
| name = "bazel_compatibility", |
| doc = |
| "A list of bazel versions that allows users to declare which Bazel versions" |
| + " are compatible with this module. It does NOT affect dependency resolution," |
| + " but bzlmod will use this information to check if your current Bazel version" |
| + " is compatible. The format of this value is a string of some constraint" |
| + " values separated by comma. Three constraints are supported: <=X.X.X: The" |
| + " Bazel version must be equal or older than X.X.X. Used when there is a known" |
| + " incompatible change in a newer version. >=X.X.X: The Bazel version must be" |
| + " equal or newer than X.X.X.Used when you depend on some features that are" |
| + " only available since X.X.X. -X.X.X: The Bazel version X.X.X is not" |
| + " compatible. Used when there is a bug in X.X.X that breaks you, but fixed in" |
| + " later versions.", |
| named = true, |
| positional = false, |
| allowedTypes = {@ParamType(type = Iterable.class, generic1 = String.class)}, |
| defaultValue = "[]"), |
| }, |
| useStarlarkThread = true) |
| public void module( |
| String name, |
| String version, |
| StarlarkInt compatibilityLevel, |
| String repoName, |
| Iterable<?> bazelCompatibility, |
| StarlarkThread thread) |
| throws EvalException { |
| ModuleThreadContext context = ModuleThreadContext.fromOrFail(thread, "module()"); |
| if (context.isModuleCalled()) { |
| throw Starlark.errorf("the module() directive can only be called once"); |
| } |
| if (context.hadNonModuleCall()) { |
| throw Starlark.errorf("if module() is called, it must be called before any other functions"); |
| } |
| context.setModuleCalled(); |
| if (!name.isEmpty()) { |
| validateModuleName(name); |
| } |
| if (repoName.isEmpty()) { |
| repoName = name; |
| context.addRepoNameUsage(name, "as the current module name", thread.getCallerLocation()); |
| } else { |
| RepositoryName.validateUserProvidedRepoName(repoName); |
| context.addRepoNameUsage( |
| repoName, "as the module's own repo name", thread.getCallerLocation()); |
| } |
| Version parsedVersion; |
| try { |
| parsedVersion = Version.parse(version); |
| } catch (ParseException e) { |
| throw new EvalException("Invalid version in module()", e); |
| } |
| context |
| .getModuleBuilder() |
| .setName(name) |
| .setVersion(parsedVersion) |
| .setCompatibilityLevel(compatibilityLevel.toInt("compatibility_level")) |
| .addBazelCompatibilityValues( |
| checkAllCompatibilityVersions(bazelCompatibility, "bazel_compatibility")) |
| .setRepoName(repoName); |
| } |
| |
| private static ImmutableList<String> checkAllAbsolutePatterns(Iterable<?> iterable, String where) |
| throws EvalException { |
| Sequence<String> list = Sequence.cast(iterable, String.class, where); |
| for (String item : list) { |
| if (!item.startsWith("//") && !item.startsWith("@")) { |
| throw Starlark.errorf( |
| "Expected absolute target patterns (must begin with '//' or '@') for '%s' argument, but" |
| + " got '%s' as an argument", |
| where, item); |
| } |
| } |
| return list.getImmutableList(); |
| } |
| |
| private static ImmutableList<String> checkAllCompatibilityVersions( |
| Iterable<?> iterable, String where) throws EvalException { |
| Sequence<String> list = Sequence.cast(iterable, String.class, where); |
| for (String version : list) { |
| if (!VALID_BAZEL_COMPATIBILITY_VERSION.matcher(version).matches()) { |
| throw Starlark.errorf( |
| "invalid version argument '%s': valid argument must 1) start with (<,<=,>,>=,-); " |
| + "2) contain a version number in form of X.X.X where X is a number", |
| version); |
| } |
| } |
| return list.getImmutableList(); |
| } |
| |
| @StarlarkMethod( |
| name = "bazel_dep", |
| doc = "Declares a direct dependency on another Bazel module.", |
| parameters = { |
| @Param( |
| name = "name", |
| doc = "The name of the module to be added as a direct dependency.", |
| named = true, |
| positional = false), |
| @Param( |
| name = "version", |
| doc = "The version of the module to be added as a direct dependency.", |
| named = true, |
| positional = false, |
| defaultValue = "''"), |
| @Param( |
| name = "max_compatibility_level", |
| doc = |
| "The maximum <code>compatibility_level</code> supported for the module to be added" |
| + " as a direct dependency. The version of the module implies the minimum" |
| + " compatibility_level supported, as well as the maximum if this attribute is" |
| + " not specified.", |
| named = true, |
| positional = false, |
| defaultValue = "-1"), |
| @Param( |
| name = "repo_name", |
| doc = |
| "The name of the external repo representing this dependency. This is by default the" |
| + " name of the module.", |
| named = true, |
| positional = false, |
| defaultValue = "''"), |
| @Param( |
| name = "dev_dependency", |
| doc = |
| "If true, this dependency will be ignored if the current module is not the root" |
| + " module or `--ignore_dev_dependency` is enabled.", |
| named = true, |
| positional = false, |
| defaultValue = "False"), |
| }, |
| useStarlarkThread = true) |
| public void bazelDep( |
| String name, |
| String version, |
| StarlarkInt maxCompatibilityLevel, |
| String repoName, |
| boolean devDependency, |
| StarlarkThread thread) |
| throws EvalException { |
| ModuleThreadContext context = ModuleThreadContext.fromOrFail(thread, "bazel_dep()"); |
| context.setNonModuleCalled(); |
| if (repoName.isEmpty()) { |
| repoName = name; |
| } |
| validateModuleName(name); |
| Version parsedVersion; |
| try { |
| parsedVersion = Version.parse(version); |
| } catch (ParseException e) { |
| throw new EvalException("Invalid version in bazel_dep()", e); |
| } |
| RepositoryName.validateUserProvidedRepoName(repoName); |
| |
| if (!(context.shouldIgnoreDevDeps() && devDependency)) { |
| context.addDep( |
| repoName, |
| DepSpec.create( |
| name, parsedVersion, maxCompatibilityLevel.toInt("max_compatibility_level"))); |
| } |
| |
| context.addRepoNameUsage(repoName, "by a bazel_dep", thread.getCallerLocation()); |
| } |
| |
| @StarlarkMethod( |
| name = "register_execution_platforms", |
| doc = |
| "Specifies already-defined execution platforms to be registered when this module is" |
| + " selected. Should be absolute target patterns (ie. beginning with either" |
| + " <code>@</code> or <code>//</code>). See <a href=\"${link toolchains}\">toolchain" |
| + " resolution</a> for more information.", |
| parameters = { |
| @Param( |
| name = "dev_dependency", |
| doc = |
| "If true, the execution platforms will not be registered if the current module is" |
| + " not the root module or `--ignore_dev_dependency` is enabled.", |
| named = true, |
| positional = false, |
| defaultValue = "False"), |
| }, |
| extraPositionals = |
| @Param( |
| name = "platform_labels", |
| allowedTypes = {@ParamType(type = Sequence.class, generic1 = String.class)}, |
| doc = "The labels of the platforms to register."), |
| useStarlarkThread = true) |
| public void registerExecutionPlatforms( |
| boolean devDependency, Sequence<?> platformLabels, StarlarkThread thread) |
| throws EvalException { |
| ModuleThreadContext context = |
| ModuleThreadContext.fromOrFail(thread, "register_execution_platforms()"); |
| context.setNonModuleCalled(); |
| if (context.shouldIgnoreDevDeps() && devDependency) { |
| return; |
| } |
| context |
| .getModuleBuilder() |
| .addExecutionPlatformsToRegister( |
| checkAllAbsolutePatterns(platformLabels, "register_execution_platforms")); |
| } |
| |
| @StarlarkMethod( |
| name = "register_toolchains", |
| doc = |
| "Specifies already-defined toolchains to be registered when this module is selected." |
| + " Should be absolute target patterns (ie. beginning with either <code>@</code> or" |
| + " <code>//</code>). See <a href=\"${link toolchains}\">toolchain resolution</a> for" |
| + " more information.", |
| parameters = { |
| @Param( |
| name = "dev_dependency", |
| doc = |
| "If true, the toolchains will not be registered if the current module is not the" |
| + " root module or `--ignore_dev_dependency` is enabled.", |
| named = true, |
| positional = false, |
| defaultValue = "False"), |
| }, |
| extraPositionals = |
| @Param( |
| name = "toolchain_labels", |
| allowedTypes = {@ParamType(type = Sequence.class, generic1 = String.class)}, |
| doc = |
| "The labels of the toolchains to register. Labels can include " |
| + "<code>:all</code>, in which case, all toolchain-providing targets in the " |
| + "package will be registered in lexicographical order by name."), |
| useStarlarkThread = true) |
| public void registerToolchains( |
| boolean devDependency, Sequence<?> toolchainLabels, StarlarkThread thread) |
| throws EvalException { |
| ModuleThreadContext context = ModuleThreadContext.fromOrFail(thread, "register_toolchains()"); |
| context.setNonModuleCalled(); |
| if (context.shouldIgnoreDevDeps() && devDependency) { |
| return; |
| } |
| context |
| .getModuleBuilder() |
| .addToolchainsToRegister(checkAllAbsolutePatterns(toolchainLabels, "register_toolchains")); |
| } |
| |
| @StarlarkMethod( |
| name = "use_extension", |
| doc = |
| "Returns a proxy object representing a module extension; its methods can be invoked to" |
| + " create module extension tags.", |
| parameters = { |
| @Param( |
| name = "extension_bzl_file", |
| doc = "A label to the Starlark file defining the module extension."), |
| @Param( |
| name = "extension_name", |
| doc = |
| "The name of the module extension to use. A symbol with this name must be exported" |
| + " by the Starlark file."), |
| @Param( |
| name = "dev_dependency", |
| doc = |
| "If true, this usage of the module extension will be ignored if the current module" |
| + " is not the root module or `--ignore_dev_dependency` is enabled.", |
| named = true, |
| positional = false, |
| defaultValue = "False"), |
| @Param( |
| name = "isolate", |
| doc = |
| "If true, this usage of the module extension will be isolated from all other " |
| + "usages, both in this and other modules. Tags created for this usage do not " |
| + "affect other usages and the repositories generated by the extension for " |
| + "this usage will be distinct from all other repositories generated by the " |
| + "extension." |
| + "<p>This parameter is currently experimental and only available with the " |
| + "flag <code>--experimental_isolated_extension_usages</code>.", |
| named = true, |
| positional = false, |
| defaultValue = "False", |
| enableOnlyWithFlag = "-experimental_isolated_extension_usages", |
| valueWhenDisabled = "False"), |
| }, |
| useStarlarkThread = true) |
| public ModuleExtensionProxy useExtension( |
| String rawExtensionBzlFile, |
| String extensionName, |
| boolean devDependency, |
| boolean isolate, |
| StarlarkThread thread) |
| throws EvalException { |
| ModuleThreadContext context = ModuleThreadContext.fromOrFail(thread, "use_extension()"); |
| context.setNonModuleCalled(); |
| |
| if (extensionName.equals(ModuleExtensionId.INNATE_EXTENSION_NAME)) { |
| throw Starlark.errorf( |
| "innate extensions cannot be directly used; try `use_repo_rule` instead"); |
| } |
| |
| String extensionBzlFile = normalizeLabelString(context.getModuleBuilder(), rawExtensionBzlFile); |
| ModuleExtensionUsageBuilder newUsageBuilder = |
| new ModuleExtensionUsageBuilder( |
| context, extensionBzlFile, extensionName, isolate, thread.getCallerLocation()); |
| |
| if (context.shouldIgnoreDevDeps() && devDependency) { |
| // This is a no-op proxy. |
| return ModuleExtensionProxy.createFromUsagesBuilder(newUsageBuilder, devDependency); |
| } |
| |
| // Find an existing usage builder corresponding to this extension. Isolated usages need to get |
| // their own proxy. |
| if (!isolate) { |
| for (ModuleExtensionUsageBuilder usageBuilder : context.getExtensionUsageBuilders()) { |
| if (usageBuilder.isForExtension(extensionBzlFile, extensionName)) { |
| return ModuleExtensionProxy.createFromUsagesBuilder(usageBuilder, devDependency); |
| } |
| } |
| } |
| |
| // If no such proxy exists, we can just use a new one. |
| context.getExtensionUsageBuilders().add(newUsageBuilder); |
| return ModuleExtensionProxy.createFromUsagesBuilder(newUsageBuilder, devDependency); |
| } |
| |
| private String normalizeLabelString(InterimModule.Builder module, String rawExtensionBzlFile) { |
| // Normalize the label by parsing and stringifying it with a repo mapping that preserves the |
| // apparent repository name, except that a reference to the main repository via the empty |
| // repo name is translated to using the module repo name. This is necessary as |
| // ModuleExtensionUsages are grouped by the string value of this label, but later mapped to |
| // their Label representation. If multiple strings map to the same Label, this would result in a |
| // crash. |
| // ownName can't change anymore as calling module() after this results in an error. |
| String ownName = module.getRepoName().orElse(module.getName()); |
| RepositoryName ownRepoName = RepositoryName.createUnvalidated(ownName); |
| try { |
| ImmutableMap<String, RepositoryName> repoMapping = ImmutableMap.of(); |
| if (module.getKey().equals(ModuleKey.ROOT)) { |
| repoMapping = ImmutableMap.of("", ownRepoName); |
| } |
| Label label = |
| Label.parseWithPackageContext( |
| rawExtensionBzlFile, |
| Label.PackageContext.of( |
| PackageIdentifier.create(ownRepoName, PathFragment.EMPTY_FRAGMENT), |
| RepositoryMapping.createAllowingFallback(repoMapping))); |
| // Skip over the leading "@" of the unambiguous form. |
| return label.getUnambiguousCanonicalForm().substring(1); |
| } catch (LabelSyntaxException ignored) { |
| // Preserve backwards compatibility by not failing eagerly, rather keep the invalid label and |
| // let the extension fail when evaluated. |
| return rawExtensionBzlFile; |
| } |
| } |
| |
| /** |
| * Returns a {@link Label} when the given string is a valid label, otherwise the string itself. |
| */ |
| private Object parseOverrideLabelAttribute(InterimModule.Builder module, String rawLabel) { |
| RepositoryMapping repoMapping = |
| RepositoryMapping.create( |
| ImmutableMap.<String, RepositoryName>builder() |
| .put("", RepositoryName.MAIN) |
| .put(module.getRepoName().orElse(module.getName()), RepositoryName.MAIN) |
| .buildKeepingLast(), |
| RepositoryName.MAIN); |
| try { |
| return Label.parseWithPackageContext( |
| rawLabel, Label.PackageContext.of(PackageIdentifier.EMPTY_PACKAGE_ID, repoMapping)); |
| } catch (LabelSyntaxException e) { |
| // Preserve backwards compatibility by not failing eagerly, rather keep the invalid label and |
| // let the module repo fail when fetched. |
| return rawLabel; |
| } |
| } |
| |
| @StarlarkBuiltin(name = "module_extension_proxy", documented = false) |
| static class ModuleExtensionProxy implements Structure, StarlarkExportable { |
| private final ModuleExtensionUsageBuilder usageBuilder; |
| private final boolean devDependency; |
| |
| /** |
| * Creates a proxy with the specified dev_dependency bit that shares accumulated imports and |
| * tags with all other such proxies, thus preserving their order across dev/non-dev deps. |
| */ |
| static ModuleExtensionProxy createFromUsagesBuilder( |
| ModuleExtensionUsageBuilder usageBuilder, boolean devDependency) { |
| if (devDependency) { |
| usageBuilder.setHasDevUseExtension(); |
| } else { |
| usageBuilder.setHasNonDevUseExtension(); |
| } |
| return new ModuleExtensionProxy(usageBuilder, devDependency); |
| } |
| |
| private ModuleExtensionProxy(ModuleExtensionUsageBuilder usageBuilder, boolean devDependency) { |
| this.usageBuilder = usageBuilder; |
| this.devDependency = devDependency; |
| } |
| |
| void addImport(String localRepoName, String exportedName, String byWhat, Location location) |
| throws EvalException { |
| usageBuilder.addImport(localRepoName, exportedName, devDependency, byWhat, location); |
| } |
| |
| class TagCallable implements StarlarkValue { |
| final String tagName; |
| |
| TagCallable(String tagName) { |
| this.tagName = tagName; |
| } |
| |
| @StarlarkMethod( |
| name = "call", |
| selfCall = true, |
| documented = false, |
| extraKeywords = @Param(name = "kwargs"), |
| useStarlarkThread = true) |
| public void call(Dict<String, Object> kwargs, StarlarkThread thread) { |
| usageBuilder.addTag( |
| Tag.builder() |
| .setTagName(tagName) |
| .setAttributeValues(AttributeValues.create(kwargs)) |
| .setDevDependency(devDependency) |
| .setLocation(thread.getCallerLocation()) |
| .build()); |
| } |
| } |
| |
| @Override |
| public TagCallable getValue(String tagName) throws EvalException { |
| return new TagCallable(tagName); |
| } |
| |
| @Override |
| public ImmutableCollection<String> getFieldNames() { |
| return ImmutableList.of(); |
| } |
| |
| @Nullable |
| @Override |
| public String getErrorMessageForUnknownField(String field) { |
| return null; |
| } |
| |
| @Override |
| public boolean isExported() { |
| return usageBuilder.isExported(); |
| } |
| |
| @Override |
| public void export(EventHandler handler, Label bzlFileLabel, String name) { |
| usageBuilder.setExportedName(name); |
| } |
| } |
| |
| @StarlarkMethod( |
| name = "use_repo", |
| doc = |
| "Imports one or more repos generated by the given module extension into the scope of the" |
| + " current module.", |
| parameters = { |
| @Param( |
| name = "extension_proxy", |
| doc = "A module extension proxy object returned by a <code>use_extension</code> call."), |
| }, |
| extraPositionals = @Param(name = "args", doc = "The names of the repos to import."), |
| extraKeywords = |
| @Param( |
| name = "kwargs", |
| doc = |
| "Specifies certain repos to import into the scope of the current module with" |
| + " different names. The keys should be the name to use in the current scope," |
| + " whereas the values should be the original names exported by the module" |
| + " extension."), |
| useStarlarkThread = true) |
| public void useRepo( |
| ModuleExtensionProxy extensionProxy, |
| Tuple args, |
| Dict<String, Object> kwargs, |
| StarlarkThread thread) |
| throws EvalException { |
| ModuleThreadContext context = ModuleThreadContext.fromOrFail(thread, "use_repo()"); |
| context.setNonModuleCalled(); |
| Location location = thread.getCallerLocation(); |
| for (String arg : Sequence.cast(args, String.class, "args")) { |
| extensionProxy.addImport(arg, arg, "by a use_repo() call", location); |
| } |
| for (Map.Entry<String, String> entry : |
| Dict.cast(kwargs, String.class, String.class, "kwargs").entrySet()) { |
| extensionProxy.addImport(entry.getKey(), entry.getValue(), "by a use_repo() call", location); |
| } |
| } |
| |
| @StarlarkMethod( |
| name = "use_repo_rule", |
| doc = |
| "Returns a proxy value that can be directly invoked in the MODULE.bazel file as a" |
| + " repository rule, one or more times. Repos created in such a way are only visible" |
| + " to the current module, under the name declared using the <code>name</code>" |
| + " attribute on the proxy. The implicit Boolean <code>dev_dependency</code>" |
| + " attribute can also be used on the proxy to denote that a certain repo is only to" |
| + " be created when the current module is the root module.", |
| parameters = { |
| @Param( |
| name = "repo_rule_bzl_file", |
| doc = "A label to the Starlark file defining the repo rule."), |
| @Param( |
| name = "repo_rule_name", |
| doc = |
| "The name of the repo rule to use. A symbol with this name must be exported by the" |
| + " Starlark file."), |
| }, |
| useStarlarkThread = true) |
| public RepoRuleProxy useRepoRule(String bzlFile, String ruleName, StarlarkThread thread) |
| throws EvalException { |
| ModuleThreadContext context = ModuleThreadContext.fromOrFail(thread, "use_repo_rule()"); |
| context.setNonModuleCalled(); |
| // The builder for the singular "innate" extension of this module. Note that there's only one |
| // such usage (and it's fabricated), so the usage location just points to this file. |
| ModuleExtensionUsageBuilder newUsageBuilder = |
| new ModuleExtensionUsageBuilder( |
| context, |
| "//:MODULE.bazel", |
| ModuleExtensionId.INNATE_EXTENSION_NAME, |
| /* isolate= */ false, |
| Location.fromFile(thread.getCallerLocation().file())); |
| for (ModuleExtensionUsageBuilder usageBuilder : context.getExtensionUsageBuilders()) { |
| if (usageBuilder.isForExtension("//:MODULE.bazel", ModuleExtensionId.INNATE_EXTENSION_NAME)) { |
| return new RepoRuleProxy(usageBuilder, bzlFile + '%' + ruleName); |
| } |
| } |
| context.getExtensionUsageBuilders().add(newUsageBuilder); |
| return new RepoRuleProxy(newUsageBuilder, bzlFile + '%' + ruleName); |
| } |
| |
| @StarlarkBuiltin(name = "repo_rule_proxy", documented = false) |
| static class RepoRuleProxy implements StarlarkValue { |
| private final ModuleExtensionUsageBuilder usageBuilder; |
| private final String tagName; |
| |
| private RepoRuleProxy(ModuleExtensionUsageBuilder usageBuilder, String tagName) { |
| this.usageBuilder = usageBuilder; |
| this.tagName = tagName; |
| } |
| |
| @StarlarkMethod( |
| name = "call", |
| selfCall = true, |
| documented = false, |
| parameters = { |
| @Param(name = "name", positional = false, named = true), |
| @Param(name = "dev_dependency", positional = false, named = true, defaultValue = "False") |
| }, |
| extraKeywords = @Param(name = "kwargs"), |
| useStarlarkThread = true) |
| public void call( |
| String name, boolean devDependency, Dict<String, Object> kwargs, StarlarkThread thread) |
| throws EvalException { |
| RepositoryName.validateUserProvidedRepoName(name); |
| if (usageBuilder.getContext().shouldIgnoreDevDeps() && devDependency) { |
| return; |
| } |
| kwargs.putEntry("name", name); |
| ModuleExtensionProxy extensionProxy = |
| ModuleExtensionProxy.createFromUsagesBuilder(usageBuilder, devDependency); |
| extensionProxy.getValue(tagName).call(kwargs, thread); |
| extensionProxy.addImport(name, name, "by a repo rule", thread.getCallerLocation()); |
| } |
| } |
| |
| @StarlarkMethod( |
| name = CompiledModuleFile.INCLUDE_IDENTIFIER, |
| doc = |
| "Includes the contents of another MODULE.bazel-like file. Effectively," |
| + " <code>include()</code> behaves as if the included file is textually placed at the" |
| + " location of the <code>include()</code> call, except that variable bindings (such" |
| + " as those used for <code>use_extension</code>) are only ever visible in the file" |
| + " they occur in, not in any included or including files.<p>Only the root module may" |
| + " use <code>include()</code>; it is an error if a <code>bazel_dep</code>'s MODULE" |
| + " file uses <code>include()</code>.<p>Only files in the main repo may be" |
| + " included.<p><code>include()</code> allows you to segment the root module file" |
| + " into multiple parts, to avoid having an enormous MODULE.bazel file or to better" |
| + " manage access control for individual semantic segments.", |
| parameters = { |
| @Param( |
| name = "label", |
| doc = |
| "The label pointing to the file to include. The label must point to a file in the" |
| + " main repo; in other words, it <strong>must<strong> start with double" |
| + " slashes (<code>//</code>)."), |
| }, |
| useStarlarkThread = true) |
| public void include(String label, StarlarkThread thread) |
| throws InterruptedException, EvalException { |
| ModuleThreadContext context = |
| ModuleThreadContext.fromOrFail(thread, CompiledModuleFile.INCLUDE_IDENTIFIER + "()"); |
| context.setNonModuleCalled(); |
| context.include(label, thread); |
| } |
| |
| @StarlarkMethod( |
| name = "single_version_override", |
| doc = |
| "Specifies that a dependency should still come from a registry, but its version should" |
| + " be pinned, or its registry overridden, or a list of patches applied. This" |
| + " directive only takes effect in the root module; in other words, if a module" |
| + " is used as a dependency by others, its own overrides are ignored.", |
| parameters = { |
| @Param( |
| name = "module_name", |
| doc = "The name of the Bazel module dependency to apply this override to.", |
| named = true, |
| positional = false), |
| @Param( |
| name = "version", |
| doc = |
| "Overrides the declared version of this module in the dependency graph. In other" |
| + " words, this module will be \"pinned\" to this override version. This" |
| + " attribute can be omitted if all one wants to override is the registry or" |
| + " the patches. ", |
| named = true, |
| positional = false, |
| defaultValue = "''"), |
| @Param( |
| name = "registry", |
| doc = |
| "Overrides the registry for this module; instead of finding this module from the" |
| + " default list of registries, the given registry should be used.", |
| named = true, |
| positional = false, |
| defaultValue = "''"), |
| @Param( |
| name = "patches", |
| doc = |
| "A list of labels pointing to patch files to apply for this module. The patch files" |
| + " must exist in the source tree of the top level project. They are applied in" |
| + " the list order.", |
| allowedTypes = {@ParamType(type = Iterable.class, generic1 = String.class)}, |
| named = true, |
| positional = false, |
| defaultValue = "[]"), |
| @Param( |
| name = "patch_cmds", |
| doc = |
| "Sequence of Bash commands to be applied on Linux/Macos after patches are applied.", |
| allowedTypes = {@ParamType(type = Iterable.class, generic1 = String.class)}, |
| named = true, |
| positional = false, |
| defaultValue = "[]"), |
| @Param( |
| name = "patch_strip", |
| doc = "Same as the --strip argument of Unix patch.", |
| named = true, |
| positional = false, |
| defaultValue = "0"), |
| }, |
| useStarlarkThread = true) |
| public void singleVersionOverride( |
| String moduleName, |
| String version, |
| String registry, |
| Iterable<?> patches, |
| Iterable<?> patchCmds, |
| StarlarkInt patchStrip, |
| StarlarkThread thread) |
| throws EvalException { |
| ModuleThreadContext context = |
| ModuleThreadContext.fromOrFail(thread, "single_version_override()"); |
| context.setNonModuleCalled(); |
| validateModuleName(moduleName); |
| Version parsedVersion; |
| try { |
| parsedVersion = Version.parse(version); |
| } catch (ParseException e) { |
| throw new EvalException("Invalid version in single_version_override()", e); |
| } |
| context.addOverride( |
| moduleName, |
| SingleVersionOverride.create( |
| parsedVersion, |
| registry, |
| Sequence.cast(patches, String.class, "patches").stream() |
| .map(l -> parseOverrideLabelAttribute(context.getModuleBuilder(), l)) |
| .collect(toImmutableList()), |
| Sequence.cast(patchCmds, String.class, "patchCmds").getImmutableList(), |
| patchStrip.toInt("single_version_override.patch_strip"))); |
| } |
| |
| @StarlarkMethod( |
| name = "multiple_version_override", |
| doc = |
| "Specifies that a dependency should still come from a registry, but multiple versions of" |
| + " it should be allowed to coexist. See <a" |
| + " href=\"/external/module#multiple-version_override\">the documentation</a> for" |
| + " more details. This" |
| + " directive only takes effect in the root module; in other words, if a module" |
| + " is used as a dependency by others, its own overrides are ignored.", |
| parameters = { |
| @Param( |
| name = "module_name", |
| doc = "The name of the Bazel module dependency to apply this override to.", |
| named = true, |
| positional = false), |
| @Param( |
| name = "versions", |
| doc = |
| "Explicitly specifies the versions allowed to coexist. These versions must already" |
| + " be present in the dependency graph pre-selection. Dependencies on this" |
| + " module will be \"upgraded\" to the nearest higher allowed version at the" |
| + " same compatibility level, whereas dependencies that have a higher version" |
| + " than any allowed versions at the same compatibility level will cause an" |
| + " error.", |
| allowedTypes = {@ParamType(type = Iterable.class, generic1 = String.class)}, |
| named = true, |
| positional = false), |
| @Param( |
| name = "registry", |
| doc = |
| "Overrides the registry for this module; instead of finding this module from the" |
| + " default list of registries, the given registry should be used.", |
| named = true, |
| positional = false, |
| defaultValue = "''"), |
| }, |
| useStarlarkThread = true) |
| public void multipleVersionOverride( |
| String moduleName, Iterable<?> versions, String registry, StarlarkThread thread) |
| throws EvalException { |
| ModuleThreadContext context = |
| ModuleThreadContext.fromOrFail(thread, "multiple_version_override()"); |
| context.setNonModuleCalled(); |
| validateModuleName(moduleName); |
| ImmutableList.Builder<Version> parsedVersionsBuilder = new ImmutableList.Builder<>(); |
| try { |
| for (String version : Sequence.cast(versions, String.class, "versions").getImmutableList()) { |
| parsedVersionsBuilder.add(Version.parse(version)); |
| } |
| } catch (ParseException e) { |
| throw new EvalException("Invalid version in multiple_version_override()", e); |
| } |
| ImmutableList<Version> parsedVersions = parsedVersionsBuilder.build(); |
| if (parsedVersions.size() < 2) { |
| throw new EvalException("multiple_version_override() must specify at least 2 versions"); |
| } |
| context.addOverride(moduleName, MultipleVersionOverride.create(parsedVersions, registry)); |
| } |
| |
| @StarlarkMethod( |
| name = "archive_override", |
| doc = |
| "Specifies that this dependency should come from an archive file (zip, gzip, etc) at a" |
| + " certain location, instead of from a registry. This" |
| + " directive only takes effect in the root module; in other words, if a module" |
| + " is used as a dependency by others, its own overrides are ignored.", |
| parameters = { |
| @Param( |
| name = "module_name", |
| doc = "The name of the Bazel module dependency to apply this override to.", |
| named = true, |
| positional = false), |
| @Param( |
| name = "urls", |
| allowedTypes = { |
| @ParamType(type = String.class), |
| @ParamType(type = Iterable.class, generic1 = String.class), |
| }, |
| doc = "The URLs of the archive; can be http(s):// or file:// URLs.", |
| named = true, |
| positional = false), |
| @Param( |
| name = "integrity", |
| doc = "The expected checksum of the archive file, in Subresource Integrity format.", |
| named = true, |
| positional = false, |
| defaultValue = "''"), |
| @Param( |
| name = "strip_prefix", |
| doc = "A directory prefix to strip from the extracted files.", |
| named = true, |
| positional = false, |
| defaultValue = "''"), |
| @Param( |
| name = "patches", |
| doc = |
| "A list of labels pointing to patch files to apply for this module. The patch files" |
| + " must exist in the source tree of the top level project. They are applied in" |
| + " the list order.", |
| allowedTypes = {@ParamType(type = Iterable.class, generic1 = String.class)}, |
| named = true, |
| positional = false, |
| defaultValue = "[]"), |
| @Param( |
| name = "patch_cmds", |
| doc = |
| "Sequence of Bash commands to be applied on Linux/Macos after patches are applied.", |
| allowedTypes = {@ParamType(type = Iterable.class, generic1 = String.class)}, |
| named = true, |
| positional = false, |
| defaultValue = "[]"), |
| @Param( |
| name = "patch_strip", |
| doc = "Same as the --strip argument of Unix patch.", |
| named = true, |
| positional = false, |
| defaultValue = "0"), |
| }, |
| useStarlarkThread = true) |
| public void archiveOverride( |
| String moduleName, |
| Object urls, |
| String integrity, |
| String stripPrefix, |
| Iterable<?> patches, |
| Iterable<?> patchCmds, |
| StarlarkInt patchStrip, |
| StarlarkThread thread) |
| throws EvalException { |
| ModuleThreadContext context = ModuleThreadContext.fromOrFail(thread, "archive_override()"); |
| context.setNonModuleCalled(); |
| validateModuleName(moduleName); |
| ImmutableList<String> urlList = |
| urls instanceof String |
| ? ImmutableList.of((String) urls) |
| : Sequence.cast(urls, String.class, "urls").getImmutableList(); |
| context.addOverride( |
| moduleName, |
| ArchiveOverride.create( |
| urlList, |
| Sequence.cast(patches, String.class, "patches").stream() |
| .map(l -> parseOverrideLabelAttribute(context.getModuleBuilder(), l)) |
| .collect(toImmutableList()), |
| Sequence.cast(patchCmds, String.class, "patchCmds").getImmutableList(), |
| integrity, |
| stripPrefix, |
| patchStrip.toInt("archive_override.patch_strip"))); |
| } |
| |
| @StarlarkMethod( |
| name = "git_override", |
| doc = |
| "Specifies that a dependency should come from a certain commit of a Git repository. This" |
| + " directive only takes effect in the root module; in other words, if a module" |
| + " is used as a dependency by others, its own overrides are ignored.", |
| parameters = { |
| @Param( |
| name = "module_name", |
| doc = "The name of the Bazel module dependency to apply this override to.", |
| named = true, |
| positional = false), |
| @Param( |
| name = "remote", |
| doc = "The URL of the remote Git repository.", |
| named = true, |
| positional = false), |
| @Param( |
| name = "commit", |
| doc = "The commit that should be checked out.", |
| named = true, |
| positional = false, |
| defaultValue = "''"), |
| @Param( |
| name = "patches", |
| doc = |
| "A list of labels pointing to patch files to apply for this module. The patch files" |
| + " must exist in the source tree of the top level project. They are applied in" |
| + " the list order.", |
| allowedTypes = {@ParamType(type = Iterable.class, generic1 = String.class)}, |
| named = true, |
| positional = false, |
| defaultValue = "[]"), |
| @Param( |
| name = "patch_cmds", |
| doc = |
| "Sequence of Bash commands to be applied on Linux/Macos after patches are applied.", |
| allowedTypes = {@ParamType(type = Iterable.class, generic1 = String.class)}, |
| named = true, |
| positional = false, |
| defaultValue = "[]"), |
| @Param( |
| name = "patch_strip", |
| doc = "Same as the --strip argument of Unix patch.", |
| named = true, |
| positional = false, |
| defaultValue = "0"), |
| @Param( |
| name = "init_submodules", |
| doc = "Whether submodules in the fetched repo should be recursively initialized.", |
| named = true, |
| positional = false, |
| defaultValue = "False"), |
| }, |
| useStarlarkThread = true) |
| public void gitOverride( |
| String moduleName, |
| String remote, |
| String commit, |
| Iterable<?> patches, |
| Iterable<?> patchCmds, |
| StarlarkInt patchStrip, |
| boolean initSubmodules, |
| StarlarkThread thread) |
| throws EvalException { |
| ModuleThreadContext context = ModuleThreadContext.fromOrFail(thread, "git_override()"); |
| context.setNonModuleCalled(); |
| validateModuleName(moduleName); |
| context.addOverride( |
| moduleName, |
| GitOverride.create( |
| remote, |
| commit, |
| Sequence.cast(patches, String.class, "patches").stream() |
| .map(l -> parseOverrideLabelAttribute(context.getModuleBuilder(), l)) |
| .collect(toImmutableList()), |
| Sequence.cast(patchCmds, String.class, "patchCmds").getImmutableList(), |
| patchStrip.toInt("git_override.patch_strip"), |
| initSubmodules)); |
| } |
| |
| @StarlarkMethod( |
| name = "local_path_override", |
| doc = |
| "Specifies that a dependency should come from a certain directory on local disk. This" |
| + " directive only takes effect in the root module; in other words, if a module" |
| + " is used as a dependency by others, its own overrides are ignored.", |
| parameters = { |
| @Param( |
| name = "module_name", |
| doc = "The name of the Bazel module dependency to apply this override to.", |
| named = true, |
| positional = false), |
| @Param( |
| name = "path", |
| doc = "The path to the directory where this module is.", |
| named = true, |
| positional = false), |
| }, |
| useStarlarkThread = true) |
| public void localPathOverride(String moduleName, String path, StarlarkThread thread) |
| throws EvalException { |
| ModuleThreadContext context = ModuleThreadContext.fromOrFail(thread, "local_path_override()"); |
| context.setNonModuleCalled(); |
| validateModuleName(moduleName); |
| context.addOverride(moduleName, LocalPathOverride.create(path)); |
| } |
| } |