diff --git a/.github/workflows/default.yaml b/.github/workflows/default.yaml index b317919..fb73e47 100644 --- a/.github/workflows/default.yaml +++ b/.github/workflows/default.yaml @@ -60,6 +60,30 @@ jobs: run: | ln -s .github/workflows/.bazelrc.ci .bazelrc.ci bazel test --compilation_mode ${{ matrix.compilation_mode }} //... + gcc_versions: + strategy: + matrix: + gcc_version: + - 12.5.0 + - 13.4.0 + - 14.3.0 + - 15.2.0 + - 16.1.0 + - 16.2.0 + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - uses: bazel-contrib/setup-bazel@0.15.0 + with: + bazelisk-cache: true + disk-cache: ${{ github.workflow }} + repository-cache: true + - name: Test GCC ${{ matrix.gcc_version }} + run: | + ln -s .github/workflows/.bazelrc.ci .bazelrc.ci + bazel test --//toolchain:gcc_version=${{ matrix.gcc_version }} //... sanitizers: strategy: matrix: diff --git a/MODULE.bazel b/MODULE.bazel index 4375ba1..098a09e 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -19,8 +19,7 @@ gcc_toolchains = use_extension("//toolchain:module_extensions.bzl", "gcc_toolcha ), use_repo(gcc_toolchains, "gcc_toolchain_{}".format(arch)), register_toolchains( - "@gcc_toolchain_{}//:cc_toolchain".format(arch), - "@gcc_toolchain_{}//:fortran_toolchain".format(arch), + "@gcc_toolchain_{}//:all".format(arch), # Register toolchains as dev dependencies so that we don't pollute the toolchain resolution of consumers. dev_dependency = True, ), diff --git a/docs/README.md b/docs/README.md index 60ed94d..c674d46 100644 --- a/docs/README.md +++ b/docs/README.md @@ -45,6 +45,48 @@ gcc_register_toolchain( ) ``` +## Selecting the GCC version + +Every GCC version listed in `AVAILABLE_GCC_VERSIONS` is registered as a toolchain, and the +`@gcc_toolchain//toolchain:gcc_version` flag picks which one resolves: + +```shell +bazel build --@gcc_toolchain//toolchain:gcc_version=15.2.0 //... +``` + +Leaving the flag unset uses the `gcc_version` the toolchain was declared with, which defaults to +the newest available version. Only the selected version is downloaded — the others are declared but +never fetched. + +Because it is an ordinary build flag, it can be bound to a `.bazelrc` config: + +```shell +build:gcc15 --@gcc_toolchain//toolchain:gcc_version=15.2.0 +``` + +To change which version is used while the flag is unset, set it when declaring the toolchain. An +explicit `--@gcc_toolchain//toolchain:gcc_version` still overrides it: + +```bazel +gcc_toolchains.toolchain( + name = "gcc_toolchain_x86_64", + gcc_version = "15.2.0", + target_arch = "x86_64", +) +``` + +Each version also gets a `config_setting`, so build rules can branch on the selected compiler: + +```bazel +copts = select({ + "@gcc_toolchain//toolchain:gcc_version_12_5_0": ["-Wno-maybe-uninitialized"], + "//conditions:default": [], +}) +``` + +Note that these settings only match when the flag is set explicitly, so a `select()` over them +needs a `//conditions:default` branch to cover the unset case. + ## Language Support ### Pure C diff --git a/docs/defs.md b/docs/defs.md index 4944ba7..5cbb763 100644 --- a/docs/defs.md +++ b/docs/defs.md @@ -9,9 +9,8 @@ This module provides the definitions for registering a GCC toolchain for C and C
 gcc_toolchain(name, binary_prefix, enable_fortran, extra_asmflags, extra_cflags, extra_cxxflags,
-              extra_fflags, extra_ldflags, extra_target_compatible_with, fincludes,
-              gcc_toolchain_workspace_name, gcc_version, gcc_versions, includes, repo_mapping,
-              supports_param_files, target_arch, target_compatible_with, target_settings)
+              extra_fflags, extra_ldflags, fincludes, gcc_toolchain_workspace_name, gcc_version,
+              gcc_versions, includes, repo_mapping, supports_param_files, target_arch)
 
