Skip to content

One-word bugfix: re-enable custom ERTS compilation by adding comptime - #237

Open
JamesLavin wants to merge 1 commit into
burrito-elixir:mainfrom
JamesLavin:reenable_custom_erts
Open

One-word bugfix: re-enable custom ERTS compilation by adding comptime#237
JamesLavin wants to merge 1 commit into
burrito-elixir:mainfrom
JamesLavin:reenable_custom_erts

Conversation

@JamesLavin

Copy link
Copy Markdown

I have tested this one-word fix on my project and it fixed the regression that blocked me from upgrading from burrito 1.5.0 to 1.6.0.

The following explanation was generated by Claude, and I am not a Zig developer.

--James

Burrito 1.6.0 blocks builds that use a non‑precompiled (custom_erts) ERTS on Linux

TL;DR — In Burrito 1.6.0, the Linux wrapper unconditionally compiles
@embedFile("musl-runtime.so"), but the musl-runtime.so file is only produced by the FetchMusl
step when the target uses a {:precompiled, _} ERTS. Any Linux target that supplies its own ERTS
(custom_erts:{:local, _} / {:local_unpacked, _}) therefore fails to compile the wrapper:

src/wrapper.zig:241:47: error: unable to open 'musl-runtime.so': FileNotFound
        const MUSL_RUNTIME_BYTES = @embedFile("musl-runtime.so");
                                              ^~~~~~~~~~~~~~~~~
error: 1 compilation errors
--> Burrito failed to wrap up your app! Check the logs for more information.
** (RuntimeError) Wrapper build failed
    (burrito 1.6.0) lib/steps/build/pack_and_build.ex:59: Burrito.Steps.Build.PackAndBuild.execute/1

This is a regression from 1.5.0, which builds the same configuration cleanly. The cause is a
lost comptime qualifier on the guard around the @embedFile. This document is intended as a report
for the Burrito maintainers.


Environment

  • Burrito: 1.6.0 (fails) vs 1.5.0 (works)
  • Zig: 0.16.0 (required by 1.6.0) vs 0.15.2 (1.5.0)
  • Target: os: :linux, cpu: :x86_64 (also :aarch64)
  • ERTS: custom_erts: pointing at a local glibc ERTS — not a Burrito‑downloaded
    precompiled (musl) ERTS. We do this deliberately because our NIFs (vix, exqlite,
    bcrypt_elixir) are compiled against glibc on the build host and bundled with their glibc
    dependencies; a musl ERTS would be ABI‑incompatible with them.

Representative target definition:

deb_x86: [
  os: :linux,
  cpu: :x86_64,
  # Local glibc ERTS from the build machine (NOT Burrito's precompiled musl ERTS)
  custom_erts: System.get_env("ERL_ROOT") || :code.root_dir() |> to_string(),
  skip_nifs: true
]

Root cause

Two pieces interact.

1. FetchMusl only runs for {:precompiled, _} ERTS

lib/steps/fetch/fetch_musl.ex matches only the precompiled‑ERTS case and no‑ops otherwise:

def execute(
      %Context{target: %Target{os: :linux, cpu: arch, erts_source: {:precompiled, _}}} = context
    ) do
  # ... downloads musl runtime, writes it, and sets __BURRITO_MUSL_RUNTIME_PATH ...
  out_path = Path.join([context.self_dir, "src", "musl-runtime.so"])
  File.write!(out_path, so_bytes)
  # ... extra_build_env: [{"__BURRITO_MUSL_RUNTIME_PATH", "/tmp/libc-musl-#{hash}.so"}]
end

def execute(context), do: context   # ← custom_erts / {:local,_} / {:local_unpacked,_} land HERE

So for a custom_erts (local) ERTS:

  • src/musl-runtime.so is never written, and
  • __BURRITO_MUSL_RUNTIME_PATH is never set, so build.zig falls back to the empty default:
    const musl_runtime_path = b.graph.environ_map.get("__BURRITO_MUSL_RUNTIME_PATH") orelse "";
    // exe_options.addOption([]const u8, "MUSL_RUNTIME_PATH", musl_runtime_path);
    i.e. build_options.MUSL_RUNTIME_PATH == "" (a comptime‑known constant).

