Skip to content

Includes

The [include] directive pulls the contents of one or more sibling fobos files into the current one before any other processing. It lets you split a large fobos into thematic chunks, share configuration across sibling repos, and keep machine-local overrides outside of version control.

Quick example

ini
# fobos
[include]
paths = templates.fobos
        rules.fobos
        ?fobos.local                ; optional — silently skipped if absent

[modes]
modes = release debug

[release]
compiler = gnu
cflags   = -c -O3
target   = main.F90

Each path is resolved relative to the including file's directory (after ${ENV} and ~ expansion, or used as-is if absolute). After resolution, every section from every included file is merged into the parent and the [include] section itself is dropped.

When to reach for [include]

Three patterns this makes possible:

Splitting a large fobos by concern

A 500-line fobos can become a 20-line fobos plus four thematic files:

ini
# fobos
[include]
paths = templates.fobos
        features.fobos
        modes.fobos
        rules.fobos

Each included file owns one kind of section, easier to navigate and review.

Sharing across sibling repos

If several projects use the same compiler templates, check them into a shared location and include from each:

ini
# project-A/fobos
[include]
paths = ../shared-templates/gnu-and-nvf.fobos

This is additive — each project still has its own modes, rules, project metadata, etc. — but the shared bits stay in sync via the shared file.

Machine-local overrides

A common pattern: project defaults committed to git, plus a per-developer override file that's git-ignored:

ini
# fobos (committed)
[include]
paths = defaults.fobos          ; required — error if missing
        ?fobos.local            ; optional — present only on dev machines

[modes]
modes = release
ini
# fobos.local (gitignored, per-developer)
[paths]
$HDF5_PREFIX = /home/me/my-custom-hdf5-build

CI and fresh checkouts skip the missing ?fobos.local silently; developers override paths without touching the committed file.

Syntax

ini
[include]
paths = a.fobos b.fobos             ; space-separated
        c.fobos                     ; or newline-continued
        ${HOME}/.fobis/local.fobos  ; env-vars expand
        ~/.fobis/global.fobos       ; ~ expands
        ?optional.fobos             ; '?' prefix → optional

The single key is paths. Tokens are separated by any whitespace (spaces, tabs, newlines). A leading ? marks the include as optional — if the file doesn't exist, the directive is silently skipped. Without ?, a missing file aborts the build with exit 1.

Path resolution order:

  1. Strip the ? prefix if present.
  2. Expand ${ENVVAR} references via os.path.expandvars.
  3. Expand a leading ~ via os.path.expanduser.
  4. If the result is relative, resolve it against the including file's directory.

Naming convention

The include resolver does not require any particular file name or extension — any readable file with valid INI syntax will work. By convention, however, included fragments use the .fobos suffix:

project/
├── fobos                      ; main entry point (no extension)
├── templates.fobos            ; included fragment
├── rules.fobos
└── prism/
    └── modes.fobos            ; nested fragment

This mirrors the Makefile + *.mk and CMakeLists.txt + *.cmake patterns already established in build-tooling and gives you three concrete benefits:

  • Editor support. Any editor that highlights fobos as INI will extend the same syntax to *.fobos with one filetype association.
  • Globbing. *.fobos matches every fragment without false positives. Untracked-file patterns like ?fobos.local work naturally for developer-local overrides.
  • Searchability. grep -r --include='*.fobos' walks the include graph cleanly without picking up stray text files.

This is a recommendation, not a rule — [include] paths = fobos_prism works exactly the same as [include] paths = prism.fobos. Pick what fits your project; if you don't have a strong preference, prefer .fobos.

Merge semantics

Includes are merged with key-level granularity, never whole-section replacement.

ConflictWinner
Parent file vs. any includeParent wins. The file you're reading is authoritative.
Earlier include vs. later includeLater wins. The last sibling in paths = ... overrides earlier ones.
Two unrelated keys in the same sectionBoth are kept.

Concretely, if the parent has [template-gnu] cflags = -O3 and an include has [template-gnu] cflags = -O0, the result is -O3. If the include also declares [template-gnu] mod_dir = ./mod/, that key flows through to the merged template (the parent didn't declare it).

Recursion and cycles

An included file may itself include other files. Inclusion is processed depth-first: deepest leaves are absorbed first, sibling-collectives are built from inside out.

Cycles are detected and abort with exit 1:

Error: include cycle detected: /repo/fobos -> /repo/a.fobos -> /repo/fobos.

A self-include (a file listing itself in paths = ...) is treated as a cycle. A diamond (two distinct paths reach the same leaf) is not a cycle — the leaf is loaded twice along independent branches and merged correctly.

The maximum nesting depth is capped at 16 to fail fast on pathological configurations:

Error: include depth exceeded (16) at 'd.fobos'. Likely a recursion bug.

Composition with other features

[include] runs as a pre-processing pass before any other fobos logic. Once includes are merged, the rest of the system sees a single combined config — so every other fobos feature works transparently across includes:

FeatureHow it composes
$variable substitutionVariables defined in any included section bind globally, exactly as if declared in the main file.
[features] and [feature:NAME]Feature definitions in includes are honoured by --features activation in the main mode.
[varset:NAME]Varsets in includes are selectable via --varset from the CLI.
Templates (template = template-X)Templates declared in includes are usable as template = ... references in the main file's modes.
[varsets] default = ...Honoured wherever it lives — main file or included file.

Diagnostics

SituationBehaviour
paths = does-not-exist.fobos (required)Hard error. Exit 1. Error suggests the ? optional prefix.
paths = ?does-not-exist.fobos (optional)Silent. Build proceeds.
Include cycle (A → B → A or self-include)Hard error. Exit 1. The cycle path is named.
Depth exceeds 16Hard error. Exit 1.
Empty [include] or no paths = keyNo-op.

What [include] doesn't do

  • It is not a macro facility — there is no parameterised expansion. Each included file is loaded once with its literal content.
  • It does not provide namespacing. All sections live in a single flat namespace; conflicting section names follow the merge rules above.
  • It does not enable conditional inclusion based on mode/CLI flags. If conditional behaviour is needed, reach for varsets or feature flags (which are designed for that).

Composition with varsets

For value-level parameterisation (cluster paths, GPU arch, install prefixes), prefer varsets — they're designed for it and don't require extra files.

For structure-level decomposition (splitting one config into many, sharing across projects), reach for [include].