@@ -29,7 +28,6 @@ gcc_toolchain(name, extra_cxxflags | Extra flags for compiling C++. | List of strings | optional | [] | | extra_fflags | Extra flags for compiling Fortran, if enabled. | List of strings | optional | [] | | extra_ldflags | Extra flags for linking. %workspace% is rendered to the toolchain root path. See https://github.com/bazelbuild/bazel/blob/a48e246e/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainProviderHelper.java#L234-L254. | List of strings | optional | [] | -| extra_target_compatible_with | Additional constraint_values appended to target_compatible_with of the toolchain, on top of the values from the target_compatible_with attribute (including its defaults). Unlike target_compatible_with, {target_arch} is not rendered. | List of labels | optional | [] | | fincludes | Extra includes for compiling Fortran, if enabled. %workspace% is rendered to the toolchain root path. | List of strings | optional | [] | | gcc_toolchain_workspace_name | The name given to the gcc-toolchain repository, if the default was not used. | String | optional | "gcc_toolchain" | | gcc_version | The version of GCC. | String | optional | "16.2.0" | @@ -38,8 +36,6 @@ gcc_toolchain(name, repo_mapping | A dictionary from local repository name to global repository name. This allows controls over workspace dependency resolution for dependencies of this repository.<p>For example, an entry "@foo": "@bar" declares that, for any time this repository depends on @foo (such as a dependency on @foo//some:target, it should actually resolve that dependency within globally-declared @bar (@bar//some:target). | Dictionary: String -> String | required | | | supports_param_files | Set supports_param_files = 1 on the generated cc_toolchain, which lets Bazel pass linker arguments via an @params file. Enable this when link command lines for large targets overflow ARG_MAX (e.g. collect2: posix_spawn: Argument list too long). Off by default to preserve historical behavior. | Boolean | optional | False | | target_arch | The target architecture this toolchain produces. E.g. x86_64. | String | required | | -| target_compatible_with | constraint_values passed to target_compatible_with of the toolchain. {target_arch} is rendered to the target_arch attribute value. | List of strings | optional | ["@platforms//os:linux", "@platforms//cpu:{target_arch}"] | -| target_settings | config_settings passed to target_compatible_with of the toolchain. {target_arch} is rendered to the target_arch attribute value. | List of strings | optional | [] | @@ -50,7 +46,11 @@ gcc_toolchain(name, name, target_arch, kwargs) -Declares a `gcc_toolchain`. +Declares a `gcc_toolchain` for every available GCC version. + +`name` is the hub repository holding the `toolchain` declarations. Each GCC version gets its +own repository holding the `cc_toolchain`, fetched only when that version is selected through +the `@gcc_toolchain//toolchain:gcc_version` flag. You should use `gcc_register_toolchain` unless you need to register toolchains manually, e.g. if you are consuming this repository as a Bzlmod dependency. @@ -61,9 +61,9 @@ e.g. if you are consuming this repository as a Bzlmod dependency. | Name | Description | Default Value | | :------------- | :------------- | :------------- | -| name | The name passed to gcc_toolchain. | none | +| name | The name of the hub repository holding the toolchain declarations. | none | | target_arch | The target architecture of the toolchain. | none | -| kwargs | The extra arguments passed to gcc_toolchain. See gcc_toolchain for more info. | none | +| kwargs | The extra arguments passed to gcc_toolchain. See gcc_toolchain for more info. The attributes of the toolchain declarations themselves are also accepted here, since they apply to the hub rather than to gcc_toolchain:

target_compatible_with: constraint_values passed to target_compatible_with of the toolchain. {target_arch} is rendered to the target_arch argument value. Defaults to ["@platforms//os:linux", "@platforms//cpu:{target_arch}"].

extra_target_compatible_with: Additional constraint_values appended to target_compatible_with of the toolchain, on top of the values from the target_compatible_with argument (including its defaults). Unlike target_compatible_with, {target_arch} is not rendered.

target_settings: Additional config_settings passed to target_settings of the toolchain, on top of the GCC version selection. {target_arch} is rendered to the target_arch argument value. | none | @@ -74,15 +74,18 @@ e.g. if you are consuming this repository as a Bzlmod dependency. gcc_register_toolchain(name, target_arch, kwargs) -Declares a `gcc_toolchain` and calls `register_toolchain` for it. +Declares a `gcc_toolchain` for every available GCC version and registers all of them. + +Which one resolves is selected by the `@gcc_toolchain//toolchain:gcc_version` flag. + **PARAMETERS** | Name | Description | Default Value | | :------------- | :------------- | :------------- | -| name | The name passed to gcc_toolchain. | none | +| name | The name of the hub repository holding the toolchain declarations. | none | | target_arch | The target architecture of the toolchain. | none | -| kwargs | The extra arguments passed to gcc_toolchain. See gcc_toolchain for more info. | none | +| kwargs | The extra arguments passed to gcc_declare_toolchain. See gcc_declare_toolchain for more info. | none | diff --git a/docs/updating-gcc-builds.md b/docs/updating-gcc-builds.md index ab199c7..402aee6 100644 --- a/docs/updating-gcc-builds.md +++ b/docs/updating-gcc-builds.md @@ -55,6 +55,15 @@ Edit `AVAILABLE_GCC_VERSIONS` in [toolchain/defs.bzl](../toolchain/defs.bzl). Wh new GCC version, add a new entry; bump `DEFAULT_GCC_VERSION` if that version becomes the default. Leave versions that the newer releases do not publish pointing at their existing release tag. +Every entry becomes a selectable toolchain and a `//toolchain:gcc_version_*` config setting, so +adding or removing a version also means: + +- adding it to the `gcc_versions` matrix in + [.github/workflows/default.yaml](../.github/workflows/default.yaml); +- checking whether the tarball ships `bin/ld.lld`. GCC versions built without it cannot use the + `linker-lld` feature, and `//tests/lld` marks those versions incompatible so the test is skipped + rather than failing. + ## 4. Regenerate the docs `AVAILABLE_GCC_VERSIONS` is serialized into the `gcc_versions` attribute default, so @@ -71,6 +80,11 @@ New tarballs can change more than their contents, so exercise every architecture ```shell bazel test //... +# Every selectable GCC version, which is what the CI matrix runs. +for gcc_version in 12.5.0 13.4.0 14.3.0 15.2.0 16.1.0 16.2.0; do + bazel test --//toolchain:gcc_version=${gcc_version} //... +done + # Cross-compilation, which the default test run does not cover. for platform in aarch64_linux armv7_linux x86_64_linux; do bazel build --platforms=//platforms:${platform} \ diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index 7251e25..61d2a02 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -18,8 +18,7 @@ gcc_toolchains = use_extension("@gcc_toolchain//toolchain:module_extensions.bzl" ), use_repo(gcc_toolchains, "gcc_toolchain_{}".format(arch)), register_toolchains( - "@gcc_toolchain_{}//:cc_toolchain".format(arch), - "@gcc_toolchain_{}//:fortran_toolchain".format(arch), + "@gcc_toolchain_{}//:all".format(arch), dev_dependency = True, ), ] diff --git a/tests/gcc_version/BUILD.bazel b/tests/gcc_version/BUILD.bazel new file mode 100644 index 0000000..c1a13dc --- /dev/null +++ b/tests/gcc_version/BUILD.bazel @@ -0,0 +1,39 @@ +# Copyright (c) Thulio Ferraz Assis 2026 +# +# 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. + +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("//rules_cc:defs.bzl", "cc_test") +load("//toolchain:defs.bzl", "AVAILABLE_GCC_VERSIONS", "DEFAULT_GCC_VERSION") + +# Asserts that the compiler picked by toolchain resolution is the one the gcc_version flag asked +# for. The build fails to compile when the selected toolchain reports a different version. +cc_test( + name = "gcc_version_test", + srcs = ["main.c"], + copts = select(dicts.add( + { + "//toolchain:gcc_version_{}".format(gcc_version.replace(".", "_")): [ + "-DEXPECTED_GCC_MAJOR={}".format(gcc_version.split(".")[0]), + "-DEXPECTED_GCC_MINOR={}".format(gcc_version.split(".")[1]), + ] + for gcc_version in AVAILABLE_GCC_VERSIONS + }, + { + "//conditions:default": [ + "-DEXPECTED_GCC_MAJOR={}".format(DEFAULT_GCC_VERSION.split(".")[0]), + "-DEXPECTED_GCC_MINOR={}".format(DEFAULT_GCC_VERSION.split(".")[1]), + ], + }, + )), +) diff --git a/tests/gcc_version/main.c b/tests/gcc_version/main.c new file mode 100644 index 0000000..a44506b --- /dev/null +++ b/tests/gcc_version/main.c @@ -0,0 +1,28 @@ +// Copyright (c) Thulio Ferraz Assis 2026 +// +// 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. + +#include + +#if __GNUC__ != EXPECTED_GCC_MAJOR +#error "The compiling GCC major version does not match the gcc_version flag." +#endif + +#if __GNUC_MINOR__ != EXPECTED_GCC_MINOR +#error "The compiling GCC minor version does not match the gcc_version flag." +#endif + +int main(void) { + printf("%d.%d.%d\n", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__); + return 0; +} diff --git a/tests/lld/BUILD.bazel b/tests/lld/BUILD.bazel index d84d978..66623c0 100644 --- a/tests/lld/BUILD.bazel +++ b/tests/lld/BUILD.bazel @@ -12,15 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("//rules_cc:defs.bzl", "cc_binary") load("@rules_shell//shell:sh_test.bzl", "sh_test") +load("//rules_cc:defs.bzl", "cc_binary") # This binary is always linked with lld via the target-level `linker-lld` -# feature, which requires the toolchain archive to ship `ld.lld`. +# feature, which requires the toolchain archive to ship `ld.lld`. GCC versions +# older than 15.2.0 are built without lld, so the test is skipped for them. cc_binary( name = "hello_world_lld", srcs = ["main.c"], features = ["linker-lld"], + target_compatible_with = select({ + "//toolchain:gcc_version_12_5_0": ["@platforms//:incompatible"], + "//toolchain:gcc_version_13_4_0": ["@platforms//:incompatible"], + "//toolchain:gcc_version_14_3_0": ["@platforms//:incompatible"], + "//conditions:default": [], + }), ) sh_test( diff --git a/toolchain/BUILD.bazel b/toolchain/BUILD.bazel index b487911..44c53ea 100644 --- a/toolchain/BUILD.bazel +++ b/toolchain/BUILD.bazel @@ -16,6 +16,27 @@ # limitations under the License. load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("@bazel_skylib//rules:common_settings.bzl", "string_flag") +load(":defs.bzl", "AVAILABLE_GCC_VERSIONS") + +# Selects which GCC version the registered toolchains resolve to. The empty default defers to the +# gcc_version each toolchain was declared with. +string_flag( + name = "gcc_version", + build_setting_default = "", + visibility = ["//visibility:public"], +) + +# Matches when gcc_version is set to that exact version. None of these match while the flag is left +# at its empty default, so a select over them needs a //conditions:default branch. +[ + config_setting( + name = "gcc_version_{}".format(gcc_version.replace(".", "_")), + flag_values = {":gcc_version": gcc_version}, + visibility = ["//visibility:public"], + ) + for gcc_version in AVAILABLE_GCC_VERSIONS +] exports_files( ["config.bzl"], diff --git a/toolchain/defs.bzl b/toolchain/defs.bzl index e490480..f96cc79 100644 --- a/toolchain/defs.bzl +++ b/toolchain/defs.bzl @@ -144,17 +144,6 @@ def _gcc_toolchain_impl(rctx): ] ] - target_compatible_with = [ - v.format(target_arch = target_arch) - for v in rctx.attr.target_compatible_with - ] - target_compatible_with.extend([str(c) for c in rctx.attr.extra_target_compatible_with]) - - target_settings = [ - v.format(target_arch = target_arch) - for v in rctx.attr.target_settings - ] - builtin_include_directories = [] builtin_include_directories.extend(c_builtin_includes) builtin_include_directories.extend(cxx_builtin_includes) @@ -261,8 +250,6 @@ def _gcc_toolchain_impl(rctx): rctx.file("BUILD.bazel", _TOOLCHAIN_BUILD_FILE_CONTENT.format( gcc_toolchain_workspace_name = rctx.attr.gcc_toolchain_workspace_name, enable_fortran = str(rctx.attr.enable_fortran), - target_compatible_with = target_compatible_with, - target_settings = target_settings, binary_prefix = binary_prefix, include_prefix = include_prefix, @@ -283,13 +270,12 @@ def _gcc_toolchain_impl(rctx): **_fortran_vars( rctx.attr.enable_fortran, rctx.attr.gcc_toolchain_workspace_name, - target_compatible_with, include_prefix, binary_prefix, ) )) -def _fortran_vars(enable_fortran, gcc_toolchain_workspace_name, target_compatible_with, include_prefix, binary_prefix): +def _fortran_vars(enable_fortran, gcc_toolchain_workspace_name, include_prefix, binary_prefix): load_ = "" toolchain = "" includes = "" @@ -302,24 +288,10 @@ def _fortran_vars(enable_fortran, gcc_toolchain_workspace_name, target_compatibl load_ = 'load("@{gcc_toolchain_workspace_name}//toolchain/fortran:defs.bzl", "fortran_toolchain")'.format( gcc_toolchain_workspace_name = gcc_toolchain_workspace_name, ) - toolchain = '''toolchain( - name = "fortran_toolchain", - exec_compatible_with = [ - "@platforms//os:linux", - "@platforms//cpu:x86_64", - ], - target_compatible_with = {target_compatible_with}, - toolchain = ":_fortran_toolchain", - toolchain_type = "@{gcc_toolchain_workspace_name}//toolchain/fortran:toolchain_type", -) - -fortran_toolchain( + toolchain = '''fortran_toolchain( name = "_fortran_toolchain", cc_toolchain = ":_cc_toolchain", -)'''.format( - gcc_toolchain_workspace_name = gcc_toolchain_workspace_name, - target_compatible_with = target_compatible_with, - ) +)''' includes = ''' # Fortran includes "lib/gcc/{include_prefix}*/finclude/**",'''.format(include_prefix = include_prefix) @@ -496,6 +468,17 @@ _FEATURE_ATTRS = { doc = "The target architecture this toolchain produces. E.g. x86_64.", mandatory = True, ), +} + +# Attributes of the `toolchain` declarations, which live in the hub rather than in the repository +# holding the compiler itself. +_TOOLCHAIN_DECLARATION_ATTRS = { + "extra_target_compatible_with": attr.label_list( + doc = "Additional constraint_values appended to target_compatible_with of the toolchain," + + " on top of the values from the target_compatible_with attribute (including its defaults)." + + " Unlike target_compatible_with, {target_arch} is not rendered.", + mandatory = False, + ), "target_compatible_with": attr.string_list( default = [ "@platforms//os:linux", @@ -504,15 +487,9 @@ _FEATURE_ATTRS = { doc = "constraint_values passed to target_compatible_with of the toolchain. {target_arch} is rendered to the target_arch attribute value.", mandatory = False, ), - "extra_target_compatible_with": attr.label_list( - doc = "Additional constraint_values appended to target_compatible_with of the toolchain," + - " on top of the values from the target_compatible_with attribute (including its defaults)." + - " Unlike target_compatible_with, {target_arch} is not rendered.", - mandatory = False, - ), "target_settings": attr.string_list( default = [], - doc = "config_settings passed to target_compatible_with of the toolchain. {target_arch} is rendered to the target_arch attribute value.", + doc = "Additional config_settings passed to target_settings of the toolchain, on top of the GCC version selection. {target_arch} is rendered to the target_arch attribute value.", mandatory = False, ), } @@ -531,8 +508,129 @@ gcc_toolchain = repository_rule( ), ) +def _sanitize_version(gcc_version): + return gcc_version.replace(".", "_") + +def _gcc_toolchains_hub_impl(rctx): + target_arch = rctx.attr.target_arch + gcc_version_flag = str(rctx.attr._gcc_version_flag) + + target_compatible_with = [ + v.format(target_arch = target_arch) + for v in rctx.attr.target_compatible_with + ] + target_compatible_with.extend([str(c) for c in rctx.attr.extra_target_compatible_with]) + + extra_target_settings = [ + v.format(target_arch = target_arch) + for v in rctx.attr.target_settings + ] + + toolchain_repos = rctx.attr.toolchain_repos + default_gcc_version = rctx.attr.default_gcc_version + if default_gcc_version not in toolchain_repos: + fail("The default GCC version {} has no toolchain for {}.".format(default_gcc_version, target_arch)) + + content = [_HUB_BUILD_FILE_HEADER] + + content.append('''config_setting( + name = "_gcc_version_unset", + flag_values = {{"{gcc_version_flag}": ""}}, +)'''.format(gcc_version_flag = gcc_version_flag)) + + for gcc_version in sorted(toolchain_repos.keys()): + content.append('''config_setting( + name = "_gcc_version_{suffix}", + flag_values = {{"{gcc_version_flag}": "{gcc_version}"}}, +)'''.format( + suffix = _sanitize_version(gcc_version), + gcc_version = gcc_version, + gcc_version_flag = gcc_version_flag, + )) + + # The default version is selected both when the flag is left at its empty default and when it + # is set to that version explicitly. + content.append('''selects.config_setting_group( + name = "_gcc_version_default", + match_any = [ + ":_gcc_version_unset", + ":_gcc_version_{suffix}", + ], +)'''.format(suffix = _sanitize_version(default_gcc_version))) + + for gcc_version in sorted(toolchain_repos.keys()): + is_default = gcc_version == default_gcc_version + suffix = "" if is_default else "_" + _sanitize_version(gcc_version) + setting = ":_gcc_version_default" if is_default else ":_gcc_version_" + _sanitize_version(gcc_version) + content.append(_HUB_TOOLCHAIN_TEMPLATE.format( + suffix = suffix, + target_compatible_with = target_compatible_with, + target_settings = [setting] + extra_target_settings, + toolchain_repo = toolchain_repos[gcc_version], + )) + if rctx.attr.enable_fortran: + content.append(_HUB_FORTRAN_TOOLCHAIN_TEMPLATE.format( + suffix = suffix, + target_compatible_with = target_compatible_with, + target_settings = [setting] + extra_target_settings, + toolchain_repo = toolchain_repos[gcc_version], + fortran_toolchain_type = str(rctx.attr._fortran_toolchain_type), + )) + + # Keep the targets of the selected toolchain reachable under the hub name, so that labels like + # @gcc_toolchain_x86_64//:libstdcxx keep resolving and follow the selected version. + aliases = list(_HUB_ALIASED_TARGETS) + if rctx.attr.enable_fortran: + aliases.append("_fortran_toolchain") + for target in aliases: + actual = { + ":_gcc_version_" + _sanitize_version(gcc_version): "@{}//:{}".format(toolchain_repo, target) + for gcc_version, toolchain_repo in toolchain_repos.items() + if gcc_version != default_gcc_version + } + actual["//conditions:default"] = "@{}//:{}".format(toolchain_repos[default_gcc_version], target) + content.append('''alias( + name = "{target}", + actual = select({actual}), +)'''.format(target = target, actual = actual)) + + rctx.file("BUILD.bazel", "\n\n".join(content) + "\n") + +_gcc_toolchains_hub = repository_rule( + _gcc_toolchains_hub_impl, + attrs = dicts.add( + _TOOLCHAIN_DECLARATION_ATTRS, + { + "default_gcc_version": attr.string( + doc = "The GCC version used when the gcc_version flag is not set.", + mandatory = True, + ), + "enable_fortran": attr.bool( + doc = "Whether to declare the Fortran toolchains.", + default = True, + ), + "target_arch": attr.string( + doc = "The target architecture the toolchains produce. E.g. x86_64.", + mandatory = True, + ), + "toolchain_repos": attr.string_dict( + doc = "Maps each available GCC version to the repository holding its cc_toolchain.", + mandatory = True, + ), + "_fortran_toolchain_type": attr.label( + default = Label("//toolchain/fortran:toolchain_type"), + ), + "_gcc_version_flag": attr.label( + default = Label("//toolchain:gcc_version"), + ), + }, + ), +) + +_SHAREABLE_ATTRS = dicts.add(_FEATURE_ATTRS, _TOOLCHAIN_DECLARATION_ATTRS) + ATTRS_SHARED_WITH_MODULE_EXTENSION = { - attr_name: _FEATURE_ATTRS[attr_name] + attr_name: _SHAREABLE_ATTRS[attr_name] for attr_name in [ "gcc_version", "gcc_versions", @@ -630,15 +728,34 @@ def gcc_declare_toolchain( name, target_arch, **kwargs): - """Declares a `gcc_toolchain`. + """Declares a `gcc_toolchain` for every available GCC version. + + `name` is the hub repository holding the `toolchain` declarations. Each GCC version gets its + own repository holding the `cc_toolchain`, fetched only when that version is selected through + the `@gcc_toolchain//toolchain:gcc_version` flag. You should use `gcc_register_toolchain` unless you need to register toolchains manually, e.g. if you are consuming this repository as a Bzlmod dependency. Args: - name: The name passed to `gcc_toolchain`. + name: The name of the hub repository holding the toolchain declarations. target_arch: The target architecture of the toolchain. **kwargs: The extra arguments passed to `gcc_toolchain`. See `gcc_toolchain` for more info. + The attributes of the `toolchain` declarations themselves are also accepted here, + since they apply to the hub rather than to `gcc_toolchain`: + + `target_compatible_with`: constraint_values passed to `target_compatible_with` of the + toolchain. `{target_arch}` is rendered to the `target_arch` argument value. Defaults to + `["@platforms//os:linux", "@platforms//cpu:{target_arch}"]`. + + `extra_target_compatible_with`: Additional constraint_values appended to + `target_compatible_with` of the toolchain, on top of the values from the + `target_compatible_with` argument (including its defaults). Unlike + `target_compatible_with`, `{target_arch}` is not rendered. + + `target_settings`: Additional config_settings passed to `target_settings` of the + toolchain, on top of the GCC version selection. `{target_arch}` is rendered to the + `target_arch` argument value. """ binary_prefix = kwargs.pop("binary_prefix", None) if binary_prefix == None: @@ -646,37 +763,85 @@ def gcc_declare_toolchain( fail("Unsupported target architecture: {}".format(target_arch)) binary_prefix = _AUTO_BINARY_PREFIX - gcc_toolchain( + default_gcc_version = kwargs.pop("gcc_version", DEFAULT_GCC_VERSION) + gcc_versions = kwargs.pop("gcc_versions", json.encode(AVAILABLE_GCC_VERSIONS)) + enable_fortran = kwargs.pop("enable_fortran", True) + extra_target_compatible_with = kwargs.pop("extra_target_compatible_with", []) + target_compatible_with = kwargs.pop("target_compatible_with", None) + target_settings = kwargs.pop("target_settings", []) + + # Left in kwargs so that the per-version repositories keep receiving it. + repo_mapping = kwargs.get("repo_mapping", None) + + extra_cflags = kwargs.pop("extra_cflags", []) + extra_cxxflags = kwargs.pop("extra_cxxflags", []) + extra_fflags = kwargs.pop("extra_fflags", []) + extra_ldflags = kwargs.pop("extra_ldflags", []) + extra_asmflags = kwargs.pop("extra_asmflags", []) + includes = kwargs.pop("includes", []) + fincludes = kwargs.pop("fincludes", []) + + toolchain_repos = {} + for gcc_version in json.decode(gcc_versions): + toolchain_repo = "{}_{}".format(name, _sanitize_version(gcc_version)) + toolchain_repos[gcc_version] = toolchain_repo + gcc_toolchain( + name = toolchain_repo, + binary_prefix = binary_prefix, + enable_fortran = enable_fortran, + extra_cflags = extra_cflags, + extra_cxxflags = extra_cxxflags, + extra_fflags = extra_fflags, + extra_ldflags = extra_ldflags, + extra_asmflags = extra_asmflags, + includes = includes, + fincludes = fincludes, + gcc_version = gcc_version, + gcc_versions = gcc_versions, + target_arch = target_arch, + **kwargs + ) + + hub_kwargs = {} + + # An explicitly empty target_compatible_with drops the default constraints, so only an omitted + # one may fall back to the attribute default. + if target_compatible_with != None: + hub_kwargs["target_compatible_with"] = target_compatible_with + + # The hub BUILD file resolves @bazel_skylib, @platforms and @rules_cc as well, so it needs the + # same repository mapping as the per-version repositories. + if repo_mapping != None: + hub_kwargs["repo_mapping"] = repo_mapping + + _gcc_toolchains_hub( name = name, - binary_prefix = binary_prefix, - extra_cflags = kwargs.pop("extra_cflags", []), - extra_cxxflags = kwargs.pop("extra_cxxflags", []), - extra_fflags = kwargs.pop("extra_fflags", []), - extra_ldflags = kwargs.pop("extra_ldflags", []), - extra_asmflags = kwargs.pop("extra_asmflags", []), - extra_target_compatible_with = kwargs.pop("extra_target_compatible_with", []), - includes = kwargs.pop("includes", []), - fincludes = kwargs.pop("fincludes", []), + default_gcc_version = default_gcc_version, + enable_fortran = enable_fortran, + extra_target_compatible_with = extra_target_compatible_with, target_arch = target_arch, - **kwargs + target_settings = target_settings, + toolchain_repos = toolchain_repos, + **hub_kwargs ) def gcc_register_toolchain( name, target_arch, **kwargs): - """Declares a `gcc_toolchain` and calls `register_toolchain` for it. + """Declares a `gcc_toolchain` for every available GCC version and registers all of them. + + Which one resolves is selected by the `@gcc_toolchain//toolchain:gcc_version` flag. Args: - name: The name passed to `gcc_toolchain`. + name: The name of the hub repository holding the toolchain declarations. target_arch: The target architecture of the toolchain. - **kwargs: The extra arguments passed to `gcc_toolchain`. See `gcc_toolchain` for more info. + **kwargs: The extra arguments passed to `gcc_declare_toolchain`. See + `gcc_declare_toolchain` for more info. """ enable_fortran = kwargs.pop("enable_fortran", True) gcc_declare_toolchain(name, target_arch, enable_fortran = enable_fortran, **kwargs) - native.register_toolchains("@{}//:cc_toolchain".format(name)) - if enable_fortran: - native.register_toolchains("@{}//:fortran_toolchain".format(name)) + native.register_toolchains("@{}//:all".format(name)) ARCHS = struct( aarch64 = "aarch64", @@ -684,27 +849,84 @@ ARCHS = struct( x86_64 = "x86_64", ) -_TOOLCHAIN_BUILD_FILE_CONTENT = """\ -load("@rules_cc//cc:defs.bzl", "cc_toolchain", "cc_library") -load("@{gcc_toolchain_workspace_name}//toolchain:cc_toolchain_config.bzl", "cc_toolchain_config") -{fortran_load} -load("//:tool_paths.bzl", "tool_paths") +# The public targets of a toolchain repository, aliased by the hub so that they track the GCC +# version selected by the flag. +_HUB_ALIASED_TARGETS = [ + "_cc_toolchain", + "all_files", + "ar", + "ar_files", + "as", + "as_files", + "cc_toolchain_config", + "compiler_files", + "coverage_files", + "dwp_files", + "gcc", + "gcov", + "include", + "ld", + "ld.bfd", + "ld_files", + "lib", + "libasan", + "liblsan", + "libstdcxx", + "libstdcxx_static", + "libtsan", + "libubsan", + "linker_files", + "lld_files", + "nm", + "objcopy", + "objcopy_files", + "objdump", + "ranlib", + "readelf", + "strip", + "strip_files", +] -package(default_visibility = ["//visibility:public"]) +_HUB_BUILD_FILE_HEADER = '''\ +load("@bazel_skylib//lib:selects.bzl", "selects") -{fortran_toolchain} +package(default_visibility = ["//visibility:public"])''' +_HUB_TOOLCHAIN_TEMPLATE = '''\ toolchain( - name = "cc_toolchain", + name = "cc_toolchain{suffix}", exec_compatible_with = [ "@platforms//os:linux", "@platforms//cpu:x86_64", ], target_compatible_with = {target_compatible_with}, target_settings = {target_settings}, - toolchain = ":_cc_toolchain", + toolchain = "@{toolchain_repo}//:_cc_toolchain", toolchain_type = "@rules_cc//cc:toolchain_type", -) +)''' + +_HUB_FORTRAN_TOOLCHAIN_TEMPLATE = '''\ +toolchain( + name = "fortran_toolchain{suffix}", + exec_compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], + target_compatible_with = {target_compatible_with}, + target_settings = {target_settings}, + toolchain = "@{toolchain_repo}//:_fortran_toolchain", + toolchain_type = "{fortran_toolchain_type}", +)''' + +_TOOLCHAIN_BUILD_FILE_CONTENT = """\ +load("@rules_cc//cc:defs.bzl", "cc_toolchain", "cc_library") +load("@{gcc_toolchain_workspace_name}//toolchain:cc_toolchain_config.bzl", "cc_toolchain_config") +{fortran_load} +load("//:tool_paths.bzl", "tool_paths") + +package(default_visibility = ["//visibility:public"]) + +{fortran_toolchain} cc_toolchain( name = "_cc_toolchain",