From 2b31ab43db31392b20acd76da984425e88b0efa7 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Thu, 15 May 2025 09:32:29 -0700 Subject: [PATCH 1/3] Added `cxx_standard` package for defining cxxopts --- cc/compiler/BUILD | 37 ++++----------- cc/compiler/compilers.bzl | 28 ++++++++++++ cc/cxx_standard/BUILD | 53 ++++++++++++++++++++++ cc/cxx_standard/cxx_standard.bzl | 78 ++++++++++++++++++++++++++++++++ tests/cxx_std_macro/BUILD | 29 ++++++++++++ tests/cxx_std_macro/main.cc | 24 ++++++++++ 6 files changed, 220 insertions(+), 29 deletions(-) create mode 100644 cc/compiler/compilers.bzl create mode 100644 cc/cxx_standard/BUILD create mode 100644 cc/cxx_standard/cxx_standard.bzl create mode 100644 tests/cxx_std_macro/BUILD create mode 100644 tests/cxx_std_macro/main.cc diff --git a/cc/compiler/BUILD b/cc/compiler/BUILD index 6db7145f..e67fecda 100644 --- a/cc/compiler/BUILD +++ b/cc/compiler/BUILD @@ -42,6 +42,7 @@ simplified by extracting the select expression into a Starlark constant. """ load("//cc/toolchains:compiler_flag.bzl", "compiler_flag") +load(":compilers.bzl", "COMPILERS") package(default_visibility = ["//visibility:public"]) @@ -49,32 +50,10 @@ licenses(["notice"]) compiler_flag(name = "compiler") -config_setting( - name = "clang", - flag_values = {":compiler": "clang"}, -) - -config_setting( - name = "clang-cl", - flag_values = {":compiler": "clang-cl"}, -) - -config_setting( - name = "gcc", - flag_values = {":compiler": "gcc"}, -) - -config_setting( - name = "mingw-gcc", - flag_values = {":compiler": "mingw-gcc"}, -) - -config_setting( - name = "msvc-cl", - flag_values = {":compiler": "msvc-cl"}, -) - -config_setting( - name = "emscripten", - flag_values = {":compiler": "emscripten"}, -) +[ + config_setting( + name = compiler, + flag_values = {":compiler": compiler}, + ) + for compiler in COMPILERS +] diff --git a/cc/compiler/compilers.bzl b/cc/compiler/compilers.bzl new file mode 100644 index 00000000..767eef96 --- /dev/null +++ b/cc/compiler/compilers.bzl @@ -0,0 +1,28 @@ +# Copyright 2025 The Bazel Authors. All rights reserved. +# +# 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. +"""Known cc compilers""" + +visibility([ + "//cc/cxx_standard/...", +]) + +COMPILERS = [ + # keep sorted + "clang", + "clang-cl", + "emscripten", + "gcc", + "mingw-gcc", + "msvc-cl", +] diff --git a/cc/cxx_standard/BUILD b/cc/cxx_standard/BUILD new file mode 100644 index 00000000..675813a9 --- /dev/null +++ b/cc/cxx_standard/BUILD @@ -0,0 +1,53 @@ +# Copyright 2025 The Bazel Authors. All rights reserved. +# +# 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//rules:common_settings.bzl", "string_flag") +load("//cc/compiler:compilers.bzl", "COMPILERS") +load(":cxx_standard.bzl", "VERSIONS") + +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +string_flag( + name = "cxx_standard", + build_setting_default = "default", + values = [ + "default", + "none", + ] + VERSIONS, +) + +[ + config_setting( + name = "cxx{}_{}".format(version, compiler), + flag_values = { + "//cc/private/toolchain:compiler": compiler, + ":cxx_standard": version, + }, + ) + for version in VERSIONS + for compiler in COMPILERS +] + +[ + config_setting( + name = "cxx_default_{}".format(compiler), + flag_values = { + "//cc/private/toolchain:compiler": compiler, + ":cxx_standard": "default", + }, + ) + for compiler in COMPILERS +] diff --git a/cc/cxx_standard/cxx_standard.bzl b/cc/cxx_standard/cxx_standard.bzl new file mode 100644 index 00000000..7ec5a1eb --- /dev/null +++ b/cc/cxx_standard/cxx_standard.bzl @@ -0,0 +1,78 @@ +# Copyright 2025 The Bazel Authors. All rights reserved. +# +# 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. +"""Globals for C++ std version flags.""" + +load("//cc/compiler:compilers.bzl", "COMPILERS") + +VERSIONS = [ + "98", + "03", + "11", + "14", + "17", + "20", + "23", + "26", + "2c", +] + +def _flag(version, compiler): + if compiler.startswith("msvc"): + return "/std:c++{}".format(version) + + return "-std=c++{}".format(version) + +def cxxopts(default = None): + """Generate a select statement which contains the correct `stdcxx` flag for `cxxopts` attributes. + + Example: + + ```starlark + load("@rules_cc//cc:cc_binary.bzl", "cc_binary") + load("@rules_cc//cc/cxx_standard:cxx_standard.bzl", "cxxopts") + + cc_binary( + name = "foo", + srcs = ["foo.cc"], + cxxopts = cxxopts(default = "20") + [ + # Any additional cxxopts + ], + ) + ``` + + Note that the `--@rules_cc//cc/cxx_standard` flag can be used to override specified `default` value. + + Args: + default (str, optional): The default version of the C++ standard to use. + + Returns: + select: A mapping of cxx version and compiler to the `cxxopts` flags. + """ + default_branches = {} + if default: + if default not in VERSIONS: + fail("Unexpected stdc++ version: {}".format(default)) + + default_branches = { + Label("//cc/cxx_standard:cxx_default_{}".format(compiler)): [_flag(default, compiler)] + for compiler in COMPILERS + } + + return select({ + Label("//cc/cxx_standard:cxx{}_{}".format(version, compiler)): [_flag(version, compiler)] + for version in VERSIONS + for compiler in COMPILERS + } | default_branches | { + "//conditions:default": [], + }) diff --git a/tests/cxx_std_macro/BUILD b/tests/cxx_std_macro/BUILD new file mode 100644 index 00000000..f7c707a6 --- /dev/null +++ b/tests/cxx_std_macro/BUILD @@ -0,0 +1,29 @@ +# Copyright 2025 The Bazel Authors. All rights reserved. +# +# 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("//cc:cc_binary.bzl", "cc_binary") +load("//cc/cxx_standard:cxx_standard.bzl", "cxxopts") + +licenses(["notice"]) + +cc_binary( + name = "main", + srcs = ["main.cc"], + copts = cxxopts(default = "17"), +) + +cc_binary( + name = "main_no_default", + srcs = ["main.cc"], + copts = cxxopts(), +) diff --git a/tests/cxx_std_macro/main.cc b/tests/cxx_std_macro/main.cc new file mode 100644 index 00000000..ae69b70a --- /dev/null +++ b/tests/cxx_std_macro/main.cc @@ -0,0 +1,24 @@ +// Copyright 2025 The Bazel Authors. All rights reserved. +// +// 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 + +int main() { +#ifdef __cplusplus + std::cout << "__cplusplus: " << __cplusplus << std::endl; +#else + std::cout << "No stdcxx identifier detected." << std::endl; +#endif + return 0; +} From 138c67ca7fdbf79a13fbdafec3c6be718ef17400 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 13 Oct 2025 13:55:13 -0700 Subject: [PATCH 2/3] Improved unit tests --- tests/cxx_std_macro/BUILD | 15 +-- tests/cxx_std_macro/cxx_std_test.bzl | 164 +++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 tests/cxx_std_macro/cxx_std_test.bzl diff --git a/tests/cxx_std_macro/BUILD b/tests/cxx_std_macro/BUILD index f7c707a6..774a5be6 100644 --- a/tests/cxx_std_macro/BUILD +++ b/tests/cxx_std_macro/BUILD @@ -11,19 +11,8 @@ # 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("//cc:cc_binary.bzl", "cc_binary") -load("//cc/cxx_standard:cxx_standard.bzl", "cxxopts") +load(":cxx_std_test.bzl", "cxx_std_test_suite") licenses(["notice"]) -cc_binary( - name = "main", - srcs = ["main.cc"], - copts = cxxopts(default = "17"), -) - -cc_binary( - name = "main_no_default", - srcs = ["main.cc"], - copts = cxxopts(), -) +cxx_std_test_suite(name = "cxx_std_test_suite") diff --git a/tests/cxx_std_macro/cxx_std_test.bzl b/tests/cxx_std_macro/cxx_std_test.bzl new file mode 100644 index 00000000..eac3549f --- /dev/null +++ b/tests/cxx_std_macro/cxx_std_test.bzl @@ -0,0 +1,164 @@ +"""Unit tests for cxx_standard flags.""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("//cc:cc_binary.bzl", "cc_binary") +load("//cc/cxx_standard:cxx_standard.bzl", "cxxopts") + +def _get_compile_action(target): + """Get the C++ compile action from a target.""" + for action in target.actions: + if action.mnemonic == "CppCompile": + return action + fail("No CppCompile action found") + +def _extract_flags_between_markers(argv): + """Extract flags between BEGIN and END markers.""" + begin_marker = "-DRULES_CC_STD_CXX_TEST_BEGIN" + end_marker = "-DRULES_CC_STD_CXX_TEST_END" + + begin_idx = -1 + end_idx = -1 + + for i, arg in enumerate(argv): + if arg == begin_marker: + begin_idx = i + elif arg == end_marker: + end_idx = i + break + + if begin_idx == -1 or end_idx == -1: + return [] + + return argv[begin_idx + 1:end_idx] + +def _cxx_flag_test_common_impl(ctx, version, inclusive): + env = analysistest.begin(ctx) + tut = analysistest.target_under_test(env) + action = _get_compile_action(tut) + + flags_between_markers = _extract_flags_between_markers(action.argv) + expected_flag = "-std=c++{}".format(version) + + if inclusive: + asserts.true( + env, + expected_flag in flags_between_markers, + "Expected flag '{}' between markers, got: {}".format(expected_flag, flags_between_markers), + ) + else: + asserts.true( + env, + expected_flag not in flags_between_markers, + "Unexpected flag '{}' between markers, got: {}".format(expected_flag, flags_between_markers), + ) + return analysistest.end(env) + +def _cxx_default_flag_test_impl(ctx): + return _cxx_flag_test_common_impl(ctx, ctx.attr.version, True) + +def _cxx_no_flag_test_impl(ctx): + return _cxx_flag_test_common_impl(ctx, ctx.attr.version, False) + +cxx_default_flag_test = analysistest.make( + _cxx_default_flag_test_impl, + doc = "A test that confirms the default provided to the `cxxopts` macro is used for compilation.", + attrs = { + "version": attr.string(doc = "The cxx version", mandatory = True), + }, + config_settings = { + str(Label("//cc/cxx_standard:cxx_standard")): "default", + }, +) + +cxx_no_flag_test = analysistest.make( + _cxx_no_flag_test_impl, + doc = "A test that force disables flags from the `cxxopts` macro regardless of any default specified.", + attrs = { + "version": attr.string(doc = "The cxx version", mandatory = True), + }, + config_settings = { + str(Label("//cc/cxx_standard:cxx_standard")): "none", + }, +) + +def _cxx_forced_17_flag_test_impl(ctx): + return _cxx_flag_test_common_impl(ctx, "17", True) + +cxx_forced_17_flag_test = analysistest.make( + _cxx_forced_17_flag_test_impl, + doc = "A test that forces `cxx17` regardless of any default specified to the `cxxopts` macro.", + config_settings = { + str(Label("//cc/cxx_standard:cxx_standard")): "17", + }, +) + +def _cxx_forced_20_flag_test_impl(ctx): + return _cxx_flag_test_common_impl(ctx, "20", True) + +cxx_forced_20_flag_test = analysistest.make( + _cxx_forced_20_flag_test_impl, + doc = "A test that forces `cxx20` regardless of any default specified to the `cxxopts` macro.", + config_settings = { + str(Label("//cc/cxx_standard:cxx_standard")): "20", + }, +) + +def _cxx_std_test(): + """Helper function to create test targets.""" + tests = [] + for version in ["98", "03", "11", "14", "17", "20", "23", "26", "2c"]: + target_under_test = "test_bin_{}".format(version) + cc_binary( + name = target_under_test, + srcs = ["main.cc"], + # To identify the specific instance of the macros stdcxx flag, the arguments are wrapped + # in some identifier flags so we can know when the flag is or isn't acutally produced. + copts = ["-DRULES_CC_STD_CXX_TEST_BEGIN"] + cxxopts(default = version) + ["-DRULES_CC_STD_CXX_TEST_END"], + ) + + test_name = "cxx{}_default_flag_test".format(version) + tests.append(test_name) + cxx_default_flag_test( + name = test_name, + target_under_test = ":" + target_under_test, + version = version, + ) + + test_name = "cxx{}_no_flag_test".format(version) + tests.append(test_name) + cxx_no_flag_test( + name = test_name, + target_under_test = ":" + target_under_test, + version = version, + ) + + test_name = "cxx{}_forced_17_flag_test".format(version) + tests.append(test_name) + cxx_forced_17_flag_test( + name = test_name, + target_under_test = ":" + target_under_test, + ) + + test_name = "cxx{}_forced_20_flag_test".format(version) + tests.append(test_name) + cxx_forced_20_flag_test( + name = test_name, + target_under_test = ":" + target_under_test, + ) + + return tests + +def cxx_std_test_suite(name, **kwargs): + """Entry-point macro called from the BUILD file. + + Args: + name: Name of the macro. + **kwargs: Additional keyword arguments. + """ + tests = _cxx_std_test() + + native.test_suite( + name = name, + tests = tests, + **kwargs + ) From d437f677960c919df43b1b94435d34d599205222 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 13 Oct 2025 14:03:43 -0700 Subject: [PATCH 3/3] Consolidate tests and fix msvc --- tests/cxx_std_macro/cxx_std_test.bzl | 60 ++++++++++++++-------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/cxx_std_macro/cxx_std_test.bzl b/tests/cxx_std_macro/cxx_std_test.bzl index eac3549f..0063451f 100644 --- a/tests/cxx_std_macro/cxx_std_test.bzl +++ b/tests/cxx_std_macro/cxx_std_test.bzl @@ -37,7 +37,11 @@ def _cxx_flag_test_common_impl(ctx, version, inclusive): action = _get_compile_action(tut) flags_between_markers = _extract_flags_between_markers(action.argv) - expected_flag = "-std=c++{}".format(version) + use_msvc = ctx.attr.use_msvc + if use_msvc: + expected_flag = "/std:c++{}".format(version) + else: + expected_flag = "-std=c++{}".format(version) if inclusive: asserts.true( @@ -59,12 +63,16 @@ def _cxx_default_flag_test_impl(ctx): def _cxx_no_flag_test_impl(ctx): return _cxx_flag_test_common_impl(ctx, ctx.attr.version, False) +_COMMON_ATTR = { + "use_msvc": attr.bool(doc = "Whether to use MSVC-style flags (/std:c++) instead of GCC/Clang style (-std=c++)", mandatory = True), +} + cxx_default_flag_test = analysistest.make( _cxx_default_flag_test_impl, doc = "A test that confirms the default provided to the `cxxopts` macro is used for compilation.", attrs = { "version": attr.string(doc = "The cxx version", mandatory = True), - }, + } | _COMMON_ATTR, config_settings = { str(Label("//cc/cxx_standard:cxx_standard")): "default", }, @@ -75,38 +83,34 @@ cxx_no_flag_test = analysistest.make( doc = "A test that force disables flags from the `cxxopts` macro regardless of any default specified.", attrs = { "version": attr.string(doc = "The cxx version", mandatory = True), - }, + } | _COMMON_ATTR, config_settings = { str(Label("//cc/cxx_standard:cxx_standard")): "none", }, ) -def _cxx_forced_17_flag_test_impl(ctx): - return _cxx_flag_test_common_impl(ctx, "17", True) - -cxx_forced_17_flag_test = analysistest.make( - _cxx_forced_17_flag_test_impl, - doc = "A test that forces `cxx17` regardless of any default specified to the `cxxopts` macro.", - config_settings = { - str(Label("//cc/cxx_standard:cxx_standard")): "17", - }, -) - -def _cxx_forced_20_flag_test_impl(ctx): - return _cxx_flag_test_common_impl(ctx, "20", True) +def _cxx_forced_11_flag_test_impl(ctx): + return _cxx_flag_test_common_impl(ctx, "11", True) -cxx_forced_20_flag_test = analysistest.make( - _cxx_forced_20_flag_test_impl, - doc = "A test that forces `cxx20` regardless of any default specified to the `cxxopts` macro.", +cxx_forced_11_flag_test = analysistest.make( + _cxx_forced_11_flag_test_impl, + doc = "A test that forces `cxx11` regardless of any default specified to the `cxxopts` macro.", + attrs = _COMMON_ATTR, config_settings = { - str(Label("//cc/cxx_standard:cxx_standard")): "20", + str(Label("//cc/cxx_standard:cxx_standard")): "11", }, ) def _cxx_std_test(): """Helper function to create test targets.""" tests = [] - for version in ["98", "03", "11", "14", "17", "20", "23", "26", "2c"]: + + use_msvc = select({ + "//cc/compiler:msvc-cl": True, + "//conditions:default": False, + }) + + for version in ["14", "17"]: target_under_test = "test_bin_{}".format(version) cc_binary( name = target_under_test, @@ -122,6 +126,7 @@ def _cxx_std_test(): name = test_name, target_under_test = ":" + target_under_test, version = version, + use_msvc = use_msvc, ) test_name = "cxx{}_no_flag_test".format(version) @@ -130,20 +135,15 @@ def _cxx_std_test(): name = test_name, target_under_test = ":" + target_under_test, version = version, + use_msvc = use_msvc, ) - test_name = "cxx{}_forced_17_flag_test".format(version) - tests.append(test_name) - cxx_forced_17_flag_test( - name = test_name, - target_under_test = ":" + target_under_test, - ) - - test_name = "cxx{}_forced_20_flag_test".format(version) + test_name = "cxx{}_forced_11_flag_test".format(version) tests.append(test_name) - cxx_forced_20_flag_test( + cxx_forced_11_flag_test( name = test_name, target_under_test = ":" + target_under_test, + use_msvc = use_msvc, ) return tests