Split scripts/lib/mtk_common.sh with the same entry-point pattern

503 -> 132 across three parts. The seven mtk_root consumers are
untouched because the path they source is unchanged.

Identical 28-function set before and after, checked after each of the
two carves. The `<<<` in this file are here-strings, not heredocs -
worth confirming rather than assuming, after enforce_vbox_hosts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-16 21:17:50 +02:00
parent 51e3945507
commit ef0a26abc7
6 changed files with 456 additions and 378 deletions

View File

@ -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."
}

View File

@ -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."
]
}

View File

@ -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
}

View File

@ -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 <args...> - 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 "<serial> <state>" 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 <serial> - 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 <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 <file>
mtk_sha256() {
sha256sum "$1" | awk '{ print $1 }'
}
# mtk_file_size <file>
mtk_file_size() {
stat -c '%s' "$1"
}
# mtk_sane_ramdisk_size <bytes>
# 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"

View File

@ -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 <args...> - 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 "<serial> <state>" 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 <serial> - 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 <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
}

View File

@ -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 <file>
mtk_sha256() {
sha256sum "$1" | awk '{ print $1 }'
}
# mtk_file_size <file>
mtk_file_size() {
stat -c '%s' "$1"
}
# mtk_sane_ramdisk_size <bytes>
# 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
}