Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 8 additions & 29 deletions cc/compiler/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -42,39 +42,18 @@ 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"])

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
]
28 changes: 28 additions & 0 deletions cc/compiler/compilers.bzl
Original file line number Diff line number Diff line change
@@ -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",
]
53 changes: 53 additions & 0 deletions cc/cxx_standard/BUILD
Original file line number Diff line number Diff line change
@@ -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
]
78 changes: 78 additions & 0 deletions cc/cxx_standard/cxx_standard.bzl
Original file line number Diff line number Diff line change
@@ -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",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the backwards compatibility story here? Could we ever delete this constant once we have added it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the rules are semantically versioned I figured a value could be dropped from release to release but I also think keeping a large list here is low cost.

]

def _flag(version, compiler):
if compiler.startswith("msvc"):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be an exact match?

Copy link
Contributor Author

@UebelAndre UebelAndre May 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, no that also works. I'm no expert in the MSVC ecosystem, I just wanted to make sure the "windows-y" flags made it to the windows compilers so opted for the prefix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this what you're thinking?

Suggested change
if compiler.startswith("msvc"):
if compiler == "msvc-cl":

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use case for not specifying this parameter vs. not using cxxopts at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could still use the --@rules_cc//cc/cxx_standard flag to globally affect just this value. This is a slight convenience over explicitly specifying --copt or something as it would only affect targets that used the macro (as not all targets may be compatible with that version of C++). Though I would not be opposed to making default mandatory as it'd cleanup the awkward none value.


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": [],
})
18 changes: 18 additions & 0 deletions tests/cxx_std_macro/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 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(":cxx_std_test.bzl", "cxx_std_test_suite")

licenses(["notice"])

cxx_std_test_suite(name = "cxx_std_test_suite")
164 changes: 164 additions & 0 deletions tests/cxx_std_macro/cxx_std_test.bzl
Original file line number Diff line number Diff line change
@@ -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)
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(
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)

_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",
},
)

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),
} | _COMMON_ATTR,
config_settings = {
str(Label("//cc/cxx_standard:cxx_standard")): "none",
},
)

def _cxx_forced_11_flag_test_impl(ctx):
return _cxx_flag_test_common_impl(ctx, "11", True)

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")): "11",
},
)

def _cxx_std_test():
"""Helper function to create test targets."""
tests = []

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,
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,
use_msvc = use_msvc,
)

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,
use_msvc = use_msvc,
)

test_name = "cxx{}_forced_11_flag_test".format(version)
tests.append(test_name)
cxx_forced_11_flag_test(
name = test_name,
target_under_test = ":" + target_under_test,
use_msvc = use_msvc,
)

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
)
Loading