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

Environment Variables

The problem

Build tools that inherit the user’s environment variables produce non-deterministic builds. Consider a C compiler invoked by a build tool:

  • If the user has CFLAGS=-O2 in their shell, the build produces optimized output.
  • If they unset it, the build produces debug output.
  • Two developers on the same project get different results from the same source files.

This breaks caching (the cache key doesn’t account for env vars), breaks reproducibility (builds differ across machines), and makes debugging harder (a build failure may depend on an env var the developer forgot they set).

Common examples of environment variables that silently affect build output:

VariableEffect
CC, CXXChanges which compiler is used
CFLAGS, CXXFLAGS, LDFLAGSChanges compiler/linker flags
PATHChanges which tool versions are found
PYTHONPATHChanges Python module resolution
LANG, LC_ALLChanges locale-dependent output (sorting, error messages)
HOMEChanges where config files are read from

RSConstruct’s approach

RSConstruct does not use environment variables from the user’s environment to control build behavior. All configuration comes from explicit, versioned sources:

  1. rsconstruct.toml — all processor configuration (compiler flags, linter args, scan dirs, etc.)
  2. Source file directives — per-file flags embedded in comments (e.g., // EXTRA_COMPILE_FLAGS_BEFORE=-pthread)
  3. Tool lock file.tools.versions locks tool versions so changes are detected

This means:

  • The same source tree always produces the same build, regardless of the user’s shell environment.
  • Cache keys are computed from file contents and config values, not ambient env vars.
  • Remote cache sharing works because two machines with different environments still produce identical cache keys for identical inputs.

Rules for processor authors

When implementing a processor (built-in or Lua plugin):

  1. Never read std::env::var() to determine build behavior. If a value is configurable, add it to the processor’s config struct in rsconstruct.toml.

  2. Never call cmd.env() to pass environment variables to external tools, unless the variable is derived from explicit config (not from std::env). The user’s environment is inherited by default — the goal is to avoid adding env-based configuration on top.

  3. Tool paths come from PATH — RSConstruct does inherit the user’s PATH to find tools like gcc, ruff, etc. This is acceptable because the tool lock file (.tools.versions) detects when tool versions change and triggers rebuilds. Use rsconstruct tools lock to pin versions.

  4. Config values, not env vars — if a tool needs a flag that varies per project, put it in rsconstruct.toml under the processor’s config section. Config values are hashed into cache keys automatically.

What RSConstruct does inherit

RSConstruct inherits the full parent environment for subprocess execution. This is unavoidable — tools need PATH to be found, HOME to read their own config files, etc. The key design decision is that RSConstruct itself never reads env vars to make build decisions, and processors never add env vars derived from the user’s environment.

The one exception is NO_COLOR — RSConstruct respects this standard env var to disable colored output, which is a display concern and does not affect build output.