A Bazel C++ toolchain for building shared libraries and dependent binaries which conform to typical Linux linking practices.
This Bazel workspace defines //:simple_x86_64_linux_toolchain. This
toolchain can be used with the standard C++ rules. As the default C++ toolchain
produces libraries and executables which do not conform to typical linking
practices (see, for example, this issue),
//:simple_x86_64_linux_toolchain was developed to have the
following features:
- Position-independent code (PIC) is only used for shared libraries and PIC archives by default.
- Executables and shared libraries are linked against their direct shared
library dependencies.
- The
DT_NEEDEDfields of these outputs hold shared library Bazel target names by default. These names are not mangled in some fashion. - The toolchain allows a shared library to be defined with a soname using the
linker option
-Wl,-soname=<library soname>. - Executables and shared libraries are not linked against shared libraries which are part of the implementation details of Bazel.
- Mechanisms such as
DT_RUNPATHare not used in executables and shared libraries by default.
- The
- A library must be exactly one of: shared library, PIC archive, or archive.
- Targets which are defined to use the toolchain can depend on targets which should not be built with the toolchain.
- The toolchain is configured for x86-64 and Linux. It uses
g++. - The current include directories assume
g++version 9. They can be found in//:toolchain_definition.bzlin listgcc_system_include_directories.
This toolchain is intended to be a starting point in the definition of C++ toolchains which conform to typical Linux linking practices. It is not intended to be a drop-in replacement for the default C++ toolchain. As it is, it does not support all C++ rule attribute semantics. The toolchain is currently used by as_components for relatively simple build actions.
The -s flag for bazel build can be used to print build command lines. These
can then be inspected to ensure that a particular target is built correctly by
the toolchain.
- The toolchain does not vary compilation and linking options for the standard
Bazel compilation modes:
dbg,fastbuild, andopt. If such behavior is desired, it can be implemented without modifying the toolchain by specifying the desired compilation mode and using instances of the--coptand--linkoptcommand line build options.
The toolchain is configured to be used with platforms.
Toolchain resolution which uses platforms only occurs for C++ rules when the
following build flag is enabled:
--incompatible_enable_cc_toolchain_resolution
When platforms are used, the definition of a cc_toolchain_suite target is not
necessary.
Using the toolchain without modification requires a few steps:
- This repository is registered as an external repository of the local
repository. For example,
local_repositorymay be used if this repository was cloned locally. - The execution platforms and toolchain are registered in the local
WORKSPACEfile as follows (assuming that the external repository is accessed with@simple_bazel_cpp_toolchain):The use ofregister_execution_platforms( "@local_config_platform//:host", "@simple_bazel_cpp_toolchain//:simple_cpp_x86_64_linux_platform" ) register_toolchains( "@simple_bazel_cpp_toolchain//:simple_x86_64_linux_toolchain" )@local_config_platform//:hostassumes that remote execution is not performed and that the host is an x86-64 Linux system. Also, as described above, use of the toolchain requiresg++and the presence of the assumed system include directories. - Targets are defined using the rules given in the target definition section below.
- The build flag
--incompatible_enable_cc_toolchain_resolutionis used. For example, a.bazelrcfile may be used to provide this flag.
The toolchain allows the definition of five kinds of conceptual targets.
- Shared library
- Executable
- Test executable
- PIC archive
- Archive
Explanations of the target structure, Bazel attribute values, and features which are necessary to define targets of these types follow. Also given are explanations of:
- how to depend on shared libraries which are produced by the toolchain
- how to depend on internal and external targets in
cc_testtargets
- Any target which uses the toolchain and which requires compilation or linking
must be instantiated with the constraint value
//nonstandard_toolchain:simple_cpp_toolchainin itsexec_compatible_withattribute argument list.
A shared library target is meant to represent a shared library which is linked to its direct dependencies and which relies on the proper installation of these dependencies.
Configuration:
- Rule type: Two targets are defined for each shared library.
- A
cc_librarytarget is defined for the shared library header. - A
cc_binarytarget is defined for the shared library.
- A
- Target names: For shared library
foo:- Header (
cc_library): A conventional pattern such asfoo_headeris suggested. - Binary (
cc_binary):libfoo.soor a versioned variant such aslibfoo.so.1.0.0
- Header (
- Dependencies (
deps): Thecc_libraryheader target. - Soname (optional): For example, a soname linker option such as
-Wl,-soname=libfoo.so.1may be included in thelinkoptsattribute of thecc_binarytarget. - Feature:
"interpret_as_shared_library"(for thecc_binarytarget)
Example:
cc_library(
name = "foo_header",
deps = [],
srcs = [],
hdrs = ["foo.h"]
)
cc_binary(
name = "libfoo.so.1.0.0",
deps = [":foo_header"],
srcs = ["foo.cc"],
linkopts = ["-Wl,-soname=libfoo.so.1"],
features = ["interpret_as_shared_library"],
exec_compatible_with = ["//nonstandard_toolchain:simple_cpp_toolchain"]
)
Any target which depends on a shared library which was defined in the way
described above must include the shared library header target in deps and the
shared library binary target in srcs.
Tests which depend on a versioned shared library which uses a major version
soname must have a target which defines a major version symbolic link to the
shared library in their data attribute.
Examples:
cc_binary(
name = "bar_on_foo",
deps = [":foo_header"],
srcs = [
"bar.cc",
":libfoo.so.1.0.0"
],
features = ["interpret_as_executable"],
exec_compatible_with = ["//nonstandard_toolchain:simple_cpp_toolchain"]
)
genrule(
name = "libfoo.so.1.0.0_soname_symlink",
srcs = [":libfoo.so.1.0.0"],
outs = ["libfoo.so.1"],
# Make variable substitution is used to access the output path.
cmd_bash = "ln -s libfoo.so.1.0.0 $@"
)
cc_test(
name = "test_on_foo",
deps = [":foo_header"],
srcs = [
"test_on_foo.cc",
":libfoo.so.1.0.0"
],
data = [":libfoo.so.1.0.0_soname_symlink"],
features = ["interpret_as_test_executable"],
exec_compatible_with = ["//nonstandard_toolchain:simple_cpp_toolchain"]
)
An executable target is meant to represent an executable program that can be executed independently of a Bazel installation provided that its shared library dependencies are properly installed.
Configuration:
- Rule type:
cc_binary - Feature:
"interpret_as_executable"
Example:
cc_binary(
name = "executable_target",
deps = [],
srcs = ["executable_target.cc"],
features = ["interpret_as_executable"],
exec_compatible_with = ["//nonstandard_toolchain:simple_cpp_toolchain"]
)
A test executable target is meant to represent a test which is executed by Bazel. It is assumed that the test must be able to execute when its shared library dependencies have not been installed.
Configuration:
- Rule type:
cc_test - Shared library dependencies: indirect shared library dependencies are made
available with
LD_LIBRARY_PATHas explained below. - feature:
"interpret_as_test_executable"
Example without indirect shared library dependencies:
cc_test(
name = "test_on_so",
deps = [":so_header"],
srcs = [
"test_on_so.cc",
":libso.so"
],
features = ["interpret_as_test_executable"],
exec_compatible_with = ["//nonstandard_toolchain:simple_cpp_toolchain"]
)
Tests can access their direct shared library dependencies without additional
configuration. Indirect shared library dependencies must be exposed using
the LD_LIBRARY_PATH environment variable of the Linux dynamic linker. The
ORIGIN token is expanded in the value of LD_LIBRARY_PATH to the directory
which contains the test executable. This can be used to describe the
directories which contain the indirect shared library dependencies of the test.
Notes on using ORIGIN for this purpose:
- A test which is defined in a package beneath the workspace root can refer
to targets above its package using the parent directory special symbol
... - Bazel stores external targets within the directory
externalwhich is located at the workspace root. The repository name as defined bylocal_repositoryis used as the root of the repository underexternal. The directory structure within a repository directory is the same as the structure of the external repository. - A colon-separated list of directories is accepted for the value of
LD_LIBRARY_PATH.
Example with indirect shared library dependencies:
libspam.sois located at the workspace root.libcram.sois located at the workspace root and is a dependency oflibspam.so.libham.sois located in packagepackage1under the workspace root and is a dependency oflibspam.so.libeggs.sois located in the external repositoryexternal_eggsat its workspace root. It is a dependency oflibspam.so.
cc_library(
name = "spam_header",
deps = [],
srcs = [],
hdrs = ["spam.h"]
)
cc_binary(
name = "libspam.so",
deps = [
":spam_header",
":cram_header",
"//package1:ham_header",
"@external_eggs//:eggs_header"
],
srcs = [
"spam.cc",
":libcram.so",
"//package1:libham.so",
"@external_eggs//:libeggs.so"
],
features = ["interpret_as_shared_library"],
exec_compatible_with = ["//nonstandard_toolchain:simple_cpp_toolchain"]
)
cc_test(
name = "test_with_indirect_so_deps",
deps = [":spam_header"],
srcs = [
"test_with_indirect_so_deps.cc",
":libspam.so"
],
# Use $$ to escape $ and prevent Make variable substitution.
env = {
"LD_LIBRARY_PATH":
"$${ORIGIN}:$${ORIGIN}/package1:$${ORIGIN}/external/external_eggs"
},
features = ["interpret_as_test_executable"],
exec_compatible_with = ["//nonstandard_toolchain:simple_cpp_toolchain"]
)
Archive targets are intended to represent archives whose sources are not also directly compiled to shared object files or executables. This assumption separates library targets into two classes: those which define shared libraries and those which define static libraries.
PIC archives and archives are defined in the usual way with two additions:
- The
linkstaticattribute is set toTrueto ensure that a shared library is not generated. - The appropriate feature, either
"interpret_as_pic_archive"or"interpret_as_archive", is used.
Examples:
cc_library(
name = "pic_archive",
deps = [],
srcs = ["pic_archive.cc"],
hdrs = ["pic_archive.h"],
linkstatic = True,
features = ["interpret_as_pic_archive"],
exec_compatible_with = ["//nonstandard_toolchain:simple_cpp_toolchain"]
)
cc_library(
name = "archive",
deps = [],
srcs = ["archive.cc"],
hdrs = ["archive.h"],
linkstatic = True,
features = ["interpret_as_archive"],
exec_compatible_with = ["//nonstandard_toolchain:simple_cpp_toolchain"]
)
Trial targets are defined which possess various dependency relationships on the three kinds of targets which can be depended upon (shared library, PIC archive, and archive). For example, in addition to an executable which depends on a shared library and a test which depends on a shared library, a test which depends on a shared library which depends on a shared library is defined.
The command lines which are generated by the toolchain for these targets can
be inspected by using the -s flag when executing bazel build or
bazel test. For example, bazel build -s ... should succeed and print the
command lines which were used for each trial target.
Several test executables are defined to allow validation of shared library
loading. An execution of bazel test ... should pass.
This section provides a brief explanation of how to define a
C++ toolchain which uses platforms. The main goal of this process is
instantiating a toolchain target and registering it in the workspace. This
section may be useful if modifications to this toolchain are desired.
Instantiating toolchain for C++ toolchains requires several steps.
A rule must be defined which provides a CcToolchainConfigInfo provider
instance. This is done by having the implementation function of the rule return
the result of invoking cc_common.create_cc_toolchain_config_info with
object arguments with provide the logic of the toolchain. Defining these
objects is the major activity of defining a toolchain. For example, compilation
and linking command lines are generated by Bazel with the information provided
to create_cc_toolchain_config_info in these objects. This toolchain
definition uses action_config instead of tool_path as tool_path is
deprecated.
The rule is then instantiated in a BUILD file. The target created by
instantiating the rule is used in the instantiation of a cc_toolchain target.
Finally, the cc_toolchain target is used in the instantiation of a
toolchain target. To use this non-standard toolchain, the toolchain target
must include //nonstandard_toolchain:simple_cpp_toolchain in the list
argument of its exec_compatible_with attribute.
The following list gives a step-by-step summary of this process for this toolchain after the custom rule definition step.
Steps:
-
In a
BUILDfile, the rulecc_toolchain_config_info_generatoris loaded from//:toolchain_definition.bzl. This rule provides an appropriate provider instance for thetoolchain_configattribute ofcc_toolchain. -
The rule is instantiated to define a toolchain configuration target. The name of this target conventionally ends with
_config. -
A
cc_toolchaintarget is instantiated with the appropriate attribute values. The value of thetoolchain_configattribute is set to the config target which was instantiated in the previous step. -
A
toolchaintarget is instantiated with:- The appropriate
exec_compatible_withandtarget_compatible_withconstraint values.- For this non-standard toolchain,
exec_compatible_withmust include//nonstandard_toolchain:simple_cpp_toolchain.
- For this non-standard toolchain,
- As the value of the
toolchainattribute, thecc_toolchaintarget which was instantiated in the previous step. - The
toolchain_typeattribute value@rules_cc//cc:toolchain_type.
The name of the
toolchaintarget conventionally end with_toolchain. This target provides the information which is needed by the general toolchain mechanism of Bazel to perform platform-based toolchain resolution on this toolchain. - The appropriate
-
The execution platforms are registered as described above in "Simple description of use".
-
The
toolchaintarget is registered in theWORKSPACEfile withregister_toolchains. -
Build and test actions use the
--incompatible_enable_cc_toolchain_resolutionflag. This flag can be provided on the command line or through the use of a.bazelrcfile.
The BUILD and WORKSPACE files of this repository serve as examples for this
process.