Skip to content

Autoconfigure the C++ toolchain on the BSDs - #862

Open
zakinko wants to merge 1 commit into
bazelbuild:mainfrom
zakinko:bsd-link-libm
Open

zakinko wants to merge 1 commit into
bazelbuild:mainfrom
zakinko:bsd-link-libm

Conversation

@zakinko

@zakinko zakinko commented Sep 1, 2026

Copy link
Copy Markdown

This replaces the -lm change, which #859 has since covered -- thanks for
taking it.

FreeBSD and OpenBSD have been routed to a static toolchain since 2016, one
that writes in the compiler path, the builtin include directories and the
link libraries, while every other Unix gets those probed. NetBSD and
DragonFly never got a name and fell through to the autoconfigured path by
accident, which works apart from the toolchain being called "k8". Building
Bazel on any of the four has meant passing things in from outside: the
include directories on the static side are those of the release they were
taken from, and the link libraries are #857 and #161.

This runs the autoconfiguration on all four. It needed two things it did
not know: to find the C driver when there is no gcc, since the BSDs that
ship clang have only cc; and to name the BSD libcs for //cc/libc, which
stopped at NetBSD. The link libraries did not need a change, which I had
not expected: clang on FreeBSD and OpenBSD maps -lstdc++ to the platform's
C++ runtime, -lc++ on FreeBSD and -lc++ -lc++abi -lpthread on OpenBSD
(cc -### shows the substitution), so the autoconfigured default holds. The
static toolchain wrote -lc++ in directly, which the driver leaves alone,
and that is what #857 ran into. The static toolchain stays behind
BAZEL_USE_LEGACY_BSD_TOOLCHAIN=1, the way the legacy macOS toolchain does.

Measured by building Bazel from source (948b8c70) with rules_cc overridden
to this branch and nothing passed in for the toolchain -- no CC, no -lm,
no -std, no link libraries:

FreeBSD 15.1     cc (clang 19)   libc freebsd     6,687 actions
GhostBSD 26.1    cc (clang 19)   libc freebsd     6,687
OpenBSD 7.9      cc (clang 19)   libc openbsd     6,687
NetBSD 11.0      gcc 12.5        libc netbsd      6,308
DragonFly 6.4    gcc 8.3         libc dragonfly   5,575

Each resulting bazel then ran a genrule in a fresh workspace. Two flags
were still passed, neither about the toolchain: --copt=-include
sys/cdefs.h, because protobuf #defines _POSIX_C_SOURCE in a source file
and hides isascii from libc++'s ; and --features=-layering_check
on the clang BSDs, because the autoconfigured toolchain gives clang a
module map, the check becomes active where the static toolchain had none,
and grpc's wait_for_single_owner.h does not declare its absl dependency.
Users on FreeBSD and OpenBSD will see layering_check start to apply, the
same as on Linux with clang.

The builds used a platforms with the NetBSD and DragonFly host detection
from bazelbuild/platforms#142 and #143 applied; I have not tried without.

@keith

keith commented Sep 1, 2026

Copy link
Copy Markdown
Member

I've got this one in #859

@zakinko zakinko changed the title bsd: link libm Autoconfigure the C++ toolchain on the BSDs Sep 9, 2026
@zakinko

zakinko commented Sep 9, 2026

Copy link
Copy Markdown
Author

Thanks for taking the -lm part in #859. I have force-pushed this branch
with what #859 does not cover: the autoconfigured toolchain on the BSDs,
so that FreeBSD, OpenBSD, NetBSD and DragonFly stop needing anything
passed in from outside. One commit; the description says what was
measured where. Please let me know if this is not the shape you had in
mind, I am happy to rework it.

return "netbsd"
if os_name.find("dragonfly") != -1:
# The JVM reports "dragonflybsd"; uname and GOOS say "dragonfly".
return "dragonfly"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i think it would be nicer to have bsd in the name still, but if that causes other issues it's probably fine not to

Comment thread cc/private/toolchain/cc_configure.bzl Outdated
"%{cpu}": get_cpu_value(repository_ctx),
})
elif cpu_value == "freebsd" or cpu_value == "openbsd":
elif cpu_value in ["freebsd", "openbsd"] and _use_legacy_bsd_toolchain(repository_ctx):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
elif cpu_value in ["freebsd", "openbsd"] and _use_legacy_bsd_toolchain(repository_ctx):
elif cpu_value in ("freebsd", "openbsd") and _use_legacy_bsd_toolchain(repository_ctx):

