diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c98849c..4bfb7f0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: # Because all the git clones are much less reliable export GOPROXY=https://proxy.golang.org go install github.com/onsi/ginkgo/v2/ginkgo@latest + go install github.com/cpuguy83/go-md2man@latest make GOOPTS=-buildvcs=false export PATH=$PATH:$HOME/go/bin make integration_tests diff --git a/.gitignore b/.gitignore index 95f150c9..e6cf732f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /bin test/e2e/e2e.test +docs/*.1 \ No newline at end of file diff --git a/Makefile b/Makefile index df455929..9aa65ba3 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,13 @@ binary_name = podman-bootc output_dir = bin build_tags = exclude_graphdriver_btrfs,btrfs_noversion,exclude_graphdriver_devicemapper,containers_image_openpgp,remote -all: out_dir +all: out_dir docs go build -tags $(build_tags) $(GOOPTS) -o $(output_dir)/$(binary_name) out_dir: mkdir -p $(output_dir) -lint: +lint: validate_docs golangci-lint --build-tags $(build_tags) run integration_tests: @@ -18,5 +18,15 @@ integration_tests: e2e_test: all ginkgo -tags $(build_tags) ./test/... +.PHONY: docs +docs: + make -C docs + clean: rm -f $(output_dir)/* + make -C docs clean + +.PHONY: validate_docs +validate_docs: + hack/man-page-checker + hack/xref-helpmsgs-manpages diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..c96ff8b4 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,39 @@ +PREFIX := /usr/local +DATADIR := ${PREFIX}/share +MANDIR := $(DATADIR)/man +GO ?= go +GOMD2MAN ?= go-md2man +ifeq ($(shell uname -s),FreeBSD) +SED=gsed +else +SED=sed +endif +# This must never include the 'hack' directory +export PATH := $(shell $(GO) env GOPATH)/bin:$(PATH) + +docs: $(patsubst %.md,%,$(wildcard *[1].md)) + +%.1: %.1.md +### sed is used to filter http/s links as well as relative links +### replaces "\" at the end of a line with two spaces +### this ensures that manpages are rendered correctly + @$(SED) -e 's/\((podman-bootc[^)]*\.md\(#.*\)\?)\)//g' \ + -e 's/\[\(podman-bootc[^]]*\)\]/\1/g' \ + -e 's/\[\([^]]*\)](http[^)]\+)/\1/g' \ + -e 's;<\(/\)\?\(a\|a\s\+[^>]*\|sup\)>;;g' \ + -e 's/\\$$/ /g' $< | \ + $(GOMD2MAN) -in /dev/stdin -out $@ + +.PHONY: install +install: docs + install -d ${DESTDIR}/${MANDIR}/man1 + install -m 0644 podman-bootc*.1 ${DESTDIR}/${MANDIR}/man1 + install -m 0644 links/podman-bootc*.1 ${DESTDIR}/${MANDIR}/man1 + +.PHONY: install-tools +install-tools: + go install github.com/cpuguy83/go-md2man@latest + +.PHONY: clean +clean: + $(RM) -f podman-bootc*.1 diff --git a/docs/podman-bootc-completion.1.md b/docs/podman-bootc-completion.1.md new file mode 100644 index 00000000..4ad24611 --- /dev/null +++ b/docs/podman-bootc-completion.1.md @@ -0,0 +1,26 @@ +% podman-bootc-completion 1 + +## NAME +podman-bootc-completion - Generate the autocompletion script for the specified shell + +## SYNOPSIS +**podman-bootc completion** *bash* | *fish* | *powershell* | *zsh* [*options*] + +## DESCRIPTION +**podman-bootc completion** generate shell completion scripts for a variety of shells. +Supported shells are *bash*, *fish*, *powershell*, and *zsh*. + +## OPTIONS + +#### **--help**, **-h** +Show details on how to use the script generated for the particular shell. + +#### **--no-descriptions** +Disable completion descriptions. + +## SEE ALSO + +**[podman-bootc(1)](podman-bootc.1.md)** + +## HISTORY +Dec, 2024, Originally compiled by Martin Skøtt diff --git a/docs/podman-bootc-images.1.md b/docs/podman-bootc-images.1.md new file mode 100644 index 00000000..d4cbc01c --- /dev/null +++ b/docs/podman-bootc-images.1.md @@ -0,0 +1,26 @@ +% podman-bootc-images 1 + +## NAME +podman-bootc-images - List bootc images in the local containers store + +## SYNOPSIS +**podman-bootc images** + +## DESCRIPTION +**podman-bootc images** list bootc images in the containers store of the podman machine. +The podman machine must be running to use this command. + +## OPTIONS + +#### **--help**, **-h** +Help for images + +#### **--log-level**=*level* +Log messages at and above specified level: __debug__, __info__, __warn__, __error__, __fatal__ or __panic__ (default: _warn_) + +## SEE ALSO + +**[podman-bootc(1)](podman-bootc.1.md)** + +## HISTORY +Dec, 2024, Originally compiled by Martin Skøtt diff --git a/docs/podman-bootc-list.1.md b/docs/podman-bootc-list.1.md new file mode 100644 index 00000000..32bd61dd --- /dev/null +++ b/docs/podman-bootc-list.1.md @@ -0,0 +1,27 @@ +% podman-bootc-list 1 + +## NAME +podman-bootc-list - List installed OS Containers + +## SYNOPSIS +**podman-bootc list** + +## DESCRIPTION +**podman-bootc list** displays installed OS containers and their status. + +The podman machine must be running to use this command. + +## OPTIONS + +#### **--help**, **-h** +Help for list + +#### **--log-level**=*level* +Log messages at and above specified level: __debug__, __info__, __warn__, __error__, __fatal__ or __panic__ (default: _warn_) + +## SEE ALSO + +**[podman-bootc(1)](podman-bootc.1.md)** + +## HISTORY +Dec, 2024, Originally compiled by Martin Skøtt diff --git a/docs/podman-bootc-rm.1.md b/docs/podman-bootc-rm.1.md new file mode 100644 index 00000000..b6e870d2 --- /dev/null +++ b/docs/podman-bootc-rm.1.md @@ -0,0 +1,35 @@ +% podman-bootc-rm 1 + +## NAME +podman-bootc-rm - Remove installed bootc VMs + +## SYNOPSIS +**podman-bootc rm** *id* [*options*] + +## DESCRIPTION +**podman-bootc rm** removes an installed bootc VM/container from the podman machine. + +Use **[podman-bootc list](podman-bootc-list.1.md)** to find the IDs of installed VMs. + +The podman machine must be running to use this command. + +## OPTIONS + +#### **--all** +Removes all non-running bootc VMs + +#### **--force**, **-f** +Terminate a running VM + +#### **--help**, **-h** +Help for rm + +#### **--log-level**=*level* +Log messages at and above specified level: __debug__, __info__, __warn__, __error__, __fatal__ or __panic__ (default: _warn_) + +## SEE ALSO + +**[podman-bootc(1)](podman-bootc.1.md)**, **[podman-bootc-list(1)](podman-bootc-list.1.md)** + +## HISTORY +Dec, 2024, Originally compiled by Martin Skøtt diff --git a/docs/podman-bootc-run.1.md b/docs/podman-bootc-run.1.md new file mode 100644 index 00000000..a6210ec5 --- /dev/null +++ b/docs/podman-bootc-run.1.md @@ -0,0 +1,66 @@ +% podman-bootc-run 1 + +## NAME +podman-bootc-run - Run a bootc container as a VM + +## SYNOPSIS +**podman-bootc run** [*options*] *image* | *id* + +## DESCRIPTION +**podman-bootc run** creates a new virtual machine from a bootc container image or starts an existing one. +It then creates an SSH connection to the VM using injected credentials (see *--background* to run in the background). + +The podman machine must be running to use this command. + +## OPTIONS + +#### **--background**, **-B** +Do not spawn SSH, run in background. + +#### **--cloudinit**=**string** +--cloud-init + +#### **--disk-size**=**string** +Allocate a disk image of this size in bytes; optionally accepts M, G, T suffixes + +#### **--filesystem**=**string** +Override the root filesystem, e.g. xfs, btrfs, ext4. + +#### **--help**, **-h** +Help for run + +#### **--log-level**=*level* +Log messages at and above specified level: __debug__, __info__, __warn__, __error__, __fatal__ or __panic__ (default: _warn_) + +#### **--quiet** +Suppress output from bootc disk creation and VM boot console + +#### **--rm** +Remove the VM and it's disk image when the SSH connection exits. Cannot be used with *--background* + +#### **--root-size-max**=**string** +Maximum size of root filesystem in bytes; optionally accepts M, G, T suffixes + +#### **--user**, **-u**=**root** | *user name* +User name of injected user, default: root + +## EXAMPLES +Create a virtual machine based on the latest bootable image from Fedora using XFS as the root filesystem. +``` +$ podman-bootc run --filesystem=xfs quay.io/fedora/fedora-bootc:latest +``` + +Start a previously created VM, using *podman-bootc list* to find its ID. +``` +$ podman-bootc list +ID REPO SIZE CREATED RUNNING SSH PORT +d0300f628e13 quay.io/fedora/fedora-bootc:latest 10.7GB 4 minutes ago false 34173 +$ podman-bootc run d0300f628e13 +``` + +## SEE ALSO + +**[podman-bootc(1)](podman-bootc.1.md)** + +## HISTORY +Dec, 2024, Originally compiled by Martin Skøtt diff --git a/docs/podman-bootc-ssh.1.md b/docs/podman-bootc-ssh.1.md new file mode 100644 index 00000000..b03f9f37 --- /dev/null +++ b/docs/podman-bootc-ssh.1.md @@ -0,0 +1,30 @@ +% podman-bootc-ssh 1 + +## NAME +podman-bootc-ssh - SSH into an existing OS Container machine + +## SYNOPSIS +**podman-bootc ssh** *id* [*options*] + +## DESCRIPTION +**podman-bootc ssh** opens an SSH connection to a running OS container machine. + +Use **[podman-bootc list](podman-bootc-list.1.md)** to find the IDs of installed VMs. + +## OPTIONS + +#### **--help**, **-h** +Help for ssh + +#### **--log-level**=*level* +Log messages at and above specified level: __debug__, __info__, __warn__, __error__, __fatal__ or __panic__ (default: _warn_) + +#### **--user**, **-u**=**root** | *user name* +User name to use for connection, default: root + +## SEE ALSO + +**[podman-bootc(1)](podman-bootc.1.md)**, **[podman-bootc-list(1)](podman-bootc-list.1.md)** + +## HISTORY +Dec, 2024, Originally compiled by Martin Skøtt diff --git a/docs/podman-bootc-stop.1.md b/docs/podman-bootc-stop.1.md new file mode 100644 index 00000000..c5b9ba21 --- /dev/null +++ b/docs/podman-bootc-stop.1.md @@ -0,0 +1,25 @@ +% podman-bootc-stop 1 + +## NAME +podman-bootc-stop - Stop an existing OS Container machine + +## SYNOPSIS +**podman-bootc stop** *id* + +## DESCRIPTION +**podman-bootc stop** stops a running OS container machine. + +## OPTIONS + +#### **--help**, **-h** +Help for stop + +#### **--log-level**=*level* +Log messages at and above specified level: __debug__, __info__, __warn__, __error__, __fatal__ or __panic__ (default: _warn_) + +## SEE ALSO + +**[podman-bootc(1)](podman-bootc.1.md)** + +## HISTORY +Dec, 2024, Originally compiled by Martin Skøtt diff --git a/docs/podman-bootc.1.md b/docs/podman-bootc.1.md new file mode 100644 index 00000000..388b0a10 --- /dev/null +++ b/docs/podman-bootc.1.md @@ -0,0 +1,43 @@ +% podman-bootc 1 + +## NAME +podman-bootc - Run bootable containers as a virtual machine + +## SYNOPSIS +**podman-bootc** [*options*] *command* + +## DESCRIPTION +**podman-bootc** is a tool to streamline the local development cycle when working with bootable containers. +It makes it easy to run a local bootc image and get shell access to it without first setting up a virtual machine. + +podman-bootc requires a rootful podman machine to be running before running a bootable container. +A machine can be set up using e.g. `podman machine init --rootful --now`. +See `podman-machine(1)` for details. + +**podman-bootc [GLOBAL OPTIONS]** + +## GLOBAL OPTIONS + +#### **--help**, **-h** +Print usage statement + +#### **--log-level**=*level* +Log messages at and above specified level: __debug__, __info__, __warn__, __error__, __fatal__ or __panic__ (default: _warn_) + +## COMMANDS + +| Command | Description | +|------------------------------------------------------------|------------------------------------------------------------| +| [podman-bootc-completion(1)](podman-bootc-completion.1.md) | Generate the autocompletion script for the specified shell | +| [podman-bootc-images(1)](podman-bootc-images.1.md) | List bootc images in the local containers store | +| [podman-bootc-list(1)](podman-bootc-list.1.md) | List installed OS Containers | +| [podman-bootc-rm(1)](podman-bootc-rm.1.md) | Remove installed bootc VMs | +| [podman-bootc-run(1)](podman-bootc-run.1.md) | Run a bootc container as a VM | +| [podman-bootc-ssh(1)](podman-bootc-ssh.1.md) | SSH into an existing OS Container machine | +| [podman-bootc-stop(1)](podman-bootc-stop.1.md) | Stop an existing OS Container machine | + +## SEE ALSO +**[podman-machine(1)](https://github.com/containers/podman/blob/main/docs/source/markdown/podman-machine.1.md)** + +## HISTORY +Dec, 2024, Originally compiled by Martin Skøtt diff --git a/hack/man-page-checker b/hack/man-page-checker new file mode 100755 index 00000000..3f536bb5 --- /dev/null +++ b/hack/man-page-checker @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# +# man-page-checker - validate and cross-reference man page names +# +verbose= +for i; do + case "$i" in + -v|--verbose) verbose=verbose ;; + esac +done + + +die() { + echo "$(basename $0): $*" >&2 + exit 1 +} + +cd $(dirname $0)/../docs/ || die "Please run me from top-level libpod dir" + +rc=0 + +for md in *.1.md;do + # Read the first line after '# NAME' (or '## NAME'). (FIXME: # and ## + # are not the same; should we stick to one convention?) + # There may be more than one name, e.g. podman-bootc-info.1.md has + # podman-bootc-system-info then another line with podman-bootc-info. We + # care only about the first. + name=$(grep -E -A1 '^#* NAME' $md|tail -1|awk '{print $1}' | tr -d \\\\) + + expect=$(basename $md .1.md) + if [ "$name" != "$expect" ]; then + echo + printf "Inconsistent program NAME in %s:\n" $md + printf " NAME= %s (expected: %s)\n" $name $expect + rc=1 + fi +done + +# Pass 2: compare descriptions. +# +# Make sure the descriptive text in podman-bootc-foo.1.md matches the one +# in the table in podman-bootc.1.md. +for md in $(ls -1 *-*-*.1.md | grep -v remote);do + desc=$(grep -E -A1 '^#* NAME' $md|tail -1|sed -e 's/^podman-bootc[^ ]\+ - //') + + # podman-bootc.1.md has a two-column table; podman-bootc-*-*.1.md all have three. + parent=$(echo $md | sed -e 's/^\(.*\)-.*$/\1.1.md/') + x=3 + if expr -- "$parent" : ".*-.*-" >/dev/null; then + x=4 + fi + + # Find the descriptive text in the parent man page. + # Strip off the final period; let's not warn about such minutia. + parent_desc=$(grep $md $parent | awk -F'|' "{print \$$x}" | sed -e 's/^ \+//' -e 's/ \+$//' -e 's/\.$//') + + if [ "$desc" != "$parent_desc" ]; then + echo + printf "Inconsistent subcommand descriptions:\n" + printf " %-32s = '%s'\n" $md "$desc" + printf " %-32s = '%s'\n" $parent "$parent_desc" + printf "Please ensure that the NAME section of $md\n" + printf "matches the subcommand description in $parent\n" + rc=1 + fi +done + +# Helper function: compares man page synopsis vs --help usage message +function compare_usage() { + local cmd="$1" + local from_man="$2" + + # Sometimes in CI we run before podman-bootc gets built. + test -x ../../../bin/podman-bootc || return + + # Run 'cmd --help', grab the line immediately after 'Usage:' + local help_output=$(../../../bin/$cmd --help) + local from_help=$(echo "$help_output" | grep -A1 '^Usage:' | tail -1) + + # strip off command name from both + from_man=$(sed -e "s/\*\*$cmd\*\*[[:space:]]*//" <<<"$from_man") + from_help=$(sed -e "s/^[[:space:]]*${cmd}[[:space:]]*//" <<<"$from_help") + + # man page lists 'foo [*options*]', help msg shows 'foo [flags]'. + # Make sure if one has it, the other does too. + if expr "$from_man" : "\[\*options\*\]" >/dev/null; then + if expr "$from_help" : "\[options\]" >/dev/null; then + : + else + echo "WARNING: $cmd: man page shows '[*options*]', help does not show [options]" + rc=1 + fi + elif expr "$from_help" : "\[flags\]" >/dev/null; then + echo "WARNING: $cmd: --help shows [flags], man page does not show [*options*]" + rc=1 + fi + + # Strip off options and flags; start comparing arguments + from_man=$(sed -e 's/^\[\*options\*\][[:space:]]*//' <<<"$from_man") + from_help=$(sed -e 's/^\[flags\][[:space:]]*//' <<<"$from_help") + + # Args in man page are '*foo*', in --help are 'FOO'. Convert all to + # UPCASE simply because it stands out better to the eye. + from_man=$(sed -e 's/\*\([a-z-]\+\)\*/\U\1/g' <<<"$from_man") + + # FIXME: one of the common patterns is for --help to show 'POD [POD...]' + # but man page show 'pod ...'. This conversion may help one day, but + # not yet: there are too many inconsistencies such as '[pod ...]' + # (brackets) and 'pod...' (no space between). +# from_help=$(sed -e 's/\([A-Z]\+\)[[:space:]]\+\[\1[[:space:]]*\.\.\.\]/\1 .../' <<<"$from_help") + + # Compare man-page and --help usage strings. For now, do so only + # when run with --verbose. + if [[ "$from_man" != "$from_help" ]]; then + if [ -n "$verbose" ]; then + printf "%-25s man='%s' help='%s'\n" "$cmd:" "$from_man" "$from_help" + # Yeah, we're not going to enable this as a blocker any time soon. + # rc=1 + fi + fi +} + +# Pass 3: compare synopses. +# +# Make sure the SYNOPSIS line in podman-bootc-foo.1.md reads '**podman-bootc foo** ...' +for md in *.1.md;do + # FIXME: several pages have a multi-line form of SYNOPSIS in which + # many or all flags are enumerated. Some of these are trivial + # and really should be made into one line (podman-bootc-container-exists, + # container-prune, others); some are more complicated and I + # would still like to see them one-lined (container-runlabel, + # image-trust) but I'm not 100% comfortable doing so myself. + # To view those: + # $ less $(for i in docs/*.1.md;do x=$(grep -A2 '^#* SYNOPSIS' $i|tail -1); if [ -n "$x" ]; then echo $i;fi;done) + # + synopsis=$(grep -E -A1 '^#* SYNOPSIS' $md|tail -1) + + # Command name must be bracketed by double asterisks; options and + # arguments are bracketed by single ones. + # E.g. '**podman-bootc volume inspect** [*options*] *volume*...' + # Get the command name, and confirm that it matches the md file name. + cmd=$(echo "$synopsis" | sed -e 's/\(.*\)\*\*.*/\1/' | tr -d \*) + md_nodash=$(basename "$md" .1.md | sed 's/-/ /2') + if [[ "$cmd" != "$md_nodash" ]]; then + echo + printf "Inconsistent program name in SYNOPSIS in %s:\n" $md + printf " SYNOPSIS = %s (expected: '%s')\n" "$cmd" "$md_nodash" + rc=1 + fi + + # The convention is to use UPPER CASE in 'podman-bootc foo --help', + # but *lower case bracketed by asterisks* in the man page + if expr "$synopsis" : ".*[A-Z]" >/dev/null; then + echo + printf "Inconsistent capitalization in SYNOPSIS in %s\n" $md + printf " '%s' should not contain upper-case characters\n" "$synopsis" + rc=1 + fi + + # (for debugging, and getting a sense of standard conventions) + #printf " %-32s ------ '%s'\n" $md "$synopsis" + + # If bin/podman-bootc is available, run "cmd --help" and compare Usage + # messages. This is complicated, so do it in a helper function. + compare_usage "$md_nodash" "$synopsis" +done + +exit $rc diff --git a/hack/xref-helpmsgs-manpages b/hack/xref-helpmsgs-manpages new file mode 100755 index 00000000..9202cb5e --- /dev/null +++ b/hack/xref-helpmsgs-manpages @@ -0,0 +1,1011 @@ +#!/usr/bin/perl +# +# xref-helpmsgs-manpages - cross-reference --help options against man pages +# +package LibPod::CI::XrefHelpmsgsManpages; + +use v5.14; +use utf8; + +use strict; +use warnings; +use Clone qw(clone); +use FindBin; + +(our $ME = $0) =~ s|.*/||; +our $VERSION = '0.1'; + +# For debugging, show data structures using DumpTree($var) +#use Data::TreeDumper; $Data::TreeDumper::Displayaddress = 0; + +# unbuffer output +$| = 1; + +############################################################################### +# BEGIN user-customizable section + +# Path to podman-bootc executable +my $Default_PodmanBootc = "$FindBin::Bin/../bin/podman-bootc"; +my $PODMANBOOTC = $ENV{PODMAN_BOOTC} || $Default_PodmanBootc; + +# Path to all doc files, including .rst and (down one level) markdown +our $Docs_Path = 'docs'; + +# Path to podman-bootc markdown source files (of the form podman-bootc-*.1.md) +our $Markdown_Path = "$Docs_Path"; + +# Global error count +our $Errs = 0; + +# Table of exceptions for documenting fields in '--format {{.Foo}}' +# +# Autocomplete is wonderful, and it's even better when we document the +# existing options. Unfortunately, sometimes internal structures get +# exposed that are of no use to anyone and cannot be guaranteed. Avoid +# documenting those. This table lists those exceptions. Format is: +# +# foo .Bar +# +# ...such that "podman-bootc foo --format '{{.Bar}}'" will not be documented. +# +my $Format_Exceptions = <<'END_EXCEPTIONS'; +# Deep internal structs; pretty sure these are permanent exceptions +events .Details +history .ImageHistoryLayer +images .Arch .ImageSummary .Os .IsManifestList +network-ls .Network + +# FIXME: this one, maybe? But someone needs to write the text +machine-list .Starting + +# No clue what these are. Some are just different-case dups of others. +pod-ps .Containers .Id .InfraId .ListPodsReport .Namespace +ps .Cgroup .CGROUPNS .IPC .ListContainer .MNT .Namespaces .NET .PIDNS .User .USERNS .UTS + +# I think .Destination is an internal struct, but .IsMachine maybe needs doc? +system-connection-list .Destination .IsMachine +END_EXCEPTIONS + +my %Format_Exceptions; +for my $line (split "\n", $Format_Exceptions) { + $line =~ s/#.*$//; # strip comments + next unless $line; # skip empty lines + my ($subcommand, @fields) = split(' ', $line); + $Format_Exceptions{"podman-bootc-$subcommand"} = \@fields; +} + +# Hardcoded list of existing duplicate-except-for-case format codes, +# with their associated subcommands. Let's not add any more. +my %Format_Option_Dup_Allowed = ( + 'podman-bootc-images' => { '.id' => 1 }, + 'podman-bootc-stats' => { '.avgcpu' => 1, '.pids' => 1 }, +); + +# Do not cross-reference these. +my %Skip_Subcommand = map { $_ => 1 } ( + "help", # has no man page + "completion", # internal (hidden) subcommand + "compose", # external tool, outside of our control +); + +# END user-customizable section +############################################################################### +# BEGIN boilerplate args checking, usage messages + +sub usage { + print <<"END_USAGE"; +Usage: $ME [OPTIONS] + +$ME recursively runs 'podman-bootc --help' against +all subcommands; and recursively reads podman-bootc-*.1.md files +in $Markdown_Path, then cross-references that each --help +option is listed in the appropriate man page and vice-versa. + +$ME invokes '\$PODMANBOOTC' (default: $Default_PodmanBootc). + +In the spirit of shoehorning functionality where it wasn't intended, +$ME also checks the SEE ALSO section of each man page +to ensure that references and links are properly formatted +and valid. + +Exit status is zero if no inconsistencies found, one otherwise + +OPTIONS: + + -v, --verbose show verbose progress indicators + -n, --dry-run make no actual changes + + --help display this message + --version display program name and version +END_USAGE + + exit; +} + +# Command-line options. Note that this operates directly on @ARGV ! +our $debug = 0; +our $verbose = 0; +sub handle_opts { + use Getopt::Long; + GetOptions( + 'debug!' => \$debug, + 'verbose|v' => \$verbose, + + help => \&usage, + version => sub { print "$ME version $VERSION\n"; exit 0 }, + ) or die "Try `$ME --help' for help\n"; +} + +# END boilerplate args checking, usage messages +############################################################################### + +############################## CODE BEGINS HERE ############################### + +# The term is "modulino". +__PACKAGE__->main() unless caller(); + +# Main code. +sub main { + # Note that we operate directly on @ARGV, not on function parameters. + # This is deliberate: it's because Getopt::Long only operates on @ARGV + # and there's no clean way to make it use @_. + handle_opts(); # will set package globals + + # Fetch command-line arguments. Barf if too many. + die "$ME: Too many arguments; try $ME --help\n" if @ARGV; + + chdir "$FindBin::Bin/.." + or die "$ME: FATAL: Cannot cd $FindBin::Bin/..: $!"; + + my $help = podman_bootc_help(); + + my $man = podman_bootc_man('podman-bootc'); + my $rst = podman_bootc_rst(); + + xref_by_help($help, $man); + xref_by_man($help, $man); + + exit !!$Errs; +} + +############################################################################### +# BEGIN cross-referencing + +################## +# xref_by_help # Find keys in '--help' but not in man +################## +sub xref_by_help { + my ($help, $man, @subcommand) = @_; + + OPTION: + for my $k (sort keys %$help) { + next if $k =~ /^_/ || $k eq "ls"; # metadata ("_desc"). Ignore. + + if (! ref($man)) { + # Super-unlikely but I've seen it + warn "$ME: 'podman-bootc @subcommand' is not documented in man pages!\n"; + ++$Errs; + next OPTION; + } + + if (exists $man->{$k}) { + if (ref $help->{$k}) { + # This happens when 'podman-bootc foo --format' offers + # autocompletion that looks like a Go template, but those + # template options aren't documented in the man pages. + if ($k eq '--format' && ! ref($man->{$k})) { + # "podman-bootc inspect" tries to autodetect if it's being run + # on an image or container. It cannot sanely be documented. + unless ("@subcommand" eq "inspect") { + warn "$ME: 'podman-bootc @subcommand': --format options are available through autocomplete2, but are not documented in $man->{_path}\n"; + ++$Errs; + } + next OPTION; + } + + xref_by_help($help->{$k}, $man->{$k}, @subcommand, $k); + } + + # Documenting --format fields is tricky! They can be scalars, structs, + # or functions. This is a complicated block because if help & man don't + # match, we want to give the most user-friendly message possible. + elsif (@subcommand && $subcommand[-1] eq '--format') { + # '!' is one of the Format_Exceptions defined at top + if (($man->{$k} ne '!') && ($man->{$k} ne $help->{$k})) { + # Fallback message + my $msg = "TELL ED TO HANDLE THIS: man='$man->{$k}' help='$help->{$k}'"; + + # Many different permutations of mismatches. + my $combo = "$man->{$k}-$help->{$k}"; + if ($combo eq '0-...') { + $msg = "is a nested structure. Please add '...' to man page."; + } + elsif ($combo =~ /^\d+-\.\.\.$/) { + $msg = "is a nested structure, but the man page documents it as a function?!?"; + } + elsif ($combo eq '...-0') { + $msg = "is a simple value, not a nested structure. Please remove '...' from man page."; + } + elsif ($combo =~ /^0-[1-9]\d*$/) { + $msg = "is a function that calls for $help->{$k} args. Please investigate what those are, then add them to the man page. E.g., '$k *bool*' or '$k *path* *bool*'"; + } + elsif ($combo =~ /^\d+-[1-9]\d*$/) { + $msg = "is a function that calls for $help->{$k} args; the man page lists $man->{$k}. Please fix the man page."; + } + + warn "$ME: 'podman-bootc @subcommand {{$k' $msg\n"; + ++$Errs; + } + } + } + else { + # Not documented in man. However, handle '...' as a special case + # in formatting strings. E.g., 'podman-bootc info .Host' is documented + # in the man page as '.Host ...' to indicate that the subfields + # are way too many to list individually. + my $k_copy = $k; + while ($k_copy =~ s/\.[^.]+$//) { + my $parent_man = $man->{$k_copy} // ''; + if (($parent_man eq '...') || ($parent_man eq '!')) { + next OPTION; + } + } + + # Nope, it's not that case. + my $man = $man->{_path} || 'man'; + # The usual case is "podman-bootc ... --help"... + my $what = '--help'; + # ...but for *options* (e.g. --filter), we're checking command completion + $what = '' if @subcommand && $subcommand[-1] =~ /^--/; + warn "$ME: 'podman-bootc @subcommand $what' lists '$k', which is not in $man\n"; + ++$Errs; + } + } +} + +################# +# xref_by_man # Find keys in man pages but not in --help +################# +# +# In an ideal world we could share the functionality in one function; but +# there are just too many special cases in man pages. +# +sub xref_by_man { + my ($help, $man, @subcommand) = @_; + + # FIXME: this generates way too much output + KEYWORD: + for my $k (grep { $_ ne '_path' } sort keys %$man) { + if ($k eq '--format' && ref($man->{$k}) && ! ref($help->{$k})) { +# warn "$ME: 'podman-bootc @subcommand': --format options documented in man page, but not available via autocomplete1\n"; + next KEYWORD; + } + + if (exists $help->{$k}) { + if (ref $man->{$k}) { + xref_by_man($help->{$k}, $man->{$k}, @subcommand, $k); + } + elsif ($k =~ /^-/) { + # This is OK: we don't recurse into options + } + else { + # FIXME: should never get here, but we do. Figure it out later. + } + } + elsif ($k ne '--help' && $k ne '-h') { + my $man = $man->{_path} || 'man'; + + # Special case: podman-bootc-inspect serves dual purpose (image, ctr) + my %ignore = map { $_ => 1 } qw(-l -s -t --latest --size --type); + next if $man =~ /-inspect/ && $ignore{$k}; + + # Special case: podman-bootc-diff serves dual purpose (image, ctr) + my %diffignore = map { $_ => 1 } qw(-l --latest ); + next if $man =~ /-diff/ && $diffignore{$k}; + + # Special case: the 'trust' man page is a mess + next if $man =~ /-trust/; + + # Special case: '--net' is an undocumented shortcut + next if $k eq '--net' && $help->{'--network'}; + + # Special case: these are actually global options + next if $k =~ /^--(cni-config-dir|runtime)$/ && $man =~ /-build/; + + # Special case: weirdness with Cobra and global/local options + next if $k eq '--namespace' && $man =~ /-ps/; + + next if "@subcommand" eq 'system' && $k eq 'service'; + + # Special case for hidden or external commands + next if $Skip_Subcommand{$k}; + + # It's not always --help, sometimes we check completion + my $what = '--help'; + $what = 'command completion' if @subcommand && $subcommand[-1] =~ /^--/; + warn "$ME: 'podman-bootc @subcommand': '$k' in $man, but not in $what\n"; + ++$Errs; + } + } +} + +############## +# xref_rst # Cross-check *.rst files against help +############## +# +# This makes a pass over top-level commands only. There is no rst +# documentation for any podman-bootc subcommands. +# +sub xref_rst { + my ($help, $rst) = @_; + + + # We key on $help because that is Absolute Truth: anything in podman-bootc --help + # must be referenced in an rst (the converse is not necessarily true) + for my $k (sort grep { $_ !~ /^[_-]/ } keys %$help) { + if (exists $rst->{$k}) { + # Descriptions must match + if ($rst->{$k}{_desc} ne $help->{$k}{_desc}) { + warn "$ME: podman-bootc $k: inconsistent description in $rst->{$k}{_source}:\n"; + warn " help: '$help->{$k}{_desc}'\n"; + warn " rst: '$rst->{$k}{_desc}'\n"; + ++$Errs; + } + } + else { + warn "$ME: Not found in rst: $k\n"; + ++$Errs; + } + } + + # Now the other way around: look for anything in Commands.rst that is + # not in podman-bootc --help + for my $k (sort grep { $rst->{$_}{_source} =~ /Commands.rst/ } keys %$rst) { + if ($k ne 'podman-bootc' && ! exists $help->{$k}) { + warn "$ME: 'podman-bootc $k' found in $rst->{$k}{_source} but not 'podman-bootc help'\n"; + ++$Errs; + } + } +} + +# END cross-referencing +############################################################################### +# BEGIN data gathering + +################# +# podman_bootc_help # Parse output of 'podman-bootc [subcommand] --help' +################# +sub podman_bootc_help { + my %help; + open my $fh, '-|', $PODMANBOOTC, @_, '--help' + or die "$ME: Cannot fork: $!\n"; + my $section = ''; + while (my $line = <$fh>) { + chomp $line; + + # First line of --help is a short command description. We compare it + # (in a later step) against the blurb in Commands.rst. + # FIXME: we should crossref against man pages, but as of 2024-03-18 + # it would be way too much work to get those aligned. + $help{_desc} //= $line; + + # Cobra is blessedly consistent in its output: + # [command blurb] + # Description: ... + # Usage: ... + # Available Commands: + # .... + # Options: + # .... + # + # Start by identifying the section we're in... + if ($line =~ /^Available\s+(Commands):/) { + if (@_ == 0) { + $section = lc $1; + } + else { + $section = 'flags'; + } + } + elsif ($line =~ /^(Flags):/) { + $section = lc $1; + } + + # ...then track commands and options. For subcommands, recurse. + elsif ($section eq 'commands') { + if ($line =~ /^\s{1,4}(\S+)\s/) { + my $subcommand = $1; + print "> podman-bootc @_ $subcommand\n" if $debug; + + # check that the same subcommand is not listed twice (#12356) + if (exists $help{$subcommand}) { + warn "$ME: 'podman-bootc @_ help' lists '$subcommand' twice\n"; + ++$Errs; + } + + $help{$subcommand} = podman_bootc_help(@_, $subcommand) + unless $Skip_Subcommand{$subcommand}; + } + } + elsif ($section eq 'flags') { + my $opt = ''; + + # Handle '--foo' or '-f, --foo' + if ($line =~ /^\s{1,10}(--\S+)\s/) { + print "> podman-bootc @_ $1\n" if $debug; + $opt = $1; + $help{$opt} = 1; + } + + # Handle "-n, --noheading" and "-u USER, --username USER" + elsif ($line =~ /^\s{1,10}(-\S)(\s+\S+)?,\s+(--\S+)\s/) { + print "> podman-bootc @_ $1, $3\n" if $debug; + $opt = $3; + $help{$1} = $help{$opt} = 1; + } + } + } + close $fh + or die "$ME: Error running 'podman-bootc @_ --help'\n"; + + return \%help; +} + + +################ +# podman_bootc_man # Parse contents of podman-bootc-*.1.md +################ +our %Man_Seen; +sub podman_bootc_man { + my $command = shift; + my $subpath = "$Markdown_Path/$command.1.md"; + print "** $subpath \n" if $debug; + + my %man = (_path => $subpath); + + # We often get called multiple times on the same man page, + # because (e.g.) podman-bootc-container-list == podman-bootc-ps. It's the + # same man page text, though, and we don't know which subcommand + # we're being called for, so there's nothing to be gained by + # rereading the man page or by dumping yet more warnings + # at the user. So, keep a cache of what we've done. + if (my $seen = $Man_Seen{$subpath}) { + return clone($seen); + } + $Man_Seen{$subpath} = \%man; + + open my $fh, '<', $subpath + or die "$ME: Cannot read $subpath: $!\n"; + my $section = ''; + my @most_recent_flags; + my $previous_subcmd = ''; + my $previous_flag = ''; + my $previous_format = ''; + my $previous_filter = ''; + LINE: + while (my $line = <$fh>) { + chomp $line; + next LINE unless $line; # skip empty lines + + # First line (page title) must match the command name. + if ($line =~ /^%\s+/) { + my $expect = "% $command 1"; + if ($line ne $expect) { + warn "$ME: $subpath:$.: wrong title line '$line'; should be '$expect'\n"; + ++$Errs; + } + } + + # .md files designate sections with leading double hash + if ($line =~ /^##\s*(GLOBAL\s+)?OPTIONS/) { + $section = 'flags'; + $previous_flag = ''; + } + elsif ($line =~ /^###\s+\w+\s+OPTIONS/) { + # podman-bootc image trust has sections for set & show + $section = 'flags'; + $previous_flag = ''; + } + elsif ($line =~ /^\#\#\s+(SUB)?COMMANDS/) { + $section = 'commands'; + } + elsif ($line =~ /^\#\#\s+SEE\s+ALSO/) { + $section = 'see-also'; + } + elsif ($line =~ /^\#\#[^#]/) { + $section = ''; + } + + # This will be a table containing subcommand names, links to man pages. + # The format is slightly different between podman-bootc.1.md and subcommands. + elsif ($section eq 'commands') { + # In podman-bootc.1.md + if ($line =~ /^\|\s*\[podman-bootc-(\S+?)\(\d\)\]/) { + # $1 will be changed by recursion _*BEFORE*_ left-hand assignment + my $subcmd = $1; + $man{$subcmd} = podman_bootc_man("podman-bootc-$subcmd"); + } + + # In podman-bootc-.1.md + # 1 1 2 3 3 4 4 2 + elsif ($line =~ /^\|\s+(\S+)\s+\|\s+(\[(\S+)\]\((\S+)\.1\.md\))/) { + my ($subcmd, $blob, $shown_name, $link_name) = ($1, $2, $3, $4); + if ($previous_subcmd gt $subcmd) { + warn "$ME: $subpath:$.: '$previous_subcmd' and '$subcmd' are out of order\n"; + ++$Errs; + } + if ($previous_subcmd eq $subcmd) { + warn "$ME: $subpath:$.: duplicate subcommand '$subcmd'\n"; + ++$Errs; + } + $previous_subcmd = $subcmd; + $man{$subcmd} = podman_bootc_man($link_name); + + # Check for inconsistencies between the displayed man page name + # and the actual man page name, e.g. + # '[podman-bootc-bar(1)](podman-bootc-baz.1.md) + $shown_name =~ s/\(\d\)$//; + $shown_name =~ s/\\//g; # backslashed hyphens + (my $should_be = $link_name) =~ s/\.1\.md$//; + if ($shown_name ne $should_be) { + warn "$ME: $subpath:$.: '$shown_name' should be '$should_be' in '$blob'\n"; + ++$Errs; + } + } + } + + # Options should always be of the form '**-f**' or '**\-\-flag**', + # possibly separated by comma-space. + elsif ($section eq 'flags') { + # e.g. 'podman-bootc run --ip6', documented in man page, but nonexistent + if ($line =~ /^not\s+implemented/i) { + delete $man{$_} for @most_recent_flags; + } + + @most_recent_flags = (); + # As of PR #8292, all options are

