Building Portable Crystal Binaries on macOS with GitHub Actions
kojix2

kojix2 @kojix2

About: Ruby & Crystal & Bioinformatics

Location:
Japan
Joined:
Jan 17, 2019

Building Portable Crystal Binaries on macOS with GitHub Actions

Publish Date: Jul 21
1 0

Overview

If you’ve ever tried to share a Crystal tool you built, you may have noticed that distributing it on macOS isn’t as straightforward as on Linux. On Linux, you can just use the official Docker image with musl to build fully static binaries.

But macOS is different. Its design doesn’t allow fully static linking, so—just like with Rust or Go—you end up with binaries that must dynamically link to system libraries. These are what we call portable binaries.

By default, Crystal binaries on macOS depend on Homebrew libraries like libgc, libevent, and libpcre. That’s not really portable. In this post, I’ll show you how to avoid those dependencies and build more portable binaries for macOS using GitHub Actions.

How Crystal Resolves Libraries

Crystal looks for libraries in this order:

  1. CRYSTAL_LIBRARY_PATH environment variable
  2. ldflags from the @[Link] annotation
  3. pkg-config
  • Tries the specified pkg_config name
  • Falls back to the library name
  • Only if both fail does it use a plain -l flag

Here’s the catch: even if you pass static libraries via --link-flags, pkg-config runs first. If it succeeds, it usually chooses shared libraries—and ignores the static ones you gave.

The Workarounds

Method 1: Use Symlinks

One way around pkg-config is to symlink the static libraries and link them directly:

brew install libgc pcre2
ln -s $(brew ls libgc | grep libgc.a) .
ln -s $(brew ls pcre2 | grep libpcre2-8.a) .
shards build --link-flags="-L $(pwd) $(pwd)/libgc.a $(pwd)/libpcre2-8.a" --release
Enter fullscreen mode Exit fullscreen mode

Method 2: Disable PKG_CONFIG_PATH

Another trick is to simply disable pkg-config so it can’t interfere:

brew install libgc pcre2
unset PKG_CONFIG_PATH
shards build --link-flags="$(brew ls libgc | grep libgc.a) $(brew ls pcre2 | grep libpcre2-8.a)" --release
Enter fullscreen mode Exit fullscreen mode

Combining both methods is the most reliable -- especially for libraries like libcrypto and libssl.

Things to Keep in Mind

  • The latest-macos runner gives you an Apple Silicon (Arm) binary
  • For Intel builds, use the macos-13 runner
  • On some systems, macOS security may require users to manually approve your binary

Alternative: Homebrew Tap

If you want the easiest experience for users, publishing a Homebrew tap is the way to go. That way, they can build your tool from source and let Homebrew handle dependencies.

Still, prebuilt binaries are handy. With the approaches above, you can distribute Crystal binaries on macOS much like you would with Rust.


That’s it for today. How about sharing the Crystal tool you built over the weekend?

Comments 0 total

    Add comment