States the scaffold status up front, documents what each verb shells out to and why those tools come from your tree rather than being vendored, and ends with a map of the 20 stubs grouped by how much they unblock.
227 lines
8.6 KiB
Markdown
227 lines
8.6 KiB
Markdown
# spectral
|
|
|
|
A monolithic kernel work wrapper, to make it easy.
|
|
|
|
> **Status: scaffold.** The command surface parses, every verb is wired
|
|
> through `main` to its module, and the paths resolve — but the bodies are
|
|
> `todo!()`. Twenty of them. Nothing builds a kernel, boots qemu, scrapes
|
|
> bugzilla, or sends mail yet. What exists is the shape of the thing and the
|
|
> seams to fill in, which is exactly what the *Where to pitch in* section
|
|
> below is for.
|
|
|
|
## 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 the maintainers are for 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 subtle mistakes — a missing `Signed-off-by`, a v2 that doesn't thread,
|
|
a missing CC — are the ones that get a patch ignored rather than reviewed.
|
|
|
|
spectral is a thin wrapper over that loop. It is deliberately *thin*: every
|
|
verb shells out to a tool you already have and already trust.
|
|
|
|
## What it wraps
|
|
|
|
Nothing here reimplements kernel tooling, and nothing vendors it:
|
|
|
|
| spectral | actually runs |
|
|
|---|---|
|
|
| `patch check` / `patch format` | `scripts/checkpatch.pl` **from your tree** |
|
|
| `patch commit` | `git commit` |
|
|
| `patch create` | `git diff <base>` |
|
|
| `patch submit` | `scripts/get_maintainer.pl` **from your tree**, then `git send-email` |
|
|
| `kernel test` | `make`, then `qemu-system-x86_64` |
|
|
| `kernel quest` | HTTP against `bugzilla.kernel.org` |
|
|
|
|
The two scripts are called out because it matters: they come from the tree
|
|
you are working in, so spectral cannot have a stale copy of the kernel's own
|
|
style rules or maintainer map. Upgrade your tree, get the new rules.
|
|
|
|
And because it is thin, you can always drop the wrapper. `patch submit
|
|
--dry-run` prints the recipients and the exact `git send-email` command line
|
|
instead of sending, so you can check the plumbing or just run it yourself.
|
|
|
|
## The loop
|
|
|
|
What the finished tool should feel like — none of this runs yet:
|
|
|
|
```console
|
|
$ spectral kernel quest
|
|
12345 usb: xhci: device does not enumerate after resume
|
|
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
|
|
$ 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>'
|
|
```
|
|
|
|
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.
|
|
|
|
## Install
|
|
|
|
Needs a recent stable Rust — edition 2024, so **1.85 or newer**; developed
|
|
against 1.98.1.
|
|
|
|
```console
|
|
$ 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.
|
|
|
|
Beyond Rust you want:
|
|
|
|
- **a kernel tree** — for `scripts/checkpatch.pl` and
|
|
`scripts/get_maintainer.pl`. Both ship with the kernel; spectral does not
|
|
carry its own.
|
|
- **git with `send-email` configured** — `git send-email` must work from a
|
|
plain shell first. If it does not, `patch submit` cannot make it work, and
|
|
is not meant to.
|
|
- **qemu** — `qemu-system-x86_64` on `$PATH`, for `kernel test`.
|
|
|
|
## Configuration
|
|
|
|
There is no config file yet. Two paths matter, and both come from the
|
|
environment with `$HOME`-relative defaults:
|
|
|
|
| what | resolution |
|
|
|---|---|
|
|
| kernel tree | `$SPECTRAL_KERNEL`, else `~/.spectral/linux` |
|
|
| patch dir | `~/.spectral/patches` |
|
|
|
|
```console
|
|
$ export SPECTRAL_KERNEL=$HOME/src/linux
|
|
```
|
|
|
|
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 required for `kernel quest`, which only needs the network.
|
|
|
|
`~/.config/spectral/config.toml` and a `spectral init` to clone the tree are
|
|
planned, not built — `src/config.rs` is the only file that will have to change
|
|
for either.
|
|
|
|
## Commands
|
|
|
|
```console
|
|
$ 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
|
|
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`
|
|
|
|
Every one of them has `--help` that says more than this README does — and,
|
|
for now, a body that panics with a description of what it is supposed to do.
|
|
|
|
## Where to pitch in
|
|
|
|
Every stub is a small, self-contained function with its signature and doc
|
|
comment already written, its caller already wired, and a `todo!()` naming the
|
|
command it should run. Pick one and the blast radius is that file. No stub
|
|
needs you to have read the rest of the crate.
|
|
|
|
Roughly in order of how much they unblock:
|
|
|
|
**`patch submit` — the most self-contained win.** Needs a kernel tree but no
|
|
network and no scraper.
|
|
|
|
- `patch/maintainers.rs` · `lookup` — run `get_maintainer.pl --git` over the
|
|
patch, split the output into `To:` and `Cc:`, and collect the files the
|
|
patch touches
|
|
- `patch/maintainers.rs` · `add_cc` — fold `--cc` flags in without duplicates
|
|
- `patch/mod.rs` · `submit` — the above plus `git send-email`, with
|
|
`--dry-run` stopping one step short
|
|
|
|
**`patch check` / `format` / `commit` / `create` — the everyday verbs.**
|
|
`patch/checkpatch.rs` · `run` already defines the `Target` enum (working
|
|
tree, a revision, or a patch file) and `Report` with an `is_clean`, so what
|
|
is missing is the process call and parsing the error/warning counts out of
|
|
its output.
|
|
|
|
**`patch update` — small, and worth doing with a test.** `patch/mod.rs` ·
|
|
`reroll_path` is the naming rule in one pure function: strip one leading
|
|
`vN-`, prepend `v<revision>-`. Re-running it at the same revision should be a
|
|
no-op, which is easier to assert than to describe.
|
|
|
|
**`kernel quest` — the fun one, if you like HTML.** `kernel/quest.rs` ·
|
|
`Bugzilla::fetch_open` is the only place `reqwest` and `scraper` earn their
|
|
place in `Cargo.toml`; `run` then applies `--filter` and picks one. The
|
|
`QuestSource` trait is the seam for swapping bugzilla for syzbot or a lore
|
|
thread later.
|
|
|
|
**`kernel test` — needs a machine you are willing to boot kernels on.**
|
|
`kernel/qemu.rs` · `build` then `boot`. Streaming serial output as it arrives
|
|
beats buffering it until qemu exits, which is what makes a boot hang
|
|
diagnosable.
|
|
|
|
**`git.rs` — the four plumbing calls.** `current_branch`, `diff_against`,
|
|
`commit`, `rev_parse`, each a few lines over the `run` that is already
|
|
written. Take these if you want to warm up on something tiny.
|
|
|
|
There are no tests yet. The naming rule in `reroll_path` and the recipient
|
|
split in `lookup` are the two that most want one.
|
|
|
|
## Development
|
|
|
|
```console
|
|
$ cargo fmt --all
|
|
$ cargo clippy --all-targets -- -D warnings
|
|
$ cargo test
|
|
```
|
|
|
|
All three are clean on `main` and are the bar for a change.
|
|
|
|
A few conventions, in the spirit of keeping the crate reviewable:
|
|
|
|
- `src/cli.rs` holds the whole command surface. It is 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; `main` prints it. Output policy,
|
|
formatting and all, lives in one place.
|
|
- Errors go through the one `Error` enum and `?`. The command paths have no
|
|
`unwrap`.
|
|
- A new stub gets a `todo!()` that names the command it will end up running.
|
|
If a whole module is unreachable until its caller exists, it carries a
|
|
single `#[allow(dead_code)]` with the reason attached — there are no
|
|
crate-wide allows, so the warnings come back as the stubs get filled in.
|
|
- No `unsafe`, no FFI.
|
|
|
|
## License
|
|
|
|
MIT — see `LICENSE`, © 2026 huntedbytheirs.
|