managers.yml 36 KB

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