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 00000000..61ba4587 Binary files /dev/null and b/tools/upstream-patches/README.md differ 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})