The environment that moves your search paths
Source references in this chapter are to GCC 15.2.0 (releases/gcc-15.2.0).
The build works on your machine and fails in CI. Same source, same compiler version, same command line — you have checked the command line three times.
Then check the environment. Five variables steer the GCC driver, and none of them appears on any command line you can read. They split cleanly into two groups: ones you set to move its search paths, and ones the driver sets so that its own subprocesses agree with it.
| Direction | Variables |
|---|---|
| In — you set them, the driver reads them | GCC_EXEC_PREFIX, COMPILER_PATH, LIBRARY_PATH, LPATH |
| Out — the driver sets them, its children read them | COLLECT_GCC, COLLECT_GCC_OPTIONS, COLLECT_LTO_WRAPPER, COLLECT_AS_OPTIONS |
This chapter assumes Chapter 1.5, because the first group feeds exactly the lists that chapter built.
The three you might set
| Variable | Feeds | Works on a cross compiler? |
|---|---|---|
GCC_EXEC_PREFIX | programs and libraries, plus header search | yes |
COMPILER_PATH | programs and headers | yes |
LIBRARY_PATH | libraries | no — native only |
LPATH | as LIBRARY_PATH, legacy | no — native only |
Their priority is settled by Listing 5-3 in the previous chapter: the enum has two
values, and all three of these land at PREFIX_PRIORITY_LAST. So -B beats the
environment, and the environment beats the compiled-in defaults, with no way to
sort anything before a -B.
LIBRARY_PATH does nothing on a cross compiler
This one wastes a lot of people's afternoons, and the reason is one clause:
temp = env.get (LIBRARY_PATH_ENV);
if (temp && *cross_compile == '0')
Listing 6-1: the native-only guard (gcc/gcc.cc:4923-4924; LPATH
gets the same treatment at :4956-4957).
*cross_compile == '0' means "this is a native compiler". So if you are debugging
why LIBRARY_PATH has no effect on your arm-linux-gnueabihf-gcc, that is the
entire answer. Use -B or -L instead.
GCC_EXEC_PREFIX and COMPILER_PATH have no such guard and work on a cross
(gcc.cc:4889).
Note also LIBRARY_PATH_ENV, which is a macro rather than a literal
(gcc.cc:209-211) — a port can rename the variable, and some do.
GCC_EXEC_PREFIX is the relocation mechanism, seen from outside
You already met this variable in Chapter 1.5 without setting it. Its documented
default is $prefix/lib/gcc/, and what makes it interesting is that when you
don't set it, the driver derives it from argv[0] and exports it anyway —
Listing 5-4. Setting it by hand merely pre-empts that computation.
When it is set, by you or by the driver, it does three things:
set_std_prefix (gcc_exec_prefix, len);
add_prefix (&exec_prefixes, gcc_libexec_prefix, "GCC",
PREFIX_PRIORITY_LAST, 0, 0);
add_prefix (&startfile_prefixes, gcc_exec_prefix, "GCC",
PREFIX_PRIORITY_LAST, 0, 0);
Listing 6-2: what GCC_EXEC_PREFIX feeds (gcc/gcc.cc:4879-4884).
The two add_prefix calls are the search lists. The set_std_prefix call is the
one people miss: it perturbs header search too, by telling
gcc/prefix.cc what to substitute for the configure-time prefix in compiled-in
directory names. That is how a relocated toolchain finds its own <stdint.h>, and
Chapter 1.9 shows the consuming code.
Two smaller behaviours worth knowing:
A trailing /lib/gcc/ is stripped. There is a small block that recognises that
suffix and shortens the length before use (gcc.cc:4864-4877), so
pointing the variable at either $prefix/ or $prefix/lib/gcc/ behaves the same.
That is a kindness, not a coincidence.
It relocates the sysroot too, but only if the toolchain was configured with the
sysroot inside $exec_prefix (gcc.cc:5542-5559). Chapter 1.5
covers the condition.
And one related flag, which is really about this mechanism: -no-canonical-prefixes
changes which relative-prefix function gets used, so a symlinked driver resolves
to the symlink's directory rather than the real one:
if (decoded_options[j].opt_index == OPT_no_canonical_prefixes)
{
get_relative_prefix = make_relative_prefix_ignore_links;
break;
}
Listing 6-3: -no-canonical-prefixes (gcc/gcc.cc:4807-4816).
Note the comment above it: this has to be handled early, before normal option processing, because the prefixes it affects are needed to build the default search paths. It is the only option in the driver treated that way.
Proving whether the environment is to blame
$ gcc -print-search-dirs
$ env -u GCC_EXEC_PREFIX -u COMPILER_PATH \
-u LIBRARY_PATH gcc -print-search-dirs
Diffing those two outputs is the fastest way to settle it, and it is a good first move whenever a toolchain behaves differently in two places.
The trap: the driver rewrites them
Before running collect2 or ld, the driver overwrites two of the variables you
may have set, from its own final prefix lists:
/* Rebuild the COMPILER_PATH and LIBRARY_PATH environment variables
for collect. */
putenv_from_prefixes (&exec_prefixes, "COMPILER_PATH", false);
putenv_from_prefixes (&startfile_prefixes, LIBRARY_PATH_ENV, true);
Listing 6-4: propagation to subprocesses
(gcc/gcc.cc:9275-9278).
So a child process sees the resolved lists — including everything contributed
by -B and by the compiled-in defaults — not whatever you originally exported.
Reading
LIBRARY_PATHinside a build script that GCC invoked tells you about the driver, not about your shell. This bites in configure scripts and inMakefiles driven from a compiler wrapper, where the value looks like something you set and is not.
The COLLECT_* family: the driver talking to itself
Same channel, flowing the other way. You do not set these; you read them when debugging.
They exist because two of the driver's children have to re-enter the driver,
as Chapter 1.1 described: collect2 must compile the
constructor tables it generates, and lto-wrapper must re-run code generation at
link time with your original options.
COLLECT_GCC — which driver
The full pathname of the running driver. The comment above the code explains the one non-obvious choice:
/* Set up to remember the pathname of gcc and any options
needed for collect. We use argv[0] instead of progname because
we need the complete pathname. */
void
driver::putenv_COLLECT_GCC (const char *argv0) const
{
obstack_init (&collect_obstack);
obstack_grow (&collect_obstack, "COLLECT_GCC=", sizeof ("COLLECT_GCC=") - 1);
obstack_grow (&collect_obstack, argv0, strlen (argv0) + 1);
Listing 6-5: exporting COLLECT_GCC
(gcc/gcc.cc:8710-8721).
progname would give you gcc. argv[0] gives you the path you actually invoked,
which is the only thing that identifies this installation out of the several you
may have.
The two consumers disagree about what to do when it is missing, and the difference matters:
| Consumer | Behaviour when unset |
|---|---|
lto-wrapper | hard error: "environment variable COLLECT_GCC must be set" |
collect2 | falls back to <target>-gcc, then to plain gcc |
Chapter 1.1 shows both code paths (lto-wrapper.cc:1444-1448 and
collect2.cc:1142-1151).
That
collect2fallback is a genuine footgun on a cross toolchain. Acollect2invoked withoutCOLLECT_GCCcan silently pick up a different compiler — and therefore a different sysroot, different startfile prefixes, and a differentcrtbegin.o. The link succeeds. The binary is wrong. You will not get a diagnostic.
COLLECT_GCC_OPTIONS — with which switches
Every switch, and each of its arguments, emitted individually single-quoted:
COLLECT_GCC_OPTIONS='-B' '/opt/alt/' '--sysroot=/arm-rootfs' '-O2' '-dumpdir' 'a-'
The function that builds it names its own contract in its comment:
/* Store switches not filtered out by %<S in spec in COLLECT_GCC_OPTIONS
and place that in the environment. */
static void
set_collect_gcc_options (void)
Listing 6-6: set_collect_gcc_options
(gcc/gcc.cc:5610-5614).
Three details follow from that, and all three surprise people.
It is not your command line. Switches removed by %<S in the specs are dropped
from it — the filtering is explicit:
/* Ignore elided switches. */
if ((switches[i].live_cond
& (SWITCH_IGNORE | SWITCH_KEEP_FOR_GCC))
== SWITCH_IGNORE)
continue;
Listing 6-7: %<S filtering (gcc/gcc.cc:5634-5638).
It is rebuilt before every subprocess, not snapshotted at startup
(gcc.cc:5841 and :6169), so it reflects
spec state at that moment.
-dumpdir appears even though you never typed it. So do other options
synthesised along the way. If you are diffing two COLLECT_GCC_OPTIONS lines,
expect content you did not write.
Embedded single quotes are escaped as '\'', in the loop just below Listing 6-6 —
which is what makes the whole line safe to paste into a shell.
Why this belongs in a chapter about search paths
This is the mechanism that makes path flags survive the recursion. Put -B,
--sysroot or -L on your command line and it lands in COLLECT_GCC_OPTIONS;
when collect2 re-invokes the driver with those options, the inner driver rebuilds
the same prefix lists and the same sysroot as the outer one. Combine that with the
COMPILER_PATH and LIBRARY_PATH rewrite of Listing 6-4, and the entire
subprocess tree agrees about where things live.
Without it, a -B would apply to the compile and silently not to the link.
The rest of the family
| Variable | Purpose | Set at |
|---|---|---|
COLLECT_LTO_WRAPPER | path to lto-wrapper | gcc.cc:8723-8726 |
COLLECT_AS_OPTIONS | accumulated -Xassembler / -Wa, options | gcc.cc:6094-6112 |
COLLECT_NO_DEMANGLE | suppresses symbol demangling in collect2 | you, by hand |
COLLECT_NO_DEMANGLE is the odd one out: it is an input, read by collect2
(collect2.cc:895) and then re-exported so that a nested invocation
inherits it (:898). It is the only member of the family you would
ever set deliberately. COLLECT_LTO_WRAPPER is likewise checked by collect2 and
turned into a hard error if absent (collect2.cc:604-614).
Reading them
gcc -v prints every variable the driver sets, because the setter itself echoes
under -v:
void
env_manager::xput (const char *string)
{
if (m_debug)
fprintf (stderr, "env_manager::xput (%s)\n", string);
if (verbose_flag)
fnotice (stderr, "%s\n", string);
Listing 6-8: why -v echoes the environment
(gcc/gcc.cc:126-132).
That is why -v output opens with COLLECT_GCC= and COLLECT_LTO_WRAPPER=, and
prints a fresh COLLECT_GCC_OPTIONS= line immediately before every subprocess —
one per rebuild, per Listing 6-7's neighbours.
$ gcc -v hello.c 2>&1 | grep '^COLLECT_'
Those lines are already shell-quoted, so they paste straight into a terminal. It is
the fastest way to reproduce exactly what collect2 was handed.
Setting them yourself is meaningful only if you are invoking collect2 or
lto-wrapper directly, which is occasionally a useful thing to do when
debugging a link. Exporting them before a normal gcc run accomplishes nothing:
the driver overwrites both before spawning anything.
Three things to carry away
-B outranks all of them. Two priority levels, and -B has the higher one.
LIBRARY_PATH does nothing on a cross compiler. One *cross_compile == '0'
guard, and no diagnostic when it silently does not apply.
The COLLECT_* pair is why path flags survive the recursion into collect2
and back — and why a collect2 without them can quietly use the wrong compiler.
Documentation coverage
GCC_EXEC_PREFIX, COMPILER_PATH and LIBRARY_PATH are documented, in the
Environment Variables node (gcc/doc/invoke.texi:37984, with the
individual items at :38057, :38092 and :38099).
What is not documented:
- The
COLLECT_*family the driver exports. The Environment Variables node covers what you may set; the outward channel is source-only. collect2's fallback to<target>-gccand thengccwhenCOLLECT_GCCis unset — the one behaviour in this chapter most likely to cost you a day.- That the driver overwrites
COMPILER_PATHandLIBRARY_PATHbefore spawning the linker, so their values inside a child process are the driver's, not yours. - That switches removed by
%<Sare absent fromCOLLECT_GCC_OPTIONS.
Next: why the directory in libraries: is not necessarily the directory that gets
searched.
All source references in this chapter are to GCC 15.2.0
(releases/gcc-15.2.0). Line numbers in other releases will differ; the
surrounding code rarely does. Where behaviour itself changed across a major
version, it is flagged inline.