Comment thread cc/private/toolchain/cc_configure.bzl Outdated
"%{cpu}": get_cpu_value(repository_ctx),
})
elif cpu_value == "freebsd" or cpu_value == "openbsd":
elif cpu_value in ["freebsd", "openbsd"] and _use_legacy_bsd_toolchain(repository_ctx):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should this contain netbsd and others too? or are we just making those to never use the legacy toolchain?

fail("FreeBSD / OpenBSD should use bsd_cc_toolchain_config.bzl")

cc = _find_generic(repository_ctx, "gcc", "CC", overridden_tools)
cc = _find_generic(repository_ctx, "gcc", "CC", overridden_tools, warn = True, silent = True)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should we be worried that this logic would select gcc on bsd if they had that installed alongside the default? maybe instead we should not make the logic so generic and just only look for cc on bsd based on cpu_value?

@c2qd

c2qd commented Sep 15, 2026

Copy link
Copy Markdown

This PR effectively drops the fix from #855, causing #854 to regress on OpenBSD. The issue can be reproduced using the code provided in #854.

When BAZEL_USE_LEGACY_BSD_TOOLCHAIN=1 is set, it works as expected, so shouldn't -z origin also be added to the autoconfigured toolchain?

@c2qd

c2qd commented Sep 15, 2026

Copy link
Copy Markdown

