Skip to content
Building ETL with Bazel

Building ETL with Bazel

ETL provides first-class Bazel support, both for developing ETL itself and for consuming it as a dependency in your own projects.

Prerequisites

  • Bazelisk (recommended) or Bazel 7.0 or later (with Bzlmod support)

Bazelisk is a launcher that automatically downloads and runs the Bazel version specified in the .bazelversion file at the project root. This ensures all contributors use a consistent Bazel version. Simply install Bazelisk and use bazel as usual — it transparently delegates to the correct version.

Syntax Checks

To validate that every ETL header is well-formed and compiles on its own (equivalent to test/run-syntax-checks.sh for CMake):

bazel build //test/syntax_check:syntax_check

This compiles a set of minimal .t.cpp files, each of which includes a single ETL header, with strict warning flags enabled.

Cleaning Build Artifacts

To remove all build outputs and symlinks (bazel-bin, bazel-out, bazel-etl, etc.):

bazel clean

For a full cleanup including the external dependency cache:

bazel clean --expunge

Running Unit Tests

To run the full test suite:

bazel test //test:etl_tests

You can also pass standard Bazel flags:

# Run with verbose test output
bazel test //test:etl_tests --test_output=all

# Run tests matching a filter (UnitTest++ subset)
bazel test //test:etl_tests --test_arg=<suite_name>

Using ETL in Your Project

With Bzlmod (recommended, Bazel 7+)

Add ETL as a dependency in your project’s MODULE.bazel:

bazel_dep(name = "etl", version = "20.47.1")

git_override(
    module_name = "etl",
    remote = "https://github.com/ETLCPP/etl.git",
    tag = "20.47.1",  # or a specific commit
)

Then depend on it in your BUILD.bazel:

cc_library(
    name = "my_library",
    srcs = ["my_library.cpp"],
    hdrs = ["my_library.h"],
    deps = ["@etl//:etl"],
)

With WORKSPACE (legacy)

In your WORKSPACE file:

load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")

git_repository(
    name = "etl",
    remote = "https://github.com/ETLCPP/etl.git",
    tag = "20.47.1",
)

Then use deps = ["@etl//:etl"] in your targets as shown above.

Project Structure

FilePurpose
MODULE.bazelModule definition and dependencies
BUILD.bazelExposes ETL as a cc_library
.bazelversionBazel version for Bazelisk
.bazelrcDefault Bazel settings
test/BUILD.bazelUnit test target
test/syntax_check/BUILD.bazelHeader syntax check target
test/UnitTest++/BUILD.bazelVendored UnitTest++ framework

Cross-Compilation

Bazel supports cross-compilation through its platforms and toolchains system. Since ETL is a header-only library, there is nothing to cross-compile for the library itself. However, when building tests or consuming ETL in an application targeting a different architecture, you need to define a platform and register an appropriate C++ toolchain.

Example platform definition (e.g. in a platforms/BUILD.bazel):

platform(
    name = "linux_arm64",
    constraint_values = [
        "@platforms//os:linux",
        "@platforms//cpu:aarch64",
    ],
)

Then build with:

bazel build //:etl --platforms=//platforms:linux_arm64

Note: You must also have a C++ toolchain registered that supports the target platform. See the Bazel toolchains documentation for details.

Running Cross-Compiled Tests under QEMU

Cross-compiled test binaries cannot run natively on the host. Pre-defined configurations in .bazelrc select the correct cross-compiler via --repo_env=CC, set the build flags to match .devcontainer/run-tests.sh (C++23, No-STL, -O0), and use --run_under to execute the resulting binary under the appropriate QEMU emulator:

# Cross-build and run tests for ARM (armhf)
bazel test //test:etl_tests --config=armhf

# Other architectures
bazel test //test:etl_tests --config=i386
bazel test //test:etl_tests --config=powerpc
bazel test //test:etl_tests --config=riscv64
bazel test //test:etl_tests --config=s390x

These configs are designed to run inside the Docker containers under .devcontainer/, which provide the cross-compiler toolchains and QEMU binaries. Each config sets CC, AR, LD, NM, STRIP, and OBJDUMP via --repo_env so that Bazel’s auto-configured toolchain finds the complete prefixed cross-tool suite.

You can also use --run_under directly for custom setups:

bazel test //test:etl_tests --run_under=/usr/bin/qemu-arm-static

Compiler and Build Configuration

Unlike CMake where options like ETL_CXX_STANDARD and CMAKE_CXX_COMPILER are set at configure time, Bazel uses command-line flags and .bazelrc configurations.

C++ Standard Version

Use --cxxopt to pass the desired standard flag:

# C++17 (default in .bazelrc)
bazel test //test:etl_tests --cxxopt=-std=c++17

# C++20
bazel test //test:etl_tests --cxxopt=-std=c++20

# C++23
bazel test //test:etl_tests --cxxopt=-std=c++23

# C++14
bazel test //test:etl_tests --cxxopt=-std=c++14

Optimization Level

