From 566243c87d79445e64c89cc2369304a943b0f548 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Tue, 30 Jun 2026 10:37:32 -0400 Subject: [PATCH] Port interactive packages to Homebrew --- docs/homebrew-publishing.md | 32 + homebrew/kandelo-homebrew/Formula/dash.rb | 33 + homebrew/kandelo-homebrew/Formula/fbdoom.rb | 31 + homebrew/kandelo-homebrew/Formula/git.rb | 39 + homebrew/kandelo-homebrew/Formula/less.rb | 33 + homebrew/kandelo-homebrew/Formula/lsof.rb | 32 + homebrew/kandelo-homebrew/Formula/modeset.rb | 32 + homebrew/kandelo-homebrew/Formula/msmtpd.rb | 32 + homebrew/kandelo-homebrew/Formula/nano.rb | 33 + homebrew/kandelo-homebrew/Formula/netcat.rb | 34 + homebrew/kandelo-homebrew/Formula/vim.rb | 34 + homebrew/kandelo-homebrew/Formula/wget.rb | 33 + .../Kandelo/kandelo_formula_support.rb | 73 ++ packages/registry/dash/build-dash.sh | 14 +- packages/registry/fbdoom/build-fbdoom.sh | 18 +- packages/registry/git/build-git.sh | 5 +- packages/registry/less/build-less.sh | 9 +- packages/registry/lsof/build-lsof.sh | 4 +- packages/registry/modeset/build-modeset.sh | 13 +- packages/registry/msmtpd/build-msmtpd.sh | 25 +- packages/registry/nano/build-nano.sh | 7 +- packages/registry/netcat/build-netcat.sh | 41 +- .../patches/udp-listen-single-socket.patch | 9 +- packages/registry/vim/build-vim.sh | 12 +- packages/registry/vim/bundle-runtime.sh | 4 +- packages/registry/wget/build-wget.sh | 7 +- scripts/homebrew-bottle-build.sh | 27 +- .../homebrew-generate-sidecars-from-env.sh | 184 +++-- scripts/homebrew-package-node-smoke.ts | 736 ++++++++++++++++++ scripts/install-local-binary.sh | 23 +- sdk/src/bin/pkg-config.ts | 3 +- 31 files changed, 1473 insertions(+), 139 deletions(-) create mode 100644 homebrew/kandelo-homebrew/Formula/dash.rb create mode 100644 homebrew/kandelo-homebrew/Formula/fbdoom.rb create mode 100644 homebrew/kandelo-homebrew/Formula/git.rb create mode 100644 homebrew/kandelo-homebrew/Formula/less.rb create mode 100644 homebrew/kandelo-homebrew/Formula/lsof.rb create mode 100644 homebrew/kandelo-homebrew/Formula/modeset.rb create mode 100644 homebrew/kandelo-homebrew/Formula/msmtpd.rb create mode 100644 homebrew/kandelo-homebrew/Formula/nano.rb create mode 100644 homebrew/kandelo-homebrew/Formula/netcat.rb create mode 100644 homebrew/kandelo-homebrew/Formula/vim.rb create mode 100644 homebrew/kandelo-homebrew/Formula/wget.rb create mode 100644 homebrew/kandelo-homebrew/Kandelo/kandelo_formula_support.rb create mode 100644 scripts/homebrew-package-node-smoke.ts diff --git a/docs/homebrew-publishing.md b/docs/homebrew-publishing.md index d14760066e..c1e180d9e6 100644 --- a/docs/homebrew-publishing.md +++ b/docs/homebrew-publishing.md @@ -215,6 +215,18 @@ path calls `scripts/homebrew-publish-sidecars.sh --status failed` so the failed attempt is durable while the previous successful bottle remains selectable when its fallback fields are complete. +The default trusted sidecar wrapper, +`scripts/homebrew-generate-sidecars-from-env.sh`, derives non-hello link +manifests from package kind. Program Formulae link their installed +`bin/` into the Homebrew prefix. Library Formulae link the declared +`[outputs]` headers, static libraries, and pkg-config files from the keg into +the prefix. Package-specific Node and browser outcome text may be supplied via +`KANDELO_HOMEBREW_NODE_SMOKE_COMMAND`, +`KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS`, and +`KANDELO_HOMEBREW_BROWSER_SMOKE_REASON`; browser compatibility is recorded +only when `KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS=success` and the browser VFS +smoke artifact environment is complete. + ## VFS Planning And Building Homebrew-derived VFS images are built from sidecars and verified bottle bytes, @@ -265,6 +277,26 @@ It clones or reads the tap, builds a Homebrew VFS from published sidecars, runs `/home/linuxbrew/.linuxbrew/bin/hello --version` through `NodeKernelHost`, and checks negative ABI-mismatch and missing-bottle cases. +For the sqlite/bzip2/xz pilot and later non-hello package checks, use the +generic package smoke runner against a generated tap root: + +```bash +npx tsx scripts/homebrew-package-node-smoke.ts \ + --tap-root /path/to/kandelo-homebrew \ + --formula sqlite \ + --formula bzip2 \ + --formula xz \ + --arch wasm32 \ + --result-dir test-runs/homebrew-package-node-smoke +``` + +The runner builds Homebrew VFS images from sidecars, writes passed, failed, +and skipped outcome lists, runs program package version smokes from the poured +prefix, and compiles SQLite's `sqlite_basic.c` against the poured headers and +static library before running the validation Wasm on Node. Dry-run bottle +evidence remains local evidence until the trusted workflow publishes GHCR +bottle bytes and tap sidecars. + Browser compatibility requires a separate browser smoke. For the current `hello` path, the trusted publisher builds a precomposed wasm32 VFS image, serves it through the browser demo, runs Chromium Playwright against diff --git a/homebrew/kandelo-homebrew/Formula/dash.rb b/homebrew/kandelo-homebrew/Formula/dash.rb new file mode 100644 index 0000000000..bf2dd6847a --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/dash.rb @@ -0,0 +1,33 @@ +require "shellwords" +require_relative "../Kandelo/kandelo_formula_support" + +class Dash < Formula + include KandeloFormulaSupport + + desc "POSIX shell for Kandelo" + homepage "http://gondor.apana.org.au/~herbert/dash/" + url "http://gondor.apana.org.au/~herbert/dash/files/dash-0.5.12.tar.gz" + sha256 "6a474ac46e8b0b32916c4c60df694c82058d3297d8b385b74508030ca4a8f28a" + license all_of: ["BSD-3-Clause", "GPL-2.0-or-later"] + + bottle do + root_url "https://ghcr.io/v2/automattic/kandelo-homebrew" + sha256 cellar: :any_skip_relocation, wasm32_kandelo: "b07b39e7eb19324f49bd6ab23cad020354f4bcbf5602a6b6936a2633969888b8" + end + + skip_clean "bin/dash" + + def install + root = kandelo_root + configure_kandelo_environment(root) + ENV["DASH_VERSION"] = version.to_s + out_dir = prepare_kandelo_package_env(source_url: "http://gondor.apana.org.au/~herbert/dash/files/dash-0.5.12.tar.gz", source_sha256: "6a474ac46e8b0b32916c4c60df694c82058d3297d8b385b74508030ca4a8f28a") + system "bash", "#{root}/packages/registry/dash/build-dash.sh" + install_kandelo_wasm(out_dir, "dash.wasm", "dash") + end + + test do + assert_kandelo_wasm "dash" + assert_match "dash-homebrew-smoke", shell_output_kandelo_wasm("dash", ["-c", "echo dash-homebrew-smoke"]) + end +end diff --git a/homebrew/kandelo-homebrew/Formula/fbdoom.rb b/homebrew/kandelo-homebrew/Formula/fbdoom.rb new file mode 100644 index 0000000000..67de69781e --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/fbdoom.rb @@ -0,0 +1,31 @@ +require "shellwords" +require_relative "../Kandelo/kandelo_formula_support" + +class Fbdoom < Formula + include KandeloFormulaSupport + + desc "fbDOOM framebuffer game port for Kandelo" + homepage "https://github.com/maximevince/fbDOOM" + url "https://github.com/maximevince/fbDOOM.git", branch: "master" + version "0.1.0" + license "GPL-2.0-or-later" + + bottle do + root_url "https://ghcr.io/v2/automattic/kandelo-homebrew" + sha256 cellar: :any_skip_relocation, wasm32_kandelo: "51f8e3d28b47a182dbee12dd6eb25ab1a39c4c11cb6f78da51183d442796173c" + end + + skip_clean "bin/fbdoom" + + def install + root = kandelo_root + configure_kandelo_environment(root) + out_dir = prepare_kandelo_package_env(source_url: "https://github.com/maximevince/fbDOOM", source_sha256: "0000000000000000000000000000000000000000000000000000000000000000") + system "bash", "#{root}/packages/registry/fbdoom/build-fbdoom.sh" + install_kandelo_wasm(out_dir, "fbdoom.wasm", "fbdoom") + end + + test do + assert_kandelo_wasm "fbdoom" + end +end diff --git a/homebrew/kandelo-homebrew/Formula/git.rb b/homebrew/kandelo-homebrew/Formula/git.rb new file mode 100644 index 0000000000..600a548948 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/git.rb @@ -0,0 +1,39 @@ +require "shellwords" +require_relative "../Kandelo/kandelo_formula_support" + +class Git < Formula + include KandeloFormulaSupport + + desc "Distributed revision control system for Kandelo" + homepage "https://git-scm.com/" + url "https://github.com/git/git/archive/refs/tags/v2.47.1.tar.gz" + sha256 "30654cc6c0142fa68050cc7b0ee6a2a65944288b907821781112efaf24293b23" + license "GPL-2.0-only" + + bottle do + root_url "https://ghcr.io/v2/automattic/kandelo-homebrew" + sha256 cellar: :any_skip_relocation, wasm32_kandelo: "2ba2c7ce61393c99a06aff34744973b55bbd9e28ded2303e2f98d2363b39295f" + end + + skip_clean "bin/git" + skip_clean "bin/git-remote-http" + + def install + root = kandelo_root + configure_kandelo_environment(root) + ENV["GIT_VERSION"] = version.to_s + out_dir = prepare_kandelo_package_env(source_url: "https://github.com/git/git/archive/refs/tags/v2.47.1.tar.gz", source_sha256: "30654cc6c0142fa68050cc7b0ee6a2a65944288b907821781112efaf24293b23") + system "bash", "#{root}/packages/registry/git/build-git.sh" + install_kandelo_wasm(out_dir, "git.wasm", "git") + install_kandelo_wasm(out_dir, "git-remote-http.wasm", "git-remote-http") + bin.install_symlink "git-remote-http" => "git-remote-https" + bin.install_symlink "git-remote-http" => "git-remote-ftp" + bin.install_symlink "git-remote-http" => "git-remote-ftps" + end + + test do + assert_kandelo_wasm "git" + assert_kandelo_wasm "git-remote-http" + assert_match "git version", shell_output_kandelo_wasm("git", ["--version"]) + end +end diff --git a/homebrew/kandelo-homebrew/Formula/less.rb b/homebrew/kandelo-homebrew/Formula/less.rb new file mode 100644 index 0000000000..02a9a7f172 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/less.rb @@ -0,0 +1,33 @@ +require "shellwords" +require_relative "../Kandelo/kandelo_formula_support" + +class Less < Formula + include KandeloFormulaSupport + + desc "Terminal pager for Kandelo" + homepage "https://www.greenwoodsoftware.com/less/" + url "https://www.greenwoodsoftware.com/less/less-668.tar.gz" + sha256 "2819f55564d86d542abbecafd82ff61e819a3eec967faa36cd3e68f1596a44b8" + license "GPL-3.0-or-later" + + bottle do + root_url "https://ghcr.io/v2/automattic/kandelo-homebrew" + sha256 cellar: :any_skip_relocation, wasm32_kandelo: "ee8799015db23b073bb5f6f3c16953c07613f1c5dd16488cca16c9008da6c061" + end + + skip_clean "bin/less" + + def install + root = kandelo_root + configure_kandelo_environment(root) + ENV["LESS_VERSION"] = version.to_s + out_dir = prepare_kandelo_package_env(source_url: "https://www.greenwoodsoftware.com/less/less-668.tar.gz", source_sha256: "2819f55564d86d542abbecafd82ff61e819a3eec967faa36cd3e68f1596a44b8") + system "bash", "#{root}/packages/registry/less/build-less.sh" + install_kandelo_wasm(out_dir, "less.wasm", "less") + end + + test do + assert_kandelo_wasm "less" + assert_match "less", shell_output_kandelo_wasm("less", ["--version"]) + end +end diff --git a/homebrew/kandelo-homebrew/Formula/lsof.rb b/homebrew/kandelo-homebrew/Formula/lsof.rb new file mode 100644 index 0000000000..c839f2f550 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/lsof.rb @@ -0,0 +1,32 @@ +require "shellwords" +require_relative "../Kandelo/kandelo_formula_support" + +class Lsof < Formula + include KandeloFormulaSupport + + desc "Kandelo /proc file descriptor lister" + homepage "https://github.com/brandonpayton/kandelo" + url "https://github.com/Automattic/kandelo/archive/1ab41fe2ad5553f4fa4bb0223f2d804b13149578.tar.gz" + sha256 "446dd26b6e3f909f25f21f77950a3b11a07c633721dbff698a85b4f05fbbc493" + version "0.1.0" + license "GPL-2.0-or-later" + + bottle do + root_url "https://ghcr.io/v2/automattic/kandelo-homebrew" + sha256 cellar: :any_skip_relocation, wasm32_kandelo: "104cc2d3f485eb2da330996de062cde2574aa28d693c60d875b7af0c0d534a6b" + end + + skip_clean "bin/lsof" + + def install + root = kandelo_root + configure_kandelo_environment(root) + out_dir = prepare_kandelo_package_env(source_url: "https://github.com/Automattic/kandelo/archive/1ab41fe2ad5553f4fa4bb0223f2d804b13149578.tar.gz", source_sha256: "446dd26b6e3f909f25f21f77950a3b11a07c633721dbff698a85b4f05fbbc493") + system "bash", "#{root}/packages/registry/lsof/build-lsof.sh" + install_kandelo_wasm(out_dir, "lsof.wasm", "lsof") + end + + test do + assert_kandelo_wasm "lsof" + end +end diff --git a/homebrew/kandelo-homebrew/Formula/modeset.rb b/homebrew/kandelo-homebrew/Formula/modeset.rb new file mode 100644 index 0000000000..6e0fe049f2 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/modeset.rb @@ -0,0 +1,32 @@ +require "shellwords" +require_relative "../Kandelo/kandelo_formula_support" + +class Modeset < Formula + include KandeloFormulaSupport + + desc "DRI/GLES framebuffer fluid simulation for Kandelo" + homepage "https://github.com/brandonpayton/kandelo" + url "https://github.com/Automattic/kandelo/archive/1ab41fe2ad5553f4fa4bb0223f2d804b13149578.tar.gz" + sha256 "446dd26b6e3f909f25f21f77950a3b11a07c633721dbff698a85b4f05fbbc493" + version "0.1.0" + license "GPL-2.0-or-later" + + bottle do + root_url "https://ghcr.io/v2/automattic/kandelo-homebrew" + sha256 cellar: :any_skip_relocation, wasm32_kandelo: "48002a330c17971fed6d7edf71872e0cce9552c8bfb3b18813092f936e14ca88" + end + + skip_clean "bin/modeset" + + def install + root = kandelo_root + configure_kandelo_environment(root) + out_dir = prepare_kandelo_package_env(source_url: "https://github.com/Automattic/kandelo/archive/1ab41fe2ad5553f4fa4bb0223f2d804b13149578.tar.gz", source_sha256: "446dd26b6e3f909f25f21f77950a3b11a07c633721dbff698a85b4f05fbbc493") + system "bash", "#{root}/packages/registry/modeset/build-modeset.sh" + install_kandelo_wasm(out_dir, "modeset.wasm", "modeset") + end + + test do + assert_kandelo_wasm "modeset" + end +end diff --git a/homebrew/kandelo-homebrew/Formula/msmtpd.rb b/homebrew/kandelo-homebrew/Formula/msmtpd.rb new file mode 100644 index 0000000000..72d447d348 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/msmtpd.rb @@ -0,0 +1,32 @@ +require "shellwords" +require_relative "../Kandelo/kandelo_formula_support" + +class Msmtpd < Formula + include KandeloFormulaSupport + + desc "SMTP capture daemon from msmtp for Kandelo" + homepage "https://marlam.de/msmtp/" + url "https://marlam.de/msmtp/releases/msmtp-1.8.32.tar.xz" + sha256 "20cd58b58dd007acf7b937fa1a1e21f3afb3e9ef5bbcfb8b4f5650deadc64db4" + license "GPL-3.0-or-later" + + bottle do + root_url "https://ghcr.io/v2/automattic/kandelo-homebrew" + sha256 cellar: :any_skip_relocation, wasm32_kandelo: "ff2c0e456efbfe2d03b30dea2f8462a7f3818146715e3dc78ab5f21580a88691" + end + + skip_clean "bin/msmtpd" + + def install + root = kandelo_root + configure_kandelo_environment(root) + out_dir = prepare_kandelo_package_env(source_url: "https://marlam.de/msmtp/releases/msmtp-1.8.32.tar.xz", source_sha256: "20cd58b58dd007acf7b937fa1a1e21f3afb3e9ef5bbcfb8b4f5650deadc64db4") + system "bash", "#{root}/packages/registry/msmtpd/build-msmtpd.sh" + install_kandelo_wasm(out_dir, "msmtpd.wasm", "msmtpd") + end + + test do + assert_kandelo_wasm "msmtpd" + assert_match(/msmtp|msmtpd/i, shell_output_kandelo_wasm("msmtpd", ["--version"])) + end +end diff --git a/homebrew/kandelo-homebrew/Formula/nano.rb b/homebrew/kandelo-homebrew/Formula/nano.rb new file mode 100644 index 0000000000..cf2188889a --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/nano.rb @@ -0,0 +1,33 @@ +require "shellwords" +require_relative "../Kandelo/kandelo_formula_support" + +class Nano < Formula + include KandeloFormulaSupport + + desc "Terminal text editor for Kandelo" + homepage "https://www.nano-editor.org/" + url "https://ftpmirror.gnu.org/gnu/nano/nano-8.0.tar.xz" + sha256 "c17f43fc0e37336b33ee50a209c701d5beb808adc2d9f089ca831b40539c9ac4" + license "GPL-3.0-or-later" + + bottle do + root_url "https://ghcr.io/v2/automattic/kandelo-homebrew" + sha256 cellar: :any_skip_relocation, wasm32_kandelo: "5c9724cea03f33dbf62b4c3edafdc797d37f2ce060a8456fee3c9f50406db2f0" + end + + skip_clean "bin/nano" + + def install + root = kandelo_root + configure_kandelo_environment(root) + ENV["NANO_VERSION"] = version.to_s + out_dir = prepare_kandelo_package_env(source_url: "https://ftpmirror.gnu.org/gnu/nano/nano-8.0.tar.xz", source_sha256: "c17f43fc0e37336b33ee50a209c701d5beb808adc2d9f089ca831b40539c9ac4") + system "bash", "#{root}/packages/registry/nano/build-nano.sh" + install_kandelo_wasm(out_dir, "nano.wasm", "nano") + end + + test do + assert_kandelo_wasm "nano" + assert_match(/GNU nano|nano/i, shell_output_kandelo_wasm("nano", ["--version"])) + end +end diff --git a/homebrew/kandelo-homebrew/Formula/netcat.rb b/homebrew/kandelo-homebrew/Formula/netcat.rb new file mode 100644 index 0000000000..b310390ee4 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/netcat.rb @@ -0,0 +1,34 @@ +require "shellwords" +require_relative "../Kandelo/kandelo_formula_support" + +class Netcat < Formula + include KandeloFormulaSupport + + desc "GNU netcat network utility for Kandelo" + homepage "https://sourceforge.net/projects/netcat/" + url "https://downloads.sourceforge.net/project/netcat/netcat/0.7.1/netcat-0.7.1.tar.gz" + sha256 "30719c9a4ffbcf15676b8f528233ccc54ee6cba96cb4590975f5fd60c68a066f" + license "GPL-2.0-or-later" + + bottle do + root_url "https://ghcr.io/v2/automattic/kandelo-homebrew" + sha256 cellar: :any_skip_relocation, wasm32_kandelo: "2eec1a038a610fca2ab8d2cc6b2feff048b009288f3fa249cbbd586107bb0efd" + end + + skip_clean "bin/nc" + + def install + root = kandelo_root + configure_kandelo_environment(root) + ENV["NETCAT_VERSION"] = version.to_s + out_dir = prepare_kandelo_package_env(source_url: "https://downloads.sourceforge.net/project/netcat/netcat/0.7.1/netcat-0.7.1.tar.gz", source_sha256: "30719c9a4ffbcf15676b8f528233ccc54ee6cba96cb4590975f5fd60c68a066f") + system "bash", "#{root}/packages/registry/netcat/build-netcat.sh" + install_kandelo_wasm(out_dir, "nc.wasm", "nc") + bin.install_symlink "nc" => "netcat" + end + + test do + assert_kandelo_wasm "nc" + assert_match(/netcat|listen|connect|usage/i, shell_output_kandelo_wasm("nc", ["--help"])) + end +end diff --git a/homebrew/kandelo-homebrew/Formula/vim.rb b/homebrew/kandelo-homebrew/Formula/vim.rb new file mode 100644 index 0000000000..461cfcf70f --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/vim.rb @@ -0,0 +1,34 @@ +require "shellwords" +require_relative "../Kandelo/kandelo_formula_support" + +class Vim < Formula + include KandeloFormulaSupport + + desc "Vim terminal editor for Kandelo" + homepage "https://www.vim.org/" + url "https://github.com/vim/vim/archive/refs/tags/v9.1.0900.tar.gz" + sha256 "30efb714ed82c5d7a1491f3e4aac6487d2c493d33c834d7ef043e6f45176772e" + license "Vim" + + bottle do + root_url "https://ghcr.io/v2/automattic/kandelo-homebrew" + sha256 cellar: :any_skip_relocation, wasm32_kandelo: "778be8fe04ce0439b260ed72c9bd81b163c60badd9e61c864864828198fee6ae" + end + + skip_clean "bin/vim" + + def install + root = kandelo_root + configure_kandelo_environment(root) + ENV["VIM_VERSION"] = version.to_s + out_dir = prepare_kandelo_package_env(source_url: "https://github.com/vim/vim/archive/refs/tags/v9.1.0900.tar.gz", source_sha256: "30efb714ed82c5d7a1491f3e4aac6487d2c493d33c834d7ef043e6f45176772e") + system "bash", "#{root}/packages/registry/vim/build-vim.sh" + install_kandelo_wasm(out_dir, "vim.wasm", "vim") + (share/"vim").install out_dir/"runtime" if (out_dir/"runtime").directory? + end + + test do + assert_kandelo_wasm "vim" + assert_match(/VIM|Vi IMproved/i, shell_output_kandelo_wasm("vim", ["--version"])) + end +end diff --git a/homebrew/kandelo-homebrew/Formula/wget.rb b/homebrew/kandelo-homebrew/Formula/wget.rb new file mode 100644 index 0000000000..ade787e421 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/wget.rb @@ -0,0 +1,33 @@ +require "shellwords" +require_relative "../Kandelo/kandelo_formula_support" + +class Wget < Formula + include KandeloFormulaSupport + + desc "GNU Wget network downloader for Kandelo" + homepage "https://www.gnu.org/software/wget/" + url "https://ftpmirror.gnu.org/gnu/wget/wget-1.25.0.tar.gz" + sha256 "766e48423e79359ea31e41db9e5c289675947a7fcf2efdcedb726ac9d0da3784" + license "GPL-3.0-or-later" + + bottle do + root_url "https://ghcr.io/v2/automattic/kandelo-homebrew" + sha256 cellar: :any_skip_relocation, wasm32_kandelo: "7d7b48ed79816e1c2e3812b68d4f0eb82d1178118a37d1b01cf0ee7e38f1a5ad" + end + + skip_clean "bin/wget" + + def install + root = kandelo_root + configure_kandelo_environment(root) + ENV["WGET_VERSION"] = version.to_s + out_dir = prepare_kandelo_package_env(source_url: "https://ftpmirror.gnu.org/gnu/wget/wget-1.25.0.tar.gz", source_sha256: "766e48423e79359ea31e41db9e5c289675947a7fcf2efdcedb726ac9d0da3784") + system "bash", "#{root}/packages/registry/wget/build-wget.sh" + install_kandelo_wasm(out_dir, "wget.wasm", "wget") + end + + test do + assert_kandelo_wasm "wget" + assert_match(/GNU Wget|Wget/i, shell_output_kandelo_wasm("wget", ["--version"])) + end +end diff --git a/homebrew/kandelo-homebrew/Kandelo/kandelo_formula_support.rb b/homebrew/kandelo-homebrew/Kandelo/kandelo_formula_support.rb new file mode 100644 index 0000000000..58113b069a --- /dev/null +++ b/homebrew/kandelo-homebrew/Kandelo/kandelo_formula_support.rb @@ -0,0 +1,73 @@ +module KandeloFormulaSupport + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def configure_kandelo_environment(root = kandelo_root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV.prepend_path "PATH", llvm_bin + end + if (rust_bin = ENV["HOMEBREW_KANDELO_RUST_BIN"]).to_s != "" + ENV.prepend_path "PATH", rust_bin + end + if (binaryen_bin = ENV["HOMEBREW_KANDELO_BINARYEN_BIN"]).to_s != "" + ENV.prepend_path "PATH", binaryen_bin + end + if (pkg_config = ENV["HOMEBREW_KANDELO_PKG_CONFIG"]).to_s != "" + ENV["PKG_CONFIG"] = pkg_config + end + end + + def prepare_kandelo_package_env(source_url:, source_sha256:) + out_dir = buildpath/"kandelo-package-out" + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = source_url + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = source_sha256 + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir.to_s + ENV["WASM_POSIX_DEP_WORK_DIR"] = (buildpath/"kandelo-package-work").to_s + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + ENV["WASM_POSIX_SKIP_LOCAL_BINARY_INSTALL"] = "1" + ENV["CARGO_TARGET_DIR"] = (buildpath/"kandelo-cargo-target").to_s + out_dir + end + + def install_kandelo_wasm(out_dir, wasm_name, installed_name = wasm_name.delete_suffix(".wasm")) + source = out_dir/wasm_name + chmod 0755, source + bin.install source => installed_name + chmod 0755, bin/installed_name + end + + def assert_kandelo_wasm(installed_name) + assert_equal "\0asm".b, File.binread(bin/installed_name, 4) + end + + def shell_output_kandelo_wasm(installed_name, args = [], stderr: true) + root = kandelo_root + configure_kandelo_environment(root) + test_wasm = testpath/"#{installed_name}.wasm" + File.binwrite(test_wasm, File.binread(bin/installed_name)) + redirect = stderr ? " 2>&1" : "" + shell_output( + "cd #{root.shellescape} && node --experimental-wasm-exnref --import tsx/esm examples/run-example.ts #{test_wasm.to_s.shellescape} #{args.shelljoin}#{redirect}", + ) + end +end diff --git a/packages/registry/dash/build-dash.sh b/packages/registry/dash/build-dash.sh index 64f11edfc9..1ca660592d 100755 --- a/packages/registry/dash/build-dash.sh +++ b/packages/registry/dash/build-dash.sh @@ -9,7 +9,9 @@ set -euo pipefail DASH_VERSION="${DASH_VERSION:-0.5.12}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/dash-src" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +SRC_DIR="$WORK_DIR/dash-src" +BIN_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/bin}" SYSROOT="$REPO_ROOT/sysroot" # --- Prerequisites --- @@ -167,17 +169,17 @@ mv "$DASH_BIN.instr" "$DASH_BIN" echo "==> dash built successfully!" # Copy to bin/ with .wasm extension (consistent with coreutils/grep/sed, needed for Vite) -mkdir -p "$SCRIPT_DIR/bin" -cp "$DASH_BIN" "$SCRIPT_DIR/bin/dash.wasm" +mkdir -p "$BIN_DIR" +cp "$DASH_BIN" "$BIN_DIR/dash.wasm" # Install into local-binaries/ so the resolver picks it up as an override. source "$REPO_ROOT/scripts/install-local-binary.sh" -install_local_binary dash "$SCRIPT_DIR/bin/dash.wasm" +install_local_binary dash "$BIN_DIR/dash.wasm" # sh ships as an alias for dash in the release; install the same bytes # under that name so `programs/sh.wasm` also resolves locally. -install_local_binary sh "$SCRIPT_DIR/bin/dash.wasm" +install_local_binary sh "$BIN_DIR/dash.wasm" -ls -lh "$SCRIPT_DIR/bin/dash.wasm" +ls -lh "$BIN_DIR/dash.wasm" echo "" echo "Run with:" echo " npx tsx packages/registry/shell/demo/serve.ts" diff --git a/packages/registry/fbdoom/build-fbdoom.sh b/packages/registry/fbdoom/build-fbdoom.sh index 7186c5575c..4e946601fc 100755 --- a/packages/registry/fbdoom/build-fbdoom.sh +++ b/packages/registry/fbdoom/build-fbdoom.sh @@ -4,15 +4,19 @@ # framebuffer mmap; the canvas renderer (host/src/framebuffer/canvas-renderer.ts) # consumes them. # -# Output: packages/registry/fbdoom/fbdoom.wasm +# Output: $WASM_POSIX_DEP_OUT_DIR/fbdoom.wasm when invoked by the +# package resolver, or packages/registry/fbdoom/fbdoom.wasm otherwise. # # Usage: bash packages/registry/fbdoom/build-fbdoom.sh set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$HERE/../../.." && pwd)" -SRC="$HERE/fbdoom-src" -CDOOM_SRC="$HERE/chocolate-doom-src" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$HERE}" +OUT_DIR="${WASM_POSIX_DEP_OUT_DIR:-$HERE}" +SRC="$WORK_DIR/fbdoom-src" +CDOOM_SRC="$WORK_DIR/chocolate-doom-src" +OUT="$OUT_DIR/fbdoom.wasm" # Pin to chocolate-doom 3.1.0. fbDOOM stripped its OPL/MIDI/MUS sources # when removing SDL; we vendor them back so the music path compiles. @@ -23,6 +27,8 @@ CDOOM_COMMIT="35fb1372d10756ca27eca05665bd8a7cebc71c05" # chocolate-doom-3.1.0 source "$REPO_ROOT/sdk/activate.sh" export WASM_POSIX_SYSROOT="$REPO_ROOT/sysroot" +mkdir -p "$WORK_DIR" "$OUT_DIR" + if [ ! -d "$SRC" ]; then echo "==> Cloning maximevince/fbDOOM..." git clone --depth 1 https://github.com/maximevince/fbDOOM "$SRC" @@ -101,13 +107,13 @@ make CC=wasm32posix-cc \ LIBS="-lm" \ NOSDL=1 -cp fbdoom "$HERE/fbdoom.wasm" +cp fbdoom "$OUT" # fbDOOM doesn't fork — no wasm-fork-instrument step needed. # (vs other ports here: dash forks via popen/system, so its build-dash.sh # runs `wasm-fork-instrument` for the fork path.) -ls -la "$HERE/fbdoom.wasm" +ls -la "$OUT" echo "==> fbdoom.wasm built." # Install into local-binaries/ (resolver priority 1) and the resolver @@ -124,4 +130,4 @@ echo "==> fbdoom.wasm built." # caches it via the Cache API — see apps/browser-demos/pages/doom/main.ts. cd "$REPO_ROOT" source "$REPO_ROOT/scripts/install-local-binary.sh" -install_local_binary fbdoom "$HERE/fbdoom.wasm" fbdoom.wasm +install_local_binary fbdoom "$OUT" fbdoom.wasm diff --git a/packages/registry/git/build-git.sh b/packages/registry/git/build-git.sh index ee68889a6a..338973b4a2 100755 --- a/packages/registry/git/build-git.sh +++ b/packages/registry/git/build-git.sh @@ -25,8 +25,9 @@ set -euo pipefail GIT_VERSION="${GIT_VERSION:-2.47.1}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/git-src" -BIN_DIR="$SCRIPT_DIR/bin" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +SRC_DIR="$WORK_DIR/git-src" +BIN_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/bin}" # Explicit env wins; else the in-tree sysroot. Matches build-libcurl.sh:49. SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" diff --git a/packages/registry/less/build-less.sh b/packages/registry/less/build-less.sh index 8a54a9beaf..9e1c25ffec 100755 --- a/packages/registry/less/build-less.sh +++ b/packages/registry/less/build-less.sh @@ -14,8 +14,9 @@ set -euo pipefail LESS_VERSION="${LESS_VERSION:-661}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/less-src" -BIN_DIR="$SCRIPT_DIR/bin" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +SRC_DIR="$WORK_DIR/less-src" +BIN_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/bin}" SYSROOT="$REPO_ROOT/sysroot" # --- Prerequisites --- @@ -32,7 +33,7 @@ fi export WASM_POSIX_SYSROOT="$SYSROOT" # --- Build termcap stub library --- -TERMCAP_DIR="$SCRIPT_DIR/libtermcap" +TERMCAP_DIR="$WORK_DIR/libtermcap" if [ ! -f "$TERMCAP_DIR/libtermcap.a" ]; then echo "==> Building termcap stub library..." mkdir -p "$TERMCAP_DIR/include" @@ -124,4 +125,4 @@ echo "Binary: $BIN_DIR/less.wasm" # Install into local-binaries/ so the resolver picks the freshly-built # binary over the fetched release. source "$REPO_ROOT/scripts/install-local-binary.sh" -[ -f "$SCRIPT_DIR/bin/less.wasm" ] && install_local_binary less "$SCRIPT_DIR/bin/less.wasm" || true +[ -f "$BIN_DIR/less.wasm" ] && install_local_binary less "$BIN_DIR/less.wasm" || true diff --git a/packages/registry/lsof/build-lsof.sh b/packages/registry/lsof/build-lsof.sh index 24c01e7732..4037b1c7ca 100755 --- a/packages/registry/lsof/build-lsof.sh +++ b/packages/registry/lsof/build-lsof.sh @@ -8,7 +8,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" SRC="$REPO_ROOT/examples/lsof.c" -OUT_BIN="$SCRIPT_DIR/lsof.wasm" +OUT_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR}" +OUT_BIN="$OUT_DIR/lsof.wasm" if [ ! -f "$SRC" ]; then echo "ERROR: source not found at $SRC" >&2 @@ -82,6 +83,7 @@ LINK_FLAGS=( ) echo "==> Building lsof.wasm from $SRC" +mkdir -p "$OUT_DIR" "$CC" "${CFLAGS[@]}" "$SRC" "${LINK_FLAGS[@]}" -o "$OUT_BIN" if [ -n "$WASM_OPT" ]; then diff --git a/packages/registry/modeset/build-modeset.sh b/packages/registry/modeset/build-modeset.sh index 83146d1146..1dafa14771 100755 --- a/packages/registry/modeset/build-modeset.sh +++ b/packages/registry/modeset/build-modeset.sh @@ -3,6 +3,8 @@ set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$HERE/../../.." && pwd)" +OUT_DIR="${WASM_POSIX_DEP_OUT_DIR:-$HERE}" +OUT="$OUT_DIR/modeset.wasm" source "$REPO_ROOT/sdk/activate.sh" export WASM_POSIX_SYSROOT="$REPO_ROOT/sysroot" @@ -20,6 +22,7 @@ PKG_CFLAGS="$(wasm32posix-pkg-config --cflags libdrm gbm egl glesv2)" PKG_LIBS="$(wasm32posix-pkg-config --libs gbm libdrm egl glesv2)" echo "==> Building modeset fluid simulation..." +mkdir -p "$OUT_DIR" wasm32posix-cc \ -std=c11 \ -O2 \ @@ -31,13 +34,13 @@ wasm32posix-cc \ "$REPO_ROOT/programs/modeset.c" \ $PKG_LIBS \ -lm \ - -o "$HERE/modeset.wasm" + -o "$OUT" "$REPO_ROOT/scripts/run-wasm-fork-instrument.sh" \ - "$HERE/modeset.wasm" \ - -o "$HERE/modeset.wasm.instr" -mv "$HERE/modeset.wasm.instr" "$HERE/modeset.wasm" + "$OUT" \ + -o "$OUT.instr" +mv "$OUT.instr" "$OUT" cd "$REPO_ROOT" source "$REPO_ROOT/scripts/install-local-binary.sh" -install_local_binary modeset "$HERE/modeset.wasm" modeset.wasm +install_local_binary modeset "$OUT" modeset.wasm diff --git a/packages/registry/msmtpd/build-msmtpd.sh b/packages/registry/msmtpd/build-msmtpd.sh index ac6a473084..93cf90cd36 100755 --- a/packages/registry/msmtpd/build-msmtpd.sh +++ b/packages/registry/msmtpd/build-msmtpd.sh @@ -11,9 +11,11 @@ SHA256="20cd58b58dd007acf7b937fa1a1e21f3afb3e9ef5bbcfb8b4f5650deadc64db4" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/msmtp-src" -BIN_DIR="$SCRIPT_DIR/bin" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +SRC_DIR="$WORK_DIR/msmtp-src" +BIN_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/bin}" OUT="$BIN_DIR/msmtpd.wasm" +TARBALL_PATH="$WORK_DIR/$TARBALL" if [ -f "$OUT" ]; then echo "==> Reusing existing msmtpd artifact in $BIN_DIR (skip rebuild)." @@ -27,14 +29,15 @@ if ! command -v wasm32posix-cc >/dev/null 2>&1; then exit 1 fi -if [ ! -f "$SCRIPT_DIR/$TARBALL" ]; then +mkdir -p "$WORK_DIR" +if [ ! -f "$TARBALL_PATH" ]; then echo "==> Downloading msmtp $VERSION..." curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL \ - -o "$SCRIPT_DIR/$TARBALL" \ + -o "$TARBALL_PATH" \ "$URL" fi -actual_sha="$(shasum -a 256 "$SCRIPT_DIR/$TARBALL" | awk '{print $1}')" +actual_sha="$(shasum -a 256 "$TARBALL_PATH" | awk '{print $1}')" if [ "$actual_sha" != "$SHA256" ]; then echo "ERROR: checksum mismatch for $TARBALL" >&2 echo " expected: $SHA256" >&2 @@ -45,8 +48,8 @@ fi if [ ! -d "$SRC_DIR/src" ]; then echo "==> Extracting msmtp $VERSION..." rm -rf "$SRC_DIR" - tar xf "$SCRIPT_DIR/$TARBALL" -C "$SCRIPT_DIR" - mv "$SCRIPT_DIR/msmtp-$VERSION" "$SRC_DIR" + tar xf "$TARBALL_PATH" -C "$WORK_DIR" + mv "$WORK_DIR/msmtp-$VERSION" "$SRC_DIR" fi cd "$SRC_DIR/src" @@ -86,14 +89,8 @@ wasm32posix-cc \ netrc.c \ -o "$OUT" -FORK_INSTRUMENT="$REPO_ROOT/tools/bin/wasm-fork-instrument" -if [ ! -x "$FORK_INSTRUMENT" ]; then - echo "ERROR: wasm-fork-instrument not found at $FORK_INSTRUMENT." >&2 - exit 1 -fi - echo "==> Applying wasm-fork-instrument to msmtpd.wasm..." -"$FORK_INSTRUMENT" "$OUT" -o "$OUT.instr" +"$REPO_ROOT/scripts/run-wasm-fork-instrument.sh" "$OUT" -o "$OUT.instr" mv "$OUT.instr" "$OUT" ls -lh "$OUT" diff --git a/packages/registry/nano/build-nano.sh b/packages/registry/nano/build-nano.sh index c97a201bcb..ea3658129a 100755 --- a/packages/registry/nano/build-nano.sh +++ b/packages/registry/nano/build-nano.sh @@ -13,8 +13,9 @@ set -euo pipefail NANO_VERSION="${NANO_VERSION:-8.3}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/nano-src" -BIN_DIR="$SCRIPT_DIR/bin" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +SRC_DIR="$WORK_DIR/nano-src" +BIN_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/bin}" # Explicit env wins; else the in-tree sysroot. Matches build-curl.sh:49. SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" @@ -222,4 +223,4 @@ echo "Binary: $BIN_DIR/nano.wasm" # Install into local-binaries/ so the resolver picks the freshly-built # binary over the fetched release. source "$REPO_ROOT/scripts/install-local-binary.sh" -install_local_binary nano "$SCRIPT_DIR/bin/nano.wasm" +install_local_binary nano "$BIN_DIR/nano.wasm" diff --git a/packages/registry/netcat/build-netcat.sh b/packages/registry/netcat/build-netcat.sh index 1ae2382684..e340e429a4 100755 --- a/packages/registry/netcat/build-netcat.sh +++ b/packages/registry/netcat/build-netcat.sh @@ -6,8 +6,9 @@ set -euo pipefail NETCAT_VERSION="${NETCAT_VERSION:-0.7.1}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/netcat-src" -BIN_DIR="$SCRIPT_DIR/bin" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +SRC_DIR="$WORK_DIR/netcat-src" +BIN_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/bin}" SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" if ! command -v wasm32posix-cc &>/dev/null; then @@ -42,17 +43,31 @@ PATCH_SET=( "disable-pktinfo.patch" ) echo "==> Verifying Kandelo netcat portability patches..." -for patch_name in "${PATCH_SET[@]}"; do - patch_file="$SCRIPT_DIR/patches/$patch_name" - if patch --reverse --dry-run -p1 < "$patch_file" >/dev/null 2>&1; then - echo " $patch_name already applied" - elif patch --forward --dry-run -p1 < "$patch_file" >/dev/null 2>&1; then - patch -p1 < "$patch_file" - else - echo "ERROR: $patch_name does not apply and is not already present" >&2 - exit 1 - fi -done + +if awk ' + /if \(netcat_mode == NETCAT_LISTEN\)/ { in_listen = 1; next } + in_listen && /glob_ret = EXIT_SUCCESS;/ { ok = 1; exit } + in_listen && /if \(opt_exec\)/ { exit } + END { exit ok ? 0 : 1 } +' src/netcat.c; then + echo " listen-success-exit.patch already applied" +else + patch --batch -p1 < "$SCRIPT_DIR/patches/listen-success-exit.patch" +fi + +udp_marker_count=$(grep -c "Kandelo exposes normal POSIX UDP sockets" src/core.c || true) +case "$udp_marker_count" in + 0) patch --batch -p1 < "$SCRIPT_DIR/patches/udp-listen-single-socket.patch" ;; + 1) echo " udp-listen-single-socket.patch already applied" ;; + *) echo "ERROR: udp-listen-single-socket.patch marker count is $udp_marker_count, expected 0 or 1" >&2; exit 1 ;; +esac + +if grep -q "/\\* # define USE_PKTINFO \\*/" src/netcat.h; then + echo " disable-pktinfo.patch already applied" +else + patch --batch -p1 < "$SCRIPT_DIR/patches/disable-pktinfo.patch" +fi + printf '%s\n' "${PATCH_SET[*]}" > "$PATCH_MARKER" if ! awk ' diff --git a/packages/registry/netcat/patches/udp-listen-single-socket.patch b/packages/registry/netcat/patches/udp-listen-single-socket.patch index 6a3a0999e3..e5cd4e25d8 100644 --- a/packages/registry/netcat/patches/udp-listen-single-socket.patch +++ b/packages/registry/netcat/patches/udp-listen-single-socket.patch @@ -2,11 +2,18 @@ diff --git a/src/core.c b/src/core.c index 7e6f3dd..d8d17c1 100644 --- a/src/core.c +++ b/src/core.c -@@ -98,2 +98,6 @@ static int core_udp_listen(nc_sock_t *ncsock) +@@ -94,7 +94,13 @@ static int core_udp_listen(nc_sock_t *ncsock) + if (ncsock->local_host.iaddrs[0].s_addr) + need_udphelper = FALSE; #endif - ++ +#if 1 + /* Kandelo exposes normal POSIX UDP sockets, but not GNU netcat's legacy + helper protocol. Use one bound datagram socket for UDP listen mode. */ + need_udphelper = FALSE; +#endif ++ + if (!need_udphelper) { + /* simulates a udphelper_sockets_open() call */ + sockbuf = calloc(2, sizeof(int)); diff --git a/packages/registry/vim/build-vim.sh b/packages/registry/vim/build-vim.sh index ffb30d52de..5e6fa347fc 100755 --- a/packages/registry/vim/build-vim.sh +++ b/packages/registry/vim/build-vim.sh @@ -16,8 +16,10 @@ set -euo pipefail VIM_VERSION="${VIM_VERSION:-9.1.0900}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/vim-src" -BIN_DIR="$SCRIPT_DIR/bin" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +SRC_DIR="$WORK_DIR/vim-src" +BIN_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/bin}" +RUNTIME_DIR="$WORK_DIR/runtime" # Explicit env wins; else the in-tree sysroot. Keeps neighbour-worktree # invocations viable (WASM_POSIX_SYSROOT=/sysroot). Same shape as # build-curl.sh:49. @@ -250,7 +252,7 @@ echo "Binary: $BIN_DIR/vim.wasm" # vim.wasm + runtime/* and is self-sufficient (consumers don't need # to re-fetch upstream source to assemble vim.zip). echo "==> Bundling Vim runtime (for archive)..." -bash "$SCRIPT_DIR/bundle-runtime.sh" +VIM_SRC_DIR="$SRC_DIR" VIM_RUNTIME_OUT="$RUNTIME_DIR" bash "$SCRIPT_DIR/bundle-runtime.sh" source "$REPO_ROOT/scripts/install-local-binary.sh" install_local_binary vim "$BIN_DIR/vim.wasm" @@ -260,8 +262,8 @@ install_local_binary vim "$BIN_DIR/vim.wasm" # the resolver, $WASM_POSIX_DEP_OUT_DIR is unset and this is a # no-op — direct invocations of build-vim.sh just leave runtime/ # at its source-tree location. -if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ] && [ -d "$SCRIPT_DIR/runtime" ]; then +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ] && [ -d "$RUNTIME_DIR" ]; then rm -rf "$WASM_POSIX_DEP_OUT_DIR/runtime" - cp -R "$SCRIPT_DIR/runtime" "$WASM_POSIX_DEP_OUT_DIR/runtime" + cp -R "$RUNTIME_DIR" "$WASM_POSIX_DEP_OUT_DIR/runtime" echo " staged runtime tree into resolver scratch" fi diff --git a/packages/registry/vim/bundle-runtime.sh b/packages/registry/vim/bundle-runtime.sh index 7bd510aa27..3db4961c8a 100755 --- a/packages/registry/vim/bundle-runtime.sh +++ b/packages/registry/vim/bundle-runtime.sh @@ -28,9 +28,9 @@ set -euo pipefail # Output: packages/registry/vim/runtime/ (directory tree) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -SRC_DIR="$SCRIPT_DIR/vim-src" +SRC_DIR="${VIM_SRC_DIR:-$SCRIPT_DIR/vim-src}" RUNTIME_SRC="$SRC_DIR/runtime" -RUNTIME_OUT="$SCRIPT_DIR/runtime" +RUNTIME_OUT="${VIM_RUNTIME_OUT:-$SCRIPT_DIR/runtime}" if [ ! -d "$RUNTIME_SRC" ]; then echo "ERROR: Vim source not found. Run build-vim.sh first." >&2 diff --git a/packages/registry/wget/build-wget.sh b/packages/registry/wget/build-wget.sh index 9de5ee73c5..9bb8f796e7 100755 --- a/packages/registry/wget/build-wget.sh +++ b/packages/registry/wget/build-wget.sh @@ -13,8 +13,9 @@ set -euo pipefail WGET_VERSION="${WGET_VERSION:-1.24.5}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/wget-src" -BIN_DIR="$SCRIPT_DIR/bin" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +SRC_DIR="$WORK_DIR/wget-src" +BIN_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/bin}" # Explicit env wins; else the in-tree sysroot. Matches build-curl.sh:49. SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" @@ -258,4 +259,4 @@ echo "Binary: $BIN_DIR/wget.wasm" # Install into local-binaries/ so the resolver picks the freshly-built # binary over the fetched release. source "$REPO_ROOT/scripts/install-local-binary.sh" -[ -f "$SCRIPT_DIR/bin/wget.wasm" ] && install_local_binary wget "$SCRIPT_DIR/bin/wget.wasm" || true +[ -f "$BIN_DIR/wget.wasm" ] && install_local_binary wget "$BIN_DIR/wget.wasm" || true diff --git a/scripts/homebrew-bottle-build.sh b/scripts/homebrew-bottle-build.sh index c418f9c82a..4072b13f05 100755 --- a/scripts/homebrew-bottle-build.sh +++ b/scripts/homebrew-bottle-build.sh @@ -66,6 +66,7 @@ esac TAP_ROOT="$(cd "$TAP_ROOT" && pwd)" mkdir -p "$OUT_DIR" OUT_DIR="$(cd "$OUT_DIR" && pwd)" +mkdir -p "$OUT_DIR/homebrew-temp" FORMULA_PATH="$TAP_ROOT/Formula/$FORMULA.rb" if [ ! -f "$FORMULA_PATH" ]; then @@ -88,6 +89,7 @@ mkdir -p "$OUT_DIR/bottles" WORK_DIR="$(mktemp -d)" BREW_REPO="" BREW_OVERLAY="" +TAP_SOURCE="$TAP_ROOT" cleanup() { if [ -n "$BREW_REPO" ] && [ -n "$BREW_OVERLAY" ] && [ -d "$BREW_OVERLAY" ]; then @@ -113,14 +115,37 @@ export HOMEBREW_NO_AUTO_UPDATE="${HOMEBREW_NO_AUTO_UPDATE:-1}" export HOMEBREW_NO_INSTALL_CLEANUP="${HOMEBREW_NO_INSTALL_CLEANUP:-1}" export HOMEBREW_NO_ANALYTICS="${HOMEBREW_NO_ANALYTICS:-1}" export HOMEBREW_DEVELOPER="${HOMEBREW_DEVELOPER:-1}" +export HOMEBREW_NO_REQUIRE_TAP_TRUST="${HOMEBREW_NO_REQUIRE_TAP_TRUST:-1}" +export HOMEBREW_AVOID_NESTED_SANDBOXING="${HOMEBREW_AVOID_NESTED_SANDBOXING:-1}" +export HOMEBREW_TEMP="${HOMEBREW_TEMP:-$OUT_DIR/homebrew-temp}" export KANDELO_HOMEBREW_ARCH="$ARCH" export KANDELO_HOMEBREW_KANDELO_ROOT="$KANDELO_ROOT" export HOMEBREW_KANDELO_ARCH="$ARCH" export HOMEBREW_KANDELO_ROOT="$KANDELO_ROOT" export HOMEBREW_KANDELO_NODE="$(command -v node)" export HOMEBREW_KANDELO_LLVM_BIN="${LLVM_BIN:-${WASM_POSIX_LLVM_DIR:-}}" +if command -v cargo >/dev/null 2>&1; then + export HOMEBREW_KANDELO_RUST_BIN="$(dirname "$(command -v cargo)")" +fi +if command -v wasm-opt >/dev/null 2>&1; then + export HOMEBREW_KANDELO_BINARYEN_BIN="$(dirname "$(command -v wasm-opt)")" +fi +if command -v pkg-config >/dev/null 2>&1; then + export HOMEBREW_KANDELO_PKG_CONFIG="$(command -v pkg-config)" +fi + +if [ ! -d "$TAP_SOURCE/.git" ]; then + TAP_SOURCE="$WORK_DIR/tap-source" + mkdir -p "$TAP_SOURCE" + rsync -a --exclude .git "$TAP_ROOT/" "$TAP_SOURCE/" + git -C "$TAP_SOURCE" init -q + git -C "$TAP_SOURCE" config user.name "kandelo-homebrew-local" + git -C "$TAP_SOURCE" config user.email "kandelo-homebrew-local@example.invalid" + git -C "$TAP_SOURCE" add . + git -C "$TAP_SOURCE" commit -q -m "stage Kandelo Homebrew tap" +fi -"$BREW_BIN" tap "$TAP_NAME" "$TAP_ROOT" +"$BREW_BIN" tap "$TAP_NAME" "$TAP_SOURCE" FORMULA_REF="$TAP_NAME/$FORMULA" TAPPED_TAP_ROOT="$("$BREW_BIN" --repository "$TAP_NAME")" TAPPED_FORMULA_PATH="$TAPPED_TAP_ROOT/Formula/$FORMULA.rb" diff --git a/scripts/homebrew-generate-sidecars-from-env.sh b/scripts/homebrew-generate-sidecars-from-env.sh index e77db39103..f3eaa403be 100755 --- a/scripts/homebrew-generate-sidecars-from-env.sh +++ b/scripts/homebrew-generate-sidecars-from-env.sh @@ -108,89 +108,149 @@ for dep in package_toml.get("depends_on", []): deps.append({"name": dep}) version = str(bottle_formula["pkg_version"]) +package_kind = package_toml.get("kind", "program") +if package_kind not in {"library", "program"}: + raise SystemExit(f"unsupported Homebrew sidecar package kind for {formula}: {package_kind!r}") + +def package_links_and_env(): + if package_kind == "library": + outputs = package_toml.get("outputs", {}) + links = [] + for key in ("headers", "libs", "pkgconfig"): + for rel in sorted(outputs.get(key, [])): + links.append({ + "type": "file", + "source": rel, + "target": rel, + "mode": "0644", + }) + if not links: + raise SystemExit(f"library formula {formula} has no declared package outputs to link") + return links, {} + + outputs = package_toml.get("outputs", []) + links = [] + if isinstance(outputs, list): + for output in outputs: + name = output.get("name") + if name: + links.append({"type": "symlink", "source": f"bin/{name}", "target": f"bin/{name}"}) + if not links: + links.append({"type": "symlink", "source": f"bin/{formula}", "target": f"bin/{formula}"}) + return links, {"PATH_prepend": ["bin"]} + +def package_fork_instrumentation(): + outputs = package_toml.get("outputs", []) + if isinstance(outputs, list): + policies = [output.get("fork_instrumentation", "auto") for output in outputs] + if policies: + if all(policy == "disabled" for policy in policies): + return "disabled" + return "auto" + return "auto" + +def primary_program_name(): + if package_kind != "program": + return formula + outputs = package_toml.get("outputs", []) + if isinstance(outputs, list): + for output in outputs: + if output.get("name"): + return output["name"] + return formula + +def default_node_smoke_text(): + if package_kind == "library": + return ( + f"Formula test compiled packages/registry/{formula}/test/{formula}_basic.c " + "against the installed keg and ran the resulting Wasm through " + "node --import tsx/esm examples/run-example.ts" + ) + if formula == "bzip2": + return ( + "Formula test ran bzip2 --help through " + "node --import tsx/esm examples/run-example.ts" + ) + return ( + f"Formula test ran {primary_program_name()} through " + "node --import tsx/esm examples/run-example.ts" + ) + +def skipped_outcome(name, reason): + return { + "name": name, + "status": "skipped", + "passed": [], + "failed": [], + "skipped": [reason], + "skip_reason": reason, + } + +def failed_outcome(name, reason): + return { + "name": name, + "status": "failed", + "passed": [], + "failed": [reason], + "skipped": [], + } + +links, link_env = package_links_and_env() browser_smoke_status = os.environ.get("KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS", "skipped") -if browser_smoke_status not in {"success", "skipped"}: +if browser_smoke_status not in {"success", "skipped", "failed"}: raise SystemExit(f"invalid KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS={browser_smoke_status!r}") browser_compatible = browser_smoke_status == "success" if browser_compatible and arch != "wasm32": raise SystemExit("browser smoke can only mark wasm32 bottles browser-compatible") runtime_support = ["node", "browser"] if browser_compatible else ["node"] -browser_smoke_outcome = { - "name": "browser_smoke", - "status": "skipped", - "passed": [], - "failed": [], - "skipped": ["browser_compatible is false for this bottle"], - "skip_reason": "No successful browser VFS smoke was recorded for this bottle.", -} -vfs_image_outcome = { - "name": "homebrew_vfs_image", - "status": "skipped", - "passed": [], - "failed": [], - "skipped": ["precomposed browser VFS image was not built"], - "skip_reason": "Browser-compatible gallery publication requires kd-8ho.10 browser smoke.", -} -gallery_outcome = { - "name": "browser_gallery", - "status": "skipped", - "passed": [], - "failed": [], - "skipped": ["browser gallery assets were not generated"], - "skip_reason": "Gallery assets require a successful browser VFS smoke.", -} +browser_reason = os.environ.get( + "KANDELO_HOMEBREW_BROWSER_SMOKE_REASON", + f"No successful browser VFS smoke was recorded for {formula} {arch}.", +) +browser_smoke_outcome = skipped_outcome("browser_smoke", browser_reason) +if browser_smoke_status == "failed": + browser_smoke_outcome = failed_outcome("browser_smoke", browser_reason) if browser_compatible: vfs_image = os.environ.get("KANDELO_HOMEBREW_VFS_IMAGE", "") vfs_report = os.environ.get("KANDELO_HOMEBREW_VFS_REPORT", "") - gallery_root = os.environ.get("KANDELO_HOMEBREW_GALLERY_ROOT", "") browser_url = os.environ.get("KANDELO_HOMEBREW_BROWSER_SMOKE_URL", "") browser_command = os.environ.get( "KANDELO_HOMEBREW_BROWSER_SMOKE_COMMAND", - "/home/linuxbrew/.linuxbrew/bin/hello --version", + f"/home/linuxbrew/.linuxbrew/bin/{formula} --version", ) missing = [ name for name, value in [ ("KANDELO_HOMEBREW_VFS_IMAGE", vfs_image), ("KANDELO_HOMEBREW_VFS_REPORT", vfs_report), - ("KANDELO_HOMEBREW_GALLERY_ROOT", gallery_root), ("KANDELO_HOMEBREW_BROWSER_SMOKE_URL", browser_url), ] if not value ] if missing: raise SystemExit("browser smoke success is missing env: " + ", ".join(missing)) - browser_smoke_outcome = { - "name": "browser_smoke", - "status": "success", - "passed": [ - f"Playwright chromium launched {browser_url}", - f"terminal command passed: {browser_command}", - ], - "failed": [], - "skipped": [], - } - vfs_image_outcome = { - "name": "homebrew_vfs_image", - "status": "success", - "passed": [ - f"built {vfs_image}", - f"wrote report {vfs_report}", - ], - "failed": [], - "skipped": [], - } - gallery_outcome = { - "name": "browser_gallery", - "status": "success", - "passed": [ + browser_passed = [ + f"built precomposed VFS image {vfs_image}", + f"wrote VFS report {vfs_report}", + f"Playwright chromium launched {browser_url}", + f"terminal command passed: {browser_command}", + ] + gallery_root = os.environ.get("KANDELO_HOMEBREW_GALLERY_ROOT", "") + if gallery_root: + browser_passed.extend([ f"generated {gallery_root}/gallery.json", f"generated {gallery_root}/index.toml", "scripts/validate-software-gallery.mjs accepted generated gallery assets", - ], + ]) + browser_smoke_outcome = { + "name": "browser_smoke", + "status": "success", + "passed": browser_passed, "failed": [], "skipped": [], } +node_smoke_text = os.environ.get("KANDELO_HOMEBREW_NODE_SMOKE_COMMAND", default_node_smoke_text()) + manifest = { "schema": 1, "tap_repository": os.environ["KANDELO_HOMEBREW_TAP_REPOSITORY"], @@ -219,7 +279,7 @@ manifest = { "prefix": "/home/linuxbrew/.linuxbrew", "runtime_support": runtime_support, "browser_compatible": browser_compatible, - "fork_instrumentation": "not-required", + "fork_instrumentation": package_fork_instrumentation(), "status": "success", "built_by": os.environ["RUN_URL"], "built_at": os.environ["GENERATED_AT"], @@ -227,11 +287,9 @@ manifest = { "url": os.environ["KANDELO_HOMEBREW_BOTTLE_URL"], "cache_key_sha": os.environ["CACHE_KEY_SHA"], "payload_root": f"{formula}/{version}", - "links": [ - {"type": "symlink", "source": f"bin/{formula}", "target": f"bin/{formula}"} - ], + "links": links, "receipts": [f".brew/{formula}.rb", "INSTALL_RECEIPT.json"], - "env": {"PATH_prepend": ["bin"]}, + "env": link_env, "build": { "github_run": os.environ["RUN_URL"], "job": os.environ.get("GITHUB_JOB", "local"), @@ -260,8 +318,8 @@ manifest = { "status": "skipped", "passed": [], "failed": [], - "skipped": ["brew audit was not part of kd-8ho.5 local verification"], - "skip_reason": "kd-8ho.5 validates the first bottle build and sidecars; tap audit can run in the real tap publication gate.", + "skipped": ["brew audit was not part of this local dry-run verification"], + "skip_reason": "Tap audit can run in the trusted tap publication gate.", }, { "name": "bottle_build", @@ -278,15 +336,11 @@ manifest = { { "name": "node_smoke", "status": "success", - "passed": [ - "Formula test ran hello --version through node --import tsx/esm examples/run-example.ts" - ], + "passed": [node_smoke_text], "failed": [], "skipped": [], }, - vfs_image_outcome, browser_smoke_outcome, - gallery_outcome, ], }, } diff --git a/scripts/homebrew-package-node-smoke.ts b/scripts/homebrew-package-node-smoke.ts new file mode 100644 index 0000000000..2495d8962d --- /dev/null +++ b/scripts/homebrew-package-node-smoke.ts @@ -0,0 +1,736 @@ +/** + * Node-side smoke coverage for Kandelo Homebrew package sidecars. + * + * The runner consumes generated Kandelo/Homebrew sidecars, materializes each + * requested package into a VFS, and runs a package-specific smoke through + * NodeKernelHost. Program packages execute their poured binary. SQLite + * compiles a test-only consumer from the poured headers and static library. + */ +import { execFileSync } from "node:child_process"; +import { + existsSync, + mkdirSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { ABI_VERSION } from "../host/src/generated/abi"; +import { fetchHomebrewBottleBytes } from "../host/src/homebrew-vfs-fetch"; +import { buildHomebrewVfs } from "../host/src/homebrew-vfs-builder"; +import { + planHomebrewVfs, + type HomebrewBottleArch, + type HomebrewTapMetadata, + type HomebrewVfsPackagePlan, +} from "../host/src/homebrew-vfs-planner"; +import { NodeKernelHost } from "../host/src/node-kernel-host"; +import { MemoryFileSystem } from "../host/src/vfs/memory-fs"; +import { saveImage } from "../images/vfs/scripts/vfs-image-helpers"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(__dirname, ".."); +const PREFIX = "/home/linuxbrew/.linuxbrew"; +const CELLAR = `${PREFIX}/Cellar`; + +type OutcomeStatus = "pass" | "fail" | "skip"; +type FormulaName = string; + +interface ProgramSmokeSpec { + argv0: string; + guestPath: string; + args: string[]; + expected: RegExp; + description?: string; +} + +const PROGRAM_SMOKES: Record = { + bzip2: { + argv0: "bzip2", + guestPath: `${PREFIX}/bin/bzip2`, + args: ["--help"], + expected: /bzip2/i, + }, + dash: { + argv0: "dash", + guestPath: `${PREFIX}/bin/dash`, + args: ["-c", "echo dash-homebrew-smoke"], + expected: /dash-homebrew-smoke/, + description: "dash -c executed a shell command", + }, + git: { + argv0: "git", + guestPath: `${PREFIX}/bin/git`, + args: ["--version"], + expected: /git version/i, + }, + less: { + argv0: "less", + guestPath: `${PREFIX}/bin/less`, + args: ["--version"], + expected: /less/i, + }, + lsof: { + argv0: "lsof", + guestPath: `${PREFIX}/bin/lsof`, + args: [], + expected: /COMMAND|PID|lsof/i, + description: "lsof enumerated visible process file descriptors", + }, + msmtpd: { + argv0: "msmtpd", + guestPath: `${PREFIX}/bin/msmtpd`, + args: ["--version"], + expected: /msmtp|msmtpd/i, + }, + nano: { + argv0: "nano", + guestPath: `${PREFIX}/bin/nano`, + args: ["--version"], + expected: /GNU nano|nano/i, + }, + netcat: { + argv0: "nc", + guestPath: `${PREFIX}/bin/nc`, + args: ["--help"], + expected: /netcat|listen|connect|usage/i, + description: "nc help path exercised the network CLI binary", + }, + vim: { + argv0: "vim", + guestPath: `${PREFIX}/bin/vim`, + args: ["--version"], + expected: /VIM|Vi IMproved/i, + }, + wget: { + argv0: "wget", + guestPath: `${PREFIX}/bin/wget`, + args: ["--version"], + expected: /GNU Wget|Wget/i, + }, + xz: { + argv0: "xz", + guestPath: `${PREFIX}/bin/xz`, + args: ["--version"], + expected: /xz/i, + }, +}; + +interface CliOptions { + resultDir: string; + tapRoot: string; + formulas: FormulaName[]; + arch: HomebrewBottleArch; + bottleCache: string; + timeoutMs: number; + maxBytes: number; + beadId: string; +} + +interface Outcome { + name: string; + status: OutcomeStatus; + durationMs: number; + details?: string; + error?: string; +} + +interface BuiltVfs { + fs: MemoryFileSystem; + imageBytes: Uint8Array; + reportPath: string; +} + +class SkipCase extends Error { + constructor(message: string) { + super(message); + this.name = "SkipCase"; + } +} + +async function main(): Promise { + const options = parseArgs(process.argv.slice(2)); + mkdirSync(options.resultDir, { recursive: true }); + mkdirSync(join(options.resultDir, "outcome-lists"), { recursive: true }); + mkdirSync(options.bottleCache, { recursive: true }); + + const metadataPath = join(options.tapRoot, "Kandelo", "metadata.json"); + const metadata = readJsonFile(metadataPath); + const tapCommit = gitRevParse(options.tapRoot); + const startedAt = new Date(); + const outcomes: Outcome[] = []; + + writeCurrentRun(options, { + status: "running", + startedAt, + tapCommit, + outcomes, + currentCase: "startup", + }); + + const builtByFormula = new Map(); + for (const formula of options.formulas) { + await runCase(outcomes, options, tapCommit, `homebrew_vfs_build_${formula}`, async () => { + const built = await buildFormulaVfs(metadata, formula, options); + builtByFormula.set(formula, built); + return `report=${built.reportPath}`; + }); + + await runCase(outcomes, options, tapCommit, `node_smoke_${formula}`, async () => { + const built = builtByFormula.get(formula); + if (!built) throw new SkipCase(`requires successful homebrew_vfs_build_${formula}`); + return await runFormulaSmoke(formula, built, options); + }); + } + + writeOutcomeLists(options.resultDir, outcomes); + writeSummary(options, { + startedAt, + completedAt: new Date(), + tapCommit, + outcomes, + }); + writeCurrentRun(options, { + status: outcomes.some((outcome) => outcome.status === "fail") ? "failed" : "complete", + startedAt, + tapCommit, + outcomes, + currentCase: "complete", + }); + + process.exit(outcomes.some((outcome) => outcome.status === "fail") ? 1 : 0); +} + +async function buildFormulaVfs( + metadata: HomebrewTapMetadata, + formula: FormulaName, + options: CliOptions, +): Promise { + const plan = await planHomebrewVfs(metadata, { + packages: [formula], + arch: options.arch, + runtime: "node", + expectedAbi: ABI_VERSION, + loadLinkManifest: (relPath) => readJsonFile(join(options.tapRoot, relPath)), + }); + const fs = createFs(options.maxBytes); + const result = await buildHomebrewVfs(plan, { + fs, + createdBy: "scripts/homebrew-package-node-smoke.ts", + loadBottleBytes: (pkg) => loadBottleBytes(pkg, options), + }); + + const reportPath = join(options.resultDir, `${formula}-${options.arch}-homebrew-vfs-report.json`); + writeFileSync(reportPath, `${JSON.stringify(result.report, null, 2)}\n`); + const imagePath = join(options.resultDir, `${formula}-${options.arch}-homebrew.vfs.zst`); + const imageBytes = await saveImage(fs, imagePath, { + metadata: { + version: 1, + kernelAbi: plan.kandeloAbi, + createdBy: "scripts/homebrew-package-node-smoke.ts", + homebrew: { + tapRepository: plan.tapRepository, + tapCommit: plan.tapCommit, + releaseTag: plan.releaseTag, + packages: plan.packages.map((pkg) => ({ + name: pkg.name, + version: pkg.version, + arch: pkg.arch, + sourceStatus: pkg.sourceStatus, + cacheKeySha: pkg.cacheKeySha, + })), + }, + }, + }); + return { fs, imageBytes, reportPath }; +} + +async function runFormulaSmoke( + formula: FormulaName, + built: BuiltVfs, + options: CliOptions, +): Promise { + switch (formula) { + case "sqlite": + return runSqliteSmoke(built, options); + case "fbdoom": + throw new SkipCase( + "fbdoom requires IWAD data and framebuffer/audio device plumbing; record browser framebuffer smoke separately", + ); + case "modeset": + throw new SkipCase( + "modeset requires DRI/GLES framebuffer device support; record browser/device smoke separately", + ); + default: { + const spec = PROGRAM_SMOKES[formula]; + if (!spec) throw new SkipCase(`no Node smoke is defined for formula ${formula}`); + return runProgramSmoke(spec, built, options); + } + } +} + +async function runProgramSmoke( + spec: ProgramSmokeSpec, + built: BuiltVfs, + options: CliOptions, +): Promise { + const programBytes = readVfsFile(built.fs, spec.guestPath); + const result = await runWasm(programBytes, [spec.argv0, ...spec.args], built.imageBytes, options); + if (result.exitCode !== 0) { + throw new Error( + `${spec.argv0} ${spec.args.join(" ")} exited ${result.exitCode}; stderr=${JSON.stringify(result.stderr)}`, + ); + } + const combined = `${result.stdout}\n${result.stderr}`; + if (!spec.expected.test(combined)) { + throw new Error( + `unexpected ${spec.argv0} ${spec.args.join(" ")} output: ${JSON.stringify(combined)}`, + ); + } + return spec.description + ?? combined.trim().split("\n").find((line) => line.trim() !== "") + ?? `${spec.argv0} ${spec.args.join(" ")} passed`; +} + +async function runSqliteSmoke(built: BuiltVfs, options: CliOptions): Promise { + const stage = join(options.resultDir, "sqlite-consumer-build", options.arch); + rmSync(stage, { recursive: true, force: true }); + mkdirSync(join(stage, "include"), { recursive: true }); + mkdirSync(join(stage, "lib"), { recursive: true }); + + const version = findPackageVersion(built.fs, "sqlite"); + writeFileSync( + join(stage, "include", "sqlite3.h"), + readVfsFile(built.fs, `${CELLAR}/sqlite/${version}/include/sqlite3.h`), + ); + writeFileSync( + join(stage, "include", "sqlite3ext.h"), + readVfsFile(built.fs, `${CELLAR}/sqlite/${version}/include/sqlite3ext.h`), + ); + writeFileSync( + join(stage, "lib", "libsqlite3.a"), + readVfsFile(built.fs, `${CELLAR}/sqlite/${version}/lib/libsqlite3.a`), + ); + + const testSrc = join(repoRoot, "packages", "registry", "sqlite", "test", "sqlite_basic.c"); + const outWasm = join(stage, "sqlite_basic.wasm"); + const cc = join(repoRoot, "sdk", "bin", `${options.arch}posix-cc`); + execFileSync(cc, [ + `-I${join(stage, "include")}`, + testSrc, + join(stage, "lib", "libsqlite3.a"), + "-lm", + "-o", + outWasm, + ], { + cwd: repoRoot, + env: { + ...process.env, + PATH: `${join(repoRoot, "sdk", "bin")}:${process.env.PATH ?? ""}`, + WASM_POSIX_SYSROOT: join(repoRoot, options.arch === "wasm64" ? "sysroot64" : "sysroot"), + }, + stdio: "pipe", + }); + + const consumerBytes = new Uint8Array(readFileSync(outWasm)); + const result = await runWasm(consumerBytes, ["sqlite_basic"], built.imageBytes, options); + if (result.exitCode !== 0) { + throw new Error(`sqlite_basic exited ${result.exitCode}; stderr=${JSON.stringify(result.stderr)}`); + } + if (!result.stdout.includes("PASS")) { + throw new Error(`sqlite_basic did not report PASS: ${JSON.stringify(result.stdout)}`); + } + return "sqlite_basic linked against poured sqlite keg and reported PASS"; +} + +async function runWasm( + programBytes: Uint8Array, + argv: string[], + rootfsImage: Uint8Array, + options: CliOptions, +): Promise<{ exitCode: number; stdout: string; stderr: string }> { + let stdout = ""; + let stderr = ""; + const host = new NodeKernelHost({ + maxWorkers: 4, + rootfsImage, + onStdout: (_pid, data) => { stdout += new TextDecoder().decode(data); }, + onStderr: (_pid, data) => { stderr += new TextDecoder().decode(data); }, + }); + await host.init(); + let timeout: ReturnType | undefined; + try { + const exitPromise = host.spawn(toArrayBuffer(programBytes), argv, { + env: [ + "PATH=/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin", + "HOME=/tmp", + "TMPDIR=/tmp", + ], + cwd: "/", + stdin: new Uint8Array(), + }); + const timeoutPromise = new Promise((_, reject) => { + timeout = setTimeout( + () => reject(new Error(`${argv[0]} timed out after ${options.timeoutMs}ms`)), + options.timeoutMs, + ); + }); + const exitCode = await Promise.race([exitPromise, timeoutPromise]); + return { exitCode, stdout, stderr }; + } finally { + if (timeout) clearTimeout(timeout); + await host.destroy().catch(() => {}); + } +} + +async function runCase( + outcomes: Outcome[], + options: CliOptions, + tapCommit: string, + name: string, + fn: () => Promise, +): Promise { + writeCurrentRun(options, { + status: "running", + tapCommit, + outcomes, + currentCase: name, + }); + const started = Date.now(); + try { + const details = await fn(); + outcomes.push({ name, status: "pass", durationMs: Date.now() - started, details }); + } catch (err) { + const error = err instanceof Error ? err : new Error(String(err)); + outcomes.push({ + name, + status: error instanceof SkipCase ? "skip" : "fail", + durationMs: Date.now() - started, + details: error.message, + error: error.stack ?? error.message, + }); + } + writeOutcomeLists(options.resultDir, outcomes); +} + +async function loadBottleBytes( + pkg: HomebrewVfsPackagePlan, + options: CliOptions, +): Promise { + if (pkg.url.startsWith("file://")) { + return new Uint8Array(readFileSync(fileURLToPath(pkg.url))); + } + + const cachePath = join(options.bottleCache, `${pkg.sha256}.tar.gz`); + if (existsSync(cachePath)) return new Uint8Array(readFileSync(cachePath)); + if (!pkg.url.startsWith("https://")) { + throw new Error( + `package ${pkg.name}@${pkg.version} bottle URL must be https:// or file://, got ${pkg.url}`, + ); + } + + const bytes = await fetchHomebrewBottleBytes(pkg.url); + writeFileSync(cachePath, bytes); + return bytes; +} + +function findPackageVersion(fs: MemoryFileSystem, formula: string): string { + const info = JSON.parse(new TextDecoder().decode(readVfsFile(fs, "/etc/kandelo/homebrew-vfs.json"))); + const pkg = info.packages?.find((candidate: { name?: string }) => candidate.name === formula); + if (!pkg) throw new Error(`package ${formula} missing from /etc/kandelo/homebrew-vfs.json`); + const keg = String(pkg.keg ?? ""); + const prefix = `${CELLAR}/${formula}/`; + if (!keg.startsWith(prefix)) throw new Error(`unexpected sqlite keg path: ${keg}`); + return keg.slice(prefix.length); +} + +function readVfsFile(fs: MemoryFileSystem, path: string): Uint8Array { + const st = fs.stat(path); + const fd = fs.open(path, 0, 0); + try { + const out = new Uint8Array(st.size); + let offset = 0; + while (offset < out.byteLength) { + const n = fs.read(fd, out.subarray(offset), null, out.byteLength - offset); + if (n <= 0) break; + offset += n; + } + return out.subarray(0, offset); + } finally { + fs.close(fd); + } +} + +function createFs(maxBytes: number): MemoryFileSystem { + const SharedArrayBufferCtor = SharedArrayBuffer as new ( + byteLength: number, + options?: { maxByteLength?: number }, + ) => SharedArrayBuffer; + return MemoryFileSystem.create( + new SharedArrayBufferCtor(maxBytes, { maxByteLength: maxBytes }), + maxBytes, + ); +} + +function parseArgs(args: string[]): CliOptions { + const defaultResultDir = join( + repoRoot, + "test-runs", + "homebrew-package-node-smoke", + new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "Z"), + ); + const options: CliOptions = { + resultDir: process.env.KANDELO_TEST_RESULT_DIR || defaultResultDir, + tapRoot: process.env.KANDELO_HOMEBREW_TAP_ROOT || "", + formulas: [], + arch: "wasm32", + bottleCache: "", + timeoutMs: 30_000, + maxBytes: 128 * 1024 * 1024, + beadId: process.env.KANDELO_BEAD_ID || "kd-1mr.2", + }; + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + switch (arg) { + case "--result-dir": + options.resultDir = requireValue(args, ++i, arg); + break; + case "--tap-root": + options.tapRoot = requireValue(args, ++i, arg); + break; + case "--formula": + options.formulas.push(parseFormula(requireValue(args, ++i, arg))); + break; + case "--arch": + options.arch = parseArch(requireValue(args, ++i, arg)); + break; + case "--bottle-cache": + options.bottleCache = requireValue(args, ++i, arg); + break; + case "--timeout-ms": + options.timeoutMs = parsePositiveInt(requireValue(args, ++i, arg), arg); + break; + case "--max-bytes": + options.maxBytes = parseByteSize(requireValue(args, ++i, arg)); + break; + case "--bead-id": + options.beadId = requireValue(args, ++i, arg); + break; + case "--help": + case "-h": + usage(0); + break; + default: + usage(2, `unexpected argument ${arg}`); + } + } + + if (!options.tapRoot) usage(2, "--tap-root is required"); + if (options.formulas.length === 0) usage(2, "at least one --formula is required"); + options.resultDir = resolve(options.resultDir); + options.tapRoot = resolve(options.tapRoot); + options.bottleCache = options.bottleCache + ? resolve(options.bottleCache) + : join(options.resultDir, "bottle-cache"); + return options; +} + +function parseFormula(value: string): FormulaName { + if (/^[a-z0-9][a-z0-9._-]*$/.test(value)) return value; + usage(2, `invalid --formula ${value}`); +} + +function parseArch(value: string): HomebrewBottleArch { + if (value === "wasm32" || value === "wasm64") return value; + usage(2, `--arch must be wasm32 or wasm64, got ${value}`); +} + +function parsePositiveInt(value: string, flag: string): number { + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed <= 0) usage(2, `${flag} must be a positive integer`); + return parsed; +} + +function parseByteSize(value: string): number { + const match = /^([1-9][0-9]*)([kKmMgG]i?[bB]?|[bB])?$/.exec(value); + if (!match) usage(2, `--max-bytes must be a positive byte size, got ${value}`); + const amount = Number(match[1]); + const suffix = (match[2] ?? "b").toLowerCase(); + const multiplier = suffix.startsWith("g") ? 1024 ** 3 + : suffix.startsWith("m") ? 1024 ** 2 + : suffix.startsWith("k") ? 1024 + : 1; + return amount * multiplier; +} + +function requireValue(args: string[], index: number, flag: string): string { + const value = args[index]; + if (!value || value.startsWith("--")) usage(2, `${flag} requires a value`); + return value; +} + +function usage(code: number, message?: string): never { + if (message) console.error(`homebrew-package-node-smoke: ${message}`); + console.error(`usage: npx tsx scripts/homebrew-package-node-smoke.ts \\ + --tap-root --formula [--formula ...] \\ + [--arch ] [--result-dir ] [--bottle-cache ]`); + process.exit(code); +} + +function writeOutcomeLists(resultDir: string, outcomes: Outcome[]): void { + const listsDir = join(resultDir, "outcome-lists"); + mkdirSync(listsDir, { recursive: true }); + const passed = outcomes.filter((outcome) => outcome.status === "pass"); + const failed = outcomes.filter((outcome) => outcome.status === "fail"); + const skipped = outcomes.filter((outcome) => outcome.status === "skip"); + writeFileSync( + join(listsDir, "passed-tests.tsv"), + ["test\tduration_ms\tdetails", ...passed.map((outcome) => + `${outcome.name}\t${outcome.durationMs}\t${tsv(outcome.details ?? "")}`, + )].join("\n") + "\n", + ); + writeFileSync( + join(listsDir, "failed-tests.tsv"), + ["test\tduration_ms\terror", ...failed.map((outcome) => + `${outcome.name}\t${outcome.durationMs}\t${tsv(outcome.error ?? outcome.details ?? "")}`, + )].join("\n") + "\n", + ); + writeFileSync( + join(listsDir, "skipped-tests.tsv"), + ["test\treason", ...skipped.map((outcome) => + `${outcome.name}\t${tsv(outcome.details ?? "")}`, + )].join("\n") + "\n", + ); + writeFileSync(join(resultDir, "failures.json"), `${JSON.stringify(failed, null, 2)}\n`); +} + +function writeSummary( + options: CliOptions, + data: { + startedAt: Date; + completedAt: Date; + tapCommit: string; + outcomes: Outcome[]; + }, +): void { + const counts = countOutcomes(data.outcomes); + const summary = { + suite: "Homebrew package Node VFS smoke", + bead_id: options.beadId, + started_at: data.startedAt.toISOString(), + completed_at: data.completedAt.toISOString(), + duration_ms: data.completedAt.getTime() - data.startedAt.getTime(), + result_dir: options.resultDir, + tap_root: options.tapRoot, + tap_commit: data.tapCommit, + arch: options.arch, + formulas: options.formulas, + counts, + outcomes: data.outcomes, + artifacts: { + passed: join(options.resultDir, "outcome-lists", "passed-tests.tsv"), + failed: join(options.resultDir, "outcome-lists", "failed-tests.tsv"), + skipped: join(options.resultDir, "outcome-lists", "skipped-tests.tsv"), + failures: join(options.resultDir, "failures.json"), + current_run: join(options.resultDir, "current-run.json"), + }, + }; + writeFileSync(join(options.resultDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`); + writeFileSync(join(options.resultDir, "summary.md"), [ + "# Homebrew package Node VFS smoke", + "", + `Result dir: \`${options.resultDir}\``, + `Tap commit: \`${data.tapCommit}\``, + `Counts: ${counts.pass} pass, ${counts.fail} fail, ${counts.skip} skip`, + "", + "| Test | Status | Details |", + "|---|---:|---|", + ...data.outcomes.map((outcome) => + `| \`${outcome.name}\` | ${outcome.status} | ${outcome.details ? outcome.details.replace(/\|/g, "\\|") : ""} |`, + ), + "", + ].join("\n")); +} + +function writeCurrentRun( + options: CliOptions, + data: { + status: "running" | "complete" | "failed"; + startedAt?: Date; + tapCommit: string; + outcomes: Outcome[]; + currentCase: string; + }, +): void { + const counts = countOutcomes(data.outcomes); + const currentRun = { + suite: "homebrew-package-node-smoke", + bead_id: options.beadId, + worktree: repoRoot, + result_dir: options.resultDir, + status: data.status, + started_at: data.startedAt?.toISOString(), + updated_at: new Date().toISOString(), + current_case: data.currentCase, + progress: { + completed: data.outcomes.length, + total: options.formulas.length * 2, + pass: counts.pass, + fail: counts.fail, + skip: counts.skip, + }, + tap_root: options.tapRoot, + tap_commit: data.tapCommit, + command: { + cwd: repoRoot, + argv: process.argv, + }, + outcome_lists: { + passed: join(options.resultDir, "outcome-lists", "passed-tests.tsv"), + failed: join(options.resultDir, "outcome-lists", "failed-tests.tsv"), + skipped: join(options.resultDir, "outcome-lists", "skipped-tests.tsv"), + }, + stale_no_runner_threshold_seconds: 600, + expected_next: data.status === "running" + ? { deterministic: true, action: "continue current smoke case" } + : { deterministic: false, action: "suite terminal" }, + }; + const out = join(options.resultDir, "current-run.json"); + const tmp = `${out}.tmp`; + writeFileSync(tmp, `${JSON.stringify(currentRun, null, 2)}\n`); + renameSync(tmp, out); +} + +function countOutcomes(outcomes: Outcome[]): { pass: number; fail: number; skip: number } { + return { + pass: outcomes.filter((outcome) => outcome.status === "pass").length, + fail: outcomes.filter((outcome) => outcome.status === "fail").length, + skip: outcomes.filter((outcome) => outcome.status === "skip").length, + }; +} + +function readJsonFile(path: string): T { + return JSON.parse(readFileSync(path, "utf8")) as T; +} + +function gitRevParse(path: string): string { + return execFileSync("git", ["-C", path, "rev-parse", "HEAD"], { + encoding: "utf8", + }).trim(); +} + +function toArrayBuffer(bytes: Uint8Array): ArrayBuffer { + return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; +} + +function tsv(value: string): string { + return value.replace(/\t/g, " ").replace(/\r?\n/g, "\\n"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/install-local-binary.sh b/scripts/install-local-binary.sh index dfd7a97763..017217c806 100755 --- a/scripts/install-local-binary.sh +++ b/scripts/install-local-binary.sh @@ -67,7 +67,10 @@ install_local_binary() { local src_basename src_basename="$(basename "$src")" local host_target - host_target="$(rustc -vV 2>/dev/null | awk '/^host/ {print $2}')" + host_target="" + if command -v rustc >/dev/null 2>&1; then + host_target="$(rustc -vV 2>/dev/null | awk '/^host/ {print $2}' || true)" + fi if ! wasm_require_no_legacy_asyncify "$src"; then return 1 @@ -152,9 +155,11 @@ install_local_binary() { dest="$repo_root/local-binaries/programs/$arch/$program$src_ext" fi - mkdir -p "$(dirname "$dest")" - cp "$src" "$dest" - echo " installed $dest" + if [ "${WASM_POSIX_SKIP_LOCAL_BINARY_INSTALL:-0}" != "1" ]; then + mkdir -p "$(dirname "$dest")" + cp "$src" "$dest" + echo " installed $dest" + fi # When invoked under the package-system resolver (`xtask build-deps # resolve`, `xtask archive-stage`), WASM_POSIX_DEP_OUT_DIR points at @@ -172,8 +177,12 @@ install_local_binary() { # path is a no-op. if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then local resolver_dest="$WASM_POSIX_DEP_OUT_DIR/$src_basename" - mkdir -p "$(dirname "$resolver_dest")" - cp "$src" "$resolver_dest" - echo " installed $resolver_dest (resolver scratch)" + if [ "$src" -ef "$resolver_dest" ]; then + echo " installed $resolver_dest (resolver scratch)" + else + mkdir -p "$(dirname "$resolver_dest")" + cp "$src" "$resolver_dest" + echo " installed $resolver_dest (resolver scratch)" + fi fi } diff --git a/sdk/src/bin/pkg-config.ts b/sdk/src/bin/pkg-config.ts index 9576f834b2..4f77b50f78 100755 --- a/sdk/src/bin/pkg-config.ts +++ b/sdk/src/bin/pkg-config.ts @@ -54,7 +54,8 @@ async function main(): Promise { const arch = detectArch(); const sysroot = findSysroot(arch); const env = buildPkgConfigEnv(process.env, sysroot); - const exitCode = await runPassthrough('pkg-config', process.argv.slice(2), env); + const pkgConfig = process.env.PKG_CONFIG || 'pkg-config'; + const exitCode = await runPassthrough(pkgConfig, process.argv.slice(2), env); process.exit(exitCode); }