name: Build ABK App on: workflow_dispatch: push: paths: - "app/**" - "app_dev/**" - "build.gradle.kts" - "settings.gradle.kts" - "gradle/**" - ".github/workflows/build-abk-app.yml" - ".github/actions/prepare-abk-app-inputs/**" pull_request: paths: - "app/**" - "app_dev/**" - "build.gradle.kts" - "settings.gradle.kts" - "gradle/**" - ".github/workflows/build-abk-app.yml" - ".github/actions/prepare-abk-app-inputs/**" jobs: build: if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'xingguangcuican6666/ABK') }} runs-on: ${{ vars.APP_RUNNER || 'ubuntu-latest' }} permissions: contents: write actions: read env: HAS_RELEASE_SIGNING: ${{ secrets.ABK_RELEASE_KEYSTORE_BASE64 != '' && secrets.ABK_RELEASE_STORE_PASSWORD != '' && secrets.ABK_RELEASE_KEY_ALIAS != '' && secrets.ABK_RELEASE_KEY_PASSWORD != '' }} SUKISU_ULTRA_REPO: https://github.com/SukiSU-Ultra/SukiSU-Ultra.git SUKISU_ULTRA_REF: main SUKISU_KSUD_ABIS: arm64-v8a,armeabi-v7a,x86_64 steps: - name: Checkout pull request merge ref if: ${{ github.event_name == 'pull_request' }} uses: actions/checkout@v6 - name: Checkout branch or tag ref if: ${{ github.event_name != 'pull_request' }} uses: actions/checkout@v6 with: ref: ${{ github.ref_name }} - name: Stamp app build metadata run: | echo "ABK_APP_UPDATE_METADATA_URL=https://raw.githubusercontent.com/xingguangcuican6666/ABK/dev/version.json" >> "$GITHUB_ENV" echo "ABK_APP_BUILD_TIMESTAMP=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_ENV" echo "ABK_APP_BUILD_TIMESTAMP_EPOCH_MILLIS=$(( $(date -u +%s) * 1000 ))" >> "$GITHUB_ENV" echo "ABK_APP_BUILD_RUN_ID=${GITHUB_RUN_ID}" >> "$GITHUB_ENV" echo "ABK_APP_BUILD_COMMIT_SHA=${GITHUB_SHA}" >> "$GITHUB_ENV" echo "ABK_APP_BUILD_WORKFLOW_NAME=${GITHUB_WORKFLOW}" >> "$GITHUB_ENV" - name: Set up JDK uses: actions/setup-java@v5 with: distribution: temurin java-version: "21" - name: Set up Android SDK uses: android-actions/setup-android@v3 - name: Install Android SDK packages run: sdkmanager --channel=3 "platforms;android-37.0" "build-tools;37.0.0" "cmake;3.22.1" "ndk;28.2.13676358" - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 with: gradle-version: "9.3.1" cache-read-only: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} - name: Set up Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,aarch64-unknown-linux-musl,x86_64-unknown-linux-musl - name: Prepare ABK app inputs id: abk-inputs uses: ./.github/actions/prepare-abk-app-inputs env: GITHUB_TOKEN: ${{ github.token }} - name: Cache Cargo registry uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git key: abk-cargo-registry-v1-linux restore-keys: abk-cargo-registry-v1- - name: Cache ksud outputs id: ksud-cache uses: actions/cache@v4 with: path: | app/build/generated/abk-assets/main app/build/generated/abk-jniLibs/main key: abk-ksud-v1-${{ steps.abk-inputs.outputs.sukisu_commit }}-${{ steps.abk-inputs.outputs.ksud_abis_key }}-${{ steps.abk-inputs.outputs.workflow_digest }} - name: Verify ksud cache id: ksud-verify run: | set -euo pipefail expected="${{ steps.abk-inputs.outputs.sukisu_commit }}" props="app/build/generated/abk-assets/main/ksud/source.properties" if [ "${{ steps.ksud-cache.outputs.cache-hit }}" != "true" ] || [ ! -f "$props" ]; then echo "needs_rebuild=true" >> "$GITHUB_OUTPUT" echo "ksud cache: miss" exit 0 fi actual="$(grep '^commit=' "$props" | cut -d= -f2- | tr -d '\r')" if [ "$actual" != "$expected" ]; then echo "ksud cache: stale (expected=$expected actual=$actual)" rm -rf app/build/generated/abk-assets/main app/build/generated/abk-jniLibs/main echo "needs_rebuild=true" >> "$GITHUB_OUTPUT" else echo "ksud cache: hit (commit=$actual)" echo "needs_rebuild=false" >> "$GITHUB_OUTPUT" fi - name: Build bundled SukiSU-Ultra ksud assets if: steps.ksud-verify.outputs.needs_rebuild == 'true' env: ANDROID_NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/28.2.13676358 run: | set -euo pipefail source_dir="$RUNNER_TEMP/sukisu-ultra" target_dir="$GITHUB_WORKSPACE/app/build/generated/abk-assets/main/ksud" jni_target_dir="$GITHUB_WORKSPACE/app/build/generated/abk-jniLibs/main" cargo_target_dir="$source_dir/target" repo_url="${SUKISU_ULTRA_REPO%.git}" rm -rf "$source_dir" "$target_dir" "$jni_target_dir" git clone --depth 1 --branch "$SUKISU_ULTRA_REF" "$SUKISU_ULTRA_REPO" "$source_dir" commit="$(git -C "$source_dir" rev-parse HEAD)" mkdir -p "$target_dir" "$jni_target_dir" setup_rust_build() { local triple="$1" local android_sdk_level="$2" local llvm_path="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64" local llvm_bin="$llvm_path/bin" unset RUSTFLAGS local ndk_triple="$triple" if [ "$ndk_triple" = "armv7-linux-androideabi" ]; then ndk_triple="armv7a-linux-androideabi" fi local clang_path="$llvm_bin/${ndk_triple}${android_sdk_level}-clang" local utriple="${triple//-/_}" local uutriple="${utriple^^}" export "CC_${utriple}=$clang_path" export "CXX_${utriple}=${clang_path}++" export "AR_${utriple}=$llvm_bin/llvm-ar" export "CARGO_TARGET_${uutriple}_LINKER=$clang_path" export "BINDGEN_EXTRA_CLANG_ARGS_${utriple}=--sysroot=$llvm_path/sysroot -I$llvm_path/sysroot/usr/include/$triple" } setup_ksuinit_build() { local triple="$1" local llvm_path="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64" local llvm_bin="$llvm_path/bin" local linker="" local env_key="" unset RUSTFLAGS case "$triple" in aarch64-unknown-linux-musl) linker="$llvm_bin/aarch64-linux-android26-clang" env_key="CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER" export RUSTFLAGS="-C link-arg=-no-pie" ;; x86_64-unknown-linux-musl) linker="$llvm_bin/x86_64-linux-android26-clang" env_key="CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER" ;; *) echo "::error::Unsupported ksuinit target: $triple" return 1 ;; esac export "$env_key=$linker" } triples_for_abi() { case "$1" in arm64-v8a) printf '%s\n' "aarch64-linux-android" ;; armeabi-v7a) printf '%s\n' "armv7-linux-androideabi" ;; x86_64) printf '%s\n' "x86_64-linux-android" ;; *) echo "::error::Unsupported ksud ABI: $1" return 1 ;; esac } asset_dir_for_abi() { case "$1" in arm64-v8a) printf '%s\n' "aarch64" ;; armeabi-v7a) printf '%s\n' "arm" ;; x86_64) printf '%s\n' "x86_64" ;; *) echo "::error::Unsupported asset ABI: $1" return 1 ;; esac } ksuinit_target_for_abi() { case "$1" in arm64-v8a) printf '%s\n' "aarch64-unknown-linux-musl" ;; armeabi-v7a) printf '%s\n' "" ;; x86_64) printf '%s\n' "x86_64-unknown-linux-musl" ;; *) echo "::error::Unsupported ksuinit ABI: $1" return 1 ;; esac } build_ksuinit_binary() { local triple="$1" [ -n "$triple" ] || return 0 setup_ksuinit_build "$triple" cargo build \ --package ksuinit \ --target "$triple" \ --release \ --target-dir "$cargo_target_dir" \ --manifest-path "$source_dir/Cargo.toml" } { echo "source_repo=$repo_url" echo "ref=$SUKISU_ULTRA_REF" echo "commit=$commit" echo "abis=$SUKISU_KSUD_ABIS" echo "built_by=.github/workflows/build-abk-app.yml" } > "$target_dir/source.properties" build_ksuinit_binary "aarch64-unknown-linux-musl" build_ksuinit_binary "x86_64-unknown-linux-musl" aarch64_ksuinit="$cargo_target_dir/aarch64-unknown-linux-musl/release/ksuinit" x86_64_ksuinit="$cargo_target_dir/x86_64-unknown-linux-musl/release/ksuinit" IFS=',' read -ra abi_list <<< "$SUKISU_KSUD_ABIS" for abi in "${abi_list[@]}"; do abi="$(printf '%s' "$abi" | xargs)" [ -n "$abi" ] || continue triple="$(triples_for_abi "$abi")" asset_dir="$(asset_dir_for_abi "$abi")" bin_dir="$source_dir/userspace/ksud/bin/$asset_dir" mkdir -p "$bin_dir" ksuinit_target="$(ksuinit_target_for_abi "$abi")" if [ -n "$ksuinit_target" ]; then ksuinit_binary="$cargo_target_dir/$ksuinit_target/release/ksuinit" else ksuinit_binary="$aarch64_ksuinit" echo "warning.$abi=ksuinit_fallback_aarch64" >> "$target_dir/source.properties" fi if [ ! -f "$ksuinit_binary" ]; then echo "::error::ksuinit binary missing for $abi" find "$cargo_target_dir" -maxdepth 4 -type f -name 'ksuinit' | sort || true exit 1 fi install -Dm755 "$ksuinit_binary" "$bin_dir/ksuinit" setup_rust_build "$triple" 26 cargo build \ --target "$triple" \ --release \ --target-dir "$cargo_target_dir" \ --manifest-path "$source_dir/userspace/ksud/Cargo.toml" binary="" for candidate in \ "$cargo_target_dir/$triple/release/ksud" \ "$source_dir/userspace/ksud/target/$triple/release/ksud" \ "$GITHUB_WORKSPACE/target/$triple/release/ksud" do if [ -f "$candidate" ]; then binary="$candidate" break fi done if [ ! -f "$binary" ]; then echo "::error::ksud binary missing for $abi ($triple)" echo "cargo_target_dir=$cargo_target_dir" find "$source_dir" -maxdepth 4 -type f \( -name 'ksud' -o -name 'ksud.exe' \) | sort || true find "$GITHUB_WORKSPACE" -maxdepth 4 -type f \( -name 'ksud' -o -name 'ksud.exe' \) | sort || true exit 1 fi install -Dm755 "$binary" "$jni_target_dir/$abi/libksud.so" sha256="$(sha256sum "$binary" | awk '{print tolower($1)}')" size="$(wc -c < "$binary" | tr -d '[:space:]')" { echo "sha256.$abi=$sha256" echo "size.$abi=$size" } >> "$target_dir/source.properties" done { echo "### Bundled SukiSU-Ultra ksud" echo echo "- Source: $repo_url" echo "- Ref: $SUKISU_ULTRA_REF" echo "- Commit: $commit" echo "- ABIs: $SUKISU_KSUD_ABIS" echo "- Embedded ksuinit: aarch64-unknown-linux-musl, x86_64-unknown-linux-musl" echo "- magiskboot: removed upstream (ksud uses bootimg crate)" } >> "$GITHUB_STEP_SUMMARY" - name: Cache LKM assets id: lkm-cache uses: actions/cache@v4 with: path: app/src/main/assets/abk_lkm key: abk-lkm-v1-${{ steps.abk-inputs.outputs.lkm_head_sha }} - name: Verify LKM cache id: lkm-verify run: | set -euo pipefail expected="${{ steps.abk-inputs.outputs.lkm_head_sha }}" props="app/src/main/assets/abk_lkm/source.properties" if [ "${{ steps.lkm-cache.outputs.cache-hit }}" != "true" ] || [ ! -f "$props" ]; then echo "needs_rebundle=true" >> "$GITHUB_OUTPUT" echo "lkm cache: miss" exit 0 fi actual="$(grep '^head_sha=' "$props" | cut -d= -f2- | tr -d '\r')" if [ "$actual" != "$expected" ]; then echo "lkm cache: stale (expected=$expected actual=$actual)" rm -rf app/src/main/assets/abk_lkm mkdir -p app/src/main/assets/abk_lkm echo "needs_rebundle=true" >> "$GITHUB_OUTPUT" else echo "lkm cache: hit (head_sha=$actual)" echo "needs_rebundle=false" >> "$GITHUB_OUTPUT" fi - name: Bundle ABK LKM artifacts if: steps.lkm-verify.outputs.needs_rebundle == 'true' env: GITHUB_TOKEN: ${{ github.token }} run: | set -euo pipefail repo="xingguangcuican6666/ABK_control_module" workflow="lkm.yml" target_dir="app/src/main/assets/abk_lkm" mkdir -p "$target_dir" rm -rf "$target_dir"/* api_get() { local url="$1" local tmp tmp="$(mktemp)" if [ -n "${GITHUB_TOKEN:-}" ] && curl -fsSL \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GITHUB_TOKEN" \ "$url" -o "$tmp"; then cat "$tmp" rm -f "$tmp" return 0 fi rm -f "$tmp" curl -fsSL -H "Accept: application/vnd.github+json" "$url" } download_artifact() { local url="$1" local out="$2" if [ -n "${GITHUB_TOKEN:-}" ] && curl -fsSL -L \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GITHUB_TOKEN" \ "$url" -o "$out"; then return 0 fi curl -fsSL -L "$url" -o "$out" } runs_json="$(api_get "https://api.github.com/repos/$repo/actions/workflows/$workflow/runs?status=success&per_page=1")" run_id="$(printf '%s' "$runs_json" | jq -r '.workflow_runs[0].id // empty')" run_sha="$(printf '%s' "$runs_json" | jq -r '.workflow_runs[0].head_sha // empty')" if [ -z "$run_id" ]; then echo "::error::未找到 $repo 的成功 LKM action" exit 1 fi artifacts_json="$(api_get "https://api.github.com/repos/$repo/actions/runs/$run_id/artifacts?per_page=100")" artifact_count=0 printf '%s' "$artifacts_json" | jq -r '.artifacts[] | select(.name | endswith("-lkm")) | [.name, .archive_download_url] | @tsv' | while IFS=$'\t' read -r name url; do [ -n "$name" ] || continue variant="${name%%-*}" remainder="${name#*-}" kmi="${remainder%-lkm}" out_dir="$target_dir/$variant" tmp_dir="$(mktemp -d)" mkdir -p "$out_dir" download_artifact "$url" "$tmp_dir/artifact.zip" unzip -q "$tmp_dir/artifact.zip" -d "$tmp_dir/unpack" ko_file="$(find "$tmp_dir/unpack" -type f -name '*.ko' | head -n 1)" if [ -z "$ko_file" ]; then echo "::error::artifact $name 中没有 .ko 文件" exit 1 fi cp "$ko_file" "$out_dir/${kmi}_kernelsu.ko" artifact_count=$((artifact_count + 1)) rm -rf "$tmp_dir" done count="$(find "$target_dir" -type f -name '*.ko' | wc -l | tr -d '[:space:]')" if [ "$count" -eq 0 ]; then echo "::error::没有成功打包任何 LKM .ko" exit 1 fi { echo "source=$repo" echo "workflow=$workflow" echo "run_id=$run_id" echo "head_sha=$run_sha" echo "count=$count" } > "$target_dir/source.properties" { echo "### ABK LKM artifacts" echo echo "- Source: $repo" echo "- Workflow: $workflow" echo "- Run: $run_id" echo "- Commit: $run_sha" echo "- Files: $count" } >> "$GITHUB_STEP_SUMMARY" - name: Build debug APK run: gradle assembleDebug - name: Decode release keystore if: ${{ env.HAS_RELEASE_SIGNING == 'true' }} env: ABK_RELEASE_KEYSTORE_BASE64: ${{ secrets.ABK_RELEASE_KEYSTORE_BASE64 }} run: | mkdir -p app/signing printf '%s' "$ABK_RELEASE_KEYSTORE_BASE64" | base64 --decode > app/signing/abk-release.jks - name: Export ABK manager certificate metadata if: ${{ env.HAS_RELEASE_SIGNING == 'true' }} env: ABK_RELEASE_STORE_PASSWORD: ${{ secrets.ABK_RELEASE_STORE_PASSWORD }} ABK_RELEASE_KEY_ALIAS: ${{ secrets.ABK_RELEASE_KEY_ALIAS }} ABK_RELEASE_STORE_TYPE: ${{ secrets.ABK_RELEASE_STORE_TYPE }} run: | set -euo pipefail cert="$RUNNER_TEMP/abk-manager-cert.der" keytool -exportcert \ -keystore app/signing/abk-release.jks \ -storetype "${ABK_RELEASE_STORE_TYPE:-JKS}" \ -storepass "$ABK_RELEASE_STORE_PASSWORD" \ -alias "$ABK_RELEASE_KEY_ALIAS" \ -file "$cert" size="$(wc -c < "$cert" | tr -d '[:space:]')" hash="$(sha256sum "$cert" | awk '{print tolower($1)}')" { echo "# Official ABK release signing certificate metadata." echo "# This is public certificate metadata, not a private signing key." echo "ABK_MANAGER_PACKAGE=com.abk.kernel" echo "ABK_MANAGER_CERT_SIZE=$size" echo "ABK_MANAGER_CERT_SHA256=$hash" } > app/signing/abk-manager-cert.generated.env { echo "### ABK manager certificate metadata" echo echo '```env' cat app/signing/abk-manager-cert.generated.env echo '```' } >> "$GITHUB_STEP_SUMMARY" - name: Verify checked-in ABK manager certificate metadata if: ${{ env.HAS_RELEASE_SIGNING == 'true' }} run: | set -euo pipefail expected="app/signing/abk-manager-cert.env" actual="app/signing/abk-manager-cert.generated.env" if [ ! -f "$expected" ]; then echo "::error::$expected is missing. Commit the public manager certificate metadata so kernel builds can trust the release APK." exit 1 fi read_value() { local file="$1" local key="$2" awk -F= -v target="$key" ' /^[[:space:]]*#/ { next } $1 == target { value = substr($0, index($0, "=") + 1) gsub(/^[[:space:]\042\047]+|[[:space:]\042\047]+$/, "", value) print value exit } ' "$file" } mismatch=0 for key in ABK_MANAGER_PACKAGE ABK_MANAGER_CERT_SIZE ABK_MANAGER_CERT_SHA256; do expected_value="$(read_value "$expected" "$key")" actual_value="$(read_value "$actual" "$key")" if [ "$expected_value" != "$actual_value" ]; then echo "::error::$key mismatch: checked-in='$expected_value' generated='$actual_value'" mismatch=1 fi done if [ "$mismatch" -ne 0 ]; then echo "::error::Update $expected with the generated metadata before building trusted manager kernels." exit 1 fi - name: Build release APK env: ABK_RELEASE_STORE_FILE: ${{ github.workspace }}/app/signing/abk-release.jks ABK_RELEASE_STORE_PASSWORD: ${{ secrets.ABK_RELEASE_STORE_PASSWORD }} ABK_RELEASE_KEY_ALIAS: ${{ secrets.ABK_RELEASE_KEY_ALIAS }} ABK_RELEASE_KEY_PASSWORD: ${{ secrets.ABK_RELEASE_KEY_PASSWORD }} run: gradle assembleRelease - name: Verify signed release APK if: ${{ env.HAS_RELEASE_SIGNING == 'true' }} run: | signed_apk="$(find app/build/outputs/apk/release -type f -name '*-release.apk' ! -name '*unsigned*' | head -n 1)" if [ -z "$signed_apk" ]; then echo "No signed release APK found." find app/build/outputs/apk/release -maxdepth 1 -type f -print exit 1 fi "$ANDROID_HOME/build-tools/37.0.0/apksigner" verify --verbose "$signed_apk" - name: Commit version.json if: ${{ github.repository == 'xingguangcuican6666/ABK' && github.event_name != 'pull_request' && startsWith(github.ref, 'refs/heads/') }} run: | set -euo pipefail branch="${GITHUB_REF_NAME}" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" for attempt in 1 2 3 4 5; do git restore --staged --worktree version.json app/build.gradle.kts app/src/main/res/values/strings.xml 2>/dev/null || true git fetch origin "${branch}" git checkout "${branch}" git pull --rebase origin "${branch}" python .github/scripts/update-version-json.py \ --channel normal \ --workflow-name "${GITHUB_WORKFLOW}" \ --run-id "${GITHUB_RUN_ID}" \ --commit-sha "${GITHUB_SHA}" \ --published-at "${ABK_APP_BUILD_TIMESTAMP}" \ --build-timestamp-epoch-millis "${ABK_APP_BUILD_TIMESTAMP_EPOCH_MILLIS}" if git diff --quiet -- version.json; then echo "version.json unchanged" exit 0 fi git add version.json git commit -m "chore(app): update unstable version metadata" if git push origin HEAD:${branch}; then exit 0 fi git reset --hard "origin/${branch}" sleep $((attempt * 3)) done echo "::error::Failed to push version.json after retries." exit 1 - name: Upload APKs uses: actions/upload-artifact@v6 with: name: abk-apks path: | app/build/outputs/apk/debug/*.apk app/build/outputs/apk/release/*.apk app/signing/abk-manager-cert.generated.env LICENSE THIRD_PARTY_NOTICES.md