Skip to content

ghosttead 0.5.0 darwin-arm64 links Homebrew harfbuzz and cannot start on a machine without it #21

Description

@jamesyong-42

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.1harfbuzz-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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions