Skip to content

fix(serve): replace enotify with fs so file watching works on modern macOS - #69

Merged
Taure merged 1 commit into
masterfrom
fix/macos-file-watching
Aug 8, 2026
Merged

fix(serve): replace enotify with fs so file watching works on modern macOS#69
Taure merged 1 commit into
masterfrom
fix/macos-file-watching

Conversation

@Taure

@Taure Taure commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixes novaframework/nova#402.

It is not an Apple Silicon bug

rebar3 nova serve has been dead on every Mac running macOS 11 or later, Intel included. The M3 in the report is incidental.

enotify_fsevents:start_port/1 always passes -F (file-events):

{args, ["-F", Path]}

and enotify's bundled mac_listener guards that flag in c_src/mac/main.c:97:

if ((osMajorVersion == 10) & (osMinorVersion >= 7)) {
    config.flags |= kFSEventStreamCreateFlagFileEvents;
} else {
    fprintf(stderr, "MacOSX 10.7 or later required for --file-events\n");
    exit(EXIT_FAILURE);
}

The check demands osMajorVersion == 10. Since Big Sur (2020) macOS reports 11, 12, 13, so the equality fails, the port exits 1, and all watcher gen_servers die with {port_exit,1} - exactly the reported crash.

Why it cannot be fixed in enotify

  • enotify's rebar.config is 1 byte. There is no port compiler, so c_src/ is never built.
  • What ships is priv/mac_listener, a binary committed in 2021. It is thin x86_64, no arm64 slice, so on Apple Silicon it also depends on Rosetta.
  • Upstream has been dormant since 2021 and Hex has only 0.1.0 from 2019.

Why fs

enotify is a fork of synrc/fs, and upstream already fixed both problems:

if ((osMajorVersion < 10) || ((osMajorVersion == 10) && (osMinorVersion < 7))) {

plus it dropped the committed binary and added rebar3_pc port_specs, so mac_listener is compiled from source at build time and is therefore native arm64. fs is maintained (11.4.1, April 2025), MIT, and on Hex - which matters, because a Hex package cannot depend on a git fork, so patching enotify in a fork would have cost rebar3_nova its ability to publish.

What changed

Only the message envelope. Every fs backend builds the same {Path, Flags} payload, and the fsevents line parser is byte-for-byte the same logic enotify used - so the swap is behaviour-preserving:

  • enotify:start_link(Dir) auto-subscribed the caller. fs needs a named backend plus an explicit fs:subscribe/1, so each watched directory gets its own registered name.
  • The receive clause goes from {ChangedFile, _Events} to {_Pid, {fs, file_event}, {ChangedFile, _Events}} (fs_server:notify/3 -> fs_event_bridge:handle_event/2).
  • Directory selection moved into watch_dirs/1 so it is testable, and a directory that fails to start now warns instead of failing silently.

Linux is unaffected: fs finds inotifywait via os:find_executable/1, the same inotify-tools requirement as before.

Verification, and its limits

compile, xref (proves the fs API calls resolve), fmt --check, and ct 28/28 all pass, plus a new test for watch_dirs/1.

I could not test this on macOS, and CI has no macOS runner, so the actual fix is unverified on the platform it targets. Everything above about the fs internals is read from source, not observed at runtime. Worth confirming on the reporter's M3:

  1. rebar3 nova serve starts without the port_exit crashes
  2. _build/default/plugins/fs/priv/mac_listener exists - it is compiled, not shipped, so if rebar3_pc does not run, fs logs backend port not found and silently watches nothing
  3. editing a .erl or .dtl triggers a reload

I also did not add a macOS CI job, which is what would actually catch this class of regression in future.

…macOS

`rebar3 nova serve` has been dead on every Mac running macOS 11 or later,
Intel included - the Apple Silicon framing in the report is incidental.

enotify always passes `-F` to its bundled `mac_listener`, and that binary
guards the flag with:

    if ((osMajorVersion == 10) & (osMinorVersion >= 7))

Since Big Sur the major version is 11+, so the equality fails, the port
prints "MacOSX 10.7 or later required for --file-events" and exits 1,
taking every watcher gen_server down with {port_exit,1}.

enotify cannot be fixed from here: its rebar.config is empty, so c_src is
never compiled and the shipped priv/mac_listener is a binary committed in
2021 - x86_64 only, needing Rosetta on Apple Silicon. Upstream is dormant
since 2021 and Hex has only 0.1.0 from 2019.

enotify is a fork of synrc/fs, which already fixed both problems: the
version guard is inverted correctly, the prebuilt binary is gone, and
mac_listener is compiled from source at build time via rebar3_pc - so it
is native arm64. fs is maintained (11.4.1, April 2025), MIT, and on Hex,
which matters because a Hex package cannot depend on a git fork.

The event payload is unchanged - every fs backend builds {Path, Flags},
and the fsevents line parser is identical to enotify's - so only the
envelope differs: fs wraps it as {Pid, {fs, file_event}, Event} and needs
an explicit subscribe per watched directory.

Linux behaviour is unchanged: fs locates inotifywait with
os:find_executable/1, the same inotify-tools requirement as before.

Closes novaframework/nova#402
@Taure
Taure merged commit b7d0315 into master Aug 8, 2026
29 checks passed
@Taure
Taure deleted the fix/macos-file-watching branch August 8, 2026 11:00
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.

enotify not working on Macbook (Apple M3 CPU)

1 participant