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.

DirectionVariables
In — you set them, the driver reads themGCC_EXEC_PREFIX, COMPILER_PATH, LIBRARY_PATH, LPATH
Out — the driver sets them, its children read themCOLLECT_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

VariableFeedsWorks on a cross compiler?
GCC_EXEC_PREFIXprograms and libraries, plus header searchyes
COMPILER_PATHprograms and headersyes
LIBRARY_PATHlibrariesno — native only
LPATHas LIBRARY_PATH, legacyno — 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_PATH inside a build script that GCC invoked tells you about the driver, not about your shell. This bites in configure scripts and in Makefiles 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:

ConsumerBehaviour when unset
lto-wrapperhard error: "environment variable COLLECT_GCC must be set"
collect2falls 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 collect2 fallback is a genuine footgun on a cross toolchain. A collect2 invoked without COLLECT_GCC can silently pick up a different compiler — and therefore a different sysroot, different startfile prefixes, and a different crtbegin.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

VariablePurposeSet at
COLLECT_LTO_WRAPPERpath to lto-wrappergcc.cc:8723-8726
COLLECT_AS_OPTIONSaccumulated -Xassembler / -Wa, optionsgcc.cc:6094-6112
COLLECT_NO_DEMANGLEsuppresses symbol demangling in collect2you, 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>-gcc and then gcc when COLLECT_GCC is unset — the one behaviour in this chapter most likely to cost you a day.
  • That the driver overwrites COMPILER_PATH and LIBRARY_PATH before spawning the linker, so their values inside a child process are the driver's, not yours.
  • That switches removed by %<S are absent from COLLECT_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.