|
| 1 | +#!/bin/bash |
| 2 | +set -euxo pipefail |
| 3 | + |
| 4 | +# Settings for adb and curl. |
| 5 | +export ANDROID_SERIAL="172.16.23.223:5555" # see adb help |
| 6 | +readonly CURL_ARGS=(-L) # follow location |
| 7 | + |
| 8 | +# Root shell loader and temporary script path for run_as_root. |
| 9 | +readonly LOADER_PATH="/data/local/tmp/loader" |
| 10 | +readonly LOADER_BUILD_DIR="$(dirname "$(readlink -f "$0")")" # script directory |
| 11 | +readonly SCRIPT_TMP_PATH="/data/local/tmp/magiskinstall-tmp" |
| 12 | + |
| 13 | +# Magisk Manager package name, version and URL |
| 14 | +readonly MAGISK_MANAGER_PACKAGE_NAME="com.topjohnwu.magisk" |
| 15 | +readonly MAGISK_MANAGER_MAIN_PACKAGE="${MAGISK_MANAGER_PACKAGE_NAME}/a.c" |
| 16 | +readonly MAGISK_MANAGER_VERSION="7.5.1" |
| 17 | +readonly MAGISK_MANAGER_APK_URL="https://github.com/topjohnwu/Magisk/releases/download/manager-v${MAGISK_MANAGER_VERSION}/MagiskManager-v${MAGISK_MANAGER_VERSION}.apk" |
| 18 | + |
| 19 | +# boot, recovery and recoveryB partitions. boot is backed up to recoveryB before patching, whose |
| 20 | +# content should match recovery. |
| 21 | +readonly BOOT_PARTNO="5" |
| 22 | +readonly BOOT_DEV="/dev/mmcblk0p${BOOT_PARTNO}" |
| 23 | +readonly RECOVERY_PARTNO="4" |
| 24 | +readonly RECOVERY_DEV="/dev/mmcblk0p${RECOVERY_PARTNO}" |
| 25 | +readonly RECOVERY_B_PARTNO="8" |
| 26 | +readonly RECOVERY_B_DEV="/dev/mmcblk0p${RECOVERY_B_PARTNO}" |
| 27 | + |
| 28 | +# Directory to place boot image in for patching from Magisk Manager. |
| 29 | +readonly DOWNLOAD_DIR="/sdcard/Download" # Something accessible from the Android UI |
| 30 | +readonly PATCH_DIR="${DOWNLOAD_DIR}/magiskinstall.$(date +%F_%T)" |
| 31 | + |
| 32 | +# Directory where Magisk Manager places the patched output boot image. |
| 33 | +readonly MAGISK_PATCHED_PATH="${DOWNLOAD_DIR}/magisk_patched.img" |
| 34 | + |
| 35 | +# Runs the passed arguments as root. |
| 36 | +run_as_root() { |
| 37 | + # Getting something escaped properly through adb shell, the loader's argv parsing, and then |
| 38 | + # the android shell is tricky, so generate a shell script and execute it on the device. |
| 39 | + local tmp="$(mktemp)" |
| 40 | + printf '#!/system/bin/sh\nPS4="(run_as_root) + "\nset -x\n' > "$tmp" |
| 41 | + printf '%q ' "$@" >> "$tmp" # Need to print "%q " individually; the entire format is repeated per arg |
| 42 | + printf '\n' >> "$tmp" |
| 43 | + chmod +x "$tmp" |
| 44 | + adb push "$tmp" "$SCRIPT_TMP_PATH" > /dev/null |
| 45 | + rm -f "$tmp" |
| 46 | + adb shell "$LOADER_PATH -trigger_exec $SCRIPT_TMP_PATH" |
| 47 | +} |
| 48 | + |
| 49 | +# Tests whether run_as_root escapes arguments and returns the return code properly. |
| 50 | +test_run_as_root() { |
| 51 | + local have="$(run_as_root sh -c 'echo $1' "foo bar" "bar baz" "baz qux")" |
| 52 | + local want="bar baz" |
| 53 | + if [ "$have" != "$want" ]; then |
| 54 | + echo "ERROR: run_as_root broken: got >>>$have<<<, want >>>$want<<<" >&2 |
| 55 | + exit 99 |
| 56 | + fi |
| 57 | + |
| 58 | + if run_as_root false; then |
| 59 | + echo "ERROR: 'run_as_root false' succeeded" >&2 |
| 60 | + exit 99 |
| 61 | + fi |
| 62 | +} |
| 63 | + |
| 64 | +# Installs the root shell loader on the device. |
| 65 | +prepare_root() { |
| 66 | + if ! adb shell test -x "$LOADER_PATH"; then |
| 67 | + echo "A root shell loader is not installed. Building and installing." |
| 68 | + make -C "$LOADER_BUILD_DIR" loader |
| 69 | + adb push "$LOADER_BUILD_DIR"/loader "$LOADER_PATH" |
| 70 | + fi |
| 71 | + test_run_as_root |
| 72 | +} |
| 73 | + |
| 74 | +# Returns the name of the given partition number from the device’s /proc/partinfo. |
| 75 | +get_partname() { |
| 76 | + local partno="$1" |
| 77 | + # ex.: part05: 00012800 0000a000 "boot" |
| 78 | + set -x |
| 79 | + run_as_root cat /proc/partinfo | \ |
| 80 | + awk '/^part0*'"$partno"':/ { gsub("\"", ""); print $NF }' |
| 81 | +} |
| 82 | + |
| 83 | +# Queries the user for a response and returns 0 for yes, 1 for no. |
| 84 | +ask_yesno() { |
| 85 | + local prompt="$1" |
| 86 | + local reply |
| 87 | + while true; do |
| 88 | + printf "%s [y/n]: " "$prompt" |
| 89 | + read reply |
| 90 | + case "$reply" in |
| 91 | + [yY]) |
| 92 | + return 0 |
| 93 | + ;; |
| 94 | + [nN]) |
| 95 | + return 1 |
| 96 | + ;; |
| 97 | + *) |
| 98 | + echo "Invalid response. Please enter 'y' or 'n'." |
| 99 | + ;; |
| 100 | + esac |
| 101 | + done |
| 102 | +} |
| 103 | + |
| 104 | +# Returns the versionName of an installed package. |
| 105 | +get_package_version() { |
| 106 | + local pkg="$1" |
| 107 | + adb shell pm dump "$pkg" | \ |
| 108 | + sed -r -n -e 's/^[[:blank:]]*versionName=([^[:blank:]]+)/\1/;T;p' |
| 109 | +} |
| 110 | + |
| 111 | +# Checks whether a given partition is an Android boot image. |
| 112 | +is_android_bootimg() { |
| 113 | + local part="$1" |
| 114 | + test "$(run_as_root dd if="$part" bs=8 count=1)" = 'ANDROID!' |
| 115 | +} |
| 116 | + |
| 117 | +# (Re-)installs Magisk Manager. |
| 118 | +install_magisk_manager(){ |
| 119 | + local tmp="$(mktemp)" |
| 120 | + curl "${CURL_ARGS[@]}" "$MAGISK_MANAGER_APK_URL" > "$tmp" |
| 121 | + if ! adb install -r "$tmp"; then |
| 122 | + if ! ask_yesno "Installation of Magisk Manager failed. Continue?"; then |
| 123 | + exit 1 |
| 124 | + fi |
| 125 | + fi |
| 126 | + rm -f "$tmp" |
| 127 | +} |
| 128 | + |
| 129 | + |
| 130 | +### Main script starts here ### |
| 131 | + |
| 132 | +echo "Preparing root environment..." |
| 133 | +prepare_root |
| 134 | + |
| 135 | +echo "Checking Magisk Manager version..." |
| 136 | +mm_version="$(get_package_version "$MAGISK_MANAGER_PACKAGE_NAME")" |
| 137 | +if [ -z "$mm_version" ]; then |
| 138 | + echo "Magisk Manager does not seem to be installed. Installing..." |
| 139 | + install_magisk_manager |
| 140 | +elif [ "$mm_version" != "$MAGISK_MANAGER_VERSION" ]; then |
| 141 | + echo "Your Magisk Manager version '$mm_version' does not match expected '$MAGISK_MANAGER_VERSION'." |
| 142 | + if ask_yesno "Reinstall?"; then |
| 143 | + install_magisk_manager |
| 144 | + fi |
| 145 | +else |
| 146 | + echo "Installed Magisk Manager version $mm_version matches expected version." |
| 147 | +fi |
| 148 | + |
| 149 | +echo "Sanity-checking partitions..." |
| 150 | +declare -rA want_partnames=(["$BOOT_PARTNO"]="boot" |
| 151 | + ["$RECOVERY_PARTNO"]="recovery" |
| 152 | + ["$RECOVERY_B_PARTNO"]="recoveryB") |
| 153 | +for partno in "${!want_partnames[@]}"; do |
| 154 | + want_name="${want_partnames[$partno]}" |
| 155 | + have_name="$(get_partname "$partno")" |
| 156 | + if [ "$have_name" != "$want_name" ]; then |
| 157 | + echo "Partition $partno is named '$have_name', expected '$want_name'. Aborting." >&2 |
| 158 | + exit 1 |
| 159 | + fi |
| 160 | + echo "Partition $partno has expected name '$want_name'." |
| 161 | +done |
| 162 | + |
| 163 | +for dev in "$BOOT_DEV" "$RECOVERY_DEV" "$RECOVERY_B_DEV"; do |
| 164 | + if ! is_android_bootimg "$dev"; then |
| 165 | + echo "Device $dev does not appear to be an Android boot image. Aborting." >&2 |
| 166 | + exit 1 |
| 167 | + fi |
| 168 | + echo "Device $dev looks like an Android boot image." |
| 169 | +done |
| 170 | + |
| 171 | +echo "Creating patch directory $PATCH_DIR..." |
| 172 | +adb shell mkdir "$PATCH_DIR" |
| 173 | + |
| 174 | +orig_img="$PATCH_DIR/boot_orig.img" |
| 175 | +run_as_root dd if="$BOOT_DEV" of="$orig_img" |
| 176 | +run_as_root sh -c 'getprop > "$1"/getprop.txt' argv0 "$PATCH_DIR" |
| 177 | + |
| 178 | +echo "Starting Magisk Manager..." |
| 179 | +adb shell am start "$MAGISK_MANAGER_MAIN_PACKAGE" |
| 180 | + |
| 181 | +cat <<EOF |
| 182 | +
|
| 183 | +Magisk Manager has been started. To patch the boot image, do the following: |
| 184 | +
|
| 185 | +* Click on 'Install' in the 'Magisk is not installed' line. You will need to click 'Install' |
| 186 | + with a mouse; just selecting and 'clicking' the line with the TV remote WILL NOT WORK. |
| 187 | +
|
| 188 | +* Select 'Install', then 'Select and Patch a File'. |
| 189 | +
|
| 190 | +* Select $orig_img |
| 191 | +
|
| 192 | +* Wait until Magisk has finished patching. |
| 193 | +
|
| 194 | +* Press [Enter] here. You can close Magisk Manager on the TV. |
| 195 | +EOF |
| 196 | +read |
| 197 | + |
| 198 | +if ! adb shell test -e "$MAGISK_PATCHED_PATH"; then |
| 199 | + echo "Patched image not found at '$MAGISK_PATCHED_PATH'. Aborting." >&2 |
| 200 | + exit 1 |
| 201 | +elif ! adb shell test "$MAGISK_PATCHED_PATH" -nt "$orig_img"; then |
| 202 | + echo "Patched image ($MAGISK_PATCHED_PATH) is not newer than $orig_img. Aborting." >&2 |
| 203 | + exit 1 |
| 204 | +fi |
| 205 | + |
| 206 | +if adb shell cmp "$MAGISK_PATCHED_PATH" "$orig_img"; then |
| 207 | + echo "Patched image $MAGISK_PATCHED_PATH equal to original image $orig_img. Aborting." >&2 |
| 208 | + exit 1 |
| 209 | +fi |
| 210 | + |
| 211 | +if ! is_android_bootimg "$MAGISK_PATCHED_PATH"; then |
| 212 | + echo "Patched image $MAGISK_PATCHED_PATH does not look like an Android boot image. Aborting." >&2 |
| 213 | + exit 1 |
| 214 | +fi |
| 215 | + |
| 216 | + |
| 217 | +patched_img="$PATCH_DIR/boot_patched.img" |
| 218 | +echo "Patched image $MAGISK_PATCHED_PATH looks sane, moving to $patched_img" |
| 219 | +adb shell mv "$MAGISK_PATCHED_PATH" "$patched_img" |
| 220 | + |
| 221 | +if run_as_root cmp "$RECOVERY_DEV" "$RECOVERY_B_DEV" || \ |
| 222 | + ask_yesno "Partitions 'recovery' and 'recoveryB' differ (after ~20MB seems normal). Do you still want to back up 'boot' to 'recoveryB'?"; then |
| 223 | + run_as_root dd if="$BOOT_DEV" of="$RECOVERY_B_DEV" |
| 224 | + echo "Backed up $BOOT_DEV (boot) to $RECOVERY_B_DEV (recoveryB). See README.md for recovery hints." |
| 225 | +else |
| 226 | + if ! ask_yesno "Not backing up boot partition. Are you sure?"; then |
| 227 | + exit 1 |
| 228 | + fi |
| 229 | +fi |
| 230 | + |
| 231 | +if ! ask_yesno "Do you want to write $patched_img to $BOOT_DEV? This is your last chance to quit."; then |
| 232 | + exit 1 |
| 233 | +fi |
| 234 | + |
| 235 | +run_as_root dd if="$patched_img" of="$BOOT_DEV" |
| 236 | + |
| 237 | +echo "Boot partition ($BOOT_DEV) has been patched. Enjoy!" |
0 commit comments