The status block no longer says scaffold, "The loop" no longer says none of it runs yet, and the table of what each verb actually runs names format-patch rather than a bare git diff. Configuration is real: the file and its keys, the two environment variables, init, and a doctor transcript showing what got resolved and from where. What is left is Later and Far, plus three gaps named rather than papered over: real mail, a full kernel build, and the fix round trip against a real kernel patch. The tests section describes the fixture harness and the three tests that want the host's own tools. ROADMAP's Now and Next record what landed and what each item does now. The series paragraph explains why the sidecar records a whole range rather than a base: a re-roll that regenerated base..HEAD would pick up every commit that landed on the branch since create and mail it with the set.
13 KiB
spectral
A monolithic kernel work wrapper, to make it easy.
Status: the loop closes. Every verb below runs. The chain from a random open bug to a patch that
git send-emailaccepts has been walked end to end, including the v2 re-roll threaded off the original.kernel testbuilds a tree and boots it under qemu,doctorsays what is missing before you start, andinitwrites the config file.NextinROADMAP.mdhas landed; what is left isLaterandFar, plus the three gaps named under What is not proven below.
Why
The kernel patch lifecycle is a dozen commands you retype every time. Find
something to fix, write it, run checkpatch, run it again after --fix,
commit with the right trailer, build, boot it under qemu, diff against
master, work out who maintains those files, send. Then a week later do it
all again as v2, remembering to thread it off the original.
None of those steps are hard. They are just easy to get subtly wrong, and
the ones that get a patch ignored rather than reviewed are the boring
details: a missing Signed-off-by, a v2 that does not thread, a maintainer
who never got CC'd.
spectral is a thin wrapper over that loop, and the thinness is the point. Every verb shells out to a tool you already have and already trust.
What it wraps
What each verb actually ends up running:
| spectral | actually runs |
|---|---|
patch check / patch format |
scripts/checkpatch.pl from your tree |
patch commit |
git commit |
patch create |
git format-patch |
patch update |
a rename, or git format-patch --reroll-count for a series |
patch submit |
scripts/get_maintainer.pl from your tree, then git send-email |
kernel test |
make, then qemu-system-x86_64 |
kernel quest |
the bugzilla.kernel.org REST API |
doctor |
git, the tree's own scripts, qemu, and the paths |
checkpatch.pl and get_maintainer.pl are worth calling out. They come from the tree you are working in, so spectral cannot drift out of date with the kernel's own style rules or maintainer map. Upgrade your tree and the new rules apply.
Because it is thin, you can drop the wrapper at any point. patch submit --dry-run prints the recipients and the exact git send-email command line
instead of sending anything. Read it, then either drop --dry-run or run
the command yourself.
The loop
What the finished tool feels like. Every line below has been run:
$ spectral kernel quest
12345 usb: xhci: device does not enumerate after resume
component: Drivers/USB
https://bugzilla.kernel.org/show_bug.cgi?id=12345
# ... go and fix it, in the tree ...
$ spectral patch check
$ spectral patch format # checkpatch --fix, then check again
$ git add -A # `patch commit` commits what you staged
$ spectral patch commit "usb: xhci: re-arm the port after resume" --signoff
$ spectral kernel test # build, then boot it under qemu
$ spectral patch create 000-xhci-port-rearm
~/.spectral/patches/000-xhci-port-rearm.patch
$ spectral patch submit 000-xhci-port-rearm.patch --dry-run
# happy? drop --dry-run and it goes out
# ... wait for review ...
$ spectral patch update 000-xhci-port-rearm.patch -v 2
~/.spectral/patches/v2-000-xhci-port-rearm.patch
$ spectral patch submit v2-000-xhci-port-rearm.patch --in-reply-to '<msg-id-of-v1>'
Staging is yours on purpose. patch commit runs git commit, so what goes in
is what you put there; spectral will not sweep a dirty tree into your patch,
because a stray file in a kernel patch is a review comment you did not want.
For a set rather than a single patch:
$ spectral patch create xhci-port-rearm --range master..HEAD \
--cover-letter "Re-arm the port after resume"
~/.spectral/patches/xhci-port-rearm
$ spectral patch submit xhci-port-rearm --dry-run
$ spectral patch update xhci-port-rearm # -v is inferred; every file moves
The series directory holds the patches plus a series.toml recording the range
it was generated for (both ends as hashes), the revision, the file list and the
cover letter, which is what lets a re-roll happen without retyping the range.
Re-rolling regenerates exactly that range: a commit that landed on the branch
since create does not join the set, because a re-roll should not mail commits
nobody asked to send. The blurb's first line becomes the cover letter's subject.
Leave --cover-letter off and the set is written with git's template subject in
it, which git send-email refuses: an unsendable series is better than a cover
letter nobody wrote.
One deviation from the obvious design: patch update is a single subcommand
taking -v N rather than a generated update-v2 / update-v3. clap
subcommands are static, and a flag beats a subcommand you cannot tab-complete.
Leave -v off and it infers the next revision from the file's current vN-
prefix, or from the sidecar for a series.
Install
Needs a recent stable Rust: edition 2024, so 1.85 or newer. Developed against 1.98.1.
$ git clone https://git.spectoria.dev/huntedbytheirs/spectral.git
$ cd spectral
$ cargo build --release
$ install -Dm755 target/release/spectral ~/.local/bin/spectral
Not on crates.io, so cargo install spectral will get you something else.
cargo install --path . from a clone works too.
You also need:
- a kernel tree, for
scripts/checkpatch.plandscripts/get_maintainer.pl. Both ship with the kernel, so spectral does not carry its own. - git with
send-emailconfigured.git send-emailhas to work from a plain shell first. If it does not, spectral cannot fix it for you and is not meant to try. - qemu, so that
qemu-system-x86_64is on$PATH, forkernel test.
Configuration
Two paths and a couple of settings, resolved in this order: the environment,
then ~/.config/spectral/config.toml, then the built-in default.
| what | environment | file key | default |
|---|---|---|---|
| kernel tree | $SPECTRAL_KERNEL |
kernel_tree |
~/.spectral/linux |
| patch dir | $SPECTRAL_PATCH_DIR |
patch_dir |
~/.spectral/patches |
| always CC | always_cc |
none | |
| send-email identity | send_email.from |
git's own user.email |
$ export SPECTRAL_KERNEL=$HOME/src/linux
$ spectral init --tree $HOME/src/linux # or --clone <url> [--depth N]
wrote /home/you/.config/spectral/config.toml
tree: /home/you/src/linux
init verifies the tree before it writes anything, and refuses to overwrite a
config that points somewhere else unless you pass --force. Run it twice with
the same tree and the second run says so and changes nothing.
A tree is accepted only if it has scripts/checkpatch.pl. Otherwise you get
not a kernel source tree rather than a confusing failure three steps later.
A missing tree reports the path and the variable that would have set it. The
tree is not needed for kernel quest, which only wants the network.
A path in the config file has to be absolute, and an unknown key is an error rather than a shrug: a mistyped setting that silently does nothing is worse than one that stops the run and says which line is wrong.
Run spectral doctor to see what got resolved and from where:
$ spectral doctor
ok kernel tree /home/you/src/linux (from the config file)
ok kernel tree shape scripts/checkpatch.pl is there
ok checkpatch.pl scripts/checkpatch.pl (the only thing that decides kernel style)
ok get_maintainer.pl scripts/get_maintainer.pl (the only thing that knows who to mail)
ok MAINTAINERS the maintainer map the script reads
ok git git version 2.55.0
ok git send-email runs
ok sender identity [email protected]
warn mail route no sendemail.* config: --dry-run works, a real send will not
-> configure `git send-email` before dropping --dry-run
ok qemu QEMU emulator version 11.1.1
ok patch directory /home/you/.spectral/patches (exists, the built-in default)
ok config file /home/you/.config/spectral/config.toml
doctor: nothing failed, 1 warned
A warning keeps the exit code at 0; a failed check exits 1 and the summary names the first one, because that is the one to fix first.
Commands
$ spectral --help
A monolithic kernel work wrapper to make it easy.
Usage: spectral <COMMAND>
Commands:
kernel Find work, build it, boot it
patch Carry a change from working tree to mailing list
doctor Check the tree, the scripts, the mail setup and the paths
init Point the config at a kernel tree, verifying or cloning one
help Print this message or the help of the given subcommand(s)
spectral kernel quest, kernel test
spectral patch check, format, commit, create, submit, update
spectral doctor, init
Every one of them has --help that says more than this README does.
What is not proven
Named rather than papered over, because a thin wrapper is only worth trusting where it says where it stops:
- Real mail is unverified. No patch from this project has been sent to a
mailing list by spectral. The acceptance evidence is
git send-email --dry-runreachingDry-OKwith the recipients spectral chose. - A full kernel build is not part of the test suite.
kernel test's build step is exercised against a Makefile that writes the image; the boot is exercised against a real qemu with a realbzImage, but nothing here builds a kernel from source, because that is hours of work that proves nothing about this crate. patch format's fix round trip is proven against a made-up patch, not against a real kernel posting whose damage checkpatch can repair.
Tests
$ cargo test --locked
No test needs a kernel tree or a network, and none needs git send-email
configured. Each one builds a throwaway kernel-shaped git repository in a temp
directory, points HOME, SPECTRAL_KERNEL and git's global config at it, and
drives the built binary through CARGO_BIN_EXE_spectral. The fixture's PATH
and git config are the test's, not the machine's, which is how a missing qemu, a
missing sender identity or a missing git send-email is produced deliberately
instead of inherited.
One host tool is assumed: git with its send-email support. The doctor tests
wrap it so a machine without that support still passes, and the send-email flow
tests use the real one, because that is the tool under test.
Three tests do want the host's own tools, and they are ignored unless asked for:
$ SPECTRAL_REAL_CHECKPATCH=1 SPECTRAL_REAL_TREE=/usr/src/linux \
cargo test --locked -- --ignored real_checkpatch
$ SPECTRAL_REAL_GETMAINTAINER=1 SPECTRAL_REAL_TREE=/usr/src/linux \
cargo test --locked -- --ignored real_get_maintainer
$ SPECTRAL_LIVE=1 cargo test --locked -- --ignored live_bugzilla
They are where a kernel-version change to checkpatch's summary line, or to get_maintainer's role words, or to bugzilla's REST answer, shows up as a test failure rather than inside someone's patch workflow.
Where to pitch in
The verbs are written and the loop closes. What is left is in ROADMAP.md:
Later holds more quest sources, more checks, review tracking and packaging;
Far holds the TUI and spectral auto.
Two of those are more useful than they look. kernel/quest.rs is behind the
QuestSource trait, so a syzbot source is one impl and no change to the
command. patch/series.rs holds the sidecar a series carries, and review
tracking is the feature that will want to add to it.
The three gaps at the end of What is not proven are honest small tasks: a
real send, a real kernel build under kernel test, and the fix round trip
against a real kernel patch.
Development
$ cargo fmt --all
$ cargo clippy --all-targets -- -D warnings
$ cargo test
$ cargo doc --no-deps
All four are clean on main and are the bar for a change.
A few conventions:
src/cli.rsholds the whole command surface. One file on purpose: the CLI is the specification, and reading it top to bottom should tell you what the tool does.- Command modules return what they produced and
mainprints it, so output formatting lives in one place. - Errors go through the one
Errorenum and?. Nothing in the command paths callsunwrap. An external command that fails reports what it said, from stdout as well as stderr, because git puts some refusals on stdout. - Every module that shells out has a comment saying why that tool and not
another API:
checkpatch.plandget_maintainer.plcome from the tree, andgit format-patchdoes the series naming because that is already its job. - No
unsafeand no FFI.std::env::set_varisunsafeunder edition 2024, which is why environment-dependent behaviour is tested through the binary rather than in a unit test. - No
todo!()is left in the command path. If you add one, say in the test suite what will prove it gone.
License
MIT. See LICENSE, © 2026 huntedbytheirs.