Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Generate compile_commands.json for any C or C++ build

Bear gives you working code navigation, autocomplete, and diagnostics on any C or C++ project, whatever the build system. Prefix your build command with bear -- and Bear produces the compile_commands.json file that Clang tooling (clangd, clang-tidy, and friends) needs to understand your code:

bear -- make

Bear captures the compiler invocations while your build runs and writes the JSON compilation database for you. No changes to your build system are required.

When to use Bear

Bear is the tool of choice when the build system cannot produce a compilation database for you:

  • Make, autotools, or custom script builds.
  • A build you cannot or do not want to modify: a third-party project, a codebase you are just exploring, a CI job.
  • Unusual toolchains: embedded, HPC, CUDA, cross-compilers, and compiler launchers such as ccache, distcc, and icecc.
  • A build you cannot run at all: Bear can read make -n dry-run output or a saved build log instead of running the build.

If your project uses CMake or Meson, those tools can export a compilation database directly, and Bazel has it through Hedron’s bazel-compile-commands-extractor. Use that when it is available; Generate compile_commands.json for a CMake project covers those exports and the cases where they are not enough.

Where to go next

Looking something up rather than reading? Command-line options and Configure Bear are generated from Bear itself, so they match the version you have; Supported compilers lists every executable Bear recognizes.

Install Bear

Install Bear with your platform’s package manager:

# Debian / Ubuntu
sudo apt install bear

# Fedora
sudo dnf install bear

# Arch Linux
sudo pacman -S bear

# macOS (Homebrew)
brew install bear

# FreeBSD
pkg install bear

Then confirm it is on your PATH:

bear --version

Compare that version against the latest release. Distribution packages often lag several releases behind, so a problem you hit may already be fixed upstream; build the current release from source before reporting a bug. bear --help, or Command-line options, lists every flag once you have it installed.

For the full list of distributions that carry a Bear package, see the Repology page. Repology tracks the version each distribution ships; if yours lags behind, or does not package Bear at all, build it from source instead. Bear is not published on crates.io, so cargo install bear will not find it; cargo build --release plus the install script below is the source-build path.

Build from source

Follow INSTALL.md in the repository for prerequisites (the Rust toolchain and a C compiler) and the full steps. The short version:

cargo build --release
./scripts/install.sh

scripts/install.sh installs to /usr/local when run as root (for example under sudo), or $HOME/.local otherwise - a plain, unelevated run already gives you a working, no-sudo install. Override either default explicitly with PREFIX:

sudo PREFIX=/usr ./scripts/install.sh

DESTDIR for packaging, separately from PREFIX

scripts/install.sh accepts two different path variables, and they are not interchangeable. PREFIX is the final, on-system path; the bear entry script it installs embeds that path literally, so PREFIX must match where the package actually runs, not where it is being built. DESTDIR, when set, is a staging root prepended to every install path on top of PREFIX, for building a package without touching the real system; it must be an absolute path. Building a distribution package sets both together:

DESTDIR=/tmp/pkgroot PREFIX=/usr ./scripts/install.sh

INTERCEPT_LIBDIR must match between build and install

On systems that use lib64 or another non-default library directory name, set INTERCEPT_LIBDIR for both the build and the install step, so bear-driver finds the preload library where it actually ends up:

INTERCEPT_LIBDIR=lib64 cargo build --release
INTERCEPT_LIBDIR=lib64 ./scripts/install.sh

Let the two commands disagree and Bear still installs without complaint, but preload interception silently fails at runtime: the dynamic linker cannot find the library at the path bear-driver expects, so it skips loading it and lets the build run anyway. Nothing about that failure is loud - the build succeeds, Bear exits 0, and the only symptom is a compile_commands.json that came out empty or suspiciously short, because a missing preload library is not one of the failures Bear itself reports (see Exit status). Bear on Linux has the matching commands for the common lib64 case.

Platform-specific notes

A distribution package or a source build both give you a working bear, but a few platforms have their own constraints on how Bear intercepts a build:

Next steps

Once bear --version works, follow Getting started with Bear for the first real run, or jump straight to a recipe for your build system.

Getting started with Bear

By the end of this page you will have built Lua 5.4 under Bear, looked at the compile_commands.json it wrote, and used that file to run clangd and clang-tidy against the Lua source, the same way an editor or a CI lint job would.

Install Bear

Install Bear with your platform’s package manager; see Install Bear for the commands. Then confirm it is on your PATH:

bear --version

Get a real project

Lua 5.4 is a good first target: a plain Makefile, no configure step, no dependencies beyond a C compiler and make. Download and extract it:

curl -fsSL -O https://www.lua.org/ftp/lua-5.4.8.tar.gz
tar xzf lua-5.4.8.tar.gz
cd lua-5.4.8

lua.org’s ftp directory does not change existing releases, so this download always matches this checksum:

$ sha256sum lua-5.4.8.tar.gz
4f18ddae154e793e46eeab727c59ef1c0c0c2b744e7b94219710d76f530629ae  lua-5.4.8.tar.gz

Build it under Bear

bear -- make

Bear runs the build exactly as make would on its own, forwarding its output, and adds one thing: a compile_commands.json file in the current directory, one entry per compiled source file. Lua’s Makefile recurses into src, so this produces 34 entries, one for each .c file that went into lua and luac.

Look at the result

cat compile_commands.json

Each entry looks like this:

{
  "file": "lapi.c",
  "arguments": [
    "/usr/bin/gcc",
    "-std=gnu99",
    "-O2",
    "-Wall",
    "-Wextra",
    "-DLUA_COMPAT_5_3",
    "-DLUA_USE_LINUX",
    "-c",
    "-o",
    "lapi.o",
    "lapi.c"
  ],
  "directory": "/home/you/lua-5.4.8/src",
  "output": "lapi.o"
}

directory is where the compiler ran, file is the source it compiled (relative to directory), arguments is the exact command as an array, and output is the object file it produced. Your own output will show your own resolved compiler path in arguments[0]; on a distribution that routes compilers through ccache, that path may be a ccache symlink instead of the compiler itself. There is one entry per compiled source and none for the headers Lua’s sources include, such as lapi.h, because Bear only records commands the build actually ran, and the build never invokes the compiler on a header directly. For what Bear does and does not capture, and why, see How Bear works.

Use the database: clangd

compile_commands.json exists for tools like clangd to read, not to be read by people. Point clangd at one Lua source file and it logs the exact command it pulled from the database:

$ clangd --check=src/lapi.c
I[...] Compile command from CDB is: [/home/you/lua-5.4.8/src] /usr/bin/gcc -std=gnu99 -O2 -Wall -Wextra -DLUA_COMPAT_5_3 -DLUA_USE_LINUX -c -o lapi.o -resource-dir=... -- /home/you/lua-5.4.8/src/lapi.c

That is the whole hand-off: an editor with clangd support opened on this directory gets working “go to definition”, completion, and diagnostics with no other setup. See Set up clangd for a project without CMake for the editor side.

Use the database: clang-tidy

clang-tidy reads the same file to know how to parse each source it lints. Run it against one Lua source file, restricted to a single check so the output stays short:

