use std::path::{Path, PathBuf};
use clap::Subcommand;
#[derive(Subcommand, Debug)]
pub enum KernelCommand {
#[command(long_about = KERNEL_LIST_LONG_ABOUT)]
List {
#[arg(long)]
json: bool,
#[arg(long)]
range: Option<String>,
},
Build {
#[arg(conflicts_with_all = ["source", "git"])]
version: Option<String>,
#[arg(long, conflicts_with_all = ["version", "git"])]
source: Option<PathBuf>,
#[arg(long, requires = "git_ref", conflicts_with_all = ["version", "source"])]
git: Option<String>,
#[arg(long = "ref", requires = "git")]
git_ref: Option<String>,
#[arg(long)]
force: bool,
#[arg(long)]
clean: bool,
#[arg(long, help = CPU_CAP_HELP)]
cpu_cap: Option<usize>,
#[arg(long = "extra-kconfig", value_name = "PATH", help = EXTRA_KCONFIG_HELP)]
extra_kconfig: Option<PathBuf>,
#[arg(long)]
skip_sha256: bool,
},
Clean {
#[arg(long)]
keep: Option<usize>,
#[arg(long)]
force: bool,
#[arg(long, conflicts_with = "keep")]
corrupt_only: bool,
},
}
pub const KERNEL_HELP_NO_RAW: &str = "Kernel identifier: a source directory \
path (e.g. `../linux`), a version (`6.14.2`, or major.minor prefix \
`6.14` for latest patch), a cache key (see `kernel list`), a \
version range (`6.12..6.14`), or a git source (`git+URL#REF`). Raw \
image files are rejected. Source directories auto-build (can be slow \
on a fresh tree); versions auto-download from kernel.org on cache \
miss. The flag is REPEATABLE on `test`, `coverage`, and `llvm-cov` \
— passing multiple `--kernel` flags fans the gauntlet across every \
resolved kernel; each (test × scenario × topology × kernel) \
tuple becomes a distinct nextest test case so nextest's parallelism, \
retries, and `-E` filtering work natively. Ranges expand to every \
`stable` and `longterm` release inside `[START, END]` inclusive \
(mainline / linux-next dropped). Git sources clone shallow at the \
ref and build once. In contrast, `ktstr shell` accepts a single \
kernel only — pass exactly one `--kernel`.";
pub const KERNEL_HELP_RAW_OK: &str = "Kernel identifier: a source directory \
path (e.g. `../linux`), a raw image file (`bzImage` / `Image`), a \
version (`6.14.2`, or major.minor prefix `6.14` for latest patch), \
or a cache key (see `kernel list`). Source directories auto-build \
(can be slow on a fresh tree); versions auto-download from kernel.org \
on cache miss. When absent, resolves via cache then filesystem, \
falling back to downloading the latest stable kernel. Ranges \
(`START..END`) and git sources (`git+URL#REF`) are not supported \
in this context; pass a single kernel.";
pub const CPU_CAP_HELP: &str = "Reserve exactly N host CPUs for the build or \
no-perf-mode shell. Integer ≥ 1; must be ≤ the calling process's \
sched_getaffinity cpuset size (the allowed CPU count, NOT the \
host's total online CPUs — under a cgroup-restricted runner the \
allowed set is typically smaller). When absent, 30% of the \
allowed CPUs are reserved (minimum 1). The planner walks whole \
LLCs in consolidation- and NUMA-aware order, filtered to the \
allowed cpuset, partial-taking the last LLC so `plan.cpus.len() \
== N` exactly. The flock set may cover more LLCs than strictly \
required (flock coordination is per-LLC even when the last LLC \
is only partially used for the CPU budget). Run `ktstr locks \
--watch 1s` to observe NUMA placement live. Under --cpu-cap, \
make's `-jN` parallelism matches the reserved CPU count and the \
kernel build runs inside a cgroup v2 sandbox that pins gcc/ld \
to the reserved CPUs + NUMA nodes; if the sandbox cannot be \
installed (missing cgroup v2, missing cpuset controller, \
permission denied), the build aborts rather than running \
without enforcement. Mutually exclusive with \
KTSTR_BYPASS_LLC_LOCKS=1. On `ktstr shell`, requires \
--no-perf-mode (perf-mode already holds every LLC exclusively). \
Also settable via KTSTR_CPU_CAP env var (CLI flag wins when both \
are present).";
pub const EXTRA_KCONFIG_HELP: &str = "Additional kconfig fragment merged on top of \
the baked-in `ktstr.kconfig`. Same line shapes the kernel uses: \
`CONFIG_FOO=y`, `CONFIG_FOO=m`, `CONFIG_FOO=\"value\"`, and \
`# CONFIG_FOO is not set`. User values win on conflict; \
`make olddefconfig` resolves dependencies. Each unique fragment \
produces a distinct cache slot via the `kc{baked}-xkc{extra}` \
key suffix. After build, `validate_kernel_config` rejects \
entries that disabled critical baked-in symbols \
(CONFIG_SCHED_CLASS_EXT, CONFIG_DEBUG_INFO_BTF, CONFIG_BPF_SYSCALL, \
CONFIG_FTRACE, CONFIG_KPROBE_EVENTS, CONFIG_BPF_EVENTS). \
The baked-in fragment lives at `ktstr.kconfig` in the ktstr \
repository root.";
macro_rules! eol_explanation_literal {
() => {
"(EOL) marks entries whose major.minor series is absent from \
kernel.org's current active releases. Suppressed when the \
active-release list cannot be fetched."
};
}
pub const EOL_EXPLANATION: &str = eol_explanation_literal!();
pub const KERNEL_LIST_LONG_ABOUT: &str = concat!(
eol_explanation_literal!(),
"\n\n",
"--json emits one JSON object with three top-level fields:\n",
"\n",
" current_ktstr_kconfig_hash hex digest of the kconfig fragment the\n",
" running binary was built with, for\n",
" stale-entry detection.\n",
" active_prefixes_fetch_error null on success; error string on\n",
" active-series fetch failure. When\n",
" non-null, every entry's `eol` is false\n",
" regardless of actual support status —\n",
" check this field before trusting `eol`.\n",
" entries array of per-entry objects. Each\n",
" element is either a VALID entry (full\n",
" field set) or a CORRUPT entry (only\n",
" `key`, `path`, `error`). Detect\n",
" corruption by the presence of `error`.\n",
"\n",
"Valid entry fields: key, path, version (nullable), source, arch,\n",
"built_at, ktstr_kconfig_hash (nullable), extra_kconfig_hash\n",
"(nullable), kconfig_status, eol, config_hash (nullable),\n",
"image_name, image_path, has_vmlinux, vmlinux_stripped.\n",
"\n",
" path absolute path to the cache entry DIRECTORY.\n",
" image_path absolute path to the boot image file INSIDE\n",
" that directory. `path` points at the dir, not\n",
" the image — scripts that want the kernel\n",
" artifact to pass to qemu/vm-loaders should\n",
" read `image_path`, not join `path` with a\n",
" hardcoded filename.\n",
" kconfig_status one of \"matches\", \"stale\", \"untracked\"\n",
" (Display form of cache::KconfigStatus).\n",
" source internally-tagged on \"type\":\n",
" {\"type\": \"tarball\"}\n",
" {\"type\": \"git\", \"git_hash\": ?, \"ref\": ?}\n",
" {\"type\": \"local\", \"source_tree_path\": ?,\n",
" \"git_hash\": ?}\n",
" Dispatch on \"type\" before reading variant\n",
" fields.\n",
" eol true iff the entry's major.minor series is absent\n",
" from the active-prefix list. Meaningful only when\n",
" active_prefixes_fetch_error is null. Also false\n",
" whenever version is null (the missing-version\n",
" short-circuit in `entry_is_eol`).\n",
" has_vmlinux true iff the uncompressed vmlinux is cached\n",
" alongside the compressed image (required for\n",
" DWARF-driven probes).\n",
" vmlinux_stripped true iff the cached vmlinux came from a\n",
" successful strip pass. false marks the\n",
" raw-fallback path — a larger on-disk payload\n",
" indicating the strip pipeline errored on this\n",
" kernel; the entry is still usable but the\n",
" fallback is a signal to investigate. Meaningful\n",
" only when has_vmlinux is true (false otherwise).\n",
" config_hash CRC32 of the final merged .config; distinct\n",
" from ktstr_kconfig_hash which covers only the\n",
" ktstr fragment.\n",
" extra_kconfig_hash\n",
" CRC32 of the user `--extra-kconfig` fragment\n",
" (raw bytes, no canonicalization), or null when\n",
" the entry was built without --extra-kconfig.\n",
" The cache key suffix grows from `kc{baked}` to\n",
" `kc{baked}-xkc{extra}` when extras are present,\n",
" and this field stores the `xkc` segment so\n",
" `kernel list` is self-describing for entries\n",
" that carry user modifications.\n",
"\n",
"When --range is set, the subcommand SWITCHES to range-preview\n",
"mode and emits a structurally different JSON shape — the cache\n",
"is not walked at all, only kernel.org's releases.json is fetched\n",
"to expand the inclusive range. The --json output is one object\n",
"with four top-level fields:\n",
"\n",
" range literal range string supplied to --range\n",
" (e.g. \"6.12..6.14\").\n",
" start parsed start endpoint\n",
" (MAJOR.MINOR[.PATCH][-rcN]).\n",
" end parsed end endpoint, same shape as start.\n",
" versions array of resolved version strings inside\n",
" [start, end] inclusive, ascending by\n",
" (major, minor, patch, rc) tuple. Stable and\n",
" longterm releases only — mainline / linux-next\n",
" are excluded by the moniker filter.\n",
"\n",
"Range-mode output never carries cache metadata\n",
"(no current_ktstr_kconfig_hash, no entries) — to inspect cached\n",
"kernels for one of the resolved versions, run `kernel list`\n",
"without --range. Consumers should dispatch on the presence of\n",
"the `range` key (range mode) versus `entries` key (list mode)\n",
"to branch the parse."
);
pub const DIRTY_TREE_CACHE_SKIP_HINT: &str = "skipping cache — working tree has uncommitted changes; \
commit or stash to enable caching";
pub const NON_GIT_TREE_CACHE_SKIP_HINT: &str = "skipping cache — source tree is not a git repository so dirty \
state cannot be detected; put the source under git, or replace \
`--source` with one of the content-keyed fetch modes that does \
not need dirty-state detection — `kernel build VERSION` \
(downloads the tarball from kernel.org) or \
`kernel build --git URL --ref REF` (shallow-clones the given \
ref) — to enable caching";
pub(crate) fn eol_legend_if_any(any_eol: bool) -> Option<&'static str> {
if any_eol { Some(EOL_EXPLANATION) } else { None }
}
pub const UNTRACKED_KCONFIG_EXPLANATION: &str = "(untracked kconfig) marks entries with no recorded ktstr.kconfig hash \
(pre-dates kconfig hash tracking). Rebuild with: kernel build --force VERSION \
(add --extra-kconfig PATH if the original entry was built with a user fragment).";
pub(crate) fn untracked_legend_if_any(any_untracked: bool) -> Option<&'static str> {
if any_untracked {
Some(UNTRACKED_KCONFIG_EXPLANATION)
} else {
None
}
}
pub const STALE_KCONFIG_EXPLANATION: &str = "warning: entries marked (stale kconfig) were built against a different ktstr.kconfig. \
Rebuild with: kernel build --force <entry version> \
(add --extra-kconfig PATH if the entry also carries the (extra kconfig) tag).";
pub(crate) fn stale_legend_if_any(any_stale: bool) -> Option<&'static str> {
if any_stale {
Some(STALE_KCONFIG_EXPLANATION)
} else {
None
}
}
pub(crate) fn format_corrupt_footer(cache_root: &Path) -> String {
format!(
"warning: entries marked (corrupt) cannot be used — cached metadata is \
missing, malformed, or references a missing image. Inspect the entry \
directory under {} to remove it manually, or run \
`kernel clean --corrupt-only --force` which removes ONLY corrupt \
entries and leaves valid ones intact. For broader cleanup, \
`kernel clean --force` removes ALL cached entries (valid and corrupt \
alike); `kernel clean --keep N --force` preserves the N newest \
cached entries while removing the rest.",
cache_root.display(),
)
}
pub(crate) fn corrupt_footer_if_any(corrupt_count: usize, cache_root: &Path) -> Option<String> {
if corrupt_count == 0 {
return None;
}
let noun = if corrupt_count == 1 {
"entry"
} else {
"entries"
};
let summary = format!(
"{corrupt_count} corrupt {noun}. \
Run `cargo ktstr kernel clean --corrupt-only` to remove.",
);
let detail = format_corrupt_footer(cache_root);
Some(format!("{summary}\n{detail}"))
}
pub const EMBEDDED_KCONFIG: &str = crate::EMBEDDED_KCONFIG;
pub fn embedded_kconfig_hash() -> String {
crate::kconfig_hash()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn eol_legend_if_any_branches() {
assert_eq!(eol_legend_if_any(true), Some(EOL_EXPLANATION));
assert_eq!(eol_legend_if_any(false), None);
}
#[test]
fn untracked_legend_if_any_branches() {
assert_eq!(
untracked_legend_if_any(true),
Some(UNTRACKED_KCONFIG_EXPLANATION),
);
assert_eq!(untracked_legend_if_any(false), None);
}
#[test]
fn stale_legend_if_any_branches() {
assert_eq!(stale_legend_if_any(true), Some(STALE_KCONFIG_EXPLANATION));
assert_eq!(stale_legend_if_any(false), None);
}
#[test]
fn stale_kconfig_explanation_shape() {
assert!(STALE_KCONFIG_EXPLANATION.starts_with("warning"));
assert!(STALE_KCONFIG_EXPLANATION.contains("(stale kconfig)"));
assert!(STALE_KCONFIG_EXPLANATION.contains("different ktstr.kconfig"));
assert!(STALE_KCONFIG_EXPLANATION.contains("kernel build --force <entry version>"));
}
#[test]
fn corrupt_footer_if_any_branches() {
let root = std::path::Path::new("/tmp/ktstr-cache-test-root");
assert_eq!(corrupt_footer_if_any(0, root), None);
let one = corrupt_footer_if_any(1, root).expect("positive count must yield Some(footer)");
assert!(one.contains("1 corrupt entry."));
assert!(one.contains("cargo ktstr kernel clean --corrupt-only"));
assert!(one.contains(&format_corrupt_footer(root)));
let many = corrupt_footer_if_any(3, root).expect("positive count must yield Some(footer)");
assert!(many.contains("3 corrupt entries."));
}
#[test]
fn corrupt_footer_is_self_documenting() {
let root = std::path::Path::new("/tmp/ktstr-cache-test-root");
let footer = format_corrupt_footer(root);
let first_sentence = footer
.split_once(". ")
.map(|(head, _)| head)
.expect("footer must terminate legend sentence with period-space");
assert!(first_sentence.contains("(corrupt)"));
assert!(first_sentence.contains("cannot be used"));
for reason_token in ["metadata is missing", "malformed", "missing image"] {
assert!(
first_sentence.contains(reason_token),
"legend sentence must enumerate corruption modes; \
expected `{reason_token}`, got: {first_sentence:?}",
);
}
assert!(footer.contains(&root.display().to_string()));
assert!(footer.contains("kernel clean --corrupt-only --force"));
assert!(footer.contains("kernel clean --force"));
assert!(footer.contains("kernel clean --keep N --force"));
assert!(footer.contains("ALL cached entries"));
let pos_corrupt_only = footer
.find("kernel clean --corrupt-only --force")
.expect("--corrupt-only must appear");
let pos_force = footer
.find("kernel clean --force")
.expect("--force must appear");
let pos_keep = footer
.find("kernel clean --keep N --force")
.expect("--keep must appear");
assert!(pos_corrupt_only < pos_force);
assert!(pos_force < pos_keep);
}
#[test]
fn dirty_tree_cache_skip_hint_shape() {
assert!(DIRTY_TREE_CACHE_SKIP_HINT.contains("skipping cache"));
assert!(DIRTY_TREE_CACHE_SKIP_HINT.contains("uncommitted changes"));
assert!(
DIRTY_TREE_CACHE_SKIP_HINT.contains("commit")
&& DIRTY_TREE_CACHE_SKIP_HINT.contains("stash")
);
}
#[test]
fn non_git_tree_cache_skip_hint_shape() {
assert!(NON_GIT_TREE_CACHE_SKIP_HINT.starts_with("skipping cache"));
assert!(NON_GIT_TREE_CACHE_SKIP_HINT.contains("not a git repository"));
assert!(NON_GIT_TREE_CACHE_SKIP_HINT.contains("put the source under git"));
assert!(NON_GIT_TREE_CACHE_SKIP_HINT.contains("kernel build VERSION"));
assert!(NON_GIT_TREE_CACHE_SKIP_HINT.contains("kernel build --git URL --ref REF"));
assert!(!NON_GIT_TREE_CACHE_SKIP_HINT.contains("stash"));
assert!(!NON_GIT_TREE_CACHE_SKIP_HINT.contains("commit"));
}
#[test]
fn untracked_legend_names_the_tag_word() {
assert!(UNTRACKED_KCONFIG_EXPLANATION.contains("(untracked kconfig)"));
}
#[test]
fn kernel_clean_rejects_corrupt_only_with_keep() {
use clap::Parser as _;
#[derive(clap::Parser, Debug)]
struct TestCli {
#[command(subcommand)]
cmd: KernelCommand,
}
let err = TestCli::try_parse_from(["prog", "clean", "--keep", "2", "--corrupt-only"])
.expect_err("--keep together with --corrupt-only must fail parsing");
let msg = err.to_string();
assert!(
msg.to_ascii_lowercase().contains("cannot be used with")
|| msg.to_ascii_lowercase().contains("conflict"),
"clap error must surface the conflict between --keep and --corrupt-only, got: {msg}",
);
}
#[test]
fn kernel_clean_accepts_corrupt_only_alone() {
use clap::Parser as _;
#[derive(clap::Parser, Debug)]
struct TestCli {
#[command(subcommand)]
cmd: KernelCommand,
}
let parsed = TestCli::try_parse_from(["prog", "clean", "--corrupt-only"])
.expect("--corrupt-only without --keep must parse cleanly");
match parsed.cmd {
KernelCommand::Clean {
keep,
force,
corrupt_only,
} => {
assert_eq!(keep, None);
assert!(!force);
assert!(corrupt_only);
}
other => panic!("expected KernelCommand::Clean, got {other:?}"),
}
}
#[test]
fn kernel_build_parses_cpu_cap_without_extra_flags() {
use clap::Parser as _;
#[derive(clap::Parser, Debug)]
struct TestCli {
#[command(subcommand)]
cmd: KernelCommand,
}
let parsed = TestCli::try_parse_from(["prog", "build", "6.14.2", "--cpu-cap", "4"])
.expect("kernel build --cpu-cap N must parse");
match parsed.cmd {
KernelCommand::Build {
cpu_cap, version, ..
} => {
assert_eq!(cpu_cap, Some(4));
assert_eq!(version.as_deref(), Some("6.14.2"));
}
other => panic!("expected KernelCommand::Build, got {other:?}"),
}
}
#[test]
fn kernel_build_without_cpu_cap_defaults_to_none() {
use clap::Parser as _;
#[derive(clap::Parser, Debug)]
struct TestCli {
#[command(subcommand)]
cmd: KernelCommand,
}
let parsed = TestCli::try_parse_from(["prog", "build", "6.14.2"])
.expect("kernel build without --cpu-cap must parse");
match parsed.cmd {
KernelCommand::Build { cpu_cap, .. } => {
assert_eq!(cpu_cap, None, "no --cpu-cap must produce None, not Some(0)");
}
other => panic!("expected KernelCommand::Build, got {other:?}"),
}
}
#[test]
fn kernel_build_cpu_cap_zero_passes_clap() {
use clap::Parser as _;
#[derive(clap::Parser, Debug)]
struct TestCli {
#[command(subcommand)]
cmd: KernelCommand,
}
let parsed = TestCli::try_parse_from(["prog", "build", "6.14.2", "--cpu-cap", "0"])
.expect("clap-level parse must accept 0; runtime validation rejects");
match parsed.cmd {
KernelCommand::Build { cpu_cap, .. } => {
assert_eq!(cpu_cap, Some(0));
}
other => panic!("expected KernelCommand::Build, got {other:?}"),
}
}
#[test]
fn kernel_build_parses_skip_sha256() {
use clap::Parser as _;
#[derive(clap::Parser, Debug)]
struct TestCli {
#[command(subcommand)]
cmd: KernelCommand,
}
let parsed = TestCli::try_parse_from(["prog", "build", "6.14.2", "--skip-sha256"])
.expect("kernel build --skip-sha256 must parse");
match parsed.cmd {
KernelCommand::Build { skip_sha256, .. } => {
assert!(
skip_sha256,
"--skip-sha256 must round-trip as true so the \
downstream download path bypasses sha256sums.asc"
);
}
other => panic!("expected KernelCommand::Build, got {other:?}"),
}
}
#[test]
fn kernel_build_without_skip_sha256_defaults_to_false() {
use clap::Parser as _;
#[derive(clap::Parser, Debug)]
struct TestCli {
#[command(subcommand)]
cmd: KernelCommand,
}
let parsed = TestCli::try_parse_from(["prog", "build", "6.14.2"])
.expect("kernel build without --skip-sha256 must parse");
match parsed.cmd {
KernelCommand::Build { skip_sha256, .. } => {
assert!(
!skip_sha256,
"no --skip-sha256 must produce false (verification \
enabled by default); got skip_sha256={skip_sha256}"
);
}
other => panic!("expected KernelCommand::Build, got {other:?}"),
}
}
#[test]
fn kernel_list_long_about_exposes_json_schema() {
assert!(
KERNEL_LIST_LONG_ABOUT.starts_with(EOL_EXPLANATION),
"KERNEL_LIST_LONG_ABOUT must embed EOL_EXPLANATION verbatim at its \
head so the --help and post-table legend share one source of \
truth; got: {KERNEL_LIST_LONG_ABOUT:?}",
);
for wrapper_field in [
"current_ktstr_kconfig_hash",
"active_prefixes_fetch_error",
"entries",
] {
assert!(
KERNEL_LIST_LONG_ABOUT.contains(wrapper_field),
"KERNEL_LIST_LONG_ABOUT must mention top-level wrapper field \
`{wrapper_field}` so scripted consumers discover the \
schema without `cargo doc`",
);
}
for valid_entry_field in [
"key",
"path",
"version",
"source",
"arch",
"built_at",
"ktstr_kconfig_hash",
"kconfig_status",
"eol",
"config_hash",
"image_name",
"image_path",
"has_vmlinux",
"vmlinux_stripped",
"git_hash",
"\"ref\"",
"source_tree_path",
] {
assert!(
KERNEL_LIST_LONG_ABOUT.contains(valid_entry_field),
"KERNEL_LIST_LONG_ABOUT must mention valid-entry JSON \
field `{valid_entry_field}`",
);
}
assert!(
KERNEL_LIST_LONG_ABOUT.contains("error"),
"KERNEL_LIST_LONG_ABOUT must mention corrupt-entry JSON \
field `error` so consumers know the corrupt-entry shape",
);
for nullable_field in ["version", "ktstr_kconfig_hash", "config_hash"] {
let marker = format!("{nullable_field} (nullable)");
assert!(
KERNEL_LIST_LONG_ABOUT.contains(&marker),
"KERNEL_LIST_LONG_ABOUT must mark `{nullable_field}` \
as `(nullable)` (expected substring `{marker}`)",
);
}
for source_variant_tag in ["\"tarball\"", "\"git\"", "\"local\""] {
assert!(
KERNEL_LIST_LONG_ABOUT.contains(source_variant_tag),
"KERNEL_LIST_LONG_ABOUT must list source variant tag \
`{source_variant_tag}`",
);
}
for status_variant in ["\"matches\"", "\"stale\"", "\"untracked\""] {
assert!(
KERNEL_LIST_LONG_ABOUT.contains(status_variant),
"KERNEL_LIST_LONG_ABOUT must list kconfig_status variant \
`{status_variant}`",
);
}
}
#[test]
fn kernel_list_long_about_wired_via_clap() {
use clap::CommandFactory as _;
#[derive(clap::Parser, Debug)]
struct TestCli {
#[command(subcommand)]
cmd: KernelCommand,
}
let cmd = TestCli::command();
let list = cmd
.find_subcommand("list")
.expect("clap must register a `list` subcommand on KernelCommand");
let long_about = list
.get_long_about()
.expect("`list` subcommand must have a long_about set")
.to_string();
assert_eq!(long_about, KERNEL_LIST_LONG_ABOUT);
}
#[test]
fn kernel_build_help_documents_skip_sha256_no_op_semantics() {
use clap::CommandFactory as _;
#[derive(clap::Parser, Debug)]
struct TestCli {
#[command(subcommand)]
cmd: KernelCommand,
}
let cmd = TestCli::command();
let build = cmd
.find_subcommand("build")
.expect("clap must register a `build` subcommand on KernelCommand");
let arg = build
.get_arguments()
.find(|a| a.get_long() == Some("skip-sha256"))
.expect("kernel build must register --skip-sha256 with clap");
let help = arg
.get_long_help()
.or_else(|| arg.get_help())
.map(|s| s.to_string())
.expect("--skip-sha256 must carry help text");
assert!(
help.contains("--source"),
"--skip-sha256 help must call out the --source no-op so \
operators don't expect bypass on local-source builds: {help}"
);
assert!(
help.contains("--git"),
"--skip-sha256 help must call out the --git no-op so \
operators don't expect bypass on git-source builds: {help}"
);
assert!(
help.contains("RC"),
"--skip-sha256 help must call out the RC-tarball no-op \
(RC archives have no upstream manifest, so the flag is \
a no-op there): {help}"
);
assert!(
help.contains("warning"),
"--skip-sha256 help must mention the bypass warning so \
ops know the lost guarantee surfaces in the same channel \
as verification-success lines: {help}"
);
}
}