Skip to content

Commit

Permalink
Add apparent repo name utilities to modules.bzl
Browse files Browse the repository at this point in the history
Adds the following macros to work with apparent repo names when running
under Bzlmod.

- `adjust_main_repo_prefix`
- `apparent_repo_label_string`
- `apparent_repo_name`

Originally developed while updating rules_scala to support Bzlmod as
part of bazelbuild/rules_scala#1482.

For examples of their use, see bazelbuild/rules_scala#1621.
  • Loading branch information
mbland committed Oct 14, 2024
1 parent 56b235e commit 1d341f6
Show file tree
Hide file tree
Showing 3 changed files with 296 additions and 2 deletions.
74 changes: 74 additions & 0 deletions docs/modules_doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,80 @@

Skylib module containing utilities for Bazel modules and module extensions.

<a id="modules.adjust_main_repo_prefix"></a>

## modules.adjust_main_repo_prefix

<pre>
modules.adjust_main_repo_prefix(<a href="#modules.adjust_main_repo_prefix-target_pattern">target_pattern</a>)
</pre>

Updates the main repo prefix to match the current Bazel version.

Used to automatically update strings representing include/exclude target
patterns so that they match actual main repo target Labels correctly. The
main repo prefix will be "@//" for Bazel < 7.1.0, and "@@//" for Bazel >=
7.1.0 under Bzlmod.


**PARAMETERS**


| Name | Description | Default Value |
| :------------- | :------------- | :------------- |
| <a id="modules.adjust_main_repo_prefix-target_pattern"></a>target_pattern | a string used to match a BUILD target pattern | none |

**RETURNS**

the string with any main repository prefix updated to match the current
Bazel version


<a id="modules.apparent_repo_label_string"></a>

## modules.apparent_repo_label_string

<pre>
modules.apparent_repo_label_string(<a href="#modules.apparent_repo_label_string-label">label</a>)
</pre>

Return a Label string starting with its apparent repo name.

**PARAMETERS**


| Name | Description | Default Value |
| :------------- | :------------- | :------------- |
| <a id="modules.apparent_repo_label_string-label"></a>label | a Label instance | none |

**RETURNS**

str(label) with its canonical repository name replaced with its apparent
repository name


<a id="modules.apparent_repo_name"></a>

## modules.apparent_repo_name

<pre>
modules.apparent_repo_name(<a href="#modules.apparent_repo_name-label_or_name">label_or_name</a>)
</pre>

Return a repository's apparent repository name.

**PARAMETERS**


| Name | Description | Default Value |
| :------------- | :------------- | :------------- |
| <a id="modules.apparent_repo_name-label_or_name"></a>label_or_name | a Label or repository name string | none |

**RETURNS**

The apparent repository name


<a id="modules.as_extension"></a>

## modules.as_extension
Expand Down
71 changes: 71 additions & 0 deletions lib/modules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,78 @@ def _use_all_repos(module_ctx, reproducible = False):
**extension_metadata_kwargs
)

def _apparent_repo_name(label_or_name):
"""Return a repository's apparent repository name.
Args:
label_or_name: a Label or repository name string
Returns:
The apparent repository name
"""
repo_name = getattr(label_or_name, "repo_name", label_or_name).lstrip("@")
delimiter_indices = []

# Bazed on this pattern from the Bazel source:
# com.google.devtools.build.lib.cmdline.RepositoryName.VALID_REPO_NAME
for i in range(len(repo_name)):
c = repo_name[i]
if not (c.isalnum() or c in "_-."):
delimiter_indices.append(i)

if len(delimiter_indices) == 0:
# Already an apparent repo name, apparently.
return repo_name

if len(delimiter_indices) == 1:
# The name is for a top level module, possibly containing a version ID.
return repo_name[:delimiter_indices[0]]

return repo_name[delimiter_indices[-1] + 1:]

def _apparent_repo_label_string(label):
"""Return a Label string starting with its apparent repo name.
Args:
label: a Label instance
Returns:
str(label) with its canonical repository name replaced with its apparent
repository name
"""
if len(label.repo_name) == 0:
return str(label)

label_str = "@" + str(label).lstrip("@")
return label_str.replace(label.repo_name, _apparent_repo_name(label))

_main_repo_prefix = str(Label("@@//:all")).split(":")[0]

def _adjust_main_repo_prefix(target_pattern):
"""Updates the main repository prefix to match the current Bazel version.
The main repo prefix will be "@//" for Bazel < 7.1.0, and "@@//" for Bazel
>= 7.1.0 under Bzlmod. This macro automatically updates strings representing
include/exclude target patterns so that they match actual main repository
target Labels correctly.
Args:
target_pattern: a string used to match a BUILD target pattern
Returns:
the string with any main repository prefix updated to match the current
Bazel version
"""
if target_pattern.startswith("@//") or target_pattern.startswith("@@//"):
return _main_repo_prefix + target_pattern.lstrip("@/")

return target_pattern

modules = struct(
as_extension = _as_extension,
use_all_repos = _use_all_repos,
apparent_repo_name = _apparent_repo_name,
apparent_repo_label_string = _apparent_repo_label_string,
main_repo_prefix = _main_repo_prefix,
adjust_main_repo_prefix = _adjust_main_repo_prefix
)
153 changes: 151 additions & 2 deletions tests/modules_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
"""Test usage of modules.bzl."""

