Files
shater/scripts/build-shaterd.sh
T
omarandClaude Opus 5 a8f2b0f068 ci: derive package versions from the git tag (B4)
PKG_VERSION/PKG_RELEASE were hand-written literals nobody bumped, so
v0.2.2 … v0.2.6 all shipped as `shaterd 0.2.0-r3` with different binaries
inside (v0.2.6's ELF is 5 491 616 B against r2's 5 488 336 B). Both opkg
and apk offer an upgrade only when the feed's version string differs from
the installed one, so `apk update` saw nothing new and the routers could
not be updated through the normal path at all.

ci/version.sh is now the single source of truth. It derives the version
from `git describe`:

    tag `vX.Y.Z`   -> PKG_VERSION=X.Y.Z  PKG_RELEASE=1
    off-tag build  -> nearest tag + PKG_RELEASE=<commits since it> + 1
    no tag/no git  -> 0.0.0-r1 (below everything ever published)

Ordering verified with the real tools, not from memory — apk-tools 3.0.3
(`apk version -t`) and opkg 38eccbb1 (`opkg compare-versions`) agree that
0.2.0-r3 < 0.2.6-r2 < 0.2.6-r10 < 0.2.6-r12 < 0.2.7-r1 < 0.3.0-r1, so a
release always outranks the rolling builds that preceded it and rolling
builds grow monotonically between releases.

The value travels as SHATER_PKG_VERSION/SHATER_PKG_RELEASE in the SDK
build environment of BOTH lanes; the Makefiles keep a literal fallback so
a manual/offline build still works with no CI and no git. Because the
hand-off crosses docker, `su` and make's env import, ci/sdk-build.sh and
ci/sdk-build-apk.sh now ASSERT that the produced .ipk/.apk really carries
that version — the B4 failure mode was a stale version shipping silently,
and that can no longer happen quietly.

The binary agrees with the package: scripts/build-shaterd.sh takes
constant.Version from the same ci/version.sh (vX.Y.Z-rR[-g<sha>]) instead
of its own `git describe`, and the workflow computes it once per job.
Both build jobs now check out with fetch-depth: 0 — `git describe` needs
tags and ancestry, which the default shallow checkout has neither of.

byedpi is deliberately left alone: PKG_VERSION:=0.17.3 is upstream
ByeDPI's own version, what PKG_HASH pins and what tells an operator which
ByeDPI is installed. Stamping our tag on it would also be a downgrade —
every comparator reads 0.2.7 < 0.17.3 (component-wise, 2 < 17), verified.

Docs: INSTALL.md gains §2.1 (the scheme + the ordering evidence), and the
update sections of §5/§6 now explicitly warn against a bare `opkg upgrade`
/ `apk upgrade` and give the targeted form instead, quoting apk-tools 3:
"If list of packages is provided, only those packages are upgraded along
with needed dependencies". README.md and the release bodies match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 12:32:38 +03:00

