Makefile
Ref: Size: 14.9 KiB History
# ghostty (pinned in build.zig.zon) hard-requires Zig 0.15.x. mise owns the
# version and resolves the executable; override with ZIG=... for a deliberate
# non-mise toolchain.
ZIG ?= $(shell mise which zig 2>/dev/null)
ifeq ($(strip $(ZIG)),)
$(error Zig 0.15.2 is managed by mise; run `mise install` or set ZIG=/path/to/zig)
endif
export ZIG
# On a Mac the pinned zig cannot link against Xcode 26.4+'s SDK (see
# deps/mac-sdk.sh). The shim dir goes FIRST on PATH for every recipe here,
# and `mac-sdk` builds it; both are no-ops on Linux and on a Mac whose SDK
# is linkable. MUX_TARGET follows the host: a Mac installs a Mac binary.
#
# `native` and NOT `aarch64-macos`, which names the same machine and does
# not build. zig 0.15.2 looks for the macOS SDK — by running `xcrun
# --show-sdk-path`, which is exactly what the shim above answers — only for
# a NATIVE target; an explicitly named one is a cross-compile and gets no
# system include path at all, so `@cImport` of <util.h> (forkpty's header,
# which lives in the SDK) fails with "file not found" and `make install` on
# a Mac cannot produce a binary. Found by test/xos.sh, which is the first
# thing in the tree to run `make install` on Darwin.
#
# RELEASE_TARGET and SHA256 follow the host for the same reason; the comment
# above `release` below says why that is now right. RELEASE_BUILD_TARGET is
# the `-Dtarget=` word and RELEASE_TARGET the tarball's NAME, because on a
# Mac those two differ: the artifact is `aarch64-macos` and the only spelling
# that builds it is `native`, per the paragraph above.
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
export PATH := $(CURDIR)/deps/mac-sdk/bin:/opt/homebrew/bin:$(PATH)
MUX_TARGET ?= native
RELEASE_TARGET ?= aarch64-macos
RELEASE_BUILD_TARGET ?= native
SHA256 ?= shasum -a 256
endif
MUX_TARGET ?= x86_64-linux-musl
.PHONY: build lint check ci test daemon-test client-test native-core-test e2e soak bench agent native native-e2e native-stress throughput vm coverage deps clean clean-deps xversion xversion-build install release release-mac mac-sdk mac xos provision-mac
# The QUIC stack (deps/quic) is built on demand by build.zig, so no target
# here needs to depend on this one. It exists to make the one-time cost
# explicit and runnable on its own: the FIRST build after a clean checkout
# downloads ~30MB and takes a few minutes. `deps` warms both targets so a
# later musl cross-build (wan.sh) does not surprise anyone mid-measurement.
deps:
./deps/quic/build-deps.sh native
./deps/quic/build-deps.sh musl
build: mac-sdk
$(MAKE) lint
$(ZIG) build
# Install the two product binaries; the other build artifacts are test fixtures.
BINDIR ?= $(HOME)/.local/bin
# ReleaseSafe, like `release` — not the Debug tree `build` leaves in
# zig-out. ghostty gates its page-integrity check on ITS optimize mode, and
# in Debug that check rebuilds a hash map over every cell (and stands up a
# fresh DebugAllocator) on every page mutation: 20k lines of `yes` took
# 3713ms against 6ms ReleaseSafe, tmux 6ms. Nobody wants that on PATH.
#
# Staged in its own prefix because a `-Doptimize=` build would otherwise
# overwrite zig-out/bin, leaving the dev tree's binaries and test fixtures
# silently not the ones `make test` just built — the same trap xversion-build
# documents below.
# The `rm -f` in the recipe takes the three names this binary replaced off
# PATH: a box with an older install keeps them forever otherwise, and a
# `muxd` still answering `--version` is a daemon someone will start.
INSTDIR ?= dist/install
# Static musl, same as every release: the installed image is the one
# `mux d upgrade HOST` streams to other boxes, and a native build dies on
# any CPU older than this one (measured: illegal instruction on the
# laptop, 2026-09-02). Cost, measured the same day, best-of-5: repaint
# 92ms -> 114ms (musl memcpy on 15MB of paint bytes, paid once per
# reattach); the solo and client legs moved inside their noise.
#
# The installed mux target is static musl on Linux, for the reason above;
# muxg uses the native target so it can link the host's GUI libraries.
# The `?=` at the top of this file is what sets it, so a Mac gets its own.
install: mac-sdk
$(ZIG) build -Dtarget=$(MUX_TARGET) -Doptimize=ReleaseSafe -p $(INSTDIR)
$(ZIG) build native -Doptimize=ReleaseSafe -p $(INSTDIR)
install -d $(BINDIR)
install -m755 $(INSTDIR)/bin/mux $(INSTDIR)/bin/muxg $(BINDIR)/
rm -f $(BINDIR)/muxd $(BINDIR)/muxa $(BINDIR)/muxweb
# mux keeps its static musl build on Linux. muxg needs the host's dynamic GUI
# libraries, so build it separately with the native target in the same prefix.
# Both use ReleaseSafe and are stripped before packaging.
#
# TARRED, and that is the point of having a target at all. Publishing the
# binary bare loses the exec bit: it downloads 0644, and the first
# thing anyone does on a fresh box is a chmod nothing documents. A tarball
# carries the mode, so `tar xzf - -C ~/.local/bin` over ssh is the whole
# remote install.
#
# Flat rather than a versioned directory inside, for the same reason: the
# common move is putting `mux` on a box, and one pipe should finish it.
VERSION := $(shell sed -n 's/^[[:space:]]*const version = "\(.*\)";/\1/p' build.zig | head -1)
RELDIR ?= dist
RELBIN = $(RELDIR)/v$(VERSION)
# The release DOES follow the host now, which reverses the rule this line
# used to carry: "what a release IS must not follow the host that cuts it".
# That rule was written when there was one release, a Linux one, and the
# danger it named was a Mac quietly writing a Mach-O binary into the Linux
# tarball. It cannot: the target is in the tarball's NAME, so a Mac cuts
# mux-vN-aarch64-macos.tar.gz and nothing downstream can mistake that file
# for mux-vN-x86_64-linux-musl.tar.gz. And now that mux runs on two
# operating systems, a release IS one tarball per OS, each cut on that OS —
# there is no cross-compiling the Mac one (README, "macOS"), so the choice
# is not between one host and another but between a Mac release existing and
# not existing. The old rule's second argument was that macOS ships no
# sha256sum; SHA256 at the top of this file answers that with `shasum -a
# 256` instead of making it a reason to refuse.
#
# RELEASE_BUILD_TARGET is the word handed to `-Dtarget=`, kept apart from
# the NAME because on Darwin the two differ: `native` is the only spelling
# that builds, and `aarch64-macos` is what the artifact must be called. On
# Linux they are the same string, so this recipe is byte-for-byte what it
# was.
RELEASE_TARGET ?= x86_64-linux-musl
RELEASE_BUILD_TARGET ?= $(RELEASE_TARGET)
SHA256 ?= sha256sum
RELTAR = $(RELDIR)/mux-v$(VERSION)-$(RELEASE_TARGET).tar.gz
# The version guard below asks the STRIPPED artifact what it is, so a
# stale stage directory cannot ship under a bumped number.
release: mac-sdk
@test -n "$(VERSION)" || { echo "release: no version found in build.zig"; exit 1; }
$(ZIG) build -Dtarget=$(RELEASE_BUILD_TARGET) -Doptimize=ReleaseSafe -p $(RELDIR)/stage
$(ZIG) build native -Doptimize=ReleaseSafe -p $(RELDIR)/stage
rm -rf $(RELBIN) $(RELTAR)
install -d $(RELBIN)
install -m755 $(RELDIR)/stage/bin/mux $(RELDIR)/stage/bin/muxg $(RELBIN)/
strip $(RELBIN)/mux $(RELBIN)/muxg
@for bin in mux muxg; do \
got=$$($(RELBIN)/$$bin --version | awk '{print $$2}'); \
[ "$$got" = "$(VERSION)" ] || { \
echo "release: $$bin reports $$got, expected $(VERSION)"; exit 1; }; \
done
tar czf $(RELTAR) -C $(RELBIN) mux muxg
@echo
@$(SHA256) $(RELTAR)
@echo "install: tar xzf $(RELTAR) -C ~/.local/bin"
@echo "remote: ssh HOST 'mkdir -p ~/.local/bin && tar xzf - -C ~/.local/bin' < $(RELTAR)"
@echo "publish: git collab release publish v$(VERSION) $(RELTAR)"
# The Mac half of a release. `make release` above cuts whichever OS it is run
# on, and this is how the OTHER one gets cut from here: MAC_BUILDER names a
# Mac holding this repo — the same box and the same variable the macOS gates
# take — and tools/release-mac.sh pushes the tag, runs `make release` there
# and publishes the tarball from there. No target of this Makefile can
# produce a Mac binary on Linux; only a Mac can. The script states the rest
# of the contract, including why MAC_BUILDER has no default.
release-mac:
./tools/release-mac.sh
test: mac-sdk
$(ZIG) build test
e2e: mac-sdk
$(ZIG) build e2e
soak: mac-sdk
$(ZIG) build soak
bench: mac-sdk
$(ZIG) build bench
# ReleaseSafe into its own prefix, for the reason throughput.sh opens with:
# in the Debug tree the engine's per-mutation integrity check dominates every
# reading, so a timing assertion there would measure ghostty's debug wiring
# and never this code. That is also why there is no build.zig step — a step
# is handed the default-optimize artifacts, which are the wrong ones.
# Line coverage for the e2e suite, via kcov (pacman -S kcov).
#
# ReleaseSafe in its own prefix, for throughput's reason: kcov traps on every
# instrumented line, so its cost rides on top of whatever the binary already
# pays per line, and a Debug ghostty pays the page-integrity check documented
# above on top of that. Nothing here has measured the Debug pairing to be
# unusable — the runs that looked slow were hanging, for the reason
# test/coverage.sh gives — so this is the documented 600x avoided, not a
# number this target established.
#
# The trade is line granularity: an optimised build inlines, so lines merge
# into their callers and the denominator shrinks. Measured on the daemon: 1639
# instrumented lines here against 2651 in Debug.
#
# Outside `ci` deliberately — it needs an optional system package and is
# still several times slower than the suite it wraps.
COVDIR ?= dist/coverage
coverage: mac-sdk
$(ZIG) build -Doptimize=ReleaseSafe -p $(COVDIR)
./test/coverage.sh $(COVDIR)/bin/mux \
$(COVDIR)/bin/rawmode $(COVDIR)/bin/delaypipe $(COVDIR)/bin/render \
$(COVDIR)/bin/ptyclient $(COVDIR)/bin/wsclient
# Real user journeys against the mux-e2e VM (test/vm.sh): a delivery-ritual
# gate beside ci, never inside it — ci stays hermetic and offline. Static
# musl because the same binary is the client here and the install there.
VMDIR ?= dist/vmgate
vm:
$(ZIG) build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe -p $(VMDIR)
./test/vm.sh $(VMDIR)/bin/mux $(VMDIR)/bin/ptyclient
THRUDIR ?= dist/throughput
throughput: mac-sdk
$(ZIG) build -Doptimize=ReleaseSafe -p $(THRUDIR)
./test/throughput.sh $(THRUDIR)/bin/mux $(THRUDIR)/bin/ptyclient
mac-sdk:
sh ./deps/mac-sdk.sh
# The `make vm` of the macOS port: user journeys on a real macOS, over ssh.
#
# Both macOS gates take their boxes BY NAME in the environment, and neither
# has a default for one — a gate that guessed a box would eventually guess
# somebody's laptop (test/box_lib.sh):
#
# MAC_BOX the macOS box under test, with no toolchain on it. Both
# gates may SCRUB it, so make it a VM: provision-mac below.
# MAC_BUILDER a Mac holding this repo with zig and deps/mac-sdk. It
# builds and is never scrubbed. Defaults to $MAC_BOX.
# LINUX_BOX (xos only) an x86_64 Linux VM both this box and MAC_BOX can
# reach, also scrubbed.
#
# eval "$(test/provision-mac.sh)" # export MAC_BOX=admin@...
# MAC_BUILDER=squirtle make mac
mac: build
sh ./test/mac.sh
# The pristine macOS guest the two gates run against: clone the hand-made
# base image on a tart host, boot it bridged onto the LAN, key it to this
# box, and print the one `export MAC_BOX=...` line to eval. `--down` when
# the run is over. The base image is a human action; this is not.
provision-mac:
@sh ./test/provision-mac.sh
# The cross-OS gate (test/xos.sh): a macOS client on a Linux daemon and a
# Linux client on a macOS daemon, over ssh and over QUIC. A hand gate beside
# vm and mac, never inside ci — it needs three other machines (MAC_BOX,
# LINUX_BOX and MAC_BUILDER, documented above), and ci stays hermetic and
# offline.
#
# Two builds, because the two sides of the seam are two architectures. The
# musl one is what gets installed on the Linux box: static, for vm's reason
# — that box is a VM on a host whose CPU this one did not build for. The
# Debug zig-out build is the Linux CLIENT and the pty fixture, which is what
# `make build` already produced.
XOSDIR ?= dist/xosgate
xos: build
$(ZIG) build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe -p $(XOSDIR)
./test/xos.sh $(XOSDIR)/bin/mux zig-out/bin/mux zig-out/bin/ptyclient
lint:
sh tools/lint.sh
# `zig build check` grades the tree; bans.sh grades the grader. The folder
# rules are the one gate whose failure mode is silence — a needle list
# edited down to nothing, or a folder dropped from a rule's list, leaves a
# green tree asserting rules that no longer bite — so one planted needle per
# rule runs here, after the gate it is checking. Two seconds.
check: mac-sdk
$(MAKE) lint
$(ZIG) build check
sh test/bans.sh $(ZIG)
# Everything a change must pass before delivery, cheapest gate first so a
# fmt slip fails in seconds rather than after the e2e suites. Sequential
# $(MAKE) calls rather than prerequisites: prerequisite order is not a
# promise make makes, and "which gate failed" is the whole output.
#
# Deliberately excludes soak (the e2e suite ten times) and xversion (needs
# a second checkout in XVER_OLD_WORKTREE). Both are asked for by name.
ci:
$(MAKE) check
$(MAKE) e2e
$(MAKE) agent
$(MAKE) throughput
agent: mac-sdk
$(ZIG) build agent
# Component gates reuse the aggregate test artifacts. Native core needs no
# window/font libraries; the full native gate below includes its tests too.
daemon-test client-test native-core-test: mac-sdk
$(ZIG) build $@
# Opt-in because these link system SDL3, freetype, fontconfig and HarfBuzz.
# They stay outside every default build and delivery gate.
native: mac-sdk
$(ZIG) build native native-test
native-e2e: mac-sdk
$(ZIG) build native-e2e -Doptimize=ReleaseSafe
native-stress: mac-sdk
$(ZIG) build native-stress -Doptimize=ReleaseSafe
# Cross-version gate (test/xversion.sh): this tree's client against a
# previous version's daemon and back, each daemon in a container.
#
# Two steps because only you know what "previous" means. XVER_OLD_REF is
# any ref with a checkout to build from — set XVER_OLD_WORKTREE to that
# checkout's path. Both sides build STATIC musl: the same binary then runs
# in the container AND on this host, so the rig needs one artifact per
# version rather than two.
#
# Installed with `-p` into .xversion/ rather than the default prefix on
# purpose: a bare `-Dtarget=` build overwrites zig-out/bin with binaries
# for the wrong libc, and the next `make e2e` would silently run those.
XVER_OLD_WORKTREE ?= ..
xversion-build: mac-sdk
$(ZIG) build -Dtarget=x86_64-linux-musl -p $(CURDIR)/.xversion/new
cd $(XVER_OLD_WORKTREE) && $(ZIG) build -Dtarget=x86_64-linux-musl -p $(CURDIR)/.xversion/old
xversion:
./test/xversion.sh
# Deliberately leaves deps/quic alone: rebuilding it needs the network and
# several minutes, and it is pinned by checksum, so a routine clean should
# not cost that. `clean-deps` is the one that does.
clean:
rm -rf zig-out .zig-cache
clean-deps:
rm -rf deps/quic/out deps/quic/work