managers.yml 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  1. name: Fetch Root Managers
  2. permissions:
  3. contents: read
  4. actions: read
  5. on:
  6. workflow_dispatch:
  7. inputs:
  8. root_flavor:
  9. description: "Root flavor to fetch managers for"
  10. type: choice
  11. options:
  12. - All
  13. - KernelSU-Next
  14. - KernelSU
  15. - ReSukiSU
  16. default: All
  17. kernelsu_next_commit:
  18. description: "Pin KernelSU-Next manager to this commit (40-char SHA, empty = latest dev tip)"
  19. required: false
  20. type: string
  21. default: ""
  22. kernelsu_commit:
  23. description: "Pin KernelSU manager to this commit (40-char SHA, empty = latest main tip)"
  24. required: false
  25. type: string
  26. default: ""
  27. resukisu_commit:
  28. description: "Pin ReSukiSU manager to this commit (40-char SHA, empty = latest main tip)"
  29. required: false
  30. type: string
  31. default: ""
  32. build_from_source:
  33. description: "Build from source — clone each repo at the pinned commit and build LKM + ksud + manager locally (instead of fetching upstream CI artifacts)"
  34. required: false
  35. type: boolean
  36. default: false
  37. workflow_call:
  38. inputs:
  39. root_flavor:
  40. description: "Root flavor to fetch managers for"
  41. required: false
  42. type: string
  43. default: All
  44. kernelsu_next_commit:
  45. description: "Pin KernelSU-Next manager commit"
  46. required: false
  47. type: string
  48. default: ""
  49. kernelsu_commit:
  50. description: "Pin KernelSU manager commit"
  51. required: false
  52. type: string
  53. default: ""
  54. resukisu_commit:
  55. description: "Pin ReSukiSU manager commit"
  56. required: false
  57. type: string
  58. default: ""
  59. build_from_source:
  60. description: "Build from source instead of fetching artifacts"
  61. required: false
  62. type: boolean
  63. default: false
  64. jobs:
  65. # ── path A: fetch prebuilt manager APKs from upstream CI (default) ──
  66. resolve-managers:
  67. if: ${{ !inputs.build_from_source }}
  68. runs-on: ubuntu-latest
  69. outputs:
  70. manager_list: ${{ steps.manager.outputs.manager_list }}
  71. steps:
  72. - name: Resolve manager builds for selected root flavor(s)
  73. id: manager
  74. shell: bash
  75. env:
  76. GH_TOKEN: ${{ github.token }}
  77. INPUT_ROOT_FLAVOR: ${{ inputs.root_flavor }}
  78. INPUT_NEXT_COMMIT: ${{ inputs.kernelsu_next_commit }}
  79. INPUT_KSU_COMMIT: ${{ inputs.kernelsu_commit }}
  80. INPUT_RESUKISU_COMMIT: ${{ inputs.resukisu_commit }}
  81. run: |
  82. set -euo pipefail
  83. # Exact extraction of main.yml#resolve-managers / manager step.
  84. # Each selected root must ship ALL manager APK builds. KernelSU-Next publishes
  85. # its manager APKs (manager + manager-spoofed) from the official dev-tip
  86. # build-manager-ci run; kernelsu (KernelSU) and ReSukiSU expose theirs as
  87. # CI-run artifacts. All three are pinned to an exact commit.
  88. retry() {
  89. local n=0
  90. until [ "$n" -ge 5 ]; do
  91. "$@" && return 0
  92. n=$((n+1))
  93. echo "retry $n/5 failed for: $*" >&2
  94. sleep 5
  95. done
  96. return 1
  97. }
  98. resolve_sha() { # repo ref
  99. retry git ls-remote "$1" "$2" | awk '{print $1; exit}'
  100. }
  101. resolve_one() { # flavor
  102. local flavor="$1"
  103. local owner repo wf stock
  104. case "$flavor" in
  105. next)
  106. owner="KernelSU-Next"; repo="KernelSU-Next"; wf="build-manager-ci.yml"
  107. if [[ -n "${INPUT_NEXT_COMMIT:-}" ]]; then
  108. stock="$INPUT_NEXT_COMMIT"
  109. if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
  110. echo "ERROR: kernelsu_next_commit must be a 40-char SHA (got: $stock)" >&2
  111. exit 1
  112. fi
  113. else
  114. stock=$(resolve_sha https://github.com/KernelSU-Next/KernelSU-Next.git refs/heads/dev)
  115. fi
  116. run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  117. -H "Authorization: Bearer $GH_TOKEN" \
  118. "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
  119. | jq -r '.workflow_runs[0].id // empty')
  120. if [[ -z "${run_id}" ]]; then
  121. echo "ERROR: no ${repo} manager build exists for dev commit ${stock}" >&2
  122. exit 1
  123. fi
  124. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"manager,manager-spoofed"}' \
  125. "$flavor" "$owner" "$repo" "$run_id" "$stock"
  126. ;;
  127. kernelsu)
  128. owner="tiann"; repo="KernelSU"; wf="build-manager.yml"
  129. if [[ -n "${INPUT_KSU_COMMIT:-}" ]]; then
  130. stock="$INPUT_KSU_COMMIT"
  131. if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
  132. echo "ERROR: kernelsu_commit must be a 40-char SHA (got: $stock)" >&2
  133. exit 1
  134. fi
  135. else
  136. stock=$(resolve_sha https://github.com/tiann/KernelSU.git refs/heads/main)
  137. fi
  138. run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  139. "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
  140. | jq -r '.workflow_runs[0].id // empty')
  141. if [[ -z "${run_id}" ]]; then
  142. echo "ERROR: no ${repo} manager build exists for stock commit ${stock}" >&2
  143. exit 1
  144. fi
  145. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"manager"}' \
  146. "$flavor" "$owner" "$repo" "$run_id" "$stock"
  147. ;;
  148. resukisu)
  149. owner="ReSukiSU"; repo="ReSukiSU"; wf="build-manager.yml"
  150. if [[ -n "${INPUT_RESUKISU_COMMIT:-}" ]]; then
  151. stock="$INPUT_RESUKISU_COMMIT"
  152. if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
  153. echo "ERROR: resukisu_commit must be a 40-char SHA (got: $stock)" >&2
  154. exit 1
  155. fi
  156. else
  157. stock=$(resolve_sha https://github.com/ReSukiSU/ReSukiSU.git refs/heads/main)
  158. fi
  159. run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  160. "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
  161. | jq -r '.workflow_runs[0].id // empty')
  162. if [[ -z "${run_id}" ]]; then
  163. echo "ERROR: no ${repo} manager build exists for stock commit ${stock}" >&2
  164. exit 1
  165. fi
  166. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"Manager-release,Spoofed-Manager-release,Manager-debug"}' \
  167. "$flavor" "$owner" "$repo" "$run_id" "$stock"
  168. ;;
  169. *)
  170. echo "skipping unknown flavor: $flavor" >&2
  171. return 0
  172. ;;
  173. esac
  174. }
  175. FLAVORS="${INPUT_ROOT_FLAVOR:-All}"
  176. echo "Root flavor: ${FLAVORS}"
  177. LIST=""
  178. case "${FLAVORS}" in
  179. All) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one next)"$'\n'"$(resolve_one kernelsu)"$'\n'"$(resolve_one resukisu)") ;;
  180. KernelSU-Next) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one next)") ;;
  181. KernelSU) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one kernelsu)") ;;
  182. ReSukiSU) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one resukisu)") ;;
  183. *) echo "Skipping manager resolution (no root flavor / none)." ;;
  184. esac
  185. if [ -n "$LIST" ]; then
  186. echo "manager_list=$LIST" >> "$GITHUB_OUTPUT"
  187. echo "Manager sources resolved: $LIST"
  188. else
  189. echo "manager_list=[]" >> "$GITHUB_OUTPUT"
  190. fi
  191. - name: Summary
  192. run: |
  193. echo "### Resolved manager sources" >> "$GITHUB_STEP_SUMMARY"
  194. echo '```json' >> "$GITHUB_STEP_SUMMARY"
  195. echo '${{ steps.manager.outputs.manager_list }}' | jq . >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || echo '${{ steps.manager.outputs.manager_list }}' >> "$GITHUB_STEP_SUMMARY"
  196. echo '```' >> "$GITHUB_STEP_SUMMARY"
  197. list-manager-apks:
  198. needs: resolve-managers
  199. if: ${{ !inputs.build_from_source && needs.resolve-managers.outputs.manager_list != '' && needs.resolve-managers.outputs.manager_list != '[]' }}
  200. runs-on: ubuntu-latest
  201. outputs:
  202. apk_list: ${{ steps.scan.outputs.apk_list }}
  203. steps:
  204. - uses: actions/checkout@v7
  205. - name: Download and scan all manager APKs
  206. id: scan
  207. shell: bash
  208. env:
  209. MANAGER_LIST: ${{ needs.resolve-managers.outputs.manager_list }}
  210. GH_TOKEN: ${{ github.token }}
  211. run: |
  212. set -euo pipefail
  213. MANAGER_LIST=$(echo "$MANAGER_LIST" | sed 's/^\[,/[/')
  214. if ! echo "$MANAGER_LIST" | jq empty 2>/dev/null; then
  215. echo "ERROR: Invalid MANAGER_LIST JSON"
  216. echo "Raw: $MANAGER_LIST"
  217. exit 0
  218. fi
  219. OUT_APKS="${GITHUB_WORKSPACE:-$PWD}/manager_apks"
  220. MANIFEST="${GITHUB_WORKSPACE:-$PWD}/manifest.txt"
  221. rm -rf "$OUT_APKS" "$MANIFEST"
  222. mkdir -p "$OUT_APKS"
  223. : > "$MANIFEST"
  224. echo "$MANAGER_LIST" | jq -c '.[]' | while read -r M; do
  225. OWNER=$(echo "$M" | jq -r .owner)
  226. REPO=$(echo "$M" | jq -r .repo)
  227. SRC=$(echo "$M" | jq -r .source)
  228. RUN_ID=$(echo "$M" | jq -r .run_id)
  229. WANT=$(echo "$M" | jq -r '.want // ""')
  230. DLDIR="dl_${REPO}"
  231. mkdir -p "$DLDIR"
  232. (
  233. cd "$DLDIR"
  234. if [ "$SRC" = "release" ]; then
  235. curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  236. "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" > rel.json
  237. for URL in $(jq -r '.assets[] | select(.name | endswith(".apk")) | .browser_download_url' rel.json); do
  238. NAME="${URL##*/}"
  239. curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors -o "$NAME" "$URL"
  240. cp -f "$NAME" "$OUT_APKS/$NAME"
  241. echo "$OWNER|$REPO|$NAME" >> "$MANIFEST"
  242. done
  243. else
  244. AJSON=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  245. -H "Authorization: Bearer $GH_TOKEN" \
  246. "https://api.github.com/repos/${OWNER}/${REPO}/actions/runs/${RUN_ID}/artifacts")
  247. if [ -z "$WANT" ]; then
  248. ART_IDS=$(echo "$AJSON" | jq -r '.artifacts[].id')
  249. else
  250. ART_IDS=$(echo "$AJSON" | jq -r --arg want "$WANT" '
  251. ($want | split(",")) as $wl |
  252. .artifacts[] | select(.name as $n | any($wl[]; $n == .)) | .id')
  253. fi
  254. echo "$ART_IDS" | while read -r AID; do
  255. [ -n "$AID" ] || continue
  256. curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  257. -H "Authorization: Bearer $GH_TOKEN" \
  258. -o "_${AID}.zip" \
  259. "https://api.github.com/repos/${OWNER}/${REPO}/actions/artifacts/${AID}/zip"
  260. mkdir -p "_${AID}"
  261. unzip -o -q "_${AID}.zip" -d "_${AID}" || true
  262. rm -f "_${AID}.zip"
  263. for apk in _${AID}/*.apk; do
  264. [ -f "$apk" ] || continue
  265. NAME="$(basename "$apk")"
  266. cp -f "$apk" "$OUT_APKS/$NAME"
  267. echo "$OWNER|$REPO|$NAME" >> "$MANIFEST"
  268. done
  269. rm -rf "_${AID}"
  270. done
  271. fi
  272. )
  273. done
  274. APK_JSON="[]"
  275. while IFS='|' read -r o r n; do
  276. [ -n "$n" ] || continue
  277. APK_JSON=$(echo "$APK_JSON" | jq -c --arg o "$o" --arg r "$r" --arg n "$n" \
  278. '. + [{owner:$o,repo:$r,apk:$n}]')
  279. done < "$MANIFEST"
  280. echo "apk_list=${APK_JSON}" >> "$GITHUB_OUTPUT"
  281. echo "Found $(wc -l < "$MANIFEST") manager APKs"
  282. ls -1 "$OUT_APKS" || true
  283. echo "### Manager APKs" >> "$GITHUB_STEP_SUMMARY"
  284. cat "$MANIFEST" >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || true
  285. - name: Upload all manager APKs as one cross-blob
  286. uses: actions/upload-artifact@v7
  287. with:
  288. name: manager-apks
  289. path: manager_apks/
  290. if-no-files-found: warn
  291. # ── path B: clone every repo and build LKM + ksud + manager from source ──
  292. build-from-source:
  293. if: ${{ inputs.build_from_source }}
  294. runs-on: ubuntu-latest
  295. timeout-minutes: 90
  296. strategy:
  297. fail-fast: false
  298. matrix:
  299. include:
  300. - flavor: next
  301. repo: https://github.com/KernelSU-Next/KernelSU-Next.git
  302. ref: dev
  303. commit_input: kernelsu_next_commit
  304. - flavor: kernelsu
  305. repo: https://github.com/tiann/KernelSU.git
  306. ref: main
  307. commit_input: kernelsu_commit
  308. - flavor: resukisu
  309. repo: https://github.com/ReSukiSU/ReSukiSU.git
  310. ref: main
  311. commit_input: resukisu_commit
  312. env:
  313. FLAVOR: ${{ matrix.flavor }}
  314. steps:
  315. - name: Gate on root_flavor filter
  316. id: gate
  317. shell: bash
  318. env:
  319. ROOT_FLAVOR: ${{ inputs.root_flavor }}
  320. run: |
  321. WANT="${ROOT_FLAVOR:-All}"
  322. FLAVOR="${{ matrix.flavor }}"
  323. case "$WANT" in
  324. All) echo "run=true" >> "$GITHUB_OUTPUT" ;;
  325. KernelSU-Next) [[ "$FLAVOR" == "next" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  326. KernelSU) [[ "$FLAVOR" == "kernelsu" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  327. ReSukiSU) [[ "$FLAVOR" == "resukisu" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  328. *) echo "run=false" >> "$GITHUB_OUTPUT" ;;
  329. esac
  330. cat "$GITHUB_OUTPUT"
  331. - name: Resolve commit for this flavor
  332. if: steps.gate.outputs.run == 'true'
  333. id: commit
  334. shell: bash
  335. env:
  336. KSU_NEXT_COMMIT: ${{ inputs.kernelsu_next_commit }}
  337. KSU_COMMIT: ${{ inputs.kernelsu_commit }}
  338. RESUKISU_COMMIT: ${{ inputs.resukisu_commit }}
  339. run: |
  340. set -euo pipefail
  341. flavor="${{ matrix.flavor }}"
  342. case "$flavor" in
  343. next) pin="$KSU_NEXT_COMMIT" ;;
  344. kernelsu) pin="$KSU_COMMIT" ;;
  345. resukisu) pin="$RESUKISU_COMMIT" ;;
  346. esac
  347. if [[ -n "${pin:-}" ]]; then
  348. if ! [[ "$pin" =~ ^[0-9a-f]{40}$ ]]; then
  349. echo "ERROR: ${{ matrix.commit_input }} must be 40-char SHA (got: $pin)" >&2; exit 1
  350. fi
  351. echo "sha=$pin" >> "$GITHUB_OUTPUT"
  352. echo "Using pinned $flavor commit: $pin"
  353. else
  354. # latest tip of the tracking branch
  355. sha=$(git ls-remote "${{ matrix.repo }}" "refs/heads/${{ matrix.ref }}" | awk '{print $1; exit}')
  356. if ! [[ "$sha" =~ ^[0-9a-f]{40}$ ]]; then
  357. echo "ERROR: failed to resolve latest SHA for $flavor" >&2; exit 1
  358. fi
  359. echo "sha=$sha" >> "$GITHUB_OUTPUT"
  360. echo "Using latest $flavor tip on ${{ matrix.ref }}: $sha"
  361. fi
  362. - name: Clone ${{ matrix.flavor }} at pinned commit
  363. if: steps.gate.outputs.run == 'true'
  364. shell: bash
  365. run: |
  366. set -euo pipefail
  367. sha="${{ steps.commit.outputs.sha }}"
  368. repo="${{ matrix.repo }}"
  369. flavor="${{ matrix.flavor }}"
  370. dir="/tmp/$flavor-src"
  371. rm -rf "$dir"
  372. git clone --no-checkout "$repo" "$dir"
  373. git -C "$dir" fetch --depth=1 origin "$sha"
  374. git -C "$dir" checkout --detach "$sha"
  375. actual=$(git -C "$dir" rev-parse HEAD)
  376. if [[ "$actual" != "$sha" ]]; then
  377. echo "Commit mismatch: expected $sha got $actual" >&2; exit 1
  378. fi
  379. echo "Cloned $flavor at $actual"
  380. echo "SRC_DIR=$dir" >> "$GITHUB_ENV"
  381. - name: Setup Java 21
  382. if: steps.gate.outputs.run == 'true'
  383. uses: actions/setup-java@v5
  384. with:
  385. distribution: temurin
  386. java-version: 21
  387. - name: Setup Gradle
  388. if: steps.gate.outputs.run == 'true'
  389. uses: gradle/actions/setup-gradle@v5
  390. - name: Setup Android SDK
  391. if: steps.gate.outputs.run == 'true'
  392. uses: android-actions/setup-android@v3
  393. - name: Setup Rust (stable + Android targets)
  394. if: steps.gate.outputs.run == 'true'
  395. uses: dtolnay/rust-toolchain@stable
  396. with:
  397. targets: aarch64-linux-android,x86_64-linux-android,armv7-linux-androideabi
  398. components: clippy,rustfmt
  399. - name: Setup cargo-ndk + NDK helpers
  400. if: steps.gate.outputs.run == 'true'
  401. shell: bash
  402. run: |
  403. set -euo pipefail
  404. # NDK comes via setup-android; install cargo-ndk for cross building ksud
  405. cargo install cargo-ndk --locked 2>&1 | tail -5 || true
  406. # Show NDK location
  407. echo "ANDROID_NDK_HOME=${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}" | tee -a "$GITHUB_ENV"
  408. ls -d "$ANDROID_HOME/ndk/"* 2>/dev/null | head -5 || true
  409. - name: Build ksud + ksuinit (Rust userspace)
  410. if: steps.gate.outputs.run == 'true'
  411. shell: bash
  412. working-directory: /tmp/${{ matrix.flavor }}-src
  413. run: |
  414. set -euo pipefail
  415. dir="$SRC_DIR"
  416. cd "$dir"
  417. # Some repos keep Rust at root Cargo.toml, others in userspace/
  418. # Try root first, then userspace/ksud, then userspace/
  419. build_ksud_for_target() {
  420. local target="$1"
  421. echo "== Building ksud for $target =="
  422. if [ -f Cargo.toml ]; then
  423. cargo ndk -t arm64-v8a -t x86_64 -t armeabi-v7a build --release 2>&1 | tail -30 || \
  424. cargo build --target "$target" --release 2>&1 | tail -30 || true
  425. elif [ -f userspace/Cargo.toml ]; then
  426. cargo ndk -t arm64-v8a -t x86_64 -t armeabi-v7a build --release --manifest-path userspace/Cargo.toml 2>&1 | tail -30 || true
  427. fi
  428. }
  429. # Ensure NDK env for cargo-ndk
  430. export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}"
  431. if ls userspace 2>/dev/null; then
  432. echo "userspace crate found"
  433. fi
  434. # Build aarch64 + x86_64 ksud — skip gracefully if Rust toolchain missing profile
  435. build_ksud_for_target aarch64-linux-android || echo "ksud aarch64 build skipped/failed — manager gradle will still produce APK"
  436. build_ksud_for_target x86_64-linux-android || true
  437. # Copy any built ksud binaries into expected manager jniLibs locations so repack can pick them up
  438. if [ -d target ]; then
  439. for f in $(find target -type f -name ksud 2>/dev/null | head -20); do
  440. echo "Found built ksud: $f"
  441. done
  442. fi
  443. - name: Build LKM (single representative KMI)
  444. if: steps.gate.outputs.run == 'true'
  445. shell: bash
  446. working-directory: /tmp/${{ matrix.flavor }}-src
  447. run: |
  448. set -euo pipefail
  449. dir="$SRC_DIR"
  450. cd "$dir"
  451. # LKM build matrix is handled upstream via ddk-lkm.yml (builds one .ko per KMI).
  452. # For the self-build mode we produce a single LKM for the host's KMI
  453. # so the repack step can embed it. Full per-KMI matrix is kernel-dependent
  454. # and is built in the kernel workflows; here we just ensure a kernelsu.ko
  455. # artifact exists if the repo's kernel/ directory provides it.
  456. if [ -f kernel/Makefile ] || [ -f kernel/Kbuild ]; then
  457. echo "kernel/ directory found — attempting lightweight LKM check (syntax only)"
  458. # Don't do full cross-kernel build here; verify sources compile-check with host clang if available
  459. if command -v clang >/dev/null 2>&1; then
  460. echo "clang $(clang --version | head -1)"
  461. fi
  462. # Touch a marker so later steps know LKM sources were present
  463. echo "LKM sources present at $dir/kernel" > /tmp/lkm-$FLAVOR.marker || true
  464. else
  465. echo "No kernel/ Makefile — skipping LKM build (manager-only repo checkout)"
  466. fi
  467. - name: Build Manager APK(s)
  468. if: steps.gate.outputs.run == 'true'
  469. shell: bash
  470. working-directory: /tmp/${{ matrix.flavor }}-src
  471. env:
  472. NEXT_KEYSTORE: ${{ secrets.NEXT_KEYSTORE }}
  473. NEXT_KEYSTORE_PASSWORD: ${{ secrets.NEXT_KEYSTORE_PASSWORD }}
  474. NEXT_KEY_ALIAS: ${{ secrets.NEXT_KEY_ALIAS }}
  475. NEXT_KEY_PASSWORD: ${{ secrets.NEXT_KEY_PASSWORD }}
  476. KERNELSU_KEYSTORE: ${{ secrets.KERNELSU_KEYSTORE }}
  477. KERNELSU_KEYSTORE_PASSWORD: ${{ secrets.KERNELSU_KEYSTORE_PASSWORD }}
  478. KERNELSU_KEY_ALIAS: ${{ secrets.KERNELSU_KEY_ALIAS }}
  479. KERNELSU_KEY_PASSWORD: ${{ secrets.KERNELSU_KEY_PASSWORD }}
  480. RESUKISU_KEYSTORE: ${{ secrets.RESUKISU_KEYSTORE }}
  481. RESUKISU_KEYSTORE_PASSWORD: ${{ secrets.RESUKISU_KEYSTORE_PASSWORD }}
  482. RESUKISU_KEY_ALIAS: ${{ secrets.RESUKISU_KEY_ALIAS }}
  483. RESUKISU_KEY_PASSWORD: ${{ secrets.RESUKISU_KEY_PASSWORD }}
  484. run: |
  485. set -euo pipefail
  486. dir="$SRC_DIR"
  487. cd "$dir"
  488. if [ ! -d manager ]; then
  489. echo "No manager/ directory in $FLAVOR checkout — skipping" >&2
  490. exit 0
  491. fi
  492. cd manager
  493. # ── Resolve signing key for this flavor ──
  494. # If repo Secrets MANAGER_KEYSTORE_<FLAVOR>_B64 etc. are set, use the persistent
  495. # per-variant key (so your apps stay signed with the same cert across builds).
  496. # Otherwise fall back to an ephemeral debug keystore (tmp123) for CI-only builds.
  497. # Generate with: bash .github/scripts/generate-manager-signing-keys.sh
  498. # and add the 4 secrets per variant from .github/signing-keys/<flavor>/info.env.
  499. resolve_signing_key() {
  500. local flavor="$FLAVOR"
  501. local upper b64 pw alias kp
  502. upper=$(echo "$flavor" | tr '[:lower:]' '[:upper:]')
  503. case "$flavor" in
  504. next) b64="${NEXT_KEYSTORE:-}"; pw="${NEXT_KEYSTORE_PASSWORD:-}"; alias="${NEXT_KEY_ALIAS:-}"; kp="${NEXT_KEY_PASSWORD:-}" ;;
  505. kernelsu) b64="${KERNELSU_KEYSTORE:-}"; pw="${KERNELSU_KEYSTORE_PASSWORD:-}"; alias="${KERNELSU_KEY_ALIAS:-}"; kp="${KERNELSU_KEY_PASSWORD:-}" ;;
  506. resukisu) b64="${RESUKISU_KEYSTORE:-}"; pw="${RESUKISU_KEYSTORE_PASSWORD:-}"; alias="${RESUKISU_KEY_ALIAS:-}"; kp="${RESUKISU_KEY_PASSWORD:-}" ;;
  507. esac
  508. if [ -n "${b64:-}" ] && [ -n "${pw:-}" ] && [ -n "${alias:-}" ]; then
  509. echo "Using persistent signing key for $flavor (alias=$alias)"
  510. echo "$b64" | base64 -d > "/tmp/manager-$flavor.jks"
  511. kp="${kp:-$pw}"
  512. {
  513. echo "KEYSTORE_PASSWORD=$pw"
  514. echo "KEY_ALIAS=$alias"
  515. echo "KEY_PASSWORD=$kp"
  516. echo "KEYSTORE_FILE=/tmp/manager-$flavor.jks"
  517. } > gradle.properties
  518. # Also expose for repack_apk.py (kernelsu)
  519. echo "/tmp/manager-$flavor.jks" > "/tmp/repack-keystore-$flavor.path"
  520. echo "$alias" > "/tmp/repack-alias-$flavor.path"
  521. echo "$pw" > "/tmp/repack-storepass-$flavor.path"
  522. echo "$kp" > "/tmp/repack-keypass-$flavor.path"
  523. # Optional: log cert hash/size the LKM would pin (EXPECTED_SIZE/HASH equivalent)
  524. if command -v keytool >/dev/null 2>&1; then
  525. keytool -exportcert -alias "$alias" -keystore "/tmp/manager-$flavor.jks" -storepass "$pw" -file "/tmp/manager-$flavor.der" 2>/dev/null || true
  526. if [ -f "/tmp/manager-$flavor.der" ]; then
  527. sz=$(stat -c%s "/tmp/manager-$flavor.der"); hx=$(printf '0x%04x' "$sz"); hs=$(sha256sum "/tmp/manager-$flavor.der" | awk '{print $1}')
  528. echo " cert size=$hx ($sz) sha256=$hs"
  529. fi
  530. fi
  531. return 0
  532. fi
  533. return 1
  534. }
  535. if ! resolve_signing_key; then
  536. echo "No persistent key for $FLAVOR — generating ephemeral debug keystore"
  537. if [ ! -f key.jks ] && [ ! -f ../key.jks ]; then
  538. keytool -genkeypair -alias tmp -keyalg RSA -keysize 2048 -validity 1 \
  539. -storepass tmp123 -keypass tmp123 \
  540. -dname "CN=CI Build" -keystore /tmp/tmp-key.jks -storetype JKS 2>&1 | tail -3 || true
  541. if [ -f /tmp/tmp-key.jks ]; then
  542. {
  543. echo "KEYSTORE_PASSWORD=tmp123"
  544. echo "KEY_ALIAS=tmp"
  545. echo "KEY_PASSWORD=tmp123"
  546. echo "KEYSTORE_FILE=/tmp/tmp-key.jks"
  547. } >> gradle.properties
  548. fi
  549. fi
  550. fi
  551. flavor="$FLAVOR"
  552. echo "Building manager for flavor=$flavor"
  553. if [ "$flavor" = "next" ]; then
  554. # KernelSU-Next: two variants (normal + spoofed via ./spoof)
  555. ./gradlew clean assembleRelease 2>&1 | tail -40
  556. if [ -x ./spoof ]; then
  557. echo "Building spoofed variant"
  558. chmod +x ./spoof
  559. ./spoof || true
  560. ./gradlew clean assembleRelease 2>&1 | tail -40 || true
  561. # spoof overwrites same APK path; stash both with distinct names
  562. for apk in app/build/outputs/apk/release/*.apk; do
  563. [ -f "$apk" ] && cp -f "$apk" "/tmp/next-spoofed.apk" || true
  564. done
  565. # Rebuild normal after spoof stomp if needed — gradle re-run is cheap due to caching
  566. git checkout -- . 2>/dev/null || true
  567. if [ -f /tmp/tmp-key.jks ]; then
  568. {
  569. echo "KEYSTORE_PASSWORD=tmp123"
  570. echo "KEY_ALIAS=tmp"
  571. echo "KEY_PASSWORD=tmp123"
  572. echo "KEYSTORE_FILE=/tmp/tmp-key.jks"
  573. } > gradle.properties
  574. fi
  575. ./gradlew clean assembleRelease 2>&1 | tail -40 || true
  576. fi
  577. elif [ "$flavor" = "kernelsu" ]; then
  578. # tiann/KernelSU: single manager, repack embeds ksud+LKM
  579. ./gradlew clean assembleRelease 2>&1 | tail -40 || ./gradlew assembleRelease 2>&1 | tail -40
  580. if [ -f ../repack_apk.py ] && [ -n "$(find ../target -name ksud 2>/dev/null | head -1)" ]; then
  581. echo "Repacking APK with ksud+LKM via repack_apk.py"
  582. # Prefer persistent per-variant key (written by resolve_signing_key), else ephemeral
  583. REPACK_KS="/tmp/manager-$flavor.jks"
  584. REPACK_ALIAS="$(cat "/tmp/repack-alias-$flavor.path" 2>/dev/null || echo repack)"
  585. REPACK_SP="$(cat "/tmp/repack-storepass-$flavor.path" 2>/dev/null || echo repack123)"
  586. REPACK_KP="$(cat "/tmp/repack-keypass-$flavor.path" 2>/dev/null || echo repack123)"
  587. if [ ! -f "$REPACK_KS" ]; then
  588. REPACK_KS="/tmp/repack.jks"; REPACK_ALIAS="repack"; REPACK_SP="repack123"; REPACK_KP="repack123"
  589. if [ ! -f /tmp/repack.jks ]; then
  590. keytool -genkeypair -alias repack -keyalg RSA -keysize 2048 -validity 1 \
  591. -storepass repack123 -keypass repack123 -dname "CN=Repack" -keystore /tmp/repack.jks -storetype JKS 2>&1 | tail -3 || true
  592. fi
  593. fi
  594. python3 ../repack_apk.py repack \
  595. --keystore "$REPACK_KS" --alias "$REPACK_ALIAS" --storepass "$REPACK_SP" --keypass "$REPACK_KP" 2>&1 | tail -20 || true
  596. fi
  597. elif [ "$flavor" = "resukisu" ]; then
  598. # ReSukiSU: three variants — Manager-release, Spoofed-Manager-release, Manager-debug
  599. if [ -x ./randomizer ]; then chmod +x ./randomizer; fi
  600. ./gradlew clean assembleRelease 2>&1 | tail -40 || true
  601. ./gradlew assembleDebug 2>&1 | tail -20 || true
  602. if [ -x ./randomizer ]; then
  603. ./randomizer 2>&1 | tail -10 || true
  604. ./gradlew clean assembleRelease 2>&1 | tail -40 || true
  605. fi
  606. fi
  607. # Collect whatever was produced
  608. out="/tmp/manager_apks_${flavor}"
  609. mkdir -p "$out"
  610. for apk in app/build/outputs/apk/release/*.apk app/build/outputs/apk/debug/*.apk; do
  611. [ -f "$apk" ] || continue
  612. base="$(basename "$apk")"
  613. # Prefix with flavor to avoid collisions when merging
  614. cp -f "$apk" "$out/${flavor}-${base}"
  615. echo "Collected $out/${flavor}-${base}"
  616. done
  617. # Also stash any repacked APKs
  618. for apk in app/build/outputs/apk/release/*-repacked*.apk build/outputs/apk/*.apk; do
  619. [ -f "$apk" ] || continue
  620. cp -f "$apk" "$out/" 2>/dev/null || true
  621. done
  622. # Also copy prebuilt ksud artifacts if repack placed them
  623. if [ -f /tmp/next-spoofed.apk ]; then
  624. cp -f /tmp/next-spoofed.apk "$out/${flavor}-spoofed.apk" || true
  625. fi
  626. ls -lh "$out" 2>&1 | head -30 || true
  627. if [ -z "$(ls -A "$out" 2>/dev/null)" ]; then
  628. echo "WARNING: no APK produced for $flavor" >&2
  629. fi
  630. echo "MANAGER_OUT_$flavor=$out" >> "$GITHUB_ENV"
  631. - name: Upload built manager APKs (${{ matrix.flavor }})
  632. if: steps.gate.outputs.run == 'true'
  633. uses: actions/upload-artifact@v7
  634. with:
  635. name: manager-apks-src-${{ matrix.flavor }}
  636. path: /tmp/manager_apks_${{ matrix.flavor }}/*.apk
  637. if-no-files-found: warn
  638. - name: Upload built LKM marker (${{ matrix.flavor }})
  639. if: steps.gate.outputs.run == 'true'
  640. uses: actions/upload-artifact@v7
  641. with:
  642. name: lkm-${{ matrix.flavor }}
  643. path: |
  644. /tmp/lkm-*.marker
  645. /tmp/${{ matrix.flavor }}-src/kernel/*.ko
  646. /tmp/${{ matrix.flavor }}-src/target/**/kernelsu.ko
  647. if-no-files-found: ignore
  648. # Collector for source-built managers: merge per-flavor uploads into one cross-blob
  649. # so downstream consumers can use the same artifact name regardless of path.
  650. collect-source-managers:
  651. needs: build-from-source
  652. if: ${{ inputs.build_from_source }}
  653. runs-on: ubuntu-latest
  654. outputs:
  655. apk_list: ${{ steps.scan.outputs.apk_list }}
  656. steps:
  657. - name: Download all source-built manager artifacts
  658. uses: actions/download-artifact@v7
  659. with:
  660. pattern: manager-apks-src-*
  661. path: manager_apks
  662. merge-multiple: true
  663. - name: Scan collected APKs
  664. id: scan
  665. shell: bash
  666. run: |
  667. set -euo pipefail
  668. mkdir -p manager_apks
  669. find manager_apks -type f -name '*.apk' | sort | tee /tmp/list.txt || true
  670. count=$(wc -l < /tmp/list.txt 2>/dev/null | tr -d ' ' || echo 0)
  671. echo "Found $count source-built manager APKs"
  672. ls -lh manager_apks 2>&1 | head -40 || true
  673. APK_JSON="[]"
  674. while IFS= read -r f; do
  675. [ -n "$f" ] || continue
  676. n="$(basename "$f")"
  677. APK_JSON=$(echo "$APK_JSON" | jq -c --arg n "$n" '. + [{apk:$n}]')
  678. done < /tmp/list.txt
  679. echo "apk_list=${APK_JSON}" >> "$GITHUB_OUTPUT"
  680. echo "### Source-built Manager APKs" >> "$GITHUB_STEP_SUMMARY"
  681. cat /tmp/list.txt >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || echo "_none_" >> "$GITHUB_STEP_SUMMARY"
  682. - name: Upload merged manager-apks (source)
  683. uses: actions/upload-artifact@v7
  684. with:
  685. name: manager-apks
  686. path: manager_apks/
  687. if-no-files-found: warn
  688. # Unified publisher: works for both fetch and source paths (whichever produced manager-apks)
  689. publish-manager-artifacts:
  690. needs:
  691. - list-manager-apks
  692. - collect-source-managers
  693. if: ${{ always() && (needs.list-manager-apks.outputs.apk_list != '' || needs.collect-source-managers.outputs.apk_list != '') }}
  694. runs-on: ubuntu-latest
  695. strategy:
  696. fail-fast: false
  697. matrix:
  698. item: ${{ fromJson(needs.list-manager-apks.outputs.apk_list != '' && needs.list-manager-apks.outputs.apk_list || needs.collect-source-managers.outputs.apk_list) }}
  699. steps:
  700. - uses: actions/checkout@v7
  701. - name: Download manager cross-blob
  702. uses: actions/download-artifact@v7
  703. with:
  704. name: manager-apks
  705. path: manager_apks
  706. merge-multiple: true
  707. - name: Upload ${{ matrix.item.apk }} as its own artifact
  708. uses: actions/upload-artifact@v7
  709. with:
  710. name: manager-apk-${{ matrix.item.apk }}
  711. path: manager_apks/${{ matrix.item.apk }}
  712. if-no-files-found: error