scaffold: CLI surface with stubbed kernel and patch verbs

Add the spectral command tree -- kernel quest/test and patch
check/format/commit/create/submit/update -- with every verb wired through
main to a todo!() body, plus the pieces they will share:

- config: resolve the kernel tree from $SPECTRAL_KERNEL, falling back to
  ~/.spectral/linux, patches under ~/.spectral/patches, and confirm a tree
  really is one before anything uses it
- git: a thin wrapper over the git binary, currently only run
- error: one thiserror enum, converted once in main
- patch/checkpatch and patch/maintainers: seams around the tree's own
  scripts/checkpatch.pl and scripts/get_maintainer.pl, so spectral never
  carries a stale copy of the kernel's rules

20 functions are todo!(); the #[allow(dead_code)] sites mark the items
that are unreachable only until their caller is written. fmt, clippy -D
warnings, and test are all clean.
This commit is contained in:
2026-09-14 20:38:20 -04:00
parent 0ee045c71e
commit c25bf94c33
13 changed files with 754 additions and 3 deletions
+78
View File
@@ -0,0 +1,78 @@
//! Where the kernel tree and the patches live.
//!
//! Everything else asks this module for paths, so the day a config file or
//! `spectral init` arrives, it is the only thing that changes.
#![allow(dead_code)] // accessors are read once the patch verbs stop being stubs
use std::env;
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
/// Environment variable that points at the kernel checkout.
pub const ENV_KERNEL_TREE: &str = "SPECTRAL_KERNEL";
/// Tree used when that variable is unset, relative to `$HOME`.
const DEFAULT_KERNEL_TREE: &str = ".spectral/linux";
/// Where written patches land, relative to `$HOME`.
const DEFAULT_PATCH_DIR: &str = ".spectral/patches";
#[derive(Debug, Clone)]
pub struct Config {
kernel_tree: PathBuf,
patch_dir: PathBuf,
}
impl Config {
/// Resolve the tree from `$SPECTRAL_KERNEL`, falling back to
/// `~/.spectral/linux`.
///
/// The tree is not checked for existence here — commands that need it call
/// [`Config::require_kernel_tree`], so `kernel quest` still works before
/// anything is cloned.
///
/// TODO: also read `~/.config/spectral/config.toml` once `spectral init`
/// exists. The environment variable should keep winning over the file.
pub fn load() -> Result<Self> {
let home = home_dir();
let kernel_tree = env::var_os(ENV_KERNEL_TREE)
.map_or_else(|| home.join(DEFAULT_KERNEL_TREE), PathBuf::from);
Ok(Self {
kernel_tree,
patch_dir: home.join(DEFAULT_PATCH_DIR),
})
}
#[must_use]
pub fn kernel_tree(&self) -> &Path {
&self.kernel_tree
}
#[must_use]
pub fn patch_dir(&self) -> &Path {
&self.patch_dir
}
/// The kernel tree, having confirmed it is one.
pub fn require_kernel_tree(&self) -> Result<&Path> {
if self.kernel_tree.join("scripts/checkpatch.pl").is_file() {
Ok(&self.kernel_tree)
} else if self.kernel_tree.exists() {
Err(Error::NotAKernelTree {
path: self.kernel_tree.clone(),
})
} else {
Err(Error::KernelTreeMissing {
path: self.kernel_tree.clone(),
env: ENV_KERNEL_TREE,
})
}
}
}
fn home_dir() -> PathBuf {
env::var_os("HOME").map_or_else(|| PathBuf::from("."), PathBuf::from)
}