Skip to content

feat: Zig 0.16.0 compatibility - #225

Merged
doawoo merged 7 commits into
burrito-elixir:mainfrom
gilbertwong96:zig-0.16.0
Jul 24, 2026
Merged

feat: Zig 0.16.0 compatibility#225
doawoo merged 7 commits into
burrito-elixir:mainfrom
gilbertwong96:zig-0.16.0

Conversation

@gilbertwong96

@gilbertwong96 gilbertwong96 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Updates the Zig wrapper to compile and run on Zig 0.16.0.

Zig 0.16.0 API changes:

  • std.c.statIo.Dir.cwd().statFile (cross-compile fix)
  • Permissions.unixNewPermissions.fromMode
  • Guard Permissions.toMode() on Windows (enum, no POSIX mode bits)
  • Drop unused arena parameter from maybe_install_musl_runtime

Process spawning:

  • Replace std.process.replace with spawn + child.wait (replace hangs on macOS arm64 in 0.16.0)
  • Unify Windows/Unix code paths

Bug fixes:

  • Split "-elixir ansi_enabled true" and "-s elixir start_cli" into separate argv entries — passing them as single strings caused the BEAM to hang in __select on macOS
  • EPIPE hang when piping output to head/less. When stdout is piped to a command that exits early, the BEAM's standard_io group leader crashes on EPIPE and the VM hangs indefinitely trying to flush IO during shutdown. The wrapper now pipes child stdout through itself via a copy thread; when the downstream pipe breaks (EPIPE on write), the copy thread kills the child process, allowing the wrapper to exit cleanly instead of blocking on child.wait() forever.

CI:

  • Add Zig 0.16.0 + macOS arm64 (macos-14) to cross-build matrix

Builds on #224 by @jsmestad. Closes #221.

Brezn and others added 3 commits May 29, 2026 12:12
The Zig standard library had a major restructuring in 0.16.0, primarily
around the new Io abstraction. This updates all source files to use the
new APIs:

- std.process.getEnvVarOwned → b.graph.environ_map.get (build.zig) and
  std.process.Environ (runtime)
- std.fs.cwd() → std.Io.Dir.cwd()
- File methods (close, stat, writer, reader) now take io: Io parameter
- std.fs.getAppDataDir removed; replaced with manual XDG/platform logic
- std.process.execve → std.process.replace
- Build module methods (addIncludePath, addCSourceFile, linkSystemLibrary)
  moved from Compile step to root_module
- main() accepts std.process.Init for Io and Environ access

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jsmestad

Copy link
Copy Markdown

Any reason you dropped support for glibc and went musl only?

@gilbertwong96

Copy link
Copy Markdown
Contributor Author

Any reason you dropped support for glibc and went musl only?

Yeah, this branch still contains non-sense change for debug and test purposes, I will sort out branch to remove non sense changes and then force push them.

@gilbertwong96

Copy link
Copy Markdown
Contributor Author

Force pushed

@gilbertwong96 gilbertwong96 changed the title Update Zig compatibility from 0.15.2 to 0.16.0 — additional fixes + CI matrix feat: Zig 0.16.0 compatibility Jun 27, 2026
Cross-compilation fixes (Zig 0.16.0 API changes):
- Replace std.c.stat with Io.Dir.cwd().statFile
- Rename Permissions.unixNew → Permissions.fromMode
- Remove unused arena parameter from maybe_install_musl_runtime
- Comptime-guard the musl runtime call site on macOS
- Guard Permissions.toMode() on Windows (enum, no POSIX mode bits)

Process spawning (cross-platform):
- Replace std.process.replace with std.process.spawn + child.wait
  (std.process.replace does not work reliably on macOS arm64)
- Unify Windows/Unix code paths via the same spawn+wait pattern

CI:
- Add Zig 0.16.0 to the cross-build matrix
- Add macOS arm64 (macos-14) runner
Passing '-elixir ansi_enabled true' and '-s elixir start_cli' as single
argv entries (with embedded spaces) caused the BEAM to hang in __select
when spawned via std.process.spawn on macOS. erlexec passes them through
to beam.smp as-is, and beam.smp doesn't split space-delimited args.

Splitting them into separate argv entries fixes the hang.
@doawoo

doawoo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

I'll have to update the runner jobs for this, but it's looking good to me. Let me run a few tests offline and then I'll merge and update CI to be correct lol

