Skip to content

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

Draft
josecm wants to merge 9 commits into
mainfrom
exp/kconfig
Draft

feat(build): Kconfig-based build configuration#397
josecm wants to merge 9 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.

josecm and others added 9 commits August 25, 2026 11:48
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>
Each build instance 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.
menuconfig and listconfig operate per build instance and configuration
options cannot be set on the command line.

Signed-off-by: Jose Martins <josemartins90@gmail.com>
Signed-off-by: Jose Martins <josemartins90@gmail.com>
Signed-off-by: Jose Martins <josemartins90@gmail.com>
Signed-off-by: Jose Martins <josemartins90@gmail.com>
Signed-off-by: Jose Martins <josemartins90@gmail.com>
PLATFORM= and CONFIG= on the command line keep selecting the classic
per-target build exactly as before, and the new classic defconfig target
seeds its .config without building. Everything else operates on an
output directory: explicit O=, or by default build/ with binaries in
bin/. Output directories carry their own .config, seeded through the
per-platform <platform>_defconfig targets, and the build takes the
platform and the VM configuration (the new CONFIG_SRC option) from it.
Mixing the two workflows in one invocation is rejected. make clean on
the default output erases build/ and bin/ wholesale; an explicit O=
directory only ever has its contents removed.
A classic PLATFORM=/CONFIG= seed writes a .pin Kconfig fragment into the
build directory fixing both from that point on: the CONFIG_SRC option
loses its prompt and takes the pinned value, the platform choice refuses
changes in menuconfig, and out-of-band edits to either are rejected when
the configuration is next resolved. Defconfig-seeded output directories
stay unpinned, and reseeding replaces any pins.
CONFIG_REPO joins CONFIG_SRC in Kconfig, empty meaning the in-tree
configs folder, and CONFIG_SRC accepts a configuration name resolved in
that repository, a configuration folder, or a config.c path. Seeding
records the name as given (plus the repository when explicit) instead of
a resolved absolute path, keeping .config files portable; the config pin
covers both values.

Signed-off-by: Jose Martins <josemartins90@gmail.com>
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.

1 participant