Files
shater/.github/workflows/lx-release.yml
T
Leadaxe 1453166fe3 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.
2026-06-12 12:39:07 +03:00

344 lines
13 KiB
YAML
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.
name: lx-release
# Cross-builds the drop-in `sing-box` binary for all platforms and publishes a
# GitHub Release with archives + checksums. Triggered by pushing a tag like
# v1.13.13-lx.1, or manually via workflow_dispatch. See SPECS/004.
on:
push:
tags: ['v*-lx.*']
workflow_dispatch:
inputs:
tag:
description: 'Release tag, e.g. v1.13.13-lx.1 (created at the current ref if missing)'
required: true
permissions:
contents: write
# Build tags are owned by Makefile.lx (single source of truth). The desktop build
# uses its LX_TAGS default; the Android AAR uses build_libbox's own tag set + the
# lx: features baked into it. `make -f Makefile.lx -s lx-print-tags` prints the set
# for the release notes so nothing is duplicated here.
jobs:
build:
name: build ${{ matrix.goos }}/${{ matrix.goarch }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# 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 }
- { goos: windows, goarch: arm64, ext: .exe }
# Windows 7 (32-bit): built with a Win7-patched Go, and without
# with_naive_outbound (cronet-go has no windows/386 build). See SPECS/004.
- { goos: windows, goarch: "386", ext: .exe, legacy_win7: true, legacy_name: windows-7 }
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- uses: actions/setup-go@v5
if: ${{ ! matrix.legacy_win7 }}
with:
go-version-file: go.mod
check-latest: true
# Win7 needs a Go toolchain that still targets Windows 7: setup_go_for_windows7.sh
# fetches stock Go and applies MetaCubeX/go patches reverting the Win7 removals.
- name: Cache Win7 Go toolchain
if: matrix.legacy_win7
id: cache-go-win7
uses: actions/cache@v4
with:
path: ~/go/go_win7
key: go_win7_${{ hashFiles('.github/setup_go_for_windows7.sh') }}
- name: Build Win7 Go toolchain
if: matrix.legacy_win7 && steps.cache-go-win7.outputs.cache-hit != 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
run: bash .github/setup_go_for_windows7.sh
- name: Use Win7 Go toolchain
if: matrix.legacy_win7
run: |
echo "PATH=$HOME/go/go_win7/bin:$PATH" >> "$GITHUB_ENV"
echo "GOROOT=$HOME/go/go_win7" >> "$GITHUB_ENV"
- name: Resolve version
id: ver
run: |
TAG="${{ github.event.inputs.tag || github.ref_name }}"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
- name: Build
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
TAGS="$(make -f Makefile.lx -s lx-print-tags)"
if [ "${{ matrix.legacy_win7 }}" = "true" ]; then
# cronet-go (with_naive_outbound) has no windows/386 build — drop it.
TAGS="${TAGS/with_naive_outbound,/}"
fi
make -f Makefile.lx lx-build \
LX_TAGS="$TAGS" \
LX_VERSION="${{ steps.ver.outputs.version }}" \
LX_OUTPUT="sing-box${{ matrix.ext }}"
- name: Package
run: |
NAME="sing-box-${{ steps.ver.outputs.version }}-${{ matrix.goos }}-${{ matrix.goarch }}"
if [ -n "${{ matrix.legacy_name }}" ]; then
NAME="${NAME}-legacy-${{ matrix.legacy_name }}"
fi
mkdir -p "stage/$NAME" dist
cp "sing-box${{ matrix.ext }}" "stage/$NAME/"
cp LICENSE LICENSING.md README.md "stage/$NAME/" 2>/dev/null || true
if [ "${{ matrix.goos }}" = "windows" ]; then
(cd stage && zip -qr "../dist/$NAME.zip" "$NAME")
else
tar -C stage -czf "dist/$NAME.tar.gz" "$NAME"
fi
- uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.goos }}-${{ matrix.goarch }}
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
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: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r28
- name: Setup OpenJDK 17
run: sudo apt-get update && sudo apt-get install -y openjdk-17-jdk-headless
- name: Resolve version
id: ver
run: |
TAG="${{ github.event.inputs.tag || github.ref_name }}"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
# build_libbox stamps Libbox.version() from `git describe` — ensure the tag exists.
git tag "$TAG" -f
- name: Build libbox.aar (with_xhttp + with_awg baked in by build_libbox)
run: |
make lib_install
export PATH="$PATH:$(go env GOPATH)/bin"
make lib_android
env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- name: Package AARs
run: |
mkdir -p dist
V="${{ steps.ver.outputs.version }}"
cp libbox.aar "dist/libbox-$V.aar"
cp libbox-legacy.aar "dist/libbox-legacy-$V.aar"
- uses: actions/upload-artifact@v4
with:
name: dist-android
path: dist/*
if-no-files-found: error
release:
name: publish release
needs: [build, build_linux_musl, build_android]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Resolve tag
id: ver
run: |
TAG="${{ github.event.inputs.tag || github.ref_name }}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
- name: Checksums
run: (cd dist && sha256sum * > SHA256SUMS && cat SHA256SUMS)
- name: Release notes
run: |
cat > notes.md <<EOF
**sing-box-lx ${{ steps.ver.outputs.version }}** — a thin downstream of [sing-box](https://github.com/SagerNet/sing-box) (base **v1.13.13**) adding two client-side features.
### Features
- **AmneziaWG 2.0** (\`with_awg\`) — live-validated against a real AWG2 server (handshake + keepalive + outbound traffic). Config: a \`wireguard\` endpoint with \`jc/jmin/jmax\`, \`s1\`–\`s4\`, \`h1\`–\`h4\`, \`i1\`–\`i5\`.
- **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 **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.
### Build tags
Desktop binary: \`$(make -f Makefile.lx -s lx-print-tags)\`
Config reference: [docs/lx-config.md](https://github.com/Leadaxe/sing-box-lx/blob/lx/docs/lx-config.md).
EOF
- name: Publish
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${{ steps.ver.outputs.tag }}" dist/* \
--title "sing-box-lx ${{ steps.ver.outputs.version }}" \
--notes-file notes.md \
--target "$GITHUB_SHA"