From cf4e72fa012cd8d0e29f49b6f954fec2688722e3 Mon Sep 17 00:00:00 2001 From: Horizon <64575533+BlueGradientHorizon@users.noreply.github.com> Date: Sat, 25 Apr 2026 19:53:15 +0300 Subject: [PATCH] fix(build): port build scripts to Python to allow Windows contributors to build subprojects (#83) * Rewrite build-version and all build-zip bash scripts to python * Add executable permissions to python build scripts * Use python build script for kmod in CI * Fix * Enhance kmod build script, add/fix docs, CI edits * Delete remaining build-zip bash scripts * Delete remaining build-zip bash scripts --- .github/workflows/ci.yml | 35 +--- README.en.md | 26 ++- README.md | 26 ++- docs/development.md | 2 +- docs/releasing.md | 4 +- kmod/BUILDING.md | 29 +-- kmod/Makefile | 3 + kmod/README.md | 2 +- kmod/build-zip.py | 166 ++++++++++++++++++ kmod/build-zip.sh | 31 ---- lsposed/app/build.gradle.kts | 12 +- .../okhsunrog/vpnhide/UpdateCheckerTest.kt | 2 +- portshide/build-zip.py | 65 +++++++ portshide/build-zip.sh | 30 ---- scripts/build-version.py | 24 +++ scripts/build-version.sh | 20 --- scripts/build_lib.py | 50 ++++++ zygisk/README.md | 4 +- zygisk/build-zip.py | 102 +++++++++++ zygisk/build-zip.sh | 63 ------- 20 files changed, 490 insertions(+), 206 deletions(-) create mode 100755 kmod/build-zip.py delete mode 100755 kmod/build-zip.sh create mode 100755 portshide/build-zip.py delete mode 100755 portshide/build-zip.sh create mode 100755 scripts/build-version.py delete mode 100755 scripts/build-version.sh create mode 100644 scripts/build_lib.py create mode 100755 zygisk/build-zip.py delete mode 100755 zygisk/build-zip.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0040f9f..133c739 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,38 +77,20 @@ jobs: container: image: ghcr.io/ylarod/ddk-min:${{ matrix.kmi }}-20260313 env: - KDIR: /opt/ddk/kdir/${{ matrix.kmi }} + KMI: ${{ matrix.kmi }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Build kernel module - run: | - CLANG=$(echo /opt/ddk/clang/clang-r*/bin) - make -C $KDIR M=$GITHUB_WORKSPACE/kmod \ - ARCH=arm64 LLVM=1 LLVM_IAS=1 \ - CC=$CLANG/clang LD=$CLANG/ld.lld \ - AR=$CLANG/llvm-ar NM=$CLANG/llvm-nm \ - OBJCOPY=$CLANG/llvm-objcopy \ - OBJDUMP=$CLANG/llvm-objdump \ - STRIP=$CLANG/llvm-strip \ - CROSS_COMPILE=aarch64-linux-gnu- \ - modules - $CLANG/llvm-strip -d kmod/vpnhide_kmod.ko + - name: Mark workspace safe + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: Package KSU module zip + - name: Build and package kernel module run: | - apt-get update -qq && apt-get install -y -qq zip git >/dev/null - git config --global --add safe.directory "$GITHUB_WORKSPACE" - BUILD_VERSION=$(./scripts/build-version.sh) - echo "Stamping kmod module.prop version=v${BUILD_VERSION} gkiVariant=${{ matrix.kmi }}" - cp kmod/vpnhide_kmod.ko kmod/module/ - sed -i "s|^version=.*|version=v${BUILD_VERSION}|" kmod/module/module.prop - echo "gkiVariant=${{ matrix.kmi }}" >> kmod/module/module.prop - echo "updateJson=https://raw.githubusercontent.com/okhsunrog/vpnhide/main/update-json/update-kmod-${{ matrix.kmi }}.json" >> kmod/module/module.prop - (cd kmod/module && zip -qr "$GITHUB_WORKSPACE/vpnhide-kmod-${{ matrix.kmi }}.zip" .) + cd kmod + python3 ./build-zip.py --kmi $KMI - name: Upload artifact uses: actions/upload-artifact@v7 @@ -150,7 +132,7 @@ jobs: UPDATE_JSON_URL: https://raw.githubusercontent.com/okhsunrog/vpnhide/main/update-json/update-zygisk.json run: | cd zygisk - ./build-zip.sh + python3 ./build-zip.py - name: Upload artifact uses: actions/upload-artifact@v7 @@ -225,9 +207,8 @@ jobs: env: UPDATE_JSON_URL: https://raw.githubusercontent.com/okhsunrog/vpnhide/main/update-json/update-ports.json run: | - sudo apt-get update -qq && sudo apt-get install -y -qq zip >/dev/null cd portshide - ./build-zip.sh + python3 ./build-zip.py mv vpnhide-ports.zip "$GITHUB_WORKSPACE/vpnhide-ports.zip" - name: Upload artifact diff --git a/README.en.md b/README.en.md index 3f73138..da324de 100644 --- a/README.en.md +++ b/README.en.md @@ -194,10 +194,32 @@ Rows 1-6, 21, and 24 are the only vectors reachable by regular apps. Everything ## Building from source -- **kmod**: `cd kmod && make && ./build-zip.sh` — see [kmod/BUILDING.md](kmod/BUILDING.md) -- **zygisk**: `cd zygisk && ./build-zip.sh` (Rust + NDK + cargo-ndk) +- **kmod**: `cd kmod && make && ./build-zip.py` — see [kmod/BUILDING.md](kmod/BUILDING.md) +- **zygisk**: `cd zygisk && ./build-zip.py` (Rust + NDK + cargo-ndk) - **lsposed**: `cd lsposed && ./gradlew assembleDebug` (JDK 17 + Rust + NDK + cargo-ndk) +### Notes for contributors stuck on Windows + +If you're on Windows, there are some inconveniences with building some subprojects. + +**lsposed**: builds fine in Android Studio. + +**portshide**: `cd .\portshide\; python .\build-zip.py` runs fine. + +For the next two, you'll (unfortunately) need to install [Docker for Windows](https://docs.docker.com/desktop/setup/install/windows-install/). + +**kmod**: +```powershell +$env:KMI="android12-5.10"; docker run --rm -it -v "${PWD}:/workspace" -e KMI=$env:KMI -w /workspace "ghcr.io/ylarod/ddk-min:$($env:KMI)-20260313" bash -c 'cd kmod && python3 ./build-zip.py --kmi $KMI' +``` +Be sure to use the same version of `ylarod/ddk-min` image (the date after KMI name) as used in the `ci.yml` workflow file. + +**zygisk**: +```powershell +docker run --rm -it -v "${PWD}:/workspace" -v "vpnhide_cargo_cache:/usr/local/cargo/registry" -w /workspace ghcr.io/okhsunrog/vpnhide/ci:latest bash -c 'cd zygisk && python3 ./build-zip.py' +``` +The reason why `zygisk` can't be built directly is because source code of dependency `zygisk-api` contains a file named `aux.rs`. Cargo uses `libgit2` for git operations and it contains a guard, which forbids creating files _containing_ reserved Windows words. You'll get an error: `cannot checkout to invalid path 'src/aux.rs'; class=Checkout (20)`. [Someone reports](https://superuser.com/a/1929659), that it bacame possible to create files containing reserved words **with** an extension after some update, but it seems such behavior wasn't modified in `libgit2`. + ## Verified against - [RKNHardering](https://github.com/xtclovver/RKNHardering/) — all detection vectors clean diff --git a/README.md b/README.md index edf41b7..091370f 100644 --- a/README.md +++ b/README.md @@ -194,10 +194,32 @@ vpnhide — это не один переключатель, а три разн ## Сборка из исходников -- **kmod**: `cd kmod && make && ./build-zip.sh` — см. [kmod/BUILDING.md](kmod/BUILDING.md) -- **zygisk**: `cd zygisk && ./build-zip.sh` (Rust + NDK + cargo-ndk) +- **kmod**: `cd kmod && make && ./build-zip.py` — см. [kmod/BUILDING.md](kmod/BUILDING.md) +- **zygisk**: `cd zygisk && ./build-zip.py` (Rust + NDK + cargo-ndk) - **lsposed**: `cd lsposed && ./gradlew assembleDebug` (JDK 17 + Rust + NDK + cargo-ndk) +### Заметки для контрибьюторов, застрявших на Windows + +Если вы используете Windows, при сборке некоторых подпроектов возникают определенные неудобства. + +**lsposed**: отлично собирается в Android Studio. + +**portshide**: `cd .\portshide\; python .\build-zip.py` выполняется без проблем. + +Для следующих двух вам (к сожалению) потребуется установить [Docker for Windows](https://docs.docker.com/desktop/setup/install/windows-install/). + +**kmod**: +```powershell +$env:KMI="android12-5.10"; docker run --rm -it -v "${PWD}:/workspace" -e KMI=$env:KMI -w /workspace "ghcr.io/ylarod/ddk-min:$($env:KMI)-20260313" bash -c 'cd kmod && python3 ./build-zip.py --kmi $KMI' +``` +Обязательно используйте ту же версию образа `ylarod/ddk-min` (дата после названия KMI), которая используется в `ci.yml`. + +**zygisk**: +```powershell +docker run --rm -it -v "${PWD}:/workspace" -v "vpnhide_cargo_cache:/usr/local/cargo/registry" -w /workspace ghcr.io/okhsunrog/vpnhide/ci:latest bash -c 'cd zygisk && python3 ./build-zip.py' +``` +Причина, по которой `zygisk` нельзя собрать напрямую, заключается в том, что исходный код зависимости `zygisk-api` содержит файл с именем `aux.rs`. Cargo использует `libgit2` для работы с git, в котором есть защита, запрещающая создавать файлы, _содержащие_ зарезервированные слова Windows. Вы получите ошибку: `cannot checkout to invalid path 'src/aux.rs'; class=Checkout (20)`. [Сообщают](https://superuser.com/a/1929659), что после какого-то обновления стало возможным создавать файлы, содержащие зарезервированные слова, **с** расширением, но, похоже, в `libgit2` это поведение не было изменено. + ## Проверено на - [RKNHardering](https://github.com/xtclovver/RKNHardering/) — все векторы обнаружения чисты diff --git a/docs/development.md b/docs/development.md index 18c9fd3..0df8ff5 100644 --- a/docs/development.md +++ b/docs/development.md @@ -56,7 +56,7 @@ keytool -genkey -v -keystore ~/vpnhide.jks \ ### zygisk module ```sh -cd zygisk && ./build-zip.sh +cd zygisk && ./build-zip.py # → zygisk/target/vpnhide-zygisk.zip ``` diff --git a/docs/releasing.md b/docs/releasing.md index 5b80202..365f4a4 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -48,7 +48,7 @@ Update-json **must** be committed *after* the GitHub release is **published** (i ## Build versions -Every packaging step runs `./scripts/build-version.sh` to compute the version string stamped into the artifact: +Every packaging step runs `./scripts/build-version.py` to compute the version string stamped into the artifact: - **On a release tag `vX.Y.Z`:** `X.Y.Z` - **N commits after the nearest tag:** `X.Y.Z-N-gSHA` (the git describe format) @@ -61,6 +61,6 @@ This string goes into: - APK `versionName` (visible in Android Settings → Apps, diagnostic debug zip, `BuildConfig.VERSION_NAME`) - Inside the zip filenames (only for release tags; dev artifacts in CI keep a stable name) -The committed `module.prop` files are **not** modified — `build-zip.sh` stages a copy, patches the version there, and zips. `lsposed/app/build.gradle.kts` evaluates `build-version.sh` at configure time and sets `versionName` dynamically. +The committed `module.prop` files are **not** modified — `build-zip.py` stages a copy, patches the version there, and zips. `lsposed/app/build.gradle.kts` evaluates `build-version.py` at configure time and sets `versionName` dynamically. `versionCode` stays at the value baked in by the last `release.py` run (monotonically increasing integer required by Android/Magisk). diff --git a/kmod/BUILDING.md b/kmod/BUILDING.md index 0885989..8e65139 100644 --- a/kmod/BUILDING.md +++ b/kmod/BUILDING.md @@ -15,16 +15,9 @@ KMI=android14-6.1 # Build the kernel module (run from repo root) docker run --rm -v $(pwd)/kmod:/work \ ghcr.io/ylarod/ddk-min:${KMI}-20260313 sh -c " - CLANG=\$(echo /opt/ddk/clang/clang-r*/bin) && \ - make -C /opt/ddk/kdir/${KMI} M=/work \ - ARCH=arm64 LLVM=1 LLVM_IAS=1 \ - CC=\$CLANG/clang LD=\$CLANG/ld.lld \ - AR=\$CLANG/llvm-ar NM=\$CLANG/llvm-nm \ - OBJCOPY=\$CLANG/llvm-objcopy \ - OBJDUMP=\$CLANG/llvm-objdump \ - STRIP=\$CLANG/llvm-strip \ - CROSS_COMPILE=aarch64-linux-gnu- \ - modules" + CLANG_DIR=\$(echo /opt/ddk/clang/clang-r*/bin) \ + KERNEL_SRC=/opt/ddk/kdir/${KMI} \ + make" # Package as KSU module cp kmod/vpnhide_kmod.ko kmod/module/ @@ -43,17 +36,11 @@ KMI=android14-6.1 podman run --rm --userns=keep-id -v "$(pwd)/kmod:/work:Z" \ ghcr.io/ylarod/ddk-min:${KMI}-20260313 sh -c ' - CLANG=$(echo /opt/ddk/clang/clang-r*/bin) && \ - make -C /opt/ddk/kdir/'${KMI}' M=/work \ - ARCH=arm64 LLVM=1 LLVM_IAS=1 \ - CC=$CLANG/clang LD=$CLANG/ld.lld \ - AR=$CLANG/llvm-ar NM=$CLANG/llvm-nm \ - OBJCOPY=$CLANG/llvm-objcopy \ - OBJDUMP=$CLANG/llvm-objdump \ - STRIP=$CLANG/llvm-strip \ - CROSS_COMPILE=aarch64-linux-gnu- \ - modules' + CLANG_DIR=\$(echo /opt/ddk/clang/clang-r*/bin) \ + KERNEL_SRC=/opt/ddk/kdir/${KMI} \ + make' +# Package as KSU module cp kmod/vpnhide_kmod.ko kmod/module/ (cd kmod/module && zip -qr ../../vpnhide-kmod.zip .) ``` @@ -70,7 +57,7 @@ cp .env.example .env # Edit .env with paths to your kernel source and clang toolchain direnv allow make -./build-zip.sh +./build-zip.py ``` See `.env.example` for the required variables. You need a prepared kernel source tree with headers and `Module.symvers`. diff --git a/kmod/Makefile b/kmod/Makefile index 5185e4c..d70c029 100644 --- a/kmod/Makefile +++ b/kmod/Makefile @@ -29,6 +29,9 @@ MAKE_ARGS := -C $(KERNEL_SRC) M=$(CURDIR) \ all: $(MAKE) $(MAKE_ARGS) modules +strip: all + $(STRIP) -d $(CURDIR)/vpnhide_kmod.ko + clean: $(MAKE) $(MAKE_ARGS) clean diff --git a/kmod/README.md b/kmod/README.md index 11d4330..27e0cb8 100644 --- a/kmod/README.md +++ b/kmod/README.md @@ -34,7 +34,7 @@ CI builds are provided for all 7 GKI generations: `android12-5.10` through `andr See [BUILDING.md](BUILDING.md) for the full guide (DDK Docker build, kernel source preparation, toolchain setup, `Module.symvers` generation). ```bash -cd kmod && ./build-zip.sh +cd kmod && ./build-zip.py ``` ## Install diff --git a/kmod/build-zip.py b/kmod/build-zip.py new file mode 100755 index 0000000..a1cfc3f --- /dev/null +++ b/kmod/build-zip.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Build and package the KernelSU/Magisk kernel module zip. + +Assembles a module staging directory so the committed module.prop stays at +its release version while the zip carries the actual build version (git describe). + +Usage: + python3 build-zip.py --kdir /path/to/kdir --kmi android14-5.15 # explicit args + python3 build-zip.py --kdir /path/to/kdir --kmi android14-5.15 --out custom.zip # custom output + # Or use environment variables: KDIR and KMI (CLI args override env vars) +""" + +from __future__ import annotations + +import argparse +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) +from build_lib import get_build_version, make_zip # type: ignore[import-not-found] + + +# Module file names +KMOD_C = "vpnhide_kmod.c" +KMOD_KO = "vpnhide_kmod.ko" + + +def main() -> int: + parser = argparse.ArgumentParser(description="Build and package the kernel module zip.") + parser.add_argument( + "--kdir", + type=str, + help="Kernel source directory (overrides KDIR or KERNEL_SRC env var)", + ) + parser.add_argument( + "--kmi", + type=str, + help="Kernel module interface variant (e.g., android14-5.15) for module.prop (overrides KMI env var)", + ) + parser.add_argument( + "--out", + type=str, + help="Output zip filename (default: vpnhide-kmod-.zip)", + ) + parser.add_argument( + "--clang-dir", + type=str, + help="Clang binaries directory (overrides CLANG_DIR env var or auto-detects)", + ) + args = parser.parse_args() + + kmod_dir = Path(__file__).resolve().parent + os.chdir(kmod_dir) + + # Resolve kdir: CLI arg > KDIR env var > KERNEL_SRC env var + if args.kdir: + kdir = args.kdir + kdir_src = "--kdir CLI argument" + else: + kdir = os.environ.get("KDIR") + kdir_src = "KDIR env var" + if not kdir: + kdir = os.environ.get("KERNEL_SRC") + kdir_src = "KERNEL_SRC env var" + if not kdir: + print("Error: --kdir argument, KDIR, or KERNEL_SRC env var is required", file=sys.stderr) + return 1 + + print(f"Using kdir from {kdir_src}: {kdir}") + + # Resolve kmi: CLI arg > KMI env var + if args.kmi: + kmi = args.kmi + kmi_src = "--kmi CLI argument" + else: + kmi = os.environ.get("KMI") + kmi_src = "KMI env var" + if not kmi: + print("Error: --kmi argument or KMI env var is required", file=sys.stderr) + return 1 + + print(f"Using kmi from {kmi_src}: {kmi}") + + # Set up environment for make + os.environ["KERNEL_SRC"] = kdir + + # Resolve clang_dir: CLI arg > CLANG_DIR env var > auto-detect + clang_dir = None + clang_dir_src = None + if args.clang_dir: + clang_dir = args.clang_dir + clang_dir_src = "--clang-dir CLI argument" + else: + clang_dir = os.environ.get("CLANG_DIR") + if clang_dir: + clang_dir_src = "CLANG_DIR env var" + else: + # Auto-detect: In CI, clang is at /opt/ddk/clang/clang-r*/bin + clang_base = Path("/opt/ddk/clang") + if clang_base.exists(): + clang_dirs = sorted(d for d in clang_base.iterdir() if d.is_dir() and d.name.startswith("clang-")) + if clang_dirs: + clang_dir = str(clang_dirs[-1] / "bin") + clang_dir_src = "auto-detected from /opt/ddk/clang" + if clang_dir: + os.environ["CLANG_DIR"] = clang_dir + print(f"Using clang-dir from {clang_dir_src}: {clang_dir}") + else: + print("Warning: clang-dir not set, using system PATH", file=sys.stderr) + + # Build the kernel module (env vars loaded by direnv from .env) + kmod_c = kmod_dir / KMOD_C + kmod_ko = kmod_dir / KMOD_KO + + if not kmod_ko.exists() or kmod_c.stat().st_mtime > kmod_ko.stat().st_mtime: + print("Building kernel module...") + subprocess.run(["make", "strip"], check=True) + + # Assemble the module staging directory so the committed module.prop + # stays at its release version while the zip carries the actual build + # version (git describe). + staging = kmod_dir / "module-staging" + if staging.exists(): + shutil.rmtree(staging) + shutil.copytree(kmod_dir / "module", staging) + shutil.copy(kmod_dir / KMOD_KO, staging / KMOD_KO) + + # Get build version + build_version = get_build_version(kmod_dir.parent) + + # Stamp version into module.prop + module_prop = staging / "module.prop" + content = module_prop.read_text(encoding="utf-8") + content = re.sub(r"^version=.*", f"version=v{build_version}", content, flags=re.MULTILINE) + # Add gkiVariant and updateJson + content = re.sub(r"^gkiVariant=.*", f"gkiVariant={kmi}", content, flags=re.MULTILINE) + if not re.search(r"^gkiVariant=", content, flags=re.MULTILINE): + content = content.rstrip() + f"\ngkiVariant={kmi}\n" + update_json_url = f"https://raw.githubusercontent.com/okhsunrog/vpnhide/main/update-json/update-kmod-{kmi}.json" + content = re.sub(r"^updateJson=.*", f"updateJson={update_json_url}", content, flags=re.MULTILINE) + if not re.search(r"^updateJson=", content, flags=re.MULTILINE): + content = content.rstrip() + f"\nupdateJson={update_json_url}\n" + module_prop.write_text(content, encoding="utf-8") + print(f"Stamped module.prop version=v{build_version} gkiVariant={kmi}") + + # Create zip in parent directory (workspace root for CI) + out_zip = kmod_dir.parent / (args.out if args.out else f"vpnhide-kmod-{kmi}.zip") + if out_zip.exists(): + out_zip.unlink() + + make_zip(staging, out_zip) + shutil.rmtree(staging) + + print() + print(f"Built: {out_zip.name}") + size_kb = out_zip.stat().st_size / 1024 + print(f" {out_zip} ({size_kb:.1f} KB)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/kmod/build-zip.sh b/kmod/build-zip.sh deleted file mode 100755 index 3f9d917..0000000 --- a/kmod/build-zip.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -cd "$(dirname "$0")" - -# Build the kernel module (env vars loaded by direnv from .env) -if [ ! -f vpnhide_kmod.ko ] || [ vpnhide_kmod.c -nt vpnhide_kmod.ko ]; then - echo "Building kernel module..." - make -fi - -# Assemble the module staging directory so the committed module.prop -# stays at its release version while the zip carries the actual build -# version (git describe). -STAGING="module-staging" -rm -rf "$STAGING" -cp -a module "$STAGING" -cp vpnhide_kmod.ko "$STAGING/vpnhide_kmod.ko" - -BUILD_VERSION="$(../scripts/build-version.sh)" -sed -i "s|^version=.*|version=v${BUILD_VERSION}|" "$STAGING/module.prop" -echo "Stamped module.prop version=v${BUILD_VERSION}" - -OUT="vpnhide-kmod.zip" -rm -f "$OUT" -(cd "$STAGING" && zip -qr "../$OUT" .) -rm -rf "$STAGING" - -echo -echo "Built: $OUT" -ls -lh "$OUT" diff --git a/lsposed/app/build.gradle.kts b/lsposed/app/build.gradle.kts index eb96626..1517579 100644 --- a/lsposed/app/build.gradle.kts +++ b/lsposed/app/build.gradle.kts @@ -11,16 +11,22 @@ android { namespace = "dev.okhsunrog.vpnhide" compileSdk = 35 - // Effective build version from ../scripts/build-version.sh: + // Effective build version from ../scripts/build-version.py: // release tag -> "0.6.2" // dev build -> "0.6.1-5-gabc1234" (+"-dirty" if uncommitted) // no git -> VERSION file + // Python instead of bash so Windows contributors can build without WSL. + // Script is stdlib-only — no `uv` / pip install needed. `python` on + // Windows, `python3` elsewhere: Ubuntu 22.04+ ships only the latter, + // Windows python.org / Store installer ships only the former. + val isWindows = System.getProperty("os.name").lowercase().contains("windows") + val pythonExe = if (isWindows) "python" else "python3" val buildVersion: String = providers .exec { commandLine( - "bash", - rootProject.projectDir.parentFile.resolve("scripts/build-version.sh").absolutePath, + pythonExe, + rootProject.projectDir.parentFile.resolve("scripts/build-version.py").absolutePath, ) }.standardOutput.asText .get() diff --git a/lsposed/app/src/test/kotlin/dev/okhsunrog/vpnhide/UpdateCheckerTest.kt b/lsposed/app/src/test/kotlin/dev/okhsunrog/vpnhide/UpdateCheckerTest.kt index 8266023..15cf3fd 100644 --- a/lsposed/app/src/test/kotlin/dev/okhsunrog/vpnhide/UpdateCheckerTest.kt +++ b/lsposed/app/src/test/kotlin/dev/okhsunrog/vpnhide/UpdateCheckerTest.kt @@ -92,7 +92,7 @@ class BaseVersionTest { @Test fun `dirty suffix alone is stripped`() { - // build-version.sh emits `X.Y.Z-dirty` when HEAD is on a tag but + // build-version.py emits `X.Y.Z-dirty` when HEAD is on a tag but // the working tree has uncommitted changes. assertEquals("0.6.2", baseVersion("0.6.2-dirty")) assertEquals("0.6.2", baseVersion("v0.6.2-dirty")) diff --git a/portshide/build-zip.py b/portshide/build-zip.py new file mode 100755 index 0000000..13b9991 --- /dev/null +++ b/portshide/build-zip.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Build and package the ports-hiding Magisk/KernelSU module zip. + +Assembles a module staging directory so the committed module.prop stays at +its release version while the zip carries the actual build version (git describe). +""" + +from __future__ import annotations + +import os +import re +import shutil +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) +from build_lib import get_build_version, make_zip # type: ignore[import-not-found] + + +def main() -> int: + script_dir = Path(__file__).resolve().parent + os.chdir(script_dir) + + # Assemble the module staging directory so the committed module.prop + # stays at its release version while the zip carries the actual build + # version (git describe). + staging = script_dir / "module-staging" + if staging.exists(): + shutil.rmtree(staging) + shutil.copytree(script_dir / "module", staging) + + # Get build version + build_version = get_build_version(script_dir.parent) + + # Stamp version into module.prop + module_prop = staging / "module.prop" + content = module_prop.read_text(encoding="utf-8") + content = re.sub(r"^version=.*", f"version=v{build_version}", content, flags=re.MULTILINE) + module_prop.write_text(content, encoding="utf-8") + print(f"Stamped module.prop version=v{build_version}") + + # CI sets UPDATE_JSON_URL so Magisk/KSU knows where to check for updates; + # local dev builds leave it unset and ship without updateJson. + update_json_url = os.environ.get("UPDATE_JSON_URL") + if update_json_url: + with open(module_prop, "a", encoding="utf-8") as f: + f.write(f"updateJson={update_json_url}\n") + + # Create zip + out_zip = script_dir / "vpnhide-ports.zip" + if out_zip.exists(): + out_zip.unlink() + + make_zip(staging, out_zip) + shutil.rmtree(staging) + + print() + print(f"Built: {out_zip.name}") + size_kb = out_zip.stat().st_size / 1024 + print(f" {out_zip} ({size_kb:.1f} KB)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/portshide/build-zip.sh b/portshide/build-zip.sh deleted file mode 100755 index 4c6b7b0..0000000 --- a/portshide/build-zip.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -cd "$(dirname "$0")" - -# Assemble the module staging directory so the committed module.prop -# stays at its release version while the zip carries the actual build -# version (git describe). -STAGING="module-staging" -rm -rf "$STAGING" -cp -a module "$STAGING" - -BUILD_VERSION="$(../scripts/build-version.sh)" -sed -i "s|^version=.*|version=v${BUILD_VERSION}|" "$STAGING/module.prop" -echo "Stamped module.prop version=v${BUILD_VERSION}" - -# CI sets UPDATE_JSON_URL so Magisk/KSU knows where to check for updates; -# local dev builds leave it unset and ship without updateJson. -if [ -n "${UPDATE_JSON_URL:-}" ]; then - echo "updateJson=${UPDATE_JSON_URL}" >> "$STAGING/module.prop" -fi - -OUT="vpnhide-ports.zip" -rm -f "$OUT" -(cd "$STAGING" && zip -qr "../$OUT" .) -rm -rf "$STAGING" - -echo -echo "Built: $OUT" -ls -lh "$OUT" diff --git a/scripts/build-version.py b/scripts/build-version.py new file mode 100755 index 0000000..6762d86 --- /dev/null +++ b/scripts/build-version.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Print the effective build version for vpnhide artifacts. + +Used by every packaging step (module.prop, APK versionName, CI +artifact names) so dev builds are unambiguously identifiable at a +glance. Called from `app/build.gradle.kts` on every Gradle build, so +stays on stdlib only — Gradle shouldn't need `uv` / external deps to +assemble the APK. +""" + +from __future__ import annotations + +import sys + +from build_lib import get_build_version + + +def main() -> int: + print(get_build_version()) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/build-version.sh b/scripts/build-version.sh deleted file mode 100755 index 41e6a2e..0000000 --- a/scripts/build-version.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -# Print the effective build version for vpnhide artifacts. -# -# - HEAD on a tag vX.Y.Z -> "X.Y.Z" (release build) -# - N commits after tag vX.Y.Z -> "X.Y.Z-N-gSHA" (dev build) -# - working tree dirty -> additional "-dirty" suffix -# - no git / no matching tag -> falls back to VERSION file -# -# Used by every packaging step (module.prop, APK versionName, CI artifact -# names) so dev builds are unambiguously identifiable at a glance. - -set -euo pipefail -cd "$(dirname "$0")/.." - -if git rev-parse --git-dir >/dev/null 2>&1 \ - && raw=$(git describe --tags --match 'v*' --dirty 2>/dev/null); then - echo "${raw#v}" -else - cat VERSION -fi diff --git a/scripts/build_lib.py b/scripts/build_lib.py new file mode 100644 index 0000000..2946f2a --- /dev/null +++ b/scripts/build_lib.py @@ -0,0 +1,50 @@ +"""Shared helpers for build scripts. + +Used by kmod/build-zip.py, portshide/build-zip.py, and zygisk/build-zip.py. +""" + +from __future__ import annotations + +import os +import zipfile +from pathlib import Path + + +def get_python_exe() -> str: + """Return 'python' on Windows, 'python3' elsewhere.""" + return "python" if os.name == "nt" else "python3" + + +def make_zip(source_dir: Path, output_zip: Path) -> None: + """Create a zip archive from source_dir contents.""" + with zipfile.ZipFile(output_zip, "w", zipfile.ZIP_DEFLATED) as zf: + for file_path in source_dir.rglob("*"): + if file_path.is_file(): + arcname = file_path.relative_to(source_dir) + zf.write(file_path, arcname) + + +def get_build_version(repo_root: Path | None = None) -> str: + """Get the effective build version for vpnhide artifacts. + + - HEAD on a tag vX.Y.Z -> "X.Y.Z" (release build) + - N commits after tag vX.Y.Z -> "X.Y.Z-N-gSHA" (dev build) + - working tree dirty -> additional "-dirty" suffix + - no git / no matching tag -> falls back to VERSION file + """ + import subprocess + + if repo_root is None: + repo_root = Path(__file__).resolve().parent.parent + + result = subprocess.run( + ["git", "describe", "--tags", "--match", "v*", "--dirty"], + cwd=repo_root, + capture_output=True, + text=True, + ) + if result.returncode == 0 and result.stdout.strip(): + return result.stdout.strip().removeprefix("v") + + version_file = repo_root / "VERSION" + return version_file.read_text(encoding="utf-8").strip() diff --git a/zygisk/README.md b/zygisk/README.md index f66bf37..4243ecf 100644 --- a/zygisk/README.md +++ b/zygisk/README.md @@ -74,7 +74,7 @@ Requirements: Build and package: ```bash -./build-zip.sh +./build-zip.py # Output: target/vpnhide-zygisk.zip (~180 KB) ``` @@ -131,7 +131,7 @@ VPN interface prefixes: `tun`, `ppp`, `tap`, `wg`, `ipsec`, `xfrm`, `utun`, `l2t - `build.rs` -- drives CMake on the shadowhook submodule - `third_party/android-inline-hook/` -- submodule (our shadowhook fork) - `module/` -- KernelSU/Magisk module metadata -- `build-zip.sh` -- cross-compile + package script +- `build-zip.py` -- cross-compile + package script ## License diff --git a/zygisk/build-zip.py b/zygisk/build-zip.py new file mode 100755 index 0000000..85cfa33 --- /dev/null +++ b/zygisk/build-zip.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Build the native library for aarch64 Android and package it into an +installable KernelSU/Magisk module zip. + +Requirements: + - rustup target aarch64-linux-android (already installed) + - cargo-ndk + - Android NDK at $ANDROID_NDK_HOME or auto-detected from $HOME/Android/Sdk/ndk/* +""" + +from __future__ import annotations + +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) +from build_lib import get_build_version, make_zip # type: ignore[import-not-found] + + +def main() -> int: + script_dir = Path(__file__).resolve().parent + os.chdir(script_dir) + + # Auto-detect NDK if ANDROID_NDK_HOME isn't set + android_ndk_home = os.environ.get("ANDROID_NDK_HOME") + if not android_ndk_home: + ndk_base = Path.home() / "Android" / "Sdk" / "ndk" + if ndk_base.exists(): + ndk_versions = sorted( + d.name for d in ndk_base.iterdir() if d.is_dir() + ) + if ndk_versions: + android_ndk_home = str(ndk_base / ndk_versions[-1]) + + if not android_ndk_home or not Path(android_ndk_home).is_dir(): + print( + "error: ANDROID_NDK_HOME not set and no NDK found under ~/Android/Sdk/ndk", + file=sys.stderr, + ) + return 1 + + print(f"Using NDK: {android_ndk_home}") + os.environ["ANDROID_NDK_HOME"] = android_ndk_home + + # Build the cdylib for arm64-v8a + subprocess.run( + ["cargo", "ndk", "-t", "arm64-v8a", "build", "--release"], + check=True, + ) + + so_src = script_dir / "target" / "aarch64-linux-android" / "release" / "libvpnhide_zygisk.so" + if not so_src.exists(): + print(f"error: expected {so_src} after cargo ndk build, not found", file=sys.stderr) + return 1 + + # Assemble the module staging directory + staging = script_dir / "target" / "module-staging" + if staging.exists(): + shutil.rmtree(staging) + shutil.copytree(script_dir / "module", staging) + (staging / "zygisk").mkdir(parents=True, exist_ok=True) + shutil.copy(so_src, staging / "zygisk" / "arm64-v8a.so") + + # Get build version + build_version = get_build_version(script_dir.parent) + + # Stamp the effective build version into the staging module.prop without + # touching the committed file. On a release tag this matches VERSION; on + # any other commit the git suffix makes dev builds identifiable. + module_prop = staging / "module.prop" + content = module_prop.read_text(encoding="utf-8") + content = re.sub(r"^version=.*", f"version=v{build_version}", content, flags=re.MULTILINE) + module_prop.write_text(content, encoding="utf-8") + print(f"Stamped module.prop version=v{build_version}") + + # CI sets UPDATE_JSON_URL so Magisk/KSU knows where to check for updates; + # local dev builds leave it unset and ship without updateJson. + update_json_url = os.environ.get("UPDATE_JSON_URL") + if update_json_url: + with open(module_prop, "a", encoding="utf-8") as f: + f.write(f"updateJson={update_json_url}\n") + + # Zip it + out_zip = script_dir / "target" / "vpnhide-zygisk.zip" + if out_zip.exists(): + out_zip.unlink() + + make_zip(staging, out_zip) + + print() + print(f"Built: {out_zip.name}") + size_kb = out_zip.stat().st_size / 1024 + print(f" {out_zip} ({size_kb:.1f} KB)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/zygisk/build-zip.sh b/zygisk/build-zip.sh deleted file mode 100755 index 24da6e2..0000000 --- a/zygisk/build-zip.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash -# Build the native library for aarch64 Android and package it into an -# installable KernelSU/Magisk module zip. -# -# Requirements: -# - rustup target aarch64-linux-android (already installed) -# - cargo-ndk -# - Android NDK at $ANDROID_NDK_HOME or auto-detected from $HOME/Android/Sdk/ndk/* -# - zip - -set -euo pipefail - -PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" -cd "$PROJECT_DIR" - -# Auto-detect NDK if ANDROID_NDK_HOME isn't set -if [ -z "${ANDROID_NDK_HOME:-}" ]; then - ANDROID_NDK_HOME="$(find "$HOME/Android/Sdk/ndk" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort -V | tail -1)" -fi -if [ -z "${ANDROID_NDK_HOME:-}" ] || [ ! -d "$ANDROID_NDK_HOME" ]; then - echo "error: ANDROID_NDK_HOME not set and no NDK found under ~/Android/Sdk/ndk" >&2 - exit 1 -fi -echo "Using NDK: $ANDROID_NDK_HOME" -export ANDROID_NDK_HOME - -# Build the cdylib for arm64-v8a -cargo ndk -t arm64-v8a build --release - -SO_SRC="target/aarch64-linux-android/release/libvpnhide_zygisk.so" -if [ ! -f "$SO_SRC" ]; then - echo "error: expected $SO_SRC after cargo ndk build, not found" >&2 - exit 1 -fi - -# Assemble the module staging directory -STAGING="target/module-staging" -rm -rf "$STAGING" -cp -a module "$STAGING" -mkdir -p "$STAGING/zygisk" -cp "$SO_SRC" "$STAGING/zygisk/arm64-v8a.so" - -# Stamp the effective build version into the staging module.prop without -# touching the committed file. On a release tag this matches VERSION; on -# any other commit the git suffix makes dev builds identifiable. -BUILD_VERSION="$(../scripts/build-version.sh)" -sed -i "s|^version=.*|version=v${BUILD_VERSION}|" "$STAGING/module.prop" -echo "Stamped module.prop version=v${BUILD_VERSION}" - -# CI sets UPDATE_JSON_URL so Magisk/KSU knows where to check for updates; -# local dev builds leave it unset and ship without updateJson. -if [ -n "${UPDATE_JSON_URL:-}" ]; then - echo "updateJson=${UPDATE_JSON_URL}" >> "$STAGING/module.prop" -fi - -# Zip it -OUT_ZIP="target/vpnhide-zygisk.zip" -rm -f "$OUT_ZIP" -(cd "$STAGING" && zip -qr "../../$OUT_ZIP" .) - -echo -echo "Built: $OUT_ZIP" -ls -lh "$OUT_ZIP"