From 273ab18874869a07142d673f5a8a3bd7e3993b67 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Sat, 11 Jul 2026 06:30:55 -0400 Subject: [PATCH 1/2] redis: add the threaded data service and client --- Formula/redis.rb | 181 ++++++++++++++++++ .../kandelo_formula_support.rb | 26 ++- .../run-virtual-network-pairs.ts | 31 ++- .../test/kandelo_formula_support_test.rb | 20 ++ README.md | 1 + 5 files changed, 243 insertions(+), 16 deletions(-) create mode 100644 Formula/redis.rb diff --git a/Formula/redis.rb b/Formula/redis.rb new file mode 100644 index 0000000..21e404f --- /dev/null +++ b/Formula/redis.rb @@ -0,0 +1,181 @@ +require_relative "../Kandelo/formula_support/kandelo_formula_support" + +class Redis < Formula + include KandeloFormulaSupport + + desc "In-memory data structure server and client for Kandelo" + homepage "https://redis.io/" + url "https://github.com/redis/redis/archive/refs/tags/7.2.5.tar.gz" + sha256 "98a8502a2e902d2a9785ef46a69a5f8d5e24cbf9ea3ae4d845afcfc6778aa783" + license "BSD-3-Clause" + + depends_on "binaryen" => :build + depends_on "wabt" => :build + + skip_clean "bin/redis-server" + skip_clean "bin/redis-cli" + + # Redis defines every module API function-pointer global before selecting its + # non-TLS branch; LLVM's Wasm AsmPrinter crashes on that unused global set. + # Keep the module-only declarations inside the existing TLS condition. + patch :DATA + + def install + kandelo_require_arch!("wasm32") + + kandelo_wasm_build do |root| + ENV["CFLAGS"] = "-O2 -gline-tables-only -fdebug-compilation-dir=." + + # Redis' Makefiles inspect the build host with uname even while using a + # cross compiler. Select the Linux-compatible target link set so pthread, + # realtime, and dl APIs come from Kandelo rather than the macOS host. + inreplace "src/Makefile" do |s| + s.sub!(/^uname_S :=.*$/, "uname_S := Linux") + s.sub!(/^uname_M :=.*$/, "uname_M := wasm32") + end + inreplace "deps/Makefile" do |s| + s.sub!(/^uname_S:=.*$/, "uname_S:= Linux") + s.sub!(/^AR=ar$/, "AR?=ar") + s.sub!(/^ARFLAGS=rc$/, "ARFLAGS?=rc\nRANLIB?=ranlib") + s.sub!("cd hiredis && $(MAKE) static $(HIREDIS_MAKE_FLAGS)", + 'cd hiredis && $(MAKE) static $(HIREDIS_MAKE_FLAGS) AR="$(AR)"') + s.sub!(/^\tcd hdr_histogram && \$\(MAKE\)$/, "\tcd hdr_histogram && $(MAKE) AR=\"$(AR)\" ARFLAGS=rcs") + s.sub!(/^\tcd fpconv && \$\(MAKE\)$/, "\tcd fpconv && $(MAKE) AR=\"$(AR)\" ARFLAGS=rcs") + s.sub!('AR="$(AR) $(ARFLAGS)"', 'AR="$(AR) $(ARFLAGS)" RANLIB="$(RANLIB)"') + end + + # Redis prefixes the top-level dependency recipe with `-`, so Make would + # ignore a failed archive build and the SDK linker would preserve those + # symbols as env imports. Build the dependencies as a checked step. + system "make", "-C", "deps", "-j#{ENV.make_jobs}", + "BUILD_TLS=no", + "hiredis", + "linenoise", + "lua", + "hdr_histogram", + "fpconv" + + system "make", "-C", "src", "-j#{ENV.make_jobs}", + "MALLOC=libc", + "USE_SYSTEMD=no", + "BUILD_TLS=no", + "OPTIMIZATION=-O2", + "redis-server", + "redis-cli" + + optimized_server = buildpath/"src/redis-server.optimized" + optimized_cli = buildpath/"src/redis-cli.optimized" + instrumented_server = buildpath/"src/redis-server.instrumented" + system "wasm-opt", "-O2", buildpath/"src/redis-server", "-o", optimized_server + system "wasm-opt", "-O2", buildpath/"src/redis-cli", "-o", optimized_cli + system "#{root}/scripts/run-wasm-fork-instrument.sh", + optimized_server, "-o", instrumented_server + + artifact_guards = "#{root}/scripts/wasm-artifact-guards.sh" + system "bash", "-c", <<~SH + set -euo pipefail + . #{artifact_guards.shellescape} + expected_abi=$(wasm_current_abi_version #{root.to_s.shellescape}) + for artifact in #{instrumented_server.to_s.shellescape} #{optimized_cli.to_s.shellescape}; do + artifact_abi=$(wasm_extract_abi_version "$artifact") + if [ -z "$expected_abi" ] || [ "$artifact_abi" != "$expected_abi" ]; then + echo "ERROR: Redis ABI $artifact_abi does not match Kandelo ABI $expected_abi: $artifact" >&2 + exit 1 + fi + wasm_require_no_legacy_asyncify "$artifact" + wasm_require_fork_instrumentation_if_needed "$artifact" + unexpected_env_imports=$(wasm-objdump -x "$artifact" | + awk '/<- env[.]/ { sub(/^.*<- env[.]/, ""); print $1 }' | + grep -Ev '^(__channel_base|memory|__wasm_dlclose|__wasm_dlerror|__wasm_dlopen|__wasm_dlsym)$' || true) + if [ -n "$unexpected_env_imports" ]; then + echo "ERROR: Redis contains unresolved non-ABI env imports: $artifact" >&2 + echo "$unexpected_env_imports" >&2 + exit 1 + fi + done + if ! wasm_has_complete_fork_instrumentation #{instrumented_server.to_s.shellescape}; then + echo "ERROR: redis-server has incomplete fork instrumentation" >&2 + exit 1 + fi + SH + end + + kandelo_install_bin(buildpath/"src", "redis-server.instrumented", "redis-server") + kandelo_install_bin(buildpath/"src", "redis-cli.optimized", "redis-cli") + end + + test do + server_version = kandelo_run_wasm(bin/"redis-server", ["--version"]) + assert_match(/Redis server v=7\.2\.5 .*malloc=libc bits=32 /, server_version) + assert_equal "redis-cli 7.2.5\n", kandelo_run_wasm(bin/"redis-cli", ["--version"]) + + commands = <<~COMMANDS + SET kandelo homebrew + GET kandelo + INCR formula-counter + INCR formula-counter + EVAL "return redis.call('GET', KEYS[1])" 1 kandelo + INFO server + SHUTDOWN NOSAVE + COMMANDS + service_output = kandelo_run_virtual_network_pairs( + bin/"redis-server", + [{ + name: "redis", + transport: "tcp", + serverArgs: [ + "redis-server", + "--bind", "10.88.0.2", + "--port", "26379", + "--protected-mode", "no", + "--save", "", + "--appendonly", "no", + "--daemonize", "no", + "--loglevel", "warning", + "--maxmemory", "64mb", + "--maxmemory-policy", "noeviction", + "--tcp-backlog", "128", + "--dir", "/tmp" + ], + clientArgs: ["redis-cli", "-h", "10.88.0.2", "-p", "26379", "--raw"], + serverStdin: "", + clientStdin: commands, + expectedServerStdoutIncludes: ["Redis is now ready to exit, bye bye..."], + expectedClientStdoutIncludes: [ + "OK\nhomebrew\n1\n2\nhomebrew\n", + "redis_version:7.2.5", + "multiplexing_api:select", + "atomicvar_api:c11-builtin", + ], + timeoutMs: 20_000, + }], + client_bin_path: bin/"redis-cli", + ) + assert_includes service_output, '"redis"' + + [bin/"redis-server", bin/"redis-cli"].each do |binary| + bytes = File.binread(binary) + refute_includes bytes, prefix.to_s + refute_includes bytes, "/nix/store/" + refute_match %r{/private/tmp/[^/]+/}, bytes + refute_match %r{/Users/[^/]+/}, bytes + end + end +end + +__END__ +diff --git a/src/tls.c b/src/tls.c +index 0fce662..d6466c5 100644 +--- a/src/tls.c ++++ b/src/tls.c +@@ -29,0 +30 @@ ++#if (USE_OPENSSL == 1 /* BUILD_YES */ ) || ((USE_OPENSSL == 2 /* BUILD_MODULE */) && (BUILD_TLS_MODULE == 2)) +@@ -30,0 +32,2 @@ ++#define REDIS_TLS_COMPILED 1 ++#endif +@@ -32,0 +36,2 @@ ++ ++#ifdef REDIS_TLS_COMPILED +@@ -36,2 +40,0 @@ +-#if (USE_OPENSSL == 1 /* BUILD_YES */ ) || ((USE_OPENSSL == 2 /* BUILD_MODULE */) && (BUILD_TLS_MODULE == 2)) +- diff --git a/Kandelo/formula_support/kandelo_formula_support.rb b/Kandelo/formula_support/kandelo_formula_support.rb index 760a62a..83e8b6d 100644 --- a/Kandelo/formula_support/kandelo_formula_support.rb +++ b/Kandelo/formula_support/kandelo_formula_support.rb @@ -715,18 +715,23 @@ def kandelo_run_framebuffer_wasm( # Run paired programs on isolated Kandelo machines joined by the host's # LocalVirtualNetwork. This exercises bind/listen/connect and datagram paths # that a single-process formula runner cannot cover. - def kandelo_run_virtual_network_pairs(bin_path, cases) + def kandelo_run_virtual_network_pairs(server_bin_path, cases, client_bin_path: server_bin_path) root = kandelo_require_root! if (node = ENV.fetch("HOMEBREW_KANDELO_NODE", nil)).to_s != "" ENV.prepend_path "PATH", File.dirname(node) end - wasm_path = Pathname(bin_path) - if wasm_path.extname != ".wasm" - staged_wasm = testpath/"#{wasm_path.basename}.wasm" - File.binwrite(staged_wasm, File.binread(wasm_path)) - wasm_path = staged_wasm + stage_wasm = lambda do |bin_path| + wasm_path = Pathname(bin_path) + if wasm_path.extname != ".wasm" + staged_wasm = testpath/"#{wasm_path.basename}.wasm" + File.binwrite(staged_wasm, File.binread(wasm_path)) + wasm_path = staged_wasm + end + wasm_path end + server_wasm_path = stage_wasm.call(server_bin_path) + client_wasm_path = stage_wasm.call(client_bin_path) config = JSON.generate({ cases: cases }) # Compiled host output shadows TypeScript source under tsx. Network-pair @@ -738,11 +743,16 @@ def kandelo_run_virtual_network_pairs(bin_path, cases) command << "KANDELO_FORMULA_VIRTUAL_PAIRS_JSON=#{Shellwords.escape(config)} " command << "node --experimental-wasm-exnref --import tsx/esm " command << "#{Shellwords.escape(runner.to_s)} #{Shellwords.escape(root)} " - command << Shellwords.escape(wasm_path.to_s) + command << "#{Shellwords.escape(server_wasm_path.to_s)} " + command << Shellwords.escape(client_wasm_path.to_s) command << " 2>&1" output = shell_output(command) - kandelo_record_node_execution!(wasm_path, [], launcher: "kandelo_run_virtual_network_pairs") + kandelo_record_node_execution!( + server_wasm_path, + [client_wasm_path], + launcher: "kandelo_run_virtual_network_pairs", + ) output end diff --git a/Kandelo/formula_support/run-virtual-network-pairs.ts b/Kandelo/formula_support/run-virtual-network-pairs.ts index a7a45c3..f707e12 100644 --- a/Kandelo/formula_support/run-virtual-network-pairs.ts +++ b/Kandelo/formula_support/run-virtual-network-pairs.ts @@ -8,7 +8,10 @@ interface PairCase { clientArgs: string[]; serverStdin: string; clientStdin: string; - expectedServerStdout: string; + expectedServerStdout?: string; + expectedServerStdoutIncludes?: string[]; + expectedClientStdout?: string; + expectedClientStdoutIncludes?: string[]; timeoutMs?: number; } @@ -17,9 +20,11 @@ interface PairConfig { } async function main(): Promise { - const [root, programPath] = process.argv.slice(2); - if (!root || !programPath) { - throw new Error("usage: run-virtual-network-pairs.ts KANDELO_ROOT PROGRAM"); + const [root, serverProgramPath, clientProgramPath] = process.argv.slice(2); + if (!root || !serverProgramPath || !clientProgramPath) { + throw new Error( + "usage: run-virtual-network-pairs.ts KANDELO_ROOT SERVER CLIENT", + ); } const config = JSON.parse( @@ -85,7 +90,7 @@ async function main(): Promise { const timeout = pair.timeoutMs ?? 10_000; const serverRun = runCentralizedProgram({ - programPath, + programPath: serverProgramPath, argv: pair.serverArgs, io: serverIO, stdin: pair.serverStdin, @@ -105,7 +110,7 @@ async function main(): Promise { }), ]); const clientRun = runCentralizedProgram({ - programPath, + programPath: clientProgramPath, argv: pair.clientArgs, io: clientIO, stdin: pair.clientStdin, @@ -117,14 +122,24 @@ async function main(): Promise { clientStatus: client.exitCode, serverStdout: server.stdout, serverStderr: server.stderr, + clientStdout: client.stdout, clientStderr: client.stderr, }; + const expectedServerStdout = pair.expectedServerStdout; + const expectedServerIncludes = pair.expectedServerStdoutIncludes ?? []; + const expectedClientStdout = pair.expectedClientStdout; + const expectedClientIncludes = pair.expectedClientStdoutIncludes ?? []; if ( server.exitCode !== 0 || client.exitCode !== 0 || - server.stdout !== pair.expectedServerStdout || + (expectedServerStdout !== undefined && + server.stdout !== expectedServerStdout) || + expectedServerIncludes.some((value) => !server.stdout.includes(value)) || server.stderr !== "" || - client.stderr !== "" + client.stderr !== "" || + (expectedClientStdout !== undefined && + client.stdout !== expectedClientStdout) || + expectedClientIncludes.some((value) => !client.stdout.includes(value)) ) { throw new Error(`${pair.name} failed: ${JSON.stringify(summary)}`); } diff --git a/Kandelo/formula_support/test/kandelo_formula_support_test.rb b/Kandelo/formula_support/test/kandelo_formula_support_test.rb index 566da60..5726a95 100644 --- a/Kandelo/formula_support/test/kandelo_formula_support_test.rb +++ b/Kandelo/formula_support/test/kandelo_formula_support_test.rb @@ -842,4 +842,24 @@ def artifact_validation_harness(dir, harness_class = Harness) harness.prefix_path = Pathname(dir)/"cellar/formula/1.0" harness end + + def test_virtual_network_pairs_accept_separate_server_and_client_programs + harness = Harness.new + harness.kandelo_run_virtual_network_pairs( + "server.wasm", + [{ + name: "service", + transport: "tcp", + serverArgs: ["server"], + clientArgs: ["client"], + expectedServerStdoutIncludes: ["server-stopped"], + expectedClientStdoutIncludes: ["service-ok"], + }], + client_bin_path: "client.wasm", + ) + + assert_includes harness.command, "server.wasm client.wasm" + assert_includes harness.command, "expectedServerStdoutIncludes" + assert_includes harness.command, "expectedClientStdoutIncludes" + end end diff --git a/README.md b/README.md index c4b50c6..364feed 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Current migration controls and pilots include: - `ctags`, Universal Ctags' maintained tag generator, `readtags` query client, and optscript interpreter with complete C and C++ workflows. - `netcat`, GNU TCP and UDP client/listener workflows across virtual Kandelo machines. +- `redis`, the threaded in-memory service and its network client. The SDK is not yet a Homebrew dependency. Trusted builds supply an `HOMEBREW_KANDELO_ROOT` checkout containing the SDK, sysroot, kernel, and Node From 0e81c80d197f43bf270c14e9d37ec11c9ba3ddd2 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Sun, 12 Jul 2026 08:43:49 -0400 Subject: [PATCH 2/2] redis: enforce reproducible artifact contracts --- Formula/redis.rb | 38 ++++++++++--------- .../test/kandelo_formula_support_test.rb | 32 ++++++++-------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/Formula/redis.rb b/Formula/redis.rb index 21e404f..9066af7 100644 --- a/Formula/redis.rb +++ b/Formula/redis.rb @@ -1,4 +1,4 @@ -require_relative "../Kandelo/formula_support/kandelo_formula_support" +require (Tap.fetch("automattic", "kandelo-homebrew").path/"Kandelo/formula_support/kandelo_formula_support").to_s class Redis < Formula include KandeloFormulaSupport @@ -24,7 +24,15 @@ def install kandelo_require_arch!("wasm32") kandelo_wasm_build do |root| - ENV["CFLAGS"] = "-O2 -gline-tables-only -fdebug-compilation-dir=." + prefix_maps = [ + "-ffile-prefix-map=#{buildpath}=/usr/src/redis", + "-fdebug-prefix-map=#{buildpath}=/usr/src/redis", + "-fmacro-prefix-map=#{buildpath}=/usr/src/redis", + "-ffile-prefix-map=#{root}=/usr/src/kandelo", + "-fdebug-prefix-map=#{root}=/usr/src/kandelo", + "-fmacro-prefix-map=#{root}=/usr/src/kandelo", + ] + ENV["CFLAGS"] = ["-O2", "-gline-tables-only", "-fdebug-compilation-dir=.", *prefix_maps].join(" ") # Redis' Makefiles inspect the build host with uname even while using a # cross compiler. Select the Linux-compatible target link set so pthread, @@ -66,24 +74,22 @@ def install optimized_server = buildpath/"src/redis-server.optimized" optimized_cli = buildpath/"src/redis-cli.optimized" instrumented_server = buildpath/"src/redis-server.instrumented" - system "wasm-opt", "-O2", buildpath/"src/redis-server", "-o", optimized_server - system "wasm-opt", "-O2", buildpath/"src/redis-cli", "-o", optimized_cli + # Linked SDK glue contributes DWARF from its own build. These release + # executables have no detached debug companion, so do not ship that + # host-specific debug metadata in either artifact. + system "wasm-opt", "-O2", "--strip-debug", buildpath/"src/redis-server", "-o", optimized_server + system "wasm-opt", "-O2", "--strip-debug", buildpath/"src/redis-cli", "-o", optimized_cli system "#{root}/scripts/run-wasm-fork-instrument.sh", optimized_server, "-o", instrumented_server - artifact_guards = "#{root}/scripts/wasm-artifact-guards.sh" + kandelo_validate_wasm_artifact(instrumented_server, fork: :required) + kandelo_validate_wasm_artifact(optimized_cli, fork: :forbidden) + + # Redis uses Kandelo's supported dynamic-loader bridge. Reject every + # other env import so a suppressed dependency-build failure stays loud. system "bash", "-c", <<~SH set -euo pipefail - . #{artifact_guards.shellescape} - expected_abi=$(wasm_current_abi_version #{root.to_s.shellescape}) for artifact in #{instrumented_server.to_s.shellescape} #{optimized_cli.to_s.shellescape}; do - artifact_abi=$(wasm_extract_abi_version "$artifact") - if [ -z "$expected_abi" ] || [ "$artifact_abi" != "$expected_abi" ]; then - echo "ERROR: Redis ABI $artifact_abi does not match Kandelo ABI $expected_abi: $artifact" >&2 - exit 1 - fi - wasm_require_no_legacy_asyncify "$artifact" - wasm_require_fork_instrumentation_if_needed "$artifact" unexpected_env_imports=$(wasm-objdump -x "$artifact" | awk '/<- env[.]/ { sub(/^.*<- env[.]/, ""); print $1 }' | grep -Ev '^(__channel_base|memory|__wasm_dlclose|__wasm_dlerror|__wasm_dlopen|__wasm_dlsym)$' || true) @@ -93,10 +99,6 @@ def install exit 1 fi done - if ! wasm_has_complete_fork_instrumentation #{instrumented_server.to_s.shellescape}; then - echo "ERROR: redis-server has incomplete fork instrumentation" >&2 - exit 1 - fi SH end diff --git a/Kandelo/formula_support/test/kandelo_formula_support_test.rb b/Kandelo/formula_support/test/kandelo_formula_support_test.rb index 5726a95..31efe62 100644 --- a/Kandelo/formula_support/test/kandelo_formula_support_test.rb +++ b/Kandelo/formula_support/test/kandelo_formula_support_test.rb @@ -827,22 +827,6 @@ def test_virtual_network_pairs_use_tap_owned_runner_and_remove_stale_host_dist end end - private - - def artifact_validation_harness(dir, harness_class = Harness) - root = Pathname(dir)/"kandelo root" - build = Pathname(dir)/"build" - (root/"scripts").mkpath - build.mkpath - (root/"scripts/wasm-artifact-guards.sh").binwrite("# validation fixture\n") - - harness = harness_class.new - harness.root_path = root.to_s - harness.build_path = build - harness.prefix_path = Pathname(dir)/"cellar/formula/1.0" - harness - end - def test_virtual_network_pairs_accept_separate_server_and_client_programs harness = Harness.new harness.kandelo_run_virtual_network_pairs( @@ -862,4 +846,20 @@ def test_virtual_network_pairs_accept_separate_server_and_client_programs assert_includes harness.command, "expectedServerStdoutIncludes" assert_includes harness.command, "expectedClientStdoutIncludes" end + + private + + def artifact_validation_harness(dir, harness_class = Harness) + root = Pathname(dir)/"kandelo root" + build = Pathname(dir)/"build" + (root/"scripts").mkpath + build.mkpath + (root/"scripts/wasm-artifact-guards.sh").binwrite("# validation fixture\n") + + harness = harness_class.new + harness.root_path = root.to_s + harness.build_path = build + harness.prefix_path = Pathname(dir)/"cellar/formula/1.0" + harness + end end