and anchored + if ($line =~ s/^\#{4}\s+//) { + # If option has long and short form, long must come first. + # This is a while-loop because there may be multiple long + # option names, e.g. --net/--network + my $is_first = 1; + while ($line =~ s/^\*\*(--[a-z0-9-]+)\*\*(,\s+)?//g) { + my $flag = $1; + $man{$flag} = 1; + if ($flag lt $previous_flag && $is_first) { + warn "$ME: $subpath:$.: $flag should precede $previous_flag\n"; + ++$Errs; + } + if ($flag eq $previous_flag) { + warn "$ME: $subpath:$.: flag '$flag' is a dup\n"; + ++$Errs; + } + $previous_flag = $flag if $is_first; + push @most_recent_flags, $flag; + + # Further iterations of /g are allowed to be out of order, + # e.g., it's OK for "--namespace, -ns" to precede --nohead + $is_first = 0; + } + # Short form + if ($line =~ s/^\*\*(-[a-zA-Z0-9])\*\*//) { + my $flag = $1; + $man{$flag} = 1; + + # Keep track of them, in case we see 'Not implemented' below + push @most_recent_flags, $flag; + } + + # Options with no '=whatever' + next LINE if !$line; + + # Anything remaining *must* be of the form '=' + if ($line !~ /^=/) { + warn "$ME: $subpath:$.: could not parse '$line' in option description\n"; + ++$Errs; + } + + # For some years it was traditional, albeit wrong, to write + # **--foo**=*bar*, **-f** + # The correct way is to add =*bar* at the end. + if ($line =~ s/,\s\*\*(-[a-zA-Z])\*\*//) { + $man{$1} = 1; + warn "$ME: $subpath:$.: please rewrite as ', **$1**$line'\n"; + ++$Errs; + } + + # List of possibilities ('=*a* | *b*') must be space-separated + if ($line =~ /\|/) { + if ($line =~ /[^\s]\|[^\s]/) { + # Sigh, except for this one special case + if ($line !~ /SOURCE-VOLUME.*HOST-DIR.*CONTAINER-DIR/) { + warn "$ME: $subpath:$.: values must be space-separated: '$line'\n"; + ++$Errs; + } + } + my $copy = $line; + if ($copy =~ s/\**true\**//) { + if ($copy =~ s/\**false\**//) { + if ($copy !~ /[a-z]/) { + warn "$ME: $subpath:$.: Do not enumerate true/false for boolean-only options\n"; + ++$Errs; + } + } + } + } + } + + # --format does not always mean a Go format! E.g., push --format=oci + if ($previous_flag eq '--format') { + # ...but if there's a table like '| .Foo | blah blah |' + # then it's definitely a Go template. There are three cases: + # .Foo - Scalar field. The usual case. + # .Foo ... - Structure with subfields, e.g. .Foo.Xyz + # .Foo ARG(s) - Function requiring one or more arguments + # + # 1 12 3 32 + if ($line =~ /^\|\s+(\.\S+)(\s+([^\|]+\S))?\s+\|/) { + my ($format, $etc) = ($1, $3); + + # Confirmed: we have a table with '.Foo' strings, so + # this is a Go template. Override previous (scalar) + # setting of the --format flag with a hash, indicating + # that we will recursively cross-check each param. + if (! ref($man{$previous_flag})) { + $man{$previous_flag} = { _path => $subpath }; + } + + # ...and document this format option. $etc, if set, + # will indicate if this is a struct ("...") or a + # function. + if ($etc) { + if ($etc eq '...') { # ok + ; + } + elsif ($etc =~ /^\*[a-z]+\*(\s+\*[a-z]+\*)*$/) { + # a function. Preserve only the arg COUNT, not + # their names. (command completion has no way + # to give us arg names or types). + $etc = scalar(split(' ', $etc)); + } + else { + warn "$ME: $subpath:$.: unknown args '$etc' for '$format'. Valid args are '...' for nested structs or, for functions, one or more asterisk-wrapped argument names.\n"; + ++$Errs; + } + } + + $man{$previous_flag}{$format} = $etc || 0; + + # Sort order check, case-insensitive + if (lc($format) lt lc($previous_format)) { + warn "$ME: $subpath:$.: format specifier '$format' should precede '$previous_format'\n"; + ++$Errs; + } + + # Dup check, would've caught #19462. + if (lc($format) eq lc($previous_format)) { + # Sigh. Allow preexisting exceptions, but no new ones. + unless ($Format_Option_Dup_Allowed{$command}{lc $format}) { + warn "$ME: $subpath:$.: format specifier '$format' is a dup\n"; + ++$Errs; + } + } + $previous_format = $format; + } + } + # Same as above, but with --filter + elsif ($previous_flag eq '--filter') { + if ($line =~ /^\|\s+(\S+)\s+\|/) { + my $filter = $1; + + # (Garbage: these are just table column titles & dividers) + next LINE if $filter =~ /^\**Filter\**$/; + next LINE if $filter =~ /---+/; + + # Special case: treat slash-separated options + # ("after/since") as identical, and require that + # each be documented. + for my $f (split '/', $filter) { + # Special case for negated options ("label!="): allow, + # but only immediately after the positive case. + if ($f =~ s/!$//) { + if ($f ne $previous_filter) { + warn "$ME: $subpath:$.: filter '$f!' only allowed immediately after its positive\n"; + ++$Errs; + } + next LINE; + } + + if (! ref($man{$previous_flag})) { + $man{$previous_flag} = { _path => $subpath }; + } + $man{$previous_flag}{$f} = 1; + } + + # Sort order check, case-insensitive + # FIXME FIXME! Disabled for now because it would make + # this PR completely impossible to review (as opposed to + # only mostly-impossible) + #if (lc($filter) lt lc($previous_filter)) { + # warn "$ME: $subpath:$.: filter specifier '$filter' should precede '$previous_filter'\n"; + # ++$Errs; + #} + + # Dup check. Yes, it happens. + if (lc($filter) eq lc($previous_filter)) { + warn "$ME: $subpath:$.: filter specifier '$filter' is a dup\n"; + ++$Errs; + } + $previous_filter = $filter; + } + } + } + + # It's easy to make mistakes in the SEE ALSO elements. + elsif ($section eq 'see-also') { + _check_seealso_links( "$subpath:$.", $line ); + } + } + close $fh; + + # Done reading man page. If there are any '--format' exceptions defined + # for this command, flag them as seen, and as '...' so we don't + # complain about any sub-fields. + if (my $fields = $Format_Exceptions{$command}) { + $man{"--format"}{$_} = '!' for @$fields; + } + + # Special case: the 'image trust' man page tries hard to cover both set + # and show, which means it ends up not being machine-readable. + if ($command eq 'podman-bootc-image-trust') { + my %set = %man; + my %show = %man; + $show{$_} = 1 for qw(--raw -j --json); + return +{ set => \%set, show => \%show } + } + + return \%man; +} + + +################ +# podman_bootc_rst # Parse contents of docs/source/*.rst +################ +sub podman_bootc_rst { + my %rst; + + # Read all .rst files, looking for ":doc:`subcmd ` description" + for my $rst (glob "$Docs_Path/*.rst") { + open my $fh, '<', $rst + or die "$ME: Cannot read $rst: $!\n"; + + # The basename of foo.rst is usually, but not always, the name of + # a podman-bootc subcommand. There are a few special cases: + (my $command = $rst) =~ s!^.*/(.*)\.rst!$1!; + + my $subcommand_href = \%rst; + if ($command eq 'Commands') { + ; + } + else { + $subcommand_href = $rst{$command} //= { _source => $rst }; + } + + my $previous_subcommand = ''; + while (my $line = <$fh>) { + if ($line =~ /^:doc:`(\S+)\s+<(.*?)>`\s+(.*)/) { + my ($subcommand, $target, $desc) = ($1, $2, $3); + + # Check that entries are in alphabetical order, and not dups + if ($subcommand lt $previous_subcommand) { + warn "$ME: $rst:$.: '$previous_subcommand' and '$subcommand' are out of order\n"; + ++$Errs; + } + if ($subcommand eq $previous_subcommand) { + warn "$ME: $rst:$.: duplicate '$subcommand'\n"; + ++$Errs; + } + $previous_subcommand = $subcommand; + + # Mark this subcommand as documented. + $subcommand_href->{$subcommand}{_desc} = $desc; + $subcommand_href->{$subcommand}{_source} = $rst; + + # Check for invalid links. These will be one of two forms: + # -> markdown/foo.1.md + # -> foo.rst + if ($target =~ m!^markdown/!) { + if (! -e "$Docs_Path/$target.md") { + warn "$ME: $rst:$.: '$subcommand' links to nonexistent $target\n"; + ++$Errs; + } + + my $expect = "markdown/podman-bootc-$subcommand.1"; + if ($subcommand eq 'Podman-Bootc') { + $expect = "markdown/podman-bootc.1"; + } + if ($target ne $expect) { + warn "$ME: $rst:$.: '$subcommand' links to $target (expected '$expect')\n"; + ++$Errs; + } + } + else { + if (! -e "$Docs_Path/$target.rst") { + warn "$ME: $rst:$.: '$subcommand' links to nonexistent $target.rst\n"; + ++$Errs; + } + } + } + } + close $fh; + } + + # Special case: 'image trust set/show' are documented in image-trust.1 + $rst{image}{trust}{$_} = { _desc => 'ok' } for (qw(set show)); + + return \%rst; +} + +################## +# _completions # run ramalama __complete, return list of completions +################## +sub _completions { + my $kidpid = open my $ramalama_fh, '-|'; + if (! defined $kidpid) { + die "$ME: Could not fork: $!\n"; + } + + if ($kidpid == 0) { + # We are the child + close STDERR; + exec $PODMANBOOTC, '__complete', @_; + die "$ME: Could not exec: $!\n"; + } + + # We are the parent + my @completions; + while (my $line = <$ramalama_fh>) { + chomp $line; + push @completions, $line; + + # Recursively expand Go templates, like '{{.Server.Os}}' + if ($line =~ /^\{\{\..*\.$/) { + my @cmd_copy = @_; # clone of podman-bootc subcommands... + pop @cmd_copy; # ...so we can recurse with new format + my @subcompletions = _completions(@cmd_copy, $line); + + # A huge number of deep fields are time-related. Don't document them. + my @is_time = grep { /Nanosecond|UnixNano|YearDay/ } @subcompletions; + push @completions, @subcompletions + unless @is_time >= 3; + } + } + close $ramalama_fh + or warn "$ME: Error running podman-bootc __complete @_\n"; + return @completions; +} + +# END data gathering +############################################################################### +# BEGIN sanity checking of SEE ALSO links + +########################## +# _check_seealso_links # Check formatting and link validity. +########################## +sub _check_seealso_links { + my $path = shift; + my $line = shift; + + return if ! $line; + + # Line must be a comma-separated list of man page references, e.g. + # **foo(1)**, **[podman-bootc-bar(1)](podman-bootc-bar.1.md)**, **[xxx(8)](http...)** + TOKEN: + for my $token (split /,\s+/, $line) { + # Elements must be separated by comma and space. (We don't do further + # checks here, so it's possible for the dev to add the space and then + # have us fail on the next iteration. I choose not to address that.) + if ($token =~ /,/) { + warn "$ME: $path: please add space after comma: '$token'\n"; + ++$Errs; + next TOKEN; + } + + # Each token must be of the form '**something**' + if ($token !~ s/^\*\*(.*)\*\*$/$1/) { + if ($token =~ /\*\*/) { + warn "$ME: $path: '$token' has asterisks in the wrong place\n"; + } + else { + warn "$ME: $path: '$token' should be bracketed by '**'\n"; + } + ++$Errs; + next TOKEN; + } + + # Is it a markdown link? + if ($token =~ /^\[(\S+)\]\((\S+)\)$/) { + my ($name, $link) = ($1, $2); + if ($name =~ /^(.*)\((\d)\)$/) { + my ($base, $section) = ($1, $2); + if (-e "$Markdown_Path/$base.$section.md") { + if ($link ne "$base.$section.md") { + warn "$ME: $path: inconsistent link $name -> $link, expected $base.$section.md\n"; + ++$Errs; + } + } + else { + if (! _is_valid_external_link($base, $section, $link)) { + warn "$ME: $path: invalid link $name -> $link\n"; + ++$Errs; + } + } + } + else { + warn "$ME: $path: could not parse '$name' as 'manpage(N)'\n"; + ++$Errs; + } + } + + # Not a markdown link; it must be a plain man reference, e.g. 'foo(5)' + elsif ($token =~ m!^(\S+)\((\d+)\)$!) { + my ($base, $section) = ($1, $2); + + # Unadorned 'podman-bootc-foo(1)' must be a link. + if (-e "$Markdown_Path/$base.$section.md") { + warn "$ME: $path: '$token' should be '[$token]($base.$section.md)'\n"; + ++$Errs; + } + + # Aliases (non-canonical command names): never link to these + if (-e "$Markdown_Path/links/$base.$section") { + warn "$ME: $path: '$token' refers to a command alias; please use the canonical command name instead\n"; + ++$Errs; + } + + # Link to man page foo(5) but without a link. This is not an error + # but Ed may sometimes want to see those on a manual test run. + warn "$ME: $path: plain '$token' would be so much nicer as a link\n" + if $verbose; + } + else { + warn "$ME: $path: invalid token '$token'\n"; + ++$Errs; + } + } +} + +############################# +# _is_valid_external_link # Tries to validate links to external man pages +############################# +# +# This performs no actual fetches, so we can't actually check for 404. +# All we do is ensure that links conform to standard patterns. This is +# good for catching things like 'conmon(8)' pointing to a .5 URL, or +# linking to .md instead of .html. +# +# FIXME: we could actually rewrite this so as to offer hints on what to fix. +# That's a lot of work, and a lot of convoluted code, for questionable ROI. +# +sub _is_valid_external_link { + my ($base, $section, $link) = @_; + + return 1 if $link =~ m!^https://github\.com/\S+/blob/(main|master)(/.*)?/\Q$base\E\.$section\.md!; + + return 1 if $link =~ m!^https://.*unix\.com/man-page/(linux|redhat)/$section/$base$!; + return 1 if $link eq "https://man7\.org/linux/man-pages/man$section/$base\.$section\.html"; + + if ($base =~ /systemd/) { + return 1 if $link eq "https://www.freedesktop.org/software/systemd/man/$base.html"; + } + + return; +} + + + + +# END sanity checking of SEE ALSO links +############################################################################### + +1; diff --git a/rpm/podman-bootc.spec b/rpm/podman-bootc.spec index d150cb02..e213229e 100644 --- a/rpm/podman-bootc.spec +++ b/rpm/podman-bootc.spec @@ -40,16 +40,20 @@ Requires: libvirt %build export BUILDTAGS="exclude_graphdriver_btrfs btrfs_noversion exclude_graphdriver_devicemapper containers_image_openpgp remote" %gobuild -o %{gobuilddir}/bin/%%{name} %{goipath} +%{__make} docs %install %gopkginstall install -m 0755 -vd %{buildroot}%{_bindir} install -m 0755 -vp %{gobuilddir}/bin/* %{buildroot}%{_bindir}/ +install -m 0755 -vd %{buildroot}%{_mandir}/man1 +install -m 0755 -vp docs/*.1 %{buildroot}%{_mandir}/man1/ %files %license LICENSE %doc README.md %{_bindir}/* +%{_mandir}/man1/*.1* %gopkgfiles