Skip to content
This repository was archived by the owner on Jul 21, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions Formula/redis.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
require (Tap.fetch("automattic", "kandelo-homebrew").path/"Kandelo/formula_support/kandelo_formula_support").to_s

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|
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,
# 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"
# 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

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
for artifact in #{instrumented_server.to_s.shellescape} #{optimized_cli.to_s.shellescape}; do
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
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))
-
26 changes: 18 additions & 8 deletions Kandelo/formula_support/kandelo_formula_support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
31 changes: 23 additions & 8 deletions Kandelo/formula_support/run-virtual-network-pairs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ interface PairCase {
clientArgs: string[];
serverStdin: string;
clientStdin: string;
expectedServerStdout: string;
expectedServerStdout?: string;
expectedServerStdoutIncludes?: string[];
expectedClientStdout?: string;
expectedClientStdoutIncludes?: string[];
timeoutMs?: number;
}

Expand All @@ -17,9 +20,11 @@ interface PairConfig {
}

async function main(): Promise<void> {
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(
Expand Down Expand Up @@ -85,7 +90,7 @@ async function main(): Promise<void> {

const timeout = pair.timeoutMs ?? 10_000;
const serverRun = runCentralizedProgram({
programPath,
programPath: serverProgramPath,
argv: pair.serverArgs,
io: serverIO,
stdin: pair.serverStdin,
Expand All @@ -105,7 +110,7 @@ async function main(): Promise<void> {
}),
]);
const clientRun = runCentralizedProgram({
programPath,
programPath: clientProgramPath,
argv: pair.clientArgs,
io: clientIO,
stdin: pair.clientStdin,
Expand All @@ -117,14 +122,24 @@ async function main(): Promise<void> {
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)}`);
}
Expand Down
20 changes: 20 additions & 0 deletions Kandelo/formula_support/test/kandelo_formula_support_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,26 @@ def test_virtual_network_pairs_use_tap_owned_runner_and_remove_stale_host_dist
end
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

private

def artifact_validation_harness(dir, harness_class = Harness)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down