autoopts generator (#895 phase 2)
July 20, 2026 · View on GitHub
Replacement for EOL GNU autogen: emit the libopts-compatible *_opts.c/h
tables and man pages directly from the .def files. The vendored libopts
runtime is untouched — this replaces only the generator.
Not committed. *_opts.c/h, *.adoc, and *.1 are ordinary build
products (see .gitignore) — regenerated from the .def at build time by
emit_h.py/emit_c.py/emit_adoc.py/asciidoctor, never checked into
git, matching this project's original convention for generated files.
make dist/make dist-xz ship only the rendered *_opts.c/h/*.1
(matching the pre-#895 behavior of shipping pre-built man pages, not
autogen's separate mdoc source) — *.adoc is a git-checkout-only build
artifact, never distributed — so release tarballs need neither python3 nor
asciidoctor. Each *.1 rule in src/Makefile.am depends on its .def
directly, not on *.adoc, so a shipped .1 that's already newer than its
.def doesn't force the (absent) .adoc to be created first. The "committed"/"golden
file" language in the Status section below describes phase 2's
development-time verification (the emitters were proven byte-identical
to real GNU autogen's output while autogen was still installed and the
generated files were briefly committed, per phase 1's now-superseded
approach) — that equivalence is a one-time proof, documented here, not a
per-commit CI gate; see check_emitters.py's module docstring for what CI
actually checks today. src/tcpedit/tcpedit_stub.h (and its .1) is the
one exception that does stay committed — see the scope-boundary section
below.
Status
- Stage 1 —
defparser.py: .def → JSON IR (heredocs, -D conditionals, -L includes, adjacent strings, blocks).validate_ir.pyproves option names/order/counts match the committed autogen output for all seven tool configurations, including thedocumentationpseudo-flag subtlety (occupies an enum index, emits no constant — tcprewrite's enum starts at 1). - Stage 2 —
emit_h.py: *_opts.h emitter. Byte-identical to the committed autogen output for all seven configurations (check_emitters.pyproves it). Layout rules discovered: enum name field ismax(maxname+1, 21)wide; the per-option value column is a fixed%-24s(long names overflow with one space); unguarded OPT_VALUE defines get a preceding blank line, ifdef-guarded ones do not; save/load-opts draw from the same 0x1001+ hex counter as long-only options; settable+arg options emit the extendedSET_OPT_NAME(a)proc-invoking form; the HAVE_OPT example in the interface comment uses the first flag including documentation pseudo-flags;EXIT_NO_CONFIG_INPUT=66appears iff load-opts is enabled. - Stage 3 —
emit_c.py: *_opts.c emitter. Byte-identical for all seven configurations. The decisive move was porting libopts'optionPrintParagraphs()(libopts/usage.c) verbatim rather than guessing at the xgettext paragraph grouping — autogen'smk-gettextabledelegates straight to it, and we vendor the same libopts, so the algorithm is authoritative and frozen. Other rules: string pool is deduplicated with cumulative byte offsets; text refills at 75 cols (descrips at 72) with whitespace collapse and two spaces after sentence-ending words; texinfo@file{}/@var{}become'x'and@itembecomes a wider paragraph break;documentationpseudo-flags emit an OPTST_DOCUMENT descriptor plus alib-name-prefixedoptDesc_ppointer and count toward the user option count;flags-must/flags-cantnames are uppercased blindly (dangling refs like tcpbridge'scachefileare legal because those blocks are#ifdef-guarded); an option carrying a baredefaultattribute becomes the default-opt index. - Stage 4a —
generate.py: driver that regenerates all 14 parser files without autogen. Verified end-to-end: with autogen removed from the system, deleting everysrc/*_opts.c/hand regenerating reproduces them byte-identically (git diffempty), the tree builds,sudo make testpasses in full, and both regressions that sank #991 work —--load-opts(theprep_configtest case) and--more-help. - Stage 4b —
emit_adoc.py: AsciiDoc man-page source emitter, +check_adoc.pygate. Not byte-oracle-verified (no in-tree source of truth for autogen's ~1900-line Scheme+Perl mdoc pipeline, see rationale in the emitter's module docstring) - instead it renders straight from the same.defcontent (descrip/doc/explain/detail) that already drives--helpand the option tables, so the docs,--helpand the .adoc source can never disagree, and there is nothing hand-duplicated to fall out of sync (the mistake in the reverted #991, which hand-wrote separate.adocfiles).check_adoc.pyverifies every real option from the.defappears in the rendered page, and (when installed) thatasciidoctorrenders it andgroff -man -wwparses the result with zero warnings. All seven configurations pass. A hand-eyeballed diff against the old autogen-rendered pages for tcpcapinfo confirms full content parity with cleaner formatting (subsections instead of run-on.NOPparagraphs). Texinfo constructs handled:@file/@var-> italic,@samp/@code->`mono`,@table/@enumerate+@item(both the "@item LABEL" and bare-"@item"-then-"@var{X}"-on-the- next-line forms) -> nested AsciiDoc description lists (with the texinfo "- " item-body marker convention stripped, not rendered as a bullet),@exampleand mid-paragraph indented-line runs -> literal blocks, all correctly nested inside an option's own list entry via AsciiDoc's+/open-block continuation syntax. Known limitation:man-doccontent (SIGNALS/custom BUGS/SEE ALSO prose in tcpreplay, tcpbridge, tcpliveplay, tcprewrite, tcpprep) is not rendered - matching current behavior, since it turns out the existingagman-cmd.tplpipeline does not emit it into the committed man pages either (verified: none of that text appears in the committed.1files). Worth revisiting as a separate enhancement if that content should actually be surfaced.
Known scope boundary: tcpedit_stub.h stays autogen-only
src/tcpedit/tcpedit_stub.h is generated from tcpedit_stub.def using a
different AutoOpts template mode ("library" options, consumed by another
program's descriptor table via an exported ..._optDesc_p pointer) than
the seven prog-name-bearing tools emit_h.py targets: no copyright
block, no NLS section, no per-program identity, a LIBRARY_OPTION_COUNT
enum sentinel instead of the usual auto-option accounting. It is
different enough to warrant its own emitter rather than a variant of
emit_h.py, and building that was not part of the man-page decision this
stage was scoped to. Unlike everything else in this directory, it (and
its .1) stays committed, regenerated only when a .def it depends
on changes, with a clear failure (not silent staleness) if autogen is
absent when that happens - because autogen genuinely is EOL, which is
the reason phase 1 committed generated files in the first place. Revisit
as future work if full autogen elimination is wanted.
Running the gates
python3 scripts/autoopts/validate_ir.py # structural, cross-checked against emit_h's own output
python3 scripts/autoopts/check_emitters.py # smoke test: emitters run cleanly, output has the right shape
python3 scripts/autoopts/check_adoc.py # content + asciidoctor/groff validity
./scripts/check-generated-opts.sh # tcpedit_stub.h vs autogen (the one file still committed)
Method: oracle-driven equivalence (phase 2 development)
Historical note on how these emitters were built and validated, not a
description of what today's CI runs (there's no committed golden file to
diff against any more - see the "Not committed" note above). While
building each emitter, autogen 5.18.16 was still installed and the
generated files were briefly committed (phase 1's approach); every
emitter was iterated, diff-by-diff per tool (smallest first: tcpcapinfo
→ tcpliveplay → tcpprep → tcpreplay → tcprewrite → tcpbridge →
tcpreplay-edit), until its output was byte-identical to autogen's, using
check-generated-opts.sh's predecessor as the comparison harness. That
proof is what check_emitters.py's docstring and the Status section
below refer to as "byte-identical" / "the committed output."
Emitter notes (learned from the committed output)
- Headers are a fixed skeleton with computed spans: filenames,
prog-name casings, guard names, the INDEX enum (names padded to the
longest entry,
= %2d, doc entries skipped but consuming indexes), OPTION_CT, per-option VALUE_OPT_/OPT_VALUE_ defines withifdefguards from the def, exit-code enum, NLS block. License blurbs are AutoOpts' canned texts parameterized by copyright type/owner/date/prog — at leastgpl(most tools) andmbsd(tcpliveplay) are needed; copy them verbatim from the committed headers into a table. - .c files: single string pool
<prog>_opt_strs[N]with/* off */byte-offset comments — offsets are simple cumulative byte counts (NUL-terminated entries). Text from descrip/explain/detail is reflowed (~76 cols) — replicate autogen's paragraph wrap exactly (compare against committed strings). Then per-option#definename/desc blocks,tOptDescarray (flag masks derived from def attributes: immediate, max, arg-type, must/cant-sets, enabled state), callback functions (flag-code passthrough verbatim, doOptDbug-style range checkers generated), usage/version strings, translation (AO_...) block,tOptionsstruct. - Generation flags per tool are encoded in CASES inside
validate_ir.pyand in../check-generated-opts.sh. - The
-DTCPREPLAY_EDITvariant emits#ifdef-guarded sections in the output where the def guards flags withifdef/omitted-usage— grep the committed tcpreplay_edit_opts.c for#ifdefto see the pattern.
Ground rules
- Python 3 stdlib only (this must not add build dependencies).
- Never modify the vendored
libopts/runtime. - Keep
validate_ir.pypassing at every commit; add per-stage equivalence checks to CI as each emitter lands.