"""Utility functions for bzlmod extensions""" def _toolchain_repos_bfs(mctx, get_tag_fn, toolchain_name, toolchain_repos_fn, default_repository = None, get_name_fn = None, get_version_fn = None): """Create toolchain repositories from bzlmod extensions using a breadth-first resolution strategy. Toolchains are assumed to have a "default" or canonical repository name so that across all invocations of the module extension with that name only a single toolchain repository is created. As such, it is recommended to default the toolchain name in the extension's tag class attributes so that diverging from the canonical name is a special case. The resolved toolchain version will be the one invoked closest to the root module, following Bazel's breadth-first ordering of modules in the dependency graph. For example, given the module extension usage in a MODULE file: ```starlark ext = use_extension("@my_lib//lib:extensions.bzl", "ext") ext.foo_toolchain(version = "1.2.3") # Default `name = "foo"` use_repo(ext, "foo") register_toolchains( "@foo//:all", ) ``` This macro would be used in the module extension implementation as follows: ```starlark extension_utils.toolchain_repos( mctx = mctx, get_tag_fn = lambda tags: tags.foo_toolchain, toolchain_name = "foo", toolchain_repos_fn = lambda name, version: register_foo_toolchains(name = name, register = False), get_version_fn = lambda attr: None, ) ``` Where `register_foo_toolchains` is a typical WORKSPACE macro used to register the foo toolchain for a particular version, minus the actual registration step which is done separately in the MODULE file. This macro enforces that only root MODULEs may use a different name for the toolchain in case several versions of the toolchain repository is desired. Args: mctx: The module context get_tag_fn: A function that takes in `module.tags` and returns the tag used for the toolchain. For example, `tag: lambda tags: tags.foo_toolchain`. This is required because `foo_toolchain` cannot be accessed as a simple string key from `module.tags`. toolchain_name: Name of the toolchain to use in error messages toolchain_repos_fn: A function that takes (name, version) and creates a toolchain repository. This lambda should call a typical reposotiory rule to create toolchains. default_repository: Default name of the toolchain repository to pass to the repos_fn. By default, it equals `toolchain_name`. get_name_fn: A function that extracts the module name from the toolchain tag's attributes. Defaults to grabbing the `name` attribute. get_version_fn: A function that extracts the module version from the a tag's attributes. Defaults to grabbing the `version` attribute. Override this to a lambda that returns `None` if version isn't used as an attribute. """ if default_repository == None: default_repository = toolchain_name if get_name_fn == None: get_name_fn = lambda attr: attr.name if get_version_fn == None: get_version_fn = lambda attr: attr.version registrations = {} for mod in mctx.modules: for attr in get_tag_fn(mod.tags): name = get_name_fn(attr) version = get_version_fn(attr) if name != default_repository and not mod.is_root: fail("Only the root module may provide a name for the {} toolchain.".format(toolchain_name)) if name in registrations.keys(): if name == default_repository: # Prioritize the root-most registration of the default toolchain version and # ignore any further registrations (modules are processed breadth-first) continue if version == registrations[name]: # No problem to register a matching toolchain twice continue fail("Multiple conflicting {} toolchains declared for name {} ({} and {})".format( toolchain_name, name, version, registrations[name], )) else: registrations[name] = version for name, version in registrations.items(): toolchain_repos_fn( name = name, version = version, ) extension_utils = struct( toolchain_repos_bfs = _toolchain_repos_bfs, )