a73x

d62bdb97

build: make release, tarred, so the exec bit survives

a73x   2026-08-21 08:33

Commit message
build: make release, tarred, so the exec bit survives

Bare binaries download 0644. A fresh box then fails as 'Permission denied'
from the remote muxd, surfaced by the client as 'no endpoint announce
(UnterminatedLine)' — which names neither the file nor the mode. A tarball
carries it, and one pipe over ssh is the whole remote install.

The recipe was living in a memory of how the last release was built. It
now checks the stripped artifacts' own --version against build.zig, which
is the failure a hand-run recipe actually has: a stale stage directory
shipping under a bumped number.

Makefile
Old New
@@ -2,7 +2,7 @@
2 # default zig is 0.17-dev. Override with ZIG=... if yours lives elsewhere. 2 # default zig is 0.17-dev. Override with ZIG=... if yours lives elsewhere.
3 ZIG ?= $(HOME)/Downloads/zig-x86_64-linux-0.15.2/zig 3 ZIG ?= $(HOME)/Downloads/zig-x86_64-linux-0.15.2/zig
4 4
5 .PHONY: build test e2e soak bench agent deps clean clean-deps xversion xversion-build install 5 .PHONY: build test e2e soak bench agent deps clean clean-deps xversion xversion-build install release
6 6
7 # The QUIC stack (deps/quic) is built on demand by build.zig, so no target 7 # The QUIC stack (deps/quic) is built on demand by build.zig, so no target
8 # here needs to depend on this one. It exists to make the one-time cost 8 # here needs to depend on this one. It exists to make the one-time cost
@@ -24,6 +24,46 @@ install: build
24 install -d $(BINDIR) 24 install -d $(BINDIR)
25 install -m755 zig-out/bin/muxd zig-out/bin/mux zig-out/bin/muxa zig-out/bin/muxweb $(BINDIR)/ 25 install -m755 zig-out/bin/muxd zig-out/bin/mux zig-out/bin/muxa zig-out/bin/muxweb $(BINDIR)/
26 26
27 # The artifact a release publishes, built the way every release since
28 # v0.0.1-4 has been: static musl so one binary runs on any x86_64 Linux,
29 # ReleaseSafe because the safety checks are worth more than the last few
30 # percent and they survive stripping — only DWARF goes, 17MB to ~3MB each.
31 #
32 # TARRED, and that is the point of having a target at all. Publishing the
33 # four binaries bare loses the exec bit: they download 0644, and the first
34 # thing anyone does on a fresh box is a chmod nothing documents. A tarball
35 # carries the mode, so `tar xzf - -C ~/.local/bin` over ssh is the whole
36 # remote install.
37 #
38 # Flat rather than a versioned directory inside, for the same reason: the
39 # common move is putting muxd on a box, and one pipe should finish it.
40 VERSION := $(shell sed -n 's/^[[:space:]]*const version = "\(.*\)";/\1/p' build.zig | head -1)
41 RELDIR ?= dist
42 RELBIN = $(RELDIR)/v$(VERSION)
43 RELTAR = $(RELDIR)/mux-v$(VERSION)-x86_64-linux-musl.tar.gz
44 # The version guard below asks the STRIPPED artifacts what they are, so a
45 # stale stage directory cannot ship under a bumped number. muxa has no
46 # --version (it is verbs and JSON), so it is built and shipped but not asked.
47 release:
48 @test -n "$(VERSION)" || { echo "release: no version found in build.zig"; exit 1; }
49 $(ZIG) build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe -p $(RELDIR)/stage
50 rm -rf $(RELBIN) $(RELTAR)
51 install -d $(RELBIN)
52 install -m755 $(RELDIR)/stage/bin/muxd $(RELDIR)/stage/bin/mux \
53 $(RELDIR)/stage/bin/muxa $(RELDIR)/stage/bin/muxweb $(RELBIN)/
54 strip $(RELBIN)/muxd $(RELBIN)/mux $(RELBIN)/muxa $(RELBIN)/muxweb
55 @for b in muxd mux muxweb; do \
56 got=$$($(RELBIN)/$$b --version | awk '{print $$2}'); \
57 [ "$$got" = "$(VERSION)" ] || { \
58 echo "release: $$b reports $$got, expected $(VERSION)"; exit 1; }; \
59 done
60 tar czf $(RELTAR) -C $(RELBIN) muxd mux muxa muxweb
61 @echo
62 @sha256sum $(RELTAR)
63 @echo "install: tar xzf $(RELTAR) -C ~/.local/bin"
64 @echo "remote: ssh HOST 'mkdir -p ~/.local/bin && tar xzf - -C ~/.local/bin' < $(RELTAR)"
65 @echo "publish: git collab release publish v$(VERSION) $(RELTAR)"
66
27 test: 67 test:
28 $(ZIG) build test 68 $(ZIG) build test
29 69
README.md
Old New
@@ -25,6 +25,16 @@ that runs on any x86_64 Linux:
25 ~/Downloads/zig-x86_64-linux-0.15.2/zig build -Dtarget=x86_64-linux-musl 25 ~/Downloads/zig-x86_64-linux-0.15.2/zig build -Dtarget=x86_64-linux-musl
26 ``` 26 ```
27 27
28 `make release` does the whole release build instead — static musl,
29 `ReleaseSafe`, stripped, checked against `build.zig`'s version, and tarred
30 into `dist/`. The tarball is what to hand another machine, because it
31 carries the exec bit that a bare downloaded binary does not:
32
33 ```sh
34 make release
35 ssh HOST 'mkdir -p ~/.local/bin && tar xzf - -C ~/.local/bin' < dist/mux-vVERSION-x86_64-linux-musl.tar.gz
36 ```
37
28 ## Quick start, local 38 ## Quick start, local
29 39
30 ```sh 40 ```sh
docs/decisions.md
Old New
@@ -5132,3 +5132,20 @@ in a local session worked with no flag. The daemon's socket now overwrites
5132 it, and a local session needs `-A` where it previously needed nothing. The 5132 it, and a local session needs `-A` where it previously needed nothing. The
5133 overwrite is the point — otherwise every client shares one identity — but it 5133 overwrite is the point — otherwise every client shares one identity — but it
5134 is a regression for one workflow and is now documented as one. 5134 is a regression for one workflow and is now documented as one.
5135
5136 **Releases are tarred, and `make release` is why there is a rule.** From
5137 v0.0.1-4 through v0.0.1-10 a release was one `mux-vX-x86_64-linux-musl.tar.gz`;
5138 v0.0.1-11 published the four binaries bare, with no decision recorded anywhere
5139 and nothing in the repo consuming either shape. The bare form costs the exec
5140 bit — a downloaded binary arrives 0644, so `mux HOST` on a freshly installed
5141 box dies with `Permission denied` from the remote `muxd` and reports it as
5142 `no endpoint announce (UnterminatedLine)`, which names neither the file nor
5143 the mode. Observed on a LAN box the day v0.0.1-12-pre1 went out.
5144
5145 So: tar, flat, one `tar xzf - -C ~/.local/bin` over ssh to install, and the
5146 recipe lives in the Makefile rather than in a memory of how the last one was
5147 built. The target asks the STRIPPED artifacts their `--version` and refuses
5148 to ship if it disagrees with `build.zig` — a stale stage directory under a
5149 bumped number is the failure a hand-run recipe actually has. The binaries are
5150 reproducible (a rebuild matched the published sha256s byte for byte); the
5151 gzip wrapper is not, since it embeds an mtime.