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>
194 lines
8.2 KiB
Bash
194 lines
8.2 KiB
Bash
#!/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 =="
|