fix(serve): replace enotify with fs so file watching works on modern macOS - #69
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes novaframework/nova#402.
It is not an Apple Silicon bug
rebar3 nova servehas been dead on every Mac running macOS 11 or later, Intel included. The M3 in the report is incidental.enotify_fsevents:start_port/1always passes-F(file-events):{args, ["-F", Path]}and enotify's bundled
mac_listenerguards that flag inc_src/mac/main.c:97: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'srebar.configis 1 byte. There is no port compiler, soc_src/is never built.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.0.1.0from 2019.Why
fsenotifyis a fork of synrc/fs, and upstream already fixed both problems:plus it dropped the committed binary and added
rebar3_pcport_specs, somac_listeneris compiled from source at build time and is therefore native arm64.fsis 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 costrebar3_novaits 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.fsneeds a named backend plus an explicitfs:subscribe/1, so each watched directory gets its own registered name.{ChangedFile, _Events}to{_Pid, {fs, file_event}, {ChangedFile, _Events}}(fs_server:notify/3->fs_event_bridge:handle_event/2).watch_dirs/1so it is testable, and a directory that fails to start now warns instead of failing silently.Linux is unaffected:
fsfindsinotifywaitviaos:find_executable/1, the same inotify-tools requirement as before.Verification, and its limits
compile,xref(proves thefsAPI calls resolve),fmt --check, andct28/28 all pass, plus a new test forwatch_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:
rebar3 nova servestarts without theport_exitcrashes_build/default/plugins/fs/priv/mac_listenerexists - it is compiled, not shipped, so ifrebar3_pcdoes not run, fs logsbackend port not foundand silently watches nothing.erlor.dtltriggers a reloadI also did not add a macOS CI job, which is what would actually catch this class of regression in future.