managers.yml 35 KB

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