Skip to content

feat(build): Kconfig-based build configuration - #397

Open
josecm wants to merge 4 commits into
mainfrom
exp/kconfig
Open

josecm wants to merge 4 commits into
mainfrom
exp/kconfig

Conversation

@josecm

@josecm josecm commented Aug 25, 2026

Copy link
Copy Markdown
Member

This PR introduces a Kconfig-based configuration system for bao's build. It replaces scattered make variables and command-line -D macros with a structured, validated configuration tree, while keeping the familiar make PLATFORM=<plat> CONFIG=<config> entry point unchanged.

How options split between config.c and Kconfig

The guiding rule is that Kconfig enables, config.c parameterizes. Kconfig decides what the hypervisor build contains: which features are compiled in at all, the platform identity and its hardware facts, and build tunables. config.c then parameterizes the features that are present, per VM or for the hypervisor itself: which VMs run, their images, memory regions, and device assignments, and each enabled feature's per-deployment settings. Cache coloring illustrates the split: it will become a Kconfig feature in a follow-up, meaning the coloring code is not compiled in when disabled; when enabled, the color assignments themselves stay exactly where they are today, per VM and for the hypervisor in config.c.

The two sides are deliberately coupled at compile time. When a feature is disabled in Kconfig, its fields are removed from the configuration structures rather than silently ignored, so a config.c that sets parameters for a disabled feature fails to build with a compiler error pointing at the offending line. This rule takes effect as features gain gating (see the follow-ups below).

Kconfig symbols fall into three groups:

  1. User options: selectable features and tunables, such as IPC or IPI_MAX_EVENTS, including platform properties a platform may default but allow the user to override, such as MEM_NON_UNIFIED.
  2. Platform and architecture facts: values fixed by the platform selection (ARCH, ARCH_SUB, ARCH_PROFILE, CPU count, GIC version, IRQC/IPIC, the memory protection model). They are visible in menuconfig for inspection but not editable, and a defconfig that tries to override one fails the configuration step. Where a platform genuinely offers a hardware choice the fact becomes a real choice: qemu riscv virt can select PLIC, APLIC, or AIA and the matching IPI mechanism.
  3. Platform capabilities: promptless PLAT_HAS_* symbols that gate which user options are even offered, e.g. MMIO slave-side protection is only offered on platforms that declare support for it.

