Skip to content

Add native macOS CI build + from-source build guide#1553

Open
ofcRS wants to merge 1 commit into
tsl0922:mainfrom
ofcRS:macos-ci-build
Open

Add native macOS CI build + from-source build guide#1553
ofcRS wants to merge 1 commit into
tsl0922:mainfrom
ofcRS:macos-ci-build

Conversation

@ofcRS

@ofcRS ofcRS commented Jun 20, 2026

Copy link
Copy Markdown

What

Adds native macOS support to the build/release pipeline:

  • A new scripts/macos-build.sh that builds a self-contained ttyd binary natively on macOS, mirroring scripts/cross-build.sh (same pinned dependency versions, same libwebsockets feature flags).
  • A new macos job in .github/workflows/backend.yml that runs that script on a two-arch matrix (macos-14 / Apple Silicon, macos-13 / Intel) and uploads ttyd.aarch64-apple-darwin and ttyd.x86_64-apple-darwin artifacts.
  • A build guide at docs/macos-build.md, linked from README.md.

No change to .github/workflows/release.yml is required — the new artifacts use the existing ttyd.<target> naming convention, so the existing publish step picks them up unchanged. (See "Releases" below.)

Why

There is currently no macOS runner in CI and no released macOS binaries (ref #1011). This adds an opt-in, fully native path so macOS users can either build from source with one script or grab a per-arch binary from a release, while keeping you in control (releases are still created as drafts for manual review).

This follows the precedent already set by the native msvc job — a per-platform native build alongside the cross job — rather than forcing macOS through the Linux-only cross-build.sh (which uses musl toolchains, -static, GNU sed -i, and linux-* OpenSSL targets that don't apply on macOS).

scripts/macos-build.sh

Native per-arch build (no cross toolchain). Pins the same dependency versions as cross-build.sh:

dep version
json-c 0.18
OpenSSL 3.6.1
libuv 1.52.1
libwebsockets 4.5.7

All four of these are linked statically. Unlike cross-build.sh (which builds zlib for musl), the macOS build uses the system libz, which stays dynamically linked along with libSystem and libutil — normal and unavoidable on macOS (no static libSystem).

How it mirrors cross-build.sh

Same recipe, only the platform-specific substitutions macOS forces:

Concern Linux (cross-build.sh) macOS (macos-build.sh)
Toolchain musl cross ${TARGET}-gcc native clang, no cross
Per-arch select CHOST / CMake cross file CMAKE_OSX_ARCHITECTURES + OpenSSL darwin64-*-cc
OpenSSL target linux-* via map_openssl_target darwin64-arm64-cc / darwin64-x86_64-cc
Final link -static -no-pie -Wl,-s -Wl,--gc-sections -Wl,-dead_strip -framework CoreFoundation -framework CoreServices -framework IOKit (no -static)
Strip -Wl,-s at link strip -S post-link
lws config patch sed -i 's/ websockets_shared//g' (GNU) sed -i '' 's/ websockets_shared//g' (BSD sed needs the empty backup arg)
Parallelism nproc sysctl -n hw.ncpu

The libwebsockets flag set is used as-is (LWS_WITH_SSL=ON, LWS_WITH_LIBUV=ON, LWS_WITH_SHARED=OFF, LWS_STATIC_PIC=ON, the full feature-OFF list, etc.). The Linux-only OpenSSL -fPIC/-latomic CFLAGS are dropped (-latomic doesn't exist on macOS; PIC is the default).

CI: new macos job in backend.yml

Mirrors the cross job's shape: strategy.fail-fast: false, the same actions/checkout@v6 and actions/upload-artifact@v7 pins, and the same ttyd.<target> artifact-naming convention. cmake comes from the runner's preinstalled Homebrew and is used only as a build-time tool — never linked into the binary (pkg-config isn't needed; ttyd discovers its deps via CMake's find_package/find_library). Each matrix leg has an explicit name so the checks render as macos (arm64) / macos (x86_64).

Releases (no release.yml change)

release.yml's publish job already downloads every artifact, derives each released asset's filename from the artifact name (for file in ttyd.*/*; ... awk -F/ '{print $1}'), runs sha256sum ttyd.*, and attaches everything via ncipollo/release-action@v1 (draft: true). Because the new job uploads artifacts named ttyd.aarch64-apple-darwin / ttyd.x86_64-apple-darwin (each containing a single ttyd binary, exactly like the cross job), they match the ttyd.*/* glob, get renamed to those exact asset names, are included in SHA256SUMS, and land on the draft release for review — with no release.yml edit. I have not run the full release flow end-to-end; it can be exercised by pushing a tag on a fork.

Verified local result

A from-source ttyd 1.7.7 build was produced on macOS arm64 with this recipe. otool -L showed only:

/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices
/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit
/usr/lib/libz.1.dylib
/usr/lib/libutil.dylib
/usr/lib/libSystem.B.dylib

OpenSSL, libwebsockets, libuv and json-c do not appear — they are statically linked. libz/libSystem/libutil resolving dynamically is expected macOS behavior.

Caveats I want to be upfront about: the x86_64 (macos-13) leg has not been run end-to-end locally — I'm relying on this PR's CI to confirm it. And macos-13 is on GitHub's runner-retirement path; happy to drop the Intel leg or switch runners if you prefer. The arm64 (macos-14) leg is the must-have.

Open question: signing & notarization

These macOS binaries are not codesigned or notarized, and this PR does not attempt to make them so. A binary downloaded from GitHub Releases without a Developer ID signature + notarization ticket is quarantined by macOS Gatekeeper; users will see "ttyd cannot be opened because the developer cannot be verified" / "is damaged and can't be opened" and must clear the quarantine attribute:

xattr -d com.apple.quarantine ./ttyd.aarch64-apple-darwin

(or right-click → Open once in Finder). Users who build from source locally are unaffected.

Codesigning + notarization would remove this friction, but it requires a paid Apple Developer ID and signing credentials stored as CI secrets — something only you can provide; a contributor cannot. I've deliberately left this as a decision for you rather than implying the artifacts are signed. The docs are explicit about it. If you'd like, I can follow up with a signing/notarization step that reads from secrets you control.

Testing / how to verify

Local:

env BUILD_TARGET=arm64 ./scripts/macos-build.sh     # or BUILD_TARGET=x86_64, on a matching Mac
otool -L build/ttyd                                  # only system libs should appear
./build/ttyd --version

CI: this PR touches scripts/* and .github/workflows/backend.yml, so backend runs on the PR. Confirm both macos (arm64) and macos (x86_64) legs are green and upload their ttyd.*-apple-darwin artifact. fail-fast: false keeps a flaky macOS leg from taking down the Linux/Windows builds.


A standalone, Homebrew-free version of the build script (for users who just want a binary without cloning the repo) is also available as a gist: https://gist.github.com/ofcRS/4efc3b945765e7b4b1ff432c439dd460

🤖 Generated with Claude Code

Adds scripts/macos-build.sh (native per-arch static build mirroring
scripts/cross-build.sh: same pinned dep versions and libwebsockets flags,
adapted for macOS), a macos matrix job in backend.yml (macos-14 arm64 +
macos-13 x86_64), and docs/macos-build.md linked from the README.

No release.yml change needed -- artifacts use the existing ttyd.<target>
convention, so the publish step picks them up unchanged.

Refs tsl0922#1011.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ofcRS ofcRS mentioned this pull request Jun 20, 2026
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.

1 participant