Summary
The published @vibecook/ghosttead-darwin-arm64@0.5.0 binary dynamically links /opt/homebrew/opt/harfbuzz/lib/libharfbuzz.0.dylib. On any macOS machine without Homebrew harfbuzz at that prefix it aborts in dyld before doing anything — including --version.
This defeats the stated goal of 0.5.0: "Desktop consumers no longer need this repository checked out beside them, or a Rust toolchain." They do still need a Homebrew package.
Reproduction
On a macOS arm64 machine with no harfbuzz formula installed:
$ npm i @vibecook/ghosttead@0.5.0
$ node -e "import('@vibecook/ghosttead').then(m=>console.log(m.ghostteadPath()))"
.../node_modules/@vibecook/ghosttead-darwin-arm64/bin/ghosttead
$ .../bin/ghosttead --version
dyld[40893]: Library not loaded: /opt/homebrew/opt/harfbuzz/lib/libharfbuzz.0.dylib
Referenced from: .../@vibecook/ghosttead-darwin-arm64/bin/ghosttead
Reason: tried: '/opt/homebrew/opt/harfbuzz/lib/libharfbuzz.0.dylib' (no such file), ...
[1] 40893 abort
ghostteadPath() itself is fine — resolution works, the binary is staged, the executable bit is set. Only the linkage is wrong.
The released binary is the only one affected
otool -L on the published binary — harfbuzz is its sole non-system dependency:
/System/Library/Frameworks/Security.framework/.../Security
/opt/homebrew/opt/harfbuzz/lib/libharfbuzz.0.dylib ← only non-system entry
/System/Library/Frameworks/CoreText.framework/.../CoreText
/System/Library/Frameworks/CoreGraphics.framework/.../CoreGraphics
/System/Library/Frameworks/CoreFoundation.framework/.../CoreFoundation
/usr/lib/libiconv.2.dylib
/usr/lib/libSystem.B.dylib
A local cargo build of the same daemon on the same machine links zero non-system libraries and runs fine. So harfbuzz is static in a local build and dynamic in the release build.
Cause
ghosttea-text depends on harfbuzz_rs 2.0.1 → harfbuzz-sys, whose build.rs is:
println!("cargo:rerun-if-env-changed=HARFBUZZ_SYS_NO_PKG_CONFIG");
if target.contains("wasm32") || env::var_os("HARFBUZZ_SYS_NO_PKG_CONFIG").is_none() {
if pkg_config::probe_library("harfbuzz").is_ok() {
return; // links the system harfbuzz dynamically
}
}
// otherwise builds the vendored source statically
With the variable unset — the default — it probes pkg-config. GitHub's macOS runner images ship harfbuzz, so the probe succeeds and it links dynamically against a path that exists only on the builder. A developer machine without the formula has nothing to probe, falls through, and statically links the vendored copy. Same source, same command, two different binaries, decided by whether the build host happens to have a Homebrew package.
Suggested fix
Set HARFBUZZ_SYS_NO_PKG_CONFIG=1 for the macOS release build so the vendored source is always linked statically, independent of the host.
Why the post-publish smoke workflow did not catch it
It installs the published version on a macOS runner and runs the daemon — but that is the same image that provides the dylib, so the missing dependency is always satisfied there. The check cannot fail on the machine that caused the problem. Asserting on the linkage itself would be a tighter gate, e.g.:
otool -L "$(node -e "import('@vibecook/ghosttead').then(m=>console.log(m.ghostteadPath()))")" \
| tail -n +2 | grep -vE '^\s+(/System/|/usr/lib/)' && exit 1 || true
That fails on any non-system dependency regardless of what the runner has installed.
Impact
Found while migrating vibecook-dev/chopsticks off its sibling-checkout paths and onto these packages — the consumer the 0.5.0 commit message describes. The migration is otherwise complete and green; it is held only on this.
@vibecook/ghosttea-native-tabs@0.5.0 has no equivalent problem: universal arm64+x86_64, system libraries only, loads and exports tabOrder.
Summary
The published
@vibecook/ghosttead-darwin-arm64@0.5.0binary dynamically links/opt/homebrew/opt/harfbuzz/lib/libharfbuzz.0.dylib. On any macOS machine without Homebrew harfbuzz at that prefix it aborts indyldbefore doing anything — including--version.This defeats the stated goal of 0.5.0: "Desktop consumers no longer need this repository checked out beside them, or a Rust toolchain." They do still need a Homebrew package.
Reproduction
On a macOS arm64 machine with no
harfbuzzformula installed:ghostteadPath()itself is fine — resolution works, the binary is staged, the executable bit is set. Only the linkage is wrong.The released binary is the only one affected
otool -Lon the published binary — harfbuzz is its sole non-system dependency:A local
cargo buildof the same daemon on the same machine links zero non-system libraries and runs fine. So harfbuzz is static in a local build and dynamic in the release build.Cause
ghosttea-textdepends onharfbuzz_rs 2.0.1→harfbuzz-sys, whosebuild.rsis:With the variable unset — the default — it probes pkg-config. GitHub's macOS runner images ship harfbuzz, so the probe succeeds and it links dynamically against a path that exists only on the builder. A developer machine without the formula has nothing to probe, falls through, and statically links the vendored copy. Same source, same command, two different binaries, decided by whether the build host happens to have a Homebrew package.
Suggested fix
Set
HARFBUZZ_SYS_NO_PKG_CONFIG=1for the macOS release build so the vendored source is always linked statically, independent of the host.Why the post-publish smoke workflow did not catch it
It installs the published version on a macOS runner and runs the daemon — but that is the same image that provides the dylib, so the missing dependency is always satisfied there. The check cannot fail on the machine that caused the problem. Asserting on the linkage itself would be a tighter gate, e.g.:
That fails on any non-system dependency regardless of what the runner has installed.
Impact
Found while migrating
vibecook-dev/chopsticksoff its sibling-checkout paths and onto these packages — the consumer the 0.5.0 commit message describes. The migration is otherwise complete and green; it is held only on this.@vibecook/ghosttea-native-tabs@0.5.0has no equivalent problem: universal arm64+x86_64, system libraries only, loads and exportstabOrder.