Infrastructure

  1. A Kconfig tree rooted at src/Kconfig sources the architecture trees (src/arch/*/Kconfig) and the platform registry (src/platform/Kconfig). Each platform contributes its identity and selects (Kconfig.plat) plus its fact defaults (Kconfig).
  2. The resolved .config lives per build instance at build/<plat>/<config>/.config, so different platform/config pairs never fight over a shared file.
  3. Seeding is layered: the platform defconfig (src/platform/<plat>/defconfig, when present), then an optional defconfig next to the VM configuration (configs/<config>/defconfig), then Kconfig defaults. PLATFORM= pins the platform choice.
  4. The build consumes two generated artifacts: auto.conf, included by make and used to gate objects (core-objs-$(CONFIG_X)+=...), and autoconf.h, force-included into every compilation unit so code sees CONFIG_* macros without explicit includes.
  5. make PLATFORM=<plat> CONFIG=<config> menuconfig edits the build's configuration interactively; listconfig prints every option with its value and which layer decided it (default, platform, config, or modified by the user), plus the fixed facts.
  6. Validation is strict: unknown symbols and attempts to set platform-fixed facts are hard errors, and CONFIG_* variables cannot be overridden on the make command line; defconfigs and menuconfig are the only configuration inputs.

The engine is kconfiglib driven by scripts/kconfig.py, so configuring a build requires python3 with kconfiglib available; all constraints that the build itself depends on remain enforced in the Makefiles.

Using it

Nothing changes for the common case: make PLATFORM=<plat> CONFIG=<config> (optionally with CONFIG_REPO= for an external configuration repository) builds as before. The first build seeds the configuration automatically; later builds reuse it, and any change to .config regenerates auto.conf/autoconf.h and rebuilds what depends on them.

Kernel-style output directories are supported as an alternative workflow. make O=<dir> <platform>_defconfig seeds <dir>/.config for that platform, and passing CONFIG= to the defconfig target records the VM configuration source in the CONFIG_SRC option. From then on make O=<dir> alone builds there, taking both the platform and the VM configuration from the .config; O= is passed on each invocation (export it in the environment for stickiness). On a build, CONFIG= overrides the configured source for that invocation only, without touching the .config (each configuration keeps its own object tree inside the directory), while PLATFORM= seeds an empty directory directly or must match the directory's configured platform. menuconfig and listconfig take O= the same way, and make O=<dir> menuconfig on an empty directory opens the full tree starting from defaults, platform choice included. clean empties the directory but keeps its .config; distclean erases the configuration as well.

A configured build directory is pinned to its platform and VM configuration: seeding drops a small generated Kconfig fragment (.pin) into it, after which the platform choice refuses changes in menuconfig, CONFIG_SRC loses its prompt and shows as a fixed value, and out-of-band edits to either are rejected the next time the configuration is resolved. CONFIG= remains the sanctioned per-invocation override, and reseeding through a <platform>_defconfig target (or deleting the fragment) replaces the pin.

To inspect or change a build's configuration:

  1. make PLATFORM=<plat> CONFIG=<config> menuconfig opens the interactive frontend on that build's .config (seeding it first if it does not exist yet).
  2. make PLATFORM=<plat> CONFIG=<config> listconfig prints every option with its type, value, default, and the layer that decided it, plus the platform-fixed facts.
  3. To reconfigure from scratch, remove the build directory (or just its .config); the next build reseeds from the defconfig layers.

Where each piece is defined:

  1. src/Kconfig: the root, sourcing the architecture, platform, and core trees.
  2. src/core/Kconfig: core feature options and tunables (IPC, MMIO_SLAVE_SIDE_PROT, IPI_MAX_EVENTS).
  3. src/arch/Kconfig and src/arch/<arch>/Kconfig: architecture selection and the architecture-owned facts (ARCH, ARCH_SUB, ARCH_PROFILE, memory protection model, CPU/GIC_VERSION on armv8, IRQC/IPIC on riscv).
  4. src/platform/Kconfig: the platform registry, declaring the platform choice and the platform-wide symbols (PLATFORM, MEM_NON_UNIFIED, PLAT_HAS_*).
  5. src/platform/<plat>/Kconfig.plat: the platform's entry in the platform choice and its selects (Kconfig grammar requires choice entries in a separate file).
  6. src/platform/<plat>/Kconfig: the platform's fact values, given as defaults conditioned on the platform symbol, and its hardware choices where they exist (the qemu riscv interrupt controller selection above).
  7. src/platform/<plat>/defconfig: the platform's optional defconfig. A platform can ship one to preset any user-visible option for builds targeting it: default a feature off because it makes no sense on that hardware, preset a tunable to a value that fits its memory budget, or pick one side of a hardware choice as the recommended one. It is the base seeding layer, so everything it sets remains a user decision that a config defconfig or menuconfig can override, which is exactly what distinguishes it from a Kconfig fact: facts are fixed by the platform selection and cannot be touched, while platform defconfig entries are just that platform's starting point. listconfig attributes such values to the platform layer. No platform ships one in this PR because every current platform default is expressible as a Kconfig default, but the mechanism is in place.
  8. configs/<config>/defconfig: an optional defconfig next to the VM configuration's config.c, for options a given deployment wants pinned (folder configurations only; single-file <config>.c configurations have no defconfig layer).
  9. build/<plat>/<config>/.config: the resolved working configuration of that build instance, plus the generated auto.conf and autoconf.h under build/<plat>/<config>/config/.

The layers superimpose in a fixed order at seeding time: the platform defconfig is applied first, the config-folder defconfig is applied on top of it and may override it, and every symbol neither mentions takes its Kconfig default (which the platform selection already specializes). The platform itself is always pinned by PLATFORM= and cannot be changed by a defconfig. After seeding, the build's .config is authoritative: menuconfig edits land there, and listconfig reports such values as modified. If a seed defconfig changes after a build was configured, the build warns that the defconfig is newer but never silently reseeds; removing the .config adopts the new defconfig.

What is configurable in this PR, and what comes next

This PR moves the existing platform and architecture build facts into Kconfig (memory protection model, non-unified memory, physical interrupt handling, interrupt controller selection) and introduces the first core feature options: MMIO_SLAVE_SIDE_PROT (bridged to the existing DEFINED()-based gating), IPI_MAX_EVENTS (now sizing the cpu message queue directly, replacing the -D override), and IPC, whose code gating lands in a follow-up.

Two follow-ups are already prepared on branches stacked on this one, each to become its own PR:

  1. Remote I/O (CONFIG_REMIO): compiles remio fully out when disabled, including the hypercall dispatch, the VM init hooks, and the remio fields of the configuration structures.
  2. Cache coloring (CONFIG_MEM_COLORING): MMU-only option that compiles the coloring module out and strips color fields and arguments from disabled builds, moving all coloring logic behind a single coloring.h boundary header.

An open question for reviewers: which other build-time knobs should move into Kconfig? Existing -D macros, debug and logging options, and per-architecture features are all candidates; input on what is missing from this first set is welcome.

@DavidMCerdeira DavidMCerdeira left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is working as intended for me, but there are a few issues:

After editing .config (which is what menuconfig does), make O=dir prints "Compiling generator .../config//config_defs_gen" followed by "undefined reference to config" from the host linker. The build then restarts and finishes correctly, so exit status is 0, but the output looks broken.

The CONFIG_SRC help in src/Kconfig:13 and the pinned-configuration error at scripts/kconfig.py:139 both say to "pass CONFIG= to override it for a build", but CONFIG= combined with O= is an explicit error and CONFIG= alone switches to the classic build. The comment at Makefile:213 says CONFIG= "additionally records the VM configuration source" for a defconfig target, but that combination is also rejected.

The RISC-V IRQC choice offers AIA and PLIC only. arch.mk and the CI matrix also support APLIC.

IPC options although present does not affect the compiled code.

Regarding the git history, I'd probably squash 6318f37 9ef0b65 22a33ec and 7297bb0

I also would squash 15f2954, 15f2954 and 0aeabb1 with 7cbb295

Also the CI should be updated with the Kconfig lib before this accepted
edit: I've added it here bao-project/bao-ci#95

josecm and others added 4 commits September 17, 2026 15:06
A goal named config or any other substring of a non-build target name
would be silently classified as one, skipping the platform and
configuration checks.

Co-authored-by: Diogo Costa <diogoandreveigacosta@gmail.com>
Signed-off-by: Jose Martins <josemartins90@gmail.com>
Signed-off-by: Jose Martins <josemartins90@gmail.com>
Each build carries its own .config, seeded by layering the platform
defconfig and the VM config folder defconfig over Kconfig defaults, and
synced by scripts/kconfig.py (kconfiglib) into a make fragment and a C
header force-included in every compilation unit. Configuration options
cannot be set on the command line; menuconfig and listconfig edit and
inspect a build's configuration.

PLATFORM= and CONFIG= on the command line keep selecting the classic
per-target build exactly as before, and the classic defconfig target
seeds its .config without building. A classic build directory is pinned
to its platform and configuration through a generated Kconfig fragment,
so menuconfig cannot switch them and out-of-band edits are rejected.

Everything else operates on a kernel-style output directory: explicit
O=, or by default build/ with binaries in bin/. Output directories are
seeded, unpinned, through the per-platform <platform>_defconfig targets,
and take the platform and the VM configuration from their .config. The
CONFIG_SRC option names the VM configuration (a name looked up in the
CONFIG_REPO repository, empty meaning the in-tree configs folder, a
configuration folder, or a config.c path). Mixing the two workflows in
one invocation is rejected.

Signed-off-by: Jose Martins <josemartins90@gmail.com>
Platform and architecture facts (architecture, profile, cpu, GIC version,
interrupt and IPI controllers, memory protection model, non-unified
memory, physical-interrupts-only) are resolved from the platform
selection instead of per-platform makefiles, and the build macros derived
from them are bridged from Kconfig. Facts cannot be overridden on the
command line; where hardware genuinely offers a choice, as the qemu RISC-V
virt interrupt and IPI controllers and the FVP GIC version, it is a
Kconfig choice. The Arm and RISC-V CI workflows select their variants
through a defconfig accordingly.

The core feature menu starts with MMIO slave-side protection, offered
only on platforms that support it, and the IPI queue depth tunable,
which now sizes the per-cpu message queue.

Signed-off-by: Jose Martins <josemartins90@gmail.com>
@josecm

josecm commented Sep 17, 2026

Copy link
Copy Markdown
Member Author

@DavidMCerdeira I think all your concerns were addressed.

@DavidMCerdeira

DavidMCerdeira commented Sep 18, 2026

Copy link
Copy Markdown
Member

Great!
One additional issue caught my attention:
make -B reseeds the .config and loses the configuration. The seeding rule for the .config has no prerequisites, so -B runs its recipe over the existing file. A value edited in menuconfig reverts to the defconfig. Example: CONFIG_SRC and CONFIG_REPO are wiped and the next build fails with "No VM configuration set".
I ran into this using compiledb make O=dir, which uses -Bnkw pass. Guarding the recipe with test -e $@ ||, or making the rule order-only, should fix this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants