diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8ab3c474..cea437fa 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 @@ -45,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 @@ -53,6 +68,13 @@ 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 + # Checksums of what this run built, to compare a rebuild against + sha256sum libv2ray*r - name: Upload build artifacts if: github.event.inputs.release_tag == '' @@ -61,6 +83,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 != '' @@ -69,3 +92,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