Skip to content

Commit f4d712b

Browse files
committed
Switch to hand-written man pages with auto option sync
See the updates to `Justfile` for how to use this. Closes: #1428 Assisted-By: Claude Code (opus + sonnet) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 84aba8e commit f4d712b

40 files changed

+1210
-788
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
- name: Run tests
4040
run: cargo test -- --nocapture --quiet
4141
- name: Manpage generation
42-
run: mkdir -p target/man && cargo run --features=docgen -- man --directory target/man
42+
run: cargo xtask update-generated
4343
- name: Clippy (gate on correctness and suspicous)
4444
run: make validate-rust
4545
fedora-container-tests:

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Justfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,14 @@ run-container-external-tests:
1616

1717
unittest *ARGS:
1818
podman build --jobs=4 --target units -t localhost/bootc-units --build-arg=unitargs={{ARGS}} .
19+
20+
# Update all generated files (man pages and JSON schemas)
21+
#
22+
# This is the unified command that:
23+
# - Auto-discovers new CLI commands and creates man page templates
24+
# - Syncs CLI options from Rust code to existing man page templates
25+
# - Updates JSON schema files
26+
#
27+
# Use this after adding, removing, or modifying CLI options or schemas.
28+
update-generated:
29+
cargo run -p xtask update-generated

Makefile

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,28 @@ TAR_REPRODUCIBLE = tar --mtime="@${SOURCE_DATE_EPOCH}" --sort=name --owner=0 --g
1010
# (Note we should also make installation of the units conditional on the rhsm feature)
1111
CARGO_FEATURES ?= $(shell . /usr/lib/os-release; if echo "$$ID_LIKE" |grep -qF rhel; then echo rhsm; fi)
1212

13-
all:
13+
all: bin manpages
14+
15+
bin:
1416
cargo build --release --features "$(CARGO_FEATURES)"
1517

