Skip to content

Commit f54b90b

Browse files
helly25helly25renovate[bot]meringTroyKomodo
authored
Implement latest requirements (bazel-contrib#472)
See issue bazel-contrib#473 This builds on PR bazel-contrib#471 This PR allows requirements support for the LLVM versions like `llvm_version = "first"` and `llvm_version = "latest"` which will effectively find the first and latest LLVM version respectively. For many people this will be the easiest way to get started and solves the issue that not always all arch/os versions have been prepared for all LLVM versions. It also means you do not have to know whether the basename starts with "LLVM" or "clang_llvm", in which order arch and os have to be provided and how the os is exactly specified. We further add the ability to specify requirements, including minimum and maximum versions as well as version exclusions. That can be easier than figuring out which versions are available for which platform. Further this allows to automatically pick up relevant versions automatically as they become available later. Example: ``` llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm", dev_dependency = True) llvm.toolchain( name = "llvm_toolchain_llvm", llvm_version = "latest:>=19.1.6,!=19.1.7,!=20.1.0", ) use_repo(llvm, "llvm_toolchain_llvm") ``` The requirements strings mostly follow semver specs and support `<`, `<=`, `>`, `>=`, `==` and `!=`. They only currently work for version components `major`, `minor` and `patch`. Since we do not list other versions in our config that is fine for now. One practical goal for this PR is to be able to run automatic tests with updated LLVM versions in the easiest possible way. Using the new extra_llvm_versions we can now easily run automated CI/CD tests against different LLVM branches. Say: * `llvm_version = 'latest:>=15,<16'` * `llvm_version = 'latest:>=19,<20'` * `llvm_version = 'latest:>=20,<21'` That above list would be used in three separate CI/CD runs that would then test the 15, 19 and 20 branches respectively. When new distributions get added upstream or locally, they can easily be added via the new `extra_llvm_distributions` and will be picked up by the setup without any further change. --------- Co-authored-by: helly25 <[email protected]> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Marcel <[email protected]> Co-authored-by: Troy <[email protected]> Co-authored-by: Fabian Meumertzheim <[email protected]> Co-authored-by: kitterion <[email protected]> Co-authored-by: trevorgray <[email protected]> Co-authored-by: Tom Rybka <[email protected]> Co-authored-by: xiaopeng-tranxmart <[email protected]>
1 parent 95fd182 commit f54b90b

File tree

12 files changed

+537
-32
lines changed

12 files changed

+537
-32
lines changed

.github/workflows/tests.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,19 @@ jobs:
4343
USE_BZLMOD: ${{ matrix.bzlmod }}
4444
run: tests/scripts/run_tests.sh
4545
toolchain_test:
46-
runs-on: ubuntu-latest
46+
strategy:
47+
fail-fast: false
48+
matrix:
49+
os: [macos-latest, ubuntu-latest]
50+
bazel_version: [7.x, latest]
51+
bzlmod: [true, false]
52+
runs-on: ${{ matrix.os }}
4753
steps:
4854
- uses: actions/checkout@v5
4955
- name: Test
5056
env:
51-
USE_BAZEL_VERSION: latest
52-
USE_BZLMOD: true
57+
USE_BAZEL_VERSION: ${{ matrix.bazel_version }}
58+
USE_BZLMOD: ${{ matrix.bzlmod }}
5359
run: tests/scripts/run_toolchain_tests.sh
5460
external_test:
5561
strategy:

MODULE.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module(
2222
bazel_dep(name = "bazel_skylib", version = "1.5.0")
2323
bazel_dep(name = "rules_cc", version = "0.2.2")
2424
bazel_dep(name = "platforms", version = "0.0.8")
25+
bazel_dep(name = "helly25_bzl", version = "0.1.2")
2526

2627
# TODO: Remove when protobuf is released with a version of rules_python that supports 8.x
2728
bazel_dep(name = "rules_python", version = "1.0.0", dev_dependency = True)

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,25 @@ See [bazel
123123
tutorial](https://docs.bazel.build/versions/main/tutorial/cc-toolchain-config.html)
124124
for how CC toolchains work in general.
125125

126+
### Requirements
127+
128+
Version attributes can be requirements of the form `first`, `first:<condition>`,
129+
`latest` or `latest:<condition>`.
130+
131+
In case of `latest`, the latest distribution matching the optional `condition`
132+
will be selected.
133+
134+
In case of `first`, the first distribution matching the optional `condition`
135+
will be selected.
136+
137+
The condition consists of a comma separated list of semver version comparisons
138+
supporting `<`, `<=`, `>`, `>=`, `==`, `!=`. Examples:
139+
140+
- `latest`
141+
- `latest:>=20.1.0`
142+
- `latest:>17.0.4,!=19.1.7,<=20.1.0`
143+
- `first:>=15.0.6,<16`
144+
126145
### Selecting Toolchains
127146

128147
If toolchains are registered (see Quickstart section above), you do not need to

WORKSPACE

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,12 @@
1515
workspace(
1616
name = "toolchains_llvm",
1717
)
18+
19+
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
20+
21+
http_archive(
22+
name = "helly25_bzl",
23+
sha256 = "404f8473bcaad2e370752e57d274d2093eb87ca94cb9a597c1a3553b76743206",
24+
strip_prefix = "bzl-0.1.2",
25+
url = "https://github.com/helly25/bzl/releases/download/0.1.2/bzl-0.1.2.tar.gz",
26+
)

tests/scripts/run_toolchain_tests.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,9 @@ targets=(
4141
"//toolchain/..."
4242
)
4343

44+
if [[ -z "${common_test_args:-}" ]]; then
45+
common_test_args=()
46+
fi
47+
4448
"${bazel}" ${TEST_MIGRATION:+"--strict"} --bazelrc=/dev/null test \
45-
"${common_test_args[@]}" "${test_args[@]}" "${targets[@]}"
49+
"${common_test_args[@]}" "${test_args[@]}" -- "${targets[@]}"

toolchain/deps.bzl

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ def bazel_toolchain_dependencies():
3535
sha256 = "6e78f0e57de26801f6f564fa7c4a48dc8b36873e416257a92bbb0937eeac8446",
3636
)
3737

38-
# Skip bazel_skylib_workspace because we are not using lib/unittest.bzl
39-
4038
# Load bazel_features if the user has not defined them.
4139
if not native.existing_rule("bazel_features"):
4240
http_archive(
@@ -45,3 +43,12 @@ def bazel_toolchain_dependencies():
4543
strip_prefix = "bazel_features-1.36.0",
4644
url = "https://github.com/bazel-contrib/bazel_features/releases/download/v1.36.0/bazel_features-v1.36.0.tar.gz",
4745
)
46+
47+
# Load helly25_bzl for version comparisons.
48+
if not native.existing_rule("helly25_bzl"):
49+
http_archive(
50+
name = "helly25_bzl",
51+
strip_prefix = "bzl-0.1.2",
52+
url = "https://github.com/helly25/bzl/releases/download/0.1.2/bzl-0.1.2.tar.gz",
53+
sha256 = "404f8473bcaad2e370752e57d274d2093eb87ca94cb9a597c1a3553b76743206",
54+
)

toolchain/internal/BUILD.bazel

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
# limitations under the License.
1414

1515
load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
16-
load("llvm_distributions.bzl", "write_distributions")
16+
load("llvm_distributions.bzl", "distributions_test_writer", "requirements_test_writer")
1717

1818
exports_files(["template.modulemap"])
1919

20-
write_distributions(
20+
distributions_test_writer(
2121
name = "llvm_distributions",
2222
testonly = True,
2323
output = "llvm_distributions.out.txt",
@@ -36,3 +36,16 @@ diff_test(
3636
file1 = "llvm_distributions.golden.sel.txt",
3737
file2 = "llvm_distributions.sel.txt",
3838
)
39+
40+
requirements_test_writer(
41+
name = "llvm_requirements_test_output",
42+
testonly = True,
43+
result = "llvm_requirements_test.output.txt",
44+
visibility = ["//visibility:private"],
45+
)
46+
47+
diff_test(
48+
name = "llvm_requirements_test",
49+
file1 = "llvm_requirements_test.golden.txt",
50+
file2 = "llvm_requirements_test.output.txt",
51+
)

toolchain/internal/configure.bzl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ load(
3737
_supported_targets = "SUPPORTED_TARGETS",
3838
_toolchain_tools = "toolchain_tools",
3939
)
40+
load("//toolchain/internal:llvm_distributions.bzl", "is_requirement", "required_llvm_release_name_rctx")
4041
load(
4142
"//toolchain/internal:sysroot.bzl",
4243
_default_sysroot_path = "default_sysroot_path",
@@ -80,6 +81,15 @@ def llvm_config_impl(rctx):
8081
if not toolchain_root:
8182
fail("LLVM toolchain root missing for ({}, {})".format(os, arch))
8283
(_key, llvm_version) = _exec_os_arch_dict_value(rctx, "llvm_versions")
84+
if is_requirement(llvm_version):
85+
llvm_version, distribution, error = required_llvm_release_name_rctx(rctx, llvm_version)
86+
if error:
87+
fail(error)
88+
if llvm_version:
89+
print("\nINFO: Resolved latest LLVM version to {llvm_version}: {distribution}".format(
90+
distribution = distribution,
91+
llvm_version = llvm_version,
92+
)) # buildifier: disable=print
8393
if not llvm_version:
8494
# LLVM version missing for (os, arch)
8595
_empty_repository(rctx)

toolchain/internal/llvm_distributions.bzl

Lines changed: 165 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "read_netrc", "use_netrc")
16+
load("@helly25_bzl//bzl/versions:versions.bzl", "versions")
1617
load(
1718
"//toolchain/internal:common.bzl",
1819
"attr_dict",
@@ -822,7 +823,8 @@ def _get_llvm_version(rctx):
822823

823824
def _get_all_llvm_distributions(*, llvm_distributions, extra_llvm_distributions, parsed_llvm_version):
824825
distributions = {}
825-
for basename, sha256 in llvm_distributions.items():
826+
for dist, sha256 in llvm_distributions.items() + (extra_llvm_distributions.items() if extra_llvm_distributions else []):
827+
basename = _distribution_basename(dist)
826828
version = _distribution_version(basename)
827829
if parsed_llvm_version and parsed_llvm_version != version:
828830
continue
@@ -831,16 +833,6 @@ def _get_all_llvm_distributions(*, llvm_distributions, extra_llvm_distributions,
831833
sha256 = sha256,
832834
version = version,
833835
)
834-
if extra_llvm_distributions:
835-
for dist, sha256 in extra_llvm_distributions.items():
836-
version = _distribution_version(dist)
837-
if parsed_llvm_version and parsed_llvm_version != version:
838-
continue
839-
distributions[_distribution_basename(dist)] = struct(
840-
distribution = dist,
841-
sha256 = sha256,
842-
version = version,
843-
)
844836
return distributions
845837

846838
_UBUNTU_NAMES = [
@@ -1103,6 +1095,10 @@ def _find_llvm_basename_list(llvm_version, all_llvm_distributions, host_info):
11031095
return []
11041096

11051097
def _find_llvm_basename_or_error(llvm_version, all_llvm_distributions, host_info):
1098+
all_llvm_distributions = _filter_llvm_distributions(
1099+
llvm_version = llvm_version,
1100+
all_llvm_distributions = all_llvm_distributions,
1101+
)
11061102
basenames = _find_llvm_basename_list(llvm_version, all_llvm_distributions, host_info)
11071103
if len(basenames) > 1:
11081104
return None, "ERROR: Multiple configurations found for version {llvm_version} on {os}/{dist_name}/{dist_version} with arch {arch}: [{basenames}].".format(
@@ -1129,8 +1125,69 @@ def _find_llvm_basename_or_error(llvm_version, all_llvm_distributions, host_info
11291125

11301126
return basenames[0], None
11311127

1128+
def _parse_version_or_requirements(version_or_requirements):
1129+
if version_or_requirements in ["latest", "first"]:
1130+
return None
1131+
for prefix in ["latest:", "first:"]:
1132+
if version_or_requirements.startswith(prefix):
1133+
return versions.parse_requirements(version_or_requirements.removeprefix(prefix))
1134+
fail("ERROR: Invalid version requirements: '{version_or_requirements}'.".format(
1135+
version_or_requirements = version_or_requirements,
1136+
))
1137+
1138+
def _get_version_from_distribution(distribution):
1139+
# We assume here that the `distribution` is a basename of the form `LLVM-<version>-...` or
1140+
# `clang+llvm-<version>-...`.
1141+
return distribution.split("-")[1]
1142+
1143+
def _get_llvm_versions(*, version_or_requirements, all_llvm_distributions):
1144+
llvm_version_dict = {}
1145+
for distribution in all_llvm_distributions.keys():
1146+
version = _get_version_from_distribution(distribution)
1147+
llvm_version_dict[_parse_version(version)] = version
1148+
1149+
return [v for k, v in sorted(llvm_version_dict.items(), reverse = version_or_requirements.startswith("latest"))]
1150+
1151+
def _required_llvm_release_name(*, version_or_requirements, all_llvm_distributions, host_info):
1152+
llvm_versions = _get_llvm_versions(version_or_requirements = version_or_requirements, all_llvm_distributions = all_llvm_distributions)
1153+
requirements = _parse_version_or_requirements(version_or_requirements)
1154+
for llvm_version in llvm_versions:
1155+
if requirements and not versions.check_all_requirements(llvm_version, requirements):
1156+
continue
1157+
basenames = _find_llvm_basename_list(llvm_version, all_llvm_distributions, host_info)
1158+
if len(basenames) == 1:
1159+
return llvm_version, basenames[0], None
1160+
return None, None, "ERROR: No matching distribution found."
1161+
1162+
def required_llvm_release_name_rctx(rctx, llvm_version):
1163+
all_llvm_distributions = _get_all_llvm_distributions(
1164+
llvm_distributions = _llvm_distributions,
1165+
extra_llvm_distributions = rctx.attr.extra_llvm_distributions,
1166+
parsed_llvm_version = _parse_version(llvm_version),
1167+
)
1168+
return _required_llvm_release_name(
1169+
version_or_requirements = llvm_version,
1170+
all_llvm_distributions = all_llvm_distributions,
1171+
host_info = host_info(rctx),
1172+
)
1173+
1174+
def is_requirement(version_or_requirement):
1175+
"""Return whether `version_or_requirement` is likely a requirement (True) or should be a version."""
1176+
for prefix in ["first:", "latest:"]:
1177+
if version_or_requirement.startswith(prefix) or version_or_requirement == prefix[:-1]:
1178+
return True
1179+
return False
1180+
1181+
def _filter_llvm_distributions(*, llvm_version, all_llvm_distributions):
1182+
"""Return (distribution: sha) entries from `all_llvm_distributions` that match `llvm_version`."""
1183+
result = {}
1184+
for k, v in all_llvm_distributions.items():
1185+
if _get_version_from_distribution(k) == llvm_version:
1186+
result[k] = v
1187+
return result
1188+
11321189
def _distribution_urls(rctx):
1133-
"""Return LLVM `urls`, `shha256` and `strip_prefix` for the given context."""
1190+
"""Return LLVM `urls`, `sha256` and `strip_prefix` for the given context."""
11341191
llvm_version = _get_llvm_version(rctx)
11351192
all_llvm_distributions = _get_all_llvm_distributions(
11361193
llvm_distributions = _llvm_distributions,
@@ -1140,7 +1197,15 @@ def _distribution_urls(rctx):
11401197
_, sha256, strip_prefix, _ = _key_attrs(rctx)
11411198

11421199
if rctx.attr.distribution == "auto":
1143-
basename, error = _find_llvm_basename_or_error(llvm_version, all_llvm_distributions, host_info(rctx))
1200+
rctx_host_info = host_info(rctx)
1201+
if is_requirement(llvm_version):
1202+
llvm_version, basename, error = _required_llvm_release_name(
1203+
version_or_requirements = llvm_version,
1204+
all_llvm_distributions = all_llvm_distributions,
1205+
host_info = rctx_host_info,
1206+
)
1207+
else:
1208+
basename, error = _find_llvm_basename_or_error(llvm_version, all_llvm_distributions, rctx_host_info)
11441209
if error:
11451210
fail(error)
11461211
dist_info = all_llvm_distributions[basename]
@@ -1177,7 +1242,7 @@ def _distribution_urls(rctx):
11771242

11781243
return urls, sha256, strip_prefix
11791244

1180-
def _write_distributions_impl(ctx):
1245+
def _distributions_test_writer_impl(ctx):
11811246
"""Analyze the configured versions and write to a file for test consumption.
11821247
11831248
The test generated file '<rule_name>.out' contains the following lines:
@@ -1387,10 +1452,94 @@ def _write_distributions_impl(ctx):
13871452
ctx.actions.write(ctx.outputs.output, "\n".join(output) + "\n")
13881453
ctx.actions.write(ctx.outputs.select, "\n".join(select) + "\n")
13891454

1390-
write_distributions = rule(
1391-
implementation = _write_distributions_impl,
1455+
distributions_test_writer = rule(
1456+
implementation = _distributions_test_writer_impl,
13921457
attrs = {
13931458
"output": attr.output(mandatory = True),
13941459
"select": attr.output(mandatory = True),
13951460
},
13961461
)
1462+
1463+
def _requirements_test_writer_impl(ctx):
1464+
"""Analyze the configured versions and write to a file for test consumption.
1465+
The test generated file '<rule_name>.out' contains the following lines:
1466+
[<arch>,<os>,<requirement>]: <llvm_distribution_basename>
1467+
"""
1468+
all_llvm_distributions = {
1469+
# In order to prevent new distributions to interfere we cut at 20.1.3.
1470+
k: v
1471+
for k, v in _llvm_distributions.items()
1472+
if _parse_version(_get_version_from_distribution(k)) <= (20, 1, 3)
1473+
}
1474+
requirement_list = [
1475+
"latest:<=20.1.0",
1476+
"latest:<=20.1.0,>17.0.4,!=19.1.7",
1477+
"latest:<20.1.0,>17.0.4,!=19.1.7",
1478+
"latest:<20.1.0,>17.0.4",
1479+
"latest:>=15.0.6,<16",
1480+
"first:>=15.0.6,<16",
1481+
"latest",
1482+
"first",
1483+
]
1484+
arch_list = [
1485+
"aarch64",
1486+
"armv7a",
1487+
"x86_64",
1488+
]
1489+
os_list = [
1490+
"darwin",
1491+
"linux",
1492+
"windows",
1493+
]
1494+
ANY_VERSION = "0" # Version does not matter, but must be a valid integer
1495+
dist_dict_list = {
1496+
"linux": [
1497+
# keep sorted
1498+
struct(name = "ubuntu", version = ANY_VERSION),
1499+
struct(name = "raspbian", version = ANY_VERSION),
1500+
struct(name = "rhel", version = ANY_VERSION),
1501+
],
1502+
}
1503+
result = []
1504+
for arch in arch_list:
1505+
for os in os_list:
1506+
dist_list = dist_dict_list.get(os, [struct(name = os, version = "")])
1507+
for dist in dist_list:
1508+
for requirement in requirement_list:
1509+
host_info = struct(
1510+
arch = arch,
1511+
os = os,
1512+
dist = dist,
1513+
)
1514+
llvm_version, basename, error = _required_llvm_release_name(
1515+
version_or_requirements = requirement,
1516+
all_llvm_distributions = all_llvm_distributions,
1517+
host_info = host_info,
1518+
)
1519+
if llvm_version and basename:
1520+
result.append("[{arch},{os}{dist_name}{dist_version},'{requirement}']: {llvm_version} = {basename}".format(
1521+
arch = arch,
1522+
os = os,
1523+
dist_name = "," + dist.name if os == "linux" else "",
1524+
dist_version = "," + dist.version if os == "linux" else "",
1525+
requirement = requirement,
1526+
llvm_version = llvm_version,
1527+
basename = basename,
1528+
))
1529+
else:
1530+
result.append("[{arch},{os},\"{requirement}\"]: {error}".format(
1531+
arch = arch,
1532+
os = os,
1533+
requirement = requirement,
1534+
llvm_version = llvm_version,
1535+
basename = basename,
1536+
error = error or "ERROR: N/A",
1537+
))
1538+
ctx.actions.write(ctx.outputs.result, "\n".join(result) + "\n")
1539+
1540+
requirements_test_writer = rule(
1541+
implementation = _requirements_test_writer_impl,
1542+
attrs = {
1543+
"result": attr.output(mandatory = True),
1544+
},
1545+
)

0 commit comments

Comments
 (0)