Skip to content

Commit 2cb69c1

Browse files
authored
Merge pull request #1619 from cgwalters/fix-man-sync
docs: Fix links to man pages
2 parents 19e82be + 856a7fb commit 2cb69c1

29 files changed

+189
-302
lines changed

Makefile

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,9 @@ all: bin manpages
1515
bin:
1616
cargo build --release --features "$(CARGO_FEATURES)"
1717

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)
18+
.PHONY: manpages
19+
manpages:
20+
cargo run --package xtask -- manpages
3421

3522
STORAGE_RELATIVE_PATH ?= $(shell realpath -m -s --relative-to="$(prefix)/lib/bootc/storage" /sysroot/ostree/bootc/storage)
3623
install:
@@ -41,12 +28,8 @@ install:
4128
ln -s "$(STORAGE_RELATIVE_PATH)" "$(DESTDIR)$(prefix)/lib/bootc/storage"
4229
install -D -m 0755 crates/cli/bootc-generator-stub $(DESTDIR)$(prefix)/lib/systemd/system-generators/bootc-systemd-generator
4330
install -d $(DESTDIR)$(prefix)/lib/bootc/install
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
31+
install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man5 target/man/*.5; \
32+
install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man8 target/man/*.8; \
5033
install -D -m 0644 -t $(DESTDIR)/$(prefix)/lib/systemd/system systemd/*.service systemd/*.timer systemd/*.path systemd/*.target
5134
install -d -m 0755 $(DESTDIR)/$(prefix)/lib/systemd/system/multi-user.target.wants
5235
ln -s ../bootc-status-updated.path $(DESTDIR)/$(prefix)/lib/systemd/system/multi-user.target.wants/bootc-status-updated.path

crates/lib/src/cli_json.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub struct CliOption {
1313
pub help: String,
1414
pub possible_values: Vec<String>,
1515
pub required: bool,
16+
pub is_boolean: bool,
1617
}
1718

1819
/// Representation of a CLI command for JSON export
@@ -73,16 +74,27 @@ pub fn command_to_json(cmd: &Command) -> CliCommand {
7374

7475
let help = arg.get_help().map(|h| h.to_string()).unwrap_or_default();
7576

77+
// For boolean flags, don't show a value name
78+
// Boolean flags use SetTrue or SetFalse actions and don't take values
79+
let is_boolean = matches!(
80+
arg.get_action(),
81+
clap::ArgAction::SetTrue | clap::ArgAction::SetFalse
82+
);
83+
let value_name = if is_boolean {
84+
None
85+
} else {
86+
arg.get_value_names()
87+
.and_then(|names| names.first())
88+
.map(|s| s.to_string())
89+
};
90+
7691
options.push(CliOption {
7792
long: arg
7893
.get_long()
7994
.map(String::from)
8095
.unwrap_or_else(|| id.to_string()),
8196
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()),
97+
value_name,
8698
default: arg
8799
.get_default_values()
88100
.first()
@@ -91,6 +103,7 @@ pub fn command_to_json(cmd: &Command) -> CliCommand {
91103
help,
92104
possible_values,
93105
required: arg.is_required_set(),
106+
is_boolean,
94107
});
95108
}
96109
}

crates/xtask/src/man.rs

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub struct CliOption {
2626
pub possible_values: Vec<String>,
2727
/// Whether the option is required
2828
pub required: bool,
29+
/// Whether this is a boolean flag
30+
pub is_boolean: bool,
2931
}
3032

3133
/// Represents a CLI command from the JSON dump
@@ -132,16 +134,17 @@ fn format_options_as_markdown(options: &[CliOption], positionals: &[CliPositiona
132134
// Add long flag
133135
flag_line.push_str(&format!("**--{}**", opt.long));
134136

135-
// Add value name if option takes argument
137+
// Add value name if option takes argument (but not for boolean flags)
138+
// Boolean flags are detected by having no value_name (set to None in cli_json.rs)
136139
if let Some(value_name) = &opt.value_name {
137140
flag_line.push_str(&format!("=*{}*", value_name));
138141
}
139142

140143
result.push_str(&format!("{}\n\n", flag_line));
141144
result.push_str(&format!(" {}\n\n", opt.help));
142145

143-
// Add possible values for enums
144-
if !opt.possible_values.is_empty() {
146+
// Add possible values for enums (but not for boolean flags)
147+
if !opt.possible_values.is_empty() && !opt.is_boolean {
145148
result.push_str(" Possible values:\n");
146149
for value in &opt.possible_values {
147150
result.push_str(&format!(" - {}\n", value));
@@ -191,10 +194,12 @@ pub fn update_markdown_with_subcommands(
191194
before, begin_marker, generated_subcommands, end_marker, after
192195
);
193196

194-
fs::write(markdown_path, new_content)
195-
.with_context(|| format!("Writing to {}", markdown_path))?;
196-
197-
println!("Updated subcommands in {}", markdown_path);
197+
// Only write if content has changed to avoid updating mtime unnecessarily
198+
if new_content != content {
199+
fs::write(markdown_path, new_content)
200+
.with_context(|| format!("Writing to {}", markdown_path))?;
201+
println!("Updated subcommands in {}", markdown_path);
202+
}
198203
Ok(())
199204
}
200205

@@ -238,10 +243,12 @@ pub fn update_markdown_with_options(
238243
format!("{}\n\n{}\n{}{}", before, begin_marker, end_marker, after)
239244
};
240245

241-
fs::write(markdown_path, new_content)
242-
.with_context(|| format!("Writing to {}", markdown_path))?;
243-
244-
println!("Updated {}", markdown_path);
246+
// Only write if content has changed to avoid updating mtime unnecessarily
247+
if new_content != content {
248+
fs::write(markdown_path, new_content)
249+
.with_context(|| format!("Writing to {}", markdown_path))?;
250+
println!("Updated {}", markdown_path);
251+
}
245252
Ok(())
246253
}
247254

@@ -374,21 +381,6 @@ pub fn sync_all_man_pages(sh: &Shell) -> Result<()> {
374381
Ok(())
375382
}
376383

377-
/// Test the sync workflow
378-
pub fn test_sync_workflow(sh: &Shell) -> Result<()> {
379-
println!("🧪 Testing man page sync workflow...");
380-
381-
// Create a backup of current files
382-
let test_dir = "target/test-sync";
383-
sh.create_dir(test_dir)?;
384-
385-
// Run sync
386-
sync_all_man_pages(sh)?;
387-
388-
println!("✅ Sync workflow test completed successfully");
389-
Ok(())
390-
}
391-
392384
/// Generate man pages from hand-written markdown sources
393385
pub fn generate_man_pages(sh: &Shell) -> Result<()> {
394386
let man_src_dir = Utf8Path::new("docs/src/man");
@@ -432,18 +424,31 @@ pub fn generate_man_pages(sh: &Shell) -> Result<()> {
432424
let content = fs::read_to_string(&path)?;
433425
let content_with_version = content.replace("<!-- VERSION PLACEHOLDER -->", &version);
434426

435-
// Create temporary file with version-replaced content
436-
let temp_path = format!("{}.tmp", path.display());
437-
fs::write(&temp_path, content_with_version)?;
427+
// Check if we need to regenerate by comparing input and output modification times
428+
let should_regenerate = if let (Ok(input_meta), Ok(output_meta)) =
429+
(fs::metadata(&path), fs::metadata(&output_file))
430+
{
431+
input_meta.modified().unwrap_or(std::time::UNIX_EPOCH)
432+
> output_meta.modified().unwrap_or(std::time::UNIX_EPOCH)
433+
} else {
434+
// If output doesn't exist or we can't get metadata, regenerate
435+
true
436+
};
437+
438+
if should_regenerate {
439+
// Create temporary file with version-replaced content
440+
let temp_path = format!("{}.tmp", path.display());
441+
fs::write(&temp_path, content_with_version)?;
438442

439-
cmd!(sh, "go-md2man -in {temp_path} -out {output_file}")
440-
.run()
441-
.with_context(|| format!("Converting {} to man page", path.display()))?;
443+
cmd!(sh, "go-md2man -in {temp_path} -out {output_file}")
444+
.run()
445+
.with_context(|| format!("Converting {} to man page", path.display()))?;
442446

443-
// Clean up temporary file
444-
fs::remove_file(&temp_path)?;
447+
// Clean up temporary file
448+
fs::remove_file(&temp_path)?;
445449

446-
println!("Generated {}", output_file);
450+
println!("Generated {}", output_file);
451+
}
447452
}
448453

449454
// Apply post-processing fixes for apostrophe handling
@@ -495,9 +500,9 @@ pub fn update_manpages(sh: &Shell) -> Result<()> {
495500
// Check each command and create man page if missing
496501
for command_parts in commands_to_check {
497502
let filename = if command_parts.len() == 1 {
498-
format!("bootc-{}.md", command_parts[0])
503+
format!("bootc-{}.8.md", command_parts[0])
499504
} else {
500-
format!("bootc-{}.md", command_parts.join("-"))
505+
format!("bootc-{}.8.md", command_parts.join("-"))
501506
};
502507

503508
let filepath = format!("docs/src/man/{}", filename);
@@ -511,14 +516,31 @@ pub fn update_manpages(sh: &Shell) -> Result<()> {
511516
let command_name_full = format!("bootc {}", command_parts.join(" "));
512517
let command_description = cmd.about.as_deref().unwrap_or("TODO: Add description");
513518

519+
// Generate SYNOPSIS line with proper arguments
520+
let mut synopsis = format!("**{}** \\[*OPTIONS...*\\]", command_name_full);
521+
522+
// Add positional arguments
523+
for positional in &cmd.positionals {
524+
if positional.required {
525+
synopsis.push_str(&format!(" <*{}*>", positional.name.to_uppercase()));
526+
} else {
527+
synopsis.push_str(&format!(" \\[*{}*\\]", positional.name.to_uppercase()));
528+
}
529+
}
530+
531+
// Add subcommand if this command has subcommands
532+
if !cmd.subcommands.is_empty() {
533+
synopsis.push_str(" <*SUBCOMMAND*>");
534+
}
535+
514536
let template = format!(
515537
r#"# NAME
516538
517539
{} - {}
518540
519541
# SYNOPSIS
520542
521-
**{}** [*OPTIONS*]
543+
{}
522544
523545
# DESCRIPTION
524546
@@ -549,19 +571,19 @@ TODO: Add practical examples showing how to use this command.
549571
std::fs::write(&filepath, template)
550572
.with_context(|| format!("Writing template to {}", filepath))?;
551573

552-
println!("Created man page template: {}", filepath);
574+
println!("Created man page template: {}", filepath);
553575
created_count += 1;
554576
}
555577
}
556578
}
557579

558580
if created_count > 0 {
559-
println!("Created {} new man page templates", created_count);
581+
println!("Created {} new man page templates", created_count);
560582
} else {
561-
println!("All commands already have man pages");
583+
println!("All commands already have man pages");
562584
}
563585

564-
println!("🔄 Syncing OPTIONS sections...");
586+
println!("Syncing OPTIONS sections...");
565587
sync_all_man_pages(sh)?;
566588

567589
println!("Man pages updated.");
@@ -585,6 +607,13 @@ fn apply_man_page_fixes(sh: &Shell, dir: &Utf8Path) -> Result<()> {
585607
.and_then(|s| s.to_str())
586608
.map_or(false, |e| e.chars().all(|c| c.is_numeric()))
587609
{
610+
// Check if the file already has the fix applied
611+
let content = fs::read_to_string(&path)?;
612+
if content.starts_with(".ds Aq \\(aq\n") {
613+
// Already fixed, skip
614+
continue;
615+
}
616+
588617
// Apply the same sed fixes as before
589618
let groffsub = r"1i .ds Aq \\(aq";
590619
let dropif = r"/\.g \.ds Aq/d";

crates/xtask/src/xtask.rs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@ fn main() {
3636
#[allow(clippy::type_complexity)]
3737
const TASKS: &[(&str, fn(&Shell) -> Result<()>)] = &[
3838
("manpages", man::generate_man_pages),
39-
("sync-manpages", man::sync_all_man_pages),
40-
("test-sync-manpages", man::test_sync_workflow),
41-
("update-json-schemas", update_json_schemas),
4239
("update-generated", update_generated),
4340
("package", package),
4441
("package-srpm", package_srpm),
@@ -47,17 +44,21 @@ const TASKS: &[(&str, fn(&Shell) -> Result<()>)] = &[
4744
];
4845

4946
fn try_main() -> Result<()> {
50-
// Ensure our working directory is the toplevel
47+
// Ensure our working directory is the toplevel (if we're in a git repo)
5148
{
52-
let toplevel_path = Command::new("git")
49+
if let Ok(toplevel_path) = Command::new("git")
5350
.args(["rev-parse", "--show-toplevel"])
5451
.output()
55-
.context("Invoking git rev-parse")?;
56-
if !toplevel_path.status.success() {
57-
anyhow::bail!("Failed to invoke git rev-parse");
52+
{
53+
if toplevel_path.status.success() {
54+
let path = String::from_utf8(toplevel_path.stdout)?;
55+
std::env::set_current_dir(path.trim()).context("Changing to toplevel")?;
56+
}
57+
}
58+
// Otherwise verify we're in the toplevel
59+
if !Utf8Path::new("ADOPTERS.md").try_exists()? {
60+
anyhow::bail!("Not in toplevel")
5861
}
59-
let path = String::from_utf8(toplevel_path.stdout)?;
60-
std::env::set_current_dir(path.trim()).context("Changing to toplevel")?;
6162
}
6263

6364
let task = std::env::args().nth(1);
@@ -393,11 +394,20 @@ fn update_json_schemas(sh: &Shell) -> Result<()> {
393394
Ok(())
394395
}
395396

396-
/// Update generated files using the new sync approach
397+
/// Update all generated files
398+
/// This is the main command developers should use to update generated content.
399+
/// It handles:
400+
/// - Creating new man page templates for new commands
401+
/// - Syncing CLI options to existing man pages
402+
/// - Updating JSON schema files
397403
#[context("Updating generated files")]
398404
fn update_generated(sh: &Shell) -> Result<()> {
405+
// Update man pages (create new templates + sync options)
399406
man::update_manpages(sh)?;
407+
408+
// Update JSON schemas
400409
update_json_schemas(sh)?;
410+
401411
Ok(())
402412
}
403413

0 commit comments

Comments
 (0)