18+
# Generate man pages from markdown sources
19+
MAN5_SOURCES := $(wildcard docs/src/man/*.5.md)
20+
MAN8_SOURCES := $(wildcard docs/src/man/*.8.md)
21+
TARGETMAN := target/man
22+
MAN5_TARGETS := $(patsubst docs/src/man/%.5.md,$(TARGETMAN)/%.5,$(MAN5_SOURCES))
23+
MAN8_TARGETS := $(patsubst docs/src/man/%.8.md,$(TARGETMAN)/%.8,$(MAN8_SOURCES))
24+
25+
$(TARGETMAN)/%.5: docs/src/man/%.5.md
26+
@mkdir -p $(TARGETMAN)
27+
go-md2man -in $< -out $@
28+
29+
$(TARGETMAN)/%.8: docs/src/man/%.8.md
30+
@mkdir -p $(TARGETMAN)
31+
go-md2man -in $< -out $@
32+
33+
manpages: $(MAN5_TARGETS) $(MAN8_TARGETS)
34+
1635
STORAGE_RELATIVE_PATH ?= $(shell realpath -m -s --relative-to="$(prefix)/lib/bootc/storage" /sysroot/ostree/bootc/storage)
1736
install:
1837
install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bootc
@@ -22,15 +41,12 @@ install:
2241
ln -s "$(STORAGE_RELATIVE_PATH)" "$(DESTDIR)$(prefix)/lib/bootc/storage"
2342
install -D -m 0755 crates/cli/bootc-generator-stub $(DESTDIR)$(prefix)/lib/systemd/system-generators/bootc-systemd-generator
2443
install -d $(DESTDIR)$(prefix)/lib/bootc/install
25-
# Support installing pre-generated man pages shipped in source tarball, to avoid
26-
# a dependency on pandoc downstream. But in local builds these end up in target/man,
27-
# so we honor that too.
28-
for d in man target/man; do \
29-
if test -d $$d; then \
30-
install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man5 $$d/*.5; \
31-
install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man8 $$d/*.8; \
32-
fi; \
33-
done
44+
if [ -n "$(MAN5_TARGETS)" ]; then \
45+
install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man5 $(MAN5_TARGETS); \
46+
fi
47+
if [ -n "$(MAN8_TARGETS)" ]; then \
48+
install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man8 $(MAN8_TARGETS); \
49+
fi
3450
install -D -m 0644 -t $(DESTDIR)/$(prefix)/lib/systemd/system systemd/*.service systemd/*.timer systemd/*.path systemd/*.target
3551
install -d -m 0755 $(DESTDIR)/$(prefix)/lib/systemd/system/multi-user.target.wants
3652
ln -s ../bootc-status-updated.path $(DESTDIR)/$(prefix)/lib/systemd/system/multi-user.target.wants/bootc-status-updated.path

crates/lib/src/cli.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -279,14 +279,6 @@ pub(crate) enum InstallOpts {
279279
PrintConfiguration,
280280
}
281281

282-
/// Options for man page generation
283-
#[derive(Debug, Parser, PartialEq, Eq)]
284-
pub(crate) struct ManOpts {
285-
#[clap(long)]
286-
/// Output to this directory
287-
pub(crate) directory: Utf8PathBuf,
288-
}
289-
290282
/// Subcommands which can be executed as part of a container build.
291283
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
292284
pub(crate) enum ContainerOpts {
@@ -540,6 +532,9 @@ pub(crate) enum InternalsOpts {
540532
#[clap(long)]
541533
merge: bool,
542534
},
535+
#[cfg(feature = "docgen")]
536+
/// Dump CLI structure as JSON for documentation generation
537+
DumpCliJson,
543538
}
544539

545540
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
@@ -704,9 +699,6 @@ pub(crate) enum Opt {
704699
#[clap(subcommand)]
705700
#[clap(hide = true)]
706701
Internals(InternalsOpts),
707-
#[clap(hide(true))]
708-
#[cfg(feature = "docgen")]
709-
Man(ManOpts),
710702
}
711703

712704
/// Ensure we've entered a mount namespace, so that we can remount
@@ -1500,6 +1492,14 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
15001492
}
15011493
#[cfg(feature = "rhsm")]
15021494
InternalsOpts::PublishRhsmFacts => crate::rhsm::publish_facts(&root).await,
1495+
#[cfg(feature = "docgen")]
1496+
InternalsOpts::DumpCliJson => {
1497+
use clap::CommandFactory;
1498+
let cmd = Opt::command();
1499+
let json = crate::cli_json::dump_cli_json(&cmd)?;
1500+
println!("{}", json);
1501+
Ok(())
1502+
}
15031503
InternalsOpts::DirDiff {
15041504
pristine_etc,
15051505
current_etc,
@@ -1523,8 +1523,6 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
15231523
Ok(())
15241524
}
15251525
},
1526-
#[cfg(feature = "docgen")]
1527-
Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory),
15281526
Opt::State(opts) => match opts {
15291527
StateOpts::WipeOstree => {
15301528
let sysroot = ostree::Sysroot::new_default();

crates/lib/src/cli_json.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//! Export CLI structure as JSON for documentation generation
2+
3+
use clap::Command;
4+
use serde::{Deserialize, Serialize};
5+
6+
/// Representation of a CLI option for JSON export
7+
#[derive(Debug, Serialize, Deserialize)]
8+
pub struct CliOption {
9+
pub long: String,
10+
pub short: Option<String>,
11+
pub value_name: Option<String>,
12+
pub default: Option<String>,
13+
pub help: String,
14+
pub possible_values: Vec<String>,
15+
pub required: bool,
16+
}
17+
18+
/// Representation of a CLI command for JSON export
19+
#[derive(Debug, Serialize, Deserialize)]
20+
pub struct CliCommand {
21+
pub name: String,
22+
pub about: Option<String>,
23+
pub options: Vec<CliOption>,
24+
pub positionals: Vec<CliPositional>,
25+
pub subcommands: Vec<CliCommand>,
26+
}
27+
28+
/// Representation of a positional argument
29+
#[derive(Debug, Serialize, Deserialize)]
30+
pub struct CliPositional {
31+
pub name: String,
32+
pub help: Option<String>,
33+
pub required: bool,
34+
pub multiple: bool,
35+
}
36+
37+
/// Convert a clap Command to our JSON representation
38+
pub fn command_to_json(cmd: &Command) -> CliCommand {
39+
let mut options = Vec::new();
40+
let mut positionals = Vec::new();
41+
42+
// Extract arguments
43+
for arg in cmd.get_arguments() {
44+
let id = arg.get_id().as_str();
45+
46+
// Skip built-in help and version
47+
if id == "help" || id == "version" {
48+
continue;
49+
}
50+
51+
// Skip hidden arguments
52+
if arg.is_hide_set() {
53+
continue;
54+
}
55+
56+
if arg.is_positional() {
57+
// Handle positional arguments
58+
positionals.push(CliPositional {
59+
name: id.to_string(),
60+
help: arg.get_help().map(|h| h.to_string()),
61+
required: arg.is_required_set(),
62+
multiple: false, // For now, simplify this
63+
});
64+
} else {
65+
// Handle options/flags
66+
let mut possible_values = Vec::new();
67+
let pvs = arg.get_possible_values();
68+
if !pvs.is_empty() {
69+
for pv in pvs {
70+
possible_values.push(pv.get_name().to_string());
71+
}
72+
}
73+
74+
let help = arg.get_help().map(|h| h.to_string()).unwrap_or_default();
75+
76+
options.push(CliOption {
77+
long: arg
78+
.get_long()
79+
.map(String::from)
80+
.unwrap_or_else(|| id.to_string()),
81+
short: arg.get_short().map(|c| c.to_string()),
82+
value_name: arg
83+
.get_value_names()
84+
.and_then(|names| names.first())
85+
.map(|s| s.to_string()),
86+
default: arg
87+
.get_default_values()
88+
.first()
89+
.and_then(|v| v.to_str())
90+
.map(String::from),
91+
help,
92+
possible_values,
93+
required: arg.is_required_set(),
94+
});
95+
}
96+
}
97+
98+
// Extract subcommands
99+
let mut subcommands = Vec::new();
100+
for subcmd in cmd.get_subcommands() {
101+
// Skip help subcommand
102+
if subcmd.get_name() == "help" {
103+
continue;
104+
}
105+
106+
// Skip hidden subcommands
107+
if subcmd.is_hide_set() {
108+
continue;
109+
}
110+
111+
subcommands.push(command_to_json(subcmd));
112+
}
113+
114+
CliCommand {
115+
name: cmd.get_name().to_string(),
116+
about: cmd.get_about().map(|s| s.to_string()),
117+
options,
118+
positionals,
119+
subcommands,
120+
}
121+
}
122+
123+
/// Dump the entire CLI structure as JSON
124+
pub fn dump_cli_json(cmd: &Command) -> Result<String, serde_json::Error> {
125+
let cli_structure = command_to_json(cmd);
126+
serde_json::to_string_pretty(&cli_structure)
127+
}

crates/lib/src/docgen.rs

Lines changed: 0 additions & 55 deletions
This file was deleted.

crates/lib/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ mod task;
2929
mod utils;
3030

3131
#[cfg(feature = "docgen")]
32-
mod docgen;
32+
mod cli_json;
3333

3434
mod bootloader;
3535
mod containerenv;

crates/xtask/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ anyhow = { workspace = true }
1717
camino = { workspace = true }
1818
chrono = { workspace = true, features = ["std"] }
1919
fn-error-context = { workspace = true }
20+
serde = { workspace = true, features = ["derive"] }
21+
serde_json = { workspace = true }
2022
tempfile = { workspace = true }
2123
toml = { workspace = true }
2224
xshell = { workspace = true }

0 commit comments

Comments
 (0)