ADR 0027: Cross-Platform Support

Status

Implemented (2026-02-17)

Context

The Problem

Beamtalk is developed and tested exclusively on Linux (Ubuntu in CI, devcontainers for development). There is no Windows CI, no macOS CI, no Windows testing, and several components use Unix-specific or Linux-specific system calls and tools. A developer on Windows cannot reliably build or run Beamtalk today, and macOS has degraded behavior in workspace management.

This matters because:

  1. Developer reach — Windows is ~45% of developer desktops, macOS ~30% (as of 2025, per Stack Overflow Developer Survey). Supporting only Linux limits adoption to a fraction of developers.
  2. macOS degradation — 6 #[cfg(target_os = "linux")] guards use /proc filesystem for process start-time verification. On macOS, these fall back to skipping stale-node detection, meaning a macOS developer could connect to a wrong/stale workspace without warning. TCP-first workspace management (see Decision) eliminates this entire category of macOS issues.
  3. Peer expectation — Gleam, Elixir, and Erlang all have first-class Windows and macOS support. A BEAM language that only tests on Linux is an outlier.
  4. CI gaps — Without Windows or macOS CI, regressions can silently break cross-platform compatibility even for code that should be portable.

Current State

The codebase has three tiers of Windows readiness:

Tier 1: Already portable (core compiler) The Rust compiler (beamtalk-core) — lexer, parser, AST, semantic analysis, codegen — is pure Rust with no platform-specific code. It uses Path/PathBuf for path handling and has no system calls. This should work on Windows today.

Tier 2: Partially guarded (CLI tooling) The CLI (beamtalk-cli) has Unix-specific code that is mostly behind #[cfg(unix)] guards with fallbacks:

CodeUnixWindows fallback
Parent PID (libc::getppid)std::process::id() stub
Cookie file permissions (0o600)✅ chmodWrites without mode
Process detection (ps command)TCP probe fallback
escript permissions (0o755)✅ chmodSkipped
Process termination (Unix signals)Returns error (not implemented)

Tier 3: Unix-only (workspace management, build scripts) Several components have no Windows path at all:

ComponentIssueFile(s)
find_beam_pid_by_node()Calls ps without #[cfg] guardcrates/beamtalk-cli/src/commands/workspace/mod.rs:573
wait_for_process_exit()Unix-only, uses signal probingcrates/beamtalk-cli/src/commands/workspace/mod.rs:737-761
stop_workspace()Returns error on Windowscrates/beamtalk-cli/src/commands/workspace/mod.rs:787-812
/proc start-time trackingLinux-only, degrades silently on macOScrates/beamtalk-cli/src/commands/workspace/mod.rs:262,293,586
REPL Unix guard#[cfg(unix)] in REPL modulecrates/beamtalk-cli/src/commands/repl/mod.rs:941
rebar3 pre-hookbash -c ./compile.sh Replaced by portable compile_fixtures.escriptruntime/rebar.config:21
Test fixture compilationBash script Replaced by compile_fixtures.escriptruntime/apps/beamtalk_runtime/test_fixtures/compile_fixtures.escript
Home directory fallbackos:getenv("HOME", "/tmp")runtime/apps/beamtalk_workspace/src/beamtalk_workspace_meta.erl:173
Project root detectionHardcoded / root checkruntime/apps/beamtalk_compiler/src/beamtalk_compiler_port.erl:153
Justfileset shell := ["bash", "-uc"]Justfile:9
CI workflowsUbuntu-only, bash commands.github/workflows/ci.yml

Constraints

Decision

Adopt a tiered approach to cross-platform support

Tier 1 (immediate): Compiler + build command work on Windows

The core compilation path — beamtalk build, beamtalk new, beamtalk test — must work on Windows. This means:

  1. Add Windows CI jobwindows-latest in GitHub Actions, running cargo test, cargo clippy, and beamtalk build on a test project.
  2. Add macOS CI jobmacos-latest to catch regressions (macOS works today via Unix compatibility, but without CI, regressions go undetected).
  3. Fix unguarded Unix code — Wrap find_beam_pid_by_node() with #[cfg(unix)] and add Windows fallback
  4. Portable path handling — Replace any hardcoded / root checks with std::path methods or Path::has_root()
  5. No bash dependency for compilation — The beamtalk build command must not require bash. The embedded compiler port (ADR 0022) already avoids shell scripts for compilation.

Tier 2 (near-term): Workspace and REPL work on Windows

The interactive development experience — beamtalk repl, workspace management — should work:

  1. TCP-first workspace management — Replace ps/signal//proc usage with TCP-based alternatives (see Process Management Strategy below):
    • TCP health probes for liveness checking
    • TCP shutdown messages for graceful termination
    • Minimal #[cfg]-guarded force-kill for hung workspaces
  2. Fix stop_workspace() on Windows — Currently returns an error; implement TCP shutdown + force-kill fallback
  3. Fix Erlang runtime Windows issues — Replace HOME with USERPROFILE, fix / root detection in compiler port