194 lines
8.2 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#
# build-shaterd.sh — Shater v0.2 "ship artifact" builder.
#
# Produces the SPA-embedded, statically-linked (musl-safe), UPX-compressed
# `shaterd` router binary for every router arch we publish, ready to be consumed
# by the openwrt/shaterd prebuilt package.
#
# What it does, in order:
# 1. Builds the admin SPA: cd panel && npm ci && npm run build (Vite → panel/dist)
# 2. Copies panel/dist/* into shater/panel/webroot/ so //go:embed all:webroot
# bakes the REAL SPA into the binary (not the "SPA not embedded" placeholder).
# 3. For each arch in {amd64, arm64}:
# CGO_ENABLED=0 GOOS=linux GOARCH=<a> go build <router tags + ldflags>
# → dist/shaterd-<a> (uncompressed, kept for debugging — D10)
# UPX --lzma --best → dist/shaterd-<a>.upx (~9–11 MB — D10)
# 4. Stages dist/shaterd-<a>.upx into openwrt/shaterd/files/ so the prebuilt
# OpenWrt package can $(INSTALL_BIN) the arch-matched artifact.
# 5. Prints a size table + a per-arch static check (ELF type / no PT_INTERP).
#
# Router build tag set = D9 (musl-static). We deliberately DROP with_purego and
# with_naive_outbound: they pull cronet-go, which forces a glibc PT_INTERP even
# with CGO_ENABLED=0, making the binary unusable on musl OpenWrt.
#
# Usage:
# scripts/build-shaterd.sh [VERSION] [--fast]
#
# VERSION Version string stamped into constant.Version. Resolution order:
# 1) this positional arg, if given
# 2) $SHATER_VERSION, if set (CI sets it from ci/version.sh)
# 3) `ci/version.sh --binary` — THE single source of truth shared
# with the package version (vX.Y.Z-rR[-g<sha>], derived from
# the git tag exactly like PKG_VERSION/PKG_RELEASE), so the
# string the panel shows always matches `apk info shaterd`
# 4) fallback: v0.2.0-dev
# --fast Skip `npm ci` when panel/node_modules already exists (dev speed-up).
#
# Env:
# UPX=/path/to/upx Override the UPX binary (default: `upx` on PATH).
# On the Windows dev host it lives at:
# C:\Users\Admin\AppData\Local\Temp\claude\C--Users-Admin-Desktop-shater\b1447f18-5596-49a9-964e-dddd1617fdff\scratchpad\upx-4.2.4-win64\upx.exe
# (UPX is cross-arch: it packs linux amd64 AND aarch64 ELFs
# from a Windows host via Git Bash.)
# SHATER_VERSION See VERSION resolution above.
#
# Idempotent: safe to run repeatedly. Each run rebuilds dist/shaterd-<a> from a
# fresh `go build`, so the UPX step always packs an unpacked ELF.
#
set -euo pipefail
# --- locate the repo root (script lives in <repo>/scripts) ------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO"
# --- args -------------------------------------------------------------------
FAST=0
VERSION_ARG=""
for a in "$@"; do
case "$a" in
--fast) FAST=1 ;;
-h|--help)
sed -n '2,45p' "$0" | sed 's/^# \{0,1\}//'
exit 0 ;;
--*) echo "build-shaterd: unknown flag: $a" >&2; exit 2 ;;
*) VERSION_ARG="$a" ;;
esac
done
# --- version resolution -----------------------------------------------------
if [ -n "$VERSION_ARG" ]; then
VERSION="$VERSION_ARG"
elif [ -n "${SHATER_VERSION:-}" ]; then
VERSION="$SHATER_VERSION"
elif VERSION="$(sh "$REPO/ci/version.sh" --binary)"; then
# Same computation the PACKAGE version comes from (ci/version.sh), so the
# binary's constant.Version and the .ipk/.apk version can never drift apart.
: # ci/version.sh always succeeds (it falls back to 0.0.0 without git)
else
VERSION="v0.2.0-dev"
fi
[ -n "$VERSION" ] || VERSION="v0.2.0-dev"
# --- config -----------------------------------------------------------------
# D9 router tag set (musl-static). Keep in sync with docs-shater/DECISIONS.md D9.
# No with_gvisor: the data plane is tproxy/redirect (netplane), generate never
# emits a tun inbound, so the userspace gvisor stack was 3.6 MB of dead weight
# (tun would fall back to the system stack anyway).
# No with_clash_api: the panel is shater's own; generate never emits a clash_api
# service ("the shater generator emits none of those" — shater/engine/engine.go).
# No with_dhcp: shater resolvers are udp/tcp/doh/dot/local/fakeip — no "dhcp://"
# DNS transport is ever generated, and the slim registry never registers it.
ROUTER_TAGS="with_quic,with_wireguard,with_utls,badlinkname,tfogo_checklinkname0,with_xhttp,with_awg,with_lx_command"
LDFLAGS="-X github.com/sagernet/sing-box/constant.Version=${VERSION} -checklinkname=0 -s -w -buildid="
UPX_BIN="${UPX:-upx}"
# UPX itself treats the environment variable UPX as extra command-line options, so
# leaving our path there makes the child upx choke ("invalid string ... in
# environment variable 'UPX'"). Drop it now that we've captured the binary path.
unset UPX
DIST="$REPO/dist"
WEBROOT="$REPO/shater/panel/webroot"
PKG_FILES="$REPO/openwrt/shaterd/files"
# OpenWrt-artifact arch -> Go GOARCH
declare -A GOARCH_OF=( [amd64]=amd64 [arm64]=arm64 )
ARCHES=(amd64 arm64)
echo "== shater ship build =="
echo " version : $VERSION"
echo " tags : $ROUTER_TAGS"
echo " upx : $UPX_BIN"
echo " go : $(go version)"
echo
# --- step 1: build the SPA --------------------------------------------------
echo "== [1/4] building admin SPA (panel/) =="
cd "$REPO/panel"
if [ "$FAST" -eq 1 ] && [ -d node_modules ]; then
echo " --fast: node_modules present, skipping npm ci"
else
npm ci
fi
npm run build
cd "$REPO"
# --- step 2: embed panel/dist into shater/panel/webroot ---------------------
echo "== [2/4] staging panel/dist -> shater/panel/webroot =="
if [ ! -f "$REPO/panel/dist/index.html" ]; then
echo " ERROR: panel/dist/index.html missing after build" >&2
exit 1
fi
mkdir -p "$WEBROOT"
# Clear stale embedded content but KEEP the tracked .gitkeep marker (it keeps the
# dir embeddable on a fresh checkout where the SPA was never built).
find "$WEBROOT" -mindepth 1 ! -name '.gitkeep' -exec rm -rf {} +
cp -a "$REPO/panel/dist/." "$WEBROOT/"
[ -f "$WEBROOT/index.html" ] || { echo " ERROR: webroot/index.html not staged" >&2; exit 1; }
echo " staged: $(cd "$WEBROOT" && find . -type f | wc -l) file(s)"
# --- step 3+4: cross-build, UPX, stage --------------------------------------
mkdir -p "$DIST" "$PKG_FILES"
declare -A SZ_RAW SZ_UPX
for arch in "${ARCHES[@]}"; do
goarch="${GOARCH_OF[$arch]}"
raw="$DIST/shaterd-$arch"
upx="$DIST/shaterd-$arch.upx"
echo
echo "== [3/4] building shaterd-$arch (GOARCH=$goarch) =="
CGO_ENABLED=0 GOOS=linux GOARCH="$goarch" \
go build -trimpath -tags "$ROUTER_TAGS" -ldflags "$LDFLAGS" \
-o "$raw" ./shater/cmd/shaterd
# UPX packs in place; copy the fresh (unpacked) ELF first so re-runs never hit
# "already packed", and the uncompressed dist/shaterd-<arch> stays for debug.
cp -f "$raw" "$upx"
"$UPX_BIN" --lzma --best "$upx" >/dev/null
echo "== [4/4] staging shaterd-$arch.upx -> openwrt/shaterd/files =="
cp -f "$upx" "$PKG_FILES/shaterd-$arch.upx"
SZ_RAW[$arch]=$(stat -c '%s' "$raw" 2>/dev/null || stat -f '%z' "$raw")
SZ_UPX[$arch]=$(stat -c '%s' "$upx" 2>/dev/null || stat -f '%z' "$upx")
# --- static check (must be ET_EXEC, no PT_INTERP => runs on musl) --------
echo " -- static check ($arch) --"
if command -v readelf >/dev/null 2>&1; then
readelf -h "$raw" | grep -E '^ (Type|Machine):' | sed 's/^/ /'
if [ "$(readelf -l "$raw" | grep -c INTERP)" -eq 0 ]; then
echo " PT_INTERP: none (static — OK for musl)"
else
echo " PT_INTERP: PRESENT (WARNING: dynamic — will NOT run on musl)"
fi
elif command -v file >/dev/null 2>&1; then
echo " $(file -b "$raw")"
else
echo " (no readelf/file; verify with: go tool nm / readelf -l on Linux)"
fi
done
# --- summary ----------------------------------------------------------------
human() { awk -v b="$1" 'BEGIN{printf "%.1f MB", b/1048576}'; }
echo
echo "== sizes =="
printf ' %-22s %12s %12s\n' "artifact" "uncompressed" "upx(--lzma)"
for arch in "${ARCHES[@]}"; do
printf ' %-22s %12s %12s\n' "shaterd-$arch" "$(human "${SZ_RAW[$arch]}")" "$(human "${SZ_UPX[$arch]}")"
done
echo
echo " dist/ : dist/shaterd-{amd64,arm64}{,.upx}"
echo " staged for package : openwrt/shaterd/files/shaterd-{amd64,arm64}.upx"
echo "== done: version $VERSION =="