From 5f12c162f4ac061baedbe4e1cb94c717c838e6fb Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Fri, 10 Jul 2026 21:52:53 -0600 Subject: [PATCH] ADFA-4664: add upstream patch staging and wire it into iiab-android Add tools/upstream-patches/ (README, idempotent applier, patches/, overlays/) to carry in-flight iiab/iiab changes into rootfs builds before they merge upstream. First carry: 0001-kolibri-trust-system-ca.patch, mirroring iiab/iiab #4442 (a patch of a patch, validated to apply and be idempotent). iiab-android pre-seeds /opt/iiab/iiab and runs the applier before the generic installer runs Ansible; safe because that installer clones iiab only if absent and never pulls/resets an existing checkout. --- iiab-android | 29 +++++ tools/upstream-patches/README.md | Bin 0 -> 5845 bytes .../apply-upstream-patches.sh | 100 ++++++++++++++++++ tools/upstream-patches/overlays/.gitkeep | 1 + .../patches/0000-EXAMPLE-template.patch.txt | 21 ++++ .../0001-kolibri-trust-system-ca.patch | 100 ++++++++++++++++++ 6 files changed, 251 insertions(+) create mode 100755 tools/upstream-patches/README.md create mode 100755 tools/upstream-patches/apply-upstream-patches.sh create mode 100755 tools/upstream-patches/overlays/.gitkeep create mode 100755 tools/upstream-patches/patches/0000-EXAMPLE-template.patch.txt create mode 100755 tools/upstream-patches/patches/0001-kolibri-trust-system-ca.patch diff --git a/iiab-android b/iiab-android index 451e4ac4..0f9c7079 100644 --- a/iiab-android +++ b/iiab-android @@ -332,6 +332,30 @@ local current_pwd="$(pwd)" fi } +# Pre-seed /opt/iiab/iiab and apply AppDevForAll's in-flight upstream patches +# BEFORE the generic installer runs Ansible. Safe because the upstream installer +# (/usr/sbin/iiab from iiab-factory) clones iiab only if it is absent and never +# pulls/resets an existing checkout, so our patched tree survives into the run +# (e.g. the kolibri role reads its .patch during it). The applier is idempotent +# and exits non-zero on context drift, so a bad carry fails the build loudly +# instead of baking a half-patched tree. See tools/upstream-patches/README.md. +apply_upstream_patches() { + local applier="${K2GO_OPT_DIR}/tools/upstream-patches/apply-upstream-patches.sh" + if [ ! -f "$applier" ]; then + log "No upstream-patches applier found; skipping." + return 0 + fi + command -v git > /dev/null 2>&1 || apt-get -y install git + command -v patch > /dev/null 2>&1 || apt-get -y install patch + mkdir -p /opt/iiab + if [ ! -d /opt/iiab/iiab ]; then + log "Pre-seeding /opt/iiab/iiab so upstream patches apply before Ansible..." + git clone https://github.com/iiab/iiab /opt/iiab/iiab || die "could not clone iiab/iiab to pre-seed upstream patches" + fi + log "Applying in-flight upstream patches to /opt/iiab/iiab..." + bash "$applier" --root /opt/iiab/iiab || die "upstream patch apply failed (see log above)" +} + #----------------------------- # Demo Contents Installer #----------------------------- @@ -533,6 +557,11 @@ disable_role_32bits kiwix "$LOCAL_VARS_DEST" #----------------------------- ensure_proot_safe_env +#----------------------------- +# Apply our in-flight upstream patches to /opt/iiab/iiab before Ansible runs +#----------------------------- +apply_upstream_patches + #----------------------------- # Fetch install.txt with fallback and run it #----------------------------- diff --git a/tools/upstream-patches/README.md b/tools/upstream-patches/README.md new file mode 100755 index 0000000000000000000000000000000000000000..61ba458749a3ec19c40591c8d84bedce8bf9be8a GIT binary patch literal 5845 zcmeHKU2oh*63jDyMI(C1?%tAXCvQ?o4_Pj_{7Rr88|9ei-CIvP}{TJk|1bbp{#8LTyaH_o^7c$@vMTxZ$s z`7wo8sj-f#R`mz=bYg1N1JmH_U_IBg!p)O5X1TJaU@5drOXRH7TJ3l8tCWX+=PJ;C z4AiNK9RD$D#}wEHvOQI!54Iy`ZD@R!U0u1zq7Cyc|gPC=c%k-SG{g;;)G|8(niZ*uZ_*Y_`pOV{3N<<7I4RMn$g(?YbTwTg?*+uX(awirv!UBLddr{&TCdPGl;#w`ixAG` z2{Vrcy+v!%8<|6S4n0Gfmpbn5ciQ_Ap#tHyV?O``JY-3*mTCZD82y&KeVTqeI|KHC zopcJr5IYrGbfLu`xGWKBHXNkJQ@o)-O?C&aQ6?Dtqnhj(vaBr2>@!0LnC{=+e)rSc zqO0jsKUb&-Kt#A#HG6&N=^rSje?Ysn?fn`dB5Cq7obyB%zKs#D=t7YAs7CfH^#5Z@ zNvK5)K9vj66^*@aG}n2p8xUBOg@K+VfUD4h&{96UCNRR$&JH1tfBc2uME}80{;i11 zoBUgE#>1N;2Ek2v#i+^eK6&&NAsFga<{DsI!ypY9=y))y;tpVVT6Nkv>k zS6|Xza$A6$RRobDgc9nTXQ7Z?gbtEG#==fS$kDJ^(ule21C`I&Qql?}5BwMamTsVz zC_<_plnYuxLov84**8iXL>+50A;lHkBG7B#_d%pzXt-BLh&bHKG_%=31_2g^^cR^e z{F6ky>V!|RMN1#fra|xC-TsrxBeZMpbP1yX`IZKZ9t{?+l$z|R&tP^C;aGB1774#+ z*e4o*r|_`qw9zVw$R08SK8rss%5fJh`^l7xhio?g=ANCnmSY27fa;2`A$TOa90~-W zWF0@YPtbbvAdX#*PSIWl%}u=)Njny?XV!+>-qN zby*a}dW$BNX~N5t8=%_xai8oA4Jr=B1xdWxV=sA>7!kJrdUg-GVqdOtDQScp?px~u zVjp?0_7GlQIUNl)e!bN+Ll z`F!3hap&)0^;B%IFkX`5N7W5y zhl#sX7A+_403sW~vuW^Fxwyd!qnCo@w#ilUNIa=dM-fnvZf{tewj|+0V;|4&$Hn6oP?~b)rw&ojL zmeE5GXCscco_H;`TC6UTzUw4bP)7ug~_ z1`IxSYV2`KZC}A?<(nY~8JyL%xGcop7@*W}TsR_6;A0@kM~~)jo(Csfs9-sqV$L5U zxOm%pjjsfN@(Wm)oXlnM!$J%aR@TVwCrQQ49*f-kAtGCoT#mc0NTKM3tQFwy6S0^N zNW+!!BUfX9_2aXID+R}pFfyL<^^^EI(Msa}kUSDFwsr-J&Jnz?UoAMMte*1iW)FwcsuKD zZU>pw=e{W`1+cwMQ1xQtPreRNo5w~Y>60udj_J>@$~vOo&nX7S6b$i8^_c|qU>ffoc`5O_i01%dxB1pWeQS0`%# literal 0 HcmV?d00001 diff --git a/tools/upstream-patches/apply-upstream-patches.sh b/tools/upstream-patches/apply-upstream-patches.sh new file mode 100755 index 00000000..1cc01495 --- /dev/null +++ b/tools/upstream-patches/apply-upstream-patches.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# +# apply-upstream-patches.sh — apply AppDevForAll's in-flight changes to the +# IIAB project checkout (/opt/iiab/iiab) that are proposed upstream but not yet +# merged. Idempotent: a patch that is already present (e.g. upstream merged it, +# or a previous run applied it) is detected and skipped, never applied twice. +# +# Design & conventions: see README.md in this folder. +# +# Usage (run inside the proot guest, after /opt/iiab/iiab exists and BEFORE the +# Ansible roles run): +# tools/upstream-patches/apply-upstream-patches.sh [--root DIR] [--dry-run] +# +# Exit codes: 0 = all patches applied or already-present; 1 = one or more +# patches could not be applied (context drift → needs regeneration). We fail +# loudly rather than bake a half-patched tree. + +set -euo pipefail + +ROOT="/opt/iiab/iiab" # the IIAB project checkout to patch +DRY_RUN=0 +STRIP=1 # patches are generated relative to the repo root → -p1 + +SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PATCH_DIR="${SELF_DIR}/patches" +OVERLAY_DIR="${SELF_DIR}/overlays" # optional: whole-file replacements + +log() { printf '[upstream-patches] %s\n' "$*"; } +warn() { printf '[upstream-patches] WARN: %s\n' "$*" >&2; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --root) ROOT="$2"; shift 2 ;; + --dry-run) DRY_RUN=1; shift ;; + -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) warn "unknown arg: $1"; exit 2 ;; + esac +done + +[[ -d "$ROOT" ]] || { warn "target root not found: $ROOT (nothing to patch)"; exit 0; } +command -v patch >/dev/null 2>&1 || { warn "'patch' not installed in the guest"; exit 1; } + +rc=0 +applied=0 skipped=0 failed=0 + +# --- 1) Unified-diff patches (the primary mechanism) ------------------------ +if [[ -d "$PATCH_DIR" ]]; then + # Deterministic order: NNNN- numeric prefix. *.patch only (templates use .txt). + shopt -s nullglob + for f in "$PATCH_DIR"/*.patch; do + name="$(basename "$f")" + # Already present? (upstream merged it, or a prior run applied it.) A clean + # REVERSE apply proves the change is already in the tree → skip. + if patch -p"$STRIP" -d "$ROOT" -R --dry-run < "$f" >/dev/null 2>&1; then + log "skip (already present): $name" + skipped=$((skipped+1)) + continue + fi + # Not present → does it apply cleanly forward? + if patch -p"$STRIP" -d "$ROOT" --dry-run < "$f" >/dev/null 2>&1; then + if [[ "$DRY_RUN" -eq 1 ]]; then + log "would apply: $name" + else + patch -p"$STRIP" -d "$ROOT" < "$f" >/dev/null + log "applied: $name" + fi + applied=$((applied+1)) + else + warn "could NOT apply (context drift / partially applied): $name" + warn " → regenerate against the current /opt/iiab/iiab, or drop if superseded." + failed=$((failed+1)); rc=1 + fi + done + shopt -u nullglob +fi + +# --- 2) Optional whole-file overlays ---------------------------------------- +# For cases better expressed as a full-file replacement than a diff (e.g. +# replacing a file that is itself a .patch). Mirror the tree under overlays/ +# rooted at the repo root; a file is copied only if it differs (idempotent). +if [[ -d "$OVERLAY_DIR" ]]; then + while IFS= read -r -d '' src; do + rel="${src#"$OVERLAY_DIR"/}" + dst="$ROOT/$rel" + if [[ -f "$dst" ]] && cmp -s "$src" "$dst"; then + log "skip overlay (identical): $rel"; skipped=$((skipped+1)); continue + fi + if [[ "$DRY_RUN" -eq 1 ]]; then + log "would overlay: $rel" + else + mkdir -p "$(dirname "$dst")" + cp -a "$src" "$dst" + log "overlay: $rel" + fi + applied=$((applied+1)) + done < <(find "$OVERLAY_DIR" -type f ! -name '.gitkeep' -print0 2>/dev/null) +fi + +log "summary: applied=${applied} skipped=${skipped} failed=${failed} (root=${ROOT})" +exit "$rc" diff --git a/tools/upstream-patches/overlays/.gitkeep b/tools/upstream-patches/overlays/.gitkeep new file mode 100755 index 00000000..0e669ecb --- /dev/null +++ b/tools/upstream-patches/overlays/.gitkeep @@ -0,0 +1 @@ +# Optional whole-file overlays mirror the /opt/iiab/iiab tree here. See README.md. diff --git a/tools/upstream-patches/patches/0000-EXAMPLE-template.patch.txt b/tools/upstream-patches/patches/0000-EXAMPLE-template.patch.txt new file mode 100755 index 00000000..86383ae6 --- /dev/null +++ b/tools/upstream-patches/patches/0000-EXAMPLE-template.patch.txt @@ -0,0 +1,21 @@ +EXAMPLE / TEMPLATE — not applied (the applier only picks up *.patch, not *.txt). +Copy this header, generate a real diff, and save as NNNN--.patch. + +Upstream-PR: https://github.com/iiab/iiab/pull/XXXX # or "not yet submitted" +Upstream-Status: open # open | merged-pending-release | superseded +Applies-to: roles//... # path within /opt/iiab/iiab +Summary: one sober line: what changes and why + +# Generate (relative to the repo root so it applies with -p1 over /opt/iiab/iiab): +# git -C /opt/iiab/iiab diff > 0001--.patch +# # or, mirroring the upstream commit: +# git -C /opt/iiab/iiab format-patch -1 --stdout > 0001--.patch + +diff --git a/roles//tasks/main.yml b/roles//tasks/main.yml +index 0000000..0000000 100644 +--- a/roles//tasks/main.yml ++++ b/roles//tasks/main.yml +@@ -1,3 +1,3 @@ + # (example context) +-old line ++new line diff --git a/tools/upstream-patches/patches/0001-kolibri-trust-system-ca.patch b/tools/upstream-patches/patches/0001-kolibri-trust-system-ca.patch new file mode 100755 index 00000000..6b8bb402 --- /dev/null +++ b/tools/upstream-patches/patches/0001-kolibri-trust-system-ca.patch @@ -0,0 +1,100 @@ +Upstream-PR: https://github.com/iiab/iiab/pull/4442 +Upstream-Status: open +Applies-to: roles/proot_services/files/kolibri-00-prevent_ip_error_out_due_A15_restrictions.patch +Summary: Kolibri: trust the system CA bundle so "Kolibri Studio (online)" import works under Android/proot + +Mirrors upstream commit bbf6cb8 ("[kolibri] update proot patch: trust system CA +so online import works"). This edits the kolibri proot patch file that the +proot_services role applies to the installed Kolibri package. If the pinned +iiab/iiab commit's copy of that file drifts, the applier fails loudly and this +should be regenerated. Remove once #4442 merges and ships in the pinned commit. + +diff --git a/roles/proot_services/files/kolibri-00-prevent_ip_error_out_due_A15_restrictions.patch b/roles/proot_services/files/kolibri-00-prevent_ip_error_out_due_A15_restrictions.patch +index 2ab48703d5..53924dafe7 100644 +--- a/roles/proot_services/files/kolibri-00-prevent_ip_error_out_due_A15_restrictions.patch ++++ b/roles/proot_services/files/kolibri-00-prevent_ip_error_out_due_A15_restrictions.patch +@@ -1,9 +1,10 @@ + This patch is necessary on, + ++* Android 16 - yes + * Android 15 - yes + * Android 14 - untested + * Android 13 - untested +-* Android 12 - no ++* Android 12 - untested + * Android 11 - untested + * Andoird 10 - untested + +@@ -41,3 +42,72 @@ index a11b0e9..4b0b8b7 100644 + + + def get_urls(listen_port=None): ++--- a/usr/lib/python3/dist-packages/kolibri/utils/http_session.py +++++ b/usr/lib/python3/dist-packages/kolibri/utils/http_session.py ++@@ -19,6 +19,24 @@ ++ class SameHostSession(requests.Session): ++ """A :class:`requests.Session` that refuses cross-host redirects.""" ++ +++ def __init__(self, *args, **kwargs): +++ super().__init__(*args, **kwargs) +++ # IIAB/proot: prefer the system CA bundle for outbound HTTPS. Under +++ # Android/proot the cert store requests bundles can be stale or absent +++ # while the system store is valid, which made every content-server +++ # request fail and disabled "Kolibri Studio (online)" despite internet. +++ import os +++ +++ for _ca in ( +++ os.environ.get("REQUESTS_CA_BUNDLE"), +++ os.environ.get("SSL_CERT_FILE"), +++ "/etc/ssl/certs/ca-certificates.crt", +++ "/etc/ssl/cert.pem", +++ ): +++ if _ca and os.path.exists(_ca): +++ self.verify = _ca +++ break +++ ++ def get_redirect_target(self, resp): ++ target = super().get_redirect_target(resp) ++ if target is None: ++--- a/usr/lib/python3/dist-packages/kolibri/core/content/api.py +++++ b/usr/lib/python3/dist-packages/kolibri/core/content/api.py ++@@ -2066,6 +2066,41 @@ ++ NetworkLocationConnectionFailure, ++ NetworkLocationNotFound, ++ ): +++ # IIAB/proot fallback: NetworkClient can wrongly report the content +++ # server as unreachable under Android/proot due stalled ca-certificates. +++ # When the device actually has internet, confirm reachability +++ # with a plain request that uses the system CA bundle and follows +++ # redirects, so the online import option is not disabled wrongly. +++ import os +++ import requests as _requests +++ +++ _verify = True +++ for _ca in ( +++ os.environ.get("REQUESTS_CA_BUNDLE"), +++ os.environ.get("SSL_CERT_FILE"), +++ "/etc/ssl/certs/ca-certificates.crt", +++ "/etc/ssl/cert.pem", +++ ): +++ if _ca and os.path.exists(_ca): +++ _verify = _ca +++ break +++ try: +++ _resp = _requests.get( +++ CENTRAL_CONTENT_BASE_URL.rstrip("/") + "/api/public/info", +++ timeout=15, +++ allow_redirects=True, +++ verify=_verify, +++ ) +++ if _resp.status_code == 200: +++ try: +++ _data = _resp.json() +++ except ValueError: +++ _data = {} +++ _data["available"] = True +++ _data["status"] = "online" +++ return Response(_data) +++ except Exception: +++ pass ++ return Response({"status": "offline", "available": False})