The link libraries did not need a change, which I had
not expected: clang on FreeBSD and OpenBSD maps -lstdc++ to the platform's
C++ runtime, -lc++ on FreeBSD and -lc++ -lc++abi -lpthread on OpenBSD
(cc -### shows the substitution), so the autoconfigured default holds.

Really? I tried it with a simple piece of code, but libpthread is missing (On OpenBSD).
This regresses #857.

Click to expand the log
$ cat main.cpp
#include 
int main() { std::cout << "Hello, world\n"; }
$ bazel9 run //:main --override_module=rules_cc=/tmp/tmp.K3Vs2Rfb0N/rules_cc
Starting local Bazel server (9.2.0) and connecting to it...
DEBUG: /home/cx/.cache/bazel/_bazel_cx/cc48eb4a457512a8f6a8aa08453146ee/external/rules_python+/python/private/repo_utils.bzl:83:16:
rules_python:python WARNING: No host compatible runtime found compatible with version 3.11
INFO: Analyzed target //:main (93 packages loaded, 545 targets configured).
INFO: Found 1 target...
Target //:main up-to-date:
  bazel-bin/main
INFO: Elapsed time: 15.597s, Critical Path: 2.43s
INFO: 11 processes: 9 internal, 2 processwrapper-sandbox.
INFO: Build completed successfully, 11 total actions
INFO: Running command line: bazel-bin/main
main:/usr/lib/libc++abi.so.10.0: undefined symbol 'pthread_rwlock_wrlock'
main:/usr/lib/libc++abi.so.10.0: undefined symbol 'pthread_rwlock_unlock'
main:/usr/lib/libc++abi.so.10.0: undefined symbol 'pthread_rwlock_rdlock'
main:/usr/lib/libc++.so.13.0: undefined symbol 'pthread_mutexattr_init'
main:/usr/lib/libc++.so.13.0: undefined symbol 'pthread_mutexattr_settype'
main:/usr/lib/libc++.so.13.0: undefined symbol 'pthread_mutexattr_destroy'
main:/usr/lib/libc++.so.13.0: undefined symbol 'pthread_join'
main:/usr/lib/libc++.so.13.0: undefined symbol 'pthread_detach'
Hello, world

$ objdump -p bazel-bin/main | grep NEEDED
NEEDED libc++.so.13.0
NEEDED libc++abi.so.10.0
NEEDED libc.so.104.0


Update:
The cc -### check only shows that the compiler driver expands -lstdc++ to -lc++ -lc++abi -lpthread. It does not show that those libraries survive the actual Bazel link.

In the Bazel link action, -lstdc++ is passed under --as-needed:

-Wl,--push-state,-as-needed
-lstdc++
-Wl,--pop-state

The compiler driver therefore emits -lpthread inside that --as-needed scope, and OpenBSD's linker drops it. The resulting binary has libc++ and libc++abi as NEEDED entries, but not libpthread, and fails at runtime with unresolved pthread symbols.

So I don't think cc -### alone is sufficient to establish that the autoconfigured toolchain preserves the behavior required by #857.

As an additional note, some OpenBSD ports disable --as-needed because it causes problems on OpenBSD.
For example, the Fcitx5 port explicitly avoids adding -Wl,--as-needed on OpenBSD: https://cvsweb.openbsd.org/checkout/ports/inputmethods/fcitx/patches/patch-cmake_Fcitx5CompilerSettings_cmake?rev=1.2


Update 2: I tried disabling --as-needed as a quick test, and the missing libpthread issue no longer occurs.

Quick patch
diff --git a/cc/private/toolchain/unix_cc_configure.bzl b/cc/private/toolchain/unix_cc_configure.bzl
index 85f4551..829f94e 100644
--- a/cc/private/toolchain/unix_cc_configure.bzl
+++ b/cc/private/toolchain/unix_cc_configure.bzl
@@ -591,7 +591,7 @@ def configure_unix_toolchain(repository_ctx, cpu_value, overridden_tools):
         bazel_default_libs = ["-lc++", "-lm"]
     else:
         bazel_default_libs = ["-lstdc++", "-lm"]
-    if is_as_needed_supported and is_push_state_supported:
+    if is_as_needed_supported and is_push_state_supported and cpu_value != "openbsd":
         # Do not link against C++ standard libraries unless they are actually
         # used.
         # We assume that --push-state support implies --pop-state support.

@zakinko

zakinko commented Sep 16, 2026

Copy link
Copy Markdown
Author

@c2qd The objdump was the thing -- my evidence was the wrong kind. I had
written that the link libraries needed no change, on the strength of cc -###
substituting the platform's C++ runtime for -lstdc++. That prints what the
driver passes to the linker, not what survives the link, and the difference is
exactly where libpthread goes missing. I have taken your patch, with the
mechanism written into the comment.

-z origin is back too. The static toolchain carried it as part of
-Wl,-z,relro,-z,now,-z,origin (#855) and the autoconfigured one adds only relro
and now, so routing OpenBSD through autoconfiguration dropped it. It is probed
separately from relro/now rather than appended to that string, so a linker that
does not take -z origin keeps the other two.

Measured on OpenBSD 7.9 in CI this time rather than by hand, since both are
runtime failures a successful build hides:

https://github.com/zakinko/NetBSD-i386/actions/runs/35099145057

hello
  NEEDED   libc++.so.12.0  libc++abi.so.9.0  libpthread.so.28.1
           libm.so.10.1  libc.so.103.0
  runs, and stderr carries no undefined symbol

main, linkstatic = False
  RUNPATH  $ORIGIN/_solib_openbsd/:$ORIGIN/main.runfiles/_main/_solib_openbsd/
  NEEDED   liblibfunc.so
  runs

Neither check stops at "it ran". Your log has the binary printing Hello, world
after the undefined-symbol lines, so a zero exit says nothing on its own. The
$ORIGIN one needed the same care in the other direction: if the link had gone
static there would be no $ORIGIN and nothing resolved through it, and the job
would have passed while measuring nothing, so it asserts the RUNPATH before it
runs anything.

@c2qd

c2qd commented Sep 16, 2026

Copy link
Copy Markdown

@zakinko I tried it locally, and it looks like all the issues I pointed out have been fixed. Thanks for the work~

@zakinko

zakinko commented Sep 16, 2026

Copy link
Copy Markdown
Author

Took the tuple, and narrowed the compiler search the way you suggest.

FreeBSD and OpenBSD have no gcc in base, so the only gcc on PATH is one
someone installed from ports or pkgsrc, and the old order would have
picked it over the base clang. The search now asks for cc by name when
cpu_value is one of the four BSDs. That does not change which compiler
is chosen anywhere: NetBSD and DragonFly carry both names in base for the
same one.

NetBSD 11.0/amd64   cc --version  ->  cc (nb3 20260326) 12.5.0
                    gcc --version ->  gcc (nb3 20260326) 12.5.0
                    https://github.com/zakinko/NetBSD-i386/actions/runs/35111604047
DragonFly 6.4.2     cc --version  ->  cc 8.3 [DragonFly] Release/2019-02-22
                    gcc --version ->  gcc 8.3 [DragonFly] Release/2019-02-22
                    https://github.com/zakinko/NetBSD-i386/actions/runs/35111590458

CC and overridden_tools still take precedence, since _find_generic
consults both before it looks at PATH. The generated toolchain is
printed and asserted on every CI run now, rather than measured once by
hand, since the branch moved twice after I first checked it:

https://github.com/zakinko/NetBSD-i386/actions/runs/35099145057

  freebsd     clang   /usr/bin/cc
  openbsd     clang   /usr/bin/cc
  netbsd      gcc     /usr/bin/cc
  dragonfly   gcc     /usr/bin/cc
  ghostbsd    clang   /usr/bin/cc

Each job prints toolchain_identifier, compiler, target_libc, the driver,
the probed cxx_builtin_include_directories, link_flags and link_libs, and
fails if the driver is anything but .../cc -- so a ports gcc shadowing the
base compiler turns the run red rather than passing quietly.

netbsd and dragonfly are left out of the legacy branch on purpose, and it
is not only that BUILD.static.bsd has no entry for them. Writing them one
means inventing it, and none of the existing values carry over -- that
config names clang at /usr/bin/clang, /usr/bin/dwp, /usr/local/include and
-lc++. None of that is DragonFly 6.4.2. The dragonfly job of

https://github.com/zakinko/NetBSD-i386/actions/runs/35111590458

finds neither /usr/bin/clang nor /usr/bin/dwp, gets "ld.gold: error:
cannot find -lc++" for cc p.o -lc++ while -lstdc++ links, and the include
path the autoconfiguration probes there is

cxx_builtin_include_directories = ["/usr/libdata/gcc80",
"/usr/include", "/usr/include/c++/8.0", "/usr/include/c++/8.0/backward"]

Three of those four carry the base compiler's version, as does
/usr/lib/gcc80 where libstdc++ lives, and NetBSD has the same shape in
/usr/include/gcc-12. Hardcoding them gives a fallback that is correct on
one release of one platform and silently wrong on the next, with no BSD in
this repository's CI to catch it -- which is the shape of what #857 ran
into. The autoconfiguration derives those same paths correctly today, so
I would rather not add two more hardcoded toolchains in the change that
argues against the first two.

If what you want is symmetry, I would go the other way: once this has
been in a release and the autoconfigured path has held up on FreeBSD and
OpenBSD, drop BUILD.static.bsd and bsd_cc_toolchain_config.bzl and the
flag with them. Happy to send that as a follow-up whenever you think the
time is right.

On the name, I would lean towards keeping dragonfly. uname -s prints
DragonFly and GOOS is dragonfly; only the JVM's os.name says
DragonFlyBSD, which is why the match here is on a substring. It is also
what the constraint_value is called in bazelbuild/platforms#143. That
said it is a one-line change -- say the word and I will make it
dragonflybsd.

@zakinko

zakinko commented Sep 16, 2026

Copy link
Copy Markdown
Author

Thank you for trying it on a real OpenBSD -- that was the check I could not
do from the build alone. The two probes (NEEDED and the $ORIGIN RUNPATH) are
part of the CI job now, so a later revision of the branch can be measured the
same way rather than by hand.

FreeBSD and OpenBSD have been routed to a static toolchain since 2016, one
that writes in the compiler path, the builtin include directories and the
link libraries, while every other Unix gets those probed. NetBSD and
DragonFly never got a name at all and fell through to the autoconfigured
path by accident, which works apart from the toolchain being called "k8".

Run the autoconfiguration on all four. Four things it did not know how to
do there:

- Find the C driver, since the BSDs install it as cc and a gcc-first search
  would prefer one from ports or pkgsrc over the compiler the system was
  built with.
- Name the BSD libcs for //cc/libc, which stopped at NetBSD.
- Set DF_ORIGIN. Bazel puts $ORIGIN in the RPATH it generates and OpenBSD's
  ld.so only expands it when that flag is set; glibc and musl expand it
  either way, so the need is invisible on Linux. The static toolchain
  carried -z origin (bazelbuild#855, for bazelbuild#854) and the autoconfigured one did not.
- Leave the C++ standard libraries outside --as-needed on OpenBSD. The
  driver expands -lstdc++ to -lc++ -lc++abi -lpthread there, and libpthread
  is only reached through the other two, so wrapping the expansion drops it
  and the binary dies at startup on undefined pthread_* symbols (bazelbuild#857).

I had claimed the link libraries needed no change, on the strength of
cc -### showing the driver substituting the platform's C++ runtime for
-lstdc++. That was the wrong measurement: cc -### prints what the driver
passes to the linker, not what survives the link. @c2qd caught it with an
objdump of the NEEDED entries and a binary that failed to start.

The static toolchain stays behind BAZEL_USE_LEGACY_BSD_TOOLCHAIN=1, the
way the legacy macOS toolchain does. It is offered only for FreeBSD and
OpenBSD, which are the two BUILD.static.bsd describes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants