diff --git a/docs/superpowers/contracts/linux-config-mtk-lib-split-2026-08-16.json b/docs/superpowers/contracts/linux-config-mtk-lib-split-2026-08-16.json new file mode 100644 index 00000000..834a4022 --- /dev/null +++ b/docs/superpowers/contracts/linux-config-mtk-lib-split-2026-08-16.json @@ -0,0 +1,17 @@ +{ + "title": "Split scripts/lib/mtk_common.sh under the 250-line cap", + "objective": "Bring the shared MediaTek library that seven mtk_root scripts source under the cap, using the same entry-point pattern proven on common.sh, without changing any name it exports.", + "acceptance_criteria": [ + "mtk_common.sh and all three new parts are at most 250 lines.", + "mtk_common.sh remains the single path every consumer sources; it sources its own parts.", + "Sourcing it yields exactly the same 28 function names as before, checked after EACH carve rather than only at the end.", + "`<<<` occurrences are confirmed to be here-strings rather than heredocs before choosing a seam.", + "The pre-commit shellcheck hook passes on every file, with no new suppression." + ], + "out_of_scope": [ + "Executing any mtk_root script; they talk to a phone over USB.", + "Changing what any helper does.", + "The other over-cap linux_configuration scripts." + ], + "verifier": "A subshell that sources mtk_common.sh and prints `declare -F`, diffed before and after; plus shellcheck -x, meta/scripts/verify_shell_split.sh on two consumers, and check_file_length.sh." +} diff --git a/docs/superpowers/evidence/linux-config-mtk-lib-split-2026-08-16.json b/docs/superpowers/evidence/linux-config-mtk-lib-split-2026-08-16.json new file mode 100644 index 00000000..7fe0f717 --- /dev/null +++ b/docs/superpowers/evidence/linux-config-mtk-lib-split-2026-08-16.json @@ -0,0 +1,38 @@ +{ + "intent": "Split scripts/lib/mtk_common.sh with the same entry-point pattern used for common.sh.", + "scope": [ + "linux_configuration/scripts/lib/mtk_common.sh (503 -> 132)", + "lib/mtk_device.sh, mtk_classify.sh, mtk_partitions.sh (new)", + "Non-goal: no behaviour change, no change to the public surface" + ], + "changes": [ + "mtk_common.sh keeps its logging, path and sanitising helpers and sources three new parts: adb/device selection, device classification, and partition discovery.", + "The seven mtk_root scripts that source it are untouched, because the path they source is unchanged.", + "The `<<<` occurrences in this file are here-strings, not heredocs, so the heredoc hazard did not apply - worth checking rather than assuming, given the enforce_vbox_hosts failure." + ], + "verification": [ + { + "command": "source mtk_common.sh in a subshell and diff `declare -F` before and after", + "result": "pass", + "evidence": "Identical 28-function set, checked after each of the two carves rather than only at the end." + }, + { + "command": "shellcheck -x mtk_common.sh and shellcheck on each part", + "result": "pass", + "evidence": "Zero findings, no suppression." + }, + { + "command": "meta/scripts/verify_shell_split.sh on 20-dump-stock.sh and install.sh", + "result": "pass", + "evidence": "Both consumers report 'libs sourced, functions defined' - they source mtk_common.sh from a sibling directory and still resolve everything through the new chain." + } + ], + "risks": [ + "None of the seven mtk_root consumers was executed: they talk to a phone over USB. The function-set diff plus the stubbed runs are the available evidence.", + "_MTK_LIB_DIR uses ${BASH_SOURCE[0]} because this file is only ever sourced; $0 would point at the calling script." + ], + "rollback": [ + "git revert the commit; mtk_common.sh returns to 503 lines and the three parts disappear.", + "After rollback, re-run the declare -F diff and expect the same 28 functions." + ] +} diff --git a/linux_configuration/scripts/lib/mtk_classify.sh b/linux_configuration/scripts/lib/mtk_classify.sh new file mode 100755 index 00000000..558d7a6d --- /dev/null +++ b/linux_configuration/scripts/lib/mtk_classify.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# Device classification, partition discovery and layout checks. +# +# Sourced by mtk_common.sh, which stays the single entry point the +# mtk_root scripts source, so their public surface is unchanged. + +# ---------------------------------------------------------------------------- +# Classification +# ---------------------------------------------------------------------------- + +# classify_device +# Sets MTK_DEVICE_CLASS to ULEFONE, PIXEL or UNKNOWN, and MTK_CLASS_REASON to +# a human-readable explanation of how it got there. +# +# ULEFONE requires a positive MediaTek signal AND a vendor/model match - both, +# never either, and never "not a Pixel, so presumably the Ulefone". A third +# phone attached by accident must land in UNKNOWN. +classify_device() { + local manufacturer="" model="" platform="" hardware="" device="" + local mtk_match=0 ulefone_match=0 pixel_match=0 + + manufacturer="$(dev_getprop ro.product.manufacturer)" + model="$(dev_getprop ro.product.model)" + platform="$(dev_getprop ro.board.platform)" + hardware="$(dev_getprop ro.hardware)" + device="$(dev_getprop ro.product.device)" + + MTK_PROP_MANUFACTURER="$manufacturer" + MTK_PROP_MODEL="$model" + MTK_PROP_PLATFORM="$platform" + MTK_PROP_HARDWARE="$hardware" + MTK_PROP_DEVICE="$device" + + if [[ $platform =~ $MTK_PLATFORM_PATTERN ]] || [[ $hardware =~ $MTK_PLATFORM_PATTERN ]]; then + mtk_match=1 + fi + if [[ $manufacturer =~ $ULEFONE_VENDOR_PATTERN ]] || [[ $model =~ $ULEFONE_MODEL_PATTERN ]]; then + ulefone_match=1 + fi + if [[ $manufacturer =~ $PIXEL_VENDOR_PATTERN ]] && [[ $model =~ $PIXEL_MODEL_PATTERN ]]; then + pixel_match=1 + fi + + if [[ $mtk_match -eq 1 && $ulefone_match -eq 1 ]]; then + MTK_DEVICE_CLASS="ULEFONE" + MTK_CLASS_REASON="MediaTek platform [${platform:-unset}/${hardware:-unset}] and Ulefone identity [${manufacturer} ${model}]" + elif [[ $pixel_match -eq 1 ]]; then + MTK_DEVICE_CLASS="PIXEL" + MTK_CLASS_REASON="Google Pixel identity [${manufacturer} ${model}]" + else + MTK_DEVICE_CLASS="UNKNOWN" + MTK_CLASS_REASON="no rule matched: manufacturer=[${manufacturer}] model=[${model}] platform=[${platform}] hardware=[${hardware}]" + fi + + export MTK_DEVICE_CLASS MTK_CLASS_REASON + export MTK_PROP_MANUFACTURER MTK_PROP_MODEL MTK_PROP_PLATFORM MTK_PROP_HARDWARE MTK_PROP_DEVICE +} + +# mtk_explain_classification - why classify_device decided what it did. +mtk_explain_classification() { + printf 'ro.product.manufacturer = %s\n' "${MTK_PROP_MANUFACTURER:-}" + printf 'ro.product.model = %s\n' "${MTK_PROP_MODEL:-}" + printf 'ro.product.device = %s\n' "${MTK_PROP_DEVICE:-}" + printf 'ro.board.platform = %s\n' "${MTK_PROP_PLATFORM:-}" + printf 'ro.hardware = %s\n' "${MTK_PROP_HARDWARE:-}" + printf '\n' + printf 'MediaTek pattern : %s\n' "$MTK_PLATFORM_PATTERN" + printf 'Ulefone vendor : %s\n' "$ULEFONE_VENDOR_PATTERN" + printf 'Ulefone model : %s\n' "$ULEFONE_MODEL_PATTERN" + printf 'Pixel vendor : %s\n' "$PIXEL_VENDOR_PATTERN" + printf 'Pixel model : %s\n' "$PIXEL_MODEL_PATTERN" + printf '\n' + printf 'Verdict: %s\n' "${MTK_DEVICE_CLASS:-unset}" + printf 'Reason : %s\n' "${MTK_CLASS_REASON:-unset}" +} + +# mtk_assert_mediatek +# A positive assertion, deliberately not "is not a Pixel". Used to gate the +# only script that touches partitions. +mtk_assert_mediatek() { + if [[ ${MTK_PROP_PLATFORM:-} =~ $MTK_PLATFORM_PATTERN ]]; then + return 0 + fi + if [[ ${MTK_PROP_HARDWARE:-} =~ $MTK_PLATFORM_PATTERN ]]; then + return 0 + fi + return 1 +} diff --git a/linux_configuration/scripts/lib/mtk_common.sh b/linux_configuration/scripts/lib/mtk_common.sh index 9d0292dc..9373d029 100755 --- a/linux_configuration/scripts/lib/mtk_common.sh +++ b/linux_configuration/scripts/lib/mtk_common.sh @@ -123,381 +123,10 @@ dev_list_by_name() { mtk_adb shell 'ls -1 /dev/block/by-name/ 2>/dev/null' 2>/dev/null | tr -d '\r' || true } -# mtk_adb - serial-pinned adb, so a second phone plugged in midway -# cannot silently become the target. -mtk_adb() { - adb -s "${MTK_SERIAL:?mtk_select_device must run first}" "$@" -} - -# ---------------------------------------------------------------------------- -# Device presence and authorization -# ---------------------------------------------------------------------------- - -# mtk_list_devices - prints " " per attached device. -mtk_list_devices() { - adb devices 2>/dev/null | - awk 'NR > 1 && $2 ~ /^(device|offline|unauthorized|recovery|sideload)$/ { print $1, $2 }' -} - -# mtk_device_state - device|unauthorized|offline|absent -mtk_device_state() { - local serial="$1" - local state="" - - state="$(mtk_list_devices | awk -v s="$serial" '$1 == s { print $2; exit }')" - printf '%s' "${state:-absent}" -} - -# mtk_select_device [requested-serial] -# Sets MTK_SERIAL. Refuses when the choice is ambiguous rather than picking -# one - a wrong guess here targets the wrong phone. -mtk_select_device() { - local requested="${1:-${MTK_SERIAL:-}}" - local -a device_rows=() - local -a serials=() - local row="" - - if [[ -n ${MTK_ROOT_FIXTURE:-} ]]; then - MTK_SERIAL="${MTK_SERIAL:-fixture}" - export MTK_SERIAL - return 0 - fi - - mapfile -t device_rows < <(mtk_list_devices) - - if [[ ${#device_rows[@]} -eq 0 ]]; then - mtk_error "No device visible to adb." - mtk_error "Check: cable seated, USB debugging enabled, and try 'adb devices'." - return 1 - fi - - for row in "${device_rows[@]}"; do - serials+=("${row%% *}") - done - - if [[ -n $requested ]]; then - for row in "${serials[@]}"; do - if [[ $row == "$requested" ]]; then - MTK_SERIAL="$requested" - export MTK_SERIAL - return 0 - fi - done - mtk_error "Requested serial '${requested}' is not attached. Attached: ${serials[*]}" - return 1 - fi - - if [[ ${#serials[@]} -gt 1 ]]; then - mtk_error "Multiple devices attached (${serials[*]}); refusing to guess." - mtk_error "Re-run with --serial ." - return 1 - fi - - MTK_SERIAL="${serials[0]}" - export MTK_SERIAL - return 0 -} - -# mtk_require_authorized -# An unauthorized device answers every getprop with an empty string, which -# reads exactly like "this device has no properties". Catch it up front so the -# report never blames the device for a prompt sitting unanswered on its screen. -mtk_require_authorized() { - local state="" - - [[ -n ${MTK_ROOT_FIXTURE:-} ]] && return 0 - - state="$(mtk_device_state "${MTK_SERIAL}")" - case "$state" in - device) - return 0 - ;; - unauthorized) - mtk_error "Device ${MTK_SERIAL} is UNAUTHORIZED." - mtk_error "The RSA authorization prompt is on the phone's screen - unlock it and tap Allow." - mtk_error "If no prompt appears: revoke USB debugging authorizations on the phone, then replug." - return 1 - ;; - offline) - mtk_error "Device ${MTK_SERIAL} is OFFLINE. Replug the cable, or run 'adb kill-server'." - return 1 - ;; - *) - mtk_error "Device ${MTK_SERIAL} is in state '${state}', which this toolkit does not read." - return 1 - ;; - esac -} - -# ---------------------------------------------------------------------------- -# Classification -# ---------------------------------------------------------------------------- - -# classify_device -# Sets MTK_DEVICE_CLASS to ULEFONE, PIXEL or UNKNOWN, and MTK_CLASS_REASON to -# a human-readable explanation of how it got there. -# -# ULEFONE requires a positive MediaTek signal AND a vendor/model match - both, -# never either, and never "not a Pixel, so presumably the Ulefone". A third -# phone attached by accident must land in UNKNOWN. -classify_device() { - local manufacturer="" model="" platform="" hardware="" device="" - local mtk_match=0 ulefone_match=0 pixel_match=0 - - manufacturer="$(dev_getprop ro.product.manufacturer)" - model="$(dev_getprop ro.product.model)" - platform="$(dev_getprop ro.board.platform)" - hardware="$(dev_getprop ro.hardware)" - device="$(dev_getprop ro.product.device)" - - MTK_PROP_MANUFACTURER="$manufacturer" - MTK_PROP_MODEL="$model" - MTK_PROP_PLATFORM="$platform" - MTK_PROP_HARDWARE="$hardware" - MTK_PROP_DEVICE="$device" - - if [[ $platform =~ $MTK_PLATFORM_PATTERN ]] || [[ $hardware =~ $MTK_PLATFORM_PATTERN ]]; then - mtk_match=1 - fi - if [[ $manufacturer =~ $ULEFONE_VENDOR_PATTERN ]] || [[ $model =~ $ULEFONE_MODEL_PATTERN ]]; then - ulefone_match=1 - fi - if [[ $manufacturer =~ $PIXEL_VENDOR_PATTERN ]] && [[ $model =~ $PIXEL_MODEL_PATTERN ]]; then - pixel_match=1 - fi - - if [[ $mtk_match -eq 1 && $ulefone_match -eq 1 ]]; then - MTK_DEVICE_CLASS="ULEFONE" - MTK_CLASS_REASON="MediaTek platform [${platform:-unset}/${hardware:-unset}] and Ulefone identity [${manufacturer} ${model}]" - elif [[ $pixel_match -eq 1 ]]; then - MTK_DEVICE_CLASS="PIXEL" - MTK_CLASS_REASON="Google Pixel identity [${manufacturer} ${model}]" - else - MTK_DEVICE_CLASS="UNKNOWN" - MTK_CLASS_REASON="no rule matched: manufacturer=[${manufacturer}] model=[${model}] platform=[${platform}] hardware=[${hardware}]" - fi - - export MTK_DEVICE_CLASS MTK_CLASS_REASON - export MTK_PROP_MANUFACTURER MTK_PROP_MODEL MTK_PROP_PLATFORM MTK_PROP_HARDWARE MTK_PROP_DEVICE -} - -# mtk_explain_classification - why classify_device decided what it did. -mtk_explain_classification() { - printf 'ro.product.manufacturer = %s\n' "${MTK_PROP_MANUFACTURER:-}" - printf 'ro.product.model = %s\n' "${MTK_PROP_MODEL:-}" - printf 'ro.product.device = %s\n' "${MTK_PROP_DEVICE:-}" - printf 'ro.board.platform = %s\n' "${MTK_PROP_PLATFORM:-}" - printf 'ro.hardware = %s\n' "${MTK_PROP_HARDWARE:-}" - printf '\n' - printf 'MediaTek pattern : %s\n' "$MTK_PLATFORM_PATTERN" - printf 'Ulefone vendor : %s\n' "$ULEFONE_VENDOR_PATTERN" - printf 'Ulefone model : %s\n' "$ULEFONE_MODEL_PATTERN" - printf 'Pixel vendor : %s\n' "$PIXEL_VENDOR_PATTERN" - printf 'Pixel model : %s\n' "$PIXEL_MODEL_PATTERN" - printf '\n' - printf 'Verdict: %s\n' "${MTK_DEVICE_CLASS:-unset}" - printf 'Reason : %s\n' "${MTK_CLASS_REASON:-unset}" -} - -# mtk_assert_mediatek -# A positive assertion, deliberately not "is not a Pixel". Used to gate the -# only script that touches partitions. -mtk_assert_mediatek() { - if [[ ${MTK_PROP_PLATFORM:-} =~ $MTK_PLATFORM_PATTERN ]]; then - return 0 - fi - if [[ ${MTK_PROP_HARDWARE:-} =~ $MTK_PLATFORM_PATTERN ]]; then - return 0 - fi - return 1 -} - -# ---------------------------------------------------------------------------- -# Partition discovery -# ---------------------------------------------------------------------------- - -# discover_partitions -# Sets MTK_AB_SCHEME, MTK_SLOT_SUFFIX, MTK_RAMDISK_PART, MTK_VBMETA_PART, -# MTK_BY_NAME (newline-separated listing). -# -# The governing rule: never name a partition in a conditional that was not -# first observed in the listing. The AOSP rule (devices launching on Android -# 13+ carry the ramdisk in init_boot; earlier ones keep it in boot) explains -# WHY the preference order below is correct, but the code never consults the -# API level to decide - it reads the filesystem. That is what lets one code -# path serve both a Pixel (no init_boot -> falls through to boot) and a -# 13-launch MediaTek device (init_boot present -> taken) without branching on -# device identity. -discover_partitions() { - local entries="" suffix="" candidate="" name="" - - entries="$(dev_list_by_name)" - MTK_BY_NAME="$entries" - export MTK_BY_NAME - - if [[ -z $entries ]]; then - MTK_AB_SCHEME="unknown" - MTK_SLOT_SUFFIX="" - MTK_RAMDISK_PART="" - MTK_VBMETA_PART="" - export MTK_AB_SCHEME MTK_SLOT_SUFFIX MTK_RAMDISK_PART MTK_VBMETA_PART - return 1 - fi - - # Slot scheme comes from observed suffixes, not from a product table. - if grep -qE '_a$' <<<"$entries"; then - MTK_AB_SCHEME="A/B" - else - MTK_AB_SCHEME="A-only" - fi - - # Prefer the device's own view of the active slot; fall back to _a only - # because a dump has to start somewhere, and say so in the report. - suffix="$(dev_getprop ro.boot.slot_suffix)" - if [[ -z $suffix && $MTK_AB_SCHEME == "A/B" ]]; then - suffix="_a" - MTK_SLOT_ASSUMED=1 - else - MTK_SLOT_ASSUMED=0 - fi - MTK_SLOT_SUFFIX="$suffix" - - # init_boot first: its presence is decisive. A device that has it keeps the - # ramdisk there even though boot also exists, so checking boot first would - # pick the wrong partition on exactly the hardware this toolkit is for. - MTK_RAMDISK_PART="" - for candidate in init_boot boot; do - for name in "${candidate}${suffix}" "${candidate}"; do - if grep -qxF -- "$name" <<<"$entries"; then - MTK_RAMDISK_PART="$name" - break 2 - fi - done - done - - MTK_VBMETA_PART="" - for name in "vbmeta${suffix}" "vbmeta"; do - if grep -qxF -- "$name" <<<"$entries"; then - MTK_VBMETA_PART="$name" - break - fi - done - - export MTK_AB_SCHEME MTK_SLOT_SUFFIX MTK_RAMDISK_PART MTK_VBMETA_PART MTK_SLOT_ASSUMED - - [[ -n $MTK_RAMDISK_PART ]] -} - -# mtk_ramdisk_carrier - "init_boot" or "boot", suffix stripped. -mtk_ramdisk_carrier() { - local part="${MTK_RAMDISK_PART:-}" - printf '%s' "${part%_[ab]}" -} - -# mtk_check_layout_expectation -# Cross-check only. first_api_level >= 33 predicts init_boot; below predicts -# boot. A mismatch means either a vendor deviation or a bad read - the human -# should look, but the filesystem still wins. -mtk_check_layout_expectation() { - local api_level="" carrier="" expected="" - - api_level="$(dev_getprop ro.product.first_api_level)" - carrier="$(mtk_ramdisk_carrier)" - - [[ -z $api_level || -z $carrier ]] && return 0 - [[ $api_level =~ ^[0-9]+$ ]] || return 0 - - if [[ $api_level -ge 33 ]]; then - expected="init_boot" - else - expected="boot" - fi - - if [[ $carrier != "$expected" ]]; then - mtk_warn "Layout disagrees with expectation: first_api_level=${api_level} predicts '${expected}', device shows '${carrier}'." - mtk_warn "Trusting the device. Verify before flashing anything." - return 1 - fi - return 0 -} - -# ---------------------------------------------------------------------------- -# Host environment checks (read-only; install.sh owns the mutating half) -# ---------------------------------------------------------------------------- - -# mtk_udev_catchall_files -# Prints any rules file containing an ACTIVE wildcard-vendor rule granting -# world read/write. Such a rule applies to every USB device on the system - -# storage, security keys, everything - not just phones. -# -# Commented-out lines are ignored: --fix-udev disables a rule by commenting it -# rather than deleting it, so matching those would leave the check permanently -# red after it had already been dealt with. Only *.rules files are considered, -# since udev itself skips any other suffix (including our .bak- backups). -mtk_udev_catchall_files() { - local file="" - for file in /etc/udev/rules.d/*.rules; do - [[ -f $file ]] || continue - if grep -qE '^[^#]*idVendor\}=="\*"[^#]*MODE="0666"|^[^#]*MODE="0666"[^#]*idVendor\}=="\*"' \ - "$file" 2>/dev/null; then - printf '%s\n' "$file" - fi - done -} - -# mtk_check_venv - is the mtkclient venv actually usable? -# Checks importability, not directory existence: the venv found on this host -# in Aug 2026 existed, looked complete, and failed at `import usb` because a -# Python upgrade had orphaned it. -mtk_check_venv() { - local python_bin="" - python_bin="$(mtk_venv_python)" - - if [[ ! -x $python_bin ]]; then - printf 'missing: no interpreter at %s' "$python_bin" - return 1 - fi - - if ! "$python_bin" -c 'import usb, usb.core, serial' 2>/dev/null; then - local err="" - err="$("$python_bin" -c 'import usb, usb.core, serial' 2>&1 | tail -1)" - printf 'broken: %s' "$err" - return 1 - fi - - printf 'ok: %s' "$("$python_bin" --version 2>&1)" - return 0 -} - -# mtk_sha256 -mtk_sha256() { - sha256sum "$1" | awk '{ print $1 }' -} - -# mtk_file_size -mtk_file_size() { - stat -c '%s' "$1" -} - -# mtk_sane_ramdisk_size -# A dump outside this band is not necessarily wrong, but it is worth stopping -# for: an empty or truncated read and an accidental whole-disk grab both look -# like success to mtkclient's exit code. -mtk_sane_ramdisk_size() { - local size="$1" - - if [[ ! $size =~ ^[0-9]+$ ]]; then - printf 'size is not a number: %s' "$size" - return 1 - fi - if [[ $size -lt $MTK_RAMDISK_MIN_BYTES ]]; then - printf 'implausibly small: %s bytes (< %s)' "$size" "$MTK_RAMDISK_MIN_BYTES" - return 1 - fi - if [[ $size -gt $MTK_RAMDISK_MAX_BYTES ]]; then - printf 'implausibly large: %s bytes (> %s)' "$size" "$MTK_RAMDISK_MAX_BYTES" - return 1 - fi - printf '%s bytes' "$size" - return 0 -} +_MTK_LIB_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" +# shellcheck source=mtk_device.sh +source "$_MTK_LIB_DIR/mtk_device.sh" +# shellcheck source=mtk_classify.sh +source "$_MTK_LIB_DIR/mtk_classify.sh" +# shellcheck source=mtk_partitions.sh +source "$_MTK_LIB_DIR/mtk_partitions.sh" diff --git a/linux_configuration/scripts/lib/mtk_device.sh b/linux_configuration/scripts/lib/mtk_device.sh new file mode 100755 index 00000000..36892ab2 --- /dev/null +++ b/linux_configuration/scripts/lib/mtk_device.sh @@ -0,0 +1,111 @@ +#!/bin/bash +# adb wrappers, device listing, selection and authorisation. +# +# Sourced by mtk_common.sh, which stays the single entry point the +# mtk_root scripts source, so their public surface is unchanged. + +# mtk_adb - serial-pinned adb, so a second phone plugged in midway +# cannot silently become the target. +mtk_adb() { + adb -s "${MTK_SERIAL:?mtk_select_device must run first}" "$@" +} + +# ---------------------------------------------------------------------------- +# Device presence and authorization +# ---------------------------------------------------------------------------- + +# mtk_list_devices - prints " " per attached device. +mtk_list_devices() { + adb devices 2>/dev/null | + awk 'NR > 1 && $2 ~ /^(device|offline|unauthorized|recovery|sideload)$/ { print $1, $2 }' +} + +# mtk_device_state - device|unauthorized|offline|absent +mtk_device_state() { + local serial="$1" + local state="" + + state="$(mtk_list_devices | awk -v s="$serial" '$1 == s { print $2; exit }')" + printf '%s' "${state:-absent}" +} + +# mtk_select_device [requested-serial] +# Sets MTK_SERIAL. Refuses when the choice is ambiguous rather than picking +# one - a wrong guess here targets the wrong phone. +mtk_select_device() { + local requested="${1:-${MTK_SERIAL:-}}" + local -a device_rows=() + local -a serials=() + local row="" + + if [[ -n ${MTK_ROOT_FIXTURE:-} ]]; then + MTK_SERIAL="${MTK_SERIAL:-fixture}" + export MTK_SERIAL + return 0 + fi + + mapfile -t device_rows < <(mtk_list_devices) + + if [[ ${#device_rows[@]} -eq 0 ]]; then + mtk_error "No device visible to adb." + mtk_error "Check: cable seated, USB debugging enabled, and try 'adb devices'." + return 1 + fi + + for row in "${device_rows[@]}"; do + serials+=("${row%% *}") + done + + if [[ -n $requested ]]; then + for row in "${serials[@]}"; do + if [[ $row == "$requested" ]]; then + MTK_SERIAL="$requested" + export MTK_SERIAL + return 0 + fi + done + mtk_error "Requested serial '${requested}' is not attached. Attached: ${serials[*]}" + return 1 + fi + + if [[ ${#serials[@]} -gt 1 ]]; then + mtk_error "Multiple devices attached (${serials[*]}); refusing to guess." + mtk_error "Re-run with --serial ." + return 1 + fi + + MTK_SERIAL="${serials[0]}" + export MTK_SERIAL + return 0 +} + +# mtk_require_authorized +# An unauthorized device answers every getprop with an empty string, which +# reads exactly like "this device has no properties". Catch it up front so the +# report never blames the device for a prompt sitting unanswered on its screen. +mtk_require_authorized() { + local state="" + + [[ -n ${MTK_ROOT_FIXTURE:-} ]] && return 0 + + state="$(mtk_device_state "${MTK_SERIAL}")" + case "$state" in + device) + return 0 + ;; + unauthorized) + mtk_error "Device ${MTK_SERIAL} is UNAUTHORIZED." + mtk_error "The RSA authorization prompt is on the phone's screen - unlock it and tap Allow." + mtk_error "If no prompt appears: revoke USB debugging authorizations on the phone, then replug." + return 1 + ;; + offline) + mtk_error "Device ${MTK_SERIAL} is OFFLINE. Replug the cable, or run 'adb kill-server'." + return 1 + ;; + *) + mtk_error "Device ${MTK_SERIAL} is in state '${state}', which this toolkit does not read." + return 1 + ;; + esac +} diff --git a/linux_configuration/scripts/lib/mtk_partitions.sh b/linux_configuration/scripts/lib/mtk_partitions.sh new file mode 100755 index 00000000..f452fd6f --- /dev/null +++ b/linux_configuration/scripts/lib/mtk_partitions.sh @@ -0,0 +1,195 @@ +#!/bin/bash +# Partition discovery, ramdisk carrier and layout expectations. +# +# Sourced by mtk_common.sh, which stays the single entry point the +# mtk_root scripts source, so their public surface is unchanged. + +# ---------------------------------------------------------------------------- +# Partition discovery +# ---------------------------------------------------------------------------- + +# discover_partitions +# Sets MTK_AB_SCHEME, MTK_SLOT_SUFFIX, MTK_RAMDISK_PART, MTK_VBMETA_PART, +# MTK_BY_NAME (newline-separated listing). +# +# The governing rule: never name a partition in a conditional that was not +# first observed in the listing. The AOSP rule (devices launching on Android +# 13+ carry the ramdisk in init_boot; earlier ones keep it in boot) explains +# WHY the preference order below is correct, but the code never consults the +# API level to decide - it reads the filesystem. That is what lets one code +# path serve both a Pixel (no init_boot -> falls through to boot) and a +# 13-launch MediaTek device (init_boot present -> taken) without branching on +# device identity. +discover_partitions() { + local entries="" suffix="" candidate="" name="" + + entries="$(dev_list_by_name)" + MTK_BY_NAME="$entries" + export MTK_BY_NAME + + if [[ -z $entries ]]; then + MTK_AB_SCHEME="unknown" + MTK_SLOT_SUFFIX="" + MTK_RAMDISK_PART="" + MTK_VBMETA_PART="" + export MTK_AB_SCHEME MTK_SLOT_SUFFIX MTK_RAMDISK_PART MTK_VBMETA_PART + return 1 + fi + + # Slot scheme comes from observed suffixes, not from a product table. + if grep -qE '_a$' <<<"$entries"; then + MTK_AB_SCHEME="A/B" + else + MTK_AB_SCHEME="A-only" + fi + + # Prefer the device's own view of the active slot; fall back to _a only + # because a dump has to start somewhere, and say so in the report. + suffix="$(dev_getprop ro.boot.slot_suffix)" + if [[ -z $suffix && $MTK_AB_SCHEME == "A/B" ]]; then + suffix="_a" + MTK_SLOT_ASSUMED=1 + else + MTK_SLOT_ASSUMED=0 + fi + MTK_SLOT_SUFFIX="$suffix" + + # init_boot first: its presence is decisive. A device that has it keeps the + # ramdisk there even though boot also exists, so checking boot first would + # pick the wrong partition on exactly the hardware this toolkit is for. + MTK_RAMDISK_PART="" + for candidate in init_boot boot; do + for name in "${candidate}${suffix}" "${candidate}"; do + if grep -qxF -- "$name" <<<"$entries"; then + MTK_RAMDISK_PART="$name" + break 2 + fi + done + done + + MTK_VBMETA_PART="" + for name in "vbmeta${suffix}" "vbmeta"; do + if grep -qxF -- "$name" <<<"$entries"; then + MTK_VBMETA_PART="$name" + break + fi + done + + export MTK_AB_SCHEME MTK_SLOT_SUFFIX MTK_RAMDISK_PART MTK_VBMETA_PART MTK_SLOT_ASSUMED + + [[ -n $MTK_RAMDISK_PART ]] +} + +# mtk_ramdisk_carrier - "init_boot" or "boot", suffix stripped. +mtk_ramdisk_carrier() { + local part="${MTK_RAMDISK_PART:-}" + printf '%s' "${part%_[ab]}" +} + +# mtk_check_layout_expectation +# Cross-check only. first_api_level >= 33 predicts init_boot; below predicts +# boot. A mismatch means either a vendor deviation or a bad read - the human +# should look, but the filesystem still wins. +mtk_check_layout_expectation() { + local api_level="" carrier="" expected="" + + api_level="$(dev_getprop ro.product.first_api_level)" + carrier="$(mtk_ramdisk_carrier)" + + [[ -z $api_level || -z $carrier ]] && return 0 + [[ $api_level =~ ^[0-9]+$ ]] || return 0 + + if [[ $api_level -ge 33 ]]; then + expected="init_boot" + else + expected="boot" + fi + + if [[ $carrier != "$expected" ]]; then + mtk_warn "Layout disagrees with expectation: first_api_level=${api_level} predicts '${expected}', device shows '${carrier}'." + mtk_warn "Trusting the device. Verify before flashing anything." + return 1 + fi + return 0 +} + +# ---------------------------------------------------------------------------- +# Host environment checks (read-only; install.sh owns the mutating half) +# ---------------------------------------------------------------------------- + +# mtk_udev_catchall_files +# Prints any rules file containing an ACTIVE wildcard-vendor rule granting +# world read/write. Such a rule applies to every USB device on the system - +# storage, security keys, everything - not just phones. +# +# Commented-out lines are ignored: --fix-udev disables a rule by commenting it +# rather than deleting it, so matching those would leave the check permanently +# red after it had already been dealt with. Only *.rules files are considered, +# since udev itself skips any other suffix (including our .bak- backups). +mtk_udev_catchall_files() { + local file="" + for file in /etc/udev/rules.d/*.rules; do + [[ -f $file ]] || continue + if grep -qE '^[^#]*idVendor\}=="\*"[^#]*MODE="0666"|^[^#]*MODE="0666"[^#]*idVendor\}=="\*"' \ + "$file" 2>/dev/null; then + printf '%s\n' "$file" + fi + done +} + +# mtk_check_venv - is the mtkclient venv actually usable? +# Checks importability, not directory existence: the venv found on this host +# in Aug 2026 existed, looked complete, and failed at `import usb` because a +# Python upgrade had orphaned it. +mtk_check_venv() { + local python_bin="" + python_bin="$(mtk_venv_python)" + + if [[ ! -x $python_bin ]]; then + printf 'missing: no interpreter at %s' "$python_bin" + return 1 + fi + + if ! "$python_bin" -c 'import usb, usb.core, serial' 2>/dev/null; then + local err="" + err="$("$python_bin" -c 'import usb, usb.core, serial' 2>&1 | tail -1)" + printf 'broken: %s' "$err" + return 1 + fi + + printf 'ok: %s' "$("$python_bin" --version 2>&1)" + return 0 +} + +# mtk_sha256 +mtk_sha256() { + sha256sum "$1" | awk '{ print $1 }' +} + +# mtk_file_size +mtk_file_size() { + stat -c '%s' "$1" +} + +# mtk_sane_ramdisk_size +# A dump outside this band is not necessarily wrong, but it is worth stopping +# for: an empty or truncated read and an accidental whole-disk grab both look +# like success to mtkclient's exit code. +mtk_sane_ramdisk_size() { + local size="$1" + + if [[ ! $size =~ ^[0-9]+$ ]]; then + printf 'size is not a number: %s' "$size" + return 1 + fi + if [[ $size -lt $MTK_RAMDISK_MIN_BYTES ]]; then + printf 'implausibly small: %s bytes (< %s)' "$size" "$MTK_RAMDISK_MIN_BYTES" + return 1 + fi + if [[ $size -gt $MTK_RAMDISK_MAX_BYTES ]]; then + printf 'implausibly large: %s bytes (> %s)' "$size" "$MTK_RAMDISK_MAX_BYTES" + return 1 + fi + printf '%s bytes' "$size" + return 0 +}