Tier 3 (deferred): Full parity including development tooling

Scripts, benchmarks, and development tooling:

  1. Justfile Windows shell — Add set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
  2. rebar3 pre-hooks — Replace bash -c compile.sh with a portable Erlang script or escript
  3. CI scripts — Add PowerShell equivalents where needed (or use cross-platform tools)
  4. Fuzz testing — Verify fuzz harness works on Windows (lower priority)

What we explicitly do NOT do

Process management strategy

The biggest portability challenge is process management for workspaces. Workspaces already expose TCP ports for REPL and MCP client connections, and the non-Unix fallback for is_node_running() already uses TCP probes (line 315-327 in workspace/mod.rs). The recommended approach extends this to TCP-first workspace management:

  1. Liveness checking — TCP health probe to workspace port (replaces ps + /proc + kill -0 polling)
  2. Graceful shutdown — TCP shutdown message with cookie authentication triggers init:stop() on the workspace's OTP application. This ensures OTP supervision trees shut down cleanly, running all terminate/2 callbacks. Must use the same cookie auth as WebSocket connections (ADR 0020) to prevent local privilege escalation.
  3. Workspace discovery — Port file in ~/.beamtalk/workspaces/ (already implemented). Port files should include a nonce verified on connection to detect stale entries.
  4. Force-kill (break glass only) — OS-specific code for workspaces that don't respond to TCP shutdown within a timeout. This is a last resort for truly hung BEAM processes (e.g., stuck in NIF, crash loop before TCP listener starts). On Windows, TerminateProcess requires handle permissions — use sysinfo crate if edge cases proliferate.

Why OTP-level shutdown matters: init:stop() gives the BEAM a chance to flush state, persist data, and run cleanup callbacks. This is critical for future persistence features (workspace state, actor snapshots, session history). An OS-level kill (kill -9, TerminateProcess) bypasses all of this and risks data loss. By making OTP shutdown the primary path and OS kill the rare fallback, we get:

// TCP-first: portable liveness and shutdown
fn is_workspace_running(port: u16) -> bool {
    TcpStream::connect_timeout(&addr, Duration::from_secs(1)).is_ok()
}

fn stop_workspace(port: u16, cookie: &str) -> Result<()> {
    // 1. Send authenticated shutdown message over TCP
    //    → workspace calls init:stop() → OTP terminate callbacks run
    // 2. Wait for process exit with timeout (TCP disconnect or port probe)
    // 3. Only if timeout expires: OS force-kill (break glass)
}

// Only force-kill needs #[cfg] guards — and it's rarely invoked:
#[cfg(unix)]
fn force_kill(pid: u32) -> Result<()> { /* kill -9 */ }
#[cfg(windows)]
fn force_kill(pid: u32) -> Result<()> { /* TerminateProcess via sysinfo */ }

Tradeoffs:

Prior Art

Pharo (Smalltalk)

Full cross-platform support (Windows, macOS, Linux, ARM). The OpenSmalltalk VM abstracts OS differences — the same Pharo image runs unchanged on any supported platform. Platform-specific code is isolated in VM plugins and FFI bindings; application-level Smalltalk code is entirely portable.

Relevant: Pharo's "portable image on cross-platform VM" model parallels Beamtalk's architecture — portable .beam bytecode on cross-platform BEAM VM. In both cases, the runtime VM handles portability; the issue is tooling around it (build scripts, process management).

Gleam

Full Windows support from early on. Rust compiler is inherently portable. Uses std::process::Command for erl/erlc invocation (works cross-platform if Erlang is in PATH). CI runs on Windows. Ships Windows binaries via GitHub releases.

Adopted: Same model — Rust compiler portable, platform differences in process management only.

Key difference: Gleam's compilation is stateless batch processing (compile files → output .beam). Beamtalk adds persistent workspaces, live hot-reload into running nodes, and process start-time tracking for stale-node detection. The process management layer is substantially more complex than Gleam's.

Elixir

Works on Windows via Erlang/OTP's Windows support. mix (Elixir's build tool) is written in Elixir itself, running on BEAM — inherently cross-platform. Some ecosystem tools (like phoenix_live_reload) use inotifywait which requires alternatives on Windows.

Adopted: Philosophy that the BEAM runtime handles most portability; the compiler/tooling layer just needs to invoke it correctly.

Rust (Cargo)

Exemplary cross-platform support. Uses #[cfg(target_os)] extensively. Has std::process::Command that handles path differences. CI matrix includes windows-latest, macos-latest, ubuntu-latest.

Adopted: #[cfg] attribute pattern, CI matrix strategy, TCP-first workspace management.

Erlang/OTP

Erlang itself has excellent Windows support — prebuilt Windows installers, werl (Windows Erlang shell), proper Windows service support. rebar3 works on Windows but some plugins assume Unix tools. The BEAM VM is fully portable.

Relevant: Beamtalk's runtime (Erlang) is already portable. The issues are in the Rust tooling layer and bash-dependent build scripts.

User Impact

Windows developer (primary beneficiary)

Can download a binary or cargo install beamtalk, run beamtalk new myapp, beamtalk build, and beamtalk repl — the full workflow works. No WSL, no bash, no Unix tools required beyond Erlang/OTP.

Linux/macOS developer (improved)

macOS: TCP-first workspace management eliminates /proc degradation — stale-node detection works reliably via TCP probes instead of process-level checks. Other macOS quirks (Gatekeeper quarantine on downloaded binaries, different signal handling in open_port) may need individual fixes but are not systematic. Linux: no change.

CI/CD operator

Windows and macOS CI jobs catch regressions early. Cross-platform matrix ensures releases work everywhere.

Contributor

New #[cfg] patterns to follow when adding process management code. TCP-first approach means most workspace management code is platform-agnostic. Only force-kill remains OS-specific. Must test on Windows CI (automatic via matrix).

Steelman Analysis

Option A: Tiered cross-platform support (this decision)

Option B: Linux-only, recommend WSL on Windows

Tension Points

  1. Reach vs maintenance — Windows support doubles the platform-specific test surface but reaches ~45% more developers. The tiered approach mitigates this by only adding Windows code where strictly necessary.
  2. WSL adequacy — For experienced developers, WSL is fine. For newcomers, "install WSL first" is a friction point that competitors (Gleam, Elixir) don't have.
  3. Process management complexity — The workspace/REPL code has the most Unix assumptions. TCP-first management eliminates most platform-specific code, but force-kill and process startup still need #[cfg] guards.

Alternatives Considered

Alternative: WSL-only on Windows

Declare WSL as a prerequisite for Windows users. No Windows-native code paths needed.

Rejected because: Creates a second-class developer experience. Gleam doesn't require WSL. Newcomers hitting "install WSL" as step 1 will try Gleam instead. The core compiler is already portable — refusing to ship it natively on Windows wastes that portability.

Alternative: Full Windows parity immediately

All features, all platforms, all at once. No tiers.

Rejected because: The workspace process management code needs significant work (signal replacements, Windows API integration). Blocking the portable parts (compiler, build) on the hard parts (workspace) delays value delivery. Ship what works now, iterate on the rest.

Alternative: Cross-platform process library (sysinfo crate)

Use the sysinfo crate for remaining OS-level operations (force-kill), replacing manual #[cfg] code.

Deferred, likely needed: sysinfo handles Windows TerminateProcess edge cases (handle permissions, access denied, wait-for-termination) that would otherwise require significant #[cfg] code. The "~10 lines" estimate for manual force-kill is optimistic — proper Windows error handling could reach 40-50 lines. Adopt sysinfo if force-kill complexity exceeds a single function.

Rejected: ProcessManager trait abstraction (superseded by TCP-first)

Originally proposed abstracting all process operations behind a ProcessManager trait with Unix and Windows implementations. TCP-first management eliminates the need for most trait methods (is_running, find_pid_by_name, terminate). Only force_terminate needs platform-specific code, which doesn't justify a trait.

Consequences

Positive

Negative

Neutral

Implementation

Phase 1: CI and compiler portability

Phase 2: TCP-first workspace management

Phase 3: Build script portability

Affected components: beamtalk-cli (process management, paths), runtime (Erlang build hooks, path handling), CI (workflow matrix), Justfile, documentation.

Affected test suites: workspace/mod.rs tests (18+ workspace management tests), paths.rs tests (PPID/session tests), beam_compiler.rs tests (23 compiler tests), beamtalk_workspace_meta_tests.erl (18 metadata tests), beamtalk_compiler_port_tests.erl (8 port tests). All must pass on Windows and macOS CI.

Estimated size: L (across all three phases)

Migration Path

Not applicable. This is an infrastructure enhancement — no existing user code, APIs, or language semantics change. All modifications are internal to the toolchain (CI configuration, build scripts, #[cfg] guards in workspace management).

Implementation Tracking

Epic: BT-609 Status: Done

PhaseIssueTitleSize
1BT-610Fix cross-platform code issues (Rust + Erlang)M
2BT-611Add TCP health and shutdown endpoints to workspaceL
2BT-612Replace process management with TCP probes and force-killL
3BT-613Add cross-platform CI and release workflowM
3BT-619Bundle beamtalk-lsp per-platform in VS Code extensionM

Dependency graph:

BT-610 (code fixes)
  ├── BT-611 (TCP endpoints) ──► BT-612 (TCP process mgmt)
  │                                      │
  └──────────────────────────────────────►├── BT-613 (CI + release)

References