From 000268eac87a356e06cd8c6a19c22461fe98aed9 Mon Sep 17 00:00:00 2001 From: AcideFluorhydrique <139669368+AcideFluorhydrique@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:49:22 +0800 Subject: [PATCH 1/4] Install gomobile at the version go.mod requires main.yml installed gomobile@latest, so rebuilding a tag later picks up whatever golang.org/x/mobile has published since, and the generated binding code can change underneath an unchanged source tree. go.mod already requires a specific golang.org/x/mobile, kept current by the go get in tidy.yml; install that one. --- .github/workflows/main.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8ab3c474..256884a5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,10 @@ jobs: - name: Install gomobile run: | - go install golang.org/x/mobile/cmd/gomobile@latest + # Use the golang.org/x/mobile version go.mod already requires (tidy.yml + # updates it with each xray-core bump) rather than whatever @latest is + # at build time, so rebuilding a tag later uses the same gomobile. + go install golang.org/x/mobile/cmd/gomobile@$(go list -m -f '{{.Version}}' golang.org/x/mobile) export PATH=$PATH:~/go/bin - name: Setup Android SDK From f7fa7936a76d5fb799bf980c0e9d9e1f631f8bc8 Mon Sep 17 00:00:00 2001 From: AcideFluorhydrique <139669368+AcideFluorhydrique@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:49:54 +0800 Subject: [PATCH 2/4] Pin the JDK gomobile compiles the Java bindings with gomobile bind compiles the generated Java bindings into the aar's classes.jar with javac, so the JDK is an input to the aar. main.yml set none up, leaving javac to whatever the runner image defaults to, which changes when GitHub updates the image. Pin Temurin 17.0.20.1+1, the ubuntu-latest default today, so the JDK stays what it currently is. It has to be written as the Adoptium semver 17.0.20+101: setup-java checks preinstalled JDKs first, and the four-part 17.0.20.1 only matches the preinstalled folder by accident of its name, then matches nothing on Adoptium once the image moves on. Checked by running setup-java v6.0.1's version matching against the live Adoptium release list. --- .github/workflows/main.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 256884a5..d9821096 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,6 +48,18 @@ jobs: --install "ndk;29.0.14206865" echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/29.0.14206865" >> $GITHUB_ENV + # gomobile compiles the aar's Java bindings (classes.jar) with javac, so the + # JDK is part of the output. Without this the runner image's default JDK + # is used, which changes whenever GitHub updates the image. This is the + # Temurin 17.0.20.1+1 that ubuntu-latest defaults to today, spelled as the + # Adoptium semver setup-java matches ('17.0.20.1' would only match the + # preinstalled copy by accident, and stop matching once the image moves). + - name: Setup Java + uses: actions/setup-java@v6.0.0 + with: + distribution: 'temurin' + java-version: '17.0.20+101' + - name: Build run: | mkdir -p assets data From ccd733c06dd5284139542182ca5fea8b5f2f8814 Mon Sep 17 00:00:00 2001 From: AcideFluorhydrique <139669368+AcideFluorhydrique@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:57:02 +0800 Subject: [PATCH 3/4] Record the exact geo data each release embeds gen_assets.sh downloads geoip.dat and geosite.dat from the v2ray-rules-dat releases/latest redirect and geoip-only-cn-private.dat from the tip of the geoip release branch. Both move daily, and nothing records which data a given release actually embedded, so no release can be rebuilt later with the same assets and compared against the published aar. Keep downloading the newest data, so releases and the tidy.yml automation behave exactly as before, but resolve both sources to an exact release tag and commit first, download from those, and write them with the SHA-256 of each file to data/geo-assets.lock. main.yml publishes that, plus the exact Go release setup-go picked, as libv2ray-build.lock beside the aar. The lock is not copied into assets/, so the aar's contents are unchanged. A new `gen_assets.sh pinned ` downloads exactly what a lock names and fails on any checksum mismatch; the README describes rebuilding a release with it. Downloads now use curl -f, so an HTTP error fails the build instead of being saved as a .dat file. --- .github/workflows/main.yml | 13 ++++++ README.md | 12 ++++++ gen_assets.sh | 88 ++++++++++++++++++++++++++++++++++---- 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9821096..d441ae4d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,6 +68,11 @@ jobs: gomobile init go mod tidy gomobile bind -v -androidapi 24 -trimpath -ldflags='-s -w -buildid= -checklinkname=0' ./ + # What a rebuild of this release needs beyond the tagged source: the geo + # data resolved above, and the exact Go release setup-go picked for + # go.mod's `go` line. Published next to the aar. + { cat data/geo-assets.lock; echo "GO_VERSION='$(go env GOVERSION)'"; } > libv2ray-build.lock + cat libv2ray-build.lock - name: Upload build artifacts if: github.event.inputs.release_tag == '' @@ -76,6 +81,7 @@ jobs: name: libv2ray path: | ${{ github.workspace }}/libv2ray*r + ${{ github.workspace }}/libv2ray-build.lock - name: Upload AndroidLibXrayLite to release if: github.event.inputs.release_tag != '' @@ -84,3 +90,10 @@ jobs: file: ./libv2ray*r tag: ${{ github.event.inputs.release_tag }} file_glob: true + + - name: Upload build lock to release + if: github.event.inputs.release_tag != '' + uses: svenstaro/upload-release-action@v2 + with: + file: ./libv2ray-build.lock + tag: ${{ github.event.inputs.release_tag }} diff --git a/README.md b/README.md index 7b1e53ea..dedcfec6 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,15 @@ 2. `gomobile init` 3. `go mod tidy -v` 4. `gomobile bind -v -androidapi 24 -trimpath -ldflags='-s -w -buildid= -checklinkname=0' ./` + +## Reproducing a release +Each release publishes a `libv2ray-build.lock` next to `libv2ray.aar`, recording +the inputs its tagged source does not pin by itself: the Go release it was built +with, and the exact geo data embedded in `assets/`. To rebuild a release's aar +byte for byte: + +1. Check out the release tag, and use the JDK, NDK and Android SDK versions named in `.github/workflows/main.yml` +2. Install the Go release named by `GO_VERSION` in `libv2ray-build.lock` +3. `mkdir -p assets data && bash gen_assets.sh pinned libv2ray-build.lock && cp data/*.dat assets/` +4. `go install golang.org/x/mobile/cmd/gomobile@$(go list -m -f '{{.Version}}' golang.org/x/mobile)` +5. `gomobile init`, `go mod tidy -v`, then the `gomobile bind` command from step 4 above diff --git a/gen_assets.sh b/gen_assets.sh index bd076f90..45af722c 100644 --- a/gen_assets.sh +++ b/gen_assets.sh @@ -10,30 +10,101 @@ __file="${__dir}/$(basename "${BASH_SOURCE[0]}")" __base="$(basename "${__file}" .sh)" DATADIR="${__dir}/data" +# Written by every run: each source resolved to an exact release tag or commit, +# with the SHA-256 of what was downloaded. Published with the aar, it lets a +# release be rebuilt with the same data via `gen_assets.sh pinned `. +LOCKFILE="${DATADIR}/geo-assets.lock" + +RULES_REPO="Loyalsoldier/v2ray-rules-dat" +GEOIP_REPO="Loyalsoldier/geoip" # Check for required dependencies check_dependencies() { command -v jq >/dev/null 2>&1 || { echo >&2 "jq is required but it's not installed. Aborting."; exit 1; } command -v go >/dev/null 2>&1 || { echo >&2 "Go is required but it's not installed. Aborting."; exit 1; } + command -v git >/dev/null 2>&1 || { echo >&2 "git is required but it's not installed. Aborting."; exit 1; } } -# Download data function -download_dat() { +# The tag that github.com//releases/latest currently redirects to +latest_release_tag() { + curl -fsSI "https://github.com/$1/releases/latest" | awk -F/ 'tolower($0) ~ /^location:/ {print $NF}' | tr -d '\r' +} + +# The commit a branch currently points at +branch_commit() { + git ls-remote "https://github.com/$1.git" "refs/heads/$2" | cut -f1 +} + +sha256() { + if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1"; else shasum -a 256 "$1"; fi | cut -d' ' -f1 +} + +# fetch [expected sha256]: download, verify if a checksum is +# given, and print the file's checksum +fetch() { + curl -fsSL "$1" -o "$2" + local sum + sum=$(sha256 "$2") + if [[ -n "${3:-}" && "$sum" != "$3" ]]; then + echo >&2 "Checksum mismatch for $2: expected $3, got $sum" + exit 1 + fi + echo "$sum" +} + +# Download from the sources named by RULES_DAT_TAG and GEOIP_RELEASE_COMMIT, +# verify against any *_SHA256 already set, and record what was fetched +fetch_dat() { if [[ ! -d "$DATADIR" ]]; then echo "Downloading failed \"$DATADIR\" does not exists" exit 1 fi - echo "Downloading geoip.dat..." - curl -sL https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat -o "$DATADIR/geoip.dat" + echo "Downloading geoip.dat from $RULES_REPO $RULES_DAT_TAG..." + GEOIP_DAT_SHA256=$(fetch "https://github.com/$RULES_REPO/releases/download/$RULES_DAT_TAG/geoip.dat" "$DATADIR/geoip.dat" "${GEOIP_DAT_SHA256:-}") + + echo "Downloading geosite.dat from $RULES_REPO $RULES_DAT_TAG..." + GEOSITE_DAT_SHA256=$(fetch "https://github.com/$RULES_REPO/releases/download/$RULES_DAT_TAG/geosite.dat" "$DATADIR/geosite.dat" "${GEOSITE_DAT_SHA256:-}") + + echo "Downloading geoip-only-cn-private.dat from $GEOIP_REPO $GEOIP_RELEASE_COMMIT..." + GEOIP_ONLY_CN_PRIVATE_DAT_SHA256=$(fetch "https://raw.githubusercontent.com/$GEOIP_REPO/$GEOIP_RELEASE_COMMIT/geoip-only-cn-private.dat" "$DATADIR/geoip-only-cn-private.dat" "${GEOIP_ONLY_CN_PRIVATE_DAT_SHA256:-}") - echo "Downloading geosite.dat..." - curl -sL https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat -o "$DATADIR/geosite.dat" + cat > "$LOCKFILE" <&2 "Could not resolve the latest geo data sources. Aborting." + exit 1 + fi + fetch_dat +} + +# Exactly the data recorded in a lock file, for rebuilding a release +pinned_dat() { + local lock="${1:-}" + if [[ -z "$lock" ]]; then + echo >&2 "Usage: gen_assets.sh pinned " + exit 1 + fi + # `.` searches PATH for a bare file name; make sure the given file is read + [[ "$lock" == */* ]] || lock="./$lock" + # shellcheck source=/dev/null + . "$lock" + fetch_dat } # Main execution logic @@ -43,5 +114,6 @@ check_dependencies case $ACTION in "download") download_dat ;; + "pinned") pinned_dat "${2:-}" ;; *) echo "Invalid action: $ACTION" ; exit 1 ;; esac From 5bdba4d665487f4068033da36b5f599d3610d15d Mon Sep 17 00:00:00 2001 From: AcideFluorhydrique <139669368+AcideFluorhydrique@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:18:18 +0800 Subject: [PATCH 4/4] Print the checksums of the built aar in the build log Comparing a rebuild against a release meant downloading the release assets first just to hash them. Print the SHA-256 of libv2ray.aar and the sources jar at the end of the build, next to the build lock, so both the inputs and the outputs of a run can be read straight from its log. --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d441ae4d..cea437fa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -73,6 +73,8 @@ jobs: # go.mod's `go` line. Published next to the aar. { cat data/geo-assets.lock; echo "GO_VERSION='$(go env GOVERSION)'"; } > libv2ray-build.lock cat libv2ray-build.lock + # Checksums of what this run built, to compare a rebuild against + sha256sum libv2ray*r - name: Upload build artifacts if: github.event.inputs.release_tag == ''