From 1fff70f2a5b7e7b25158e6e37b071cbb42cc867e Mon Sep 17 00:00:00 2001 From: HarshwardhanPatil07 Date: Mon, 28 Sep 2026 12:20:01 +0530 Subject: [PATCH 1/8] man: Fix reference manual rendering Set manual headers from filenames, keep temporary inputs in target/man, and regenerate when version metadata changes. Escape leading apostrophes so roff does not silently drop prose. Add regression tests for headers, apostrophes, and boolean options. Generated-by: AI Signed-off-by: HarshwardhanPatil07 --- crates/xtask/src/man.rs | 116 +++++++++++++++++++++++++++++++--------- 1 file changed, 91 insertions(+), 25 deletions(-) diff --git a/crates/xtask/src/man.rs b/crates/xtask/src/man.rs index b41565c407..90a0307cff 100644 --- a/crates/xtask/src/man.rs +++ b/crates/xtask/src/man.rs @@ -12,6 +12,46 @@ use xshell::{Shell, cmd}; use crate::out_of_sync_error; +fn convert_markdown(sh: &Shell, markdown: &str, output: &Utf8Path) -> Result<()> { + // Temporary and generated files never belong in docs/src. + let mut input = tempfile::NamedTempFile::new_in(output.parent().unwrap())?; + input.write_all(markdown.as_bytes())?; + let input = input.path(); + cmd!(sh, "go-md2man -in {input} -out {output}") + .run() + .with_context(|| format!("Generating {output}"))?; + // A source newline inside inline code can leave a leading apostrophe + // unescaped by go-md2man. Roff treats it as a request and drops the line. + let rendered = fs::read_to_string(output)?; + let escaped = escape_roff_apostrophes(&rendered); + if escaped != rendered { + fs::write(output, escaped)?; + } + Ok(()) +} + +fn escape_roff_apostrophes(content: &str) -> String { + let mut result = String::new(); + for line in content.split_inclusive('\n') { + if line.starts_with('\'') { + result.push_str("\\&"); + } + result.push_str(line); + } + result +} + +fn reference_markdown(content: &str, name: &str, section: u8, version: &str) -> String { + // go-md2man consumes the first H1 as its title, not a body heading. + // Supply it from the filename so NAME remains visible and the header + // identifies the actual manual instead of displaying NAME(). + format!( + "# {} {section}\n\n{}", + name.to_ascii_uppercase(), + content.replace("", version) + ) +} + /// Represents a CLI option extracted from the JSON dump #[derive(Debug, Serialize, Deserialize)] pub struct CliOption { @@ -473,31 +513,9 @@ pub fn generate_man_pages(sh: &Shell) -> Result<()> { // Read markdown content and replace version placeholders let content = fs::read_to_string(&path).with_context(|| format!("Reading {path:?}"))?; - let content_with_version = content.replace("", &version); - - // Check if we need to regenerate by comparing input and output modification times - let should_regenerate = if let (Ok(input_meta), Ok(output_meta)) = - (fs::metadata(&path), fs::metadata(&output_file)) - { - input_meta.modified().unwrap_or(std::time::UNIX_EPOCH) - > output_meta.modified().unwrap_or(std::time::UNIX_EPOCH) - } else { - // If output doesn't exist or we can't get metadata, regenerate - true - }; - - if should_regenerate { - // Create temporary file with version-replaced content - let mut tmpf = tempfile::NamedTempFile::new_in(path.parent().unwrap())?; - tmpf.write_all(content_with_version.as_bytes())?; - let tmpf = tmpf.path(); - - cmd!(sh, "go-md2man -in {tmpf} -out {output_file}") - .run() - .with_context(|| format!("Converting {} to man page", path.display()))?; - - println!("Generated {}", output_file); - } + let markdown = reference_markdown(&content, base_name, section, &version); + convert_markdown(sh, &markdown, &output_file)?; + println!("Generated {}", output_file); } // Apply post-processing fixes for apostrophe handling @@ -781,3 +799,51 @@ fn apply_man_page_fixes(sh: &Shell, dir: &Utf8Path) -> Result<()> { Ok(()) } + +#[cfg(test)] +mod rendering_tests { + use super::*; + + #[test] + fn leading_apostrophe_is_text_not_a_roff_request() { + let input = ".TH BOOTC 8\nDon't change normal prose.\n'wheel' in /etc/group\\fR).\n"; + let expected = ".TH BOOTC 8\nDon't change normal prose.\n\\&'wheel' in /etc/group\\fR).\n"; + assert_eq!(escape_roff_apostrophes(input), expected); + assert_eq!(escape_roff_apostrophes(expected), expected); + } + + #[test] + fn reference_title_and_version_do_not_change_prose() { + let input = "# NAME\n\nbootc - Don't lose apostrophes or `code`\n\n# VERSION\n\n\n"; + for (name, section) in [("bootc", 8), ("bootc-config", 5)] { + let rendered = reference_markdown(input, name, section, "v-test"); + assert_eq!( + rendered, + format!( + "# {} {section}\n\n{}", + name.to_ascii_uppercase(), + input.replace("", "v-test") + ) + ); + assert!(reference_markdown(input, name, section, "v-next").contains("v-next")); + } + } + + #[test] + fn boolean_options_do_not_gain_values() { + let options = [CliOption { + long: "apply".into(), + short: None, + value_name: None, + default: None, + help: "Don't delay".into(), + possible_values: vec!["true".into(), "false".into()], + required: false, + is_boolean: true, + }]; + assert_eq!( + format_options_as_markdown(&options, &[]), + "**--apply**\n\n Don't delay\n\n" + ); + } +} From 8180aad4addecfe55577ed1a04b4208532ff72de Mon Sep 17 00:00:00 2001 From: HarshwardhanPatil07 Date: Mon, 28 Sep 2026 12:21:18 +0530 Subject: [PATCH 2/8] docs: Add canonical documentation coverage checks Map narrative chapters to stable section 7 names and validate one-to-one coverage with SUMMARY.md. Keep existing navigation flat while exposing the previously unlisted command references. Reject missing, duplicate, empty, and unsafe entries before rendering. Generated-by: AI Signed-off-by: HarshwardhanPatil07 --- Cargo.lock | 1 + crates/xtask/Cargo.toml | 2 + crates/xtask/src/man.rs | 12 ++ crates/xtask/src/man/guides.rs | 283 +++++++++++++++++++++++++++++++++ crates/xtask/src/xtask.rs | 3 + docs/manpages.toml | 41 +++++ docs/src/SUMMARY.md | 15 ++ 7 files changed, 357 insertions(+) create mode 100644 crates/xtask/src/man/guides.rs create mode 100644 docs/manpages.toml diff --git a/Cargo.lock b/Cargo.lock index 2986bb9f2f..dc60308de7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4333,6 +4333,7 @@ dependencies = [ "itertools 0.15.0", "mandown", "owo-colors", + "pulldown-cmark", "rand 0.10.3", "serde", "serde_json", diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml index 8f9eb6c43b..5593f31d1d 100644 --- a/crates/xtask/Cargo.toml +++ b/crates/xtask/Cargo.toml @@ -29,6 +29,8 @@ xshell = { workspace = true } # Crate-specific dependencies cargo_metadata = "0.23" mandown = "1.1.0" +# Parse source links without round-tripping Markdown through roff/HTML. +pulldown-cmark = "0.13" rand = "0.10" serde_yaml = "0.9" tar = "0.4" diff --git a/crates/xtask/src/man.rs b/crates/xtask/src/man.rs index 90a0307cff..f31c20d053 100644 --- a/crates/xtask/src/man.rs +++ b/crates/xtask/src/man.rs @@ -12,6 +12,18 @@ use xshell::{Shell, cmd}; use crate::out_of_sync_error; +mod guides; + +/// Validate one-to-one website and installed manual coverage. +pub fn check_docs() -> Result<()> { + let inventory = guides::Inventory::load()?; + println!( + "Validated {} canonical pages for website and manuals", + inventory.pages.len() + ); + Ok(()) +} + fn convert_markdown(sh: &Shell, markdown: &str, output: &Utf8Path) -> Result<()> { // Temporary and generated files never belong in docs/src. let mut input = tempfile::NamedTempFile::new_in(output.parent().unwrap())?; diff --git a/crates/xtask/src/man/guides.rs b/crates/xtask/src/man/guides.rs new file mode 100644 index 0000000000..8e9929c6ae --- /dev/null +++ b/crates/xtask/src/man/guides.rs @@ -0,0 +1,283 @@ +//! Publish canonical mdBook chapters as manuals, without generating website copies. + +use anyhow::{Context, Result, ensure}; +use camino::{Utf8Path, Utf8PathBuf}; +use pulldown_cmark::{Event, Parser, Tag, TagEnd}; +use serde::Deserialize; +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, +}; + +const DOCS_SRC: &str = "docs/src"; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Manifest { + guides: BTreeMap, +} + +pub(super) struct Page { + pub name: String, + pub section: u8, +} + +impl Page { + pub fn filename(&self) -> String { + format!("{}.{}", self.name, self.section) + } +} + +pub(super) struct Inventory { + pub pages: Vec, +} + +fn sources_in( + root: &Utf8Path, + dir: &Utf8Path, + sources: &mut BTreeMap, +) -> Result<()> { + for entry in fs::read_dir(dir).with_context(|| format!("Reading {dir}"))? { + let path = Utf8PathBuf::from_path_buf(entry?.path()) + .map_err(|_| anyhow::anyhow!("Non-UTF-8 documentation path"))?; + if path.is_dir() { + sources_in(root, &path, sources)?; + } else if path.extension() == Some("md") { + let source = path.strip_prefix(root)?.to_string(); + let content = fs::read_to_string(&path).with_context(|| format!("Reading {path}"))?; + sources.insert(source, content); + } + } + Ok(()) +} + +/// Navigation is shared by the website and the offline index, including order. +fn chapters(summary: &str) -> Vec<(String, String)> { + let mut group = String::new(); + let mut in_heading = false; + let mut result = Vec::new(); + for event in Parser::new(summary) { + match event { + Event::Start(Tag::Heading { .. }) => { + group.clear(); + in_heading = true; + } + Event::End(TagEnd::Heading(_)) => in_heading = false, + Event::Text(text) | Event::Code(text) if in_heading => group.push_str(&text), + Event::Start(Tag::Link { dest_url, .. }) => { + result.push((group.clone(), dest_url.into_string())); + } + _ => (), + } + } + result +} + +fn title_and_body(content: &str) -> Result<(&str, &str)> { + let (heading, body) = content + .trim_start() + .split_once('\n') + .context("Page has no body")?; + let title = heading + .strip_prefix("# ") + .context("Page must start with an H1 heading")? + .trim(); + ensure!( + !title.is_empty() && !body.trim().is_empty(), + "Empty documentation page" + ); + Ok((title, body.trim_start_matches('\n'))) +} + +impl Inventory { + pub fn load() -> Result { + let manifest = fs::read_to_string("docs/manpages.toml")?; + let root = Utf8Path::new(DOCS_SRC); + let mut sources = BTreeMap::new(); + sources_in(root, root, &mut sources)?; + Self::from_sources(&manifest, sources) + } + + fn from_sources(manifest: &str, mut sources: BTreeMap) -> Result { + let manifest: Manifest = toml::from_str(manifest).context("Parsing docs/manpages.toml")?; + let summary = sources.remove("SUMMARY.md").context("Missing SUMMARY.md")?; + // This historical, unlinked placeholder is not documentation. If it acquires + // content, require it to participate in coverage like every other chapter. + if sources + .get("related.md") + .is_some_and(|s| s.trim() == "# Related projects") + { + sources.remove("related.md"); + } + let mut seen = BTreeSet::new(); + let mut outputs = BTreeSet::from(["bootc-docs.7".to_string()]); + let mut pages = Vec::new(); + let mut mapped = BTreeSet::new(); + for (_group, source) in chapters(&summary) { + ensure!( + seen.insert(source.clone()), + "Duplicate navigation entry: {source}" + ); + let content = sources + .get(&source) + .with_context(|| format!("Missing chapter: {source}"))?; + title_and_body(content).with_context(|| format!("Invalid chapter: {source}"))?; + let (name, section) = if let Some(native) = source.strip_prefix("man/") { + let (name, section) = native + .strip_suffix(".md") + .and_then(|s| s.rsplit_once('.')) + .context("Invalid native manual filename")?; + let section: u8 = section.parse()?; + ensure!( + name.contains("bootc") + && name.chars().all(|c| c.is_ascii_lowercase() + || c.is_ascii_digit() + || matches!(c, '-' | '.')), + "Manual name must be safe and match the RPM bootc file pattern: {source}" + ); + ensure!( + matches!(section, 5 | 8), + "Native manuals must use section 5 or 8: {source}" + ); + (name.to_string(), section) + } else { + let name = manifest + .guides + .get(&source) + .with_context(|| format!("Missing man page mapping: {source}"))?; + ensure!( + name.starts_with("bootc-") + && name + .chars() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'), + "Invalid guide manual name: {name}" + ); + mapped.insert(source.clone()); + (name.clone(), 7) + }; + let page = Page { name, section }; + ensure!( + outputs.insert(page.filename()), + "Duplicate manual output: {}", + page.filename() + ); + pages.push(page); + } + let missing: Vec<_> = sources.keys().filter(|p| !seen.contains(*p)).collect(); + ensure!( + missing.is_empty(), + "Chapters missing from SUMMARY.md: {missing:?}" + ); + let extra: Vec<_> = manifest + .guides + .keys() + .filter(|p| !mapped.contains(*p)) + .collect(); + ensure!(extra.is_empty(), "Unused guide mappings: {extra:?}"); + Ok(Self { pages }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const MANIFEST: &str = "[guides]\n\"intro.md\" = \"bootc-overview\"\n"; + + fn sources() -> BTreeMap { + BTreeMap::from([ + ("SUMMARY.md".into(), "# Guides\n\n- [Introduction](intro.md)\n\n# Commands\n\n- [bootc](man/bootc.8.md)\n".into()), + ("intro.md".into(), "# Introduction\n\nA guide with **formatting**.\n".into()), + ("man/bootc.8.md".into(), "# NAME\n\nbootc - Bootable containers\n".into()), + ]) + } + + #[test] + fn coverage_and_order() { + let inventory = Inventory::from_sources(MANIFEST, sources()).unwrap(); + assert_eq!( + inventory + .pages + .iter() + .map(Page::filename) + .collect::>(), + ["bootc-overview.7", "bootc.8"] + ); + } + + #[test] + fn coverage_rejects_omissions_duplicates_and_empty_pages() { + for (path, content, error) in [ + ( + "unlisted.md", + "# Missing\n\nContent\n", + "missing from SUMMARY", + ), + ("intro.md", "# Empty\n", "Invalid chapter"), + ("SUMMARY.md", "- [Missing](missing.md)\n", "Missing chapter"), + ( + "SUMMARY.md", + "- [Intro](intro.md)\n- [Again](intro.md)\n", + "Duplicate navigation", + ), + ( + "SUMMARY.md", + "- [Intro](intro.md)\n", + "missing from SUMMARY", + ), + ] { + let mut sources = sources(); + sources.insert(path.into(), content.into()); + let result = Inventory::from_sources(MANIFEST, sources); + let message = result.err().unwrap().to_string(); + assert!(message.contains(error), "{path}: {message}"); + } + } + + #[test] + fn manifest_rejects_missing_extra_unsafe_and_reserved_names() { + for manifest in [ + "[guides]\n", + "[guides]\n\"intro.md\"=\"bootc-overview\"\n\"missing.md\"=\"bootc-missing\"\n", + "[guides]\n\"intro.md\"=\"../bad\"\n", + "[guides]\n\"intro.md\"=\"bootc-docs\"\n", + ] { + assert!( + Inventory::from_sources(manifest, sources()).is_err(), + "{manifest}" + ); + } + let mut sources = sources(); + sources.insert("second.md".into(), "# Second\n\nContent\n".into()); + sources + .get_mut("SUMMARY.md") + .unwrap() + .push_str("- [Second](second.md)\n"); + assert!( + Inventory::from_sources( + &format!("{MANIFEST}\"second.md\"=\"bootc-overview\"\n"), + sources + ) + .is_err() + ); + } + + #[test] + fn placeholder_is_exempt_only_while_empty() { + let mut sources = sources(); + sources.insert("related.md".into(), "# Related projects\n".into()); + assert!(Inventory::from_sources(MANIFEST, sources.clone()).is_ok()); + sources + .get_mut("related.md") + .unwrap() + .push_str("\nNow it has content.\n"); + assert!(Inventory::from_sources(MANIFEST, sources).is_err()); + } + + #[test] + fn title_keeps_body_and_subheadings() { + let (title, body) = title_and_body("# A guide\n\nContent.\n\n## Subtopic\n").unwrap(); + assert_eq!(title, "A guide"); + assert_eq!(body, "Content.\n\n## Subtopic\n"); + } +} diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index 5a55b45ea7..ad6d235f65 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -70,6 +70,8 @@ struct Cli { enum Commands { /// Generate man pages Manpages, + /// Check website and installed manual coverage without building the bootc CLI + CheckDocs, /// Update or check generated files UpdateGenerated { #[command(subcommand)] @@ -368,6 +370,7 @@ fn try_main() -> Result<()> { match cli.command { Commands::Manpages => man::generate_man_pages(&sh), + Commands::CheckDocs => man::check_docs(), Commands::UpdateGenerated { command } => match command { UpdateGeneratedCommands::Direct { check } => { if check { diff --git a/docs/manpages.toml b/docs/manpages.toml new file mode 100644 index 0000000000..8c69911f8e --- /dev/null +++ b/docs/manpages.toml @@ -0,0 +1,41 @@ +# Canonical chapter -> installed section 7 manual name. +# Ordering and grouping come only from src/SUMMARY.md; prose stays in src/. +[guides] +"boot-failure-detection.md" = "bootc-boot-failure-detection" +"bootc-images.md" = "bootc-compatible-images" +"bootc-in-container.md" = "bootc-in-container" +"bootc-install.md" = "bootc-installation" +"bootc-via-api.md" = "bootc-api" +"booting-local-builds.md" = "bootc-local-builds" +"bootloaders.md" = "bootc-bootloaders" +"building/bootc-runtime.md" = "bootc-container-runtime" +"building/dns.md" = "bootc-dns" +"building/guidance.md" = "bootc-building-images" +"building/kernel-arguments.md" = "bootc-kernel-arguments" +"building/management-services.md" = "bootc-management-services" +"building/secrets.md" = "bootc-secrets" +"building/users-and-groups.md" = "bootc-users-and-groups" +"experimental-bootc-image.md" = "bootc-experimental-image" +"experimental-composefs.md" = "bootc-experimental-composefs" +"experimental-container-export.md" = "bootc-experimental-container-export" +"experimental-fsck.md" = "bootc-experimental-fsck" +"experimental-install-reset.md" = "bootc-experimental-install-reset" +"experimental-progress-fd.md" = "bootc-experimental-progress-fd" +"experimental-unified-storage.md" = "bootc-experimental-unified-storage" +"filesystem-encryption.md" = "bootc-filesystem-encryption" +"filesystem-storage.md" = "bootc-container-storage" +"filesystem-sysroot.md" = "bootc-sysroot" +"filesystem.md" = "bootc-filesystem" +"initramfs.md" = "bootc-initramfs" +"installation.md" = "bootc-base-images" +"internals.md" = "bootc-internals" +"intro.md" = "bootc-overview" +"logically-bound-images.md" = "bootc-logically-bound-images" +"package-managers.md" = "bootc-package-managers" +"packaging-and-integration.md" = "bootc-packaging-and-integration" +"registries-and-offline.md" = "bootc-registries-and-offline" +"relationship-oci-artifacts.md" = "bootc-oci-artifacts" +"relationship-particles.md" = "bootc-systemd-particles" +"relationships.md" = "bootc-relationships" +"security.md" = "bootc-security" +"upgrades.md" = "bootc-upgrades" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 164f695c37..7fcd8f9eb0 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -12,6 +12,7 @@ - [Container runtime vs bootc runtime](building/bootc-runtime.md) - [DNS and resolv.conf](building/dns.md) - [Users, groups, SSH keys](building/users-and-groups.md) +- [`man bootc-sysusers-shadow-sync.service`](man/bootc-sysusers-shadow-sync.service.5.md) - [Kernel arguments](building/kernel-arguments.md) - [Secrets](building/secrets.md) - [Management Services](building/management-services.md) @@ -25,6 +26,9 @@ - [Booting local builds](booting-local-builds.md) - [Managing the initramfs after installation](initramfs.md) - [`man bootc`](man/bootc.8.md) +- [`man bootc-config`](man/bootc-config.5.md) +- [`man bootc-config-diff`](man/bootc-config-diff.8.md) +- [`man bootc-edit`](man/bootc-edit.8.md) - [`man bootc-status`](man/bootc-status.8.md) - [`man bootc-upgrade`](man/bootc-upgrade.8.md) - [`man bootc-switch`](man/bootc-switch.8.md) @@ -44,11 +48,19 @@ - [`man bootc-install-to-filesystem`](man/bootc-install-to-filesystem.8.md) - [`man bootc-install-to-existing-root`](man/bootc-install-to-existing-root.8.md) - [`man bootc-install-mount`](man/bootc-install-mount.8.md) +- [`man bootc-install-finalize`](man/bootc-install-finalize.8.md) +- [`man bootc-install-ensure-completion`](man/bootc-install-ensure-completion.8.md) +- [`man bootc-install-print-configuration`](man/bootc-install-print-configuration.8.md) +- [`man system-reinstall-bootc`](man/system-reinstall-bootc.8.md) - [`man bootc-destructive-cleanup.service`](man/bootc-destructive-cleanup.service.5.md) # Bootc usage in containers - [Read-only when in a default container](bootc-in-container.md) +- [`man bootc-container`](man/bootc-container.8.md) +- [`man bootc-container-inspect`](man/bootc-container-inspect.8.md) +- [`man bootc-container-split-kernel-and-rootfs`](man/bootc-container-split-kernel-and-rootfs.8.md) +- [`man bootc-container-ukify`](man/bootc-container-ukify.8.md) - [`man bootc-container-lint`](man/bootc-container-lint.8.md) # Architecture @@ -58,6 +70,8 @@ - [Filesystem: sysroot](filesystem-sysroot.md) - [Container storage](filesystem-storage.md) - [Bootloader](bootloaders.md) +- [`man bootc-loader-entries`](man/bootc-loader-entries.8.md) +- [`man bootc-loader-entries-set-options-for-source`](man/bootc-loader-entries-set-options-for-source.8.md) - [Disk encryption (e.g. LUKS)](filesystem-encryption.md) # Security @@ -68,6 +82,7 @@ - [bootc image](experimental-bootc-image.md) - [composefs backend](experimental-composefs.md) +- [`man bootc-composefs-finalize-staged`](man/bootc-composefs-finalize-staged.8.md) - [unified storage](experimental-unified-storage.md) - [`man bootc-root-setup.service`](man/bootc-root-setup.service.5.md) - [`man bootc-setup-root-conf.toml`](man/bootc-setup-root-conf.5.md) From efc51a986660a3ddf41ad45477e0f0e4121c080f Mon Sep 17 00:00:00 2001 From: HarshwardhanPatil07 Date: Mon, 28 Sep 2026 12:21:48 +0530 Subject: [PATCH 3/8] man: Resolve documentation links to manual references Use parsed Markdown spans to preserve formatting and code examples while mapping chapter links to installed manuals. Keep external links intact and resolve generated web artifacts against bootc.dev. Reject missing or escaping local documentation links. Generated-by: AI Signed-off-by: HarshwardhanPatil07 --- crates/xtask/src/man/guides.rs | 155 ++++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 3 deletions(-) diff --git a/crates/xtask/src/man/guides.rs b/crates/xtask/src/man/guides.rs index 8e9929c6ae..19217a1f85 100644 --- a/crates/xtask/src/man/guides.rs +++ b/crates/xtask/src/man/guides.rs @@ -2,14 +2,16 @@ use anyhow::{Context, Result, ensure}; use camino::{Utf8Path, Utf8PathBuf}; -use pulldown_cmark::{Event, Parser, Tag, TagEnd}; +use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd}; use serde::Deserialize; use std::{ collections::{BTreeMap, BTreeSet}, fs, + ops::Range, }; const DOCS_SRC: &str = "docs/src"; +const BOOK_URL: &str = "https://bootc.dev/bootc/"; #[derive(Deserialize)] #[serde(deny_unknown_fields)] @@ -18,6 +20,7 @@ struct Manifest { } pub(super) struct Page { + pub source: String, pub name: String, pub section: u8, } @@ -26,10 +29,15 @@ impl Page { pub fn filename(&self) -> String { format!("{}.{}", self.name, self.section) } + + fn reference(&self) -> String { + format!("**{}**({})", self.name, self.section) + } } pub(super) struct Inventory { pub pages: Vec, + names: BTreeMap, } fn sources_in( @@ -155,7 +163,11 @@ impl Inventory { mapped.insert(source.clone()); (name.clone(), 7) }; - let page = Page { name, section }; + let page = Page { + source, + name, + section, + }; ensure!( outputs.insert(page.filename()), "Duplicate manual output: {}", @@ -174,10 +186,94 @@ impl Inventory { .filter(|p| !mapped.contains(*p)) .collect(); ensure!(extra.is_empty(), "Unused guide mappings: {extra:?}"); - Ok(Self { pages }) + let names = pages + .iter() + .map(|p| (p.source.clone(), p.reference())) + .collect(); + let result = Self { pages, names }; + // Validate internal Markdown links even in website-only builds; + // mdBook otherwise tolerates missing pages. + for page in &result.pages { + let content = &sources[&page.source]; + result.rewrite_links(content, &page.source)?; + } + Ok(result) + } + + /// Change only link spans, not the surrounding Markdown. A Markdown parser + /// handles multiline/reference links and avoids links in inline/fenced code. + pub fn rewrite_links(&self, body: &str, source: &str) -> Result<(String, BTreeSet)> { + let mut replacements = Vec::new(); + let mut references = BTreeSet::new(); + let mut link: Option<(String, Range, Option>)> = None; + for (event, span) in Parser::new_ext(body, Options::ENABLE_TABLES).into_offset_iter() { + match event { + Event::Start(Tag::Link { dest_url, .. }) => { + link = Some((dest_url.into_string(), span, None)); + } + Event::End(TagEnd::Link) => { + let (destination, whole, label) = + link.take().context("Unmatched Markdown link")?; + let label = label.map(|r| &body[r]).unwrap_or(""); + let replacement = if destination.starts_with('#') { + Some(label.to_string()) + } else if destination.contains(':') || destination.starts_with('/') { + None + } else { + let split = destination.find(['#', '?']).unwrap_or(destination.len()); + let (path, suffix) = destination.split_at(split); + let path = normalize_link_path(source, path)?; + if let Some(reference) = self.names.get(&path) { + references.insert(reference.clone()); + Some(format!("{label} (see {reference})")) + } else if path.ends_with(".md") { + anyhow::bail!("Unmapped local link in {source}: {destination}"); + } else { + // Generated rustdoc and schemas are online artifacts, + // not narrative Markdown chapters or offline manuals. + Some(format!("[{label}]({BOOK_URL}{path}{suffix})")) + } + }; + if let Some(replacement) = replacement { + replacements.push((whole, replacement)); + } + } + _ => { + if let Some((_, _, label)) = &mut link { + if let Some(label) = label { + label.start = label.start.min(span.start); + label.end = label.end.max(span.end); + } else { + *label = Some(span); + } + } + } + } + } + let mut output = body.to_string(); + for (span, replacement) in replacements.into_iter().rev() { + output.replace_range(span, &replacement); + } + Ok((output, references)) } } +fn normalize_link_path(source: &str, destination: &str) -> Result { + let mut parts: Vec<&str> = source.split('/').collect(); + parts.pop(); + for part in destination.split('/') { + match part { + "" | "." => (), + ".." => { + ensure!(!parts.is_empty(), "Link escapes docs/src: {destination}"); + parts.pop(); + } + _ => parts.push(part), + } + } + Ok(parts.join("/")) +} + #[cfg(test)] mod tests { use super::*; @@ -274,6 +370,59 @@ mod tests { assert!(Inventory::from_sources(MANIFEST, sources).is_err()); } + #[test] + fn parsed_links_preserve_formatting_and_code() { + let inventory = Inventory::from_sources(MANIFEST, sources()).unwrap(); + for (input, expected) in [ + ( + "[guide](../intro.md#details)", + "guide (see **bootc-overview**(7))", + ), + ( + "[**guide**\nlabel](../intro.md)", + "**guide**\nlabel (see **bootc-overview**(7))", + ), + ( + "[guide][g]\n\n[g]: ../intro.md\n", + "guide (see **bootc-overview**(7))\n\n[g]: ../intro.md\n", + ), + ("[`bootc`](bootc.8.md)", "`bootc` (see **bootc**(8))"), + ("[local](#details)", "local"), + ( + "[web](https://example.com/a(b))", + "[web](https://example.com/a(b))", + ), + ( + "[email](mailto:bootc@example.com)", + "[email](mailto:bootc@example.com)", + ), + ( + "[api](../internals/api.html#section)", + "[api](https://bootc.dev/bootc/internals/api.html#section)", + ), + ("`[code](../missing.md)`", "`[code](../missing.md)`"), + ( + "~~~~md\n[code](../missing.md)\n~~~~\n", + "~~~~md\n[code](../missing.md)\n~~~~\n", + ), + ] { + let (actual, _) = inventory.rewrite_links(input, "man/bootc.8.md").unwrap(); + assert_eq!(actual, expected, "{input}"); + } + let (_, refs) = inventory + .rewrite_links("[guide](../intro.md)", "man/bootc.8.md") + .unwrap(); + assert_eq!(refs, BTreeSet::from(["**bootc-overview**(7)".into()])); + } + + #[test] + fn missing_or_escaping_doc_links_fail() { + let inventory = Inventory::from_sources(MANIFEST, sources()).unwrap(); + for link in ["[bad](missing.md)", "[bad](../../intro.md)"] { + assert!(inventory.rewrite_links(link, "intro.md").is_err()); + } + } + #[test] fn title_keeps_body_and_subheadings() { let (title, body) = title_and_body("# A guide\n\nContent.\n\n## Subtopic\n").unwrap(); From d3d7da4fc81b0011df2fd2ea8804146efcc11d5d Mon Sep 17 00:00:00 2001 From: HarshwardhanPatil07 Date: Mon, 28 Sep 2026 12:22:23 +0530 Subject: [PATCH 4/8] man: Adapt diagrams and tables for terminal output Convert supported Mermaid flowcharts and two-column guide tables using parsed Markdown spans, leaving fenced examples untouched. Reject unsupported diagram syntax rather than silently losing it. Wrap long command examples for terminal reading. Generated-by: AI Signed-off-by: HarshwardhanPatil07 --- crates/xtask/src/man/guides.rs | 134 +++++++++++++++++++++++ docs/src/bootc-install.md | 20 +++- docs/src/building/management-services.md | 4 +- docs/src/building/secrets.md | 3 +- docs/src/logically-bound-images.md | 6 +- docs/src/package-managers.md | 7 +- 6 files changed, 160 insertions(+), 14 deletions(-) diff --git a/crates/xtask/src/man/guides.rs b/crates/xtask/src/man/guides.rs index 19217a1f85..61e738e800 100644 --- a/crates/xtask/src/man/guides.rs +++ b/crates/xtask/src/man/guides.rs @@ -196,6 +196,9 @@ impl Inventory { for page in &result.pages { let content = &sources[&page.source]; result.rewrite_links(content, &page.source)?; + if page.section == 7 { + adapt_guide_markdown_for_man(content)?; + } } Ok(result) } @@ -274,6 +277,112 @@ fn normalize_link_path(source: &str, destination: &str) -> Result { Ok(parts.join("/")) } +// Use parsed spans so fences inside examples remain examples, and equivalent +// Markdown spellings (tilde fences, aligned tables) receive the same treatment. +fn adapt_guide_markdown_for_man(body: &str) -> Result { + let mut events = Parser::new_ext(body, Options::ENABLE_TABLES).into_offset_iter(); + let mut replacements = Vec::new(); + while let Some((event, span)) = events.next() { + match event { + Event::Start(Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Fenced(info))) + if info.split_whitespace().next() == Some("mermaid") => + { + let raw = &body[span.clone()]; + let opening = raw.lines().next().context("Missing Mermaid fence")?.trim(); + let marker = opening.chars().next().context("Empty Mermaid fence")?; + let width = opening.chars().take_while(|c| *c == marker).count(); + let closing = raw.lines().last().unwrap_or("").trim(); + ensure!( + closing.chars().count() >= width && closing.chars().all(|c| c == marker), + "Unclosed Mermaid code fence" + ); + let mut diagram = String::new(); + for (event, _) in events.by_ref() { + match event { + Event::Text(text) => diagram.push_str(&text), + Event::End(TagEnd::CodeBlock) => break, + _ => (), + } + } + replacements.push((span, flowchart_as_text(&diagram)?)); + } + Event::Start(Tag::Table(columns)) => { + ensure!( + columns.len() == 2, + "Only two-column guide tables have a terminal representation" + ); + let mut rows = Vec::new(); + let mut row = Vec::new(); + for (event, cell) in events.by_ref() { + match event { + Event::Start(Tag::TableCell) => row.push(body[cell].trim().to_string()), + Event::End(TagEnd::TableHead | TagEnd::TableRow) => { + ensure!(row.len() == 2, "Malformed guide table row"); + rows.push(std::mem::take(&mut row)); + } + Event::End(TagEnd::Table) => break, + _ => (), + } + } + let headers = rows.first().context("Missing table header")?; + let mut text = String::new(); + for row in rows.iter().skip(1) { + text.push_str(&format!( + "- {}: {}; {}: {}\n", + headers[0], row[0], headers[1], row[1] + )); + } + text.push('\n'); + replacements.push((span, text)); + } + _ => (), + } + } + let mut result = body.to_string(); + for (span, replacement) in replacements.into_iter().rev() { + result.replace_range(span, &replacement); + } + Ok(result) +} + +fn flowchart_as_text(diagram: &str) -> Result { + let mut lines = diagram + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()); + ensure!( + lines + .next() + .is_some_and(|line| line.starts_with("flowchart ")), + "Only Mermaid flowcharts have a terminal representation" + ); + let mut text = String::from("Connected components:\n\n"); + for edge in lines { + let nodes: Vec<_> = edge + .split("---") + .map(|node| { + let node = node.trim(); + if let Some((_, label)) = node.split_once("[\"") { + label.strip_suffix("\"]").map(str::to_string) + } else if !node.is_empty() + && node + .chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '/')) + { + Some(node.to_string()) + } else { + None + } + }) + .collect::>() + .context("Unsupported Mermaid node syntax")?; + ensure!(nodes.len() >= 2, "Unsupported Mermaid edge: {edge}"); + text.push_str(&format!("- {}\n", nodes.join(" — "))); + } + text.push('\n'); + Ok(text) +} + #[cfg(test)] mod tests { use super::*; @@ -429,4 +538,29 @@ mod tests { assert_eq!(title, "A guide"); assert_eq!(body, "Content.\n\n## Subtopic\n"); } + #[test] + fn diagrams_and_tables_have_terminal_text() { + let input = "```mermaid\nflowchart TD\nbootc --- image[\"containers/storage\"]\n```\n\n| Mode | Method |\n|---|---|\n| `to-disk` | UUID |\n"; + let output = adapt_guide_markdown_for_man(input).unwrap(); + assert!(output.contains("bootc — containers/storage")); + assert!(output.contains("Mode: `to-disk`; Method: UUID")); + assert!(!output.contains("```mermaid")); + assert_eq!( + adapt_guide_markdown_for_man(&input.replace("```", "~~~~")).unwrap(), + output + ); + assert_eq!( + adapt_guide_markdown_for_man(&input.replace("|---|---|", "| :--- | ---: |")).unwrap(), + output + ); + let example = "````markdown\n```mermaid\nnot a real diagram\n```\n````\n"; + assert_eq!(adapt_guide_markdown_for_man(example).unwrap(), example); + for bad in [ + "```mermaid\nsequenceDiagram\nA->>B: hello\n```\n", + "```mermaid\nflowchart TD\nA --> B\n```\n", + "```mermaid\nflowchart TD\nA --- B\n", + ] { + assert!(adapt_guide_markdown_for_man(bad).is_err(), "{bad}"); + } + } } diff --git a/docs/src/bootc-install.md b/docs/src/bootc-install.md index 88a25f30ef..3dcffecbd5 100644 --- a/docs/src/bootc-install.md +++ b/docs/src/bootc-install.md @@ -60,7 +60,10 @@ to an existing system and install your container image. Failure to run Here's an example of using `bootc install` (root/elevated permission required): ```bash -podman run --rm --privileged --pid=host --ipc=host -v /var/lib/containers:/var/lib/containers -v /dev:/dev --security-opt label=type:unconfined_t bootc install to-disk /path/to/disk +podman run --rm --privileged --pid=host --ipc=host \ + -v /var/lib/containers:/var/lib/containers -v /dev:/dev \ + --security-opt label=type:unconfined_t \ + bootc install to-disk /path/to/disk ``` Note that while `--privileged` is used, this command will not perform any @@ -200,7 +203,11 @@ process, you can create a raw disk image that you can boot via virtualization. R ```bash truncate -s 10G myimage.raw -podman run --rm --privileged --pid=host --ipc=host --security-opt label=type:unconfined_t -v /dev:/dev -v /var/lib/containers:/var/lib/containers -v .:/output bootc install to-disk --generic-image --via-loopback /output/myimage.raw +podman run --rm --privileged --pid=host --ipc=host \ + --security-opt label=type:unconfined_t -v /dev:/dev \ + -v /var/lib/containers:/var/lib/containers -v .:/output \ + bootc install to-disk --generic-image \ + --via-loopback /output/myimage.raw ``` Notice that we use `--generic-image` for this use case. @@ -220,10 +227,11 @@ support the root storage setup already initialized. The core command should look like this (root/elevated permission required): ```bash -podman run --rm --privileged -v /dev:/dev -v /var/lib/containers:/var/lib/containers -v /:/target \ - --pid=host --security-opt label=type:unconfined_t \ - \ - bootc install to-existing-root +podman run --rm --privileged -v /dev:/dev \ + -v /var/lib/containers:/var/lib/containers -v /:/target \ + --pid=host --security-opt label=type:unconfined_t \ + \ + bootc install to-existing-root ``` It is assumed in this command that the target rootfs is passed via `-v /:/target` at this time. diff --git a/docs/src/building/management-services.md b/docs/src/building/management-services.md index 1740c8bc35..3e3d365b21 100644 --- a/docs/src/building/management-services.md +++ b/docs/src/building/management-services.md @@ -39,7 +39,8 @@ WantedBy=multi-user.target EOT # Link the service to run at startup -RUN ln -s /usr/lib/systemd/system/management-client.service /usr/lib/systemd/system/multi-user.target.wants/management-client.service +RUN ln -s /usr/lib/systemd/system/management-client.service \ + /usr/lib/systemd/system/multi-user.target.wants/management-client.service # Store the credentials in a file to be used by the systemd service RUN echo -e "CLIENT_ACTIVATION_KEY=${activation_key}" > /etc/management-client/.credentials @@ -48,4 +49,3 @@ RUN echo -e "CLIENT_ACTIVATION_KEY=${activation_key}" > /etc/management-client/. # The systemd service will remove this file after the registration completes the first time RUN touch /etc/management-client/.run_next_boot ``` - diff --git a/docs/src/building/secrets.md b/docs/src/building/secrets.md index 7bf100e28a..b220d5100f 100644 --- a/docs/src/building/secrets.md +++ b/docs/src/building/secrets.md @@ -31,7 +31,8 @@ you can pass `--authfile` to set the bootc authfile explicitly; for example ```bash -echo | podman login --authfile /run/ostree/auth.json -u someuser --password-stdin +echo | podman login \ + --authfile /run/ostree/auth.json -u someuser --password-stdin ``` This pattern of using the ephemeral location in `/run` can work diff --git a/docs/src/logically-bound-images.md b/docs/src/logically-bound-images.md index 6be695aef3..4ddb3cd305 100644 --- a/docs/src/logically-bound-images.md +++ b/docs/src/logically-bound-images.md @@ -34,8 +34,10 @@ FROM quay.io/myorg/myimage:latest COPY ./my-app.image /usr/share/containers/systemd/my-app.image COPY ./another-app.container /usr/share/containers/systemd/another-app.container -RUN ln -s /usr/share/containers/systemd/my-app.image /usr/lib/bootc/bound-images.d/my-app.image && \ - ln -s /usr/share/containers/systemd/another-app.container /usr/lib/bootc/bound-images.d/another-app.container +RUN ln -s /usr/share/containers/systemd/my-app.image \ + /usr/lib/bootc/bound-images.d/my-app.image && \ + ln -s /usr/share/containers/systemd/another-app.container \ + /usr/lib/bootc/bound-images.d/another-app.container ``` In the `.container` definition, you should use: diff --git a/docs/src/package-managers.md b/docs/src/package-managers.md index 03bc979d6e..98639d37fd 100644 --- a/docs/src/package-managers.md +++ b/docs/src/package-managers.md @@ -70,7 +70,8 @@ Error: Transaction test error: ``` ``` -$ podman run --read-only --rm --tmpfs /var -ti debian /bin/sh -c 'apt update && apt -y install strace' +$ podman run --read-only --rm --tmpfs /var -ti debian \ + /bin/sh -c 'apt update && apt -y install strace' ... dpkg: error processing archive /var/cache/apt/archives/libunwind8_1.6.2-3_amd64.deb (--unpack): unable to clean up mess surrounding './usr/lib/x86_64-linux-gnu/libunwind-coredump.so.0.0.0' before installing another version: Read-only file system @@ -79,7 +80,8 @@ dpkg: error processing archive /var/cache/apt/archives/libunwind8_1.6.2-3_amd64. These errors message are misleading and confusing for the user. A more useful error may look like e.g.: ``` -$ podman run --read-only --rm --tmpfs /var -ti debian /bin/sh -c 'apt update && apt -y install strace' +$ podman run --read-only --rm --tmpfs /var -ti debian \ + /bin/sh -c 'apt update && apt -y install strace' error: read-only /usr detected, refusing to operate. See `man apt-image-based` for more information. ``` @@ -108,4 +110,3 @@ in the ostree repo. This section will be expanded later; you may also be able to find more information in [booting local builds](booting-local-builds.md). - From 462a8b599f1bd8e6a163ee672319f625fd10a2df Mon Sep 17 00:00:00 2001 From: HarshwardhanPatil07 Date: Mon, 28 Sep 2026 12:23:17 +0530 Subject: [PATCH 5/8] man: Generate guide manuals and the offline index Render canonical narrative chapters into section 7 without creating Markdown copies. Generate bootc-docs(7) in website navigation order, include manual cross-references and version metadata, and remove stale generated guide output. Generated-by: AI Signed-off-by: HarshwardhanPatil07 --- crates/xtask/src/man.rs | 12 ++++ crates/xtask/src/man/guides.rs | 115 ++++++++++++++++++++++++++++++++- crates/xtask/src/xtask.rs | 3 + 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/crates/xtask/src/man.rs b/crates/xtask/src/man.rs index f31c20d053..22468ba924 100644 --- a/crates/xtask/src/man.rs +++ b/crates/xtask/src/man.rs @@ -24,6 +24,15 @@ pub fn check_docs() -> Result<()> { Ok(()) } +/// Render narrative guides without rebuilding the bootc CLI. +pub fn generate_guide_man_pages(sh: &Shell) -> Result<()> { + let inventory = guides::Inventory::load()?; + let output = Utf8Path::new("target/man"); + sh.create_dir(output)?; + inventory.generate_guides(sh, output, &get_package_version()?)?; + apply_man_page_fixes(sh, output) +} + fn convert_markdown(sh: &Shell, markdown: &str, output: &Utf8Path) -> Result<()> { // Temporary and generated files never belong in docs/src. let mut input = tempfile::NamedTempFile::new_in(output.parent().unwrap())?; @@ -485,6 +494,7 @@ pub fn sync_all_man_pages(sh: &Shell) -> Result<()> { /// Generate man pages from hand-written markdown sources #[context("Generating manpages")] pub fn generate_man_pages(sh: &Shell) -> Result<()> { + let inventory = guides::Inventory::load()?; let man_src_dir = Utf8Path::new("docs/src/man"); let man_output_dir = Utf8Path::new("target/man"); @@ -530,6 +540,8 @@ pub fn generate_man_pages(sh: &Shell) -> Result<()> { println!("Generated {}", output_file); } + inventory.generate_guides(sh, man_output_dir, &version)?; + // Apply post-processing fixes for apostrophe handling apply_man_page_fixes(sh, man_output_dir)?; diff --git a/crates/xtask/src/man/guides.rs b/crates/xtask/src/man/guides.rs index 61e738e800..67dadcefb1 100644 --- a/crates/xtask/src/man/guides.rs +++ b/crates/xtask/src/man/guides.rs @@ -9,6 +9,7 @@ use std::{ fs, ops::Range, }; +use xshell::Shell; const DOCS_SRC: &str = "docs/src"; const BOOK_URL: &str = "https://bootc.dev/bootc/"; @@ -23,6 +24,7 @@ pub(super) struct Page { pub source: String, pub name: String, pub section: u8, + group: String, } impl Page { @@ -121,7 +123,7 @@ impl Inventory { let mut outputs = BTreeSet::from(["bootc-docs.7".to_string()]); let mut pages = Vec::new(); let mut mapped = BTreeSet::new(); - for (_group, source) in chapters(&summary) { + for (group, source) in chapters(&summary) { ensure!( seen.insert(source.clone()), "Duplicate navigation entry: {source}" @@ -167,6 +169,7 @@ impl Inventory { source, name, section, + group, }; ensure!( outputs.insert(page.filename()), @@ -191,8 +194,8 @@ impl Inventory { .map(|p| (p.source.clone(), p.reference())) .collect(); let result = Self { pages, names }; - // Validate internal Markdown links even in website-only builds; - // mdBook otherwise tolerates missing pages. + // Validate all internal Markdown links and terminal-only transformations + // even in website-only builds; mdBook otherwise tolerates missing pages. for page in &result.pages { let content = &sources[&page.source]; result.rewrite_links(content, &page.source)?; @@ -259,6 +262,50 @@ impl Inventory { } Ok((output, references)) } + + pub fn generate_guides(&self, sh: &Shell, output: &Utf8Path, version: &str) -> Result<()> { + let expected = self + .pages + .iter() + .filter(|p| p.section == 7) + .map(Page::filename) + .chain(["bootc-docs.7".to_string()]) + .collect(); + prune_stale_guides(output, &expected)?; + let mut index = String::from( + "# BOOTC-DOCS 7\n\n## NAME\n\nbootc-docs - Guide to bootc documentation\n\n## DESCRIPTION\n\nThe documentation shipped with bootc: guides in section 7, commands in section 8, and configuration and services in section 5. The groups below follow the website navigation.\n", + ); + let mut group = ""; + for page in &self.pages { + let content = fs::read_to_string(Utf8Path::new(DOCS_SRC).join(&page.source))?; + let (title, body) = title_and_body(&content)?; + if page.group != group { + group = &page.group; + index.push_str(&format!("\n## {group}\n\n")); + } + let description = if page.section == 7 { + title.to_string() + } else { + reference_description(body) + }; + index.push_str(&format!("- {} - {description}\n", page.reference())); + if page.section != 7 { + continue; + } + let adapted = adapt_guide_markdown_for_man(body)?; + let (body, mut references) = self.rewrite_links(&adapted, &page.source)?; + references.extend(["**bootc**(8)".to_string(), "**bootc-docs**(7)".to_string()]); + let markdown = format!( + "# {} 7\n\n## NAME\n\n{} - {title}\n\n## DESCRIPTION\n\n{body}\n\n## SEE ALSO\n\n{}\n\n## VERSION\n\n{version}\n", + page.name.to_ascii_uppercase(), + page.name, + references.into_iter().collect::>().join(", ") + ); + super::convert_markdown(sh, &markdown, &output.join(page.filename()))?; + } + index.push_str(&format!("\n## VERSION\n\n{version}\n")); + super::convert_markdown(sh, &index, &output.join("bootc-docs.7")) + } } fn normalize_link_path(source: &str, destination: &str) -> Result { @@ -277,6 +324,37 @@ fn normalize_link_path(source: &str, destination: &str) -> Result { Ok(parts.join("/")) } +fn reference_description(body: &str) -> String { + let paragraph = body + .trim_start() + .lines() + .take_while(|line| !line.trim().is_empty()) + .map(str::trim) + .collect::>() + .join(" "); + paragraph + .split_once(" - ") + .map(|(_, description)| description.to_string()) + .unwrap_or(paragraph) +} + +// target/man is build output, and bootc-*.7 is owned by this generator. +// Remove obsolete guide artifacts so the Makefile's wildcard cannot install them. +fn prune_stale_guides(output: &Utf8Path, expected: &BTreeSet) -> Result<()> { + for entry in fs::read_dir(output)? { + let path = Utf8PathBuf::from_path_buf(entry?.path()) + .map_err(|_| anyhow::anyhow!("Non-UTF-8 output path"))?; + let filename = path.file_name().context("Invalid output filename")?; + if filename.starts_with("bootc-") + && path.extension() == Some("7") + && !expected.contains(filename) + { + fs::remove_file(&path).with_context(|| format!("Removing stale guide {path}"))?; + } + } + Ok(()) +} + // Use parsed spans so fences inside examples remain examples, and equivalent // Markdown spellings (tilde fences, aligned tables) receive the same treatment. fn adapt_guide_markdown_for_man(body: &str) -> Result { @@ -408,6 +486,8 @@ mod tests { .collect::>(), ["bootc-overview.7", "bootc.8"] ); + assert_eq!(inventory.pages[0].group, "Guides"); + assert_eq!(inventory.pages[1].group, "Commands"); } #[test] @@ -538,6 +618,7 @@ mod tests { assert_eq!(title, "A guide"); assert_eq!(body, "Content.\n\n## Subtopic\n"); } + #[test] fn diagrams_and_tables_have_terminal_text() { let input = "```mermaid\nflowchart TD\nbootc --- image[\"containers/storage\"]\n```\n\n| Mode | Method |\n|---|---|\n| `to-disk` | UUID |\n"; @@ -563,4 +644,32 @@ mod tests { assert!(adapt_guide_markdown_for_man(bad).is_err(), "{bad}"); } } + + #[test] + fn reference_index_keeps_wrapped_name_paragraphs() { + assert_eq!( + reference_description( + "\nbootc-install - Install to an externally\ncreated filesystem\n\n# OPTIONS\n" + ), + "Install to an externally created filesystem" + ); + assert_eq!( + reference_description("bootc-config.toml\n\n# DESCRIPTION\n"), + "bootc-config.toml" + ); + } + + #[test] + fn stale_guides_removed_without_touching_references() { + let tmp = tempfile::tempdir().unwrap(); + let dir = Utf8Path::from_path(tmp.path()).unwrap(); + for name in ["bootc-current.7", "bootc-stale.7", "bootc.8", "other.7"] { + fs::write(dir.join(name), "test").unwrap(); + } + prune_stale_guides(dir, &BTreeSet::from(["bootc-current.7".into()])).unwrap(); + assert!(dir.join("bootc-current.7").exists()); + assert!(!dir.join("bootc-stale.7").exists()); + assert!(dir.join("bootc.8").exists()); + assert!(dir.join("other.7").exists()); + } } diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index ad6d235f65..180f8a0ab4 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -70,6 +70,8 @@ struct Cli { enum Commands { /// Generate man pages Manpages, + /// Generate section 7 guide man pages without building the bootc CLI + GuideManpages, /// Check website and installed manual coverage without building the bootc CLI CheckDocs, /// Update or check generated files @@ -370,6 +372,7 @@ fn try_main() -> Result<()> { match cli.command { Commands::Manpages => man::generate_man_pages(&sh), + Commands::GuideManpages => man::generate_guide_man_pages(&sh), Commands::CheckDocs => man::check_docs(), Commands::UpdateGenerated { command } => match command { UpdateGeneratedCommands::Direct { check } => { From 9397bf1a7c85b1de2b75c5fb265b31c229f27aa7 Mon Sep 17 00:00:00 2001 From: HarshwardhanPatil07 Date: Mon, 28 Sep 2026 12:23:33 +0530 Subject: [PATCH 6/8] build: Install guide manuals through the existing RPM path Install section 7 alongside sections 5 and 8, fail the install target if generation fails, and validate coverage during website and generated-file checks. Route reference generation through the shared inventory and point bootc(8) at the offline index. The existing RPM man* file glob already covers the new manuals. Generated-by: AI Signed-off-by: HarshwardhanPatil07 --- Makefile | 5 ++-- crates/xtask/src/man.rs | 62 ++++++++++------------------------------- docs/Dockerfile.mdbook | 2 ++ docs/src/man/bootc.8.md | 8 +++++- 4 files changed, 26 insertions(+), 51 deletions(-) diff --git a/Makefile b/Makefile index 5577e9b0a5..236c3d3145 100644 --- a/Makefile +++ b/Makefile @@ -67,8 +67,9 @@ install: completion ln -s "$(STORAGE_RELATIVE_PATH)" "$(DESTDIR)$(prefix)/lib/bootc/storage" install -D -m 0755 crates/cli/bootc-generator-stub $(DESTDIR)$(prefix)/lib/systemd/system-generators/bootc-systemd-generator install -d $(DESTDIR)$(prefix)/lib/bootc/install - install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man5 target/man/*.5; \ - install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man8 target/man/*.8; \ + install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man5 target/man/*.5 + install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man7 target/man/*.7 + install -D -m 0644 -t $(DESTDIR)$(prefix)/share/man/man8 target/man/*.8 install -D -m 0644 target/completion/bootc.bash $(DESTDIR)$(prefix)/share/bash-completion/completions/bootc install -D -m 0644 target/completion/bootc.elvish $(DESTDIR)$(prefix)/share/elvish/lib/bootc.elv install -D -m 0644 target/completion/bootc.fish $(DESTDIR)$(prefix)/share/fish/vendor_completions.d/bootc.fish diff --git a/crates/xtask/src/man.rs b/crates/xtask/src/man.rs index 22468ba924..5618102bff 100644 --- a/crates/xtask/src/man.rs +++ b/crates/xtask/src/man.rs @@ -491,61 +491,26 @@ pub fn sync_all_man_pages(sh: &Shell) -> Result<()> { Ok(()) } -/// Generate man pages from hand-written markdown sources +/// Generate manuals from the same canonical Markdown used by the website. #[context("Generating manpages")] pub fn generate_man_pages(sh: &Shell) -> Result<()> { let inventory = guides::Inventory::load()?; - let man_src_dir = Utf8Path::new("docs/src/man"); - let man_output_dir = Utf8Path::new("target/man"); - - // Ensure output directory exists - sh.create_dir(man_output_dir) - .with_context(|| format!("Creating {man_output_dir}"))?; - - // First, sync the markdown files with current CLI options + let output = Utf8Path::new("target/man"); + sh.create_dir(output)?; sync_all_man_pages(sh)?; - - // Get version for replacement during generation let version = get_package_version()?; - // Convert each markdown file to man page format - for entry in fs::read_dir(man_src_dir).context("Reading manpages")? { - let entry = entry?; - let path = entry.path(); - - if path.extension().and_then(|s| s.to_str()) != Some("md") { - continue; - } - - let filename = path - .file_stem() - .and_then(|s| s.to_str()) - .ok_or_else(|| anyhow::anyhow!("Invalid filename"))?; - - // Parse section from filename (e.g., bootc.8, bootc-config.5) - // All man page files must have a section number - let (base_name, section) = filename - .rsplit_once('.') - .and_then(|(name, section_str)| { - section_str.parse::().ok().map(|section| (name, section)) - }) - .ok_or_else(|| anyhow::anyhow!("Man page filename must include section number (e.g., bootc.8.md, bootc-config.5.md): {}.md", filename))?; - - let output_file = man_output_dir.join(format!("{}.{}", base_name, section)); - - // Read markdown content and replace version placeholders - let content = fs::read_to_string(&path).with_context(|| format!("Reading {path:?}"))?; - let markdown = reference_markdown(&content, base_name, section, &version); - convert_markdown(sh, &markdown, &output_file)?; - println!("Generated {}", output_file); + for page in inventory.pages.iter().filter(|p| p.section != 7) { + let source = Utf8Path::new("docs/src").join(&page.source); + let content = fs::read_to_string(&source)?; + let (content, _) = inventory.rewrite_links(&content, &page.source)?; + // Always regenerate: version and linked manual names can change even + // when the source Markdown's mtime does not. + let content = reference_markdown(&content, &page.name, page.section, &version); + convert_markdown(sh, &content, &output.join(page.filename()))?; } - - inventory.generate_guides(sh, man_output_dir, &version)?; - - // Apply post-processing fixes for apostrophe handling - apply_man_page_fixes(sh, man_output_dir)?; - - Ok(()) + inventory.generate_guides(sh, output, &version)?; + apply_man_page_fixes(sh, output) } /// Get version from Cargo.toml @@ -691,6 +656,7 @@ TODO: Add practical examples showing how to use this command. /// Fails with an error if any file would change, similar to `cargo fmt --check`. #[context("Checking man pages")] pub fn check_manpages(sh: &Shell) -> Result<()> { + check_docs()?; let cli_structure = extract_cli_json(sh)?; // First: check no man pages are missing diff --git a/docs/Dockerfile.mdbook b/docs/Dockerfile.mdbook index 55f90b6972..f3896b0b5d 100644 --- a/docs/Dockerfile.mdbook +++ b/docs/Dockerfile.mdbook @@ -24,6 +24,8 @@ set -xeuo pipefail cargo doc --workspace --no-deps --document-private-items # Also build docs for key external git dependencies (not on docs.rs) cargo doc --no-deps --document-private-items -p composefs-ctl +# Check sources before mdBook can create placeholders for missing chapters. +cargo run --locked --package xtask -- check-docs # Build mdbook cd docs mdbook-mermaid install . diff --git a/docs/src/man/bootc.8.md b/docs/src/man/bootc.8.md index 1b50497815..e564852fb3 100644 --- a/docs/src/man/bootc.8.md +++ b/docs/src/man/bootc.8.md @@ -17,6 +17,9 @@ directly via `bootc install` (executed as part of a container) or via another mechanism such as an OS installer tool, further updates can be pulled and `bootc upgrade`. +For guides to building, installing, and managing bootable images, see +**bootc-docs**(7). + @@ -38,7 +41,10 @@ pulled and `bootc upgrade`. +# SEE ALSO + +**bootc-docs**(7) + # VERSION - From ff44d8598bb7b1a8e1f1355f2fbf0b7cfe145ec8 Mon Sep 17 00:00:00 2001 From: HarshwardhanPatil07 Date: Mon, 28 Sep 2026 12:47:12 +0530 Subject: [PATCH 7/8] docs: Consolidate accurate reference explanations Keep each shared explanation in one canonical source so website and installed manuals cannot drift through separately maintained copies. Keep installation configuration discovery and merge precedence in its reference manual, and soft-reboot behavior in the upgrades guide. Replace repeated explanations with links while retaining command-specific details. Align the canonical text and CLI help with the implemented behavior, including platform-specific bootloader setup and the experimental composefs soft-reboot fallback limitation. No runtime behavior changes. Assisted-by: AI Signed-off-by: HarshwardhanPatil07 --- crates/lib/src/cli.rs | 10 ++++++---- docs/src/bootc-install.md | 20 +++++++++----------- docs/src/man/bootc-install-config.5.md | 13 ++++++++++--- docs/src/man/bootc-install.8.md | 20 ++++++-------------- docs/src/man/bootc-switch.8.md | 11 ++++------- docs/src/man/bootc-upgrade.8.md | 10 ++-------- docs/src/upgrades.md | 25 ++++++++++++++++++++++--- 7 files changed, 59 insertions(+), 50 deletions(-) diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 9d25ef7480..5218bdd1b5 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -119,13 +119,14 @@ pub(crate) struct UpgradeOpts { /// Restart or reboot into the new target image. /// - /// Currently, this always reboots. Future versions may support userspace-only restart. + /// Use --soft-reboot to request a userspace-only restart when supported. #[clap(long, conflicts_with = "check")] pub(crate) apply: bool, /// Configure soft reboot behavior. /// - /// 'required' fails if soft reboot unavailable, 'auto' falls back to regular reboot. + /// 'required' fails if soft reboot is unavailable; 'auto' uses it when possible. + /// See bootc-upgrades(7) for backend-specific fallback behavior. #[clap(long = "soft-reboot", conflicts_with = "check")] pub(crate) soft_reboot: Option, @@ -152,13 +153,14 @@ pub(crate) struct SwitchOpts { /// Restart or reboot into the new target image. /// - /// Currently, this always reboots. Future versions may support userspace-only restart. + /// Use --soft-reboot to request a userspace-only restart when supported. #[clap(long)] pub(crate) apply: bool, /// Configure soft reboot behavior. /// - /// 'required' fails if soft reboot unavailable, 'auto' falls back to regular reboot. + /// 'required' fails if soft reboot is unavailable; 'auto' uses it when possible. + /// See bootc-upgrades(7) for backend-specific fallback behavior. #[clap(long = "soft-reboot")] pub(crate) soft_reboot: Option, diff --git a/docs/src/bootc-install.md b/docs/src/bootc-install.md index 3dcffecbd5..f8247fcb76 100644 --- a/docs/src/bootc-install.md +++ b/docs/src/bootc-install.md @@ -9,11 +9,11 @@ or virtualized), one needs a few key components: - kernel (and optionally initramfs) - root filesystem (xfs/ext4/btrfs etc.) -The bootloader state is managed by the external [bootupd](https://github.com/coreos/bootupd/) -project which abstracts over bootloader installs and upgrades. The invocation of -`bootc install` will always run `bootupd` to handle bootloader installation -to the target disk. The default expectation is that bootloader contents and install logic -come from the container image in a `bootc` based system. +Bootloader installation depends on the platform and selected bootloader. +For example, GRUB installation uses [bootupd](https://github.com/coreos/bootupd/), +while systemd-boot uses `bootctl` and s390x uses `zipl`. Bootloader installation +can also be disabled. The default expectation is that bootloader contents +and install logic come from the container image in a `bootc` based system. The Linux kernel (and optionally initramfs) is embedded in the container image; the canonical location is `/usr/lib/modules/$kver/vmlinuz`, and the initramfs should be in `initramfs.img` @@ -113,12 +113,10 @@ create a file named `/usr/lib/bootc/install/00-.toml` with the contents type = "xfs" ``` -Configuration files found in this directory will be merged, with higher alphanumeric values -taking precedence. If for example you are building a derived container image from the above OS, -you could create a `50-myos.toml` that sets `type = "btrfs"` which will override the -prior setting. - -For other available options, see [bootc-install-config](man/bootc-install-config.5.md). +For example, a derived image can supply `50-myos.toml` with +`type = "btrfs"` to override this default. +See [bootc-install-config](man/bootc-install-config.5.md) for file discovery, +merge precedence, and the available configuration fields. ## Installing an "unconfigured" image diff --git a/docs/src/man/bootc-install-config.5.md b/docs/src/man/bootc-install-config.5.md index 8ed61e8b0a..7211c4aab7 100644 --- a/docs/src/man/bootc-install-config.5.md +++ b/docs/src/man/bootc-install-config.5.md @@ -4,9 +4,16 @@ bootc-install-config.toml # DESCRIPTION -The `bootc install` process supports some basic customization. This configuration file -is in TOML format, and will be discovered by the installation process in via "drop-in" -files in `/usr/lib/bootc/install` that are processed in alphanumerical order. +The `bootc install` process supports customization through TOML drop-in files +in `/usr/lib/bootc/install`, `/usr/local/lib/bootc/install`, `/etc/bootc/install`, +and `/run/bootc/install`. If the same filename occurs in multiple directories, +the file in the later directory in this list takes precedence. The selected +files are then processed in alphanumerical filename order. + +Fragments whose `match_architectures` includes the current architecture, or +which omit that field, are merged. Values such as the root filesystem type +are overridden when specified in a later fragment. The `kargs` and +`karg-deletes` lists are appended instead of replaced. The individual files are merged into a single final installation config, so it is supported for e.g. a container base image to provide a default root filesystem type, diff --git a/docs/src/man/bootc-install.8.md b/docs/src/man/bootc-install.8.md index f29443aaa5..eeb1203485 100644 --- a/docs/src/man/bootc-install.8.md +++ b/docs/src/man/bootc-install.8.md @@ -12,21 +12,13 @@ Install the running container to a target. ## Understanding installations -OCI containers are effectively layers of tarballs with JSON for -metadata; they cannot be booted directly. The `bootc install` flow is -a highly opinionated method to take the contents of the container image -and install it to a target block device (or an existing filesystem) in -such a way that it can be booted. +The `bootc install` flow turns a container image into a bootable system, +including filesystem, bootloader, and update metadata setup. It is not +simply a copy of the container filesystem. -For example, a Linux partition table and filesystem is used, and the -bootloader and kernel embedded in the container image are also prepared. - -A bootc installed container currently uses OSTree as a backend, and this -sets it up such that a subsequent `bootc upgrade` can perform in-place -updates. - -An installation is not simply a copy of the container filesystem, but -includes other setup and metadata. +See [Installing bootc compatible images](../bootc-install.md) for the +installation model, prerequisites, and end-to-end examples. This reference +documents the command and its subcommands. ## Secure Boot Keys diff --git a/docs/src/man/bootc-switch.8.md b/docs/src/man/bootc-switch.8.md index 407aa0bc09..4ca52a169a 100644 --- a/docs/src/man/bootc-switch.8.md +++ b/docs/src/man/bootc-switch.8.md @@ -23,16 +23,13 @@ It is also supported to provide explicit digests, via e.g. `bootc switch quay.io ## Applying Changes -The `--apply` option will automatically take action (rebooting) if the system has changed after switching to the new image. Currently, this option always reboots the system. In the future, this command may detect cases where no kernel changes are queued and perform a userspace-only restart instead. +The `--apply` option will automatically restart the system if it has +changed after switching to the new image. ## Soft Reboot -The `--soft-reboot` option configures soft reboot behavior when used with `--apply`: - -- `required`: The operation will fail if soft reboot is not available on the target system -- `auto`: Uses soft reboot if available on the target system, otherwise falls back to a regular reboot - -Soft reboot allows faster system restart by avoiding full hardware reboot when possible. +For shared `--apply` and `--soft-reboot` behavior, see +[Soft reboots](../upgrades.md#soft-reboots). # OPTIONS diff --git a/docs/src/man/bootc-upgrade.8.md b/docs/src/man/bootc-upgrade.8.md index b1b3f3b694..4440326988 100644 --- a/docs/src/man/bootc-upgrade.8.md +++ b/docs/src/man/bootc-upgrade.8.md @@ -24,19 +24,13 @@ Currently by default, the update will be applied at shutdown time via `ostree-fi There is also an explicit `bootc upgrade --apply` verb which will automatically take action (rebooting) if the system has changed. -The `--apply` option currently always reboots the system. In the future, this command may detect cases where no kernel changes are queued and perform a userspace-only restart instead. - However, in the future this is likely to change such that reboots outside of a `bootc upgrade --apply` do *not* automatically apply the update in addition. ## Soft Reboot -The `--soft-reboot` option configures soft reboot behavior when used with `--apply`: - -- `required`: The operation will fail if soft reboot is not available on the target system -- `auto`: Uses soft reboot if available on the target system, otherwise falls back to a regular reboot - -Soft reboot allows faster system restart by avoiding full hardware reboot when possible. +For shared `--apply` and `--soft-reboot` behavior, see +[Soft reboots](../upgrades.md#soft-reboots). # OPTIONS diff --git a/docs/src/upgrades.md b/docs/src/upgrades.md index d5276939ba..b61fe13136 100644 --- a/docs/src/upgrades.md +++ b/docs/src/upgrades.md @@ -8,7 +8,7 @@ updates from a registry and booting into them, while supporting rollback. This will query the container image source and queue an updated container image for the next boot. -This is backed today by ostree, implementing an A/B style upgrade system. +With the default OSTree backend, this implements an A/B style upgrade system. Changes to the base image are staged, and the running system is not changed by default. @@ -148,6 +148,27 @@ host SSH keys and home directories. Man page: [bootc-switch](man/bootc-switch.8.md). +## Soft reboots + +Soft reboot restarts userspace without restarting the kernel, avoiding a full +hardware reboot. On the OSTree backend, `bootc upgrade` and `bootc switch` +support these `--soft-reboot` modes: + +- `required`: Fails if the target deployment is not soft-reboot capable. +- `auto`: Prepares a soft reboot if the target deployment is capable; + otherwise, leaves it configured for a regular reboot. + +Use `--apply` to request an immediate restart after preparing the deployment. +Without `--apply`, `--soft-reboot` prepares the deployment but does not restart +the system immediately. Without `--soft-reboot`, `--apply` requests a regular +reboot. + +The [experimental composefs backend](experimental-composefs.md) currently +differs: both modes fail if systemd lacks soft-reboot support. If the target +deployment is not soft-reboot capable, `auto` leaves it staged without +restarting, even with `--apply`; it does not automatically fall back to a +regular reboot in this case. + ## Rollback There is a `bootc rollback` verb, and associated declarative interface @@ -155,5 +176,3 @@ accessible to tools via `bootc edit`. This will swap the bootloader ordering to the previous boot entry. Man page: [bootc-rollback](man/bootc-rollback.8.md). - - From 12513d12f2b47e2d04519b98acd04332970c28c2 Mon Sep 17 00:00:00 2001 From: HarshwardhanPatil07 Date: Mon, 28 Sep 2026 13:18:40 +0530 Subject: [PATCH 8/8] docs: Derive manual names from chapter filenames Keep one naming convention for guides and reference manuals instead of maintaining a separate chapter-to-manual manifest. Rename narrative chapters to stable section-7 filenames, reuse filename parsing, and update links without changing prose or navigation. Preserve published website URLs with mdBook redirects. Generated-by: AI Signed-off-by: HarshwardhanPatil07 --- CONTRIBUTING.md | 2 +- crates/xtask/src/man.rs | 575 +++++++++++++++++- crates/xtask/src/man/guides.rs | 564 +---------------- docs/book.toml | 41 ++ docs/manpages.toml | 41 -- docs/src/SUMMARY.md | 76 +-- docs/src/{bootc-via-api.md => bootc-api.7.md} | 0 ...installation.md => bootc-base-images.7.md} | 2 +- ...n.md => bootc-boot-failure-detection.7.md} | 0 ...{bootloaders.md => bootc-bootloaders.7.md} | 0 ...images.md => bootc-compatible-images.7.md} | 2 +- ...torage.md => bootc-container-storage.7.md} | 6 +- ...s.md => bootc-experimental-composefs.7.md} | 6 +- ... bootc-experimental-container-export.7.md} | 0 ...l-fsck.md => bootc-experimental-fsck.7.md} | 0 ...image.md => bootc-experimental-image.7.md} | 2 +- ... => bootc-experimental-install-reset.7.md} | 0 ...md => bootc-experimental-progress-fd.7.md} | 0 ...> bootc-experimental-unified-storage.7.md} | 6 +- ...on.md => bootc-filesystem-encryption.7.md} | 2 +- .../{filesystem.md => bootc-filesystem.7.md} | 18 +- ...n-container.md => bootc-in-container.7.md} | 0 .../{initramfs.md => bootc-initramfs.7.md} | 4 +- ...otc-install.md => bootc-installation.7.md} | 8 +- .../{internals.md => bootc-internals.7.md} | 2 +- ...ocal-builds.md => bootc-local-builds.7.md} | 0 ...s.md => bootc-logically-bound-images.7.md} | 2 +- ...-artifacts.md => bootc-oci-artifacts.7.md} | 0 docs/src/{intro.md => bootc-overview.7.md} | 0 ...anagers.md => bootc-package-managers.7.md} | 4 +- ...d => bootc-packaging-and-integration.7.md} | 2 +- ...e.md => bootc-registries-and-offline.7.md} | 0 ...ationships.md => bootc-relationships.7.md} | 2 +- docs/src/{security.md => bootc-security.7.md} | 0 ...lesystem-sysroot.md => bootc-sysroot.7.md} | 4 +- ...ticles.md => bootc-systemd-particles.7.md} | 0 docs/src/{upgrades.md => bootc-upgrades.7.md} | 2 +- ...guidance.md => bootc-building-images.7.md} | 10 +- ...untime.md => bootc-container-runtime.7.md} | 4 +- docs/src/building/{dns.md => bootc-dns.7.md} | 4 +- ...guments.md => bootc-kernel-arguments.7.md} | 0 ...ices.md => bootc-management-services.7.md} | 0 .../{secrets.md => bootc-secrets.7.md} | 0 ...-groups.md => bootc-users-and-groups.7.md} | 2 +- .../man/bootc-install-to-existing-root.8.md | 2 +- docs/src/man/bootc-install.8.md | 2 +- docs/src/man/bootc-switch.8.md | 2 +- docs/src/man/bootc-upgrade.8.md | 2 +- 48 files changed, 708 insertions(+), 693 deletions(-) delete mode 100644 docs/manpages.toml rename docs/src/{bootc-via-api.md => bootc-api.7.md} (100%) rename docs/src/{installation.md => bootc-base-images.7.md} (89%) rename docs/src/{boot-failure-detection.md => bootc-boot-failure-detection.7.md} (100%) rename docs/src/{bootloaders.md => bootc-bootloaders.7.md} (100%) rename docs/src/{bootc-images.md => bootc-compatible-images.7.md} (98%) rename docs/src/{filesystem-storage.md => bootc-container-storage.7.md} (94%) rename docs/src/{experimental-composefs.md => bootc-experimental-composefs.7.md} (96%) rename docs/src/{experimental-container-export.md => bootc-experimental-container-export.7.md} (100%) rename docs/src/{experimental-fsck.md => bootc-experimental-fsck.7.md} (100%) rename docs/src/{experimental-bootc-image.md => bootc-experimental-image.7.md} (96%) rename docs/src/{experimental-install-reset.md => bootc-experimental-install-reset.7.md} (100%) rename docs/src/{experimental-progress-fd.md => bootc-experimental-progress-fd.7.md} (100%) rename docs/src/{experimental-unified-storage.md => bootc-experimental-unified-storage.7.md} (96%) rename docs/src/{filesystem-encryption.md => bootc-filesystem-encryption.7.md} (97%) rename docs/src/{filesystem.md => bootc-filesystem.7.md} (96%) rename docs/src/{bootc-in-container.md => bootc-in-container.7.md} (100%) rename docs/src/{initramfs.md => bootc-initramfs.7.md} (97%) rename docs/src/{bootc-install.md => bootc-installation.7.md} (99%) rename docs/src/{internals.md => bootc-internals.7.md} (98%) rename docs/src/{booting-local-builds.md => bootc-local-builds.7.md} (100%) rename docs/src/{logically-bound-images.md => bootc-logically-bound-images.7.md} (98%) rename docs/src/{relationship-oci-artifacts.md => bootc-oci-artifacts.7.md} (100%) rename docs/src/{intro.md => bootc-overview.7.md} (100%) rename docs/src/{package-managers.md => bootc-package-managers.7.md} (96%) rename docs/src/{packaging-and-integration.md => bootc-packaging-and-integration.7.md} (98%) rename docs/src/{registries-and-offline.md => bootc-registries-and-offline.7.md} (100%) rename docs/src/{relationships.md => bootc-relationships.7.md} (98%) rename docs/src/{security.md => bootc-security.7.md} (100%) rename docs/src/{filesystem-sysroot.md => bootc-sysroot.7.md} (95%) rename docs/src/{relationship-particles.md => bootc-systemd-particles.7.md} (100%) rename docs/src/{upgrades.md => bootc-upgrades.7.md} (98%) rename docs/src/building/{guidance.md => bootc-building-images.7.md} (95%) rename docs/src/building/{bootc-runtime.md => bootc-container-runtime.7.md} (97%) rename docs/src/building/{dns.md => bootc-dns.7.md} (98%) rename docs/src/building/{kernel-arguments.md => bootc-kernel-arguments.7.md} (100%) rename docs/src/building/{management-services.md => bootc-management-services.7.md} (100%) rename docs/src/building/{secrets.md => bootc-secrets.7.md} (100%) rename docs/src/building/{users-and-groups.md => bootc-users-and-groups.7.md} (99%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f9df3c5b90..4d8b5e6ea1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -206,7 +206,7 @@ just validate-composefs-digest The `build-sealed` target generates test Secure Boot keys in `target/test-secureboot/` and builds a complete sealed image with all the sealed composefs settings. See -[experimental-composefs.md](docs/src/experimental-composefs.md) for +[experimental-composefs.md](docs/src/bootc-experimental-composefs.7.md) for more information on sealed images. diff --git a/crates/xtask/src/man.rs b/crates/xtask/src/man.rs index 5618102bff..d2042e60bf 100644 --- a/crates/xtask/src/man.rs +++ b/crates/xtask/src/man.rs @@ -2,21 +2,339 @@ //! //! This module handles both the generation of man pages from markdown sources //! and the synchronization of CLI options from Rust code to those markdown templates. +//! Canonical chapters use `.
.md`; SUMMARY.md owns their +//! navigation order. Guides and references share preparation and conversion. -use anyhow::{Context, Result}; -use camino::Utf8Path; +use anyhow::{Context, Result, ensure}; +use camino::{Utf8Path, Utf8PathBuf}; use fn_error_context::context; +use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd}; use serde::{Deserialize, Serialize}; -use std::{fs, io::Write}; +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + io::Write, + ops::Range, +}; use xshell::{Shell, cmd}; use crate::out_of_sync_error; mod guides; +const DOCS_SRC: &str = "docs/src"; +const BOOK_URL: &str = "https://bootc.dev/bootc/"; + +struct Page { + source: String, + name: String, + section: u8, + group: String, + content: String, +} + +impl Page { + fn filename(&self) -> String { + format!("{}.{}", self.name, self.section) + } + + fn reference(&self) -> String { + format!("**{}**({})", self.name, self.section) + } +} + +struct Inventory { + pages: Vec, + names: BTreeMap, +} + +fn sources_in( + root: &Utf8Path, + dir: &Utf8Path, + sources: &mut BTreeMap, +) -> Result<()> { + for entry in fs::read_dir(dir).with_context(|| format!("Reading {dir}"))? { + let path = Utf8PathBuf::from_path_buf(entry?.path()) + .map_err(|_| anyhow::anyhow!("Non-UTF-8 documentation path"))?; + if path.is_dir() { + sources_in(root, &path, sources)?; + } else if path.extension() == Some("md") { + let source = path.strip_prefix(root)?.to_string(); + let content = fs::read_to_string(&path).with_context(|| format!("Reading {path}"))?; + sources.insert(source, content); + } + } + Ok(()) +} + +/// Navigation is shared by the website and the offline index, including order. +fn chapters(summary: &str) -> Vec<(String, String)> { + let mut group = String::new(); + let mut in_heading = false; + let mut result = Vec::new(); + for event in Parser::new(summary) { + match event { + Event::Start(Tag::Heading { .. }) => { + group.clear(); + in_heading = true; + } + Event::End(TagEnd::Heading(_)) => in_heading = false, + Event::Text(text) | Event::Code(text) if in_heading => group.push_str(&text), + Event::Start(Tag::Link { dest_url, .. }) => { + result.push((group.clone(), dest_url.into_string())); + } + _ => (), + } + } + result +} + +fn title_and_body(content: &str) -> Result<(&str, &str)> { + let (heading, body) = content + .trim_start() + .split_once('\n') + .context("Page has no body")?; + let title = heading + .strip_prefix("# ") + .context("Page must start with an H1 heading")? + .trim(); + ensure!( + !title.is_empty() && !body.trim().is_empty(), + "Empty documentation page" + ); + Ok((title, body.trim_start_matches('\n'))) +} + +impl Inventory { + fn load() -> Result { + let root = Utf8Path::new(DOCS_SRC); + let mut sources = BTreeMap::new(); + sources_in(root, root, &mut sources)?; + Self::from_sources(sources) + } + + fn from_sources(mut sources: BTreeMap) -> Result { + let summary = sources.remove("SUMMARY.md").context("Missing SUMMARY.md")?; + // This historical, unlinked placeholder is not documentation. If it acquires + // content, require it to participate in coverage like every other chapter. + if sources + .get("related.md") + .is_some_and(|s| s.trim() == "# Related projects") + { + sources.remove("related.md"); + } + let mut seen = BTreeSet::new(); + let mut outputs = BTreeSet::from(["bootc-docs.7".to_string()]); + let mut pages = Vec::new(); + for (group, source) in chapters(&summary) { + ensure!( + seen.insert(source.clone()), + "Duplicate navigation entry: {source}" + ); + let content = sources + .remove(&source) + .with_context(|| format!("Missing chapter: {source}"))?; + title_and_body(&content).with_context(|| format!("Invalid chapter: {source}"))?; + // Use the same filename convention for guides and native manuals, + // regardless of their source directory. Keep service-name dots. + let (name, section) = Utf8Path::new(&source) + .file_name() + .and_then(|s| s.strip_suffix(".md")) + .and_then(|s| s.rsplit_once('.')) + .with_context(|| format!("Expected .
.md: {source}"))?; + ensure!( + matches!(section, "5" | "7" | "8"), + "Manuals must use section 5, 7 or 8: {source}" + ); + let section: u8 = section.parse()?; + ensure!( + name.contains("bootc") + && name.chars().all(|c| c.is_ascii_lowercase() + || c.is_ascii_digit() + || matches!(c, '-' | '.')), + "Manual name must be safe and match the RPM bootc file pattern: {source}" + ); + ensure!( + section != 7 || name.starts_with("bootc-"), + "Guide manual names must start with bootc-: {source}" + ); + let name = name.to_string(); + let page = Page { + source, + name, + section, + group, + content, + }; + ensure!( + outputs.insert(page.filename()), + "Duplicate manual output: {}", + page.filename() + ); + pages.push(page); + } + let missing: Vec<_> = sources.keys().collect(); + ensure!( + missing.is_empty(), + "Chapters missing from SUMMARY.md: {missing:?}" + ); + let names = pages + .iter() + .map(|p| (p.source.clone(), p.reference())) + .collect(); + Ok(Self { pages, names }) + } + + /// Change only link spans, not the surrounding Markdown. A Markdown parser + /// handles multiline/reference links and avoids links in inline/fenced code. + fn rewrite_links(&self, body: &str, source: &str) -> Result<(String, BTreeSet)> { + let mut replacements = Vec::new(); + let mut references = BTreeSet::new(); + let mut link: Option<(String, Range, Option>)> = None; + for (event, span) in Parser::new_ext(body, Options::ENABLE_TABLES).into_offset_iter() { + match event { + Event::Start(Tag::Link { dest_url, .. }) => { + link = Some((dest_url.into_string(), span, None)); + } + Event::End(TagEnd::Link) => { + let (destination, whole, label) = + link.take().context("Unmatched Markdown link")?; + let label = label.map(|r| &body[r]).unwrap_or(""); + let replacement = if destination.starts_with('#') { + Some(label.to_string()) + } else if destination.contains(':') || destination.starts_with('/') { + None + } else { + let split = destination.find(['#', '?']).unwrap_or(destination.len()); + let (path, suffix) = destination.split_at(split); + let path = normalize_link_path(source, path)?; + if let Some(reference) = self.names.get(&path) { + references.insert(reference.clone()); + Some(format!("{label} (see {reference})")) + } else if path.ends_with(".md") { + anyhow::bail!("Unmapped local link in {source}: {destination}"); + } else { + // Generated rustdoc and schemas are online artifacts, + // not narrative Markdown chapters or offline manuals. + Some(format!("[{label}]({BOOK_URL}{path}{suffix})")) + } + }; + if let Some(replacement) = replacement { + replacements.push((whole, replacement)); + } + } + _ => { + if let Some((_, _, label)) = &mut link { + if let Some(label) = label { + label.start = label.start.min(span.start); + label.end = label.end.max(span.end); + } else { + *label = Some(span); + } + } + } + } + } + let mut output = body.to_string(); + for (span, replacement) in replacements.into_iter().rev() { + output.replace_range(span, &replacement); + } + Ok((output, references)) + } + + /// Prepare every manual from one source snapshot, without running a converter. + fn prepare(&self, version: &str) -> Result> { + let mut manuals = Vec::new(); + let mut index = String::from( + "# BOOTC-DOCS 7\n\n## NAME\n\nbootc-docs - Guide to bootc documentation\n\n## DESCRIPTION\n\nThe documentation shipped with bootc: guides in section 7, commands in section 8, and configuration and services in section 5. The groups below follow the website navigation.\n", + ); + let mut group = ""; + for page in &self.pages { + let (title, body) = title_and_body(&page.content) + .with_context(|| format!("Invalid chapter: {}", page.source))?; + if page.group != group { + group = &page.group; + index.push_str(&format!("\n## {group}\n\n")); + } + let description = if page.section == 7 { + title.to_string() + } else { + reference_description(body) + }; + index.push_str(&format!("- {} - {description}\n", page.reference())); + let content = if page.section == 7 { + // Validate source links before terminal-only adaptations. + self.rewrite_links(&page.content, &page.source)?; + guides::adapt_markdown(body) + .with_context(|| format!("Preparing manual for {}", page.source))? + } else { + page.content.clone() + }; + let (content, references) = self.rewrite_links(&content, &page.source)?; + let markdown = if page.section == 7 { + guides::markdown(&page.name, title, &content, references, version) + } else { + reference_markdown(&content, &page.name, page.section, version) + }; + manuals.push((page.filename(), markdown)); + } + index.push_str(&format!("\n## VERSION\n\n{version}\n")); + manuals.push(("bootc-docs.7".to_string(), index)); + Ok(manuals) + } +} + +fn normalize_link_path(source: &str, destination: &str) -> Result { + let mut parts: Vec<&str> = source.split('/').collect(); + parts.pop(); + for part in destination.split('/') { + match part { + "" | "." => (), + ".." => { + ensure!(!parts.is_empty(), "Link escapes docs/src: {destination}"); + parts.pop(); + } + _ => parts.push(part), + } + } + Ok(parts.join("/")) +} + +fn reference_description(body: &str) -> String { + let paragraph = body + .trim_start() + .lines() + .take_while(|line| !line.trim().is_empty()) + .map(str::trim) + .collect::>() + .join(" "); + paragraph + .split_once(" - ") + .map(|(_, description)| description.to_string()) + .unwrap_or(paragraph) +} + +// target/man is build output, and bootc-*.7 is owned by this generator. +// Remove obsolete guide artifacts so the Makefile's wildcard cannot install them. +fn prune_stale_guides(output: &Utf8Path, expected: &BTreeSet) -> Result<()> { + for entry in fs::read_dir(output)? { + let path = Utf8PathBuf::from_path_buf(entry?.path()) + .map_err(|_| anyhow::anyhow!("Non-UTF-8 output path"))?; + let filename = path.file_name().context("Invalid output filename")?; + if filename.starts_with("bootc-") + && path.extension() == Some("7") + && !expected.contains(filename) + { + fs::remove_file(&path).with_context(|| format!("Removing stale guide {path}"))?; + } + } + Ok(()) +} + /// Validate one-to-one website and installed manual coverage. pub fn check_docs() -> Result<()> { - let inventory = guides::Inventory::load()?; + let inventory = Inventory::load()?; + inventory.prepare(&get_package_version()?)?; println!( "Validated {} canonical pages for website and manuals", inventory.pages.len() @@ -26,10 +344,25 @@ pub fn check_docs() -> Result<()> { /// Render narrative guides without rebuilding the bootc CLI. pub fn generate_guide_man_pages(sh: &Shell) -> Result<()> { - let inventory = guides::Inventory::load()?; + generate_pages(sh, true) +} + +/// Guides and references use the same preparation and conversion path. +fn generate_pages(sh: &Shell, guides_only: bool) -> Result<()> { + let inventory = Inventory::load()?; + let manuals = inventory.prepare(&get_package_version()?)?; let output = Utf8Path::new("target/man"); sh.create_dir(output)?; - inventory.generate_guides(sh, output, &get_package_version()?)?; + let expected = manuals + .iter() + .map(|(filename, _)| filename.clone()) + .collect(); + prune_stale_guides(output, &expected)?; + for (filename, markdown) in &manuals { + if !guides_only || filename.ends_with(".7") { + convert_markdown(sh, markdown, &output.join(filename))?; + } + } apply_man_page_fixes(sh, output) } @@ -494,23 +827,9 @@ pub fn sync_all_man_pages(sh: &Shell) -> Result<()> { /// Generate manuals from the same canonical Markdown used by the website. #[context("Generating manpages")] pub fn generate_man_pages(sh: &Shell) -> Result<()> { - let inventory = guides::Inventory::load()?; - let output = Utf8Path::new("target/man"); - sh.create_dir(output)?; + // Load the source snapshot after CLI options have been synchronized. sync_all_man_pages(sh)?; - let version = get_package_version()?; - - for page in inventory.pages.iter().filter(|p| p.section != 7) { - let source = Utf8Path::new("docs/src").join(&page.source); - let content = fs::read_to_string(&source)?; - let (content, _) = inventory.rewrite_links(&content, &page.source)?; - // Always regenerate: version and linked manual names can change even - // when the source Markdown's mtime does not. - let content = reference_markdown(&content, &page.name, page.section, &version); - convert_markdown(sh, &content, &output.join(page.filename()))?; - } - inventory.generate_guides(sh, output, &version)?; - apply_man_page_fixes(sh, output) + generate_pages(sh, false) } /// Get version from Cargo.toml @@ -837,3 +1156,215 @@ mod rendering_tests { ); } } + +#[cfg(test)] +mod documentation_tests { + use super::*; + + fn sources() -> BTreeMap { + BTreeMap::from([ + ("SUMMARY.md".into(), "# Guides\n\n- [Introduction](bootc-overview.7.md)\n\n# Commands\n\n- [bootc](man/bootc.8.md)\n".into()), + ("bootc-overview.7.md".into(), "# Introduction\n\nA guide with **formatting**.\n".into()), + ("man/bootc.8.md".into(), "# NAME\n\nbootc - Bootable containers\n".into()), + ]) + } + + #[test] + fn coverage_and_order() { + let inventory = Inventory::from_sources(sources()).unwrap(); + assert_eq!( + inventory + .pages + .iter() + .map(Page::filename) + .collect::>(), + ["bootc-overview.7", "bootc.8"] + ); + assert_eq!(inventory.pages[0].group, "Guides"); + assert_eq!(inventory.pages[1].group, "Commands"); + } + + #[test] + fn coverage_rejects_omissions_duplicates_and_empty_pages() { + for (path, content, error) in [ + ( + "unlisted.md", + "# Missing\n\nContent\n", + "missing from SUMMARY", + ), + ("bootc-overview.7.md", "# Empty\n", "Invalid chapter"), + ("SUMMARY.md", "- [Missing](missing.md)\n", "Missing chapter"), + ( + "SUMMARY.md", + "- [Intro](bootc-overview.7.md)\n- [Again](bootc-overview.7.md)\n", + "Duplicate navigation", + ), + ( + "SUMMARY.md", + "- [Intro](bootc-overview.7.md)\n", + "missing from SUMMARY", + ), + ] { + let mut sources = sources(); + sources.insert(path.into(), content.into()); + let result = Inventory::from_sources(sources); + let message = result.err().unwrap().to_string(); + assert!(message.contains(error), "{path}: {message}"); + } + } + + #[test] + fn filenames_identify_manuals_in_any_directory() { + for (source, expected) in [ + ("building/bootc-images.7.md", "bootc-images.7"), + ("man/bootc-guide.7.md", "bootc-guide.7"), + ("configuration/bootc-config.5.md", "bootc-config.5"), + ("man/bootc-example.service.5.md", "bootc-example.service.5"), + ( + "man/system-reinstall-bootc.8.md", + "system-reinstall-bootc.8", + ), + ("commands/bootc-example.8.md", "bootc-example.8"), + ] { + let mut sources = sources(); + sources.remove("bootc-overview.7.md"); + sources.insert(source.into(), "# Example\n\nContent\n".into()); + let summary = sources.get_mut("SUMMARY.md").unwrap(); + *summary = summary.replace("bootc-overview.7.md", source); + let inventory = Inventory::from_sources(sources).unwrap(); + assert_eq!(inventory.pages[0].filename(), expected, "{source}"); + } + } + + #[test] + fn filenames_reject_invalid_and_colliding_manuals() { + for (source, error) in [ + ("intro.md", "Expected .
.md"), + ("bootc-intro.1.md", "Manuals must use section 5, 7 or 8"), + ("bootc-intro.07.md", "Manuals must use section 5, 7 or 8"), + ("bootc-intro.x.md", "Manuals must use section 5, 7 or 8"), + ("other.7.md", "Manual name must be safe"), + ("bootc-Bad.7.md", "Manual name must be safe"), + ("bootc-bad name.7.md", "Manual name must be safe"), + ( + "system-bootc.7.md", + "Guide manual names must start with bootc-", + ), + ("bootc-docs.7.md", "Duplicate manual output"), + ("nested/bootc-overview.7.md", "Duplicate manual output"), + ] { + let mut sources = sources(); + sources.insert(source.into(), "# Additional\n\nContent\n".into()); + sources + .get_mut("SUMMARY.md") + .unwrap() + .push_str(&format!("- [Additional](<{source}>)\n")); + let message = Inventory::from_sources(sources).err().unwrap().to_string(); + assert!(message.contains(error), "{source}: {message}"); + } + } + + #[test] + fn placeholder_is_exempt_only_while_empty() { + let mut sources = sources(); + sources.insert("related.md".into(), "# Related projects\n".into()); + assert!(Inventory::from_sources(sources.clone()).is_ok()); + sources + .get_mut("related.md") + .unwrap() + .push_str("\nNow it has content.\n"); + assert!(Inventory::from_sources(sources).is_err()); + } + + #[test] + fn parsed_links_preserve_formatting_and_code() { + let inventory = Inventory::from_sources(sources()).unwrap(); + for (input, expected) in [ + ( + "[guide](../bootc-overview.7.md#details)", + "guide (see **bootc-overview**(7))", + ), + ( + "[**guide**\nlabel](../bootc-overview.7.md)", + "**guide**\nlabel (see **bootc-overview**(7))", + ), + ( + "[guide][g]\n\n[g]: ../bootc-overview.7.md\n", + "guide (see **bootc-overview**(7))\n\n[g]: ../bootc-overview.7.md\n", + ), + ("[`bootc`](bootc.8.md)", "`bootc` (see **bootc**(8))"), + ("[local](#details)", "local"), + ( + "[web](https://example.com/a(b))", + "[web](https://example.com/a(b))", + ), + ( + "[email](mailto:bootc@example.com)", + "[email](mailto:bootc@example.com)", + ), + ( + "[api](../internals/api.html#section)", + "[api](https://bootc.dev/bootc/internals/api.html#section)", + ), + ("`[code](../missing.md)`", "`[code](../missing.md)`"), + ( + "~~~~md\n[code](../missing.md)\n~~~~\n", + "~~~~md\n[code](../missing.md)\n~~~~\n", + ), + ] { + let (actual, _) = inventory.rewrite_links(input, "man/bootc.8.md").unwrap(); + assert_eq!(actual, expected, "{input}"); + } + let (_, refs) = inventory + .rewrite_links("[guide](../bootc-overview.7.md)", "man/bootc.8.md") + .unwrap(); + assert_eq!(refs, BTreeSet::from(["**bootc-overview**(7)".into()])); + } + + #[test] + fn missing_or_escaping_doc_links_fail() { + let inventory = Inventory::from_sources(sources()).unwrap(); + for link in ["[bad](missing.md)", "[bad](../../bootc-overview.7.md)"] { + assert!( + inventory + .rewrite_links(link, "bootc-overview.7.md") + .is_err() + ); + } + } + + #[test] + fn title_keeps_body_and_subheadings() { + let (title, body) = title_and_body("# A guide\n\nContent.\n\n## Subtopic\n").unwrap(); + assert_eq!(title, "A guide"); + assert_eq!(body, "Content.\n\n## Subtopic\n"); + } + + #[test] + fn reference_index_keeps_wrapped_name_paragraphs() { + assert_eq!( + reference_description( + "\nbootc-install - Install to an externally\ncreated filesystem\n\n# OPTIONS\n" + ), + "Install to an externally created filesystem" + ); + assert_eq!( + reference_description("bootc-config.toml\n\n# DESCRIPTION\n"), + "bootc-config.toml" + ); + } + + #[test] + fn stale_guides_removed_without_touching_references() { + let tmp = tempfile::tempdir().unwrap(); + let dir = Utf8Path::from_path(tmp.path()).unwrap(); + for name in ["bootc-current.7", "bootc-stale.7", "bootc.8", "other.7"] { + fs::write(dir.join(name), "test").unwrap(); + } + prune_stale_guides(dir, &BTreeSet::from(["bootc-current.7".into()])).unwrap(); + assert!(dir.join("bootc-current.7").exists()); + assert!(!dir.join("bootc-stale.7").exists()); + assert!(dir.join("bootc.8").exists()); + assert!(dir.join("other.7").exists()); + } +} diff --git a/crates/xtask/src/man/guides.rs b/crates/xtask/src/man/guides.rs index 67dadcefb1..d56911d700 100644 --- a/crates/xtask/src/man/guides.rs +++ b/crates/xtask/src/man/guides.rs @@ -1,363 +1,29 @@ -//! Publish canonical mdBook chapters as manuals, without generating website copies. +//! Pure adaptations needed only by narrative section-7 manuals. +//! Discovery, validation, indexing and conversion are shared in man.rs. use anyhow::{Context, Result, ensure}; -use camino::{Utf8Path, Utf8PathBuf}; use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd}; -use serde::Deserialize; -use std::{ - collections::{BTreeMap, BTreeSet}, - fs, - ops::Range, -}; -use xshell::Shell; - -const DOCS_SRC: &str = "docs/src"; -const BOOK_URL: &str = "https://bootc.dev/bootc/"; - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct Manifest { - guides: BTreeMap, -} - -pub(super) struct Page { - pub source: String, - pub name: String, - pub section: u8, - group: String, -} - -impl Page { - pub fn filename(&self) -> String { - format!("{}.{}", self.name, self.section) - } - - fn reference(&self) -> String { - format!("**{}**({})", self.name, self.section) - } -} - -pub(super) struct Inventory { - pub pages: Vec, - names: BTreeMap, -} - -fn sources_in( - root: &Utf8Path, - dir: &Utf8Path, - sources: &mut BTreeMap, -) -> Result<()> { - for entry in fs::read_dir(dir).with_context(|| format!("Reading {dir}"))? { - let path = Utf8PathBuf::from_path_buf(entry?.path()) - .map_err(|_| anyhow::anyhow!("Non-UTF-8 documentation path"))?; - if path.is_dir() { - sources_in(root, &path, sources)?; - } else if path.extension() == Some("md") { - let source = path.strip_prefix(root)?.to_string(); - let content = fs::read_to_string(&path).with_context(|| format!("Reading {path}"))?; - sources.insert(source, content); - } - } - Ok(()) -} - -/// Navigation is shared by the website and the offline index, including order. -fn chapters(summary: &str) -> Vec<(String, String)> { - let mut group = String::new(); - let mut in_heading = false; - let mut result = Vec::new(); - for event in Parser::new(summary) { - match event { - Event::Start(Tag::Heading { .. }) => { - group.clear(); - in_heading = true; - } - Event::End(TagEnd::Heading(_)) => in_heading = false, - Event::Text(text) | Event::Code(text) if in_heading => group.push_str(&text), - Event::Start(Tag::Link { dest_url, .. }) => { - result.push((group.clone(), dest_url.into_string())); - } - _ => (), - } - } - result -} - -fn title_and_body(content: &str) -> Result<(&str, &str)> { - let (heading, body) = content - .trim_start() - .split_once('\n') - .context("Page has no body")?; - let title = heading - .strip_prefix("# ") - .context("Page must start with an H1 heading")? - .trim(); - ensure!( - !title.is_empty() && !body.trim().is_empty(), - "Empty documentation page" - ); - Ok((title, body.trim_start_matches('\n'))) -} - -impl Inventory { - pub fn load() -> Result { - let manifest = fs::read_to_string("docs/manpages.toml")?; - let root = Utf8Path::new(DOCS_SRC); - let mut sources = BTreeMap::new(); - sources_in(root, root, &mut sources)?; - Self::from_sources(&manifest, sources) - } - - fn from_sources(manifest: &str, mut sources: BTreeMap) -> Result { - let manifest: Manifest = toml::from_str(manifest).context("Parsing docs/manpages.toml")?; - let summary = sources.remove("SUMMARY.md").context("Missing SUMMARY.md")?; - // This historical, unlinked placeholder is not documentation. If it acquires - // content, require it to participate in coverage like every other chapter. - if sources - .get("related.md") - .is_some_and(|s| s.trim() == "# Related projects") - { - sources.remove("related.md"); - } - let mut seen = BTreeSet::new(); - let mut outputs = BTreeSet::from(["bootc-docs.7".to_string()]); - let mut pages = Vec::new(); - let mut mapped = BTreeSet::new(); - for (group, source) in chapters(&summary) { - ensure!( - seen.insert(source.clone()), - "Duplicate navigation entry: {source}" - ); - let content = sources - .get(&source) - .with_context(|| format!("Missing chapter: {source}"))?; - title_and_body(content).with_context(|| format!("Invalid chapter: {source}"))?; - let (name, section) = if let Some(native) = source.strip_prefix("man/") { - let (name, section) = native - .strip_suffix(".md") - .and_then(|s| s.rsplit_once('.')) - .context("Invalid native manual filename")?; - let section: u8 = section.parse()?; - ensure!( - name.contains("bootc") - && name.chars().all(|c| c.is_ascii_lowercase() - || c.is_ascii_digit() - || matches!(c, '-' | '.')), - "Manual name must be safe and match the RPM bootc file pattern: {source}" - ); - ensure!( - matches!(section, 5 | 8), - "Native manuals must use section 5 or 8: {source}" - ); - (name.to_string(), section) - } else { - let name = manifest - .guides - .get(&source) - .with_context(|| format!("Missing man page mapping: {source}"))?; - ensure!( - name.starts_with("bootc-") - && name - .chars() - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'), - "Invalid guide manual name: {name}" - ); - mapped.insert(source.clone()); - (name.clone(), 7) - }; - let page = Page { - source, - name, - section, - group, - }; - ensure!( - outputs.insert(page.filename()), - "Duplicate manual output: {}", - page.filename() - ); - pages.push(page); - } - let missing: Vec<_> = sources.keys().filter(|p| !seen.contains(*p)).collect(); - ensure!( - missing.is_empty(), - "Chapters missing from SUMMARY.md: {missing:?}" - ); - let extra: Vec<_> = manifest - .guides - .keys() - .filter(|p| !mapped.contains(*p)) - .collect(); - ensure!(extra.is_empty(), "Unused guide mappings: {extra:?}"); - let names = pages - .iter() - .map(|p| (p.source.clone(), p.reference())) - .collect(); - let result = Self { pages, names }; - // Validate all internal Markdown links and terminal-only transformations - // even in website-only builds; mdBook otherwise tolerates missing pages. - for page in &result.pages { - let content = &sources[&page.source]; - result.rewrite_links(content, &page.source)?; - if page.section == 7 { - adapt_guide_markdown_for_man(content)?; - } - } - Ok(result) - } - - /// Change only link spans, not the surrounding Markdown. A Markdown parser - /// handles multiline/reference links and avoids links in inline/fenced code. - pub fn rewrite_links(&self, body: &str, source: &str) -> Result<(String, BTreeSet)> { - let mut replacements = Vec::new(); - let mut references = BTreeSet::new(); - let mut link: Option<(String, Range, Option>)> = None; - for (event, span) in Parser::new_ext(body, Options::ENABLE_TABLES).into_offset_iter() { - match event { - Event::Start(Tag::Link { dest_url, .. }) => { - link = Some((dest_url.into_string(), span, None)); - } - Event::End(TagEnd::Link) => { - let (destination, whole, label) = - link.take().context("Unmatched Markdown link")?; - let label = label.map(|r| &body[r]).unwrap_or(""); - let replacement = if destination.starts_with('#') { - Some(label.to_string()) - } else if destination.contains(':') || destination.starts_with('/') { - None - } else { - let split = destination.find(['#', '?']).unwrap_or(destination.len()); - let (path, suffix) = destination.split_at(split); - let path = normalize_link_path(source, path)?; - if let Some(reference) = self.names.get(&path) { - references.insert(reference.clone()); - Some(format!("{label} (see {reference})")) - } else if path.ends_with(".md") { - anyhow::bail!("Unmapped local link in {source}: {destination}"); - } else { - // Generated rustdoc and schemas are online artifacts, - // not narrative Markdown chapters or offline manuals. - Some(format!("[{label}]({BOOK_URL}{path}{suffix})")) - } - }; - if let Some(replacement) = replacement { - replacements.push((whole, replacement)); - } - } - _ => { - if let Some((_, _, label)) = &mut link { - if let Some(label) = label { - label.start = label.start.min(span.start); - label.end = label.end.max(span.end); - } else { - *label = Some(span); - } - } - } - } - } - let mut output = body.to_string(); - for (span, replacement) in replacements.into_iter().rev() { - output.replace_range(span, &replacement); - } - Ok((output, references)) - } - - pub fn generate_guides(&self, sh: &Shell, output: &Utf8Path, version: &str) -> Result<()> { - let expected = self - .pages - .iter() - .filter(|p| p.section == 7) - .map(Page::filename) - .chain(["bootc-docs.7".to_string()]) - .collect(); - prune_stale_guides(output, &expected)?; - let mut index = String::from( - "# BOOTC-DOCS 7\n\n## NAME\n\nbootc-docs - Guide to bootc documentation\n\n## DESCRIPTION\n\nThe documentation shipped with bootc: guides in section 7, commands in section 8, and configuration and services in section 5. The groups below follow the website navigation.\n", - ); - let mut group = ""; - for page in &self.pages { - let content = fs::read_to_string(Utf8Path::new(DOCS_SRC).join(&page.source))?; - let (title, body) = title_and_body(&content)?; - if page.group != group { - group = &page.group; - index.push_str(&format!("\n## {group}\n\n")); - } - let description = if page.section == 7 { - title.to_string() - } else { - reference_description(body) - }; - index.push_str(&format!("- {} - {description}\n", page.reference())); - if page.section != 7 { - continue; - } - let adapted = adapt_guide_markdown_for_man(body)?; - let (body, mut references) = self.rewrite_links(&adapted, &page.source)?; - references.extend(["**bootc**(8)".to_string(), "**bootc-docs**(7)".to_string()]); - let markdown = format!( - "# {} 7\n\n## NAME\n\n{} - {title}\n\n## DESCRIPTION\n\n{body}\n\n## SEE ALSO\n\n{}\n\n## VERSION\n\n{version}\n", - page.name.to_ascii_uppercase(), - page.name, - references.into_iter().collect::>().join(", ") - ); - super::convert_markdown(sh, &markdown, &output.join(page.filename()))?; - } - index.push_str(&format!("\n## VERSION\n\n{version}\n")); - super::convert_markdown(sh, &index, &output.join("bootc-docs.7")) - } -} - -fn normalize_link_path(source: &str, destination: &str) -> Result { - let mut parts: Vec<&str> = source.split('/').collect(); - parts.pop(); - for part in destination.split('/') { - match part { - "" | "." => (), - ".." => { - ensure!(!parts.is_empty(), "Link escapes docs/src: {destination}"); - parts.pop(); - } - _ => parts.push(part), - } - } - Ok(parts.join("/")) -} - -fn reference_description(body: &str) -> String { - let paragraph = body - .trim_start() - .lines() - .take_while(|line| !line.trim().is_empty()) - .map(str::trim) - .collect::>() - .join(" "); - paragraph - .split_once(" - ") - .map(|(_, description)| description.to_string()) - .unwrap_or(paragraph) -} - -// target/man is build output, and bootc-*.7 is owned by this generator. -// Remove obsolete guide artifacts so the Makefile's wildcard cannot install them. -fn prune_stale_guides(output: &Utf8Path, expected: &BTreeSet) -> Result<()> { - for entry in fs::read_dir(output)? { - let path = Utf8PathBuf::from_path_buf(entry?.path()) - .map_err(|_| anyhow::anyhow!("Non-UTF-8 output path"))?; - let filename = path.file_name().context("Invalid output filename")?; - if filename.starts_with("bootc-") - && path.extension() == Some("7") - && !expected.contains(filename) - { - fs::remove_file(&path).with_context(|| format!("Removing stale guide {path}"))?; - } - } - Ok(()) +use std::collections::BTreeSet; + +/// Wrap narrative text in manual sections without changing the website source. +pub(super) fn markdown( + name: &str, + title: &str, + body: &str, + mut references: BTreeSet, + version: &str, +) -> String { + references.extend(["**bootc**(8)".to_string(), "**bootc-docs**(7)".to_string()]); + format!( + "# {} 7\n\n## NAME\n\n{name} - {title}\n\n## DESCRIPTION\n\n{body}\n\n## SEE ALSO\n\n{}\n\n## VERSION\n\n{version}\n", + name.to_ascii_uppercase(), + references.into_iter().collect::>().join(", ") + ) } // Use parsed spans so fences inside examples remain examples, and equivalent // Markdown spellings (tilde fences, aligned tables) receive the same treatment. -fn adapt_guide_markdown_for_man(body: &str) -> Result { +pub(super) fn adapt_markdown(body: &str) -> Result { let mut events = Parser::new_ext(body, Options::ENABLE_TABLES).into_offset_iter(); let mut replacements = Vec::new(); while let Some((event, span)) = events.next() { @@ -465,211 +131,29 @@ fn flowchart_as_text(diagram: &str) -> Result { mod tests { use super::*; - const MANIFEST: &str = "[guides]\n\"intro.md\" = \"bootc-overview\"\n"; - - fn sources() -> BTreeMap { - BTreeMap::from([ - ("SUMMARY.md".into(), "# Guides\n\n- [Introduction](intro.md)\n\n# Commands\n\n- [bootc](man/bootc.8.md)\n".into()), - ("intro.md".into(), "# Introduction\n\nA guide with **formatting**.\n".into()), - ("man/bootc.8.md".into(), "# NAME\n\nbootc - Bootable containers\n".into()), - ]) - } - - #[test] - fn coverage_and_order() { - let inventory = Inventory::from_sources(MANIFEST, sources()).unwrap(); - assert_eq!( - inventory - .pages - .iter() - .map(Page::filename) - .collect::>(), - ["bootc-overview.7", "bootc.8"] - ); - assert_eq!(inventory.pages[0].group, "Guides"); - assert_eq!(inventory.pages[1].group, "Commands"); - } - - #[test] - fn coverage_rejects_omissions_duplicates_and_empty_pages() { - for (path, content, error) in [ - ( - "unlisted.md", - "# Missing\n\nContent\n", - "missing from SUMMARY", - ), - ("intro.md", "# Empty\n", "Invalid chapter"), - ("SUMMARY.md", "- [Missing](missing.md)\n", "Missing chapter"), - ( - "SUMMARY.md", - "- [Intro](intro.md)\n- [Again](intro.md)\n", - "Duplicate navigation", - ), - ( - "SUMMARY.md", - "- [Intro](intro.md)\n", - "missing from SUMMARY", - ), - ] { - let mut sources = sources(); - sources.insert(path.into(), content.into()); - let result = Inventory::from_sources(MANIFEST, sources); - let message = result.err().unwrap().to_string(); - assert!(message.contains(error), "{path}: {message}"); - } - } - - #[test] - fn manifest_rejects_missing_extra_unsafe_and_reserved_names() { - for manifest in [ - "[guides]\n", - "[guides]\n\"intro.md\"=\"bootc-overview\"\n\"missing.md\"=\"bootc-missing\"\n", - "[guides]\n\"intro.md\"=\"../bad\"\n", - "[guides]\n\"intro.md\"=\"bootc-docs\"\n", - ] { - assert!( - Inventory::from_sources(manifest, sources()).is_err(), - "{manifest}" - ); - } - let mut sources = sources(); - sources.insert("second.md".into(), "# Second\n\nContent\n".into()); - sources - .get_mut("SUMMARY.md") - .unwrap() - .push_str("- [Second](second.md)\n"); - assert!( - Inventory::from_sources( - &format!("{MANIFEST}\"second.md\"=\"bootc-overview\"\n"), - sources - ) - .is_err() - ); - } - - #[test] - fn placeholder_is_exempt_only_while_empty() { - let mut sources = sources(); - sources.insert("related.md".into(), "# Related projects\n".into()); - assert!(Inventory::from_sources(MANIFEST, sources.clone()).is_ok()); - sources - .get_mut("related.md") - .unwrap() - .push_str("\nNow it has content.\n"); - assert!(Inventory::from_sources(MANIFEST, sources).is_err()); - } - - #[test] - fn parsed_links_preserve_formatting_and_code() { - let inventory = Inventory::from_sources(MANIFEST, sources()).unwrap(); - for (input, expected) in [ - ( - "[guide](../intro.md#details)", - "guide (see **bootc-overview**(7))", - ), - ( - "[**guide**\nlabel](../intro.md)", - "**guide**\nlabel (see **bootc-overview**(7))", - ), - ( - "[guide][g]\n\n[g]: ../intro.md\n", - "guide (see **bootc-overview**(7))\n\n[g]: ../intro.md\n", - ), - ("[`bootc`](bootc.8.md)", "`bootc` (see **bootc**(8))"), - ("[local](#details)", "local"), - ( - "[web](https://example.com/a(b))", - "[web](https://example.com/a(b))", - ), - ( - "[email](mailto:bootc@example.com)", - "[email](mailto:bootc@example.com)", - ), - ( - "[api](../internals/api.html#section)", - "[api](https://bootc.dev/bootc/internals/api.html#section)", - ), - ("`[code](../missing.md)`", "`[code](../missing.md)`"), - ( - "~~~~md\n[code](../missing.md)\n~~~~\n", - "~~~~md\n[code](../missing.md)\n~~~~\n", - ), - ] { - let (actual, _) = inventory.rewrite_links(input, "man/bootc.8.md").unwrap(); - assert_eq!(actual, expected, "{input}"); - } - let (_, refs) = inventory - .rewrite_links("[guide](../intro.md)", "man/bootc.8.md") - .unwrap(); - assert_eq!(refs, BTreeSet::from(["**bootc-overview**(7)".into()])); - } - - #[test] - fn missing_or_escaping_doc_links_fail() { - let inventory = Inventory::from_sources(MANIFEST, sources()).unwrap(); - for link in ["[bad](missing.md)", "[bad](../../intro.md)"] { - assert!(inventory.rewrite_links(link, "intro.md").is_err()); - } - } - - #[test] - fn title_keeps_body_and_subheadings() { - let (title, body) = title_and_body("# A guide\n\nContent.\n\n## Subtopic\n").unwrap(); - assert_eq!(title, "A guide"); - assert_eq!(body, "Content.\n\n## Subtopic\n"); - } - #[test] fn diagrams_and_tables_have_terminal_text() { let input = "```mermaid\nflowchart TD\nbootc --- image[\"containers/storage\"]\n```\n\n| Mode | Method |\n|---|---|\n| `to-disk` | UUID |\n"; - let output = adapt_guide_markdown_for_man(input).unwrap(); + let output = adapt_markdown(input).unwrap(); assert!(output.contains("bootc — containers/storage")); assert!(output.contains("Mode: `to-disk`; Method: UUID")); assert!(!output.contains("```mermaid")); assert_eq!( - adapt_guide_markdown_for_man(&input.replace("```", "~~~~")).unwrap(), + adapt_markdown(&input.replace("```", "~~~~")).unwrap(), output ); assert_eq!( - adapt_guide_markdown_for_man(&input.replace("|---|---|", "| :--- | ---: |")).unwrap(), + adapt_markdown(&input.replace("|---|---|", "| :--- | ---: |")).unwrap(), output ); let example = "````markdown\n```mermaid\nnot a real diagram\n```\n````\n"; - assert_eq!(adapt_guide_markdown_for_man(example).unwrap(), example); + assert_eq!(adapt_markdown(example).unwrap(), example); for bad in [ "```mermaid\nsequenceDiagram\nA->>B: hello\n```\n", "```mermaid\nflowchart TD\nA --> B\n```\n", "```mermaid\nflowchart TD\nA --- B\n", ] { - assert!(adapt_guide_markdown_for_man(bad).is_err(), "{bad}"); - } - } - - #[test] - fn reference_index_keeps_wrapped_name_paragraphs() { - assert_eq!( - reference_description( - "\nbootc-install - Install to an externally\ncreated filesystem\n\n# OPTIONS\n" - ), - "Install to an externally created filesystem" - ); - assert_eq!( - reference_description("bootc-config.toml\n\n# DESCRIPTION\n"), - "bootc-config.toml" - ); - } - - #[test] - fn stale_guides_removed_without_touching_references() { - let tmp = tempfile::tempdir().unwrap(); - let dir = Utf8Path::from_path(tmp.path()).unwrap(); - for name in ["bootc-current.7", "bootc-stale.7", "bootc.8", "other.7"] { - fs::write(dir.join(name), "test").unwrap(); + assert!(adapt_markdown(bad).is_err(), "{bad}"); } - prune_stale_guides(dir, &BTreeSet::from(["bootc-current.7".into()])).unwrap(); - assert!(dir.join("bootc-current.7").exists()); - assert!(!dir.join("bootc-stale.7").exists()); - assert!(dir.join("bootc.8").exists()); - assert!(dir.join("other.7").exists()); } } diff --git a/docs/book.toml b/docs/book.toml index 9e3eb4e067..17ad2db3f3 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -16,3 +16,44 @@ footers = [ [output.html] additional-js = ["mermaid.min.js", "mermaid-init.js"] + +# Preserve published URLs when canonical chapters adopt manual filenames. +[output.html.redirect] +"boot-failure-detection.html" = "bootc-boot-failure-detection.7.html" +"bootc-images.html" = "bootc-compatible-images.7.html" +"bootc-in-container.html" = "bootc-in-container.7.html" +"bootc-install.html" = "bootc-installation.7.html" +"bootc-via-api.html" = "bootc-api.7.html" +"booting-local-builds.html" = "bootc-local-builds.7.html" +"bootloaders.html" = "bootc-bootloaders.7.html" +"building/bootc-runtime.html" = "bootc-container-runtime.7.html" +"building/dns.html" = "bootc-dns.7.html" +"building/guidance.html" = "bootc-building-images.7.html" +"building/kernel-arguments.html" = "bootc-kernel-arguments.7.html" +"building/management-services.html" = "bootc-management-services.7.html" +"building/secrets.html" = "bootc-secrets.7.html" +"building/users-and-groups.html" = "bootc-users-and-groups.7.html" +"experimental-bootc-image.html" = "bootc-experimental-image.7.html" +"experimental-composefs.html" = "bootc-experimental-composefs.7.html" +"experimental-container-export.html" = "bootc-experimental-container-export.7.html" +"experimental-fsck.html" = "bootc-experimental-fsck.7.html" +"experimental-install-reset.html" = "bootc-experimental-install-reset.7.html" +"experimental-progress-fd.html" = "bootc-experimental-progress-fd.7.html" +"experimental-unified-storage.html" = "bootc-experimental-unified-storage.7.html" +"filesystem-encryption.html" = "bootc-filesystem-encryption.7.html" +"filesystem-storage.html" = "bootc-container-storage.7.html" +"filesystem-sysroot.html" = "bootc-sysroot.7.html" +"filesystem.html" = "bootc-filesystem.7.html" +"initramfs.html" = "bootc-initramfs.7.html" +"installation.html" = "bootc-base-images.7.html" +"internals.html" = "bootc-internals.7.html" +"intro.html" = "bootc-overview.7.html" +"logically-bound-images.html" = "bootc-logically-bound-images.7.html" +"package-managers.html" = "bootc-package-managers.7.html" +"packaging-and-integration.html" = "bootc-packaging-and-integration.7.html" +"registries-and-offline.html" = "bootc-registries-and-offline.7.html" +"relationship-oci-artifacts.html" = "bootc-oci-artifacts.7.html" +"relationship-particles.html" = "bootc-systemd-particles.7.html" +"relationships.html" = "bootc-relationships.7.html" +"security.html" = "bootc-security.7.html" +"upgrades.html" = "bootc-upgrades.7.html" diff --git a/docs/manpages.toml b/docs/manpages.toml deleted file mode 100644 index 8c69911f8e..0000000000 --- a/docs/manpages.toml +++ /dev/null @@ -1,41 +0,0 @@ -# Canonical chapter -> installed section 7 manual name. -# Ordering and grouping come only from src/SUMMARY.md; prose stays in src/. -[guides] -"boot-failure-detection.md" = "bootc-boot-failure-detection" -"bootc-images.md" = "bootc-compatible-images" -"bootc-in-container.md" = "bootc-in-container" -"bootc-install.md" = "bootc-installation" -"bootc-via-api.md" = "bootc-api" -"booting-local-builds.md" = "bootc-local-builds" -"bootloaders.md" = "bootc-bootloaders" -"building/bootc-runtime.md" = "bootc-container-runtime" -"building/dns.md" = "bootc-dns" -"building/guidance.md" = "bootc-building-images" -"building/kernel-arguments.md" = "bootc-kernel-arguments" -"building/management-services.md" = "bootc-management-services" -"building/secrets.md" = "bootc-secrets" -"building/users-and-groups.md" = "bootc-users-and-groups" -"experimental-bootc-image.md" = "bootc-experimental-image" -"experimental-composefs.md" = "bootc-experimental-composefs" -"experimental-container-export.md" = "bootc-experimental-container-export" -"experimental-fsck.md" = "bootc-experimental-fsck" -"experimental-install-reset.md" = "bootc-experimental-install-reset" -"experimental-progress-fd.md" = "bootc-experimental-progress-fd" -"experimental-unified-storage.md" = "bootc-experimental-unified-storage" -"filesystem-encryption.md" = "bootc-filesystem-encryption" -"filesystem-storage.md" = "bootc-container-storage" -"filesystem-sysroot.md" = "bootc-sysroot" -"filesystem.md" = "bootc-filesystem" -"initramfs.md" = "bootc-initramfs" -"installation.md" = "bootc-base-images" -"internals.md" = "bootc-internals" -"intro.md" = "bootc-overview" -"logically-bound-images.md" = "bootc-logically-bound-images" -"package-managers.md" = "bootc-package-managers" -"packaging-and-integration.md" = "bootc-packaging-and-integration" -"registries-and-offline.md" = "bootc-registries-and-offline" -"relationship-oci-artifacts.md" = "bootc-oci-artifacts" -"relationship-particles.md" = "bootc-systemd-particles" -"relationships.md" = "bootc-relationships" -"security.md" = "bootc-security" -"upgrades.md" = "bootc-upgrades" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 7fcd8f9eb0..e6cda5f256 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -1,30 +1,30 @@ # Summary -- [Introduction](intro.md) +- [Introduction](bootc-overview.7.md) # Installation -- [Installation](installation.md) +- [Installation](bootc-base-images.7.md) # Building images -- [Building images](building/guidance.md) -- [Container runtime vs bootc runtime](building/bootc-runtime.md) -- [DNS and resolv.conf](building/dns.md) -- [Users, groups, SSH keys](building/users-and-groups.md) +- [Building images](building/bootc-building-images.7.md) +- [Container runtime vs bootc runtime](building/bootc-container-runtime.7.md) +- [DNS and resolv.conf](building/bootc-dns.7.md) +- [Users, groups, SSH keys](building/bootc-users-and-groups.7.md) - [`man bootc-sysusers-shadow-sync.service`](man/bootc-sysusers-shadow-sync.service.5.md) -- [Kernel arguments](building/kernel-arguments.md) -- [Secrets](building/secrets.md) -- [Management Services](building/management-services.md) +- [Kernel arguments](building/bootc-kernel-arguments.7.md) +- [Secrets](building/bootc-secrets.7.md) +- [Management Services](building/bootc-management-services.7.md) # Using bootc -- [Upgrade and rollback](upgrades.md) -- [Boot failure detection](boot-failure-detection.md) -- [Accessing registries and offline updates](registries-and-offline.md) -- [Logically bound images](logically-bound-images.md) -- [Booting local builds](booting-local-builds.md) -- [Managing the initramfs after installation](initramfs.md) +- [Upgrade and rollback](bootc-upgrades.7.md) +- [Boot failure detection](bootc-boot-failure-detection.7.md) +- [Accessing registries and offline updates](bootc-registries-and-offline.7.md) +- [Logically bound images](bootc-logically-bound-images.7.md) +- [Booting local builds](bootc-local-builds.7.md) +- [Managing the initramfs after installation](bootc-initramfs.7.md) - [`man bootc`](man/bootc.8.md) - [`man bootc-config`](man/bootc-config.5.md) - [`man bootc-config-diff`](man/bootc-config-diff.8.md) @@ -37,11 +37,11 @@ - [`man bootc-fetch-apply-updates.service`](man/bootc-fetch-apply-updates.service.5.md) - [`man bootc-status-updated.path`](man/bootc-status-updated.path.5.md) - [`man bootc-status-updated.target`](man/bootc-status-updated.target.5.md) -- [Controlling bootc via API](bootc-via-api.md) +- [Controlling bootc via API](bootc-api.7.md) # Using `bootc install` -- [Understanding `bootc install`](bootc-install.md) +- [Understanding `bootc install`](bootc-installation.7.md) - [`man bootc-install`](man/bootc-install.8.md) - [`man bootc-install-config`](man/bootc-install-config.5.md) - [`man bootc-install-to-disk`](man/bootc-install-to-disk.8.md) @@ -56,7 +56,7 @@ # Bootc usage in containers -- [Read-only when in a default container](bootc-in-container.md) +- [Read-only when in a default container](bootc-in-container.7.md) - [`man bootc-container`](man/bootc-container.8.md) - [`man bootc-container-inspect`](man/bootc-container-inspect.8.md) - [`man bootc-container-split-kernel-and-rootfs`](man/bootc-container-split-kernel-and-rootfs.8.md) @@ -65,40 +65,40 @@ # Architecture -- [Image layout](bootc-images.md) -- [Filesystem](filesystem.md) -- [Filesystem: sysroot](filesystem-sysroot.md) -- [Container storage](filesystem-storage.md) -- [Bootloader](bootloaders.md) +- [Image layout](bootc-compatible-images.7.md) +- [Filesystem](bootc-filesystem.7.md) +- [Filesystem: sysroot](bootc-sysroot.7.md) +- [Container storage](bootc-container-storage.7.md) +- [Bootloader](bootc-bootloaders.7.md) - [`man bootc-loader-entries`](man/bootc-loader-entries.8.md) - [`man bootc-loader-entries-set-options-for-source`](man/bootc-loader-entries-set-options-for-source.8.md) -- [Disk encryption (e.g. LUKS)](filesystem-encryption.md) +- [Disk encryption (e.g. LUKS)](bootc-filesystem-encryption.7.md) # Security -- [Security and threat model](security.md) +- [Security and threat model](bootc-security.7.md) # Experimental features -- [bootc image](experimental-bootc-image.md) -- [composefs backend](experimental-composefs.md) +- [bootc image](bootc-experimental-image.7.md) +- [composefs backend](bootc-experimental-composefs.7.md) - [`man bootc-composefs-finalize-staged`](man/bootc-composefs-finalize-staged.8.md) -- [unified storage](experimental-unified-storage.md) +- [unified storage](bootc-experimental-unified-storage.7.md) - [`man bootc-root-setup.service`](man/bootc-root-setup.service.5.md) - [`man bootc-setup-root-conf.toml`](man/bootc-setup-root-conf.5.md) -- [fsck](experimental-fsck.md) -- [install reset](experimental-install-reset.md) -- [--progress-fd](experimental-progress-fd.md) -- [container export](experimental-container-export.md) +- [fsck](bootc-experimental-fsck.7.md) +- [install reset](bootc-experimental-install-reset.7.md) +- [--progress-fd](bootc-experimental-progress-fd.7.md) +- [container export](bootc-experimental-container-export.7.md) # More information -- [Packaging and integration](packaging-and-integration.md) -- [Package manager integration](package-managers.md) -- [Relationship with other projects](relationships.md) -- [Relationship with OCI artifacts](relationship-oci-artifacts.md) -- [Relationship with systemd "particles"](relationship-particles.md) +- [Packaging and integration](bootc-packaging-and-integration.7.md) +- [Package manager integration](bootc-package-managers.7.md) +- [Relationship with other projects](bootc-relationships.7.md) +- [Relationship with OCI artifacts](bootc-oci-artifacts.7.md) +- [Relationship with systemd "particles"](bootc-systemd-particles.7.md) # Development -- [Internals (rustdoc)](internals.md) +- [Internals (rustdoc)](bootc-internals.7.md) diff --git a/docs/src/bootc-via-api.md b/docs/src/bootc-api.7.md similarity index 100% rename from docs/src/bootc-via-api.md rename to docs/src/bootc-api.7.md diff --git a/docs/src/installation.md b/docs/src/bootc-base-images.7.md similarity index 89% rename from docs/src/installation.md rename to docs/src/bootc-base-images.7.md index 9228581051..85d951a103 100644 --- a/docs/src/installation.md +++ b/docs/src/bootc-base-images.7.md @@ -14,7 +14,7 @@ There are some overlaps between `bootc` and `ignition` and `zincati` however; se [this pull request](https://github.com/coreos/fedora-coreos-docs/pull/540) for more information. For other derivatives such as the ["Atomic desktops"](https://gitlab.com/fedora/ostree), see -discussion of [relationships](relationships.md) which particularly covers interactions with rpm-ostree. +discussion of [relationships](bootc-relationships.7.md) which particularly covers interactions with rpm-ostree. ## Other diff --git a/docs/src/boot-failure-detection.md b/docs/src/bootc-boot-failure-detection.7.md similarity index 100% rename from docs/src/boot-failure-detection.md rename to docs/src/bootc-boot-failure-detection.7.md diff --git a/docs/src/bootloaders.md b/docs/src/bootc-bootloaders.7.md similarity index 100% rename from docs/src/bootloaders.md rename to docs/src/bootc-bootloaders.7.md diff --git a/docs/src/bootc-images.md b/docs/src/bootc-compatible-images.7.md similarity index 98% rename from docs/src/bootc-images.md rename to docs/src/bootc-compatible-images.7.md index be6c52b7c4..b98705df46 100644 --- a/docs/src/bootc-images.md +++ b/docs/src/bootc-compatible-images.7.md @@ -31,7 +31,7 @@ This is [a bug](https://github.com/bootc-dev/bootc/issues/2256): currently a `/o ## composefs backend -There are no strict additional basic filesystem/layout requirements for images which plan to deploy with composefs. However, see also [bootloaders](bootloaders.md). +There are no strict additional basic filesystem/layout requirements for images which plan to deploy with composefs. However, see also [bootloaders](bootc-bootloaders.7.md). ## ostree backend diff --git a/docs/src/filesystem-storage.md b/docs/src/bootc-container-storage.7.md similarity index 94% rename from docs/src/filesystem-storage.md rename to docs/src/bootc-container-storage.7.md index 316cc293e5..4f703c8bd1 100644 --- a/docs/src/filesystem-storage.md +++ b/docs/src/bootc-container-storage.7.md @@ -4,7 +4,7 @@ The bootc project uses [ostree](https://github.com/ostreedev/ostree/) and specif the [ostree-rs-ext](https://github.com/ostreedev/ostree-rs-ext/) Rust library which handles storage of container images on top of an ostree-based system for the booted host, and additionally there is a -[containers/storage](https://github.com/containers/storage) instance for [logically bound images](logically-bound-images.md). +[containers/storage](https://github.com/containers/storage) instance for [logically bound images](bootc-logically-bound-images.7.md). ## Architecture @@ -62,7 +62,7 @@ This is implemented in the [ostree-rs-ext/container module](https://docs.rs/ostr ### SELinux labeling -See the SELinux section of [Image layout](bootc-images.md). +See the SELinux section of [Image layout](bootc-compatible-images.7.md). ### Origin files @@ -85,4 +85,4 @@ This is what is referenced by the `ostree=` kernel commandline. ## Logically bound images -In addition to the base image, bootc supports [logically bound images](logically-bound-images.md). +In addition to the base image, bootc supports [logically bound images](bootc-logically-bound-images.7.md). diff --git a/docs/src/experimental-composefs.md b/docs/src/bootc-experimental-composefs.7.md similarity index 96% rename from docs/src/experimental-composefs.md rename to docs/src/bootc-experimental-composefs.7.md index 4218d016f4..2fe382f580 100644 --- a/docs/src/experimental-composefs.md +++ b/docs/src/bootc-experimental-composefs.7.md @@ -270,7 +270,7 @@ See [CONTRIBUTING.md](https://github.com/bootc-dev/bootc/blob/main/CONTRIBUTING. Whenever the container image has a UKI, bootc automatically selects the composefs backend during installation (see [Prerequisites](#prerequisites) above for the currently-supported UKI + systemd-boot configuration for building sealed images). Note that having a UKI does not by itself make an install sealed — that also depends on whether fs-verity enforcement is on, per [Overview](#overview) above. -Composefs installs using a traditional `vmlinuz`/`initramfs.img` layout instead of a UKI can enforce fs-verity, but are never sealed, since nothing authenticates the root digest. They can use either `bootupd` (GRUB) or systemd-boot, the same as the ostree backend. See [bootloaders.md](bootloaders.md) for the general bootloader selection rules. Under the hood, bootc writes standard BLS boot entries for both UKI and traditional kernels; see the [composefs boot module documentation](https://github.com/bootc-dev/bootc/blob/main/crates/lib/src/bootc_composefs/boot.rs) for details on how entry filenames and sort-keys are chosen to sort correctly on both GRUB and systemd-boot. +Composefs installs using a traditional `vmlinuz`/`initramfs.img` layout instead of a UKI can enforce fs-verity, but are never sealed, since nothing authenticates the root digest. They can use either `bootupd` (GRUB) or systemd-boot, the same as the ostree backend. See [bootloaders.md](bootc-bootloaders.7.md) for the general bootloader selection rules. Under the hood, bootc writes standard BLS boot entries for both UKI and traditional kernels; see the [composefs boot module documentation](https://github.com/bootc-dev/bootc/blob/main/crates/lib/src/bootc_composefs/boot.rs) for details on how entry filenames and sort-keys are chosen to sort correctly on both GRUB and systemd-boot. ## Installation @@ -305,8 +305,8 @@ The composefs backend is experimental; on-disk formats are subject to change. ## Additional Resources -- See [filesystem.md](filesystem.md) for information about composefs in the standard ostree backend -- See [bootloaders.md](bootloaders.md) for bootloader configuration details +- See [filesystem.md](bootc-filesystem.7.md) for information about composefs in the standard ostree backend +- See [bootloaders.md](bootc-bootloaders.7.md) for bootloader configuration details - [composefs-rs](https://github.com/composefs/composefs-rs) - The underlying composefs implementation - [composefs-rs repository format](https://github.com/composefs/composefs-rs/blob/main/crates/composefs/src/repository_format.rs) - Detailed on-disk layout of the `/composefs` repository - [Unified Kernel Images specification](https://uapi-group.org/specifications/specs/unified_kernel_image/) diff --git a/docs/src/experimental-container-export.md b/docs/src/bootc-experimental-container-export.7.md similarity index 100% rename from docs/src/experimental-container-export.md rename to docs/src/bootc-experimental-container-export.7.md diff --git a/docs/src/experimental-fsck.md b/docs/src/bootc-experimental-fsck.7.md similarity index 100% rename from docs/src/experimental-fsck.md rename to docs/src/bootc-experimental-fsck.7.md diff --git a/docs/src/experimental-bootc-image.md b/docs/src/bootc-experimental-image.7.md similarity index 96% rename from docs/src/experimental-bootc-image.md rename to docs/src/bootc-experimental-image.7.md index c35e385b32..e86a7c8e39 100644 --- a/docs/src/experimental-bootc-image.md +++ b/docs/src/bootc-experimental-image.7.md @@ -7,7 +7,7 @@ Tracking issue: ## Using `bootc image copy-to-storage` -This experimental command is intended to aid in [booting local builds](booting-local-builds.md). +This experimental command is intended to aid in [booting local builds](bootc-local-builds.7.md). Invoking this command will default to copying the booted container image into the `containers-storage:` area as used by e.g. `podman`, under the image tag `localhost/bootc` by default. It can diff --git a/docs/src/experimental-install-reset.md b/docs/src/bootc-experimental-install-reset.7.md similarity index 100% rename from docs/src/experimental-install-reset.md rename to docs/src/bootc-experimental-install-reset.7.md diff --git a/docs/src/experimental-progress-fd.md b/docs/src/bootc-experimental-progress-fd.7.md similarity index 100% rename from docs/src/experimental-progress-fd.md rename to docs/src/bootc-experimental-progress-fd.7.md diff --git a/docs/src/experimental-unified-storage.md b/docs/src/bootc-experimental-unified-storage.7.md similarity index 96% rename from docs/src/experimental-unified-storage.md rename to docs/src/bootc-experimental-unified-storage.7.md index 22fabdef1b..c9f0608453 100644 --- a/docs/src/experimental-unified-storage.md +++ b/docs/src/bootc-experimental-unified-storage.7.md @@ -10,7 +10,7 @@ Tracking issue: Unified storage is the goal of having all storage for bootc be "unified" with the storage used by a container runtime, such as podman. -Currently, bootc uses either ostree or composefs. [Logically bound images](logically-bound-images.md) +Currently, bootc uses either ostree or composefs. [Logically bound images](bootc-logically-bound-images.7.md) use the podman container storage. ## Goals @@ -133,7 +133,7 @@ podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage run localhost/b ## Relationship to composefs backend -Unified storage is complementary to the [composefs backend](experimental-composefs.md). +Unified storage is complementary to the [composefs backend](bootc-experimental-composefs.7.md). While unified storage changes *how images are pulled* (using containers/storage), the composefs backend changes *how the filesystem is stored and verified*. @@ -180,7 +180,7 @@ unified storage model is documented in the rustdoc comments of the relevant sour - **Progress reporting**: Pull progress from podman is not yet integrated with bootc's progress reporting - **Garbage collection**: Images in bootc storage are garbage collected based - on deployment references; see [logically-bound-images.md](logically-bound-images.md) + on deployment references; see [logically-bound-images.md](bootc-logically-bound-images.7.md) for details ## Related issues diff --git a/docs/src/filesystem-encryption.md b/docs/src/bootc-filesystem-encryption.7.md similarity index 97% rename from docs/src/filesystem-encryption.md rename to docs/src/bootc-filesystem-encryption.7.md index 32b86d8e31..7d471a94ab 100644 --- a/docs/src/filesystem-encryption.md +++ b/docs/src/bootc-filesystem-encryption.7.md @@ -39,7 +39,7 @@ example `tpm2-luks` requires one, since GRUB and most bootloaders can't read a LUKS-encrypted `/boot`. If yours does, pass `--boot-mount-spec` to `to-filesystem` so bootc knows where to install boot assets. See -[More advanced installation with `to-filesystem`](bootc-install.md#more-advanced-installation-with-to-filesystem). +[More advanced installation with `to-filesystem`](bootc-installation.7.md#more-advanced-installation-with-to-filesystem). ### Root filesystem discovery diff --git a/docs/src/filesystem.md b/docs/src/bootc-filesystem.7.md similarity index 96% rename from docs/src/filesystem.md rename to docs/src/bootc-filesystem.7.md index b0d693d2f6..f916263c75 100644 --- a/docs/src/filesystem.md +++ b/docs/src/bootc-filesystem.7.md @@ -2,7 +2,7 @@ As noted in other chapters, the bootc project currently depends on the [ostree project](https://github.com/ostreedev/ostree/) -for storing the base container image. Additionally there is a [containers/storage](https://github.com/containers/storage) instance for [logically bound images](logically-bound-images.md). +for storing the base container image. Additionally there is a [containers/storage](https://github.com/containers/storage) instance for [logically bound images](bootc-logically-bound-images.7.md). However, bootc is intending to be a "fresh, new container-native interface", and ostree is an implementation detail. @@ -23,7 +23,7 @@ is very important for achieving correct semantics. When run *as a container* (e.g. as part of a container build), the filesystem is fully mutable in order to allow derivation to work. -For more on container builds, see [build guidance](building/guidance.md). +For more on container builds, see [build guidance](building/bootc-building-images.7.md). The rest of this document describes the state of the system when "deployed" to a physical or virtual machine, and managed by `bootc`. @@ -38,7 +38,7 @@ For more information, see [this tracker issue](https://github.com/bootc-dev/boot When the system is fully booted, it is into the equivalent of a `chroot`. The "physical" host root filesystem will be mounted at `/sysroot`. -For more on this, see [filesystem: sysroot](filesystem-sysroot.md). +For more on this, see [filesystem: sysroot](bootc-sysroot.7.md). This `chroot` filesystem is called a "deployment root". All the remaining filesystem paths below are part of a deployment root which is used as a @@ -98,7 +98,7 @@ influenced by the initial image version. This can lead to problems where e.g. a change to `/etc/sudoers` (to give one simple example) would require external intervention to apply. -For more on configuration file best practices, see [Building](building/guidance.md). +For more on configuration file best practices, see [Building](building/bootc-building-images.7.md). To emphasize again, it's recommended to enable `etc.transient` if possible, though when using that you may need to store some machine-specific state in e.g. the @@ -145,13 +145,13 @@ particular OS or distribution. Consult your OS/distribution documentation for guidance on confext and sysext. For per-host configuration, use the patterns described in -[Building images: Configuration](building/guidance.md#configuration) instead: +[Building images: Configuration](building/bootc-building-images.7.md#configuration) instead: image-embedded configuration (prefer `/usr` where possible), persistent `/etc` with day-2 configuration management tools, or machine-local kernel arguments via `rpm-ostree kargs` or `/usr/lib/bootc/kargs.d`. For more on the design rationale, see -[Relationship with systemd "particles"](relationship-particles.md). +[Relationship with systemd "particles"](bootc-systemd-particles.7.md). ## `/var` @@ -208,7 +208,7 @@ other toplevels such as `/usr`. Some software (especially "3rd party" deb/rpm packages) expect to be able to write to a subdirectory of `/opt` such as `/opt/examplepkg`. -See [building images](building/guidance.md) for recommendations on how to build +See [building images](building/bootc-building-images.7.md) for recommendations on how to build container images and adjust the filesystem for cases like this. However, for some use cases, it may be easier to allow some level of mutability. @@ -375,7 +375,7 @@ Both transient root and state overlays above provide ways for packages that install in `/opt` to operate. However, for maximum immutability the best approach is simply to symlink just the parts of the `/opt` needed into `/var`. See the section on `/opt` in [Image building and configuration -guidance](building/guidance.md) for a more concrete example. +guidance](building/bootc-building-images.7.md) for a more concrete example. ## Increased filesystem integrity with fsverity @@ -412,7 +412,7 @@ want a "transient etc" model. #### Does not apply to logically bound images -The [logically bound images](logically-bound-images.md) store is currently +The [logically bound images](bootc-logically-bound-images.7.md) store is currently implemented using a separate mechanism and configuring fsverity for the bootc storage has no effect on it. diff --git a/docs/src/bootc-in-container.md b/docs/src/bootc-in-container.7.md similarity index 100% rename from docs/src/bootc-in-container.md rename to docs/src/bootc-in-container.7.md diff --git a/docs/src/initramfs.md b/docs/src/bootc-initramfs.7.md similarity index 97% rename from docs/src/initramfs.md rename to docs/src/bootc-initramfs.7.md index a03772edbc..01eddb6e61 100644 --- a/docs/src/initramfs.md +++ b/docs/src/bootc-initramfs.7.md @@ -69,7 +69,7 @@ exact dracut modules and arguments depend on the base image and the content being added. Build the image locally and deploy it exactly as any other local build; see -[Booting local builds](booting-local-builds.md) for building against the booted +[Booting local builds](bootc-local-builds.7.md) for building against the booted image, the `containers-storage` transport, and automating the rebuild. Rebuild and switch to this derived image whenever either the base image or the machine-specific configuration changes. @@ -93,7 +93,7 @@ well, so inspect the pending changes before running it. Therefore, do not use `rpm-ostree initramfs --enable` for this workflow if the system is intended to continue receiving updates through bootc; use a derived container image instead. See also -[Relationship with rpm-ostree](relationships.md#relationship-with-rpm-ostree). +[Relationship with rpm-ostree](bootc-relationships.7.md#relationship-with-rpm-ostree). ## Future direction diff --git a/docs/src/bootc-install.md b/docs/src/bootc-installation.7.md similarity index 99% rename from docs/src/bootc-install.md rename to docs/src/bootc-installation.7.md index f8247fcb76..e5a051542c 100644 --- a/docs/src/bootc-install.md +++ b/docs/src/bootc-installation.7.md @@ -124,7 +124,7 @@ The bootc project aims to support generic/general-purpose operating systems and distributions that will ship unconfigured images. An unconfigured image does not have a default password or SSH key, etc. -For more information, see [Image building and configuration guidance](building/guidance.md). +For more information, see [Image building and configuration guidance](building/bootc-building-images.7.md). ## More advanced installation with `to-filesystem` @@ -140,7 +140,7 @@ The `bootc install to-disk` command is effectively: There may be a bit more involved here; for example configuring `--block-setup tpm2-luks` will configure the root filesystem with LUKS bound to the TPM2 chip, currently via [systemd-cryptenroll](https://www.freedesktop.org/software/systemd/man/systemd-cryptenroll.html#). -**We don't recommend this for new deployments**; see [Disk encryption (e.g. LUKS)](filesystem-encryption.md) +**We don't recommend this for new deployments**; see [Disk encryption (e.g. LUKS)](bootc-filesystem-encryption.7.md) for why, and for the recommended approach of setting up encryption independently of bootc via `systemd-cryptsetup` or Ignition. @@ -438,7 +438,7 @@ If you're building tooling that uses `bootc install to-filesystem`, you should: On a bootc system, the "physical root" is different from the "logical root" of the booted container. For more on -that, see [filesystem](filesystem.md). This section +that, see [filesystem](bootc-filesystem.7.md). This section is about how the physical root filesystem is discovered. Systems using systemd will often default to using @@ -468,7 +468,7 @@ for legacy `/etc/fstab` references for `/` to use ## Configuring machine-local state -Per the [filesystem](filesystem.md) section, `/etc` and `/var` are +Per the [filesystem](bootc-filesystem.7.md) section, `/etc` and `/var` are machine-local state by default. To inject additional content after installation, use `bootc install mount --sysroot /path/to/target --latest /mnt/installed` and mutate `/mnt/installed/etc` or `/mnt/installed/var`. This is the diff --git a/docs/src/internals.md b/docs/src/bootc-internals.7.md similarity index 98% rename from docs/src/internals.md rename to docs/src/bootc-internals.7.md index aa0f24d7ae..f3ed61e46c 100644 --- a/docs/src/internals.md +++ b/docs/src/bootc-internals.7.md @@ -85,7 +85,7 @@ part of the admin experience. The `podstorage` module implements bootc's own `containers-storage:` instance at `/sysroot/ostree/bootc/storage/` (symlinked to `/usr/lib/bootc/storage/`). -This supports [Logically Bound Images](logically-bound-images.md) with proper +This supports [Logically Bound Images](bootc-logically-bound-images.7.md) with proper lifecycle management and garbage collection tied to deployments. ## Rustdoc API Documentation diff --git a/docs/src/booting-local-builds.md b/docs/src/bootc-local-builds.7.md similarity index 100% rename from docs/src/booting-local-builds.md rename to docs/src/bootc-local-builds.7.md diff --git a/docs/src/logically-bound-images.md b/docs/src/bootc-logically-bound-images.7.md similarity index 98% rename from docs/src/logically-bound-images.md rename to docs/src/bootc-logically-bound-images.7.md index 4ddb3cd305..b6d41e7a49 100644 --- a/docs/src/logically-bound-images.md +++ b/docs/src/bootc-logically-bound-images.7.md @@ -61,7 +61,7 @@ by a file in `/usr/lib/bootc/bound-images.d`. ## Installation Logically bound images must be present in the default container store (`/var/lib/containers`) when invoking -[bootc install](bootc-install.md); the images will be copied into the target system and present +[bootc install](bootc-installation.7.md); the images will be copied into the target system and present directly at boot, alongside the bootc base image. ## Limitations diff --git a/docs/src/relationship-oci-artifacts.md b/docs/src/bootc-oci-artifacts.7.md similarity index 100% rename from docs/src/relationship-oci-artifacts.md rename to docs/src/bootc-oci-artifacts.7.md diff --git a/docs/src/intro.md b/docs/src/bootc-overview.7.md similarity index 100% rename from docs/src/intro.md rename to docs/src/bootc-overview.7.md diff --git a/docs/src/package-managers.md b/docs/src/bootc-package-managers.7.md similarity index 96% rename from docs/src/package-managers.md rename to docs/src/bootc-package-managers.7.md index 98639d37fd..7ada6554fa 100644 --- a/docs/src/package-managers.md +++ b/docs/src/bootc-package-managers.7.md @@ -99,7 +99,7 @@ situation and inform the user that the changes will be ephemeral. ## Persistent changes A bootc system by default *does* have a writable, persistent data store that holds -multiple container image versions (more in [filesystem](filesystem.md)). +multiple container image versions (more in [filesystem](bootc-filesystem.7.md)). Systems such as [rpm-ostree](https://github.com/coreos/rpm-ostree/) implement a "hybrid" mechanism where packages can be persistently layered and re-applied; @@ -108,5 +108,5 @@ the system effectively does a "local build", unioning the intermediate filesyste One aspect of how rpm-ostree implements this is by caching individual unpacked RPMs as ostree commits in the ostree repo. -This section will be expanded later; you may also be able to find more information in [booting local builds](booting-local-builds.md). +This section will be expanded later; you may also be able to find more information in [booting local builds](bootc-local-builds.7.md). diff --git a/docs/src/packaging-and-integration.md b/docs/src/bootc-packaging-and-integration.7.md similarity index 98% rename from docs/src/packaging-and-integration.md rename to docs/src/bootc-packaging-and-integration.7.md index 0ab936c549..1056f25d7e 100644 --- a/docs/src/packaging-and-integration.md +++ b/docs/src/bootc-packaging-and-integration.7.md @@ -121,7 +121,7 @@ This installs: ## Base image content Alongside building the binary here, you may also want to prepare -a base image. For that, see [bootc-images](bootc-images.md). +a base image. For that, see [bootc-images](bootc-compatible-images.7.md). ## Additional Resources diff --git a/docs/src/registries-and-offline.md b/docs/src/bootc-registries-and-offline.7.md similarity index 100% rename from docs/src/registries-and-offline.md rename to docs/src/bootc-registries-and-offline.7.md diff --git a/docs/src/relationships.md b/docs/src/bootc-relationships.7.md similarity index 98% rename from docs/src/relationships.md rename to docs/src/bootc-relationships.7.md index aaa2774617..8cc60c2345 100644 --- a/docs/src/relationships.md +++ b/docs/src/bootc-relationships.7.md @@ -65,7 +65,7 @@ rpm-ostree. Hence, when using a container source, `rpm-ostree upgrade` and - The ostree project never tried to have an opinionated "install" mechanism, but bootc does with `bootc install to-filesystem` - Bootc has additional features such as `/usr/lib/bootc/kargs.d` and - [logically bound images](logically-bound-images.md). + [logically bound images](bootc-logically-bound-images.7.md). ### Client side changes diff --git a/docs/src/security.md b/docs/src/bootc-security.7.md similarity index 100% rename from docs/src/security.md rename to docs/src/bootc-security.7.md diff --git a/docs/src/filesystem-sysroot.md b/docs/src/bootc-sysroot.7.md similarity index 95% rename from docs/src/filesystem-sysroot.md rename to docs/src/bootc-sysroot.7.md index e6d5c6d886..75acc754f1 100644 --- a/docs/src/filesystem-sysroot.md +++ b/docs/src/bootc-sysroot.7.md @@ -29,7 +29,7 @@ operate on the physical root. ### bootc-owned container storage -For [logically bound images](logically-bound-images.md), +For [logically bound images](bootc-logically-bound-images.7.md), bootc maintains a dedicated [containers/storage](https://github.com/containers/storage) instance using the `overlay` backend (the same type of thing that backs `/var/lib/containers`). @@ -75,4 +75,4 @@ is recommended, along with an `ExecStartPre=mount -o remount,rw /sysroot`. ### Detecting bootc/ostree systems -See the [package managers](package-managers.md) section on "Detecting image based systems". +See the [package managers](bootc-package-managers.7.md) section on "Detecting image based systems". diff --git a/docs/src/relationship-particles.md b/docs/src/bootc-systemd-particles.7.md similarity index 100% rename from docs/src/relationship-particles.md rename to docs/src/bootc-systemd-particles.7.md diff --git a/docs/src/upgrades.md b/docs/src/bootc-upgrades.7.md similarity index 98% rename from docs/src/upgrades.md rename to docs/src/bootc-upgrades.7.md index b61fe13136..48eabce0d9 100644 --- a/docs/src/upgrades.md +++ b/docs/src/bootc-upgrades.7.md @@ -163,7 +163,7 @@ Without `--apply`, `--soft-reboot` prepares the deployment but does not restart the system immediately. Without `--soft-reboot`, `--apply` requests a regular reboot. -The [experimental composefs backend](experimental-composefs.md) currently +The [experimental composefs backend](bootc-experimental-composefs.7.md) currently differs: both modes fail if systemd lacks soft-reboot support. If the target deployment is not soft-reboot capable, `auto` leaves it staged without restarting, even with `--apply`; it does not automatically fall back to a diff --git a/docs/src/building/guidance.md b/docs/src/building/bootc-building-images.7.md similarity index 95% rename from docs/src/building/guidance.md rename to docs/src/building/bootc-building-images.7.md index 7a3df7947e..f37f8dadd7 100644 --- a/docs/src/building/guidance.md +++ b/docs/src/building/bootc-building-images.7.md @@ -27,7 +27,7 @@ images have all parts of the filesystem (e.g. `/usr` in particular) as fully mutable state, and writing there is encouraged (see below). When "deployed" to a physical or virtual machine, the container image -files are read-only by default; for more, see [filesystem](../filesystem.md). +files are read-only by default; for more, see [filesystem](../bootc-filesystem.7.md). ## Installing software @@ -90,7 +90,7 @@ on a package-based system. ## Users and groups Note that the above `postgresql` today will allocate a user; -this leads to the topic of [users, groups and SSH keys](users-and-groups.md). +this leads to the topic of [users, groups and SSH keys](bootc-users-and-groups.7.md). ## Configuration @@ -110,7 +110,7 @@ or `/etc`. systemd has long advocated and supported a model where `/usr` (e.g. `/usr/lib/systemd/system`) contains content owned by the operating system image. -`/etc` is machine-local state. However, per [filesystem.md](../filesystem.md) +`/etc` is machine-local state. However, per [filesystem.md](../bootc-filesystem.7.md) it's important to note that the underlying OSTree system performs a 3-way merge of `/etc`, so changes you make in the container image to e.g. `/etc/postgresql.conf` @@ -119,7 +119,7 @@ locally. Resolver configuration has an additional interaction with the container runtime used to build the image. See -[DNS and `/etc/resolv.conf`](dns.md) for the recommended approach. +[DNS and `/etc/resolv.conf`](bootc-dns.7.md) for the recommended approach. ### Prefer using drop-in directories @@ -149,7 +149,7 @@ for example, although this can run afoul of SELinux labeling. ### Secrets -There is a dedicated document for [secrets](secrets.md), +There is a dedicated document for [secrets](bootc-secrets.7.md), which is a special case of configuration. ## Handling read-only vs writable locations diff --git a/docs/src/building/bootc-runtime.md b/docs/src/building/bootc-container-runtime.7.md similarity index 97% rename from docs/src/building/bootc-runtime.md rename to docs/src/building/bootc-container-runtime.7.md index 84995df20c..2663fd7e40 100644 --- a/docs/src/building/bootc-runtime.md +++ b/docs/src/building/bootc-container-runtime.7.md @@ -17,7 +17,7 @@ Crucially, besides setting up some mounts, bootc itself does not act as any kind This distinction also applies to DNS configuration. A container runtime may inject `/etc/resolv.conf` while building or running a container, but it is not present to do so after a bootc image boots. See -[DNS and `/etc/resolv.conf`](dns.md). +[DNS and `/etc/resolv.conf`](bootc-dns.7.md). Another example of this: While one can add [Container configuration](https://github.com/opencontainers/image-spec/blob/main/config.md) metadata, `bootc` generally ignores that at runtime today. @@ -92,4 +92,4 @@ system is deployed. ## SELinux For more on the intersection of SELinux and current bootc (OSTree container) -images, see [bootc images - SELinux](../bootc-images.md#SELinux). +images, see [bootc images - SELinux](../bootc-compatible-images.7.md#SELinux). diff --git a/docs/src/building/dns.md b/docs/src/building/bootc-dns.7.md similarity index 98% rename from docs/src/building/dns.md rename to docs/src/building/bootc-dns.7.md index 9af6dc6823..8d8f243518 100644 --- a/docs/src/building/dns.md +++ b/docs/src/building/bootc-dns.7.md @@ -21,7 +21,7 @@ the image. When the image is installed and booted, there is no outer Podman or other container runtime to recreate these files. The network stack included in the image is responsible for generating the booted host's resolver configuration. -See [Container runtime vs bootc runtime](bootc-runtime.md) for more about this +See [Container runtime vs bootc runtime](bootc-container-runtime.7.md) for more about this distinction. In particular: @@ -159,7 +159,7 @@ Content that the *image* provides for `/etc/resolv.conf` should reach it through a symlink created by a `systemd-tmpfiles` rule, not as a regular file in the image: `/etc` is three-way merged across upgrades, and once a regular file there has been modified locally a later image default no longer takes -effect (see [Filesystem: `/etc`](../filesystem.md#etc)). The target depends on +effect (see [Filesystem: `/etc`](../bootc-filesystem.7.md#etc)). The target depends on the policy: - a file under `/run` when the resolver is dynamic -- this is what the diff --git a/docs/src/building/kernel-arguments.md b/docs/src/building/bootc-kernel-arguments.7.md similarity index 100% rename from docs/src/building/kernel-arguments.md rename to docs/src/building/bootc-kernel-arguments.7.md diff --git a/docs/src/building/management-services.md b/docs/src/building/bootc-management-services.7.md similarity index 100% rename from docs/src/building/management-services.md rename to docs/src/building/bootc-management-services.7.md diff --git a/docs/src/building/secrets.md b/docs/src/building/bootc-secrets.7.md similarity index 100% rename from docs/src/building/secrets.md rename to docs/src/building/bootc-secrets.7.md diff --git a/docs/src/building/users-and-groups.md b/docs/src/building/bootc-users-and-groups.7.md similarity index 99% rename from docs/src/building/users-and-groups.md rename to docs/src/building/bootc-users-and-groups.7.md index 3d7e68ba39..11b9bb40a9 100644 --- a/docs/src/building/users-and-groups.md +++ b/docs/src/building/bootc-users-and-groups.7.md @@ -190,7 +190,7 @@ change a system group's GID, see ### Machine-local state for users -At this point, it is important to understand the [filesystem](../filesystem.md) +At this point, it is important to understand the [filesystem](../bootc-filesystem.7.md) layout - the default is up to the base image. The default Linux concept of a user has data stored in both `/etc` (`/etc/passwd`, `/etc/shadow` and groups) diff --git a/docs/src/man/bootc-install-to-existing-root.8.md b/docs/src/man/bootc-install-to-existing-root.8.md index 168478fd8d..973c174f84 100644 --- a/docs/src/man/bootc-install-to-existing-root.8.md +++ b/docs/src/man/bootc-install-to-existing-root.8.md @@ -109,7 +109,7 @@ servers. Migrate the authoritative configuration, such as NetworkManager connection profiles or resolver drop-ins, into the configuration used by the new system. If both systems intentionally use a hand-managed static file, it may instead be selectively migrated. See -[DNS and `/etc/resolv.conf`](../building/dns.md) for details. +[DNS and `/etc/resolv.conf`](../building/bootc-dns.7.md) for details. **Note:** For filesystem mounts from `/etc/fstab` in the old system, consider using kernel arguments (via `systemd.mount-extra`) injected before reboot instead diff --git a/docs/src/man/bootc-install.8.md b/docs/src/man/bootc-install.8.md index eeb1203485..9b747f2f96 100644 --- a/docs/src/man/bootc-install.8.md +++ b/docs/src/man/bootc-install.8.md @@ -16,7 +16,7 @@ The `bootc install` flow turns a container image into a bootable system, including filesystem, bootloader, and update metadata setup. It is not simply a copy of the container filesystem. -See [Installing bootc compatible images](../bootc-install.md) for the +See [Installing bootc compatible images](../bootc-installation.7.md) for the installation model, prerequisites, and end-to-end examples. This reference documents the command and its subcommands. diff --git a/docs/src/man/bootc-switch.8.md b/docs/src/man/bootc-switch.8.md index 4ca52a169a..c32911e938 100644 --- a/docs/src/man/bootc-switch.8.md +++ b/docs/src/man/bootc-switch.8.md @@ -29,7 +29,7 @@ changed after switching to the new image. ## Soft Reboot For shared `--apply` and `--soft-reboot` behavior, see -[Soft reboots](../upgrades.md#soft-reboots). +[Soft reboots](../bootc-upgrades.7.md#soft-reboots). # OPTIONS diff --git a/docs/src/man/bootc-upgrade.8.md b/docs/src/man/bootc-upgrade.8.md index 4440326988..0a2face2bc 100644 --- a/docs/src/man/bootc-upgrade.8.md +++ b/docs/src/man/bootc-upgrade.8.md @@ -30,7 +30,7 @@ do *not* automatically apply the update in addition. ## Soft Reboot For shared `--apply` and `--soft-reboot` behavior, see -[Soft reboots](../upgrades.md#soft-reboots). +[Soft reboots](../bootc-upgrades.7.md#soft-reboots). # OPTIONS