Hive Hive
Sign in

Add granular Elixir target rules

GitHub issue · Closed

Metadata
Source
tuist/once #39
Updated
Jun 24, 2026
Domains
Once
Details

Summary

  • New fabrik-elixir crate lowers [[elixir.library]] and [[elixir.binary]] targets into per-target compile actions, each producing a .ebin directory of .beam files. Dep .ebins flow into downstream invocations through -pa so the BEAM code path resolves transitive modules at compile time.
  • [[elixir.binary]] additionally emits a launcher script that walks up to the workspace root via .fabrik/ at run time, so the cached launcher stays byte-identical across machines with different absolute paths.
  • Compile actions go through a fabrik elixir-compile wrapper that talks to a long-lived BEAM compile daemon when one is reachable and falls back to spawning elixirc directly otherwise. The wrapper’s argv is identical in both modes, so daemon presence is invisible to the cache.
  • Wires the new kinds through the frontend manifest ([[elixir.library]] / [[elixir.binary]] / [[elixir.test]] and rule = "elixir.*"), the CLI build dispatcher, an example project, and a shellspec suite that uses fake elixirc + fabrik shims so CI doesn’t need an Elixir toolchain.

Compile daemon

The daemon is opt-in. Without it, builds still work via the direct elixirc fallback. Run it when you want to amortize BEAM startup across many actions.

fabrik elixir-daemon start # listens on .fabrik/elixir-daemon.sock
fabrik elixir-daemon status # probe round-trip health

The Elixir program that implements the daemon is checked in at crates/fabrik-elixir/elixir/fabrik_compiler.exs and embedded into the fabrik binary via include_str!; start materializes it under .fabrik/daemon/ on first use. Protocol is line-delimited JSON over a unix domain socket; the Rust client lives in fabrik_elixir::daemon and is unix-gated.

Cache behavior

Per-target actions are content-addressed on sources, dep action digests, and the resolved tool path. A live demo against the example:

edit core (leaf) -> 0 hit, 2 miss
no change -> 2 hit, 0 miss
edit cli only (leaf+1) -> 1 hit, 1 miss # core stays cached

That last case is the granularity win mix can’t match. Both daemon and fallback modes produce identical .beam bytes (same Elixir version, same sources, same dep code path), so the cache key intentionally omits daemon presence.

Explicitly deferred

  • ExUnit test runner. use ExUnit.Case registers modules at compile time inside the VM that subsequently runs the suite, so compile and run can’t trivially be split into separate cached actions. fabrik test returns a clear “elixir test runner not yet wired” message for now; [[elixir.test]] compiles via elixir_library semantics.
  • Pinning Elixir / Erlang in mise.toml plus real benchmarks against mix compile for the daemon’s cold-build win. The wiring is in place; numbers come next.
  • Tracer-driven per-module dep manifest to push caching down from per-target to per-module granularity.

Test plan

  • cargo test --workspace (200 tests, 27 in fabrik-elixir including daemon protocol round-trip + materialization)
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo fmt --check
  • shellspec (96 specs, 3 new in examples_elixir_spec covering library + binary + cache hit, all going through the daemon-fallback path)
  • Live cache demo against the checked-in example shows hit/miss propagation per the table above

🤖 Generated with Claude Code

Comments

No GitHub comments yet.