load("//lib:modules.bzl", "modules")
load("//lib:unittest.bzl", "asserts", "unittest")
load("//rules:build_test.bzl", "build_test")

_is_bzlmod_enabled = str(Label("//tests:module_tests.bzl")).startswith("@@")

def _repo_rule_impl(repository_ctx):
repository_ctx.file("WORKSPACE")
repository_ctx.file("BUILD", """exports_files(["hello"])""")
Expand Down Expand Up @@ -45,12 +48,158 @@ use_all_repos_test_ext = module_extension(
doc = "Only used for testing modules.use_all_repos().",
)

def _apparent_repo_name_test(ctx):
"""Unit tests for modules.apparent_repo_name."""
env = unittest.begin(ctx)

asserts.equals(
env, "", modules.apparent_repo_name(""),
msg = "Handles the empty string as input",
)

asserts.equals(
env, "foo", modules.apparent_repo_name("foo"),
msg = (
"Return the original name unchanged if it doesn't start with `@`.",
),
)

asserts.equals(
env, "foo", modules.apparent_repo_name("@foo"),
msg = "Return the original name without `@` if already apparent.",
)

asserts.equals(
env, "foo", modules.apparent_repo_name(Label("@foo").repo_name),
msg = "Return the apparent name from a canonical name string.",
)

asserts.equals(
env, "", modules.apparent_repo_name(Label("@@//:all")),
msg = "Returns the empty string for a main repository Label.",
)

asserts.equals(
env, "", modules.apparent_repo_name(Label("@bazel_skylib//:all")),
msg = " ".join([
"Returns the empty string for a Label containing the main",
"repository's module name.",
]),
)

asserts.equals(
env, "foo", modules.apparent_repo_name(Label("@foo")),
msg = "Return the apparent name from a Label.",
)

asserts.equals(
env, "rules_pkg", modules.apparent_repo_name(Label("@rules_pkg")),
msg = " ".join([
"Top level module repos have the canonical name delimiter at the",
"end. Therefore, this should not return the empty string, but the",
"name without the leading `@` and trailing delimiter.",
]),
)

asserts.equals(
env,
"stardoc" if _is_bzlmod_enabled else "io_bazel_stardoc",
modules.apparent_repo_name(Label("@io_bazel_stardoc")),
msg = " ".join([
"Label values will already map bazel_dep repo_names to",
"actual repo names under Bzlmod (no-op under WORKSPACE)."
])
)

asserts.equals(
env, "foo", modules.apparent_repo_name("foo+1.2.3"),
msg = "Ignores version numbers in canonical repo names",
)

return unittest.end(env)

apparent_repo_name_test = unittest.make(_apparent_repo_name_test)

def _apparent_repo_label_string_test(ctx):
"""Unit tests for modules.apparent_repo_label_string."""
env = unittest.begin(ctx)

main_repo = str(Label("//:all"))
asserts.equals(
env, main_repo, modules.apparent_repo_label_string(Label("//:all")),
msg = "Returns top level target with leading `@` or `@@`",
)

main_module_label = Label("@bazel_skylib//:all")
asserts.equals(
env, main_repo, modules.apparent_repo_label_string(main_module_label),
msg = " ".join([
"Returns top level target with leading `@` or `@@`",
"for a Label containing the main module's name",
]),
)

rules_pkg = "@rules_pkg//:all"
asserts.equals(
env, rules_pkg, modules.apparent_repo_label_string(Label(rules_pkg)),
msg = "Returns original repo name",
)

asserts.equals(
env,
"@%s//:all" % ("stardoc" if _is_bzlmod_enabled else "io_bazel_stardoc"),
modules.apparent_repo_label_string(Label("@io_bazel_stardoc//:all")),
msg = " ".join([
"Returns the actual module name instead of",
"repo_name from bazel_dep() (no-op under WORKSPACE).",
]),
)

return unittest.end(env)

apparent_repo_label_string_test = unittest.make(
_apparent_repo_label_string_test
)

def _adjust_main_repo_prefix_test(ctx):
"""Unit tests for modules.apparent_repo_label_string."""
env = unittest.begin(ctx)

expected = modules.main_repo_prefix + ":all"
asserts.equals(
env, expected, modules.adjust_main_repo_prefix("@//:all"),
msg = "Normalizes a target pattern starting with `@//`.",
)

asserts.equals(
env, expected, modules.adjust_main_repo_prefix("@@//:all"),
msg = "Normalizes a target pattern starting with `@@//`.",
)

original = "@not_the_main_repo"
asserts.equals(
env, original, modules.adjust_main_repo_prefix(original),
msg = "Returns non main repo target patterns unchanged.",
)

return unittest.end(env)

adjust_main_repo_prefix_test = unittest.make(
_adjust_main_repo_prefix_test
)

# buildifier: disable=unnamed-macro
def modules_test_suite():
"""Creates the tests for modules.bzl if Bzlmod is enabled."""

is_bzlmod_enabled = str(Label("//tests:module_tests.bzl")).startswith("@@")
if not is_bzlmod_enabled:
unittest.suite(
"modules_tests",
apparent_repo_name_test,
apparent_repo_label_string_test,
adjust_main_repo_prefix_test,
)

if not _is_bzlmod_enabled:
return

build_test(
Expand Down

0 comments on commit 1d341f6

Please sign in to comment.