Files
sing-box-rr-edition/build-apk.sh
T
2026-09-28 10:48:45 +03:00

522 lines
15 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$SCRIPT_DIR/sing-box-rr"
GO_BIN="${GO_BIN:-/usr/local/go/bin/go}"
GO_CACHE_DIR="${GO_CACHE_DIR:-/tmp/sing-box-rr-go-build}"
GO_MODCACHE_DIR="${GO_MODCACHE_DIR:-/tmp/sing-box-rr-go-mod}"
GO_PROXY_VALUE="${GOPROXY:-https://proxy.golang.org|direct}"
LD_LLD_BIN="${LD_LLD_BIN:-}"
UPX_BIN="${UPX_BIN:-upx}"
ENABLE_UPX="${ENABLE_UPX:-0}"
SDK_ARM64="/home/fresa/sdk/openwrt-sdk-25.12.5-mediatek-filogic_gcc-14.3.0_musl.Linux-x86_64"
SDK_X86="/home/fresa/sdk/openwrt-sdk-25.12.5-x86-64_gcc-14.3.0_musl.Linux-x86_64"
PACKAGES_DIR="$SCRIPT_DIR/packages/25.12"
PKG_NAME="sing-box-rr"
FORCE_REBUILD=0
LOG_DIR="$SCRIPT_DIR/build-logs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/build-apk-$(date +%Y%m%d-%H%M%S).log"
LOG_LATEST="$LOG_DIR/build-apk-latest.log"
: > "$LOG_FILE"
ln -sf "$(basename "$LOG_FILE")" "$LOG_LATEST"
ls -1t "$LOG_DIR"/build-apk-*.log 2>/dev/null | tail -n +11 | xargs -r rm -f
echo "=== Logging to $LOG_FILE ==="
run() { "$@" 2>&1 | tee -a "$LOG_FILE"; return "${PIPESTATUS[0]}"; }
project_version() {
if [ -f "$SCRIPT_DIR/VERSION" ]; then
cat "$SCRIPT_DIR/VERSION"
else
echo "0.0.0-unknown-r1"
fi
}
project_source_date_epoch() {
if [ -f "$SCRIPT_DIR/SOURCE_DATE_EPOCH" ]; then
cat "$SCRIPT_DIR/SOURCE_DATE_EPOCH"
else
date +%s
fi
}
project_version_base() {
project_version | sed -E 's/-r[0-9]+$//'
}
package_release() {
local release
release="$(project_version | sed -nE 's/^.*-r([0-9]+)$/\1/p' | head -n1)"
if [ -n "$release" ]; then
echo "$release"
else
echo "1"
fi
}
apk_version() {
local base version_tail apk_suffix
base="$(project_version_base | sed -nE 's/^([0-9]+\.[0-9]+\.[0-9]+).*$/\1/p' | head -n1)"
[ -n "$base" ] || base="0.0.0"
apk_suffix="$(project_version_base | awk -F- '{
out = ""
for (i = 2; i <= NF; i++) {
if ($i ~ /^[0-9]+([.][0-9]+)*$/) {
if (out != "") {
out = out "."
}
out = out $i
}
}
print out
}')"
if [ -n "$apk_suffix" ]; then
echo "${base}.${apk_suffix}-r$(package_release)"
else
echo "${base}-r$(package_release)"
fi
}
build_tags() {
tr -d '\n' < "$PROJECT_DIR/release/DEFAULT_BUILD_TAGS_OTHERS"
}
has_build_tag() {
printf '%s' "$(build_tags)" | tr ',' '\n' | grep -qx "$1"
}
uses_musl_toolchain() {
has_build_tag "with_musl"
}
prefers_lld_linker() {
has_build_tag "with_naive_outbound"
}
goarch_to_sdk_cc_name() {
case "$1" in
arm64) echo "aarch64-openwrt-linux-musl-gcc" ;;
amd64) echo "x86_64-openwrt-linux-musl-gcc" ;;
*) return 1 ;;
esac
}
goarch_to_sdk_cxx_name() {
case "$1" in
arm64) echo "aarch64-openwrt-linux-musl-g++" ;;
amd64) echo "x86_64-openwrt-linux-musl-g++" ;;
*) return 1 ;;
esac
}
find_toolchain_binary() {
local sdk_dir="$1" binary_name="$2"
find "$sdk_dir/staging_dir" -type f -path "*/bin/$binary_name" | head -n1
}
goarch_to_sdk_ld_bfd_name() {
case "$1" in
arm64) echo "aarch64-openwrt-linux-musl-ld.bfd" ;;
amd64) echo "x86_64-openwrt-linux-musl-ld.bfd" ;;
*) return 1 ;;
esac
}
goarch_to_sdk_ld_name() {
case "$1" in
arm64) echo "aarch64-openwrt-linux-musl-ld" ;;
amd64) echo "x86_64-openwrt-linux-musl-ld" ;;
*) return 1 ;;
esac
}
find_linker_binary() {
local sdk_dir="$1" goarch="$2"
local ld_bfd_name ld_name ld_bin
if [ -n "$LD_LLD_BIN" ]; then
if [ -x "$LD_LLD_BIN" ]; then
echo "$LD_LLD_BIN"
return 0
fi
echo "configured LD_LLD_BIN is not executable: $LD_LLD_BIN" | tee -a "$LOG_FILE"
exit 1
fi
ld_bfd_name="$(goarch_to_sdk_ld_bfd_name "$goarch")" || return 1
ld_name="$(goarch_to_sdk_ld_name "$goarch")" || return 1
if prefers_lld_linker; then
ld_bin="$(find_lld_binary "$sdk_dir")"
if [ -n "$ld_bin" ]; then
echo "$ld_bin"
return 0
fi
else
ld_bin="$(find_toolchain_binary "$sdk_dir" "$ld_bfd_name")"
if [ -n "$ld_bin" ]; then
echo "$ld_bin"
return 0
fi
ld_bin="$(find_toolchain_binary "$sdk_dir" "$ld_name")"
if [ -n "$ld_bin" ]; then
echo "$ld_bin"
return 0
fi
fi
if ! prefers_lld_linker; then
find_lld_binary "$sdk_dir"
return 0
fi
ld_bin="$(find_toolchain_binary "$sdk_dir" "$ld_bfd_name")"
if [ -n "$ld_bin" ]; then
echo "$ld_bin"
return 0
fi
find_toolchain_binary "$sdk_dir" "$ld_name"
}
find_lld_binary() {
local sdk_dir="$1"
local lld_bin
lld_bin="$(find_external_lld_binary)"
if [ -n "$lld_bin" ]; then
echo "$lld_bin"
return 0
fi
lld_bin="$(find "$sdk_dir/staging_dir/host" -maxdepth 3 -name 'ld.lld' | head -n1)"
if [ -n "$lld_bin" ]; then
echo "$lld_bin"
return 0
fi
find "$sdk_dir/staging_dir/host" -maxdepth 3 -type f -name 'lld' | head -n1
}
find_external_lld_binary() {
local candidate
for candidate in /tmp/lld*-download/pkg/usr/lib/llvm-*/bin/ld.lld /tmp/lld*-download/pkg/usr/lib/llvm-*/bin/lld; do
if [ -x "$candidate" ]; then
echo "$candidate"
return 0
fi
done
return 1
}
prepare_musl_toolchain() {
local sdk_dir="$1" goarch="$2" toolchain_dir="$3"
local cc_name cxx_name cc_bin cxx_bin ld_bin staging_dir
staging_dir="$sdk_dir/staging_dir"
cc_name="$(goarch_to_sdk_cc_name "$goarch")" || { echo "unsupported GOARCH for musl toolchain: $goarch" | tee -a "$LOG_FILE"; exit 1; }
cxx_name="$(goarch_to_sdk_cxx_name "$goarch")" || { echo "unsupported GOARCH for musl toolchain: $goarch" | tee -a "$LOG_FILE"; exit 1; }
cc_bin="$(find_toolchain_binary "$sdk_dir" "$cc_name")"
cxx_bin="$(find_toolchain_binary "$sdk_dir" "$cxx_name")"
ld_bin="$(find_linker_binary "$sdk_dir" "$goarch")"
[ -n "$cc_bin" ] || { echo "$cc_name not found under $staging_dir" | tee -a "$LOG_FILE"; exit 1; }
[ -n "$cxx_bin" ] || { echo "$cxx_name not found under $staging_dir" | tee -a "$LOG_FILE"; exit 1; }
[ -n "$ld_bin" ] || { echo "no suitable linker found under $sdk_dir/staging_dir" | tee -a "$LOG_FILE"; exit 1; }
mkdir -p "$toolchain_dir"
cat > "$toolchain_dir/ld" <<EOF
#!/bin/sh
exec '$ld_bin' "\$@"
EOF
cat > "$toolchain_dir/cc" <<EOF
#!/bin/sh
export STAGING_DIR='$staging_dir'
exec '$cc_bin' -B'$toolchain_dir' "\$@"
EOF
cat > "$toolchain_dir/cxx" <<EOF
#!/bin/sh
export STAGING_DIR='$staging_dir'
exec '$cxx_bin' -B'$toolchain_dir' "\$@"
EOF
chmod +x "$toolchain_dir/ld" "$toolchain_dir/cc" "$toolchain_dir/cxx"
}
build_ldflags() {
local base_ldflags
base_ldflags="$(tr -d '\n' < "$PROJECT_DIR/release/LDFLAGS")"
echo "${base_ldflags} -X github.com/sagernet/sing-box/constant.Version=$(project_version) -s -w"
}
go_toolchain_value() {
local required_version
if [ -n "${GOTOOLCHAIN:-}" ]; then
printf '%s\n' "$GOTOOLCHAIN"
return 0
fi
required_version="$(sed -nE 's/^go ([0-9]+([.][0-9]+){1,2}).*$/\1/p' "$PROJECT_DIR/go.mod" | head -n1)"
if [ -n "$required_version" ]; then
printf 'go%s+auto\n' "$required_version"
else
printf 'auto\n'
fi
}
require_tools() {
[ -x "$GO_BIN" ] || { echo "go not found at $GO_BIN" | tee -a "$LOG_FILE"; exit 1; }
if [ "$ENABLE_UPX" = 1 ]; then
command -v "$UPX_BIN" >/dev/null || { echo "upx not found: $UPX_BIN" | tee -a "$LOG_FILE"; exit 1; }
fi
}
reset_go_cache_if_needed() {
if [ "$FORCE_REBUILD" = 1 ]; then
chmod -R u+w "$GO_CACHE_DIR" "$GO_MODCACHE_DIR" 2>/dev/null || true
rm -rf "$GO_CACHE_DIR" "$GO_MODCACHE_DIR"
fi
mkdir -p "$GO_CACHE_DIR" "$GO_MODCACHE_DIR"
}
build_binary() {
local goarch="$1" output="$2" sdk_dir="$3" toolchain_dir="$4"
local tags ldflags gotoolchain
local -a env_cmd
tags="$(build_tags)"
ldflags="$(build_ldflags)"
gotoolchain="$(go_toolchain_value)"
env_cmd=(
env
"PATH=$(dirname "$GO_BIN"):$PATH"
"GOTOOLCHAIN=$gotoolchain"
"GOCACHE=$GO_CACHE_DIR"
"GOMODCACHE=$GO_MODCACHE_DIR"
"GOPROXY=$GO_PROXY_VALUE"
"GOOS=linux"
"GOARCH=$goarch"
)
if uses_musl_toolchain; then
prepare_musl_toolchain "$sdk_dir" "$goarch" "$toolchain_dir"
env_cmd+=(
"CGO_ENABLED=1"
"CC=$toolchain_dir/cc"
"CXX=$toolchain_dir/cxx"
)
else
env_cmd+=("CGO_ENABLED=0")
fi
if [ "$goarch" = "amd64" ]; then
env_cmd+=("GOAMD64=v1")
fi
echo "=== Building sing-box for GOARCH=$goarch ===" | tee -a "$LOG_FILE"
(
cd "$PROJECT_DIR"
run "${env_cmd[@]}" "$GO_BIN" build \
-buildvcs=false \
-trimpath \
-tags "$tags" \
-ldflags "$ldflags" \
-o "$output" \
./cmd/sing-box
)
if [ "$ENABLE_UPX" = 1 ]; then
# Experimental UPX compression for evaluating flash savings on routers.
echo "=== Experimental UPX compression (default) ===" | tee -a "$LOG_FILE"
run "$UPX_BIN" "$output"
fi
}
stage_common_tree() {
local root_dir="$1" binary_path="$2"
install -d "$root_dir/usr/bin" \
"$root_dir/usr/share/sing-box" \
"$root_dir/etc/sing-box" \
"$root_dir/etc/config" \
"$root_dir/etc/init.d" \
"$root_dir/lib/upgrade/keep.d" \
"$root_dir/lib/apk/packages"
install -m755 "$binary_path" "$root_dir/usr/bin/sing-box"
install -m644 "$PROJECT_DIR/release/config/config.json" "$root_dir/etc/sing-box/config.json"
install -m644 "$PROJECT_DIR/release/config/openwrt.conf" "$root_dir/etc/config/sing-box"
install -m755 "$PROJECT_DIR/release/config/openwrt.init" "$root_dir/etc/init.d/sing-box"
install -m644 "$PROJECT_DIR/release/config/openwrt.keep" "$root_dir/lib/upgrade/keep.d/sing-box"
}
stage_apk_metadata() {
local root_dir="$1"
local packages_meta="$root_dir/lib/apk/packages"
local list_file="$packages_meta/${PKG_NAME}.list"
local tmp_list_file
local conffiles_file="$packages_meta/${PKG_NAME}.conffiles"
local conffiles_static_file="$packages_meta/${PKG_NAME}.conffiles_static"
echo "sing-box=5566:sing-box=5566" > "$packages_meta/${PKG_NAME}.rusers"
tmp_list_file="$(mktemp /tmp/sing-box-rr-apk-list.XXXXXX)"
(
cd "$root_dir"
find . \( -type f -o -type l \) | sed 's|^\./|/|' | sort > "$tmp_list_file"
)
mv "$tmp_list_file" "$list_file"
cat > "$conffiles_file" <<'EOF'
/etc/config/sing-box
/etc/sing-box/config.json
EOF
while IFS= read -r conffile; do
[ -f "$root_dir$conffile" ] || continue
printf '%s %s\n' "$conffile" "$(sha256sum "$root_dir$conffile" | cut -d' ' -f1)"
done < "$conffiles_file" > "$conffiles_static_file"
}
prepare_apk_root() {
local apk_root_dir="$1"
mkdir -p "$apk_root_dir/etc"
cat > "$apk_root_dir/etc/passwd" <<'EOF'
root:x:0:0:root:/root:/sbin/nologin
EOF
cat > "$apk_root_dir/etc/group" <<'EOF'
root:x:0:root
EOF
}
package_apk() {
local pkg_root="$1" sdk_dir="$2" pkg_arch="$3" out_dir="$4"
local apk_bin apk_root_dir fakeroot_bin output_file source_epoch
apk_bin="$sdk_dir/staging_dir/host/bin/apk"
fakeroot_bin="$sdk_dir/staging_dir/host/bin/fakeroot"
[ -x "$apk_bin" ] || {
echo "apk host tool not found at $apk_bin" | tee -a "$LOG_FILE"
exit 1
}
[ -x "$fakeroot_bin" ] || {
echo "fakeroot host tool not found at $fakeroot_bin" | tee -a "$LOG_FILE"
exit 1
}
mkdir -p "$out_dir"
rm -f "$out_dir"/${PKG_NAME}-*.apk
apk_root_dir="$(mktemp -d /tmp/sing-box-rr-apk-root.XXXXXX)"
output_file="$out_dir/${PKG_NAME}-$(apk_version).apk"
source_epoch="$(project_source_date_epoch)"
prepare_apk_root "$apk_root_dir"
run env \
"SOURCE_DATE_EPOCH=$source_epoch" \
"STAGING_DIR_HOST=$sdk_dir/staging_dir/host" \
"$fakeroot_bin" sh -c '
set -e
pkg_dir="$1"
apk_root="$2"
apk_tool="$3"
output="$4"
arch="$5"
version="$6"
script="$7"
chown -R 0:0 "$pkg_dir"
"$apk_tool" --root "$apk_root" mkpkg \
--info "name:sing-box-rr" \
--info "version:$version" \
--info "description:RR Edition trimmed network-only sing-box fork for OpenWrt." \
--info "arch:$arch" \
--info "license:GPL-3.0-or-later" \
--info "origin:sing-box-rr-edition" \
--info "url:https://git.my-handbook.ru/fresa/sing-box-rr-edition" \
--info "maintainer:RR Edition" \
--info "provides:sing-box sing-box-tiny" \
--info "depends:ca-bundle kmod-inet-diag kmod-tun" \
--script "pre-deinstall:$script" \
--files "$pkg_dir" \
--output "$output"
' sh "$pkg_root" "$apk_root_dir" "$apk_bin" "$output_file" "$pkg_arch" "$(apk_version)" "$PROJECT_DIR/release/config/openwrt.prerm"
rm -rf "$apk_root_dir"
}
build_target() {
local label="$1" goarch="$2" pkg_arch="$3" sdk_dir="$4" out_dir="$5"
local temp_dir binary_path pkg_root toolchain_dir
temp_dir="$(mktemp -d /tmp/sing-box-rr-apk.XXXXXX)"
trap 'rm -rf "$temp_dir"' RETURN
binary_path="$temp_dir/sing-box"
pkg_root="$temp_dir/pkg"
toolchain_dir="$temp_dir/toolchain"
echo "=== Building $PKG_NAME $label for OpenWrt 25.12 ===" | tee -a "$LOG_FILE"
build_binary "$goarch" "$binary_path" "$sdk_dir" "$toolchain_dir"
stage_common_tree "$pkg_root" "$binary_path"
stage_apk_metadata "$pkg_root"
package_apk "$pkg_root" "$sdk_dir" "$pkg_arch" "$out_dir"
run ls -lh "$out_dir"/${PKG_NAME}-*.apk
trap - RETURN
rm -rf "$temp_dir"
}
build_arm64() {
[ -d "$SDK_ARM64" ] || { echo "arm64 SDK not found at $SDK_ARM64" | tee -a "$LOG_FILE"; exit 1; }
build_target "arm64" "arm64" "aarch64_cortex-a53" "$SDK_ARM64" "$PACKAGES_DIR/arm64"
}
build_x86() {
[ -d "$SDK_X86" ] || { echo "x86_64 SDK not found at $SDK_X86" | tee -a "$LOG_FILE"; exit 1; }
build_target "x86_64" "amd64" "x86_64" "$SDK_X86" "$PACKAGES_DIR/x86_64"
}
for arg in "$@"; do
[ "$arg" = "--force" ] || [ "$arg" = "-f" ] && FORCE_REBUILD=1
done
args=()
for arg in "$@"; do
[ "$arg" != "--force" ] && [ "$arg" != "-f" ] && args+=("$arg")
done
set -- "${args[@]}"
require_tools
reset_go_cache_if_needed
case "${1:-arm64}" in
arm64)
build_arm64
;;
x86|x86_64)
build_x86
;;
all)
rm -rf "$PACKAGES_DIR/arm64" "$PACKAGES_DIR/x86_64" 2>/dev/null || true
build_arm64
build_x86
;;
*)
echo "Usage: $0 [arm64|x86|all] [--force|-f]"
echo " arm64 - build APK package for arm64 (default)"
echo " x86 - build APK package for x86_64"
echo " all - build APK packages for arm64 and x86_64"
echo ""
echo " --force|-f Rebuild with a clean Go cache"
exit 1
;;
esac
echo ""
echo "=== $PKG_NAME $(apk_version) ==="
ls -la "$PACKAGES_DIR"/arm64/*.apk "$PACKAGES_DIR"/x86_64/*.apk 2>/dev/null || true
echo ""
echo "=== Full log: $LOG_FILE ==="
ERRS=$(grep -E "error:|undefined reference|No such file|failed to build" "$LOG_FILE" \
| grep -v "ignored)" | head -20 || true)
if [ -n "$ERRS" ]; then
echo "=== Errors detected: ==="
echo "$ERRS"
fi