# Feature design: kernel-style config system
## Status
Draft — awaiting review. The user flagged this as "a big item" in
`problems.txt`. This doc unpacks what they likely mean and proposes a
phased shape.
## Origin
`problems.txt`:
> want a config system like the kernels config system.
> This is a big item.
## What the kernel's config system *is*
The Linux kernel has a layered config story most rsconstruct users
will know in outline:
1. **Kconfig files**: distributed across the source tree
(`drivers/net/Kconfig`, `arch/x86/Kconfig`, etc.). Each declares
*symbols* (`CONFIG_NETFILTER`), their type (bool, tristate, int,
string), their default values, and **dependency expressions**
(`depends on NET && X86`).
2. **`make menuconfig` / `make defconfig` / `make oldconfig`**: tools
that read the Kconfig graph and let the user select symbols
interactively, from a base file, or by reconciling an existing
`.config` against a newer Kconfig graph.
3. **`.config`**: the resolved config — a flat `CONFIG_FOO=y` /
`CONFIG_BAR=m` / `# CONFIG_BAZ is not set` file.
4. **`include/generated/autoconf.h`**: derived from `.config` and
`#include`d everywhere; turns `CONFIG_FOO=y` into `#define
CONFIG_FOO 1`.
5. **The build system** (`make`): reads `.config`, conditionally
includes Kbuild fragments, conditionally compiles files based on
`obj-$(CONFIG_FOO) += foo.o`.
The pieces that make this *the* kernel-style config system, as
opposed to "just a config file":
- **Distributed declarations** (each subsystem owns its symbols).
- **Typed symbols** with declared dependencies between them.
- **Dependency-aware UI** (menuconfig grays out unavailable options).
- **Reconciliation** (oldconfig keeps your choices when symbols
appear / disappear).
- **Multiple stored configs** (`arch/x86/configs/x86_64_defconfig`).
- **Config drives compilation** (builds use the resolved config).
## What rsconstruct has today
A flat `rsconstruct.toml`:
```toml
[processor.ruff]
enabled = true
args = ["--target-version=py313"]
[processor.cc]
cflags = ["-O2", "-Wall"]
```
That is, today rsconstruct's config is shaped much more like
`Cargo.toml` or `pyproject.toml` than like a kernel `.config`. There
is no symbol declaration with types and dependencies; there is no
menuconfig-style UI; there is no notion of a "saved config preset"
(though see the [variants design doc](variants.md) for an adjacent
proposal).
## What "kernel-style" might mean for rsconstruct
The user's request is broad. There are at least four distinct things
they might want, and each has different implications:
### Reading 1 — `defconfig` presets
Save a complete `rsconstruct.toml` shape under a name; switch between
named presets:
```bash
rsconstruct defconfig minimal # writes a minimal toml
rsconstruct defconfig debug # writes a debug toml
rsconstruct defconfig release # writes a release toml
```
Today the closest analogues are `rsconstruct smart enable-detected`
and the `[[profile]]` proposal in `doc/variants.md`. A real defconfig
system would let users name and version their full configs and
distribute them as files.
Cost: medium. Mostly a CLI feature plus a `configs/` directory
convention. No deep architecture change.
### Reading 2 — `menuconfig`-style interactive editor
A TUI that walks the user through every option:
```
[*] Enable ruff processor
└── Target Python version: py313
[*] Enable cc processor
└── Compiler flags: ...
[ ] Enable mypy processor
```
Cost: high. We'd need a TUI dependency (ratatui, cursive), a
declarative description of every config field (we already have
`KnownFields` and `field_descriptions`), and dependency rules that
say "ruff requires Python being configured".
Real value if there are >50 config knobs and users get lost. With
~20 processors at one section each, our "knob count" is small enough
that menuconfig is overkill. `processors config <name>` already shows
each processor's effective config.
### Reading 3 — `oldconfig`-style reconciliation
When rsconstruct adds a new processor with config fields, projects
that pin to an older version should be told "here are the new fields,
which value do you want?". Today `deny_unknown_fields` rejects
forward-compat fields; new fields don't surface in old projects until
the user updates.
Cost: low to medium. We have provenance tracking already (which fields
are defaults, which are user-set). A `rsconstruct config sync`
command that walks the rsconstruct.toml, lists newly-added fields with
their defaults, and asks the user to accept/edit each is a real
feature.
### Reading 4 — symbol dependency graph
The deepest interpretation: declare that `[processor.cpplint]`
*requires* `[processor.cc]` to be enabled, and let the config system
enforce this. Today such cross-processor dependencies live in code
(processors silently noop if their inputs don't exist).
Cost: high. Touches every processor's configuration story. Probably
overkill for rsconstruct's scope — projects that need cross-processor
coupling typically express it through directory layout and src_dirs,
not through symbol expressions.
## Recommendation
I don't think "fully kernel-style" is the right target. Three of the
four readings above are heavy features for relatively small gains.
What I'd actually ship, in priority order:
1. **Defconfig presets** (Reading 1). Small, useful, matches the
variants-doc proposal of named build profiles.
Concrete shape: `configs/<name>.toml` files in the project, plus
`rsconstruct defconfig <name>` to copy one to `rsconstruct.toml`,
plus `rsconstruct defconfig --save <name>` to write the current
one out. Makes "we have a CI config, a debug config, and a
release config" easy to manage.
2. **Field reconciliation** (Reading 3). When rsconstruct adds new
config fields, `rsconstruct config sync` should report them and
offer to add them with defaults. Right now the user has to read
release notes.
Skip **menuconfig** (Reading 2) and **symbol dependencies** (Reading
4). They're real-kernel features but the cost/benefit isn't there for
a build tool with our knob count.
## Defconfig — concrete proposal
### Layout
A `configs/` directory at project root, gitignored or not at the
user's discretion:
```
configs/
├── ci.toml
├── dev.toml
└── release.toml
```
Each file is a complete `rsconstruct.toml` (same schema, same
validation).
### Commands
```bash
rsconstruct defconfig list # show available defconfigs
rsconstruct defconfig load <name> # copy configs/<name>.toml to rsconstruct.toml
rsconstruct defconfig save <name> # write current rsconstruct.toml to configs/<name>.toml
rsconstruct defconfig diff <name> # diff current vs configs/<name>.toml
rsconstruct defconfig diff <a> <b> # diff two named configs
```
`load` warns / errors if `rsconstruct.toml` has uncommitted changes
(use `git status` heuristic); add `--force` to override.
`save` warns if a `configs/<name>.toml` exists, requires `--force` to
overwrite.
### What this is NOT
- It is not a layered/inherited config (`extends = "ci"`). That's a
more invasive feature.
- It is not a typed config system with menus.
- It is not a dependency graph between symbols.
It is the simplest thing that gives users *named, versioned configs*
they can switch between with one command.
### Estimated cost
~250 lines for the four subcommands plus integration tests. No new
deps. Schema reuse is 100% — `configs/X.toml` is just a regular
`rsconstruct.toml` parked elsewhere.
## Field reconciliation — concrete proposal
### Command
```bash
rsconstruct config sync
```
Walks the rsconstruct.toml, compares against the schema, identifies:
- New fields (rsconstruct knows about them, user doesn't have them).
Emit each with its default value, ask "add to config? [y/N/edit]".
- Removed fields (user has them, rsconstruct no longer knows them).
Emit each, ask "remove? [y/N]".
- Changed defaults (rsconstruct's default has changed; user is using
the old default). Emit each with old → new, ask "update? [y/N]".
### Mechanism
The provenance system already tracks which fields are user-set vs
default-derived. We add a new query: "which fields exist in the
schema but are missing from the user's config?". Compare against the
processor's `KnownFields::known_fields()` list.
### Estimated cost
~150 lines. Reuses `KnownFields`, provenance tracking, and the
existing config-validation pipeline.
## Open questions
1. **Is the user actually asking for menuconfig-style?** "Like the
kernel's" is broad. If they specifically want `make menuconfig`
(Reading 2), that's a much bigger ship and I'd push back on it
given our knob count. Tell me which reading matches your intent.
2. **Defconfig naming**: `configs/<name>.toml` vs
`.rsconstruct/configs/<name>.toml`. The former is project-visible
(likely the right answer — these are intended to be checked in);
the latter hides them. Recommend the former.
3. **Defconfig vs variants**: the [variants doc](variants.md) proposes
a `--profile` flag that selects per-processor config sections at
runtime. Defconfig is "swap whole files". They're complementary
but if we ship variants first, defconfig becomes "make defconfig
that sets a default profile". If we ship defconfig first, variants
becomes "swap fields without rewriting the file".
I'd actually ship defconfig first — it's smaller, more useful
immediately, and doesn't require the variant infrastructure
decision.
4. **Reconciliation interactivity**: blocking prompts vs a non-
interactive `--apply-defaults` mode? The kernel `oldconfig`
blocks; that's annoying in CI. Default to non-interactive (just
list the diffs and exit), with `--interactive` for the prompt
flow. CI users can pipe to a parser.
## What I want from you before writing code
- Which reading (1, 2, 3, 4) actually matches what you want?
- If 1 or 3 (or both): ship in the order I proposed (defconfig
first, reconciliation second), or different order?
- If 2 or 4: confirm the cost is worth it for our scope. I'll
push back unless I'm convinced.