Use --compilation_mode (shorthand -c) for standard profiles, or --copt for explicit flags:

# Debug (default) — no optimization, debug symbols
bazel test //test:etl_tests -c dbg

# Optimized — O2 with NDEBUG
bazel test //test:etl_tests -c opt

# Fast build — no optimization, no debug symbols
bazel test //test:etl_tests -c fastbuild

# Custom optimization level
bazel test //test:etl_tests --copt=-O3
bazel test //test:etl_tests --copt=-O1
bazel test //test:etl_tests --copt=-Os

Selecting the Compiler (GCC vs Clang)

Bazel uses the system’s default CC environment variable. Override it to switch compilers:

# Use Clang
bazel test //test:etl_tests --repo_env=CC=clang

# Use a specific GCC version
bazel test //test:etl_tests --repo_env=CC=gcc-13

# Use a specific Clang version
bazel test //test:etl_tests --repo_env=CC=clang-18

Note: Bazel’s auto-configured toolchain infers the C++ compiler from CC automatically (e.g. CC=gcc-13g++-13 for C++ compilation). There is no need to set CXX separately.

Combining Options

Flags can be combined freely:

# Clang, C++20, optimized
bazel test //test:etl_tests --repo_env=CC=clang --cxxopt=-std=c++20 -c opt

# GCC 13, C++23, debug
bazel test //test:etl_tests --repo_env=CC=gcc-13 --cxxopt=-std=c++23 -c dbg

STL vs. No-STL Mode

ETL can operate without the standard library, which is common on bare-metal embedded targets. Use --copt to define ETL_NO_STL:

# Build and test without STL
bazel test //test:etl_tests --copt=-DETL_NO_STL

# Build with STL (default, no flag needed)
bazel test //test:etl_tests

When ETL_NO_STL is defined, ETL provides its own implementations of containers, algorithms, and utilities instead of delegating to <algorithm>, <type_traits>, etc.

Type Traits Configuration

ETL supports three type traits strategies, controlled via preprocessor defines:

ModeDefineDescription
STL type traits(default)Uses <type_traits> from the standard library
Compiler builtinsETL_USE_TYPE_TRAITS_BUILTINSUses compiler intrinsics (__is_trivially_copyable, etc.) — useful when STL headers are unavailable or incomplete
User-definedETL_USER_DEFINED_TYPE_TRAITSUses ETL’s own type traits implementations
# Use compiler built-in type traits
bazel test //test:etl_tests --copt=-DETL_USE_TYPE_TRAITS_BUILTINS

# Use ETL's own user-defined type traits
bazel test //test:etl_tests --copt=-DETL_USER_DEFINED_TYPE_TRAITS

These are mutually exclusive — define at most one. If neither is defined and STL is available, ETL uses <type_traits>.

Other Configuration Defines

Additional defines can be passed the same way via --copt=-D...:

DefineDescription
ETL_FORCE_TEST_CPP03_IMPLEMENTATIONForce C++03 code paths even when a newer standard is available
ETL_MESSAGES_ARE_NOT_VIRTUALUse non-virtual message types
# Force C++03 implementation paths
bazel test //test:etl_tests --copt=-DETL_FORCE_TEST_CPP03_IMPLEMENTATION

Using .bazelrc Presets

To avoid retyping flags, add configurations to .bazelrc:

# .bazelrc

# Named configurations
build:clang    --repo_env=CC=clang
build:gcc13    --repo_env=CC=gcc-13
build:c++20    --cxxopt=-std=c++20
build:c++23    --cxxopt=-std=c++23
build:release  --compilation_mode=opt

Then use them with --config:

bazel test //test:etl_tests --config=clang --config=c++20 --config=release

Injecting a Custom Profile Header

Instead of passing many individual --copt=-D... flags, ETL can be configured in bulk through a user-provided etl_profile.h header. When such a header is reachable on the include path, include/etl/platform.h includes it automatically; otherwise ETL falls back to its built-in defaults (ETL_NO_PROFILE_HEADER).

Because the header must sit on ETL’s include path, BUILD.bazel exposes an etl_profile label_flag so you can inject it as a Bazel target — without patching ETL’s BUILD.bazel.

First, wrap your profile header in a cc_library that places etl_profile.h on its include path:

# path/to/BUILD.bazel
cc_library(
    name = "my_etl_profile",
    hdrs = ["etl_profile.h"],
    strip_include_prefix = ".",
)

Then point the etl_profile flag at that target when building:

bazel build @etl//:etl --@etl//:etl_profile=//path/to:my_etl_profile

The flag defaults to an empty library (:no_profile), so ETL uses its built-in defaults unless you override it. As with any flag, the override can be made permanent in .bazelrc:

# .bazelrc
build --@etl//:etl_profile=//path/to:my_etl_profile

Note: Use the repository-qualified form --@etl//:etl_profile=... when consuming ETL as an external dependency. When building from within the ETL repository itself, drop the repository prefix: --//:etl_profile=....