On macOS, `brew install elixir xz` installed Elixir 1.20.2 (OTP 29)
which shadowed setup-beam's Elixir 1.18.3 (OTP 27), causing a Hex ABI
mismatch (`beam_load.c` errors) at `mix deps.get`. Only `xz` is needed
via brew since setup-beam already provides Elixir.

Drop the 0.15.2 Zig matrix entries since lib/burrito.ex hardcodes
`@zig_version_expected` to 0.16.0 with a strict equality check, making
0.15.2 builds fail the version guard.
@joshrotenberg

Copy link
Copy Markdown

Tested this branch on macOS 26.5 (Tahoe) / Apple Silicon with zig 0.16.0. Packaging works: it builds a self-contained binary that boots the BEAM with no Elixir on PATH. Thanks for tackling this.

One regression: elixir:start_cli/0 now runs and consumes the application's CLI arguments.

Minimal repro, a burrito app that just prints what it sees:

# lib/argv_probe/application.ex
def start(_type, _args) do
  IO.puts(:stderr, "running_standalone? = #{Burrito.Util.running_standalone?()}")
  IO.puts(:stderr, "get_arguments()     = #{inspect(Burrito.Util.Args.get_arguments())}")
  Supervisor.start_link([], strategy: :one_for_one, name: ArgvProbe.Sup)
end

Built with this branch and run:

$ ./argv_probe hello world
running_standalone? = true
get_arguments()     = ["hello", "world"]     # the app receives args correctly
No file named hello                          # ...but start_cli also runs, treats "hello" as a script
# exit 1

So argument delivery is fine (get_arguments/0 returns them, __BURRITO is set). The problem is that elixir:start_cli/0 executes and interprets the same -extra plain arguments (which Burrito.Util.Args.get_arguments/0 reads via :init.get_plain_arguments/0) as Elixir CLI arguments, then halts the VM. --version prints the Erlang/Elixir banner; any other first argument becomes "No file named ".

This traces to the -s elixir start_cli change in src/erlang_launcher.zig. In 1.5.0 it was a single argv token, so start_cli never actually ran and the app's own arg-reading was the only consumer. Split into -s, elixir, start_cli, it runs for real. The split does fix the macOS boot hang, so this looks like a side effect rather than a reason to drop it. Happy to test any patch on this macOS 26 host.

@doawoo

doawoo commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

@joshrotenberg Thanks for testing this out, and reporting the regression there... I'll try and look into that

@gilbertwong96

Copy link
Copy Markdown
Contributor Author

Hi, @doawoo, push one more commit to fix the bug EPIPE hang when piping output to head/less.

When stdout is piped to a command that exits early (e.g. `app cmd |
head -5`), the BEAM's standard_io group leader crashes on EPIPE and
the VM hangs indefinitely trying to flush IO during shutdown.

The wrapper now pipes child stdout through itself via a copy thread.
When the downstream pipe breaks (EPIPE on write), the copy thread
kills the child process, allowing the wrapper to exit cleanly instead
of blocking on child.wait() forever.
@doawoo

doawoo commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@gilbertwong96 Thank you for this massively helpful contribution! All tests look good!

@doawoo
doawoo merged commit fe22a9d into burrito-elixir:main Jul 24, 2026
8 checks passed
@gilbertwong96
gilbertwong96 deleted the zig-0.16.0 branch July 24, 2026 08:40
@gilbertwong96

Copy link
Copy Markdown
Contributor Author

Will appreciate it if you publish a new release on hex.pm :)

@doawoo

doawoo commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Released!

bougyman added a commit to rubyists/linear-cli that referenced this pull request Aug 20, 2026
The bug is Burrito 1.6’s new stdout relay from PR #225
(burrito-elixir/burrito#225). It made the
wrapper a second owner of stdout. Issue #234
(burrito-elixir/burrito#234) and PR #235
(burrito-elixir/burrito#235) use the practical
fix: when stdout is a TTY, let BEAM inherit it directly;
  retain the relay only for pipes.

I ported that fix:

- app/mix.exs:1 installs the release hook.
- app/release/burrito_patches.exs:1 applies the upstream patch and fails
closed if Burrito changes.
- app/test/linear_cli/release/burrito_patches_test.exs:1 verifies it.

- Linux Burrito binary compiled successfully.
- A throttled pseudo-terminal emitted all 5,000 rows, including the
final row, then exited 0.
- Full suite: 312 tests passed.
- Formatter check passed.

Need a follow-up on this when burrito gets the patch into mainstream
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.

Support Zig 0.16.x (macOS 26 compatibility blocker)

4 participants