From 09fd4c28cccf2420339a7f56028679a385583d70 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Thu, 23 Jul 2026 11:00:28 -0400 Subject: [PATCH 1/2] [Homebrew] Provide a real manual-page viewer Package upstream mandoc 1.14.6 as the real man, formatter, lookup, and pager implementation. Cross-build with explicit Kandelo target capabilities, normal zlib and less dependencies, and required fork instrumentation for the pager path. Keep manual-page lookup deterministic over Homebrew MANPATH without generating a global database that would materialize every deferred bottle. Remove only the old posix-utils-lite man alias and prove Node, Chromium, section selection, roff formatting, and real PTY paging. --- Formula/mandoc.rb | 224 ++++++++++++++++++++++++++++++++++++ Formula/posix-utils-lite.rb | 2 +- 2 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 Formula/mandoc.rb diff --git a/Formula/mandoc.rb b/Formula/mandoc.rb new file mode 100644 index 00000000..4bfd6f8c --- /dev/null +++ b/Formula/mandoc.rb @@ -0,0 +1,224 @@ +require (Tap.fetch("kandelo-dev", "tap-core").path/"Kandelo/formula_support/kandelo_formula_support").to_s + +class Mandoc < Formula + include KandeloFormulaSupport + + GUEST_CELLAR = "/home/linuxbrew/.linuxbrew/Cellar".freeze + GUEST_MANPATH = "/home/linuxbrew/.linuxbrew/share/man:/usr/share/man".freeze + GUEST_OPT_PREFIX = "/home/linuxbrew/.linuxbrew/opt/mandoc".freeze + GUEST_PAGER = "/home/linuxbrew/.linuxbrew/opt/less/bin/less".freeze + + desc "Manual page formatter and viewer for Kandelo" + homepage "https://mandoc.bsd.lv/" + url "https://mandoc.bsd.lv/snapshots/mandoc-1.14.6.tar.gz" + sha256 "8bf0d570f01e70a6e124884088870cbed7537f36328d512909eb10cd53179d9c" + license "ISC" + + depends_on "binaryen" => :build + depends_on "wabt" => :build + depends_on "kandelo-dev/tap-core/less" + depends_on "kandelo-dev/tap-core/zlib" + + skip_clean "bin/mandoc", "bin/demandoc", "bin/soelim" + + def install + kandelo_require_arch!("wasm32") + zlib = formula_opt_prefix("kandelo-dev/tap-core/zlib") + + kandelo_wasm_build do + # Upstream treats its exact filename fallback as evidence that a global + # mandoc.db is stale. Kandelo intentionally does not build that database: + # doing so at runtime would enumerate and materialize manual pages from + # every deferred bottle. Keep upstream's deterministic + # MANPATH/man
/.
lookup without printing a + # misleading warning for this supported no-database layout. + fallback_warning = [ + "found:", + "\twarnx(\"outdated mandoc.db lacks %s(%s) entry, run %s %s\",", + "\t name, sec, BINM_MAKEWHATIS, paths->paths[ipath]);", + ].join("\n") + unless (buildpath/"main.c").read.include?(fallback_warning) + odie "Mandoc filename-fallback warning changed upstream" + end + inreplace buildpath/"main.c", fallback_warning, "found:" + + # Mandoc's custom configure compiles and executes every feature probe. + # A cross-build must therefore declare the target results explicitly; + # otherwise successful host probes silently describe macOS or Linux + # rather than Kandelo. + target_features = { + "WFLAG" => 1, + "STATIC" => 0, + "ATTRIBUTE" => 1, + "CMSG" => 1, + "DIRENT_NAMLEN" => 0, + "EFTYPE" => 0, + "ENDIAN" => 1, + "ERR" => 1, + "FTS" => 0, + "FTS_COMPARE_CONST" => 0, + "GETLINE" => 1, + "GETSUBOPT" => 1, + "ISBLANK" => 1, + "LESS_T" => 1, + "MKDTEMP" => 1, + "MKSTEMPS" => 0, + "NANOSLEEP" => 1, + "NTOHL" => 1, + "O_DIRECTORY" => 1, + "OHASH" => 0, + "PATH_MAX" => 1, + "PLEDGE" => 0, + "PROGNAME" => 0, + "REALLOCARRAY" => 0, + "RECALLOCARRAY" => 0, + "RECVMSG" => 1, + "REWB_BSD" => 0, + "REWB_SYSV" => 0, + "SANDBOX_INIT" => 0, + "STRCASESTR" => 0, + "STRINGLIST" => 0, + "STRLCAT" => 0, + "STRLCPY" => 0, + "STRNDUP" => 1, + "STRPTIME" => 1, + "STRSEP" => 1, + "STRTONUM" => 0, + "SYS_ENDIAN" => 0, + "VASPRINTF" => 1, + "WCHAR" => 1, + } + configure_lines = [ + "CC=#{ENV.fetch("CC").shellescape}", + "AR=#{ENV.fetch("AR").shellescape}", + "CFLAGS=\"-O2 -I#{zlib}/include\"", + "LDFLAGS=\"-L#{zlib}/lib\"", + "PREFIX=\"#{prefix}\"", + "MANDIR=\"#{man}\"", + "MANPATH_DEFAULT=\"#{GUEST_MANPATH}\"", + "MANPATH_BASE=\"#{GUEST_MANPATH}\"", + "READ_ALLOWED_PATH=\"#{GUEST_CELLAR}\"", + "BINM_PAGER=\"#{GUEST_PAGER}\"", + "OSENUM=MANDOC_OS_OTHER", + "OSNAME=\"Kandelo\"", + "UTF8_LOCALE=C.UTF-8", + "LN=\"ln -sf\"", + *target_features.map { |name, value| "HAVE_#{name}=#{value}" }, + ] + (buildpath/"configure.local").write("#{configure_lines.join("\n")}\n") + + system "./configure" + system "make", "-j#{ENV.make_jobs}" + + # man(1) starts the user's pager through the normal fork/exec path. + # Instrument the shared multicall binary before make creates its man, + # apropos, whatis, and makewhatis links. + kandelo_fork_instrument(buildpath/"mandoc") + + stage = buildpath/"kandelo-stage" + system "make", "install", "DESTDIR=#{stage}" + staged_prefix = stage/prefix.to_s.delete_prefix("/") + odie "Mandoc did not install into its staged prefix" unless staged_prefix.directory? + + kandelo_validate_wasm_artifact( + staged_prefix/"bin/mandoc", fork: :required, forbidden_paths: [zlib] + ) + %w[demandoc soelim].each do |name| + kandelo_validate_wasm_artifact( + staged_prefix/"bin"/name, fork: :forbidden, forbidden_paths: [zlib] + ) + end + prefix.install staged_prefix.children + end + end + + test do + %w[mandoc man apropos whatis demandoc soelim].each do |name| + assert_path_exists bin/name + end + assert_path_exists sbin/"makewhatis" + %w[mandoc.1 man.1 apropos.1 whatis.1 demandoc.1 soelim.1].each do |name| + assert_path_exists man1/name + end + assert_path_exists man5/"man.conf.5" + %w[man.7 mdoc.7 roff.7 eqn.7 tbl.7 mandoc_char.7].each do |name| + assert_path_exists man7/name + end + assert_path_exists man8/"makewhatis.8" + + manual_root = "/manual" + section_one = testpath/"hello.1" + section_five = testpath/"hello.5" + section_one.write <<~ROFF + .TH HELLO 1 "July 2026" "Kandelo" "General Commands Manual" + .SH NAME + hello \\- print a greeting + .SH DESCRIPTION + This text came from the section one page. + ROFF + section_five.write <<~ROFF + .TH HELLO 5 "July 2026" "Kandelo" "File Formats Manual" + .SH NAME + hello \\- describe the greeting configuration + .SH DESCRIPTION + This text came from the section five page. + ROFF + manual_files = { + "#{manual_root}/man1/hello.1" => section_one, + "#{manual_root}/man5/hello.5" => section_five, + } + env = { + "HOME" => "/", + "KERNEL_CWD" => "/", + "TERM" => "xterm-256color", + } + + first = kandelo_run_wasm( + bin/"man", ["-M", manual_root, "hello"], + argv0: "#{GUEST_OPT_PREFIX}/bin/man", env: env, guest_files: manual_files + ) + assert_includes first, "This text came from the section one page." + refute_includes first, ".TH HELLO" + + fifth = kandelo_run_wasm( + bin/"man", ["-M", manual_root, "5", "hello"], + argv0: "#{GUEST_OPT_PREFIX}/bin/man", env: env, guest_files: manual_files + ) + assert_includes fifth, "This text came from the section five page." + refute_includes fifth, "This text came from the section one page." + + default_path = kandelo_run_wasm( + bin/"man", ["1", "hello"], + argv0: "#{GUEST_OPT_PREFIX}/bin/man", env: env, merge_stderr: true, + guest_files: { "/home/linuxbrew/.linuxbrew/share/man/man1/hello.1" => section_one } + ) + assert_includes default_path, "This text came from the section one page." + refute_includes default_path, "outdated mandoc.db" + + browser = kandelo_run_browser_wasm( + bin/"man", ["-M", manual_root, "1", "hello"], + argv0: "man", guest_program_path: "#{GUEST_OPT_PREFIX}/bin/man", env: env, + guest_files: { "#{manual_root}/man1/hello.1" => section_one }, + merge_stderr: true + ) + assert_includes browser, "This text came from the section one page." + refute_includes browser, ".TH HELLO" + + less = formula_opt_bin("kandelo-dev/tap-core/less")/"less" + transcript = kandelo_run_pty_wasm( + bin/"man", ["-M", manual_root, "1", "hello"], + argv0: "#{GUEST_OPT_PREFIX}/bin/man", env: env, + exec_programs: { GUEST_PAGER => less }, + guest_files: { "#{manual_root}/man1/hello.1" => section_one }, + inputs: ["q"], expected_fork_descendants: 1, initial_delay_ms: 100 + ) + assert_includes transcript, "This text came from the section one page." + + bytes = File.binread(bin/"mandoc") + assert_includes bytes, GUEST_MANPATH + assert_includes bytes, GUEST_PAGER + [buildpath, prefix, zlib].each do |path| + refute_includes bytes, path.to_s + end + end +end diff --git a/Formula/posix-utils-lite.rb b/Formula/posix-utils-lite.rb index 627c72ed..a5be8093 100644 --- a/Formula/posix-utils-lite.rb +++ b/Formula/posix-utils-lite.rb @@ -7,7 +7,7 @@ class PosixUtilsLite < Formula UTILITIES = %w[ ar asa cal cflow compress ctags cxref ed ex fuser gencat getconf gettext - iconv ipcrm ipcs lex locale logger man more msgfmt ngettext nm patch pax + iconv ipcrm ipcs lex locale logger more msgfmt ngettext nm patch pax pgrep ps renice strings strip uncompress uudecode uuencode what xgettext yacc ].freeze From 694ab114827e895ee87af842be20c62f5eee692a Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Thu, 23 Jul 2026 13:14:15 -0400 Subject: [PATCH 2/2] [Homebrew/Test] Prove the complete manual-page lifecycle Exercise section lookup, aliases, compressed and mdoc pages, helper tools, database-backed search, and the exact Homebrew Cellar symlink layout. Bump posix-utils-lite so its next bottle truthfully drops the retired placeholder man command. --- Formula/mandoc.rb | 131 +++++++++++++++++++++++++++++++++--- Formula/posix-utils-lite.rb | 3 +- README.md | 3 + 3 files changed, 127 insertions(+), 10 deletions(-) diff --git a/Formula/mandoc.rb b/Formula/mandoc.rb index 4bfd6f8c..7a82dc6f 100644 --- a/Formula/mandoc.rb +++ b/Formula/mandoc.rb @@ -1,12 +1,14 @@ require (Tap.fetch("kandelo-dev", "tap-core").path/"Kandelo/formula_support/kandelo_formula_support").to_s +require "zlib" class Mandoc < Formula include KandeloFormulaSupport - GUEST_CELLAR = "/home/linuxbrew/.linuxbrew/Cellar".freeze - GUEST_MANPATH = "/home/linuxbrew/.linuxbrew/share/man:/usr/share/man".freeze - GUEST_OPT_PREFIX = "/home/linuxbrew/.linuxbrew/opt/mandoc".freeze - GUEST_PAGER = "/home/linuxbrew/.linuxbrew/opt/less/bin/less".freeze + GUEST_PREFIX = "/home/linuxbrew/.linuxbrew".freeze + GUEST_CELLAR = "#{GUEST_PREFIX}/Cellar".freeze + GUEST_MANPATH = "#{GUEST_PREFIX}/share/man:/usr/share/man".freeze + GUEST_OPT_PREFIX = "#{GUEST_PREFIX}/opt/mandoc".freeze + GUEST_PAGER = "#{GUEST_PREFIX}/opt/less/bin/less".freeze desc "Manual page formatter and viewer for Kandelo" homepage "https://mandoc.bsd.lv/" @@ -149,6 +151,11 @@ def install manual_root = "/manual" section_one = testpath/"hello.1" section_five = testpath/"hello.5" + alias_page = testpath/"hello-alias.1" + compressed_page = testpath/"compressed.1.gz" + mdoc_page = testpath/"kandelo.7" + soelim_page = testpath/"soelim-main.roff" + soelim_include = testpath/"soelim-include.roff" section_one.write <<~ROFF .TH HELLO 1 "July 2026" "Kandelo" "General Commands Manual" .SH NAME @@ -163,9 +170,36 @@ def install .SH DESCRIPTION This text came from the section five page. ROFF + alias_page.write ".so man1/hello.1\n" + Zlib::GzipWriter.open(compressed_page) do |gzip| + gzip.write <<~ROFF + .TH COMPRESSED 1 "July 2026" "Kandelo" "General Commands Manual" + .SH NAME + compressed \\- exercise a compressed bottle manual + .SH DESCRIPTION + This text came from the compressed page. + ROFF + end + mdoc_page.write <<~MDOC + .Dd July 23, 2026 + .Dt KANDELO 7 + .Os Kandelo + .Sh NAME + .Nm kandelo + .Nd run POSIX software in WebAssembly + .Sh DESCRIPTION + This text came from the mdoc page. + MDOC + soelim_page.write "before include\n.so soelim-include.roff\nafter include\n" + soelim_include.write "included text\n" manual_files = { - "#{manual_root}/man1/hello.1" => section_one, - "#{manual_root}/man5/hello.5" => section_five, + "#{manual_root}/man1/compressed.1.gz" => compressed_page, + "#{manual_root}/man1/hello-alias.1" => alias_page, + "#{manual_root}/man1/hello.1" => section_one, + "#{manual_root}/man5/hello.5" => section_five, + "#{manual_root}/man7/kandelo.7" => mdoc_page, + "#{manual_root}/soelim-include.roff" => soelim_include, + "#{manual_root}/soelim-main.roff" => soelim_page, } env = { "HOME" => "/", @@ -187,14 +221,93 @@ def install assert_includes fifth, "This text came from the section five page." refute_includes fifth, "This text came from the section one page." + assert_equal "#{manual_root}/man1/hello.1\n", kandelo_run_wasm( + bin/"man", ["-M", manual_root, "-w", "1", "hello"], + argv0: "#{GUEST_OPT_PREFIX}/bin/man", env: env, guest_files: manual_files + ) + + alias_output = kandelo_run_wasm( + bin/"man", ["-M", manual_root, "hello-alias"], + argv0: "#{GUEST_OPT_PREFIX}/bin/man", env: env, guest_files: manual_files + ) + assert_includes alias_output, "This text came from the section one page." + + compressed_output = kandelo_run_wasm( + bin/"man", ["-M", manual_root, "compressed"], + argv0: "#{GUEST_OPT_PREFIX}/bin/man", env: env, guest_files: manual_files + ) + assert_includes compressed_output, "This text came from the compressed page." + + mdoc_output = kandelo_run_wasm( + bin/"man", ["-M", manual_root, "7", "kandelo"], + argv0: "#{GUEST_OPT_PREFIX}/bin/man", env: env, guest_files: manual_files + ) + assert_includes mdoc_output, "This text came from the mdoc page." + + demandoc_output = kandelo_run_wasm(bin/"demandoc", [], stdin: section_one.read) + assert_includes demandoc_output, "print a greeting" + refute_includes demandoc_output, ".TH HELLO" + + soelim_output = kandelo_run_wasm( + bin/"soelim", ["soelim-main.roff"], + env: env.merge("KERNEL_CWD" => manual_root), + guest_files: manual_files + ) + assert_equal "before include\nincluded text\nafter include\n", soelim_output + + missing_output = kandelo_run_wasm( + bin/"man", ["-M", manual_root, "missing"], + argv0: "#{GUEST_OPT_PREFIX}/bin/man", env: env, merge_stderr: true, + guest_files: manual_files, expected_status: 5 + ) + assert_match(/No entry for missing in the manual/, missing_output) + + # Homebrew keeps a package's page in its versioned keg and exposes the + # ordinary global MANPATH entry as a relative symlink. This fixture matches + # the composed VFS shape: resolving the link is metadata-only, while + # opening its Cellar target is what materializes the owning bottle. + homebrew_prefix = testpath/"homebrew-prefix" + bottled_page = homebrew_prefix/"Cellar/bottled/1.0/share/man/man1/bottled.1" + bottled_link = homebrew_prefix/"share/man/man1/bottled.1" + bottled_page.dirname.mkpath + bottled_link.dirname.mkpath + bottled_page.write( + section_one.read + .sub("HELLO", "BOTTLED") + .sub("hello \\- print a greeting", "bottled \\- read a linked bottle page") + .sub("section one page", "linked bottle page"), + ) + bottled_link.make_symlink("../../../Cellar/bottled/1.0/share/man/man1/bottled.1") + prefix_mount = { GUEST_PREFIX => homebrew_prefix.realpath } default_path = kandelo_run_wasm( - bin/"man", ["1", "hello"], + bin/"man", ["1", "bottled"], argv0: "#{GUEST_OPT_PREFIX}/bin/man", env: env, merge_stderr: true, - guest_files: { "/home/linuxbrew/.linuxbrew/share/man/man1/hello.1" => section_one } + writable_host_directories: prefix_mount ) - assert_includes default_path, "This text came from the section one page." + assert_includes default_path, "This text came from the linked bottle page." refute_includes default_path, "outdated mandoc.db" + # Database-backed lookup is optional for viewing but remains a supported + # user-facing surface for apropos(1), whatis(1), and makewhatis(8). + assert_empty kandelo_run_wasm( + sbin/"makewhatis", ["#{GUEST_PREFIX}/share/man"], + argv0: "#{GUEST_OPT_PREFIX}/sbin/makewhatis", env: env, + writable_host_directories: prefix_mount + ) + assert_path_exists homebrew_prefix/"share/man/mandoc.db" + whatis_output = kandelo_run_wasm( + bin/"whatis", ["-M", "#{GUEST_PREFIX}/share/man", "bottled"], + argv0: "#{GUEST_OPT_PREFIX}/bin/whatis", env: env, + writable_host_directories: prefix_mount + ) + assert_match(/^bottled\(1\) - read a linked bottle page$/, whatis_output) + apropos_output = kandelo_run_wasm( + bin/"apropos", ["-M", "#{GUEST_PREFIX}/share/man", "linked bottle"], + argv0: "#{GUEST_OPT_PREFIX}/bin/apropos", env: env, + writable_host_directories: prefix_mount + ) + assert_match(/^bottled\(1\) - read a linked bottle page$/, apropos_output) + browser = kandelo_run_browser_wasm( bin/"man", ["-M", manual_root, "1", "hello"], argv0: "man", guest_program_path: "#{GUEST_OPT_PREFIX}/bin/man", env: env, diff --git a/Formula/posix-utils-lite.rb b/Formula/posix-utils-lite.rb index a5be8093..f02d9693 100644 --- a/Formula/posix-utils-lite.rb +++ b/Formula/posix-utils-lite.rb @@ -18,6 +18,7 @@ class PosixUtilsLite < Formula version "0.1.0" sha256 "07e7a7ebff8003114f6b4bef1ccdc2e9b15ecfbd5e6ccc3bf8563107b8151fde" license "GPL-2.0-or-later" + revision 1 depends_on "binaryen" => :build depends_on "wabt" => :build @@ -27,7 +28,7 @@ class PosixUtilsLite < Formula def install kandelo_require_arch!("wasm32") - # Transitional Tier-2 bridge: keep the current 37-command multicall + # Transitional Tier-2 bridge: keep the remaining 36-command multicall # recipe intact for the exact-shell proof. Splitting commands into their # maintained upstream Formulae remains explicit migration debt. out_dir = kandelo_build_package(script_env: {}) diff --git a/README.md b/README.md index 77a37279..e91c6aaf 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,9 @@ Formula source currently present in this repository includes: - `curl`, the matching command-line transfer client linked against the tap library; - `ncurses`, the wide-character terminal library and CLI dependency root; - `less` and its upstream `more` compatibility mode, terminal pagers linked against the tap's real ncurses termcap interface; +- `mandoc`, the normal `man`, `apropos`, `whatis`, and `makewhatis` toolset, + with roff formatting, section lookup, compressed-page support, and real + terminal paging through the tap's `less`; - `bash`, the GNU interactive shell with real pipelines, subprocesses, and process substitution; - `sed`, the GNU stream-editing CLI used by shell and build workflows; - `gzip`, the GNU compression CLI with native gunzip and zcat aliases;