lx(ci): static musl Linux router builds with naive preserved

lx-release.yml: new build_linux_musl job (amd64/arm64/armv7/mipsle) that
clones cronet-go, fetches the Chromium musl toolchain via cmd/build-naive,
and builds CGO_ENABLED=1 with with_musl (swapping with_purego) so libcronet
is linked statically — no libdl.so.2, runs on musl routers, naive kept.
Linux moves out of the desktop build job. Artifact names mirror upstream
arch suffixes (armv7, mipsle-softfloat) without the -musl suffix since
Linux ships a single (musl) variant.

lx-ci.yml: dispatch-only linux_musl smoke job runs the same pipeline
(build + verify statically-linked / no libdl) without publishing.
This commit is contained in:
Leadaxe
2026-06-12 12:39:07 +03:00
parent bd9dcb0331
commit 1453166fe3
2 changed files with 220 additions and 4 deletions
+93
View File
@@ -175,3 +175,96 @@ jobs:
libbox-legacy.aar
if-no-files-found: error
retention-days: 7
# Router musl builds: prove the static-musl + naive pipeline compiles and links
# statically (no libdl.so.2) for the router arches. Build + verify only, no
# publish — the release is lx-release.yml's build_linux_musl. Keep the toolchain
# steps here in sync with that job. See SPECS/006.
linux_musl:
if: github.event_name == 'workflow_dispatch'
name: linux-musl ${{ matrix.asset }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- { arch: amd64, asset: linux-amd64 }
- { arch: arm64, asset: linux-arm64 }
- { arch: arm, goarm: "7", asset: linux-armv7 }
- { arch: mipsle, gomips: softfloat, asset: linux-mipsle-softfloat }
steps:
- uses: actions/checkout@v4
with: { submodules: recursive, fetch-depth: 0 }
- uses: actions/setup-go@v5
with: { go-version-file: go.mod, check-latest: true }
- name: Clone cronet-go
run: |
set -xeuo pipefail
CRONET_GO_VERSION="$(cat .github/CRONET_GO_VERSION)"
git init ~/cronet-go
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
git -C ~/cronet-go checkout FETCH_HEAD
git -C ~/cronet-go submodule update --init --recursive --depth=1
- name: Regenerate Debian keyring
run: |
set -xeuo pipefail
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
cd ~/cronet-go
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
- name: Cache Chromium toolchain
uses: actions/cache@v4
with:
path: |
~/cronet-go/naiveproxy/src/third_party/llvm-build/
~/cronet-go/naiveproxy/src/gn/out/
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
~/cronet-go/naiveproxy/src/out/sysroot-build/
key: chromium-toolchain-musl-${{ matrix.arch }}-${{ hashFiles('.github/CRONET_GO_VERSION') }}
- name: Download Chromium musl toolchain
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
- name: Set Chromium toolchain environment
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> "$GITHUB_ENV"
- name: Build (musl + naive, static)
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
run: |
set -xeuo pipefail
TAGS="$(make -f Makefile.lx -s lx-print-tags)"
TAGS="${TAGS/with_purego/with_musl}"
go build -v -trimpath -tags "$TAGS" \
-ldflags "-checklinkname=0 -s -w -buildid=" \
-o "sing-box-${{ matrix.asset }}" ./cmd/sing-box
- name: Verify static (no libdl)
run: |
set -xeuo pipefail
file "sing-box-${{ matrix.asset }}"
file "sing-box-${{ matrix.asset }}" | grep -q "statically linked"
if strings -a "sing-box-${{ matrix.asset }}" | grep -q "libdl.so.2"; then
echo "FAIL: libdl.so.2 reference present — not a static musl build"; exit 1
fi
echo "OK: statically linked, no libdl.so.2"
- uses: actions/upload-artifact@v4
with:
name: sing-box-${{ matrix.asset }}
path: sing-box-${{ matrix.asset }}
if-no-files-found: error
retention-days: 7
+127 -4
View File
@@ -29,8 +29,9 @@ jobs:
fail-fast: false
matrix:
include:
- { goos: linux, goarch: amd64 }
- { goos: linux, goarch: arm64 }
# Linux lives in the build_linux_musl job (static musl + naive, for
# routers). See SPECS/006. This job covers the purego/native targets
# where libdl is a non-issue.
- { goos: darwin, goarch: amd64 }
- { goos: darwin, goarch: arm64 }
- { goos: windows, goarch: amd64, ext: .exe }
@@ -112,6 +113,124 @@ jobs:
path: dist/*
if-no-files-found: error
# Static musl Linux builds for routers (AsusWRT Merlin, OpenWrt, Keenetic).
# The desktop `build` job ships Linux via with_purego, which pulls a dynamic
# libdl.so.2 dependency (purego's //go:cgo_import_dynamic) and won't load on
# musl. Here we mirror upstream build.yml: clone cronet-go, fetch the Chromium
# musl toolchain via its cmd/build-naive, and build CGO_ENABLED=1 with
# `with_musl` — libcronet.a is linked statically and naive is preserved.
# See SPECS/006.
build_linux_musl:
name: build linux-musl/${{ matrix.asset }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- { arch: amd64, asset: linux-amd64 }
- { arch: arm64, asset: linux-arm64 }
- { arch: arm, goarm: "7", asset: linux-armv7 }
- { arch: mipsle, gomips: softfloat, asset: linux-mipsle-softfloat }
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
- name: Resolve version
id: ver
run: |
TAG="${{ github.event.inputs.tag || github.ref_name }}"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
# cronet-go carries the build-naive tool + the naiveproxy/src Chromium
# toolchain sources. Pin to the same commit go.mod depends on.
- name: Clone cronet-go
run: |
set -xeuo pipefail
CRONET_GO_VERSION="$(cat .github/CRONET_GO_VERSION)"
git init ~/cronet-go
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
git -C ~/cronet-go checkout FETCH_HEAD
git -C ~/cronet-go submodule update --init --recursive --depth=1
- name: Regenerate Debian keyring
run: |
set -xeuo pipefail
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
cd ~/cronet-go
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
- name: Cache Chromium toolchain
uses: actions/cache@v4
with:
path: |
~/cronet-go/naiveproxy/src/third_party/llvm-build/
~/cronet-go/naiveproxy/src/gn/out/
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
~/cronet-go/naiveproxy/src/out/sysroot-build/
key: chromium-toolchain-musl-${{ matrix.arch }}-${{ hashFiles('.github/CRONET_GO_VERSION') }}
- name: Download Chromium musl toolchain
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
- name: Set Chromium toolchain environment
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> "$GITHUB_ENV"
- name: Build (musl + naive, static)
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
run: |
set -xeuo pipefail
# LX_TAGS is the single source of truth (Makefile.lx). Swap the purego
# cronet loader for the static musl one; with_naive_outbound stays.
TAGS="$(make -f Makefile.lx -s lx-print-tags)"
TAGS="${TAGS/with_purego/with_musl}"
mkdir -p dist
go build -v -trimpath -tags "$TAGS" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ steps.ver.outputs.version }}' -checklinkname=0 -s -w -buildid=" \
-o dist/sing-box ./cmd/sing-box
- name: Verify static (no libdl)
run: |
set -xeuo pipefail
file dist/sing-box
file dist/sing-box | grep -q "statically linked"
if strings -a dist/sing-box | grep -q "libdl.so.2"; then
echo "FAIL: libdl.so.2 reference present — not a static musl build"; exit 1
fi
echo "OK: statically linked, no libdl.so.2"
- name: Package
run: |
NAME="sing-box-${{ steps.ver.outputs.version }}-${{ matrix.asset }}"
mkdir -p "stage/$NAME"
cp dist/sing-box "stage/$NAME/"
cp LICENSE LICENSING.md README.md "stage/$NAME/" 2>/dev/null || true
tar -C stage -czf "dist/$NAME.tar.gz" "$NAME"
- uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.asset }}
path: dist/*.tar.gz
if-no-files-found: error
build_android:
name: build android (libbox.aar)
runs-on: ubuntu-latest
@@ -167,7 +286,7 @@ jobs:
release:
name: publish release
needs: [build, build_android]
needs: [build, build_linux_musl, build_android]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -199,7 +318,11 @@ jobs:
- **XHTTP** transport (\`with_xhttp\`) — live-validated against a real Xray (3x-ui) server in \`packet-up\`/\`auto\` mode (handshake + DNS + HTTPS + download). Xray-compatible "splithttp"; composes with Reality. (\`stream-one\` has a known framing bug — use \`auto\`.)
### Binaries
Drop-in \`sing-box\` for {linux, darwin, windows} × {amd64, arm64}, plus a **Windows 7 (32-bit)** legacy build (\`sing-box-${{ steps.ver.outputs.version }}-windows-386-legacy-windows-7.zip\` — built with a Win7-patched Go; without naive/cronet, which has no windows/386 target). Each archive contains the \`sing-box\` binary (\`sing-box version\` reports \`${{ steps.ver.outputs.version }}\`). Verify downloads against \`SHA256SUMS\`.
Drop-in \`sing-box\` for **darwin / windows** × {amd64, arm64}, plus a **Windows 7 (32-bit)** legacy build (\`sing-box-${{ steps.ver.outputs.version }}-windows-386-legacy-windows-7.zip\` — built with a Win7-patched Go; without naive/cronet, which has no windows/386 target).
**Linux — static musl builds for routers** (AsusWRT Merlin, OpenWrt, Keenetic): \`linux-amd64\`, \`linux-arm64\`, \`linux-armv7\`, \`linux-mipsle-softfloat\`. These are statically linked (no \`libdl.so.2\`/glibc dependency) and **keep NaïveProxy** — they run on musl routers where the previous dynamic builds failed with \`libdl.so.2: cannot open shared object file\`. See SPECS/006.
Each archive contains the \`sing-box\` binary (\`sing-box version\` reports \`${{ steps.ver.outputs.version }}\`). Verify downloads against \`SHA256SUMS\`.
### Android
\`libbox-${{ steps.ver.outputs.version }}.aar\` (+ \`libbox-legacy-…\` for SDK 21) — gomobile build of \`experimental/libbox\` with \`with_xhttp\`+\`with_awg\` enabled, for embedding in an Android app. \`Libbox.version()\` reports the lx version.