diff --git a/docs.json b/docs.json index b79de5b..84b2dbb 100644 --- a/docs.json +++ b/docs.json @@ -229,6 +229,15 @@ "rules/lib/overview", "rules/lib/globals" ] + }, + { + "group": "External dependencies", + "pages": [ + "external/extension", + "external/module", + "external/lockfile", + "external/repo" + ] } ] }, @@ -254,6 +263,7 @@ { "group": "Programs", "pages": [ + "community/update", "community/sig", "community/experts", "community/partners", @@ -499,6 +509,14 @@ "8.4.2/rules/lib/overview", "8.4.2/rules/lib/globals" ] + }, + { + "group": "External dependencies", + "pages": [ + "8.4.2/external/advanced", + "8.4.2/external/module", + "8.4.2/external/lockfile" + ] } ] }, @@ -524,6 +542,7 @@ { "group": "Programs", "pages": [ + "8.4.2/community/update", "8.4.2/community/sig", "8.4.2/community/experts", "8.4.2/community/partners", @@ -769,6 +788,14 @@ "8.3.1/rules/lib/overview", "8.3.1/rules/lib/globals" ] + }, + { + "group": "External dependencies", + "pages": [ + "8.3.1/external/advanced", + "8.3.1/external/module", + "8.3.1/external/lockfile" + ] } ] }, @@ -794,6 +821,7 @@ { "group": "Programs", "pages": [ + "8.3.1/community/update", "8.3.1/community/sig", "8.3.1/community/experts", "8.3.1/community/partners", @@ -1039,6 +1067,14 @@ "8.2.1/rules/lib/overview", "8.2.1/rules/lib/globals" ] + }, + { + "group": "External dependencies", + "pages": [ + "8.2.1/external/advanced", + "8.2.1/external/module", + "8.2.1/external/lockfile" + ] } ] }, @@ -1064,6 +1100,7 @@ { "group": "Programs", "pages": [ + "8.2.1/community/update", "8.2.1/community/sig", "8.2.1/community/experts", "8.2.1/community/partners", @@ -1309,6 +1346,14 @@ "8.1.1/rules/lib/overview", "8.1.1/rules/lib/globals" ] + }, + { + "group": "External dependencies", + "pages": [ + "8.1.1/external/advanced", + "8.1.1/external/module", + "8.1.1/external/lockfile" + ] } ] }, @@ -1334,6 +1379,7 @@ { "group": "Programs", "pages": [ + "8.1.1/community/update", "8.1.1/community/sig", "8.1.1/community/experts", "8.1.1/community/partners", @@ -1579,6 +1625,14 @@ "8.0.1/rules/lib/overview", "8.0.1/rules/lib/globals" ] + }, + { + "group": "External dependencies", + "pages": [ + "8.0.1/external/advanced", + "8.0.1/external/module", + "8.0.1/external/lockfile" + ] } ] }, @@ -1604,6 +1658,7 @@ { "group": "Programs", "pages": [ + "8.0.1/community/update", "8.0.1/community/sig", "8.0.1/community/experts", "8.0.1/community/partners", @@ -1849,6 +1904,14 @@ "7.6.2/rules/lib/overview", "7.6.2/rules/lib/globals" ] + }, + { + "group": "External dependencies", + "pages": [ + "7.6.1/external/advanced", + "7.6.1/external/module", + "7.6.1/external/lockfile" + ] } ] }, @@ -1874,6 +1937,7 @@ { "group": "Programs", "pages": [ + "7.6.2/community/update", "7.6.2/community/sig", "7.6.2/community/experts", "7.6.2/community/partners", @@ -2119,6 +2183,14 @@ "6.5.0/rules/lib/overview", "6.5.0/rules/lib/globals" ] + }, + { + "group": "External dependencies", + "pages": [ + "6.5.0/external/advanced", + "6.5.0/external/module", + "6.5.0/external/lockfile" + ] } ] }, @@ -2144,6 +2216,7 @@ { "group": "Programs", "pages": [ + "6.5.0/community/update", "6.5.0/community/sig", "6.5.0/community/experts", "6.5.0/community/partners", @@ -2414,6 +2487,7 @@ { "group": "Programs", "pages": [ + "5.4.1/community/update", "5.4.1/community/sig", "5.4.1/community/experts", "5.4.1/community/partners", diff --git a/external/extension.mdx b/external/extension.mdx new file mode 100644 index 0000000..b320e82 --- /dev/null +++ b/external/extension.mdx @@ -0,0 +1,310 @@ +--- +title: 'Module extensions' +--- + +Module extensions allow users to extend the module system by reading input data +from modules across the dependency graph, performing necessary logic to resolve +dependencies, and finally creating repos by calling [repo +rules](/external/repo). These extensions have capabilities similar to repo +rules, which enables them to perform file I/O, send network requests, and so on. +Among other things, they allow Bazel to interact with other package management +systems while also respecting the dependency graph built out of Bazel modules. + +You can define module extensions in `.bzl` files, just like repo rules. They're +not invoked directly; rather, each module specifies pieces of data called *tags* +for extensions to read. Bazel runs module resolution before evaluating any +extensions. The extension reads all the tags belonging to it across the entire +dependency graph. + +## Extension usage + +Extensions are hosted in Bazel modules themselves. To use an extension in a +module, first add a `bazel_dep` on the module hosting the extension, and then +call the [`use_extension`](/rules/lib/globals/module#use_extension) built-in function +to bring it into scope. Consider the following example — a snippet from a +`MODULE.bazel` file to use the "maven" extension defined in the +[`rules_jvm_external`](https://github.com/bazelbuild/rules_jvm_external) +module: + +```python +bazel_dep(name = "rules_jvm_external", version = "4.5") +maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") +``` + +This binds the return value of `use_extension` to a variable, which allows the +user to use dot-syntax to specify tags for the extension. The tags must follow +the schema defined by the corresponding *tag classes* specified in the +[extension definition](#extension_definition). For an example specifying some +`maven.install` and `maven.artifact` tags: + +```python +maven.install(artifacts = ["org.junit:junit:4.13.2"]) +maven.artifact(group = "com.google.guava", + artifact = "guava", + version = "27.0-jre", + exclusions = ["com.google.j2objc:j2objc-annotations"]) +``` + +Use the [`use_repo`](/rules/lib/globals/module#use_repo) directive to bring repos +generated by the extension into the scope of the current module. + +```python +use_repo(maven, "maven") +``` + +Repos generated by an extension are part of its API. In this example, the +"maven" module extension promises to generate a repo called `maven`. With the +declaration above, the extension properly resolves labels such as +`@maven//:org_junit_junit` to point to the repo generated by the "maven" +extension. + +Note: Module extensions are evaluated lazily. This means that an extension will +typically not be evaluated unless some module brings one of its repositories +into scope using `use_repo` and that repository is referenced in a build. While +testing a module extension, `bazel mod deps` can be useful as it +unconditionally evaluates all module extensions. + +## Extension definition + +You can define module extensions similarly to [repo rules](/external/repo), +using the [`module_extension`](/rules/lib/globals/bzl#module_extension) +function. However, while repo rules have a number of attributes, module +extensions have [`tag_class`es](/rules/lib/globals/bzl#tag_class), each of which +has a number of attributes. The tag classes define schemas for tags used by this +extension. For example, the "maven" extension above might be defined like this: + +```python +# @rules_jvm_external//:extensions.bzl + +_install = tag_class(attrs = {"artifacts": attr.string_list(), ...}) +_artifact = tag_class(attrs = {"group": attr.string(), "artifact": attr.string(), ...}) +maven = module_extension( + implementation = _maven_impl, + tag_classes = {"install": _install, "artifact": _artifact}, +) +``` + +These declarations show that `maven.install` and `maven.artifact` tags can be +specified using the specified attribute schema. + +The implementation function of module extensions are similar to those of repo +rules, except that they get a [`module_ctx`](/rules/lib/builtins/module_ctx) object, +which grants access to all modules using the extension and all pertinent tags. +The implementation function then calls repo rules to generate repos. + +```python +# @rules_jvm_external//:extensions.bzl + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") # a repo rule +def _maven_impl(ctx): + # This is a fake implementation for demonstration purposes only + + # collect artifacts from across the dependency graph + artifacts = [] + for mod in ctx.modules: + for install in mod.tags.install: + artifacts += install.artifacts + artifacts += [_to_artifact(artifact) for artifact in mod.tags.artifact] + + # call out to the coursier CLI tool to resolve dependencies + output = ctx.execute(["coursier", "resolve", artifacts]) + repo_attrs = _process_coursier_output(output) + + # call repo rules to generate repos + for attrs in repo_attrs: + http_file(**attrs) + _generate_hub_repo(name = "maven", repo_attrs) +``` + +### Extension identity + +Module extensions are identified by the name and the `.bzl` file that appears +in the call to `use_extension`. In the following example, the extension `maven` +is identified by the `.bzl` file `@rules_jvm_external//:extension.bzl` and the +name `maven`: + +```python +maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") +``` + +Re-exporting an extension from a different `.bzl` file gives it a new identity +and if both versions of the extension are used in the transitive module graph, +then they will be evaluated separately and will only see the tags associated +with that particular identity. + +As an extension author you should make sure that users will only use your +module extension from one single `.bzl` file. + +## Repository names and visibility + +Repos generated by extensions have canonical names in the form of `{{ "" +}}module_repo_canonical_name{{ "" }}+{{ "" }}extension_name{{ +"" }}+`repo_name``. Note that the canonical name +format is not an API you should depend on — it's subject to change at any time. + +This naming policy means that each extension has its own "repo namespace"; two +distinct extensions can each define a repo with the same name without risking +any clashes. It also means that `repository_ctx.name` reports the canonical name +of the repo, which is *not* the same as the name specified in the repo rule +call. + +Taking repos generated by module extensions into consideration, there are +several repo visibility rules: + +* A Bazel module repo can see all repos introduced in its `MODULE.bazel` file + via [`bazel_dep`](/rules/lib/globals/module#bazel_dep) and + [`use_repo`](/rules/lib/globals/module#use_repo). +* A repo generated by a module extension can see all repos visible to the + module that hosts the extension, *plus* all other repos generated by the + same module extension (using the names specified in the repo rule calls as + their apparent names). + * This might result in a conflict. If the module repo can see a repo with + the apparent name `foo`, and the extension generates a repo with the + specified name `foo`, then for all repos generated by that extension + `foo` refers to the former. +* Similarly, in a module extension's implementation function, repos created + by the extension can refer to each other by their apparent names in + attributes, regardless of the order in which they are created. + * In case of a conflict with a repository visible to the module, labels + passed to repository rule attributes can be wrapped in a call to + [`Label`](/rules/lib/toplevel/attr#label) to ensure that they refer to + the repo visible to the module instead of the extension-generated repo + of the same name. + +### Overriding and injecting module extension repos + +The root module can use +[`override_repo`](/rules/lib/globals/module#override_repo) and +[`inject_repo`](/rules/lib/globals/module#inject_repo) to override or inject +module extension repos. + +#### Example: Replacing `rules_java`'s `java_tools` with a vendored copy + +```python +# MODULE.bazel +local_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:local.bzl", "local_repository") +local_repository( + name = "my_java_tools", + path = "vendor/java_tools", +) + +bazel_dep(name = "rules_java", version = "7.11.1") +java_toolchains = use_extension("@rules_java//java:extension.bzl", "toolchains") + +override_repo(java_toolchains, remote_java_tools = "my_java_tools") +``` + +#### Example: Patch a Go dependency to depend on `@zlib` instead of the system zlib + +```python +# MODULE.bazel +bazel_dep(name = "gazelle", version = "0.38.0") +bazel_dep(name = "zlib", version = "1.3.1.bcr.3") + +go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") +go_deps.from_file(go_mod = "//:go.mod") +go_deps.module_override( + patches = [ + "//patches:my_module_zlib.patch", + ], + path = "example.com/my_module", +) +use_repo(go_deps, ...) + +inject_repo(go_deps, "zlib") +``` + +```diff +# patches/my_module_zlib.patch +--- a/BUILD.bazel ++++ b/BUILD.bazel +@@ -1,6 +1,6 @@ + go_binary( + name = "my_module", + importpath = "example.com/my_module", + srcs = ["my_module.go"], +- copts = ["-lz"], ++ cdeps = ["@zlib"], + ) +``` + +## Best practices + +This section describes best practices when writing extensions so they are +straightforward to use, maintainable, and adapt well to changes over time. + +### Put each extension in a separate file + +When extensions are in a different files, it allows one extension to load +repositories generated by another extension. Even if you don't use this +functionality, it's best to put them in separate files in case you need it +later. This is because the extension's identify is based on its file, so moving +the extension into another file later changes your public API and is a backwards +incompatible change for your users. + +### Specify reproducibility and use facts + +If your extension always defines the same repositories given the same inputs +(extension tags, files it reads, etc.) and in particular doesn't rely on +any [downloads](/rules/lib/builtins/module_ctx#download) that aren't guarded by +a checksum, consider returning +[`extension_metadata`](/rules/lib/builtins/module_ctx#extension_metadata) with +`reproducible = True`. This allows Bazel to skip this extension when writing to +the `MODULE.bazel` lockfile, which helps keep the lockfile small and reduces +the chance of merge conflicts. Note that Bazel still caches the results of +reproducible extensions in a way that persists across server restarts, so even +a long-running extension can be marked as reproducible without a performance +penalty. + +If your extension relies on effectively immutable data obtained from outside +the build, most commonly from the network, but you don't have a checksum +available to guard the download, consider using the `facts` parameter of +[`extension_metadata`](/rules/lib/builtins/module_ctx#extension_metadata) to +persistently record such data and thus allow your extension to become +reproducible. `facts` is expected to be a dictionary with string keys and +arbitrary JSON-like Starlark values that is always persisted in the lockfile and +available to future evaluations of the extension via the +[`facts`](/rules/lib/builtins/module_ctx#facts) field of `module_ctx`. + +`facts` are not invalidated even when the code of your module extension changes, +so be prepared to handle the case where the structure of `facts` changes. +Bazel also assumes that two different `facts` dicts produced by two different +evaluations of the same extension can be shallowly merged (i.e., as if by using +the `|` operator on two dicts). This is partially enforced by `module_ctx.facts` +not supporting enumeration of its entries, just lookups by key. + +An example of using `facts` would be to record a mapping from version numbers of +some SDK to the an object containing the download URL and checksum of that +version. The first time the extension is evaluated, it can fetch this mapping +from the network, but on later evaluations it can use the mapping from `facts` +to avoid the network requests. + +### Specify dependence on operating system and architecture + +If your extension relies on the operating system or its architecture type, +ensure to indicate this in the extension definition using the `os_dependent` +and `arch_dependent` boolean attributes. This ensures that Bazel recognizes the +need for re-evaluation if there are changes to either of them. + +Since this kind of dependence on the host makes it more difficult to maintain +the lockfile entry for this extension, consider +[marking the extension reproducible](#specify_reproducibility) if possible. + +### Only the root module should directly affect repository names + +Remember that when an extension creates repositories, they are created within +the namespace of the extension. This means collisions can occur if different +modules use the same extension and end up creating a repository with the same +name. This often manifests as a module extension's `tag_class` having a `name` +argument that is passed as a repository rule's `name` value. + +For example, say the root module, `A`, depends on module `B`. Both modules +depend on module `mylang`. If both `A` and `B` call +`mylang.toolchain(name="foo")`, they will both try to create a repository named +`foo` within the `mylang` module and an error will occur. + +To avoid this, either remove the ability to set the repository name directly, +or only allow the root module to do so. It's OK to allow the root module this +ability because nothing will depend on it, so it doesn't have to worry about +another module creating a conflicting name. +