Skip to content

Many Environments

An environment is one complete answer to the question "build this project for what, how?" The guide's project has two, and they share almost everything — sources, the library, one build flag — while differing in the three ways that matter: the toolchain, the front-end program, and one library. That shape, one project, several targets, is PlatformIO's home ground, and it generalises: the same pattern holds a debug and a release build, two boards on one product, or a simulator beside the hardware.

What is shared, what is not

shared per-environment
sources lib/, include/ the src/ files each env selects
settings anything in [env] anything in [env:NAME] wins
downloads the core directory's platforms .pio/build/<env>/, .pio/libdeps/<env>/

The mechanism is section inheritance: an option set in [env] applies everywhere; the same option in [env:uno] overrides it for the Uno alone. Nothing cleverer is happening, which is why it can be reasoned about.

The source filter: two programs, one src/

The one real puzzle in a multi-target project is src/. A desktop program starts at main(); an Arduino program starts at setup() and loop(). Both files cannot be compiled into both firmwares. The guide's split:

  • src/native_main.cpp — the desktop twin: main(), terminal output.
  • src/arduino_main.cpp — the board program: setup()/loop(), LED and serial output.

and each environment claims its own with build_src_filter:

[env:native]
build_src_filter = +<native_main.cpp>

[env:uno]
build_src_filter = +<arduino_main.cpp>

A filter of +<file> means "of src/, compile this". (The default, when no filter is set, is everything — which is why single-board projects never meet this option, and why the first two-program project fails to link with duplicate main-style errors until it does.) Anything both programs need does not belong in either file; it belongs in lib/, which both environments compile unfiltered — the arrangement chapter 7 argued for, here paying its rent.

Choosing what builds

pio run              # default_envs: both
pio run -e uno       # just this one
pio run -e native    # just the twin

The summary table at the end of a full run is the scoreboard (t05):

Environment    Status    Duration
-------------  --------  ------------
native         SUCCESS   00:00:02.799
uno            SUCCESS   00:00:04.921

Two practical notes. Builds are independent: a broken Uno environment does not stop the native one from building, testing and passing, which is how a hardware problem stays a hardware problem instead of becoming a whole project's problem. And build time adds up per environment — the desktop twin is the cheap one you run a hundred times a day; the board builds are the ones you run when the twin is happy.

Growing to three

Adding a target later is the same shape again: a new [env:...] section, its board and platform named, its source filter pointed at a new front-end file if the hardware differs, and default_envs updated if it should join the default build. The library, the tests and the shared settings do not move. Chapter 14's first recipe walks the steps.

Check: the pattern in one line

If your project can say "same thinking in lib/, thin front ends in src/, one environment per target," it can grow a new board, a simulator or a test harness without restructuring — the file changes, the code does not.