$ clang-tidy -p . -checks='-*,clang-analyzer-deadcode.DeadStores' src/lfunc.c
1 warning generated.
lfunc.c:196:42: warning: Although the value stored to 'upl' is used in the enclosing expression, the value is never actually read from 'upl' [clang-analyzer-deadcode.DeadStores]
  196 |   while ((uv = L->openupval) != NULL && (upl = uplevel(uv)) >= level) {
      |                                          ^

That is a real finding in Lua’s own source, produced by clang-tidy, not by Bear: Bear’s job ends at writing compile_commands.json, and every tool built on Clang’s tooling API, from editors to linters to refactoring tools, takes it from there.

Next steps

Build an autotools project under Bear

By the end of this page you will have built libtasn1 4.20.0 twice: once the ordinary way, once under Bear, and compared what each run leaves behind. This page assumes Bear is already installed; see Getting started with Bear for a first run against a plain Makefile project, or Install Bear for platform packages.

Get a real project

libtasn1 is a small GNU library built with automake and autoconf, a common shape for the C and C++ projects Bear targets. Its release tarball ships a generated configure script, so building it needs nothing beyond a C compiler and make, no autoconf or automake installed. Download and extract it:

curl -fsSL -O https://ftp.gnu.org/gnu/libtasn1/libtasn1-4.20.0.tar.gz
tar xzf libtasn1-4.20.0.tar.gz
cd libtasn1-4.20.0

GNU’s release tarballs do not change after release, so this download always matches this checksum:

$ sha256sum libtasn1-4.20.0.tar.gz
92e0e3bd4c02d4aeee76036b2ddd83f0c732ba4cda5cb71d583272b23587a76c  libtasn1-4.20.0.tar.gz

Build it the ordinary way

./configure
make

This builds libtasn1.so under lib/.libs/, the same as it would for anyone packaging or installing this library. Look for a compilation database and there is none:

$ ls compile_commands.json
ls: cannot access 'compile_commands.json': No such file or directory

Nothing here was written to produce one; configure and make have no notion of a compilation database. That is the gap Bear fills.

Build it under Bear

Start from a clean tree and repeat the same two commands, this time running make under Bear:

make distclean
./configure
bear -- make

./configure is exactly the command from the previous section, run the same way, outside Bear. Only make runs under Bear. Preload interception, the default on Linux, watches every process the build spawns, so it does not matter that configure ran unsupervised earlier: Bear only needs to be watching for the commands it must record, and those are the compiler invocations make issues. “Build it again, in wrapper mode” below builds this same project a second time under the other interception method, where the configure step does need to run under Bear.

Look at the result

$ grep -c '"file":' compile_commands.json
34

libtasn1’s Makefile.am recurses into six subdirectories, and each one contributes its own entries: lib (9), lib/gl (3), src (4), src/gl (14), examples (3), fuzz (1). If you get fewer, check whether make stopped early: this release also builds its Texinfo manual, which needs makeinfo from the texinfo package, and a failure there ends the recursion before the last directories are reached. That is worth seeing once, because it is the general case with interception: Bear records what the build got to, so a build that stops early gives you a database that stops with it. A recursive automake build like this compiles in several directories at once, and gnulib’s bundled replacement sources under the two gl directories are compiled with their own flags, separate from the library’s.

Here is one entry from lib:

{
  "file": "ASN1.c",
  "arguments": [
    "/usr/bin/gcc",
    "-DHAVE_CONFIG_H",
    "-I.",
    "-I..",
    "-I./gl",
    "-I./includes",
    "-DASN1_BUILDING",
    "-fanalyzer",
    "-Wall",
    "-Wextra",
    "-Wshadow",
    "-Wwrite-strings",
    "...",
    "-g",
    "-O2",
    "-c",
    "ASN1.c",
    "-fPIC",
    "-o",
    ".libs/ASN1.o"
  ],
  "directory": "/home/you/libtasn1-4.20.0/lib",
  "output": ".libs/ASN1.o"
}

(the real entry lists several dozen more -W flags; they are elided above with ...)

And one from src/gl:

{
  "file": "cloexec.c",
  "arguments": [
    "/usr/bin/gcc",
    "-DHAVE_CONFIG_H",
    "-I.",
    "-I../..",
    "-Wno-cast-qual",
    "-Wno-conversion",
    "-Wno-sign-compare",
    "-Wno-unused-function",
    "-Wno-unused-parameter",
    "-g",
    "-O2",
    "-c",
    "cloexec.c",
    "-fPIC",
    "-o",
    ".libs/libsgl_la-cloexec.o"
  ],
  "directory": "/home/you/libtasn1-4.20.0/src/gl",
  "output": ".libs/libsgl_la-cloexec.o"
}

lib turns on a wall of analyzer and style warnings for libtasn1’s own code; src/gl turns most of them back off, because that directory holds gnulib’s imported replacement sources, not code the project wants held to its own warning level. One database, one directory per entry, describes the whole recursive build: a tool reading it gets the flags that actually compiled each file, instead of one guessed command applied everywhere.

Use the database: clangd

$ clangd --check=lib/ASN1.c
I[...] Compile command from CDB is: [/home/you/libtasn1-4.20.0/lib] /usr/bin/gcc -DHAVE_CONFIG_H -I. -I.. -I./gl -I./includes -DASN1_BUILDING -fanalyzer -Wall -Wextra ... -g -O2 -c -fPIC -o .libs/ASN1.o -resource-dir=... -- /home/you/libtasn1-4.20.0/lib/ASN1.c

clangd pulled the same flags straight out of compile_commands.json, directory and all. See Set up clangd for a project without CMake for the editor side of this hand-off.

Build it again, in wrapper mode

Preload interception, used above, watches every process the build spawns, so it never mattered that ./configure ran outside Bear: the commands Bear needed to see were make’s. Wrapper mode works differently: it puts wrapper executables on PATH in place of the real compilers, so a step that discovers and records the compiler has to see the wrapper while it does so, or the build later invokes the real compiler directly and Bear observes nothing. For libtasn1, that step is ./configure. See How Bear works for the mechanism behind both interception methods.

That makes this project’s configure step the one place on this site where running under Bear is required, not just harmless. Write a bear.yml next to the source tree:

schema: "4.2"
intercept:
  mode: wrapper

Bear finds bear.yml in the current working directory on its own, so no flag is needed; naming it explicitly with --config bear.yml works the same way. This same file selects wrapper mode as the interception method on every platform, including Linux and the BSDs, where preload is otherwise the default. Configure Bear covers the rest of the file’s keys and the other places Bear looks for it.

From a clean tree, run configure and make as two separate Bear invocations, in order:

make distclean
bear --config bear.yml -- ./configure
bear --config bear.yml -- make

The first command writes a database of its own: six entries, all of them configure’s own throwaway compiler probes (conftest.c and similar, plus a libtool probe under a temporary directory), not libtasn1’s sources. The second command writes the same 34 entries, across the same six directories, as the preload build above, with no conftest entry among them. Bear overwrites the output file on every run by default, so the second command’s database simply replaces the first’s; the probes are not merged in and do not need to be filtered out. Do not add --append to either command here, since that flag places new entries alongside the old ones instead of replacing them, which would let the probes survive into the final database.

Next steps

Recover compile_commands.json from a build log

By the end of this page you will have turned a plain text build log, with no live build behind it, into a working compile_commands.json, using bear parse-sh instead of running anything.

This is for the build you cannot run again: a CI job that already finished and left its log in an artifact, a machine you no longer have access to, or a build that takes an hour when you only need the compiler flags out of it. Getting started with Bear covers the other path, running a build under Bear as it happens; this page covers reconstructing a database from text alone, after the fact.

Get a build to reconstruct

Download Lua 5.4.8, a small, dependency-free C project, and extract it:

curl -LO https://www.lua.org/ftp/lua-5.4.8.tar.gz
tar xzf lua-5.4.8.tar.gz
cd lua-5.4.8

Capture a dry run

Ask make to print the commands it would run, without running them, and save that text to a file:

make -nw > build.log

-n is the dry run: make prints each recipe line instead of executing it. -w makes make also print Entering directory and Leaving directory lines around every directory it works in. Lua’s top-level Makefile hands the real build off to a make invocation in src/; that inner make already announces its own directory by default, and -w additionally names the top-level one, so the log names every directory the build touched, not just the ones recursion would have announced anyway.

Turn the log into a database

$ bear parse-sh -i build.log
bear: warning: line 4: skipped (command substitution)
bear: warning: line 5: skipped (command substitution)
bear: warning: parse-sh: 40 command(s) parsed, 2 line(s) skipped

Those two warnings point at Lua’s own platform-detection recipe, which runs echo Guessing `uname` and make `uname` to pick a default target: the backticks are shell command substitution, outside the subset parse-sh understands, so it skips both lines and says why. Neither line is a compiler invocation, so nothing is missing from the result; a skip only costs you an entry when the skipped line was one. The run still exits 0, because most of the log did parse.

compile_commands.json now holds one entry per source file make would have compiled:

$ grep -c '"file"' compile_commands.json
34

and each entry looks like this:

{
  "directory": "/home/you/lua-5.4.8/src",
  "file": "lapi.c",
  "arguments": [
    "gcc",
    "-std=gnu99",
    "-O2",
    "-Wall",
    "-Wextra",
    "-DLUA_COMPAT_5_3",
    "-DLUA_USE_LINUX",
    "-c",
    "-o",
    "lapi.o",
    "lapi.c"
  ],
  "output": "lapi.o"
}

directory is src, not the checkout root: parse-sh followed the cd src in the top-level recipe and the Entering directory lines -w added, the same way it would for a real recursive build.

The same log, read somewhere else

The scenario this page is really for is a log you did not just produce: someone hands you build.log from a build that ran elsewhere, or ran an hour ago on a machine that is gone now. Copy it somewhere that has never seen the Lua checkout, and parse it from there:

$ mkdir ~/handed-a-log
$ cp lua-5.4.8/build.log ~/handed-a-log/
$ cd ~/handed-a-log
$ bear parse-sh -i build.log
$ grep -m1 '"directory"' compile_commands.json
    "directory": "/home/you/lua-5.4.8/src",

Same 34 entries, same directory, even though ~/handed-a-log has no lua-5.4.8 anywhere near it. parse-sh gets directory from the cd and Entering directory text inside the log itself, never from where you happen to run it; that is what makes a saved log portable. Capture with -nw as a habit: it costs nothing and guarantees the log names its own build root, including for a Makefile whose top level compiles directly instead of only delegating, which the Makefile recipe covers in more detail.

Limits of reconstructing from text

parse-sh reconstructs commands from what a build system chose to print; it never observes anything running. That makes it lower fidelity than intercepting a real build, in specific, permanent ways:

  • A dry run can omit commands entirely. Recursive make does not always propagate -n to sub-makes, and a command that compiles a not-yet-generated source never gets printed in the first place.

    The sharpest case of this is a build that produces a tool and then consults that tool to decide what to compile, for example SRCS := $(shell ./tool) in a makefile, or an equivalent generated-flags step. make -n only prints the recipe that builds the tool; it never runs it, so $(shell ./tool) has nothing to run and the compiles that depend on its output never reach the log at all. On a project shaped this way, bear -- make records every compile, the tool’s own included; make -n piped into bear parse-sh records only the tool’s compile - silently, with no warning, and exit status 0. A generated makefile that is included, by contrast, is not affected: GNU make remakes an included makefile for real even under -n, so a source list coming from an included .mk file survives the dry run. It is specifically the $(shell ...)-at-parse-time shape that goes missing. Nothing signals the gap, so notice it by comparing the entry count against what you expect for the project; when you find one, run the actual build under Bear (bear -- make) if it can be run at all.

  • A bare executable name in the log, like gcc above, resolves against the environment and PATH you run parse-sh in, not the original build’s. If the build used a different compiler on PATH, the database will point at yours instead.

  • Only a documented subset of POSIX shell is understood: word splitting and quoting, the ;, &&, ||, &, and | separators, comments, redirections, cd, brace groups, and make’s directory markers. Subshells, command substitution (what tripped the two lines above), parameter expansion, globs in the executable position, here-documents, and shell keywords such as if or for are all outside it and get skipped, loudly, rather than guessed at. See bear parse-sh in the bear(1) man page for the exact list.

Prefer bear -- make (see Getting started with Bear and the Makefile recipe) whenever you can actually run the build: it observes the real exec() calls, so it cannot miss a command or resolve a compiler name wrong. Reach for parse-sh only when running the build again is not an option.

Recipes

One page per task. Each recipe answers a single question and opens with the command that answers it.

Toolchains

Related pages: Getting started with Bear for the first run, Troubleshooting for a database that came out wrong, and How Bear works for the mechanism.

Generate compile_commands.json for a Makefile project

Run this from the directory holding the Makefile:

bear -- make

Bear runs make exactly as it would run on its own, watches every command it executes, and writes compile_commands.json next to it, one entry per compiled source file. Nothing about the Makefile changes. Make has no built-in way to export a compilation database (unlike CMake or Meson, which do; see the home page if that is your build system instead), so intercepting the real build is the standard way to get one for a Make project. For the full walkthrough of a first run, including what the output looks like, see Getting started with Bear; this page covers everything beyond that first run.

Autotools: ./configure and make

Run configure on its own, then build under Bear:

./configure
bear -- make

This is enough whenever preload is the active interception method: configure only needs to run so it can write a Makefile, and make is the command Bear needs to watch.

In wrapper mode a build step that discovers the compiler on its own has to run under Bear too, or it never sees the wrapper. See Configure Bear for which mode is the default on your platform and how to force one or the other. Practically, the wrapper-mode requirement means running the configure step under Bear as well, and discarding its output:

bear -- ./configure
make clean
bear -- make

Do not combine ./configure and make into a single Bear run (for example bear -- sh -c './configure && make', or a Makefile target that re-runs configure itself): configure’s throwaway toolchain probes (conftest.c and similar) end up recorded alongside the real build. Run configure and make as two separate Bear invocations instead, as above; the second run’s output overwrites the first’s, so nothing from the probe phase survives. See Troubleshooting for the mechanics, and for excluding probe files by pattern instead, if you would rather keep one combined run.

Incremental builds

Make only rebuilds what is out of date, and Bear only records commands the build actually executes, so a make with nothing to do records nothing: running bear -- make a second time in a row against an unchanged tree overwrites compile_commands.json with an empty []. Build from clean when you want a complete database:

make clean
bear -- make

To fold in just the parts you rebuild, add --append. It places new entries before the existing ones in the output file, so accumulate the database as you touch different parts of the project instead of rebuilding everything at once:

bear --append -- make main.o
bear --append -- make greet.o

After the first call the database holds one entry (main.c); after the second it holds two, with the newest entry first. On the first call there is nothing to append to, so Bear warns and ignores the option. If a file is later rebuilt with different flags, its newest entry wins by default; see Configure Bear for the matching rule, and Bear produces an empty compile_commands.json if entries still seem to go missing.

Parallel builds

bear -- make -j4 needs nothing special: every compiler process, however many run concurrently, reports back to Bear on its own. A parallel build produces the same entries as the serial one.

Recursive Makefiles

A real build under Bear needs no extra flags for make -C or $(MAKE) recursion: each intercepted command carries its own working directory, so entries come out correctly scoped to the subdirectory they were compiled in, however deep the recursion goes. In a project whose top-level Makefile recurses into a libgreet/ subdirectory, the database holds main.c with directory set to the project root and greet.c with directory set to libgreet, with no -w/--print-directory needed.

The -w flag matters only for the dry-run path below, not for a real build.

Bear itself, not make, decides where compile_commands.json is written: it always lands in the directory Bear was started from, even when the Makefile it drives is elsewhere. Running bear -- make -C libgreet from inside libgreet/ writes the file there; running the same command from the project root writes it at the root instead.

A build you cannot re-run

When you cannot run the build again, for example to recover a database from a CI log after the fact, feed a dry run into bear parse-sh instead of running anything:

make -n | bear parse-sh

For a recursive build add -w, so the top-level make also prints the Entering directory markers that sub-makes already print by default; parse-sh uses them to resolve each command to the right directory:

make -nw | bear parse-sh

Parsing a plain make -n for a recursive project from a directory other than the build root puts the top-level source’s directory at the parsing directory rather than the build root; make -nw from the same place resolves it correctly. Without -w, the top-level make prints no directory marker of its own, so parse-sh falls back to its own working directory (or --directory, if given) for commands issued before any marker appears.

This path is reconstruction from text, not observation, and is lower fidelity than a real build: a dry run can omit commands entirely (recursive make does not always propagate -n to sub-makes, and commands behind not-yet-generated sources never print), the environment and PATH used to resolve bare executable names is the parsing one, not the real build’s, and only a documented subset of POSIX shell syntax is understood (subshells, command substitution, and here-documents are not, among others; an unparsed line is skipped and reported on standard error with its line number). Prefer bear -- make whenever you can actually run the build; reach for parse-sh only when you cannot. See bear parse-sh in the bear(1) man page for the full list of what it understands.

Where the output goes, and how to change it

By default Bear writes compile_commands.json in the directory it was started from. Use -o/--output (see Command-line options) to write it somewhere else:

bear -o build/compile_commands.json -- make

- (standard output) is not accepted for -o in combined mode, since standard output belongs to the build; see the bear(1) man page for the split intercept/semantic modes, which do accept it.

Generate compile_commands.json for a CMake project

CMake writes compile_commands.json itself; turn that on before reaching for Bear:

cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build build

CMake places the file in the build directory, the one passed to -B (build above), not the source tree. Setting the same variable through a CMakePresets.json cacheVariables entry, instead of the -D flag on the command line, has the same effect. Point your editor at the file this produces with Set up clangd for a project without CMake, which covers --compile-commands-dir and the .clangd CompilationDatabase key.

This is the better answer for the ordinary CMake project, and the rest of this page is about the cases where it is not enough.

Generators that ignore the variable

CMake’s own documentation for CMAKE_EXPORT_COMPILE_COMMANDS states: “This option is implemented only by Makefile Generators and Ninja Generators. It is ignored on other generators.” The Visual Studio and Xcode generators fall into “other generators”: setting the variable with either of those has no effect, and no compile_commands.json appears no matter how the project builds. Reach for Bear instead, pointed at whatever actually invokes the compiler (see Command-line options for the -- split between Bear’s own flags and the build command):

bear -- cmake --build build

This works regardless of which generator produced the build files; see How Bear works for why watching the build succeeds where the generator’s own export does not.

When the export does not match what actually ran

CMake’s export records the command it intends to run, generated at configure time. Three situations make that different from what the build actually executes:

  • a compiler launcher in front of the compiler (CMAKE_C_COMPILER_LAUNCHER, CMAKE_CXX_COMPILER_LAUNCHER, or a ccache/distcc setup baked into the toolchain file);
  • a wrapper script standing in for the compiler;
  • a custom command or custom target that compiles something outside CMake’s own compile rules.

Interception records what the build actually ran instead of what CMake planned:

bear -- cmake --build build

See Use Bear with ccache, distcc, or icecc for the launcher case in detail.

A superbuild, or CMake as one step of a larger build

When CMake is only part of the build - a superbuild that configures and builds several subprojects, or a top-level Makefile that drives cmake alongside other steps - CMake’s own export only ever covers the CMake part. Run the whole thing under Bear instead, and every compiler invocation ends up in the same database regardless of which step produced it:

bear -- make

or, when the top-level driver is itself CMake:

bear -- cmake --build .

If the project does not use CMake

CMake is not the only build system that writes compile_commands.json itself. Check whether yours already does before reaching for Bear:

  • Ninja: ninja -t compdb > compile_commands.json
  • Qbs: qbs generate --generator clangdb
  • Waf: load the clang_compilation_database tool from wscript, then run waf clangdb
  • Bazel: no native flag, but Hedron’s Compile Commands Extractor does the same job as a third-party target
  • Clang itself: pass -MJ <fragment>.json to every invocation and concatenate the fragments into a single JSON array

Use your build system’s own export when it has one. Reach for Bear when it does not, or when what it exports does not match what actually ran.

clangd cannot find the file CMake wrote

compile_commands.json living in the build directory rather than the project root is the usual reason clangd does not pick it up automatically. See Set up clangd for a project without CMake for how clangd searches and for pointing it at the exact directory, and Bear produces an empty compile_commands.json if the file exists but is missing entries rather than missing entirely.

Related: Generate compile_commands.json for a Makefile project for the equivalent Bear-only workflow when there is no CMake export to fall back on, and the Recipes index.

Bear produces an empty compile_commands.json

The most common cause is that the build compiled nothing: an incremental build with everything already up to date runs no compiler at all, and Bear only records commands the build actually executes. Rebuild from clean:

make clean
bear -- make

If a clean rebuild still leaves [], work through the other causes below, most common first. Each one shows the command that confirms it and the command that fixes it.

The build had nothing to compile

An incremental build that finds every target up to date runs no compiler, so there is nothing for Bear to intercept. This is not a bug: Bear observes execution, it does not read your build files to guess what “should” happen.

Confirm: check whether the build itself said it did nothing, for example make’s

make: Nothing to be done for 'all'.

Fix: clean first, or build into an empty directory, as shown above. When you only rebuilt part of a larger project and want to keep the rest of a previous run’s entries instead of doing a full clean, use --append (see Command-line options for its exact semantics, and Troubleshooting for the duplicate-handling rules that apply when you do).

The compiler is not recognized

Bear decides whether a command is a compilation by matching the executable’s filename against a table of known names; a name outside that table produces no entry, even though the compiler ran and built your code successfully.

Confirm: check whether your compiler’s name is one Bear knows, on Supported compilers, or ask the installed build directly:

bear semantic --print-compilers

A custom or renamed executable will not appear here. The Diagnostics section below shows a more precise way to confirm the exact command that went unrecognized.

Fix: add a compilers: hint mapping the executable’s path to the family it behaves like:

schema: "4.2"
compilers:
  - path: /usr/local/bin/my-custom-cc
    as: gcc

A Makefile rule invoking ./my-totally-custom-cc, itself a plain copy of gcc, produces [] on its own, and a working entry once the hint above names its path. The accepted as: values are listed on Supported compilers.

Preload interception is blocked or unavailable

Bear has two interception methods, chosen once per invocation: preload injects a library into every process the build starts; wrapper substitutes the known compilers on PATH instead. See Configure Bear for which one is the default on your platform and how to force the other, and How Bear works for the mechanism. A few situations keep preload from seeing a compilation at all:

A statically linked build tool. Preload works by hooking library calls inside the process that makes them; a statically linked executable never loads the dynamic linker, so no hook is ever installed in it, and any compiler it execs directly is invisible to Bear. This is a fundamental limit of the approach, not a bug.

Confirm with:

file "$(command -v your-build-tool)"

A report of “statically linked” (rather than “dynamically linked”) means preload cannot see what that tool executes. A statically linked launcher that directly execs a compiler produces [] under the default (preload) mode on Linux, and a correct entry once wrapper mode is forced:

schema: "4.2"
intercept:
  mode: wrapper

macOS with System Integrity Protection enabled. SIP strips the preload environment variable from protected executables before it can take effect, so preload cannot run there; it is wrapper mode’s reason for being the macOS default in the first place. See Bear on macOS for the detail. Forcing preload mode where it is unavailable (SIP-protected macOS, or Windows, which has no preload library at all) is a startup error naming wrapper mode as the alternative - Bear does not silently fall back, and the build does not run, so this case shows up as an explicit error rather than an empty compile_commands.json.

Running against a container from the outside. bear -- docker exec ... on the host does not work: the daemon runs the command inside the container’s own process tree, which the host-side Bear process never observes, so the database comes out empty with no other symptom. Bear must run inside the container as part of the build. See Run Bear inside a Docker container for the correct invocation.

In every one of these cases, wrapper mode is the general fallback, with one condition of its own: the build has to actually pick the wrapper up, covered below.

The preload library failed to load

A missing or broken libexec.so install is not a failure the dynamic linker treats as fatal: it drops a preload target it cannot open, prints a warning on standard error, and lets the build run anyway. Only the top-level command Bear launched is observed directly; everything that command itself spawns runs unwatched, so a build whose compiler is invoked through make or a shell script comes back with an empty database, and Bear still exits 0 because the build succeeded. See Exit status for why success and an empty database are not mutually exclusive.

Confirm: look for this line in the build’s own standard error, not Bear’s own log output:

ERROR: ld.so: object '.../libexec.so' from LD_PRELOAD cannot be preloaded: ignored.

Fix: see Troubleshooting for the install-path check.

Wrapper mode: the build never picks up the wrapper

Wrapper mode works by putting reporting executables ahead of the real compilers on PATH. A build step that discovers the compiler on its own, before the wrapper directory is in place, bakes in the real compiler’s path directly, and every later invocation goes straight to the real compiler with no wrapper in between: a ./configure script run outside Bear, or a CMake configure step that resolves cc to an absolute path, are the common versions of this.

Confirm: look for the real compiler’s absolute path already baked into a generated build file (config.mk, CMakeCache.txt, or similar) from before the build ran under Bear.

Fix: run the configure step under Bear too, discarding its output, then run the real build under Bear:

bear --config bear.yml -- ./configure
bear --config bear.yml -- make

A configure.sh that resolves cc to an absolute path and writes it into a makefile fragment produces [] when only the subsequent make runs under Bear in wrapper mode. Running configure.sh under Bear as well, with the same config and discarding that run’s own output, makes the following build’s database come out populated. This matches the two-step CMake example in the bear(1) man page’s EXAMPLES section.

The build failed before it reached a compiler

Bear records the compiler invocations that actually happen; if the build fails before starting any of them - a missing include in a Makefile, a dependency that does not exist, a configure step that aborts - there is nothing to record.

Note the distinction: a compile that fails after the compiler was invoked (a syntax error in the source) still produces an entry, because Bear records the attempted command, not its outcome. An empty database from a failed build specifically means the failure happened before any compiler ran at all.

Confirm: check the build’s own error output and Bear’s exit code (see Exit status); a non-zero exit with no compiler messages at all points here.

Fix: fix whatever stops the build before it reaches a compile step, then rebuild under Bear.

Source filtering removed everything

A sources: rule in the configuration file that is broader than intended can exclude every compiled file, leaving a [] even though the build compiled normally. (A duplicates: rule cannot do this by itself: it always keeps the first occurrence of a group of duplicates, so it can shrink a database but never empty it out on its own; see Troubleshooting for that case.)

Confirm: temporarily remove the sources: section from your configuration and rebuild; if entries appear, a rule there is the cause.

Fix: narrow the rule. For example, excluding every .c file:

schema: "4.2"
sources:
  files:
    - pattern: "*.c"
      action: exclude

produces [] for a project with only .c sources, by design. See Configure Bear for the full sources: syntax.

Diagnostics

Before anything else, re-run with logging turned on:

RUST_LOG=info bear -- <build command>

Near the end of the output is a summary that tells you which side of the pipeline lost your entries:

Output pipeline:
  semantic events: 5
  current entries: 2
  previous entries: 0
  filtered entries by duplicate: 0
  filtered entries by source: 0
  synthesized header entries: 0
  dropped entries (invalid): 0
  total entries written: 1
  • semantic events near the minimum (as low as 1, just the build command itself) means the build never executed a compiler - the “nothing to compile” or “build failed early” cases above.
  • semantic events clearly greater than zero but current entries: 0 means commands ran but none were recognized as a compiler - the “compiler not recognized” case.
  • filtered entries by source or filtered entries by duplicate greater than zero, with total entries written at zero, means recognized entries were filtered out by your configuration.

Set RUST_LOG=debug instead for more detail: every intercepted command is tagged Recognized(...) or NotRecognized(...), which pins down exactly which invocation Bear saw and how it classified it. Without RUST_LOG set, Bear prints only warnings and errors; see the bear(1) man page’s ENVIRONMENT section for the exact levels.

Related: Troubleshooting for a database that is short or wrong rather than empty, Supported compilers for the recognized names, How Bear works for the interception and analysis mechanism, the Recipes index, and the platform notes for Linux, macOS, Windows, and BSD.

Set up clangd for a project without CMake

bear -- make

Run your build under Bear once to produce compile_commands.json, and clangd picks it up automatically the next time you open a source file in your editor - no CMake, no manual flag lists. The rest of this page is about getting that file where clangd looks for it, and confirming that it actually found it.

Where the file has to be

clangd searches for compile_commands.json starting from the file you are editing: the file’s own directory, a build/ subdirectory of it, then the parent directory, that parent’s build/ subdirectory, and so on up the tree (see clangd’s project setup documentation). Running bear -- make from your project root places the file exactly where this search finds it for any source file under that root, which is why the plain invocation above is usually all you need.

Two situations fall outside that search:

  • the database was generated somewhere other than the project root, for example a separate build directory named with --output (bear --output build/compile_commands.json, or bear intercept’s own --output);
  • you are editing a source tree that clangd’s search does not reach from the file’s own location.

Pointing clangd at a database in a different place

Pass --compile-commands-dir on clangd’s own command line (consult your editor’s clangd-integration settings for where to add this):

clangd --compile-commands-dir=/path/to/build

or, to keep it in the project instead of your editor configuration, add a .clangd file at the project root:

CompileFlags:
  CompilationDatabase: build

The CompilationDatabase key takes a directory (absolute, or relative to the .clangd file’s own location) to search instead of walking upward from the edited file. See clangd’s configuration documentation for the complete set of .clangd keys; this page only covers the one that matters for finding the database.

With compile_commands.json in a build/ directory and sources in a sibling src/ directory, both clangd --compile-commands-dir=build and a .clangd file with CompilationDatabase: build at the project root make clangd load the database for a file under src/.

Confirming clangd actually loaded it

Ask clangd to parse a single file outside of your editor, and look at the lines it logs while loading:

$ clangd --check=path/to/file.c
Loaded compilation database from /path/to/build/compile_commands.json
Compile command from CDB is: [...] /usr/bin/cc -o main ...

Compile command from CDB is: means clangd found and used an entry for that exact file. There are two other outcomes worth telling apart.

Compile command inferred from greet.c is: [...] -x c-header ... -- /path/to/greet.h

clangd found no entry for this file but borrowed one from a neighbour in the same directory. This is the normal, working result for a header, since Bear records the sources a build compiles and headers are not compiled on their own. Take it as success unless the flags it inferred are wrong for that file.

Generic fallback command is: [...] clang ... -- /path/to/file.c

clangd loaded some database but found nothing to work from, not even a neighbour - a sign the file was filtered out of compile_commands.json, or was never compiled (see Bear produces an empty compile_commands.json if the database itself is empty). If it never mentions loading a database at all, it did not find one on its search path or at the configured location; see Where the file has to be above.

Related: Bear produces an empty compile_commands.json if the file has no entries at all, Generate compile_commands.json for a Makefile project, and the Recipes index.

clangd has no compile command for a header

clangd already guesses a compile command for a header with no entry of its own, by borrowing the flags of a compiled source file nearby, and for many projects that guess is good enough as it stands. When it is not - the borrowed flags are wrong for this header, or there is no source nearby to borrow from - turn on header-entry synthesis in Bear’s configuration file:

schema: "4.2"
headers:
  enabled: true
  strategy: siblings

This makes Bear write a real entry for the header itself, cloned from a compiled source’s arguments with the source path swapped in for the header’s and the output flag removed, instead of leaving clangd to infer one. The full set of headers: keys and their defaults is on Configure Bear.

Telling the situations apart

Run clangd against the header directly and read the log line it prints while loading the database:

clangd --check=path/to/header.h
  • Compile command from CDB is: ... - the database already has a real entry for this header. This is not the problem this page is about.
  • Compile command inferred from some_source.c is: ... - clangd found no entry but borrowed one from a neighboring source in the same directory. This is often fine as it stands; look at whether the borrowed flags actually apply to the header (same defines, same target) before changing anything.
  • Generic fallback command is: ... - clangd found no entry and no neighbor to borrow from at all. This is the case synthesis exists for.

Set up clangd for a project without CMake documents these same three log lines with the fuller context of a working database; this page only adds the header-specific reading of them.

Synthesis is not always the difference between broken and working

In a split include/ + src/ project (include/greet.h used by src/greet.c, which lives in a different directory), running with header synthesis off still leaves clangd able to work on include/greet.h: it infers a command from src/greet.c and logs Compile command inferred from src/greet.c is: ... -x c-header .... Turning on the dependency-files strategy replaces that with a real entry, and the log line changes to Compile command from CDB is: ... - but the header was already usable before that change. Reach for synthesis when one of these is actually true, not by default:

  • the flags a neighboring source would donate are wrong for this header (a different target, different defines, a header shared across parts of the build that compile differently);
  • no compiled source is in reach to donate flags from at all (the Generic fallback command case above);
  • a consumer needs a real database entry rather than clangd’s own inference - running clang-tidy directly against the header, or another tool that reads compile_commands.json without clangd’s fallback logic.

Choosing a strategy

Two strategies are available, and they reach different headers:

  • siblings clones flags from a compiled source in the same directory as the header. It needs nothing from the build, but it reaches only headers that share a directory with a compiled source: in a split include/ + src/ layout it picks up a header living beside .c files in src/, but not one living alone in include/, because that directory has no compiled source to clone from. The flags it clones are approximate, borrowed from whichever compiled source happens to sit next to the header, not necessarily the one that actually includes it.
  • dependency-files reads the make-style .d files the build already left on disk (from -MMD, -MD, or an equivalent flag) and synthesizes an entry for every header named as a prerequisite, wherever it lives. This is how it reaches a header in include/ from a .d file written while compiling a source in src/. It needs the build to have actually emitted those files: look for *.d files next to the object files after a build, or check the compiler flags for -MMD/-MD, before choosing this strategy. A build that never passes one of those flags leaves nothing for dependency-files to read, and synthesis silently reaches no headers.

Start with siblings when the project’s headers all sit next to the sources that use them; reach for dependency-files when they do not, or when siblings’s approximate flags are visibly wrong, and the build already produces .d files.

A third strategy is conceivable: post-process an existing compilation database and determine, for each translation unit, which header files it actually used. That would be more accurate than cloning a neighbor’s flags, and would not require the build to have emitted dependency files. Bear does not implement this strategy yet; external tools exist that do this kind of post-processing.

Related: Bear produces an empty compile_commands.json for a database missing more than just header entries, Generate compile_commands.json for a Makefile project, and the Recipes index.

Use Bear with ccache, distcc, or icecc

In the common case Bear needs no configuration for a launcher in front of a compiler. Run the build exactly as you already do, launcher and all:

bear -- ccache gcc -c main.c -o main.o

produces this entry in compile_commands.json:

[
  {
    "file": "main.c",
    "arguments": ["gcc", "-c", "main.c", "-o", "main.o"],
    "directory": "/path/to/project",
    "output": "main.o"
  }
]

The ccache token is gone: arguments holds the real compiler’s argv, taken verbatim from whatever followed the launcher’s name, not a resolved or canonicalized path. distcc and icecc drop out the same way; the database never contains an entry whose command is the launcher itself:

bear -- ccache gcc -c main.c -o main.o           # -> "gcc", ...
bear -- ccache cc -c main.c -o main.o            # -> "cc", ...
bear -- ccache /usr/bin/gcc -c main.c -o main.o  # -> "/usr/bin/gcc", ...

In each case arguments starts with exactly the token shown in the comment. Bear does not chase the name further; if the build wrote a bare cc, that is what ends up in the database.

The ccache masquerade case

Some distributions (Fedora, Arch, Gentoo) put a directory of symlinks ahead of the real compilers on PATH, so a plain cc or gcc resolves to ccache without the build ever typing the word ccache. This is different from the case above: there is no launcher token for Bear to drop, because the build’s command line never named one. On a host where /usr/lib64/ccache/cc is a symlink to ccache:

bear -- cc -c main.c -o main.o

records "cc" as the compiler, exactly as invoked, not resolved past the symlink and not rewritten to gcc. Bear classifies the ambiguous cc name by probing --version (ccache passes that probe through to the real compiler), so the entry parses with the right flag family, but the recorded name stays what the build actually ran. This matches the contract in Supported compilers for ambiguous names.

Preload mode versus wrapper mode

Both interception modes handle an explicit launcher the same way: a build command of ccache gcc -c main.c -o main.o produces the identical single entry (compiler gcc, ccache dropped) whether intercept.mode is preload or wrapper.

Wrapper mode has one behavior preload mode does not need: when a build discovers its compiler through PATH or a CC variable and that lookup lands on a ccache masquerade directory (not an explicit ccache token), Bear resolves past the masquerade at discovery time and records the real compiler’s absolute path. With CC=gcc and a masquerade directory first on PATH, Bear logs

resolve: masquerade wrapper at /usr/lib64/ccache/gcc; re-resolving 'gcc' past /usr/lib64/ccache

and the entry records the real compiler the masquerade pointed at, never the masquerade symlink and never Bear’s own .bear/ wrapper. See “The recursion hazard” below for why this step exists.

distcc specifics

distcc’s own options that appear before the compiler name (-j, --jobs, -v, --verbose, -i, --show-hosts, --scan-avail, --show-principal) are recognized and skipped while Bear looks for the compiler, so they never get mistaken for it and never leak into the recorded arguments. distcc -j4 clang -c main.c therefore records clang -c main.c, not -j4 clang -c main.c.

icecc specifics

icecc follows the same contract as ccache, distcc, and sccache: the icecc token is dropped and the real compiler’s arguments are recorded as invoked. icecream’s icerun is a different tool (it runs arbitrary commands on the cluster, not compilations) and is not recognized as a launcher.

The recursion hazard, and how Bear avoids it

Wrapper mode works by prepending its own directory of compiler-named executables to PATH. A masquerade wrapper does the same thing for its own purpose. If Bear recorded the masquerade wrapper’s path as “the compiler” instead of resolving past it, the two directories would look each other up indefinitely: Bear’s wrapper would call the masquerade symlink, which would search PATH for the real compiler, find Bear’s own wrapper first, and call back into it. Bear avoids this by resolving its lookup PATH past any masquerade directory before recording what a bare compiler name points to, so the real compiler is what gets wrapped and recorded, never the masquerade link or Bear’s own wrapper. Detection is a filesystem check (does the resolved path’s target look like a known launcher binary), not a subprocess probe, and it only changes what Bear resolves, not the build’s own child environment: a build that actually wants ccache’s caching behavior for other invocations still gets it. Setting CCACHE_COMPILER or CCACHE_PATH, or stripping the masquerade directory from the build’s own PATH, were all considered and rejected, because each changes the build’s behaviour rather than only Bear’s lookup; the reasoning is recorded with the source, under docs/rationale/.

What does not work

  • A launcher wrapping another launcher (ccache distcc gcc -c main.c) produces no entry. Bear does not chase a chain of launchers; the first launcher’s argument has to be a real compiler.
  • A launcher whose argument is not a recognized compiler (ccache make all), or a bare launcher invocation with no argument at all, produces no entry.
  • A launcher under a name Bear does not recognize is not treated as a launcher at all: its own invocation produces no entry (it is not a compiler), though a real compiler it execs directly may still be recorded on its own. Add an override (below) to fix the launcher’s own entry instead of relying on that side effect.

A launcher at a nonstandard name or path

If the build wraps the compiler with a launcher under a custom path or a renamed binary, teach Bear the mapping in the configuration file, the same way you would hint an unrecognized compiler:

schema: "4.2"
compilers:
  - path: /opt/build-tools/my-ccache
    as: ccache

With this override, my-ccache gcc -c main.c -o main.o records the same single entry, compiler gcc, as a plain ccache invocation would. as accepts each launcher’s own name for the equivalent case with those tools, and wrapper as the generic spelling for any of them; see Configure Bear for the compilers: key in full, and Supported compilers for the accepted names, generated from Bear’s own definitions.

Related: Supported compilers for the recognized launcher and compiler names, Troubleshooting for a database that comes out wrong, and the Recipes index for other tasks.

Run Bear inside a Docker container

Run Bear inside the container, as part of the build it observes, the same way you would on a bare host:

RUN bear -- make -j4

or against an already-running container:

docker exec my-container sh -c "bear -- make -j4"

This is the working answer for the common case: a normal, dynamically linked build toolchain, building for the container’s own architecture. The rest of this page covers getting Bear into the image, the cases where the mechanism needs help, and the one problem that is specific to containers: the paths Bear records belong to the container’s filesystem, not the host’s.

Installing Bear in the image

Add Bear to the image the same way you would install it anywhere else: your distribution’s package if it carries a current-enough one, or a source build otherwise. Install Bear covers both; a source build inside a Dockerfile follows the same three commands as INSTALL.md:

# Install the Rust toolchain, a C compiler, and lld or mold first
# (see INSTALL.md for the exact prerequisites for your base image).
COPY . /src
WORKDIR /src
RUN cargo build --release && ./scripts/install.sh

Build Bear in the same image (or one with the same libc) it will run in. The preload library is a compiled shared object, so a copy built elsewhere only works if that build’s libc is no newer than the image’s, the same ABI constraint Troubleshooting describes for cross-compilation.

Why the build has to run inside the container

bear -- docker exec ... from the host does not work: docker exec hands the command to the Docker daemon, which runs it in the container’s own process tree, one the host-side Bear process never sees, so it never observes the compiler invocations. This is explained in full in Bear on Linux, WSL2, and Docker; this page takes it as the starting point, not something to reprove.

What the container needs for preload interception

Preload mode works inside a container with no extra privileges: LD_PRELOAD is a plain dynamic-linker feature, not a ptrace-based interception, so it needs no --privileged flag and no added capabilities. It does need:

  • a dynamic linker in the image. A scratch or fully static final image has none, so a build that runs in such an image cannot use preload mode there; build in a normal base image instead, or use wrapper mode (below).
  • the container’s libc to match what Bear’s preload library was built against, per the ABI note above.
  • a build tool that is itself dynamically linked. A statically linked build tool never loads the dynamic linker, so its own exec() calls are invisible to preload the same way they are on a bare host; this is not a Docker-specific limitation.
  • working loopback networking, for the TCP connection intercepted processes use to report back to bear-driver. Bear binds this on 127.0.0.1 (or [::1]), and the driver and the processes it observes are in the same network namespace, so the connection stays inside the container whichever --network mode started it.

When to fall back to wrapper mode

If the build tool inside the container is statically linked (a Go build using cgo is the common case; see How Bear works), set wrapper mode in the configuration file the build reads:

schema: "4.2"
intercept:
  mode: wrapper

As on any platform, a “configure” step that discovers compilers before the real build runs must itself run under Bear so it discovers the wrapper; see Bear produces an empty compile_commands.json.

The path-mapping problem

By default Bear records directory and file exactly as the build used them, with no transformation (format.paths). Inside a container that path is almost always absolute under the container’s own filesystem root, for example /src/main.c. clangd, clang-tidy, and similar tools normally run on the host, where /src does not exist; a compile_commands.json copied out of the container as-is points at files the host-side tool cannot find.

Three ways to deal with it, in order of how little they change:

  • Bind-mount the project at the same absolute path inside the container as on the host, so the recorded paths are already correct on both sides:

    docker run -v "$(pwd):$(pwd)" -w "$(pwd)" myimage bear -- make
    

    This needs no post-processing and is the simplest fix when the host and container can agree on a path.

  • Rewrite the path prefix after copying the database out, when the layouts cannot match (a different OS, or a host path the container cannot mount at the same location): substitute the container’s mount point for the host’s project root in directory and file with sed or jq before pointing clangd at the file.

  • Run the editor and language server inside the container too (a dev-container setup), so nothing outside the container ever reads the database, and the mismatch does not arise.

Related: Bear on Linux, WSL2, and Docker for the platform-level notes this page builds on, Troubleshooting for a database that comes out wrong, and the Recipes index for other tasks.

Generate compile_commands.json when cross-compiling

Run the cross build under Bear exactly as you would run it directly:

bear -- make

Bear records the cross compiler the same way it records a native one: the executable path the build actually invoked, and every argument verbatim, including a cross-compilation target prefix in the name (arm-none-eabi-gcc, aarch64-linux-gnu-g++) and flags such as --sysroot or a target-specific -I. Bear does not interpret or rewrite argument values (only @file response-file expansion and a couple of environment-variable foldings are optional exceptions, see format.arguments in Configure Bear), so a value like --sysroot=/opt/arm-sdk/sysroot lands in the entry’s arguments exactly as the build passed it. Nothing about the build changes; what is worth knowing is which names get recognized as cross-compiled, and one limitation of the default interception method that shows up specifically in cross builds.

Prefixed and version-suffixed names

GCC, Clang, NVIDIA’s nvcc, Flang, and Vala’s valac are recognized under a cross-compilation target prefix, a version suffix, or both together; every such spelling is parsed with its base name’s own flag rules. This support varies by family - it is not a blanket rule applied to every entry - so Cross-compiler prefixes and version suffixes is the source of truth for which families it covers and how the pattern works; this page only covers the cross-build-specific failure below.

When preload cannot reach the compiler

Preload is the default interception method on Linux and the BSDs, and it works for a cross build the same way it works for a native one, with one exception: the preload library is built for the host’s ELF class. Many vendor cross-toolchains still ship a 32-bit compiler driver binary even on a 64-bit host (common for older embedded SDKs distributed as prebuilt binaries); loading a 64-bit preload library into that 32-bit process fails with a “wrong ELF class” error from the dynamic linker. The build still completes, but that invocation is not intercepted, so its source file is silently missing from compile_commands.json.

A related but different failure is a glibc symbol-version mismatch, which Troubleshooting covers in full, including the commands to compare glibc versions and the fix (link a Bear build against a glibc no newer than the SDK’s).

Either failure means preload cannot inject into that particular compiler process. Switch to wrapper mode instead, since it substitutes the compiler executable rather than injecting into it, so it does not care about the target binary’s ELF class or its glibc:

schema: "4.2"
intercept:
  mode: wrapper

Wrapper mode needs the build to discover the wrapper as the compiler, so a “configure” step that detects compilers on its own has to run under Bear too; see Configure Bear for how to set the mode and How Bear works for what each method can and cannot reach.

Intercept a 32-bit build on a 64-bit host

A build that runs 32-bit compilers or 32-bit build tools on a 64-bit host records nothing from them: the build still exits 0, no error appears, and compile_commands.json just comes out short or empty. The preload library a normal Bear install ships is built for the host’s 64-bit ELF class, and the dynamic linker cannot inject a 64-bit shared library into a 32-bit process (see How Bear works for what preload does and why the ELF class has to match). Exit status covers why a missing preload library never turns into a non-zero exit: the dynamic linker treats it as something to warn about and skip, not to fail on, so nothing about the run tells you a source file went unrecorded.

On glibc Linux this is fixable: build a second, 32-bit preload library, install it alongside the normal 64-bit one, and point bear-driver at both with a single INTERCEPT_LIBDIR setting. INTERCEPT_LIBDIR names the directory bear-driver looks in for the preload library, relative to its own bin/, and on glibc it accepts the literal string $LIB as that name. The dynamic linker itself expands $LIB at load time, per process: lib64 when loading into a 64-bit process, lib when loading into a 32-bit one. One install then serves both kinds of process, provided both libraries exist on disk.

Get a 32-bit toolchain and Rust target

# Fedora
sudo dnf install glibc-devel.i686 libgcc.i686 libatomic.i686

# Debian / Ubuntu
sudo apt install gcc-multilib

rustup target add i686-unknown-linux-gnu

For any other distribution, install whatever it calls its 32-bit development packages; the two lines above are only confirmed for Fedora.

Work around the missing cross driver

The preload library’s build script compiles a small C shim with cc-rs, which looks for a i686-linux-gnu-gcc cross driver. Fedora (and any distribution that provides multilib through gcc -m32 rather than a separate cross-driver binary) has no such executable, so the build fails with:

error occurred in cc-rs: failed to find tool "i686-linux-gnu-gcc"

There is no supported fix for this yet; the workaround is a two-line shim on PATH that execs gcc -m32, pointed to by CARGO_TARGET_I686_UNKNOWN_LINUX_GNU_LINKER:

cat >/tmp/i686-linux-gnu-gcc <<'EOF'
#!/bin/sh
exec gcc -m32 "$@"
EOF
chmod +x /tmp/i686-linux-gnu-gcc
export PATH="/tmp:${PATH}"
export CARGO_TARGET_I686_UNKNOWN_LINUX_GNU_LINKER=/tmp/i686-linux-gnu-gcc

Build the 32-bit preload library

INTERCEPT_LIBDIR='$LIB' cargo build -p intercept-preload \
    --target i686-unknown-linux-gnu

This produces target/i686-unknown-linux-gnu/debug/libexec.so, a 32-bit libexec.so distinct from the normal 64-bit one.

Build and install the 64-bit Bear, then place both libraries

Build and install the regular 64-bit Bear with the same $LIB setting:

INTERCEPT_LIBDIR='$LIB' cargo build
SRCDIR=target/debug PREFIX=<prefix> INTERCEPT_LIBDIR='$LIB' ./scripts/install.sh

(See Install Bear for what PREFIX, DESTDIR, and INTERCEPT_LIBDIR each control; the $LIB value here is what makes this recipe work and is on top of what that page documents.)

install.sh does not understand $LIB as a dynamic-linker token: it creates a directory literally named $LIB under <prefix>/libexec/bear/ and installs the 64-bit library there. Finishing the layout is a manual step, and there is currently no supported way to have the build or install scripts do it for you:

cd <prefix>/libexec/bear
mkdir -p lib lib64
mv 'target/i686-unknown-linux-gnu/debug/libexec.so' lib/libexec.so
mv '$LIB/libexec.so' lib64/libexec.so
rmdir '$LIB'

The result differs from the ordinary layout in exactly one place: the single $INTERCEPT_LIBDIR directory becomes two, one per ELF class, and no directory named $LIB is left behind.

$PREFIX/
|-- bin/
|   `-- bear                              (shell script)
|-- libexec/
|   `-- bear/
|       |-- bin/
|       |   |-- bear-driver
|       |   `-- bear-wrapper
|       |-- lib/
|       |   `-- libexec.so                (ELF 32-bit)
|       `-- lib64/
|           `-- libexec.so                (ELF 64-bit)
`-- share/                                (unchanged)

Check the two libraries really are different ELF classes, and that the 32-bit one is the one under lib:

$ file <prefix>/libexec/bear/lib/libexec.so <prefix>/libexec/bear/lib64/libexec.so
libexec/bear/lib/libexec.so:   ELF 32-bit LSB shared object
libexec/bear/lib64/libexec.so: ELF 64-bit LSB shared object

Swapping them produces the same silent, short database as having no 32-bit library at all: a 32-bit process looks only in lib, finds an object of the wrong class, and skips it. That output is worth including in any report about a multilib install. The rest of the tree, and what PREFIX and DESTDIR mean, is in Install Bear.

Confirm it worked

Run the build under bear -- <build> as usual. If it drives any 32-bit compiler or a 32-bit build tool that execs a compiler on its own behalf, compare the entry count in compile_commands.json with and without lib/libexec.so in place: removing the 32-bit library drops exactly the entries that 32-bit process would have contributed, while the build itself still exits 0. With the library missing, the dynamic linker also prints the warning covered in Troubleshooting: LD_PRELOAD errors - cannot be preloaded (cannot open shared object file): ignored. - which is the tell that this is the missing-32-bit-library case rather than some other silent gap.

Limitations

  • This works only on glibc Linux. musl, macOS, and the BSDs have no $LIB-style token: they need a concrete library directory name instead, so one install cannot serve two ELF classes the same way.
  • The cc-rs shim above is a workaround for a missing cross driver, not a fix; a distribution that ships i686-linux-gnu-gcc directly does not need it.
  • scripts/install.sh has no option to produce the lib / lib64 split itself. Repeat the manual step after every reinstall.

This is a different problem from a vendor cross-toolchain whose compiler driver is itself a 32-bit binary; that case, and wrapper mode as its fix, is covered in Generate compile_commands.json when cross-compiling.

Generate compile_commands.json for an STM32 or arm-none-eabi project

Run the project’s Makefile under Bear:

bear -- make

Cross-prefixed GNU Arm Embedded GCC (arm-none-eabi-gcc, arm-none-eabi-g++) and Arm Compiler 6 (armclang, armclang++, the compiler behind Keil MDK) are the two toolchains Bear recognizes for a bare-metal Arm target; see Supported compilers for the full name table. arm-none-eabi-gcc is recognized through GCC’s general cross-compilation prefix support, covered in Generate compile_commands.json when cross-compiling; armclang has its own family instead.

Where STM32CubeIDE’s Makefile actually is

STM32CubeIDE does not build from a Makefile at the project root: it generates one per build configuration, under a subdirectory named for that configuration - Debug/Makefile, Release/Makefile, or a custom configuration’s own name - and that generated file is what the IDE’s “Build” button actually runs. Once a configuration has been built at least once from the IDE, run Bear against that Makefile directly:

cd Debug
bear -- make

If the IDE already finished building that configuration, make there has nothing left to do and Bear records nothing; force a full rebuild the same way Generate compile_commands.json for a Makefile project recommends for any other up-to-date build tree, for example bear -- make clean all. A plain CubeMX-generated Makefile project (no IDE) writes its Makefile at the project root instead of a per-configuration subdirectory, but the same rule applies: run Bear against the Makefile that actually compiles your sources, from clean if it is already up to date.

When preload cannot inject into the compiler

Vendor Arm toolchains, including some STM32CubeIDE and Keil MDK installations, still ship a 32-bit compiler driver binary even when installed on a 64-bit host. Preload, the default interception method on Linux, is built for the host’s own ELF class, so injecting it into a 32-bit process fails with a “wrong ELF class” error from the dynamic linker: the build still completes, but that invocation goes unintercepted and its source file is silently missing from compile_commands.json. Generate compile_commands.json when cross-compiling covers this failure and its glibc-mismatch cousin in full, including the intercept.mode: wrapper fix, which does not care about the target binary’s ELF class.

What clangd needs for a bare-metal entry

The entry Bear records names the cross compiler itself (arm-none-eabi-gcc or armclang) with the project’s flags intact: -mcpu, -mthumb, --specs, -I for the vendor HAL and CMSIS headers, and any -D for the target MCU. Without --query-driver pointed at that same cross compiler, clangd parses the entry with its own host Clang, and host Clang rejects flags that only GCC’s embedded target support understands: -mcpu=cortex-m4, -mthumb, and --specs=nano.specs each produce an “unknown argument” diagnostic instead of the target and header search path they set on the real compiler.

--query-driver=/path/to/arm-none-eabi-gcc fixes this by asking the real driver for its target and built-in include paths instead of guessing from host Clang’s own, the same mechanism used for a wrapper’s baked-in paths on Generate compile_commands.json for an MPI project. Without that option, strip the GCC-specific flags in .clangd instead so clangd stops rejecting the command line outright:

CompileFlags:
  Remove: [-mcpu=*, -mthumb, --specs=*]

See Set up clangd for a project without CMake for pointing your editor at the database once it exists.

Use Bear with a vendor embedded toolchain

Run the project’s Makefile under Bear exactly as you already build it:

bear -- make

This covers three vendor toolchains: QNX Momentics, Microchip’s MPLAB X (XC8), and Texas Instruments’ Clang-based Arm compiler. Each builds through a Makefile (MPLAB X generates one), and Bear needs nothing toolchain-specific once that Makefile runs under it. What differs between them is which executable name gets recognized as what; the full name table, as: ids, and prefix/suffix rules for every family are on Supported compilers and are not repeated here.

QNX

Bear recognizes qcc and q++, QNX Neutrino’s compiler drivers, under the qnx family, parsed with GCC’s flag rules (QNX 8 ships a GCC 12.2-based toolchain). This is a fixed pair of names: unlike GCC or Clang, qcc/q++ are not recognized under a cross-compilation prefix or a version suffix.

A QNX SDP installation also carries a set of ntoARCH-prefixed GCC binaries underneath the driver (ntoaarch64-gcc, ntox86_64-gcc, and similar, one per target architecture). These are recognized too, but as gcc, not qnx: GCC’s cross-compilation prefix pattern matches any name ending in -gcc, and QNX’s target names happen to fit that shape. A Makefile that calls one of these directly, rather than through qcc, still gets an entry, parsed with GCC’s own flag rules instead of QNX’s.

QNX’s -V flag picks the target/compiler variant on the command line (-Vgcc_ntoaarch64le); the qnx family models it as a single token that is never split and never swallows a following source file, matching both the attached form and the bare -V (which lists available variants).

Microchip XC8 / MPLAB X

Bear recognizes both xc8-cc (the GCC-styled driver name used from XC8 v2.30 onward) and the legacy xc8 name, and records both under the gcc family, not an XC8-specific id: XC8’s command-line syntax is GCC-styled, so Bear parses it with GCC’s own flag rules.

XC16 and XC32 need no entry of their own: their driver names, xc16-gcc and xc32-gcc, already match the general <prefix>-gcc cross-compilation pattern GCC is recognized under (see Generate compile_commands.json when cross-compiling). XC8 needed its own table row because xc8-cc and xc8 do not follow that prefix pattern.

MPLAB X generates a Makefile for every build configuration and uses it as the project’s real build entry point, the same way its own “Build” button does internally. Run bear -- make from the project directory that holds that generated Makefile.

Texas Instruments

Bear recognizes tiarmclang, TI’s Clang-based compiler for Arm targets, under the clang family (not a TI-specific id, and not armclang, which names Arm Ltd.’s own Compiler 6 instead).

TI’s earlier, non-Clang Code Generation Tools - the classic drivers for C2000, C6000, MSP430, and the pre-Clang Arm compiler (armcl, cl2000, cl430, cl6x) - are not in Bear’s recognition table at all. An invocation of one of these produces no entry: not a wrong one, none.

A compilers: entry pointing at the executable’s path can force one of these to be parsed with, for example, as: gcc, but that only helps to the extent its flag dialect actually matches GCC’s. It does not for the option that matters most for the recorded entry: these tools spell an output file --output_file=main.obj, not GCC’s -o main.obj, so the output field GCC’s rules populate from -o comes out empty, and every other flag the two dialects spell differently is misparsed the same way. The other option is filing an issue against the project; see what is not recognized.

Generate compile_commands.json for an Emscripten project

Run the build under Bear, with Bear as the outermost command:

bear -- emmake make

Bear captures every process the build tree starts, however many wrapper scripts sit in between, so it does not matter that emmake itself sets CC/CXX to emcc/em++ before handing off to make: those exec calls happen inside the tree Bear is already watching. Bear has to be the outermost command for that to hold, so bear -- emmake make is the order to use.

emcc, em++, and their .py-suffixed spellings are recognized under the clang family, not a separate emscripten id, since Emscripten’s driver is a Clang wrapper whose command line follows Clang’s flag syntax closely enough for Bear’s Clang rules to parse it correctly. There is no emscripten as value to write in a compilers: override; use clang if a nonstandard emcc path needs a hint. See Supported compilers for the full recognized-name table.

CMake projects: emcmake

For a CMake-based Emscripten project, emcmake only needs to run once, to generate a build tree configured for emcc/em++; the actual build is what Bear needs to watch:

emcmake cmake -B build
bear -- cmake --build build

This is the same two-step shape as the general CMake case in the bear(1) man page’s EXAMPLES: the generation step does not compile your project’s sources, so it does not need to run under Bear. In wrapper mode (the default on macOS and Windows, or forced with intercept.mode: wrapper), a step that discovers the compiler on its own has to run under Bear too, so run the generation step under Bear as well and discard its output, the same way Generate compile_commands.json for a Makefile project handles ./configure:

bear -- emcmake cmake -B build
bear -- cmake --build build

clangd and an emcc entry

clangd reads the recorded compiler as emcc, a binary it does not run and does not know the way it knows a plain clang. When it cannot infer a driver’s built-in include paths and default target from the recorded command alone, point it at the real driver directly with --query-driver, the same mechanism the bear(1) man page documents for MPI compiler wrappers:

clangd --query-driver=/path/to/emcc

See Set up clangd for a project without CMake for getting the database itself into a place clangd finds it.

Generate compile_commands.json for a CUDA project

Run the build under Bear exactly as you already run it:

bear -- make

Bear recognizes nvcc, NVIDIA’s CUDA compiler driver, under the cuda family (as: cuda), including a cross-compilation target prefix and a version suffix (aarch64-linux-gnu-nvcc, nvcc-12.0). The recorded entry looks like any other compiler entry, with nvcc-specific flags (--gpu-architecture, -gencode, -Xcompiler, and similar) parsed and kept in arguments verbatim:

[
  {
    "file": "main.cu",
    "arguments": ["nvcc", "-c", "main.cu", "-o", "main.o"],
    "directory": "/path/to/project",
    "output": "main.o"
  }
]

See Supported compilers for the full recognized-name table and how prefix/suffix recognition works in general.

clangd and clang-tidy do not speak the same nvcc dialect

nvcc’s command-line flags are its own, not Clang’s CUDA mode: options such as -gencode, --generate-code, -Xcompiler, -Xptxas, and --extended-lambda have no equivalent that Clang-based tooling accepts, so an entry recorded straight from an nvcc invocation can make clangd or clang-tidy reject the command line outright when they try to parse a .cu file with it. Bear itself has no option to filter or rewrite individual flags in a recorded entry; format.arguments in the bear(1) man page only covers @file expansion and a couple of environment-variable foldings, and sources only decides which files get an entry, not what their recorded arguments contain.

The fix is on the consumer side. A .clangd file can strip the unsupported flags before clangd parses the entry:

CompileFlags:
  Remove: [-gencode*, --generate-code*, -Xcompiler*, -Xptxas*, --extended-lambda]

If you would rather not have clangd touch .cu files at all, suppress its diagnostics on them the same way the bear(1) man page recommends for the valac entries of a mixed C/Vala project:

If:
  PathMatch: .*\.cu
Diagnostics:
  Suppress: '*'

Or drop CUDA sources from the database entirely with a sources rule in bear.yml (see CONFIGURATION in the man page):

sources:
  files:
    - pattern: "*.cu"
      action: exclude

nvcc’s host-compiler sub-invocations

nvcc splits a .cu file into a device-code path and a host-code path, and for the host side it generates an intermediate file and compiles that with the host compiler (gcc, clang, or MSVC’s cl, depending on platform) as a separate process. Bear intercepts that process the same way it intercepts nvcc itself, and because the host compiler’s intermediate file has a different name than the original .cu source, this sub-invocation does not collide with nvcc’s own entry under Bear’s default duplicate matching (directory and file): both land in compile_commands.json as separate entries, one for the .cu file and one for the generated intermediate file the host compiler actually saw. The intermediate file’s entry is rarely useful to an editor, since the file it names does not exist once the build finishes; exclude it the same way as .cu files above, with a pattern matching the intermediate filename your local nvcc generates (for example "*.cudafe1.cpp"), or scope a sources rule to directories instead if the host-compiler pass runs from its own temporary directory.

Use Bear with Intel oneAPI compilers

Source the oneAPI environment script for the current shell, then run the build under Bear exactly as you would for any other compiler:

source /opt/intel/oneapi/setvars.sh
bear -- make

setvars.sh is what puts icx, icpx, ifx, and the rest of the oneAPI drivers on PATH. Bear itself does nothing toolchain-specific at that point: it watches whatever executable the build actually runs, the same as for GCC or Clang. Skipping the source step is the most common reason a build “under Bear” still records the system compiler: the shell running make never had the oneAPI drivers on PATH to begin with, so bear -- make and a plain make invoke exactly the same programs.

Which family each driver is recognized as

icx/icpx (current oneAPI) and icc/icpc (the older Classic drivers) share one family id, intel_cc, and one Intel-specific flag table (-qopenmp, -ipo, -fp-model, and the rest). The Fortran drivers, ifort and ifx, are a separate family, intel_fortran. See Supported compilers for the full recognized-name table; bear semantic --print-compilers prints the same mapping for the version you have installed.

Intel’s MPI wrappers (mpiicc, mpiifx, and the rest) reuse these same two families rather than a generic MPI wrapper id. See Generate compile_commands.json for an MPI project for that mapping and why the split exists.

If a driver is not recognized

A build that calls the compiler through a renamed copy or a nonstandard path (a custom install, a CI image that symlinks icx to something else) needs a compilers: hint:

schema: "4.2"
compilers:
  - path: /opt/custom/bin/icx-wrapper
    as: intel_cc

Use as: intel_fortran for a renamed Fortran driver. See as and ignore hints for the accepted values, and the compilers section of Configure Bear for the key itself.

Use Bear with Cray and NVIDIA HPC compilers

Load the programming environment module for the current shell, then run the build under Bear:

module load PrgEnv-cray
bear -- make

On an HPE Cray system the toolchain is selected by which module is loaded, not by which package is installed: module load rewrites PATH (and other environment variables) for the shell that runs it. Once the right compiler is on PATH, Bear needs nothing else; it watches whatever the build executes, the same as anywhere else. The same applies to an NVIDIA HPC SDK module (module load nvhpc) on a cluster that provides one.

Recognized drivers

Cray’s own compiler drivers - craycc, crayCC, craycxx (cray_cc, built on Clang, with a few Cray-specific extensions such as -fcray-*) and crayftn/ftn (cray_fortran) - and the NVIDIA HPC SDK’s nvc, nvc++, nvfortran (nvidia_hpc) are all recognized directly. The legacy PGI names, pgcc, pgc++, pgfortran, map to the same nvidia_hpc family and its flag table (-Mvect, -acc, -gpu, and the rest), since the PGI compiler became the NVIDIA HPC SDK. See Supported compilers for the full name table.

cc, CC, and ftn: the PrgEnv wrapper names

cc and CC are ambiguous on a Cray system - either can front CCE, GCC, or another vendor’s compiler depending on the loaded programming environment module - so neither is in Bear’s static table; Bear classifies them by probing --version once and caching the result, the mechanism Supported compilers documents in general. The probe answers gcc or clang only, never cray_cc, so add a compilers: entry when you need the family pinned exactly, or when the probe cannot identify the compiler at all.

ftn, by contrast, is recognized directly as Cray Fortran (as: cray_fortran) regardless of which programming environment module is loaded. If a loaded module makes ftn front a different Fortran compiler on your system, override it the same way you would any misclassified compiler:

schema: "4.2"
compilers:
  - path: /opt/cray/pe/craype/default/bin/ftn
    as: intel_fortran

Generate compile_commands.json for an MPI project

Run the build under Bear exactly as you would without MPI:

bear -- make

A build that compiles through mpicc, mpicxx, mpifort, or one of the Intel MPI wrappers needs no special configuration: Bear recognizes all of them and records the wrapper invocation itself. See MPI compiler wrappers and Supported compilers for the full name table and the wrapper-is-recorded-as-invoked contract in general. This page covers what that contract means in practice: what actually lands in the recorded entry, and the one place the database can differ from what a reader expects.

Open MPI/MPICH vs. Intel

Open MPI’s and MPICH’s wrappers share one family id, mpi, and parse with the GCC flag table. Intel’s wrappers (mpiicc, mpiifort, and the rest) are recognized too, but as extra names on the intel_cc and intel_fortran families instead of the generic mpi id: mpiicc is icc/icx with MPI’s paths baked in, and Intel’s flag table has arity rules (for example -debug takes a following argument) that the GCC table would mis-parse. See Use Bear with Intel oneAPI compilers for those two families in general.

What actually lands in the entry

Given

mpicc -c main.c -o main.o

where mpicc is a script that execs gcc with -I/opt/mpi/include -L/opt/mpi/lib prepended, the entry Bear writes is

[
  {
    "file": "main.c",
    "arguments": ["mpicc", "-c", "main.c", "-o", "main.o"],
    "directory": "/path/to/project",
    "output": "main.o"
  }
]

arguments holds the command exactly as the build wrote it: no gcc, and none of the wrapper’s baked-in -I/-L flags, since Bear does not run the wrapper to learn them. Clang tooling that needs those flags to resolve MPI headers should point at the wrapper directly; clangd does this with --query-driver. See Set up clangd for a project without CMake for pointing an editor at the database this page produces.

The compiler the wrapper execs is usually not a second entry

In preload mode Bear does intercept the compiler the wrapper execs internally, not just the wrapper itself, but that rarely shows up as a second entry in compile_commands.json: the default duplicate filter (directory and file) treats the two invocations as the same compilation and keeps whichever comes first, the wrapper’s. That lower-level invocation surfaces as its own entry only if you widen duplicates.match_on to include arguments (see Configure Bear); ordinarily that is not useful, since it names an intermediate compiler call rather than the one your build system actually asked for.

MPICH’s compiler-override options, -cc=, -cxx=, and -fc=, are also accepted without the = (-cc gcc); either form is recognized as a driver option that takes a value, so it never gets misread as compiling a source file named gcc.

Troubleshooting

What to do when Bear runs but the result is not what you expected: an empty or short compile_commands.json, entries with the wrong flags, or a build that behaves differently under Bear than without it. If the database is completely empty, start with Bear produces an empty compile_commands.json, which works through the causes one by one; this page collects the rest.

Enable debug logging

Before investigating anything else, re-run the build with debug logging on standard error:

RUST_LOG=debug bear -- <build command>

Without RUST_LOG set, Bear prints only warnings and errors. Setting it to debug (or info/warn/error for less detail) switches every helper process (bear-driver, bear-wrapper, the preload library) to a verbose, timestamped format detailed enough to see which executables were intercepted, how each was classified, and why an entry was or was not written. Include this output when reporting a problem.

The output is missing entries

Same causes as an empty database, but partial - a compiler ran and was recognized, yet its entry did not survive to the file. The most common cause is --append (see Command-line options): each Bear run overwrites the output by default, so building incrementally without it discards the previous run’s entries. Accumulate results across runs instead:

bear --append -- make module_a
bear --append -- make module_b

New entries are placed before the existing ones, so a later rebuild’s entry for a given file takes precedence over the stale one (see the duplicates section below).

A file you know is compiled twice shows up once

If a project compiles the same source file more than once - once for a static library and once for a shared one, for example - and the database has only one entry for it, this is not --append: it is the default duplicate match dropping the second invocation. Building zlib 1.3.1 shows the effect directly. ./configure && bear -- make produces 17 entries, one per source file, even though zlib compiles every one of its 17 sources twice in the same directory: once for the static build, and once with -fPIC -DPIC for the shared one. The two invocations recorded for adler32.c:

gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -c -o adler32.o adler32.c
gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -DPIC -c -o objs/adler32.o adler32.c

Bear’s default duplicates.match_on compares only directory and file (see Configure Bear), and both invocations share both, so only the first is kept. Add arguments to the default match, keeping directory and file in it:

schema: "4.2"
duplicates:
  match_on:
    - directory
    - file
    - arguments

Rebuilding zlib with this configuration produces all 34 entries, two per source file. Keep directory in the list: a project with a util.c in two subdirectories, compiled with the same flags in each, would otherwise have one of them treated as a duplicate of the other and dropped. This is a trade-off, not a strict improvement: one entry per file per directory is what most Clang tooling wants, and matching on arguments too gives you every invocation at the cost of a larger database in which clangd (and most other consumers) still picks only one entry per file.

The output has duplicate entries

Two entries are duplicates only when every field listed in duplicates.match_on matches; of a set of duplicates, Bear keeps the first one in the file - the build’s first invocation within a single run, or (per the ordering described above) the newest one across --append runs.

The default match is directory and file, so one entry survives per source file per directory whatever its arguments were. If that is dropping invocations you wanted to keep, see A file you know is compiled twice shows up once above for the fix and its trade-off. Configure Bear enumerates the fields match_on accepts.

The output has extra entries

There are two different causes, and they need different fixes.

--append across runs carries forward entries from earlier invocations, including ones for files that no longer exist. Drop --append and rebuild from clean to get a database with only the current build’s entries:

make clean
bear -- make -j4

Without --append each Bear run overwrites the previous output, so bear -- ./configure followed by bear -- make leaves nothing behind from the configure run: the build run replaces the file wholesale.

A configure step inside a single Bear run is the other cause, and --append has nothing to do with it. bear -- sh -c './configure && make', or a Makefile that re-runs configure itself, puts the configure phase’s throwaway compiles (conftest.c and friends) in the same output as the real build. Split the two, so the build run’s output is the one that survives:

bear -- ./configure
make clean
bear -- make -j4

If the configure step has to run under Bear for the build to be intercepted at all - wrapper mode, where the configure step must discover the wrapper as its compiler - keep the two runs separate as above rather than chaining them with --append, or exclude the probe files with a sources rule.

The build behaves differently with Bear

Usually harmless: extra lines in the build’s own output, such as a loader warning about libexec.so, or additional environment variables Bear sets for its own bookkeeping. These do not change what gets compiled.

Worth reporting as a bug if the build’s actual result changes: a different binary, a step that fails only under Bear, or output written to an unexpected place. Two places to check first, since they are the things Bear adds to the build environment:

  • the .bear/ wrapper directory in wrapper mode, in case the build system has its own use for a directory of that name in the working directory;
  • environment variables Bear sets for interception (LD_PRELOAD/DYLD_INSERT_LIBRARIES in preload mode, a modified PATH in wrapper mode), in case the build inspects or forwards its environment in a way that is sensitive to them.

Report these with RUST_LOG=debug output attached.

LD_PRELOAD errors

ERROR: ld.so: object '.../libexec.so' from LD_PRELOAD cannot be preloaded: ignored.

This means the dynamic linker could not find or load libexec.so at the path bear-driver expects, relative to its own location (../$INTERCEPT_LIBDIR/libexec.so, lib by default). Check that Bear was built and installed with the same INTERCEPT_LIBDIR value; see Bear on Linux for the build/install commands. The warning does not stop the build or fail Bear: see Bear produces an empty compile_commands.json for why this is a silent cause of a database missing exactly the entries a subprocess would have reported.

glibc version errors in cross-compilation

.../libexec.so: version `GLIBC_2.33' not found (required by .../libexec.so)

This is a different failure from the one above: the linker finds libexec.so, but the library needs a newer glibc symbol version than the C library available to the process it was injected into. It shows up when the build runs compilers from a cross-compilation SDK whose sysroot ships an older glibc than the host Bear was built on - the preload library must be ABI-compatible with the libc the intercepted compiler process loads from the SDK sysroot, not only with the host’s. The intercepted invocation fails outright, so that command is silently missing from the database rather than reported as a warning.

Compare the highest glibc version Bear’s preload library requires against the highest the SDK’s libc provides:

# Highest glibc version Bear's preload library requires
strings <prefix>/libexec/bear/lib/libexec.so | grep -oE 'GLIBC_[0-9.]+' | sort -V | tail -1

# Highest glibc version the SDK toolchain's libc provides
strings /path/to/sdk/sysroot/lib/libc.so.6 | grep -oE 'GLIBC_[0-9.]+' | sort -V | tail -1

objdump reports the same requirement more precisely, if it is available:

objdump -T <prefix>/libexec/bear/lib/libexec.so | grep -oE 'GLIBC_[0-9.]+' | sort -uV | tail

If the version Bear’s library requires is newer than what the SDK’s libc provides, build (or obtain) a Bear linked against a glibc no newer than the SDK’s, and use that build for the cross-compilation. Building on a host whose own glibc is no newer than the SDK’s avoids the mismatch entirely.

Getting help

  1. Check bear --version against the latest release; distribution packages are often old, see Install Bear.
  2. Run with RUST_LOG=debug and read the output.
  3. Search existing issues.
  4. Check How Bear works if the behaviour itself is the surprise.
  5. Open a new issue with the debug log and your platform (OS, Bear version from bear --version, and build system).

Related: Command-line options and Configure Bear for the flags and keys named above, Bear produces an empty compile_commands.json for that case, the Recipes index for task-shaped pages, and the platform notes for Linux, macOS, Windows, and BSD.

Command-line options

bear runs in four modes, selected by the subcommand or its absence: combined (bear, no subcommand, the default), bear intercept, bear semantic, and bear parse-sh. Options given before -- belong to Bear; everything after -- is the build command, passed through untouched.

  • Combined runs the build, captures it, and writes the compilation database in one step. It is the default and the recommended way to run Bear.
  • bear intercept runs the build and only captures it, writing the raw event stream to a file for later analysis.
  • bear semantic reads an event stream and writes the compilation database, without running a build.
  • bear parse-sh writes the compilation database from shell command text, such as a build’s dry-run output or a saved build log, without running anything.

See How Bear works for the mechanism behind capture and analysis, Configure Bear for every key the -c/--config file accepts, and Exit status for what Bear’s own exit code means in each mode.

Global usage

bear [OPTIONS] -- <BUILD_COMMAND>...
bear [OPTIONS] <COMMAND>

Bear is a tool that generates a compilation database for clang tooling.

Subcommands

CommandDescription
interceptintercepts command execution
semanticdetect semantics of command executions
parse-shproduces the compilation database from build-system dry-run text (e.g. make -n output)
helpPrint this message or the help of the given subcommand(s)

Arguments

ArgumentDescription
<BUILD_COMMAND>...Build command

Options

FlagDescriptionDefault
-c, --config <FILE>Path of the config file-
-o, --output <FILE>Path of the result filecompile_commands.json
-a, --appendAppend result to an existing output file-
-h, --helpPrint help-
-V, --versionPrint version-

bear intercept

intercepts command execution

bear intercept --output <FILE> -- <BUILD_COMMAND>...

Arguments

ArgumentDescription
<BUILD_COMMAND>...Build command

Options

FlagDescriptionDefault
-o, --output <FILE>Path of the event file to write-
-h, --helpPrint help-

bear semantic

detect semantics of command executions

bear semantic [OPTIONS]

Options

FlagDescriptionDefault
-i, --input <FILE>Path of the event file to read-
-o, --output <FILE>Path of the result filecompile_commands.json
-a, --appendAppend result to an existing output file-
--print-compilersPrint the compilers Bear recognizes and exit-
-h, --helpPrint help-

Reads the event stream that bear intercept (or a third-party producer) writes and produces the compilation database, e.g.:

bear semantic --input events.json

bear parse-sh

produces the compilation database from build-system dry-run text (e.g. make -n output)

bear parse-sh [OPTIONS]

Options

FlagDescriptionDefault
-i, --input <FILE>Path of the shell text to parse-
-o, --output <FILE>Path of the result filecompile_commands.json
-a, --appendAppend result to an existing output file-
-C, --directory <DIR>Initial working directory for the parsed commands-
-h, --helpPrint help-

Parses the text a build system prints in dry-run mode and writes the compilation database, without running a build, e.g.:

make -n | bear parse-sh

Configure Bear

Bear reads an optional bear.yml. Every key is optional except schema, which is required once you write a file at all; every section below states its default, so a key you do not write keeps that value. The canonical source for every key, accepted value, and default is the bear(1) man page CONFIGURATION section; this page mirrors it, organized one section at a time, with the shape a user would actually write next to its default.

Where the file is found

Without --config, Bear searches for bear.yml in the current working directory first, then the platform’s per-user configuration directory (the XDG locations on Linux and the BSDs, %LOCALAPPDATA% / %APPDATA% on Windows). The first file found is loaded; the rest are not consulted. The exhaustive, ordered list of paths is the man page’s FILES section. Pass -c/--config FILE to load a specific file instead of searching.

schema

schema: "4.2"

Names the configuration format version this file is written for. It is the one key with no default: a file that omits it, or names a version this Bear release does not support, is rejected rather than partially applied.

intercept

intercept:
  mode: wrapper
  • intercept.mode: preload or wrapper. Default: preload on Linux and the BSDs, wrapper on macOS and Windows.

compilers

compilers:
  - path: /usr/bin/cc
    as: gcc
  - path: /usr/local/bin/gcc
    ignore: true

Default: [] (no overrides; every compiler is recognized automatically).

  • compilers.path: Path of the compiler executable. Required. A path on its own, with no other key, is already enough to make an otherwise unrecognized executable be treated as a compiler.
  • compilers.as: Compiler type hint. Optional. The accepted values are the family ids listed on Supported compilers, which is generated from Bear’s own compiler definitions, plus wrapper for a compiler launcher (see Use Bear with ccache, distcc, or icecc). bear semantic --print-compilers prints the same set for the version you have installed. When as is omitted, Bear guesses the family from the executable’s filename using its normal recognition patterns, falling back to gcc when no pattern matches; an MSVC-style or otherwise unusual compiler whose name matches nothing is then parsed with GCC’s flag rules unless as is set explicitly.
  • compilers.ignore: Exclude this executable’s invocations from the database. Optional. Default: false.

sources

sources:
  directories:
    - path: /project/tests
      action: exclude
  files:
    - pattern: "moc_*.cpp"
      action: exclude

Default: {} (both lists empty; nothing is filtered out).

  • sources.directories.path, sources.directories.action: list of directory rules; action is include or exclude.
  • sources.files.pattern, sources.files.action: list of filename-glob rules; action is include or exclude.

duplicates

duplicates:
  match_on:
    - file
    - arguments
  • duplicates.match_on: list of entry fields to compare: file, arguments, directory, command, output. Two entries are duplicates when all configured fields match; the first occurrence is kept. Default: [directory, file].

format

format:
  paths:
    directory: canonical
    file: canonical
  entries:
    use_array_format: true
    include_output_field: true
  arguments:
    from_response_files: false
    from_environment: true

format.paths

  • format.paths.directory, format.paths.file: as-is, canonical, relative, or absolute. Default: as-is for both.

format.entries

  • format.entries.use_array_format: write the arguments array instead of the command string. Default: true.
  • format.entries.include_output_field: include the output field. Default: true.

format.arguments

  • format.arguments.from_response_files: expand @file response-file references into their tokenized contents. Default: false.
  • format.arguments.from_environment: fold compiler environment variables (CPATH, C_INCLUDE_PATH, CPLUS_INCLUDE_PATH, OBJC_INCLUDE_PATH, MSVC’s CL / _CL_) into the recorded arguments. Default: true.

headers

headers:
  enabled: true
  strategy: dependency-files
  • headers.enabled: turn header-entry synthesis on. Default: false.
  • headers.strategy: siblings or dependency-files. Default: siblings.

Checking what is actually in effect

Bear logs its fully resolved configuration as YAML whenever RUST_LOG is set to info or a more verbose level:

RUST_LOG=info bear -- true

This is the same line whether the values came from a bear.yml or from built-in defaults, so it is the way to check what a given file actually changed on this machine, rather than working it out from the sections above.

See also: How Bear works for the interception and semantic-analysis mechanism this configuration shapes, Supported compilers for compiler recognition, and the Recipes for task-oriented uses of these sections (for example excluding generated sources).

Supported compilers

Bear decides whether an intercepted command is a compilation by matching the name of the executable the build ran against a table of known compiler names. A name it does not know is not a compiler, and Bear does not run the program to guess otherwise. The one exception is a short list of names that are genuinely ambiguous across platforms, and Ambiguous names below says which and why. Once a name matches, the command line is parsed with that family’s own flag rules, since GCC, MSVC, and Swift (to pick three) do not agree on how to spell an include path or an output file. This page explains how that matching works and lists the names it currently covers; the configuration keys that let you extend or correct it are in the bear(1) man page and explained in Configure Bear.

Cross-compiler prefixes and version suffixes

A handful of core families are recognized under more than their plain name. GCC and Clang (and, following the same pattern, CUDA’s nvcc, Flang, and Vala’s valac) are also recognized with a cross-compilation target prefix (arm-linux-gnueabihf-gcc, aarch64-linux-gnu-clang), a version suffix (gcc-12, clang-15), or both together (arm-linux-gnueabi-gcc-12). Every such spelling is parsed with its base name’s flag rules. Support for this varies by family: it is not a blanket rule applied to every entry in the table below.

Compiler launchers

ccache, sccache, distcc, and icecc wrap a real compiler invocation rather than compiling anything themselves. Bear recognizes each launcher by name, finds the real compiler among its arguments, and records the compilation as if the launcher were not there: ccache gcc -c main.c is recorded as a gcc invocation, not a ccache one. A launcher invocation whose argument is not a recognized compiler (ccache make all), or that wraps another launcher (ccache distcc gcc -c main.c), produces no entry; Bear does not chase a chain of launchers. distcc’s own options (-j, --jobs, -v, and similar) are skipped while looking for the compiler, so they never get mistaken for one.

MPI compiler wrappers

mpicc, mpicxx, mpifort, and the vendor-specific MPI wrappers (Intel’s mpiicc, mpiifort, and similar) are recorded as invoked, not expanded to the compiler they wrap. Clang tooling that needs a wrapper’s baked-in include paths can point at the wrapper directly, for example with clangd’s --query-driver. An information-only invocation (mpicc -showme, -show, -compile_info, -link_info) produces no entry, and a wrapper option that carries a value (MPICH’s -cc=gcc) never swallows a following source file.

Ambiguous names

cc, c++, and the HPE Cray PrgEnv wrapper CC are not in the name table at all, because the same basename is a different compiler depending on the platform or the loaded environment module (GCC on most Linux distributions, Clang on the BSDs and macOS, whatever the loaded programming environment selects on a Cray system). Bear classifies these by running the executable once with --version and caching the result; a compilers: entry in the configuration (see below) skips the probe and forces a classification when its output does not match a known signature.

as and ignore hints

A compilers: entry in the configuration names a path and either an as value (the family to parse it with) or ignore: true (drop its invocations entirely). Reach for it when a compiler sits at a path or under a name the table below does not cover, or when a generic name is misclassified. The generic names cc and c++ are the usual reason to add an entry, since not every custom build of GCC or Clang answers --version in a way the probe recognizes.

An as value is a family identifier, not a display name: it is the short, lower-case id given as each family’s “Configuration as: value” below, spelled verbatim. There are no aliases, so as: gcc is accepted while as: GCC is not, and several families share one id (icx and icc are both intel_cc). The accepted ids are the ones in the family tables below, which are generated from the compiler definitions, plus wrapper for a compiler launcher. This page does not repeat them here as a list: the tables are the copy that cannot drift. An unaccepted value is rejected when the configuration loads, with an error listing every value that would have worked, and bear semantic --print-compilers prints the same mapping for the version you have installed.

Recognized families

Internal jobs that a driver spawns for itself are recognized only so Bear can filter them back out: they are never user-facing invocations and never produce a database entry. The tables below label each one as internal, separately from the family’s real, user-facing names.

The compiler launchers share one kind rather than a compiler family, so wrapper, or any launcher’s own name from the launcher table below, selects it.

Compiler families

armclang

Configuration as: value: armclang.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
armclang, armclang++Arm Compiler 6armclang-12not recognizeddeveloper.arm.com

clang

Configuration as: value: clang.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
clang, clang++Clangclang-12arm-linux-gnueabihf-clangclang.llvm.org
amdclang, amdclang++, hipccAMD ROCm Clang/HIPnot recognizednot recognizedrocm.docs.amd.com
emcc, em++, emcc.py, em++.pyEmscriptennot recognizednot recognizedemscripten.org
tiarmclangTexas Instruments Arm Clangnot recognizednot recognizedti.com

An invocation is also ignored when its arguments include -cc1: that is an internal frontend or codegen call, not a user-facing compile.

clang_cl

Configuration as: value: clang_cl.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
clang-clClang (MSVC mode)clang-cl-12not recognizedclang.llvm.org

cray_cc

Configuration as: value: cray_cc.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
craycc, crayCC, craycxxCray C/C++craycc-12not recognizedsupport.hpe.com

cray_fortran

Configuration as: value: cray_fortran.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
crayftn, ftnCray Fortrancrayftn-12not recognizedsupport.hpe.com

cuda

Configuration as: value: cuda.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
nvccNVIDIA CUDAnvcc-12arm-linux-gnueabihf-nvccdocs.nvidia.com

fasm

Configuration as: value: fasm.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
fasmflat assemblernot recognizednot recognizedflatassembler.net

flang

Configuration as: value: flang.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
flang, flang-newFlangflang-12arm-linux-gnueabihf-flangflang.llvm.org
amdflangAMD ROCm Flangnot recognizednot recognizedrocm.docs.amd.com

gcc

Configuration as: value: gcc.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
gcc, g++, gfortran, egfortran, f95GCCgcc-12arm-linux-gnueabihf-gccgcc.gnu.org
xc8-cc, xc8Microchip XC8not recognizednot recognizedmicrochip.com

Internal, not user-facing: cc1, cc1plus, cc1obj, cc1objplus, f951, collect2, lto1. Bear recognizes these only so it can filter them back out; they never produce a database entry.

ibm_xl

Configuration as: value: ibm_xl.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
ibm-clang, ibm-clang++IBM Open XL C/C++ibm-clang-12not recognizedibm.com
xlclang, xlclang++IBM XL C/C++xlclang-12not recognizedibm.com

intel_cc

Configuration as: value: intel_cc.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
icx, icpxIntel oneAPI C/C++icx-12not recognizedintel.com
icc, icpcIntel C++ Compiler Classicicc-12not recognizedintel.com
mpiicc, mpiicpc, mpiicx, mpiicpxIntel MPI C/C++ wrappernot recognizednot recognizedintel.com

intel_fortran

Configuration as: value: intel_fortran.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
ifort, ifxIntel Fortranifort-12not recognizedintel.com
mpiifort, mpiifxIntel Fortran MPI wrappernot recognizednot recognizedintel.com

mpi

Configuration as: value: mpi.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
mpicc, mpicxx, mpic++, mpiCC, mpifort, mpif77, mpif90MPI compiler wrappermpicc-12not recognizedopen-mpi.org, mpich.org

msvc

Configuration as: value: msvc.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
clMicrosoft Visual C++not recognizednot recognizedlearn.microsoft.com

Internal, not user-facing: c1, c1xx, c2. Bear recognizes these only so it can filter them back out; they never produce a database entry.

nasm

Configuration as: value: nasm.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
nasm, yasmNASM / YASM assemblernot recognizednot recognizednasm.us, github.com

nvidia_hpc

Configuration as: value: nvidia_hpc.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
nvc, nvc++, nvfortranNVIDIA HPC SDKnvc-12not recognizeddocs.nvidia.com
pgcc, pgc++, pgfortranPGI (legacy NVIDIA HPC)pgcc-12not recognizeddocs.nvidia.com

qnx

Configuration as: value: qnx.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
qcc, q++QNX qccnot recognizednot recognizedqnx.com

swift

Configuration as: value: swift.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
swiftcSwiftnot recognizednot recognizedswift.org

Internal, not user-facing: swift-frontend. Bear recognizes these only so it can filter them back out; they never produce a database entry.

An invocation is also ignored when its arguments include -frontend: that is an internal frontend or codegen call, not a user-facing compile.

vala

Configuration as: value: vala.

Executable namesRecognized asVersion suffixCross-compilation prefixDocumentation
valacValavalac-12arm-linux-gnueabihf-valacvala.dev

Compiler launchers

Executable nameRecognized asConfiguration as: valueDocumentation
ccacheCompiler cacheccacheccache.dev
distccDistributed compilerdistccdistcc.org
iceccDistributed compilericeccgithub.com
sccacheCompiler cachesccachegithub.com

This list is kept in step with Bear’s own compiler definitions, and grows by request as users bring toolchains that are not covered yet; it is not a fixed set.

What is not recognized

A few names are excluded on purpose, not by oversight:

  • as (the GNU assembler): GCC and Clang already spawn it on a temporary file for every ordinary compile, so recognizing it would add a throwaway entry per compilation, keyed on a name that differs every run.
  • mpirun, mpiexec: these launch programs, they do not compile anything.
  • swift: this is the Swift subcommand dispatcher (swift build, swift run); the actual compiler invocation is swiftc.
  • amdgpu-arch: reports target GPU architectures; despite the shared prefix, it is not a compiler driver.
  • ml, ml64 (MASM): not currently supported.

None of these can be recovered with a compilers: override, because as only redirects a path to one of the families already in the tables above; it cannot teach Bear a new one. If your build uses a toolchain that genuinely is not covered anywhere in this page, that is a gap in Bear’s compiler definitions, not a configuration problem: it is worth filing as an issue against the project.

See also: Configure Bear for the compilers: section in full, and How Bear works for where recognition fits between interception and writing the database.

Exit status

Modes that run a build report the build’s own outcome; modes that only transform data report whether Bear understood its input.

ModeExit status
bear -- <build> (combined)The build’s exit code, byte for byte: 0 on success, the same non-zero code otherwise.
bear intercept -- <build>Same as combined: the build’s exit code.
bear semantic0 when at least one input event was understood, or the input was empty. Non-zero when the input was non-empty and every event was rejected.
bear parse-sh0 when at least one line parsed as a shell command, even when none of them named a recognized compiler, or when the input was empty. Non-zero when the input was non-empty and every line was skipped.
bear semantic --print-compilersAlways 0.
bear --help (or a subcommand’s --help)Always 0.
A malformed invocation (unknown flag, missing build command)Non-zero (2), reported before anything runs.

See command-line options for what each mode does.

Notes

A build killed by a signal never exits 0, but the signal number is not encoded. bear -- sh -c 'kill -TERM $$' exits 1, and so does a build killed with SIGKILL. Bear does not follow the 128 + signal shell convention: a caller that only inspects Bear’s own exit code cannot tell a signalled build from an ordinary failing one. The signal itself still reaches the build unchanged.

Empty input is not an error. An empty semantic or parse-sh run writes an empty database, exits 0, and prints a notice on standard error so an accidentally empty pipeline does not pass unnoticed. The same holds for parse-sh input that parses cleanly but names no compiler invocation, such as a lone mv command: no line is skipped, no diagnostic is printed, and the empty database is a success.

Bear can fail on its own, independent of the build or the input. A configuration file that fails to parse, an invalid combination of flags (for example directing the compilation database to standard output while also running a build), or a named input file that does not exist are all reported before any build runs or any input is read, and all exit non-zero. An output path that cannot be written (a directory sitting where the database file should go) is also non-zero, and this one can surface after the build already succeeded: the build’s own outcome does not change, but Bear’s exit code still reports non-zero because it could not deliver its result.

A missing preload library is not one of these failures, and the consequence is worth knowing: the dynamic linker ignores a preload target it cannot open, prints its own warning on standard error, and lets the build run. The build’s own commands are still observed, but the processes it spawns are not, so a build whose compiler runs under make yields an empty database and an exit status of 0. Success here means the build succeeded, never that the database is complete. See Bear produces an empty compile_commands.json.

For what to do about a non-zero exit, see troubleshooting.

How Bear works

Producing a compilation database is two separate problems: capturing which commands a build runs, and recognizing which of those commands are compilations worth recording. Bear keeps the two apart. A capture stage produces a stream of execution events (program, arguments, working directory, environment); a semantic analysis stage turns the events it recognizes as compiler invocations into compilation-database entries. Combined mode runs both in one process; bear intercept and bear semantic run them as two separate steps over a file in between, and bear parse-sh replaces capture entirely by reconstructing events from text instead of running anything. This page explains the mechanism; for the commands themselves see Getting started with Bear and the Recipes.

Two ways to capture a real build

Interception observes the build as it runs. Bear has two mechanisms for it, and uses exactly one per invocation, chosen before the build starts. Which one is the default on your system is on its platform page - Linux, macOS, Windows, BSD - and Configure Bear explains how to force the other one.

Preload injects a small shared library into every process the build starts, using the dynamic linker’s library-preloading facility (LD_PRELOAD on Linux and the BSDs, DYLD_INSERT_LIBRARIES on macOS). The library intercepts the exec family of calls (and posix_spawn, popen, system) before they reach the kernel, reports the call, then lets it proceed unchanged. Because it hooks the mechanism every process uses to start another one, it sees every compiler invocation regardless of how the build found it, and the build itself is unaware anything is watching. It cannot see into a statically linked executable, because such a binary never goes through the dynamic linker in the first place. It cannot run at all on Windows, which has no preloading facility to hook and for which the library is not even built, nor on a macOS host with System Integrity Protection enabled, since SIP strips DYLD_INSERT_LIBRARIES from protected executables before it can take effect.

Wrapper takes the opposite approach: instead of watching every process, it puts a reporting executable where the compiler is expected to be. Bear creates a .bear/ directory holding one wrapper for each compiler it recognizes on PATH at startup, puts that directory first on PATH, and points compiler-selection variables (CC, CXX, and the like) at it. When the build launches what it thinks is the compiler, it launches the wrapper instead; the wrapper reports the call, then execs the real compiler found at its resolved, absolute path so the build sees no difference. This works on every platform, including Windows, but only for compilers the build actually reaches through PATH or a variable Bear resolved at startup: a build step that discovers the compiler on its own (a ./configure probing for cc) needs to run under Bear too, so the wrapper is in place by the time the real build starts.

Neither mechanism is a fallback for the other. Where preload is unavailable, Bear does not quietly switch to wrapper mode; forcing preload there is a startup error that names wrapper mode as the alternative, and the build does not run.

From an intercepted call to the driver

Both mechanisms report through the same channel: the process running the build (bear-driver, from the bear-driver binary) opens a TCP listener on the loopback interface, and every preload library instance or wrapper process connects to it and writes one length-prefixed JSON event per execution. This keeps the reporting side dependency-free and lets interception and semantic analysis live in separate processes when running as bear intercept and bear semantic, with the event stream as the file in between. The shared pieces of this runtime (the execution type, the wire encoding, environment-variable filtering) live in the intercept crate; the driver-side half that supervises the build, runs the TCP collector, and prepares the wrapper directory or preload environment lives in intercept-supervisor; the injected library itself is intercept-preload, and the wrapper executable’s own code is bear-wrapper.

Reconstructing events without running anything

bear parse-sh takes a third path that skips capture entirely: instead of observing real execution, it parses shell command text, typically a build system’s dry-run output (make -n) or a saved build log, and reconstructs the executions that text describes. It understands a documented subset of sh syntax and make’s recursive-directory markers, and feeds what it reconstructs through the same semantic analysis as a real build. This is inherently lower fidelity: a dry run can omit commands a real build would issue, and the environment used to resolve bare executable names is the parsing environment, not the build’s. It exists for builds that cannot be run at all, such as recovering a database from a CI log after the fact; interception remains the higher-fidelity default whenever the build can run.

Semantic analysis

Whatever produced the event stream, from here the processing is the same. Each event’s executable name is checked against Bear’s compiler definitions, including cross-compiler prefixes, version suffixes, and compiler launchers such as ccache and distcc that carry the real compiler in their own arguments; see Supported compilers for how that recognition works. An event that names no recognized compiler produces nothing.

A recognized invocation is then turned into zero, one, or several entries, depending on the compiler and what the invocation asked it to do:

  • most compilers treat each source file as its own translation unit, so an invocation naming several sources yields one entry per source, each stripped down to look like a command that compiled only that file;
  • a compiler that always compiles a whole target as one unit (Vala’s valac) collapses to a single entry keyed on the first source, with every source kept in its arguments;
  • a compiler whose sources depend on each other within one invocation (Swift’s swiftc in whole-module mode) produces one entry per source, but every entry keeps the complete invocation’s arguments;
  • an invocation that only links, or only asks for information (--version, preprocessing, --help), produces no entry at all.

From there, the entries pass through whatever the configuration requests: dropping generated sources by directory or filename, collapsing duplicate entries, resolving or leaving paths as they are, choosing between an arguments array and a command string, and optionally synthesizing entries for header files. Configure Bear explains each of these.

Output

The result is written as a JSON array of entries conforming to the Clang JSON Compilation Database specification, the format clangd, clang-tidy, and similar tools read to know how each source file is compiled.

See also: Getting started with Bear for the first run, the Recipes for specific tasks, and Troubleshooting for a database that came out empty or wrong.

Bear on Linux, WSL2, and Docker

Preload is the default interception method on Linux (see how Bear works for the mechanism, and Configure Bear for the setting). It cannot see into a statically linked build tool; force intercept.mode: wrapper instead.

WSL2

If .wslconfig sets networkingMode=mirrored, the loopback connection intercepted processes use to report back to bear-driver breaks, producing an empty or short compile_commands.json with no other error. Remove the setting and restart WSL2 (wsl --shutdown).

Docker

Bear must run inside the container, as part of the build it observes; bear -- docker exec ... from the host does not work. See Run Bear inside a Docker container.

Installing the preload library to a non-standard lib directory

INTERCEPT_LIBDIR=lib64 cargo build --release
INTERCEPT_LIBDIR=lib64 ./scripts/install.sh

A mismatch here fails silently: preload never loads and the database comes out empty or short. See Installing Bear for why.

Related: Troubleshooting for output that comes out wrong, and the Recipes index for other tasks.

Bear on macOS

Wrapper is the default interception method on macOS: Apple-signed Xcode compilers block preload, because System Integrity Protection strips DYLD_INSERT_LIBRARIES before it can take effect (see how Bear works). Preload can be forced with intercept.mode: preload, but only works with SIP disabled.

Building through Xcode or xcodebuild

Xcode passes many flags through an @file response file, and Bear’s default recording keeps that argument literal. Turn on format.arguments.from_response_files so an entry built through xcodebuild carries the actual flags.

Homebrew-installed compilers

Homebrew’s GCC formula installs only versioned binaries (gcc-14, not gcc); Bear recognizes these by filename automatically (see Supported compilers).

Related: how Bear works for the preload/wrapper mechanism, Troubleshooting for output that comes out wrong, and the Recipes index for other tasks.

Bear on Windows

Wrapper is the only interception method on Windows: there is no LD_PRELOAD/DYLD_INSERT_LIBRARIES equivalent, so forcing preload is a startup error before anything runs (see how Bear works and Configure Bear).

MSYS2 and MinGW64 environments

Bear builds under MSYS2’s Unix-like environments (MINGW64, UCRT64, CLANG64, CLANGARM64). $MINGW_PREFIX points at whichever one you launched (/mingw64, /ucrt64, /clang64, and so on), the natural PREFIX for installing into it:

DESTDIR="$pkgdir" PREFIX="$MINGW_PREFIX" ./scripts/install.sh

Only bear-driver and bear-wrapper are installed in this configuration; there is no preload library to package.

Related: how Bear works for the wrapper mechanism, Troubleshooting for output that comes out wrong, and the Recipes index for other tasks.

Bear on BSD

Bear runs on FreeBSD, OpenBSD, NetBSD, and DragonFly BSD with the same defaults as Linux: preload is the default interception method (see how Bear works and Configure Bear), and it cannot see into a statically linked build tool. What differs is packaging and the default compiler and linker names.

cc and c++ mean Clang here

On FreeBSD, OpenBSD, NetBSD, and DragonFly BSD (and on macOS), the base system’s cc/c++ is Clang, not GCC as on most Linux distributions. Bear resolves this by probing --version; no configuration is needed for the common case. See Supported compilers to override when the probe cannot classify a compiler.

Installing

FreeBSD packages Bear (pkg install bear). On OpenBSD, NetBSD, and DragonFly BSD, build from source per INSTALL.md; check your ports/pkgsrc tree first.

Related: how Bear works for the preload mechanism, Troubleshooting for output that comes out wrong, and the Recipes index for other tasks.