2. The wrapper embeds the file behind a runtime guard (regression)

@embedFile is a compile‑time builtin: Zig evaluates it whenever the enclosing code is semantically
analyzed. A runtime if does not prevent that analysis — only a comptime‑false condition
elides the block.

1.5.0 — src/wrapper.zig:203 (works): the guard is comptime, so when
MUSL_RUNTIME_PATH == "" the whole block — including the @embedFile — is eliminated at compile
time:

fn maybe_install_musl_runtime(arena: std.mem.Allocator) !void {
    if (comptime IS_LINUX and !std.mem.eql(u8, build_options.MUSL_RUNTIME_PATH, "")) {
        // ...
        const MUSL_RUNTIME_BYTES = @embedFile("musl-runtime.so");   // never compiled when path == ""
        // ...
    }
}

1.6.0 — same function, guard lost its comptime (fails): the guard is now a runtime condition,
so Zig analyzes the body regardless and evaluates the @embedFile (the wrapper.zig:241 in the
error above), which fails because the file is absent:

fn maybe_install_musl_runtime(io: Io) !void {
    if (!std.mem.eql(u8, build_options.MUSL_RUNTIME_PATH, "")) {   // ← runtime `if`, no `comptime`
        // ...
        const MUSL_RUNTIME_BYTES = @embedFile("musl-runtime.so");   // ALWAYS compiled → FileNotFound
        // ...
    }
}

The call site is compiled for every Linux build:

if (comptime IS_LINUX) try maybe_install_musl_runtime(io);

(Line numbers cited for 1.6.0 are approximate — take the failing @embedFile line from your own
build output; the essential point is that the enclosing if is a runtime condition, whereas in
1.5.0 it was comptime.)

Because IS_LINUX is true, maybe_install_musl_runtime is analyzed, and its now‑runtime if no
longer shields the @embedFile. Result: any Linux target whose ERTS is not {:precompiled, _}
fails to compile the wrapper.


Minimal reproduction

  1. A project with any Linux Burrito target that supplies its own ERTS, e.g.
    custom_erts: :code.root_dir() |> to_string() (so erts_source resolves to {:local, _} /
    {:local_unpacked, _}, not {:precompiled, _}).
  2. MIX_ENV=prod mix release with Burrito 1.6.0 + Zig 0.16.0.

Expected: a wrapped executable. Actual: error: unable to open 'musl-runtime.so': FileNotFound.

The same project on Burrito 1.5.0 + Zig 0.15.2 builds successfully.


Suggested fix

Restore the compile‑time elision so the musl-runtime.so embed is only compiled when a musl runtime
path was actually provided. build_options.MUSL_RUNTIME_PATH is a comptime‑known constant, so a
one‑word change suffices:

// src/wrapper.zig
fn maybe_install_musl_runtime(io: Io) !void {
    if (comptime !std.mem.eql(u8, build_options.MUSL_RUNTIME_PATH, "")) {   // ← add `comptime`
        // ... @embedFile("musl-runtime.so") is now elided when no musl runtime was fetched ...
    }
}

Alternatives that would also work:

  • Gate the embed on the target ABI (only embed for musl targets), or
  • Have FetchMusl provide musl-runtime.so for all Linux targets (but embedding a musl runtime into
    a glibc/custom‑ERTS wrapper is unnecessary), or
  • Guard the @embedFile itself in an if (comptime ...) block so it is never analyzed unless needed.

The comptime‑guard approach matches 1.5.0's behavior and keeps custom_erts Linux builds working.


Workaround (what we did)

Pin to the last Zig‑0.15 release, which still has the comptime guard:

{:burrito, "~> 1.5.0"}   # Zig 0.15.2

We would prefer to move to 1.6.0 / Zig 0.16.0 once the wrapper only embeds musl-runtime.so when a
musl runtime is actually fetched.


My project ships desktop builds via Burrito for macOS/Windows/Linux (deb + rpm, x86_64 + aarch64),
each built on native‑arch runners with host‑compiled glibc NIFs and skip_nifs: true.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant