Split three more linux_configuration giants

raspberry_pi_nextcloud.sh 1276 -> 163, root_bl9000.sh 1116 -> 211,
lookup_docs.sh 942 -> 165. Sixteen new libs.

lookup_python was one 300-line function; its keyword-to-anchor mapping
became python_keyword_doc, emitting result and desc tab-separated so the
caller keeps its control flow.

bl9000_mtkclient had cd X; work; cd - with three return paths.
install_mtkclient became a subshell; extract_boot_with_mtkclient could
not, since it exports BOOT_IMG - so the venv check moved before the cd,
the cd is guarded, and the restores use a saved $PWD.

Four more unguarded cd calls fixed, and a third instance of the lib
chaining bug flattened. lookup_docs.sh had no SCRIPT_DIR at all.

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 19:43:55 +02:00
parent 5bacfa3883
commit 7a74804786
24 changed files with 3066 additions and 2820 deletions

View File

@ -0,0 +1,18 @@
{
"title": "Split three more very large linux_configuration scripts",
"objective": "Bring raspberry_pi_nextcloud.sh (1276), root_bl9000.sh (1116) and lookup_docs.sh (942) under the 250-line cap, fixing the cd handling the splits expose rather than suppressing it.",
"acceptance_criteria": [
"All three entry scripts and all sixteen new libs are at most 250 lines.",
"The pre-commit shellcheck hook passes on every touched file, with no new suppression and no disable directive.",
"No lib sources a sibling: each entry script sources every lib directly, in dependency order.",
"The stubbed run reports every moved function as defined, for all three scripts.",
"Every SC2164 and SC2103 the split exposes is fixed at the source. Where a function exports a variable to its caller it must not become a subshell; guard the cd and restore the previous directory instead.",
"No script is executed. These flash SD cards, root a phone over USB and configure a web server."
],
"out_of_scope": [
"Executing any flash, root or configure step.",
"check_and_enable_services.sh and the other deferred scripts, whose seams cross shared mutable state.",
"Changing what any script does when run, beyond the cd guards."
],
"verifier": "meta/scripts/verify_shell_split.sh per script, plus shellcheck over every lib in isolation, plus a grep confirming no lib sources a sibling, plus pre-commit run --files and check_file_length.sh."
}

View File

@ -0,0 +1,47 @@
{
"intent": "Split three more very large linux_configuration scripts under the cap, and fix the directory handling the splits exposed.",
"scope": [
"single_use/features/raspberry_pi_nextcloud.sh (1276 -> 163) + 7 libs",
"single_use/utils/root_bl9000.sh (1116 -> 211) + 6 libs",
"single_use/utils/lookup_docs.sh (942 -> 165) + 6 libs",
"Non-goal: no script executed"
],
"changes": [
"Sixteen new libs across the three scripts. raspberry_pi_nextcloud and root_bl9000 each needed a second carve after the first pass left a lib over the cap.",
"lookup_docs.sh's lookup_python was a single 300-line function. Extracted the keyword-to-anchor mapping into python_keyword_doc, which emits result and desc tab-separated so the caller keeps its own control flow - the block only ever assigned those two variables.",
"Fixed four more unguarded cd calls in the Nextcloud libs, each before an occ command or a download.",
"bl9000_mtkclient.sh had the cd X; work; cd - pattern with three separate return paths. install_mtkclient became a subshell. extract_boot_with_mtkclient could NOT: it exports BOOT_IMG to its caller, so a subshell would swallow it. Instead the venv check moved to before the cd (so that early return needs no cd back), the cd is guarded, and the restores use a saved $PWD rather than cd -.",
"Flattened two chained source blocks. The root_bl9000 usage split had absorbed the entry's whole source block into a lib, and the docs split had done the same. Both entries now source every lib directly.",
"lookup_docs.sh had no SCRIPT_DIR at all - it needed one adding before its new source block."
],
"verification": [
{
"command": "meta/scripts/verify_shell_split.sh per script",
"result": "fail then pass",
"evidence": "Three rounds of failures, each a real defect: SC2164 on the unguarded cd calls, SC1091 from the chained source blocks, and then '/lib/docs_python_keywords.sh: No such file' because SCRIPT_DIR was undefined. All three report 'libs sourced, functions defined' now."
},
{
"command": "shellcheck over every lib in isolation",
"result": "pass",
"evidence": "Clean, including SC2103, with no disable directive anywhere."
},
{
"command": "grep -l 'source \"$SCRIPT_DIR/lib/' over all libs in the touched directories",
"result": "pass",
"evidence": "No matches - no lib sources a sibling. This is the third occurrence of the chaining bug, so it is now checked explicitly rather than noticed."
},
{
"command": "bash ~/utils/scripts/check_file_length.sh --all",
"result": "pass",
"evidence": "None of the three entry scripts or sixteen libs is listed."
}
],
"risks": [
"None of the three was run: they flash SD cards, put a phone into BROM mode over USB, and configure Apache and MariaDB. The cd fixes change failure paths only.",
"extract_boot_with_mtkclient still uses cd rather than a subshell, deliberately, because it exports BOOT_IMG. The restore is now unconditional on both exit paths, but a future edit adding a third return must restore too - a subshell would make that impossible to get wrong, at the cost of the exported variable."
],
"rollback": [
"git revert the commit; the three scripts return to their previous sizes and the sixteen libs disappear.",
"After rollback, re-run bash -n and shellcheck on the restored scripts."
]
}

View File

@ -0,0 +1,179 @@
#!/bin/bash
# CA certificate installation.
#
# Sourced by raspberry_pi_nextcloud.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
# =============================================================================
# Install CA Certificate on Client
# =============================================================================
phase_install_ca() {
log_info "=== Installing Nextcloud CA Certificate ==="
if [[ -z $PI_PASSWORD ]]; then
die "PI_PASSWORD not set. Run this after running install-remote or flash."
fi
local pi_ip
pi_ip=$(discover_raspberry_pi)
if [[ -z $pi_ip ]]; then
die "Failed to discover Raspberry Pi"
fi
log_info "Downloading CA certificate from Pi..."
local ca_file="/tmp/nextcloud-ca.crt"
# Use SSH with sudo to cat the file (since it's in a protected directory)
sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no \
"${PI_USER}@${pi_ip}" "echo '$PI_PASSWORD' | sudo -S cat /etc/ssl/nextcloud/ca.crt" >"$ca_file" 2>/dev/null
if [[ ! -f $ca_file ]] || [[ ! -s $ca_file ]]; then
die "Failed to download CA certificate"
fi
log_success "CA certificate downloaded to: $ca_file"
# Detect OS and install appropriately
if [[ -f /etc/arch-release ]]; then
log_info "Detected Arch Linux - installing CA..."
sudo cp "$ca_file" /etc/ca-certificates/trust-source/anchors/nextcloud-ca.crt
sudo trust extract-compat
log_success "CA installed in system trust store"
elif [[ -f /etc/debian_version ]]; then
log_info "Detected Debian/Ubuntu - installing CA..."
sudo cp "$ca_file" /usr/local/share/ca-certificates/nextcloud-ca.crt
sudo update-ca-certificates
log_success "CA installed in system trust store"
elif [[ -f /etc/redhat-release ]]; then
log_info "Detected RHEL/Fedora - installing CA..."
sudo cp "$ca_file" /etc/pki/ca-trust/source/anchors/nextcloud-ca.crt
sudo update-ca-trust
log_success "CA installed in system trust store"
elif [[ "$(uname)" == "Darwin" ]]; then
log_info "Detected macOS - installing CA..."
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$ca_file"
log_success "CA installed in system keychain"
else
log_warning "Unknown OS - please install CA manually from: $ca_file"
fi
# Install in browser certificate stores
log_info "Installing CA in browser certificate stores..."
# Chrome/Chromium (uses NSS)
if [[ -d ~/.pki/nssdb ]] || command -v certutil &>/dev/null; then
mkdir -p ~/.pki/nssdb
if ! certutil -d sql:~/.pki/nssdb -L 2>/dev/null | grep -q "Nextcloud"; then
# Initialize NSS db if needed
certutil -d sql:~/.pki/nssdb -N --empty-password 2>/dev/null || true
if certutil -d sql:~/.pki/nssdb -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2>/dev/null; then
log_success "CA installed in Chrome/Chromium"
else
log_warning "Could not install in Chrome/Chromium NSS db"
fi
else
log_info "CA already installed in Chrome/Chromium"
fi
fi
# Firefox (has its own profile NSS databases)
if [[ -d ~/.mozilla/firefox ]]; then
local installed=0
for profile_dir in ~/.mozilla/firefox/*.default* ~/.mozilla/firefox/*.esr*; do
if [[ -d $profile_dir ]]; then
if ! certutil -d sql:"$profile_dir" -L 2>/dev/null | grep -q "Nextcloud"; then
certutil -d sql:"$profile_dir" -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2>/dev/null &&
installed=1
else
installed=1
fi
fi
done
if [[ $installed -eq 1 ]]; then
log_success "CA installed in Firefox"
else
log_warning "Could not install in Firefox - you may need to import manually"
fi
fi
# Add hostname to /etc/hosts if not present
if ! grep -q "$PI_HOSTNAME" /etc/hosts 2>/dev/null; then
log_info "Adding $PI_HOSTNAME to /etc/hosts..."
echo "$pi_ip $PI_HOSTNAME ${PI_HOSTNAME}.local" | sudo tee -a /etc/hosts >/dev/null
log_success "Added $PI_HOSTNAME to /etc/hosts"
else
log_info "$PI_HOSTNAME already in /etc/hosts"
fi
# Verify
log_info "Verifying HTTPS connection..."
if curl -s --max-time 5 "https://$PI_HOSTNAME/status.php" 2>/dev/null | grep -q "installed"; then
log_success "HTTPS connection verified - no certificate warnings!"
else
log_warning "Could not verify HTTPS - you may need to restart your browser"
fi
log_success "========================================"
log_success "CA Certificate installed!"
log_success "========================================"
echo
log_info "Access Nextcloud at: https://$PI_HOSTNAME"
log_info "Your browser should now trust the certificate without warnings."
echo
log_info "For other devices (phones, tablets, other computers):"
log_info " Download: https://$PI_HOSTNAME/ca/nextcloud-ca.crt"
log_info " Then install the certificate in your device's trust store."
}
# =============================================================================
# Main
# =============================================================================
show_help() {
cat <<'EOF'
Nextcloud Installation Script for Raspberry Pi
Usage: ./raspberry_pi_nextcloud.sh <command>
Commands:
install-remote Install Nextcloud via SSH from your laptop (recommended)
setup-ssl-remote Setup Let's Encrypt SSL with DuckDNS (auto-trusted on all devices)
install-ca Install self-signed CA on this machine (alternative to setup-ssl)
configure Configure Pi system (run on Pi)
install-local Install Nextcloud (run on Pi)
fix Fix common Nextcloud issues (run on Pi)
setup-ssl Setup Let's Encrypt SSL (run on Pi)
help Show this help message
The script will:
1. Configure the Raspberry Pi system (SSH hardening, firewall, etc.)
2. Install Apache, PHP, MariaDB, Redis
3. Download and install Nextcloud
4. Configure caching, background jobs, and security
For HTTPS trusted on ALL devices automatically:
./raspberry_pi_nextcloud.sh install-remote
./raspberry_pi_nextcloud.sh setup-ssl-remote
This uses DuckDNS (free) + Let's Encrypt for real trusted certificates.
Go to https://www.duckdns.org/ to get your free domain first.
For self-signed certificates (requires manual CA install on each device):
./raspberry_pi_nextcloud.sh install-remote
./raspberry_pi_nextcloud.sh install-ca
For local installation (on Pi):
sudo ./raspberry_pi_nextcloud.sh configure
sudo ./raspberry_pi_nextcloud.sh install-local
sudo ./raspberry_pi_nextcloud.sh fix
sudo ./raspberry_pi_nextcloud.sh setup-ssl
EOF
}

View File

@ -0,0 +1,119 @@
#!/bin/bash
# Dependency install, apt lock wait and Pi discovery.
#
# Sourced by raspberry_pi_nextcloud.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
wait_for_apt_lock() {
local max_wait=600
local waited=0
while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock >/dev/null 2>&1; do
if [[ $waited -eq 0 ]]; then
log_info "Waiting for other apt/dpkg processes to finish..."
pgrep -a 'apt|dpkg' | head -5 >&2 || true
fi
sleep 5
waited=$((waited + 5))
if [[ $waited -ge $max_wait ]]; then
die "Timeout waiting for apt lock after ${max_wait}s"
fi
if [[ $((waited % 30)) -eq 0 ]]; then
log_info "Still waiting... (${waited}s elapsed)"
fi
done
if [[ $waited -gt 0 ]]; then
log_success "Apt lock acquired after ${waited}s"
fi
}
# =============================================================================
# Network Discovery (for remote installation)
# =============================================================================
ensure_dependencies() {
local missing_packages=()
if ! command -v nmap &>/dev/null; then
missing_packages+=("nmap")
fi
if ! command -v sshpass &>/dev/null; then
missing_packages+=("sshpass")
fi
if [[ ${#missing_packages[@]} -gt 0 ]]; then
log_info "Installing missing packages: ${missing_packages[*]}"
if command -v pacman &>/dev/null; then
sudo pacman -S --noconfirm "${missing_packages[@]}"
elif command -v apt-get &>/dev/null; then
sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}"
elif command -v dnf &>/dev/null; then
sudo dnf install -y "${missing_packages[@]}"
else
die "Could not detect package manager. Please install manually: ${missing_packages[*]}"
fi
fi
}
discover_raspberry_pi() {
log_info "Auto-discovering Raspberry Pi on local network..."
ensure_dependencies
local my_ip
my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1)
local gateway
gateway=$(ip route | grep default | awk '{print $3}' | head -1)
local network="${gateway%.*}.0/24"
log_info "Local IP: $my_ip, Network: $network"
log_info "Scanning for Raspberry Pi (hostname: $PI_HOSTNAME)..."
local pi_ip=""
# Try resolving hostname directly
pi_ip=$(getent hosts "$PI_HOSTNAME" 2>/dev/null | awk '{print $1}' | head -1) || true
if [[ -z $pi_ip ]]; then
pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2>/dev/null | awk '{print $1}' | head -1) || true
fi
if [[ -n $pi_ip ]]; then
log_success "Found Pi by hostname: $pi_ip"
echo "$pi_ip"
return
fi
log_info "Hostname resolution failed, scanning network..."
nmap -sn -T4 "$network" &>/dev/null || true
local ssh_hosts
ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) || true
if [[ -z $ssh_hosts ]]; then
die "No SSH-enabled devices found. Is the Pi connected and booted?"
fi
log_info "Found SSH-enabled devices: $(echo "$ssh_hosts" | tr '\n' ' ')"
for ip in $ssh_hosts; do
log_info "Trying $ip with user '$PI_USER'..."
if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "hostname" 2>/dev/null | grep -qi "$PI_HOSTNAME"; then
log_success "Found Raspberry Pi at $ip"
echo "$ip"
return
fi
if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then
log_success "Found device responding to Pi credentials at $ip"
echo "$ip"
return
fi
done
die "Could not find Raspberry Pi on network."
}

View File

@ -0,0 +1,233 @@
#!/bin/bash
# The post-install fix-up phase.
#
# Sourced by raspberry_pi_nextcloud.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
# =============================================================================
# Fix Nextcloud Issues
# =============================================================================
# shellcheck disable=SC2120 # Function does not use positional args
phase_fix_issues() {
check_root
log_info "=== Fixing Nextcloud Issues ==="
cd /var/www/nextcloud || { log_error "Nextcloud is not installed at /var/www/nextcloud"; return 1; }
# 1. Fix background jobs (cron not running properly)
log_info "Fixing background jobs..."
# Ensure cron is set as background job method
sudo -u www-data php occ background:cron
# Ensure cron job exists and is correct
(
crontab -u www-data -l 2>/dev/null | grep -v "cron.php"
echo "*/5 * * * * php -f /var/www/nextcloud/cron.php"
) | crontab -u www-data -
# Run cron manually now to reset the timer
log_info "Running cron job manually..."
sudo -u www-data php /var/www/nextcloud/cron.php
log_success "Background jobs configured"
# 2. Setup HTTPS with proper CA-signed certificate
log_info "Setting up HTTPS with trusted CA..."
local pi_ip
pi_ip=$(hostname -I | awk '{print $1}')
local ssl_dir="/etc/ssl/nextcloud"
mkdir -p "$ssl_dir"
chmod 700 "$ssl_dir"
# Generate CA if it doesn't exist
if [[ ! -f "$ssl_dir/ca.crt" ]]; then
log_info "Creating Certificate Authority (CA)..."
# Generate CA private key
openssl genrsa -out "$ssl_dir/ca.key" 4096
chmod 600 "$ssl_dir/ca.key"
# Generate CA certificate (valid for 10 years)
openssl req -x509 -new -nodes -key "$ssl_dir/ca.key" \
-sha256 -days 3650 \
-out "$ssl_dir/ca.crt" \
-subj "/C=PL/ST=Home/L=Local/O=Nextcloud Home CA/OU=Certificate Authority/CN=Nextcloud Home CA"
log_success "CA created: $ssl_dir/ca.crt"
fi
# Generate server certificate signed by our CA
local regenerate="${1:-}"
if [[ ! -f "$ssl_dir/server.crt" ]] || [[ $regenerate == "--regenerate" ]]; then
log_info "Generating server certificate signed by CA..."
# Generate server private key
openssl genrsa -out "$ssl_dir/server.key" 2048
chmod 600 "$ssl_dir/server.key"
# Create certificate signing request (CSR)
openssl req -new -key "$ssl_dir/server.key" \
-out "$ssl_dir/server.csr" \
-subj "/C=PL/ST=Home/L=Local/O=Nextcloud/OU=Server/CN=$PI_HOSTNAME"
# Create extension file for SAN (Subject Alternative Names)
# This allows the certificate to be valid for hostname, IP, and .local
cat >"$ssl_dir/server.ext" <<EXTEOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = $PI_HOSTNAME
DNS.2 = ${PI_HOSTNAME}.local
DNS.3 = localhost
IP.1 = $pi_ip
IP.2 = 127.0.0.1
EXTEOF
# Sign the certificate with our CA (valid for 2 years)
openssl x509 -req -in "$ssl_dir/server.csr" \
-CA "$ssl_dir/ca.crt" \
-CAkey "$ssl_dir/ca.key" \
-CAcreateserial \
-out "$ssl_dir/server.crt" \
-days 730 \
-sha256 \
-extfile "$ssl_dir/server.ext"
rm -f "$ssl_dir/server.csr" "$ssl_dir/server.ext"
log_success "Server certificate created and signed by CA"
fi
# Copy CA to web-accessible location for easy download
mkdir -p /var/www/nextcloud/ca
cp "$ssl_dir/ca.crt" /var/www/nextcloud/ca/nextcloud-ca.crt
chown -R www-data:www-data /var/www/nextcloud/ca
log_info "CA certificate available at: https://$PI_HOSTNAME/ca/nextcloud-ca.crt"
# Create HTTPS Apache config
cat >/etc/apache2/sites-available/nextcloud-ssl.conf <<EOF
<VirtualHost *:443>
ServerAdmin admin@localhost
DocumentRoot /var/www/nextcloud
ServerName $PI_HOSTNAME
ServerAlias ${PI_HOSTNAME}.local $pi_ip
SSLEngine on
SSLCertificateFile $ssl_dir/server.crt
SSLCertificateKeyFile $ssl_dir/server.key
<Directory /var/www/nextcloud/>
Require all granted
AllowOverride All
Options FollowSymLinks MultiViews
<IfModule mod_dav.c>
Dav off
</IfModule>
</Directory>
# Security headers
Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "no-referrer"
ErrorLog \${APACHE_LOG_DIR}/nextcloud_ssl_error.log
CustomLog \${APACHE_LOG_DIR}/nextcloud_ssl_access.log combined
</VirtualHost>
# Redirect HTTP to HTTPS
<VirtualHost *:80>
ServerName $PI_HOSTNAME
ServerAlias $pi_ip
Redirect permanent / https://$PI_HOSTNAME/
</VirtualHost>
EOF
a2enmod ssl
a2enmod headers
a2ensite nextcloud-ssl
# Update Nextcloud config for HTTPS
sudo -u www-data php occ config:system:set overwrite.cli.url --value="https://$PI_HOSTNAME"
sudo -u www-data php occ config:system:set overwriteprotocol --value="https"
systemctl restart apache2
log_success "HTTPS configured with CA-signed certificate"
# 3. Run mimetype migrations
log_info "Running mimetype migrations..."
sudo -u www-data php occ maintenance:repair --include-expensive
log_success "Mimetype migrations complete"
# 4. Add missing database indices
log_info "Adding missing database indices..."
sudo -u www-data php occ db:add-missing-indices
log_success "Database indices added"
# 5. Install ImageMagick SVG support
log_info "Installing ImageMagick SVG support..."
DEBIAN_FRONTEND=noninteractive apt-get install -y libmagickcore-6.q16-6-extra
# Enable SVG in ImageMagick policy
local policy_file="/etc/ImageMagick-6/policy.xml"
if [[ -f $policy_file ]]; then
# Remove SVG restrictions if present
sed -i 's/<policy domain="coder" rights="none" pattern="SVG" \/>/<policy domain="coder" rights="read|write" pattern="SVG" \/>/' "$policy_file"
# If no SVG policy exists, add one allowing it
if ! grep -q 'pattern="SVG"' "$policy_file"; then
sed -i '/<policymap>/a\ <policy domain="coder" rights="read|write" pattern="SVG" \/>' "$policy_file"
fi
fi
systemctl restart apache2
log_success "ImageMagick SVG support configured"
# 6. Set up basic SMTP (placeholder - user needs to configure actual mail server)
log_info "Note: Email server not configured - please configure in Nextcloud admin settings"
# 7. Clear any remaining warnings
log_info "Clearing Nextcloud caches..."
sudo -u www-data php occ maintenance:repair
sudo -u www-data php occ files:scan --all
# 8. Verify all fixes
log_info "Verifying fixes..."
# Run cron again to update last run time
sudo -u www-data php /var/www/nextcloud/cron.php
log_success "========================================"
log_success "Nextcloud issues fixed!"
log_success "========================================"
echo
log_info "Summary of changes:"
log_info " ✓ Background jobs (cron) configured and running"
log_info " ✓ HTTPS enabled with CA-signed certificate"
log_info " ✓ Strict-Transport-Security header added"
log_info " ✓ Mimetype migrations completed"
log_info " ✓ Missing database indices added"
log_info " ✓ ImageMagick SVG support installed"
echo
log_info "Current certificate: self-signed CA (requires manual install on devices)"
log_info " - Run: $0 install-ca (on your laptop)"
log_info " - Or download: https://$PI_HOSTNAME/ca/nextcloud-ca.crt"
echo
log_info "For auto-trusted HTTPS on ALL devices (recommended):"
log_info " 1. Get free domain at https://www.duckdns.org/"
log_info " 2. Run: $0 setup-ssl"
echo
log_info "Access Nextcloud at: https://$PI_HOSTNAME"
}

View File

@ -0,0 +1,228 @@
#!/bin/bash
# The Nextcloud install phase.
#
# Sourced by raspberry_pi_nextcloud.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
# =============================================================================
# Nextcloud Installation Phase
# =============================================================================
phase_install_nextcloud() {
check_root
log_info "=== Installing Nextcloud ==="
wait_for_apt_lock
log_info "Installing Apache, PHP, MariaDB, and dependencies..."
DEBIAN_FRONTEND=noninteractive apt-get install -y \
apache2 \
mariadb-server \
php \
php-gd \
php-json \
php-mysql \
php-curl \
php-mbstring \
php-intl \
php-imagick \
php-xml \
php-zip \
php-bz2 \
php-bcmath \
php-gmp \
php-apcu \
php-redis \
php-ldap \
libapache2-mod-php \
redis-server \
certbot \
python3-certbot-apache \
imagemagick \
libmagickcore-6.q16-6-extra
log_success "Packages installed"
# Configure MariaDB
log_info "Configuring MariaDB..."
local db_password
db_password=$(generate_password 32)
mysql -u root <<EOF
CREATE DATABASE IF NOT EXISTS nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS 'nextcloud'@'localhost' IDENTIFIED BY '${db_password}';
GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost';
FLUSH PRIVILEGES;
EOF
echo "$db_password" >/root/.nextcloud_db_password
chmod 600 /root/.nextcloud_db_password
log_success "MariaDB configured"
# Download Nextcloud
log_info "Downloading Nextcloud..."
cd /tmp || { log_error "Cannot enter /tmp"; return 1; }
if [[ ! -f nextcloud.zip ]]; then
wget -q --show-progress "https://download.nextcloud.com/server/releases/latest.zip" -O nextcloud.zip >&2
fi
rm -rf /var/www/nextcloud
unzip -q nextcloud.zip -d /var/www/
chown -R www-data:www-data /var/www/nextcloud
log_success "Nextcloud downloaded and extracted"
# Configure Apache
log_info "Configuring Apache..."
cat >/etc/apache2/sites-available/nextcloud.conf <<'EOF'
<VirtualHost *:80>
ServerAdmin admin@localhost
DocumentRoot /var/www/nextcloud
<Directory /var/www/nextcloud/>
Require all granted
AllowOverride All
Options FollowSymLinks MultiViews
<IfModule mod_dav.c>
Dav off
</IfModule>
</Directory>
ErrorLog ${APACHE_LOG_DIR}/nextcloud_error.log
CustomLog ${APACHE_LOG_DIR}/nextcloud_access.log combined
</VirtualHost>
EOF
a2enmod rewrite
a2enmod headers
a2enmod env
a2enmod dir
a2enmod mime
a2enmod ssl
a2dissite 000-default
a2ensite nextcloud
systemctl restart apache2
log_success "Apache configured"
# Configure PHP
log_info "Configuring PHP..."
local php_version
php_version=$(php -v | head -1 | grep -oP '\d+\.\d+')
local php_ini="/etc/php/${php_version}/apache2/php.ini"
sed -i 's/memory_limit = .*/memory_limit = 512M/' "$php_ini"
sed -i 's/upload_max_filesize = .*/upload_max_filesize = 16G/' "$php_ini"
sed -i 's/post_max_size = .*/post_max_size = 16G/' "$php_ini"
sed -i 's/max_execution_time = .*/max_execution_time = 3600/' "$php_ini"
sed -i 's/max_input_time = .*/max_input_time = 3600/' "$php_ini"
sed -i 's/;date.timezone =.*/date.timezone = Europe\/Warsaw/' "$php_ini"
if ! grep -q "opcache.interned_strings_buffer" "$php_ini"; then
cat >>"$php_ini" <<'EOF'
; Nextcloud optimizations
opcache.enable=1
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.memory_consumption=128
opcache.save_comments=1
opcache.revalidate_freq=1
; APCu configuration
apc.enable_cli=1
EOF
fi
systemctl restart apache2
log_success "PHP configured"
# Configure Redis
log_info "Configuring Redis..."
systemctl enable redis-server
systemctl start redis-server
log_success "Redis configured"
# Install Nextcloud
log_info "Installing Nextcloud..."
auto_generate_nextcloud_password
local pi_ip
pi_ip=$(hostname -I | awk '{print $1}')
cd /var/www/nextcloud || { log_error "Nextcloud is not installed at /var/www/nextcloud"; return 1; }
sudo -u www-data php occ maintenance:install \
--database "mysql" \
--database-name "nextcloud" \
--database-user "nextcloud" \
--database-pass "$db_password" \
--admin-user "$NEXTCLOUD_ADMIN_USER" \
--admin-pass "$NEXTCLOUD_ADMIN_PASSWORD" \
--data-dir "$NEXTCLOUD_DATA_DIR"
# Configure trusted domains
sudo -u www-data php occ config:system:set trusted_domains 1 --value="$pi_ip"
sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME"
sudo -u www-data php occ config:system:set trusted_domains 3 --value="${PI_HOSTNAME}.local"
# Configure caching
sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu'
sudo -u www-data php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis'
sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis'
sudo -u www-data php occ config:system:set redis host --value='localhost'
sudo -u www-data php occ config:system:set redis port --value=6379 --type=integer
# Set default phone region
sudo -u www-data php occ config:system:set default_phone_region --value='PL'
# Set maintenance window
sudo -u www-data php occ config:system:set maintenance_window_start --value=1 --type=integer
log_success "Nextcloud installed"
# Setup background jobs
log_info "Setting up Nextcloud background jobs..."
sudo -u www-data php occ background:cron
# Add cron job
(
crontab -u www-data -l 2>/dev/null || true
echo "*/5 * * * * php -f /var/www/nextcloud/cron.php"
) | sort -u | crontab -u www-data -
log_success "Cron jobs configured"
# Verify installation
log_info "Verifying Nextcloud installation..."
if sudo -u www-data php occ status | grep -q "installed: true"; then
log_success "Nextcloud is responding!"
sudo -u www-data php occ status
else
log_warning "Nextcloud may not be fully configured"
fi
save_config
log_success "========================================"
log_success "Nextcloud installation complete!"
log_success "========================================"
log_info "Access Nextcloud at: http://$pi_ip"
log_info "Admin user: $NEXTCLOUD_ADMIN_USER"
log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD"
log_info "Database password saved at: /root/.nextcloud_db_password"
}

View File

@ -0,0 +1,197 @@
#!/bin/bash
# SSL setup, local and remote.
#
# Sourced by raspberry_pi_nextcloud.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
# =============================================================================
# Setup Let's Encrypt SSL with DuckDNS
# =============================================================================
phase_setup_ssl() {
check_root
log_info "=== Setting up Let's Encrypt SSL with DuckDNS ==="
# Check if DuckDNS is configured
if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then
echo
log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain."
log_info "1. Go to https://www.duckdns.org/ and sign in with Google/GitHub/etc."
log_info "2. Create a subdomain (e.g., 'myhomecloud' for myhomecloud.duckdns.org)"
log_info "3. Copy your token from the DuckDNS page"
echo
read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN
read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN
read -r -p "Enter your email (for Let's Encrypt notifications): " LETSENCRYPT_EMAIL
if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]] || [[ -z $LETSENCRYPT_EMAIL ]]; then
die "All fields are required"
fi
fi
local full_domain="${DUCKDNS_DOMAIN}.duckdns.org"
local pi_local_ip
pi_local_ip=$(hostname -I | awk '{print $1}')
# Get public IP for DuckDNS (Let's Encrypt needs external access)
local public_ip
public_ip=$(curl -s https://api.ipify.org) || public_ip=$(curl -s https://ifconfig.me) || true
log_info "Domain: $full_domain"
log_info "Pi local IP: $pi_local_ip"
log_info "Public IP: $public_ip"
echo
log_warning "=== IMPORTANT: Port Forwarding Required ==="
log_warning "For Let's Encrypt to work, you MUST forward ports on your router:"
log_warning " - Forward port 80 (HTTP) to $pi_local_ip"
log_warning " - Forward port 443 (HTTPS) to $pi_local_ip"
log_warning ""
log_warning "Go to your router admin page (usually http://192.168.1.1)"
log_warning "and set up port forwarding before continuing."
echo
read -r -p "Have you set up port forwarding? (yes/no): " port_forward_done
if [[ $port_forward_done != "yes" ]]; then
log_info "Please set up port forwarding and run this command again."
log_info "Without port forwarding, Let's Encrypt cannot verify your domain."
exit 0
fi
# Update DuckDNS to point to PUBLIC IP (not local IP)
log_info "Updating DuckDNS to point to public IP $public_ip..."
local duckdns_response
# When ip= is empty, DuckDNS auto-detects the public IP
duckdns_response=$(curl -s "https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=")
if [[ $duckdns_response != "OK" ]]; then
die "Failed to update DuckDNS: $duckdns_response"
fi
log_success "DuckDNS updated to public IP"
# Set up automatic DuckDNS updates (cron) - auto-detect public IP
log_info "Setting up automatic DuckDNS IP updates..."
mkdir -p /opt/duckdns
cat >/opt/duckdns/duck.sh <<DUCKEOF
#!/bin/bash
echo url="https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=" | curl -k -o /opt/duckdns/duck.log -K -
DUCKEOF
chmod 700 /opt/duckdns/duck.sh
# Add cron job for DuckDNS update every 5 minutes
(crontab -l 2>/dev/null || true) | grep -v "duckdns" | {
cat
echo "*/5 * * * * /opt/duckdns/duck.sh >/dev/null 2>&1"
} | crontab -
log_success "DuckDNS auto-update configured"
# Wait for DNS propagation
log_info "Waiting for DNS propagation (this may take a minute)..."
local dns_ip=""
local attempts=0
while [[ $dns_ip != "$public_ip" ]] && [[ $attempts -lt 12 ]]; do
sleep 5
dns_ip=$(dig +short "$full_domain" 2>/dev/null | tail -1) || true
attempts=$((attempts + 1))
log_info " DNS lookup: $dns_ip (expecting $public_ip, attempt $attempts/12)"
done
if [[ $dns_ip != "$public_ip" ]]; then
log_warning "DNS may not have propagated yet. Continuing anyway..."
else
log_success "DNS verified: $full_domain -> $public_ip"
fi
# Install certbot if not present
if ! command -v certbot &>/dev/null; then
log_info "Installing certbot..."
DEBIAN_FRONTEND=noninteractive apt-get install -y certbot python3-certbot-apache
fi
# Get Let's Encrypt certificate
log_info "Obtaining Let's Encrypt certificate..."
# First update Apache config with the new domain
cat >/etc/apache2/sites-available/nextcloud-ssl.conf <<EOF
<VirtualHost *:443>
ServerAdmin ${LETSENCRYPT_EMAIL}
DocumentRoot /var/www/nextcloud
ServerName ${full_domain}
SSLEngine on
# Certbot will update these paths
SSLCertificateFile /etc/ssl/nextcloud/server.crt
SSLCertificateKeyFile /etc/ssl/nextcloud/server.key
<Directory /var/www/nextcloud/>
Require all granted
AllowOverride All
Options FollowSymLinks MultiViews
<IfModule mod_dav.c>
Dav off
</IfModule>
</Directory>
# Security headers
Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "no-referrer"
ErrorLog \${APACHE_LOG_DIR}/nextcloud_ssl_error.log
CustomLog \${APACHE_LOG_DIR}/nextcloud_ssl_access.log combined
</VirtualHost>
<VirtualHost *:80>
ServerName ${full_domain}
Redirect permanent / https://${full_domain}/
</VirtualHost>
EOF
systemctl reload apache2
# Run certbot
certbot --apache -d "$full_domain" --non-interactive --agree-tos --email "$LETSENCRYPT_EMAIL" --redirect
log_success "Let's Encrypt certificate obtained!"
# Update Nextcloud trusted domains
log_info "Updating Nextcloud configuration..."
cd /var/www/nextcloud || { log_error "Nextcloud is not installed at /var/www/nextcloud"; return 1; }
sudo -u www-data php occ config:system:set trusted_domains 0 --value="$full_domain"
sudo -u www-data php occ config:system:set overwrite.cli.url --value="https://$full_domain"
sudo -u www-data php occ config:system:set overwriteprotocol --value="https"
sudo -u www-data php occ config:system:set overwritehost --value="$full_domain"
# Keep local access working
sudo -u www-data php occ config:system:set trusted_domains 1 --value="$pi_local_ip"
sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME"
sudo -u www-data php occ config:system:set trusted_domains 3 --value="${PI_HOSTNAME}.local"
log_success "Nextcloud configured for $full_domain"
# Set up auto-renewal
log_info "Setting up automatic certificate renewal..."
systemctl enable certbot.timer
systemctl start certbot.timer
log_success "========================================"
log_success "Let's Encrypt SSL configured!"
log_success "========================================"
echo
log_info "Your Nextcloud is now accessible at:"
log_info " https://$full_domain (from anywhere on the internet)"
log_info " https://$pi_local_ip (from your local network)"
echo
log_info "This certificate is trusted by ALL browsers and devices automatically!"
log_info "No manual certificate installation required."
echo
log_info "Certificate auto-renewal is enabled."
log_info "DuckDNS IP auto-update is enabled."
}

View File

@ -0,0 +1,111 @@
#!/bin/bash
# Remote SSL setup and the remote install phase.
#
# Sourced by raspberry_pi_nextcloud.sh; split out to keep rpi_nc_ssl.sh
# under the 250-line cap. Sourced rather than run, so it inherits the
# caller's strict mode and the variables defined above the source line.
phase_setup_ssl_remote() {
log_info "=== Setting up Let's Encrypt SSL via SSH ==="
if [[ -z $PI_PASSWORD ]]; then
die "PI_PASSWORD not set. Run install-remote first."
fi
local pi_ip
pi_ip=$(discover_raspberry_pi)
if [[ -z $pi_ip ]]; then
die "Failed to discover Raspberry Pi"
fi
# Get DuckDNS credentials if not set
if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then
echo
log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain."
log_info "1. Go to https://www.duckdns.org/ and sign in"
log_info "2. Create a subdomain (e.g., 'myhomecloud')"
log_info "3. Copy your token"
echo
read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN
read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN
read -r -p "Enter your email (for Let's Encrypt): " LETSENCRYPT_EMAIL
fi
save_config
log_info "Copying script to Pi..."
sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/raspberry_pi_nextcloud.sh"
log_info "Running SSL setup on Pi..."
sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \
"echo '$PI_PASSWORD' | sudo -S DUCKDNS_DOMAIN='$DUCKDNS_DOMAIN' DUCKDNS_TOKEN='$DUCKDNS_TOKEN' LETSENCRYPT_EMAIL='$LETSENCRYPT_EMAIL' bash /tmp/raspberry_pi_nextcloud.sh setup-ssl"
local full_domain="${DUCKDNS_DOMAIN}.duckdns.org"
log_success "========================================"
log_success "SSL setup complete!"
log_success "========================================"
echo
log_info "Access your Nextcloud at: https://$full_domain"
log_info "This works on ALL devices without certificate warnings!"
}
# =============================================================================
# Remote Installation
# =============================================================================
phase_install_remote() {
log_info "=== Installing Nextcloud via SSH ==="
if [[ -z $PI_PASSWORD ]]; then
die "PI_PASSWORD not set. Did you run flash script first?"
fi
local pi_ip
pi_ip=$(discover_raspberry_pi)
if [[ -z $pi_ip ]]; then
die "Failed to discover Raspberry Pi"
fi
log_info "Using Raspberry Pi at: $pi_ip"
# Remove old host key if present
ssh-keygen -R "$pi_ip" 2>/dev/null || true
log_info "Copying script to Pi..."
sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/raspberry_pi_nextcloud.sh"
log_info "Running system configuration on Pi..."
sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \
"echo '$PI_PASSWORD' | sudo -S bash /tmp/raspberry_pi_nextcloud.sh configure"
log_info "Installing Nextcloud on Pi..."
auto_generate_nextcloud_password
save_config
log_success "Nextcloud admin user: $NEXTCLOUD_ADMIN_USER"
log_success "Nextcloud admin password: $NEXTCLOUD_ADMIN_PASSWORD"
sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \
"echo '$PI_PASSWORD' | sudo -S NEXTCLOUD_ADMIN_PASSWORD='$NEXTCLOUD_ADMIN_PASSWORD' NEXTCLOUD_ADMIN_USER='$NEXTCLOUD_ADMIN_USER' bash /tmp/raspberry_pi_nextcloud.sh install-local"
log_info "Fixing Nextcloud issues..."
sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \
"echo '$PI_PASSWORD' | sudo -S bash /tmp/raspberry_pi_nextcloud.sh fix"
log_success "========================================"
log_success "Remote Nextcloud installation complete!"
log_success "========================================"
echo
log_info "=== Access Information ==="
log_info "Nextcloud URL: https://$pi_ip"
log_info "Admin user: $NEXTCLOUD_ADMIN_USER"
log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD"
log_info "All credentials saved in: $CONFIG_FILE"
echo
log_info "=== Trust the certificate ==="
log_info "Run: $0 install-ca"
}

View File

@ -0,0 +1,104 @@
#!/bin/bash
# System configuration phase.
#
# Sourced by raspberry_pi_nextcloud.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
# =============================================================================
# System Configuration Phase
# =============================================================================
phase_configure_system() {
check_root
log_info "=== Configuring Raspberry Pi System ==="
wait_for_apt_lock
log_info "Fixing any broken packages..."
DEBIAN_FRONTEND=noninteractive dpkg --configure -a --force-confdef --force-confold || true
log_info "Updating system packages..."
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y
log_info "Setting timezone to $PI_TIMEZONE..."
timedatectl set-timezone "$PI_TIMEZONE"
log_info "Configuring locale..."
sed -i "s/^# *$PI_LOCALE/$PI_LOCALE/" /etc/locale.gen
locale-gen
update-locale LANG="$PI_LOCALE"
log_info "Hardening SSH configuration..."
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
cat >>/etc/ssh/sshd_config.d/hardening.conf <<'EOF'
# Security hardening
PermitRootLogin no
PasswordAuthentication yes
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
EOF
systemctl restart sshd
log_info "Installing useful packages..."
DEBIAN_FRONTEND=noninteractive apt-get install -y \
vim \
htop \
curl \
wget \
git \
ufw \
fail2ban \
unattended-upgrades
log_info "Configuring firewall..."
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
log_info "Configuring fail2ban..."
cat >/etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
EOF
systemctl enable fail2ban
systemctl restart fail2ban
log_info "Enabling automatic security updates..."
cat >/etc/apt/apt.conf.d/50unattended-upgrades <<'EOF'
Unattended-Upgrade::Origins-Pattern {
"origin=Debian,codename=${distro_codename},label=Debian-Security";
"origin=Raspbian,codename=${distro_codename},label=Raspbian";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
EOF
cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
EOF
log_success "System configuration complete!"
}

View File

@ -0,0 +1,165 @@
#!/bin/bash
# Device data backup before rooting.
#
# Sourced by root_bl9000.sh; split out to keep bl9000_deps.sh under the
# 250-line cap. Sourced rather than run, so it inherits the caller's
# strict mode and the variables defined above the source line.
backup_device_data() {
print_header "Backing Up Device Data"
local backup_dir
backup_dir="${WORK_DIR}/backup_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$backup_dir"
log "Backup directory: $backup_dir" # Check device connection first
if ! adb get-state > /dev/null 2>&1; then
error "Device not connected. Please connect your device first."
return 1
fi
log "Starting comprehensive backup process..."
# 1. Backup internal storage (DCIM, Pictures, Documents, Downloads, etc.)
log "Backing up internal storage (this may take a while)..."
local storage_dirs=("DCIM" "Pictures" "Documents" "Download" "Music" "Movies" "WhatsApp" "Telegram")
for dir in "${storage_dirs[@]}"; do
if adb shell "[ -d /sdcard/$dir ]" 2> /dev/null; then
log " → Backing up /sdcard/$dir..."
if adb pull "/sdcard/$dir" "$backup_dir/$dir" 2>&1 | grep -v "^$"; then
log " ✓ $dir backed up successfully"
else
warn " ⚠ Could not backup $dir (may be empty or inaccessible)"
fi
fi
done
# 2. Backup SMS/MMS (if possible)
log "Backing up SMS/MMS database..."
if adb shell "su -c 'cp /data/data/com.android.providers.telephony/databases/mmssms.db /sdcard/mmssms.db'" 2> /dev/null; then
adb pull /sdcard/mmssms.db "$backup_dir/mmssms.db" 2> /dev/null && log " ✓ SMS/MMS backed up"
adb shell "rm /sdcard/mmssms.db" 2> /dev/null || true
else
warn " ⚠ SMS/MMS backup requires root (skipping)"
fi
# 3. Backup contacts
log "Backing up contacts..."
if adb shell "su -c 'cp /data/data/com.android.providers.contacts/databases/contacts2.db /sdcard/contacts2.db'" 2> /dev/null; then
adb pull /sdcard/contacts2.db "$backup_dir/contacts2.db" 2> /dev/null && log " ✓ Contacts backed up"
adb shell "rm /sdcard/contacts2.db" 2> /dev/null || true
else
warn " ⚠ Contacts backup requires root (skipping)"
fi
# 4. Backup call logs
log "Backing up call logs..."
if adb shell "su -c 'cp /data/data/com.android.providers.contacts/databases/calllog.db /sdcard/calllog.db'" 2> /dev/null; then
adb pull /sdcard/calllog.db "$backup_dir/calllog.db" 2> /dev/null && log " ✓ Call logs backed up"
adb shell "rm /sdcard/calllog.db" 2> /dev/null || true
else
warn " ⚠ Call logs backup requires root (skipping)"
fi
# 5. Backup app list
log "Backing up installed apps list..."
adb shell "pm list packages -f" > "$backup_dir/installed_apps.txt"
log " ✓ App list saved to installed_apps.txt"
# 6. Backup APKs for user-installed apps (optional, can be large)
if confirm "Backup APK files for installed apps? (This can take a long time and use lots of space)"; then
log "Backing up user-installed APKs..."
local apk_dir="$backup_dir/apks"
mkdir -p "$apk_dir"
# Get user-installed packages
local user_apps
user_apps=$(adb shell "pm list packages -3 -f" | sed 's/package://' | cut -d'=' -f2)
local count=0
while IFS= read -r pkg; do
if [[ -n $pkg ]]; then
log " → Backing up $pkg..."
local apk_path
apk_path=$(adb shell "pm path $pkg" | head -n1 | sed 's/package://')
if [[ -n $apk_path ]]; then
adb pull "$apk_path" "$apk_dir/${pkg}.apk" > /dev/null 2>&1 && count=$((count + 1))
fi
fi
done <<< "$user_apps"
log " ✓ Backed up $count APK files"
fi
# 7. Full ADB backup (app data, if device supports it)
log "Creating full ADB backup (app data)..."
if confirm "Create full ADB backup? (You'll need to confirm on your device)"; then
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " On your phone: Tap 'Back up my data' when prompted"
echo " You can set a password or leave it blank"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
if adb backup -apk -shared -all -system -f "$backup_dir/full_backup.ab"; then
log " ✓ Full ADB backup completed"
log " Note: Restore with: adb restore full_backup.ab"
else
warn " ⚠ ADB backup failed or was cancelled"
fi
fi
# 8. Backup device info
log "Saving device information..."
{
echo "Device Backup Information"
echo "========================="
echo "Date: $(date)"
echo
echo "Device Model: $(adb shell getprop ro.product.model | tr -d '\r\n')"
echo "Android Version: $(adb shell getprop ro.build.version.release | tr -d '\r\n')"
echo "Build Number: $(adb shell getprop ro.build.display.id | tr -d '\r\n')"
echo "Security Patch: $(adb shell getprop ro.build.version.security_patch | tr -d '\r\n')"
echo "Serial: $(adb shell getprop ro.serialno | tr -d '\r\n')"
echo
echo "Installed Apps:"
adb shell "pm list packages -3" | sed 's/package:/ - /'
} > "$backup_dir/device_info.txt"
log " ✓ Device info saved"
# Summary
local backup_size
backup_size=$(du -sh "$backup_dir" 2> /dev/null | cut -f1 || echo "unknown")
echo
echo -e "${GREEN}╔═══════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Backup Completed Successfully! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════╝${NC}"
echo
log "Backup location: $backup_dir"
log "Backup size: $backup_size"
echo
echo "What was backed up:"
echo " ✓ Photos (DCIM)"
echo " ✓ Pictures"
echo " ✓ Documents"
echo " ✓ Downloads"
echo " ✓ Music"
echo " ✓ Movies"
echo " ✓ WhatsApp data (if present)"
echo " ✓ Telegram data (if present)"
echo " ✓ Installed apps list"
echo " ✓ Device information"
if [[ -f "$backup_dir/full_backup.ab" ]]; then
echo " ✓ Full app data backup"
fi
if [[ -d "$backup_dir/apks" ]]; then
echo " ✓ APK files"
fi
echo
log "Keep this backup safe! You'll need it to restore your data after rooting."
return 0
}

View File

@ -0,0 +1,157 @@
#!/bin/bash
# Dependency installation and prerequisite checks.
#
# Sourced by root_bl9000.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
install_dependencies() {
print_header "Installing Dependencies"
local packages=()
local missing=()
# Check for required commands
if ! command -v adb > /dev/null 2>&1; then
packages+=("android-tools")
missing+=("adb")
fi
if ! command -v fastboot > /dev/null 2>&1 && ! pacman -Q android-tools > /dev/null 2>&1; then
packages+=("android-tools")
missing+=("fastboot")
fi
if ! command -v unzip > /dev/null 2>&1; then
packages+=("unzip")
missing+=("unzip")
fi
if ! command -v curl > /dev/null 2>&1; then
packages+=("curl")
missing+=("curl")
fi
if ! command -v python3 > /dev/null 2>&1; then
packages+=("python")
missing+=("python3")
fi
if ! command -v git > /dev/null 2>&1; then
packages+=("git")
missing+=("git")
fi
# Check for libusb and fuse2 (needed for mtkclient)
if ! pacman -Q libusb > /dev/null 2>&1; then
packages+=("libusb")
missing+=("libusb")
fi
if ! pacman -Q fuse2 > /dev/null 2>&1; then
packages+=("fuse2")
missing+=("fuse2")
fi
# Check for python-protobuf (needed for boot image tools)
if ! python3 -c "import google.protobuf" 2> /dev/null; then
packages+=("python-protobuf")
missing+=("python-protobuf")
fi
if [[ ${#missing[@]} -eq 0 ]]; then
log "All dependencies are already installed."
return 0
fi
log "Missing dependencies: ${missing[*]}"
# Remove duplicates
readarray -t packages < <(printf '%s\n' "${packages[@]}" | sort -u)
if ! confirm "Install missing packages: ${packages[*]}?"; then
die "Cannot proceed without required dependencies."
fi
log "Installing packages: ${packages[*]}"
sudo pacman -S --needed --noconfirm "${packages[@]}" || die "Failed to install dependencies"
log "Dependencies installed successfully."
}
setup_udev_rules() {
print_header "Setting Up USB Access"
local udev_file="/etc/udev/rules.d/51-android.rules"
local mtk_udev_dir="${WORK_DIR}/mtkclient/mtkclient/Setup/Linux"
# Install MTKClient udev rules if mtkclient is present
if [[ -d "${WORK_DIR}/mtkclient" ]]; then
log "Installing MTKClient udev rules..."
if [[ -d $mtk_udev_dir ]]; then
sudo cp "$mtk_udev_dir"/*.rules /etc/udev/rules.d/ 2> /dev/null || warn "Failed to copy MTKClient rules"
fi
fi
if [[ -f $udev_file ]]; then
log "Android udev rules already exist at $udev_file"
else
if ! confirm "Create udev rules for Android device access?"; then
warn "Skipping udev rules. You may need to run commands with sudo."
return 0
fi
log "Creating Android udev rules..."
# Create comprehensive udev rules for Android devices
sudo tee "$udev_file" > /dev/null << 'EOF'
# Android Debug Bridge (ADB) devices
# Add your device's vendor ID if not listed
# Google
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="adbusers"
# MediaTek (common in BL9000)
SUBSYSTEM=="usb", ATTR{idVendor}=="0e8d", MODE="0666", GROUP="adbusers"
# Generic catch-all for Android devices
SUBSYSTEM=="usb", ATTR{idVendor}=="*", ATTR{idProduct}=="*", MODE="0666", GROUP="adbusers", SYMLINK+="android%n"
EOF
fi
# Create adbusers group if it doesn't exist
if ! getent group adbusers > /dev/null; then
sudo groupadd -r adbusers
log "Created adbusers group"
fi
# Add current user to adbusers and plugdev groups
if ! groups "$USER" | grep -q '\badbusers\b'; then
sudo usermod -aG adbusers "$USER"
log "Added $USER to adbusers group"
fi
if ! getent group plugdev > /dev/null; then
sudo groupadd -r plugdev
fi
if ! groups "$USER" | grep -q '\bplugdev\b'; then
sudo usermod -aG plugdev "$USER"
log "Added $USER to plugdev group"
fi
if ! getent group dialout > /dev/null; then
sudo groupadd -r dialout
fi
if ! groups "$USER" | grep -q '\bdialout\b'; then
sudo usermod -aG dialout "$USER"
log "Added $USER to dialout group"
warn "You need to log out and back in for group membership to take effect."
warn "Alternatively, run: newgrp dialout"
fi
# Reload udev rules
sudo udevadm control --reload-rules
sudo udevadm trigger
log "USB access configured successfully."
}

View File

@ -0,0 +1,194 @@
#!/bin/bash
# Device detection and connection checks.
#
# Sourced by root_bl9000.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
check_device() {
print_header "Checking Device Connection"
log "Starting ADB server..."
adb start-server > /dev/null 2>&1 || true
log "Waiting for device..."
if ! adb wait-for-device; then
error "Failed to detect device via ADB."
echo
echo "Troubleshooting steps:"
echo "1. Make sure USB debugging is enabled on your phone"
echo " Settings → About Phone → Tap Build Number 7 times"
echo " Settings → Developer Options → Enable USB Debugging"
echo "2. Connect your phone via USB cable"
echo "3. Accept the 'Allow USB debugging' prompt on your phone"
echo "4. Run: adb devices"
echo
return 1
fi
local device_info
device_info=$(adb devices -l | grep -v "List of devices" | grep -v "^$" | head -n1)
if [[ -z $device_info ]]; then
error "No device detected"
return 1
fi
log "Device connected: $device_info"
# Check device properties
local model
model=$(adb shell getprop ro.product.model 2> /dev/null | tr -d '\r\n' || echo "Unknown")
log "Model: $model"
local android_version
android_version=$(adb shell getprop ro.build.version.release 2> /dev/null | tr -d '\r\n' || echo "Unknown")
log "Android version: $android_version"
local battery_level
battery_level=$(adb shell dumpsys battery | grep level | awk '{print $2}' | tr -d '\r\n' || echo "Unknown")
log "Battery level: ${battery_level}%"
if [[ $battery_level != "Unknown" && $battery_level -lt 50 ]]; then
warn "Battery level is below 50%. Charge your phone before proceeding."
if ! confirm "Continue anyway?"; then
return 1
fi
fi
# Check if bootloader is unlocked
local unlock_status
unlock_status=$(adb shell getprop ro.boot.verifiedbootstate 2> /dev/null | tr -d '\r\n' || echo "unknown")
if [[ $unlock_status == "orange" || $unlock_status == "red" ]]; then
log "Bootloader unlock status: ${GREEN}UNLOCKED${NC}"
else
warn "Bootloader appears to be LOCKED. You'll need to unlock it to root."
fi
# Check if OEM unlocking is enabled
local oem_unlock
oem_unlock=$(adb shell getprop sys.oem_unlock_allowed 2> /dev/null | tr -d '\r\n' || echo "unknown")
if [[ $oem_unlock == "1" ]]; then
log "OEM unlocking: ${GREEN}ENABLED${NC}"
else
warn "OEM unlocking is not enabled in Developer Options."
echo "Enable it at: Settings → Developer Options → OEM unlocking"
fi
return 0
}
unlock_bootloader() {
print_header "Unlocking Bootloader"
echo
echo -e "${RED}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}║ WARNING ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ Unlocking the bootloader will ERASE ALL DATA on your phone! ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ This includes: ║${NC}"
echo -e "${RED}║ - All apps and app data ║${NC}"
echo -e "${RED}║ - Photos, videos, and files ║${NC}"
echo -e "${RED}║ - System settings ║${NC}"
echo -e "${RED}║ - Everything else on internal storage ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ Make sure you have backed up important data! ║${NC}"
echo -e "${RED}╚═══════════════════════════════════════════════════════════════╝${NC}"
echo
if ! confirm "Have you backed up all important data and want to proceed?"; then
log "Bootloader unlock cancelled by user."
return 1
fi
if ! confirm "Are you ABSOLUTELY SURE? This cannot be undone!"; then
log "Bootloader unlock cancelled by user."
return 1
fi
log "Rebooting device to bootloader..."
adb reboot bootloader || die "Failed to reboot to bootloader"
log "Waiting for fastboot mode..."
sleep 5
if ! fastboot devices | grep -q .; then
error "Device not detected in fastboot mode."
echo
echo "If the device doesn't enter fastboot automatically:"
echo "1. Power off the phone completely"
echo "2. Hold Volume Down + Power buttons simultaneously"
echo "3. Release when you see the bootloader/fastboot screen"
echo "4. Run: fastboot devices"
echo
return 1
fi
log "Device in fastboot mode"
# Check current bootloader status
local bl_status
bl_status=$(fastboot getvar unlocked 2>&1 | grep "unlocked:" | awk '{print $2}' || echo "unknown")
if [[ $bl_status == "yes" ]]; then
log "Bootloader is already unlocked."
fastboot reboot
return 0
fi
log "Attempting to unlock bootloader..."
# Try different unlock commands (varies by device)
if fastboot flashing unlock 2>&1 | grep -qi "okay\|finished"; then
log "Bootloader unlock command sent successfully."
elif fastboot oem unlock 2>&1 | grep -qi "okay\|finished"; then
log "Bootloader unlock command sent successfully."
else
error "Bootloader unlock command may have failed."
echo
echo "On your phone:"
echo "1. Use volume buttons to select 'Unlock the bootloader'"
echo "2. Press power button to confirm"
echo
if ! confirm "Did you complete the unlock on the device?"; then
fastboot reboot
return 1
fi
fi
log "Rebooting device..."
fastboot reboot || true
log "Bootloader unlocked successfully!"
log "Device will now boot up and perform factory reset..."
log "Waiting for device to come back online..."
sleep 10
adb wait-for-device || true
log "Please complete the initial setup on your phone, then re-enable USB debugging."
echo
return 0
}
download_magisk() {
print_header "Downloading Magisk"
local magisk_apk="$WORK_DIR/magisk.apk"
if [[ -f $magisk_apk ]]; then
log "Magisk APK already downloaded at $magisk_apk"
return 0
fi
log "Downloading latest Magisk APK..."
if ! curl -L -o "$magisk_apk" "$MAGISK_APK_URL"; then
error "Failed to download Magisk APK"
return 1
fi
log "Magisk downloaded successfully: $magisk_apk"
return 0
}

View File

@ -0,0 +1,144 @@
#!/bin/bash
# Boot image patching with Magisk.
#
# Sourced by root_bl9000.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
patch_boot_with_magisk() {
print_header "Patching Boot Image with Magisk"
if [[ ! -f ${BOOT_IMG:-} ]]; then
die "Boot image not found: ${BOOT_IMG:-none}"
fi
local magisk_apk="$WORK_DIR/magisk.apk"
if [[ ! -f $magisk_apk ]]; then
die "Magisk APK not found. Run download step first."
fi
log "Checking if device is connected..."
if ! adb devices | grep -q "device$"; then
die "No device detected. Make sure USB debugging is enabled and device is connected."
fi
log "Installing Magisk APK on device..."
if ! adb install -r "$magisk_apk" 2> /dev/null; then
warn "Magisk APK installation failed (may already be installed)"
fi
log "Pushing boot image to device..."
adb push "$BOOT_IMG" /sdcard/Download/boot.img || die "Failed to push boot image"
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " MANUAL STEP REQUIRED"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
echo "On your phone:"
echo "1. Open the Magisk app"
echo "2. Tap 'Install' next to Magisk"
echo "3. Select 'Select and Patch a File'"
echo "4. Navigate to Downloads and select boot.img"
echo "5. Tap 'Let's Go' and wait for patching to complete"
echo "6. The patched file will be saved as magisk_patched_*.img"
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
if ! confirm "Have you completed patching the boot image in Magisk app?"; then
error "Patching cancelled by user"
return 1
fi
log "Pulling patched boot image from device..."
local patched_img
patched_img=$(adb shell "ls /sdcard/Download/magisk_patched_*.img 2>/dev/null" | tr -d '\r\n' | head -n1 || echo "")
if [[ -z $patched_img ]]; then
error "Could not find patched boot image on device."
echo "Please ensure the patching completed successfully in Magisk app."
return 1
fi
PATCHED_BOOT_IMG="$WORK_DIR/magisk_patched.img"
if ! adb pull "$patched_img" "$PATCHED_BOOT_IMG"; then
error "Failed to pull patched boot image"
return 1
fi
log "Patched boot image saved to: $PATCHED_BOOT_IMG"
return 0
}
flash_patched_boot() {
print_header "Flashing Patched Boot Image"
if [[ ! -f ${PATCHED_BOOT_IMG:-} ]]; then
die "Patched boot image not found: ${PATCHED_BOOT_IMG:-none}"
fi
echo
echo -e "${YELLOW}This will flash the patched boot image to your device.${NC}"
echo "Device uses A/B partitions - will flash to boot_a"
echo
if ! confirm "Proceed with flashing?"; then
log "Flashing cancelled by user"
return 1
fi
log "Rebooting to bootloader..."
adb reboot bootloader || die "Failed to reboot to bootloader"
log "Waiting for fastboot mode..."
sleep 5
if ! sudo fastboot devices | grep -q .; then
die "Device not detected in fastboot mode"
fi
log "Flashing patched boot image to boot_a..."
if ! sudo fastboot flash boot_a "$PATCHED_BOOT_IMG"; then
error "Failed to flash boot image"
return 1
fi
log "Flashed successfully!"
log "Rebooting device..."
sudo fastboot reboot
log "Waiting for device to boot..."
sleep 10
adb wait-for-device || true
echo
echo -e "${GREEN}╔═══════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Root Process Complete! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════╝${NC}"
echo
echo "Your BL9000 phone should now be rooted with Magisk!"
echo
echo "Next steps:"
echo "1. Open the Magisk app on your phone"
echo "2. Verify that it shows 'Installed' for both Magisk and App"
echo "3. Grant root access to apps as needed"
echo "4. Install Magisk modules if desired"
echo
echo "Note: Some banking and secure apps may not work with root."
echo " Use Magisk's DenyList feature to hide root from specific apps."
echo
return 0
}
clean_work_dir() {
if [[ -d $WORK_DIR ]]; then
log "Removing working directory: $WORK_DIR"
rm -rf "$WORK_DIR"
log "Cleaned successfully"
else
log "Work directory doesn't exist: $WORK_DIR"
fi
}

View File

@ -0,0 +1,187 @@
#!/bin/bash
# mtkclient checkout and setup.
#
# Sourced by root_bl9000.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
install_mtkclient() {
print_header "Installing MTKClient"
local mtk_dir="${WORK_DIR}/mtkclient"
if [[ -d $mtk_dir && -f "$mtk_dir/mtk.py" ]]; then
log "MTKClient already installed at $mtk_dir"
return 0
fi
if ! confirm "Install MTKClient for MediaTek boot image extraction?"; then
return 1
fi
log "Cloning MTKClient repository..."
if ! git clone https://github.com/bkerler/mtkclient "$mtk_dir"; then
error "Failed to clone MTKClient"
return 1
fi
log "Installing MTKClient Python dependencies..."
# Subshell so the cd cannot leak, and so a failure cannot strand the caller
# in $mtk_dir the way the old `cd -` did on an early return.
(
cd "$mtk_dir" || exit 1
python3 -m pip install --user -r requirements.txt || warn "Some dependencies may have failed to install"
python3 -m pip install --user . || warn "MTKClient installation may be incomplete"
) || warn "MTKClient installation may be incomplete"
log "MTKClient installed successfully"
return 0
}
extract_boot_with_mtkclient() {
print_header "Extracting Boot with MTKClient"
local mtk_dir="${WORK_DIR}/mtkclient"
local boot_img="$WORK_DIR/boot.img"
local boot_a_img="$WORK_DIR/boot_a.img"
local vbmeta_a_img="$WORK_DIR/vbmeta_a.img"
if [[ ! -d $mtk_dir ]]; then
error "MTKClient not installed. Run: $SCRIPT_NAME install-mtk"
return 1
fi
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " MTKClient Boot ROM Mode Instructions"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
echo "BEFORE continuing, you MUST:"
echo
echo " 1. Power off your phone COMPLETELY"
echo " 2. DISCONNECT the USB cable from your phone"
echo " 3. Have the USB cable ready in your hand"
echo
echo "When you press Enter:"
echo
echo " 4. Press and hold BOTH Volume buttons (Up + Down)"
echo " 5. While holding BOTH buttons, connect USB cable"
echo " 6. Keep holding until device is detected (may take 5-10 seconds)"
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
if ! confirm "Phone is OFF and USB DISCONNECTED - ready to proceed?"; then
return 1
fi
log "Starting MTKClient NOW - quickly enter BROM mode!"
log "Hold BOTH volume buttons and connect USB cable..."
echo
# Checked BEFORE the cd, so the early return needs no `cd -`. A subshell is
# not an option here: this function exports BOOT_IMG to its caller.
if [[ ! -d "$mtk_dir/venv" ]]; then
error "MTKClient virtual environment not found. Run: $SCRIPT_NAME install-mtk"
return 1
fi
local prev_dir="$PWD"
cd "$mtk_dir" || {
error "Cannot enter $mtk_dir"
return 1
}
# shellcheck source=/dev/null
source venv/bin/activate
# BL9000 uses A/B partitions, so extract boot_a and vbmeta_a
if python3 mtk.py r boot_a,vbmeta_a "$boot_a_img,$vbmeta_a_img"; then
log "Boot_a and vbmeta_a extracted successfully!"
# Copy boot_a.img to boot.img for Magisk compatibility
cp "$boot_a_img" "$boot_img"
BOOT_IMG="$boot_img"
log "Boot image ready for patching: $BOOT_IMG"
# Reset device
log "Resetting device..."
python3 mtk.py reset || warn "Failed to reset device, please reboot manually"
# Deactivate venv if function exists
type deactivate &> /dev/null && deactivate
cd "$prev_dir" || true
return 0
else
# Deactivate venv if function exists
type deactivate &> /dev/null && deactivate
error "Failed to extract boot image with MTKClient"
cd "$prev_dir" || true
return 1
fi
}
extract_boot_image() {
print_header "Extracting Boot Image"
local boot_img="$WORK_DIR/boot.img"
if [[ -n ${BOOT_IMG:-} && -f $BOOT_IMG ]]; then
log "Using provided boot image: $BOOT_IMG"
cp "$BOOT_IMG" "$boot_img"
BOOT_IMG="$boot_img"
return 0
fi
log "Attempting to extract boot image from device..."
# Method 1: Try MTKClient first (best for MediaTek devices)
if [[ -d "${WORK_DIR}/mtkclient" ]]; then
log "Trying MTKClient extraction..."
if extract_boot_with_mtkclient; then
return 0
fi
warn "MTKClient extraction failed, trying ADB methods..."
fi
# Method 2: Try to pull boot partition directly via ADB
local boot_partition
boot_partition=$(adb shell "find /dev/block -name boot | head -n1" 2> /dev/null | tr -d '\r\n' || echo "")
if [[ -n $boot_partition ]]; then
log "Found boot partition: $boot_partition"
if adb pull "$boot_partition" "$boot_img" 2> /dev/null; then
log "Boot image extracted successfully"
BOOT_IMG="$boot_img"
return 0
fi
fi
# Method 3: Try to get boot partition via by-name
boot_partition=$(adb shell "ls /dev/block/by-name/boot*" 2> /dev/null | head -n1 | tr -d '\r\n' || echo "")
if [[ -n $boot_partition ]]; then
log "Found boot partition: $boot_partition"
if adb shell "su -c 'dd if=$boot_partition of=/sdcard/boot.img'" 2> /dev/null &&
adb pull /sdcard/boot.img "$boot_img" 2> /dev/null; then
adb shell rm /sdcard/boot.img 2> /dev/null || true
log "Boot image extracted successfully"
BOOT_IMG="$boot_img"
return 0
fi
fi
error "Failed to extract boot image automatically."
echo
echo "Manual extraction options:"
echo "1. Use MTKClient: $SCRIPT_NAME extract-mtk"
echo "2. Extract boot.img from your device's firmware package"
echo "3. Get boot.img from device manufacturer's official ROM"
echo
echo "Then run: $SCRIPT_NAME root --boot-img /path/to/boot.img"
echo
return 1
}

View File

@ -0,0 +1,107 @@
#!/bin/bash
# Usage text and the argument parsing help.
#
# Sourced by root_bl9000.sh; split out to keep it under the 250-line cap.
# Sourced rather than run, so it inherits the caller's strict mode and
# the helper functions and variables defined above the source line.
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] [COMMAND]
Root BL9000 phone from Arch Linux using Magisk.
Commands:
install-deps Install required dependencies (adb, fastboot, tools)
install-mtk Install MTKClient for MediaTek boot extraction
check Check device connection and prerequisites
backup Backup phone data before unlocking bootloader
unlock Unlock bootloader (WARNING: wipes all data!)
extract-mtk Extract boot.img using MTKClient (for MediaTek devices)
patch Patch boot.img with Magisk (manual step on phone)
flash [IMG] Flash patched boot image (uses magisk_patched.img or specified file)
auto-root Automated: extract -> patch -> flash in one command
root Extract boot, patch with Magisk, and flash
full Run complete rooting process (deps + unlock + root)
clean Remove temporary working directory
help Show this message
Options:
-h, --help Show this message
--work-dir DIR Set working directory (default: $WORK_DIR)
--boot-img FILE Use existing boot.img instead of extracting from device
Examples:
$SCRIPT_NAME install-deps # Install required tools
$SCRIPT_NAME install-mtk # Install MTKClient for boot extraction
$SCRIPT_NAME check # Verify device connection
$SCRIPT_NAME backup # Backup phone data first!
$SCRIPT_NAME extract-mtk # Extract boot.img with MTKClient
$SCRIPT_NAME auto-root # Automated rooting (extract + patch + flash)
$SCRIPT_NAME full # Complete rooting process
$SCRIPT_NAME root # Root only (assumes bootloader unlocked)
WARNING: Unlocking the bootloader will ERASE ALL DATA on your phone!
Make sure to back up important data before proceeding.
EOF
}
run_full_process() {
print_header "BL9000 Full Root Process"
log "Starting complete rooting process..."
install_dependencies || die "Failed to install dependencies"
setup_udev_rules || true
echo
if ! confirm "Continue to device check?"; then
die "Process cancelled by user"
fi
check_device || die "Device check failed"
echo
if ! confirm "Continue to backup device data?"; then
die "Process cancelled by user"
fi
backup_device_data || warn "Backup failed or incomplete"
echo
if ! confirm "Continue to bootloader unlock?"; then
die "Process cancelled by user"
fi
unlock_bootloader || die "Bootloader unlock failed"
echo
log "Please complete device setup and re-enable USB debugging, then press Enter..."
read -r
check_device || die "Device check failed after unlock"
download_magisk || die "Failed to download Magisk"
extract_boot_image || die "Failed to extract boot image"
patch_boot_with_magisk || die "Failed to patch boot image"
flash_patched_boot || die "Failed to flash patched boot"
log "Full root process completed successfully!"
}
run_root_only() {
print_header "BL9000 Root Process (Skip Unlock)"
log "Starting root process (assuming bootloader is already unlocked)..."
check_device || die "Device check failed"
download_magisk || die "Failed to download Magisk"
extract_boot_image || die "Failed to extract boot image"
patch_boot_with_magisk || die "Failed to patch boot image"
flash_patched_boot || die "Failed to flash patched boot"
log "Root process completed successfully!"
}

View File

@ -0,0 +1,161 @@
#!/bin/bash
# Cross-language lookup and content extraction.
#
# Sourced by lookup_docs.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
#==============================================================================
# Generic lookup (searches all languages)
#==============================================================================
lookup_all() {
local term="$1"
# Try each language
for lang in python cpp js rust go shell; do
local result
result=$(lookup_$lang "$term" 2>/dev/null)
if [ -n "$result" ]; then
echo "$lang: $result"
fi
done
}
#==============================================================================
# Parse Python import and lookup the actual imported item
#==============================================================================
parse_python_import() {
local import_line="$1"
# Handle "from X import Y" format
if [[ $import_line =~ ^from[[:space:]]+([^[:space:]]+)[[:space:]]+import[[:space:]]+(.+) ]]; then
local module="${BASH_REMATCH[1]}"
local items="${BASH_REMATCH[2]}"
# Clean up items (remove parentheses, commas, etc.)
items=$(echo "$items" | sed 's/[(),]//g' | awk '{print $1}')
# Output: module and first imported item
echo "$module|$items"
return 0
fi
# Handle "import X" format
if [[ $import_line =~ ^import[[:space:]]+([^[:space:],]+) ]]; then
local module="${BASH_REMATCH[1]}"
echo "$module|"
return 0
fi
return 1
}
#==============================================================================
# Smart lookup for imports
#==============================================================================
lookup_import() {
local import_line="$1"
local lang="$2"
case "$lang" in
python)
local parsed
parsed=$(parse_python_import "$import_line")
if [ -n "$parsed" ]; then
local module item
module=$(echo "$parsed" | cut -d'|' -f1)
item=$(echo "$parsed" | cut -d'|' -f2)
# For "from X import Y", look up Y within module X's documentation
if [ -n "$item" ] && [ -n "$module" ]; then
local result
# Pass both item and module to lookup_python
result=$(lookup_python "$item" "$module")
if [ -n "$result" ]; then
echo "$result"
return 0
fi
fi
# Fall back to module documentation
lookup_python "$module"
fi
;;
c_cpp)
# Extract header name from #include <header> or #include "header"
local header
header=$(echo "$import_line" | sed -E 's/#include\s*[<"]([^">]+)[">]/\1/' | sed 's/\.h$//')
lookup_cpp "$header"
;;
javascript | typescript)
# Extract module from import/require
local module=""
# Match: from "module" or from 'module'
module=$(echo "$import_line" | grep -oP "from\s+['\"]\\K[^'\"]+")
if [ -z "$module" ]; then
# Match: require("module") or require('module')
module=$(echo "$import_line" | grep -oP "require\\(['\"]\\K[^'\"]+")
fi
[ -n "$module" ] && lookup_js "$module"
;;
*)
echo "Unknown language: $lang"
;;
esac
}
#==============================================================================
# Extract documentation content
#==============================================================================
extract_doc_content() {
local file="$1"
local term="$2"
local max_lines="${3:-20}"
if [[ $file == *.html ]]; then
# Extract text from HTML, find section about term
if command -v html2text &>/dev/null; then
html2text "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines"
elif command -v lynx &>/dev/null; then
lynx -dump -nolist "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines"
else
# Basic extraction
sed 's/<[^>]*>//g' "$file" | grep -A"$max_lines" -i "$term" | head -"$max_lines"
fi
elif [[ $file == *.json ]]; then
# Pretty print JSON section
grep -A5 "\"$term\"" "$file" 2>/dev/null
else
# Plain text
grep -A"$max_lines" -i "$term" "$file" | head -"$max_lines"
fi
}
#==============================================================================
# Main
#==============================================================================
usage() {
cat <<EOF
Usage: $0 <term> [language] [options]
Search offline documentation for a term.
Languages: python, cpp, c_cpp, js, javascript, rust, go, shell, all
Options:
--open Open the documentation file (requires xdg-open)
--extract Extract and display relevant content
--import Parse and lookup an import statement
--batch Process multiple terms from a file
Examples:
$0 Path python # Find Path in Python docs
$0 vector cpp # Find vector in C++ docs
$0 map # Find map in all languages
$0 --import "from pathlib import Path" python
$0 --batch imports.txt python
EOF
}

View File

@ -0,0 +1,93 @@
#!/bin/bash
# C++ documentation lookup.
#
# Sourced by lookup_docs.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
#==============================================================================
# C/C++ specific lookup
#==============================================================================
lookup_cpp() {
local term="$1"
local doc_dir="$DOCS_DIR/c_cpp"
local result=""
local desc=""
# Resolve symlink if present (system package installs to c_cpp/system/)
[ -L "$doc_dir/system" ] && doc_dir="$doc_dir/system"
# Common C headers
case "$term" in
stdio.h | stdio)
[ -f "$doc_dir/reference/cstdio/index.html" ] && result="$doc_dir/reference/cstdio/index.html"
[ -f "$doc_dir/en/c/io.html" ] && result="$doc_dir/en/c/io.html"
desc="C standard I/O header"
;;
stdlib.h | stdlib)
[ -f "$doc_dir/reference/cstdlib/index.html" ] && result="$doc_dir/reference/cstdlib/index.html"
[ -f "$doc_dir/en/c/memory.html" ] && result="$doc_dir/en/c/memory.html"
desc="C standard library header"
;;
string.h | cstring)
[ -f "$doc_dir/reference/cstring/index.html" ] && result="$doc_dir/reference/cstring/index.html"
desc="C string handling header"
;;
math.h | cmath)
[ -f "$doc_dir/reference/cmath/index.html" ] && result="$doc_dir/reference/cmath/index.html"
desc="C math header"
;;
esac
# C++ STL containers
case "$term" in
vector)
[ -f "$doc_dir/reference/vector/index.html" ] && result="$doc_dir/reference/vector/index.html"
[ -f "$doc_dir/en/cpp/container/vector.html" ] && result="$doc_dir/en/cpp/container/vector.html"
desc="C++ std::vector container"
;;
map)
[ -f "$doc_dir/reference/map/index.html" ] && result="$doc_dir/reference/map/index.html"
desc="C++ std::map container"
;;
string)
[ -f "$doc_dir/reference/string/index.html" ] && result="$doc_dir/reference/string/index.html"
desc="C++ std::string"
;;
iostream)
[ -f "$doc_dir/reference/iostream/index.html" ] && result="$doc_dir/reference/iostream/index.html"
desc="C++ iostream header"
;;
esac
# C keywords
case "$term" in
if | else | for | while | do | switch | case | break | continue | return | goto)
[ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html"
[ -f "$doc_dir/en/cpp/language/$term.html" ] && result="$doc_dir/en/cpp/language/$term.html"
desc="C/C++ keyword: $term"
;;
int | char | float | double | void | long | short | unsigned | signed)
[ -f "$doc_dir/en/c/language/type.html" ] && result="$doc_dir/en/c/language/type.html"
desc="C/C++ type: $term"
;;
struct | union | enum | typedef)
[ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html"
desc="C/C++ keyword: $term"
;;
esac
# Search in files if not found (use -L to follow symlinks)
if [ -z "$result" ]; then
local found
found=$(find -L "$doc_dir" -name "*${term}*" -type f 2>/dev/null | head -1)
if [ -n "$found" ]; then
result="$found"
desc="C/C++: $term"
fi
fi
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}

View File

@ -0,0 +1,166 @@
#!/bin/bash
# JavaScript documentation lookup.
#
# Sourced by lookup_docs.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
#==============================================================================
# JavaScript/MDN specific lookup
# Searches the cloned MDN content repository
#==============================================================================
lookup_js() {
local term="$1"
local mdn_dir="$DOCS_DIR/mdn-content/files/en-us"
# Normalize term for searching
local term_lower
term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]')
# Handle common statement aliases (MDN uses if...else, try...catch, etc.)
local statement_aliases=(
"if:if...else"
"else:if...else"
"try:try...catch"
"catch:try...catch"
"finally:try...catch"
"do:do...while"
"while:while"
"for:for"
"switch:switch"
"case:switch"
"default:switch"
)
for alias in "${statement_aliases[@]}"; do
local key="${alias%%:*}"
local value="${alias##*:}"
if [ "$term_lower" = "$key" ]; then
local stmt_dir="$mdn_dir/web/javascript/reference/statements/$value"
if [ -d "$stmt_dir" ] && [ -f "$stmt_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$stmt_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$stmt_dir/index.md|${title:-$term}"
return 0
fi
fi
done
# Handle boolean/null literals
case "$term_lower" in
true | false)
local bool_dir="$mdn_dir/web/javascript/reference/global_objects/boolean"
if [ -d "$bool_dir" ] && [ -f "$bool_dir/index.md" ]; then
echo "$bool_dir/index.md|Boolean ($term)"
return 0
fi
;;
null)
local null_dir="$mdn_dir/web/javascript/reference/operators/null"
if [ -d "$null_dir" ] && [ -f "$null_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$null_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$null_dir/index.md|${title:-null}"
return 0
fi
;;
undefined)
local undef_dir="$mdn_dir/web/javascript/reference/global_objects/undefined"
if [ -d "$undef_dir" ] && [ -f "$undef_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$undef_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$undef_dir/index.md|${title:-undefined}"
return 0
fi
;;
esac
# Search JavaScript reference directory structure (priority order)
local search_dirs=(
"$mdn_dir/web/javascript/reference/statements"
"$mdn_dir/web/javascript/reference/operators"
"$mdn_dir/web/javascript/reference/global_objects"
"$mdn_dir/web/javascript/reference/functions"
"$mdn_dir/web/javascript/reference/classes"
)
for search_dir in "${search_dirs[@]}"; do
if [ -d "$search_dir" ]; then
# Look for exact directory match (MDN uses directories with index.md)
local found_dir
found_dir=$(find "$search_dir" -maxdepth 2 -type d -iname "$term" 2>/dev/null | head -1)
if [ -n "$found_dir" ] && [ -f "$found_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found_dir/index.md|${title:-$term}"
return 0
fi
fi
done
# Search Web APIs - prioritize *_api directories for common terms
if [ -d "$mdn_dir/web/api" ]; then
# First try <term>_api directory (e.g., fetch_api, console_api)
local api_dir="$mdn_dir/web/api/${term_lower}_api"
if [ -d "$api_dir" ] && [ -f "$api_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$api_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$api_dir/index.md|${title:-$term API}"
return 0
fi
# Then try exact top-level API interface (e.g., Console, Document, Element)
local found
found=$(find "$mdn_dir/web/api" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found/index.md|${title:-$term}"
return 0
fi
# Try window/<term> for global functions like alert, confirm, etc.
local window_method="$mdn_dir/web/api/window/${term_lower}"
if [ -d "$window_method" ] && [ -f "$window_method/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$window_method/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$window_method/index.md|${title:-Window.$term()}"
return 0
fi
# Search nested API methods
found=$(find "$mdn_dir/web/api" -maxdepth 3 -type d -iname "$term" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found/index.md|${title:-$term}"
return 0
fi
fi
# Now try partial matches in Global Objects (e.g., Array.from, Object.keys)
if [ -d "$mdn_dir/web/javascript/reference/global_objects" ]; then
local found
found=$(find "$mdn_dir/web/javascript/reference/global_objects" -maxdepth 2 -type d -iname "*${term}*" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found/index.md|${title:-$term}"
return 0
fi
fi
# Glossary as last resort
if [ -d "$mdn_dir/glossary" ]; then
local found
found=$(find "$mdn_dir/glossary" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found/index.md|${title:-$term}"
return 0
fi
fi
return 1
}

View File

@ -0,0 +1,150 @@
#!/bin/bash
# Python documentation lookup.
#
# Sourced by lookup_docs.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
#==============================================================================
# Python-specific lookup
#==============================================================================
lookup_python() {
local term="$1"
local in_module="$2" # Optional: look for term within this module
local doc_dir="$DOCS_DIR/python"
local result=""
local desc=""
# Normalize term (preserve case for True/False/None)
local term_lower
term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]')
# If looking for a term within a specific module
if [ -n "$in_module" ]; then
local module_lower
module_lower=$(echo "$in_module" | tr '[:upper:]' '[:lower:]')
if [ -f "$doc_dir/library/${module_lower}.html" ]; then
# Find anchor for the specific item in the module
local anchor
anchor=$(grep -oP "id=\"[^\"]*${term}[^\"]*\"" "$doc_dir/library/${module_lower}.html" 2>/dev/null | head -1 | sed 's/id="//;s/"//')
if [ -n "$anchor" ]; then
result="$doc_dir/library/${module_lower}.html#$anchor"
desc="Python: $in_module.$term"
else
# Just link to the module
result="$doc_dir/library/${module_lower}.html"
desc="Python: $term in module $in_module"
fi
echo "$result|$desc"
return 0
fi
fi
local _kw
_kw="$(python_keyword_doc "$doc_dir" "$term" "$term_lower")"
IFS=$'\t' read -r result desc <<<"$_kw"
# Verify file exists for keyword lookups
if [ -n "$result" ] && [ ! -f "${result%%#*}" ]; then
result=""
desc=""
fi
#--------------------------------------------------------------------------
# PRIORITY 2: Check if it's a module (pathlib, os, sys, etc.)
#--------------------------------------------------------------------------
if [ -z "$result" ] && [ -f "$doc_dir/library/${term_lower}.html" ]; then
result="$doc_dir/library/${term_lower}.html"
desc="Python module: $term"
fi
#--------------------------------------------------------------------------
# PRIORITY 3: Built-in functions (library/functions.html)
#--------------------------------------------------------------------------
if [ -z "$result" ] && [ -f "$doc_dir/library/functions.html" ]; then
if grep -q "id=\"$term_lower\"" "$doc_dir/library/functions.html" 2>/dev/null; then
result="$doc_dir/library/functions.html#$term_lower"
desc="Python built-in function: $term"
fi
fi
#--------------------------------------------------------------------------
# PRIORITY 4: Built-in types (library/stdtypes.html)
#--------------------------------------------------------------------------
if [ -z "$result" ]; then
case "$term_lower" in
str | string)
result="$doc_dir/library/stdtypes.html#str"
desc="Python: str type"
;;
int | integer)
result="$doc_dir/library/stdtypes.html#int"
desc="Python: int type"
;;
float)
result="$doc_dir/library/stdtypes.html#float"
desc="Python: float type"
;;
list)
result="$doc_dir/library/stdtypes.html#list"
desc="Python: list type"
;;
dict | dictionary)
result="$doc_dir/library/stdtypes.html#dict"
desc="Python: dict type"
;;
set)
result="$doc_dir/library/stdtypes.html#set"
desc="Python: set type"
;;
tuple)
result="$doc_dir/library/stdtypes.html#tuple"
desc="Python: tuple type"
;;
bool | boolean)
result="$doc_dir/library/stdtypes.html#boolean-values"
desc="Python: bool type"
;;
bytes)
result="$doc_dir/library/stdtypes.html#bytes"
desc="Python: bytes type"
;;
esac
fi
#--------------------------------------------------------------------------
# PRIORITY 5: Check for class/function in module docs (exact id match)
#--------------------------------------------------------------------------
if [ -z "$result" ]; then
local found_in
# Look for exact id match first
found_in=$(grep -l "id=\"$term\"" "$doc_dir/library/"*.html 2>/dev/null | head -1)
if [ -n "$found_in" ]; then
result="$found_in#$term"
local module
module=$(basename "$found_in" .html)
desc="Python: $term in module $module"
fi
fi
#--------------------------------------------------------------------------
# PRIORITY 6: Search in index
#--------------------------------------------------------------------------
if [ -z "$result" ] && [ -f "$INDEX_DIR/python_index.txt" ]; then
local index_match
index_match=$(grep -i "^$term " "$INDEX_DIR/python_index.txt" 2>/dev/null | head -1)
if [ -n "$index_match" ]; then
result=$(echo "$index_match" | cut -d' ' -f2-)
desc="Python: $term (from index)"
fi
fi
# NO full-text search fallback - it produces garbage results
# If we can't find a specific doc, return nothing (will fall back to online)
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}

View File

@ -0,0 +1,167 @@
#!/bin/bash
# Mapping Python keywords and operators to exact documentation anchors.
#
# Sourced by lookup_docs.sh; split out to keep docs_python.sh under the
# 250-line cap. Emits "<result>\t<desc>" so the caller keeps its own
# control flow; it reads doc_dir/term/term_lower from the caller's scope.
python_keyword_doc() {
local doc_dir="$1" term="$2" term_lower="$3"
local result="" desc=""
#--------------------------------------------------------------------------
# PRIORITY 1: Python keywords - map to exact documentation locations
#--------------------------------------------------------------------------
# Compound statements (reference/compound_stmts.html)
case "$term_lower" in
if | elif | else)
result="$doc_dir/reference/compound_stmts.html#if"
desc="Python: if statement"
;;
for)
result="$doc_dir/reference/compound_stmts.html#for"
desc="Python: for statement"
;;
while)
result="$doc_dir/reference/compound_stmts.html#while"
desc="Python: while statement"
;;
def)
result="$doc_dir/reference/compound_stmts.html#def"
desc="Python: function definition"
;;
class)
result="$doc_dir/reference/compound_stmts.html#class"
desc="Python: class definition"
;;
try | except | finally)
result="$doc_dir/reference/compound_stmts.html#try"
desc="Python: try statement"
;;
with)
result="$doc_dir/reference/compound_stmts.html#with"
desc="Python: with statement"
;;
async)
result="$doc_dir/reference/compound_stmts.html#async"
desc="Python: async definition"
;;
match | case)
result="$doc_dir/reference/compound_stmts.html#match"
desc="Python: match statement"
;;
esac
# Simple statements (reference/simple_stmts.html)
if [ -z "$result" ]; then
case "$term_lower" in
return)
result="$doc_dir/reference/simple_stmts.html#return"
desc="Python: return statement"
;;
pass)
result="$doc_dir/reference/simple_stmts.html#pass"
desc="Python: pass statement"
;;
break)
result="$doc_dir/reference/simple_stmts.html#break"
desc="Python: break statement"
;;
continue)
result="$doc_dir/reference/simple_stmts.html#continue"
desc="Python: continue statement"
;;
import | from)
result="$doc_dir/reference/simple_stmts.html#import"
desc="Python: import statement"
;;
raise)
result="$doc_dir/reference/simple_stmts.html#raise"
desc="Python: raise statement"
;;
assert)
result="$doc_dir/reference/simple_stmts.html#assert"
desc="Python: assert statement"
;;
yield)
result="$doc_dir/reference/simple_stmts.html#yield"
desc="Python: yield expression"
;;
del)
result="$doc_dir/reference/simple_stmts.html#del"
desc="Python: del statement"
;;
global)
result="$doc_dir/reference/simple_stmts.html#global"
desc="Python: global statement"
;;
nonlocal)
result="$doc_dir/reference/simple_stmts.html#nonlocal"
desc="Python: nonlocal statement"
;;
type)
result="$doc_dir/reference/simple_stmts.html#type"
desc="Python: type alias statement"
;;
esac
fi
# Expressions/operators (reference/expressions.html)
if [ -z "$result" ]; then
case "$term_lower" in
and)
result="$doc_dir/reference/expressions.html#and"
desc="Python: and operator"
;;
or)
result="$doc_dir/reference/expressions.html#or"
desc="Python: or operator"
;;
not)
result="$doc_dir/reference/expressions.html#not"
desc="Python: not operator"
;;
in)
result="$doc_dir/reference/expressions.html#in"
desc="Python: in operator"
;;
is)
result="$doc_dir/reference/expressions.html#is"
desc="Python: is operator"
;;
lambda)
result="$doc_dir/reference/expressions.html#lambda"
desc="Python: lambda expression"
;;
await)
result="$doc_dir/reference/expressions.html#await"
desc="Python: await expression"
;;
esac
fi
# Built-in constants (library/constants.html) - case-sensitive!
if [ -z "$result" ]; then
case "$term" in
True | False)
result="$doc_dir/library/constants.html#$term"
desc="Python: $term constant"
;;
None)
result="$doc_dir/library/constants.html#None"
desc="Python: None constant"
;;
Ellipsis)
result="$doc_dir/library/constants.html#Ellipsis"
desc="Python: Ellipsis constant"
;;
NotImplemented)
result="$doc_dir/library/constants.html#NotImplemented"
desc="Python: NotImplemented constant"
;;
esac
fi
printf '%s\t%s\n' "$result" "$desc"
}

View File

@ -0,0 +1,98 @@
#!/bin/bash
# Rust documentation lookup.
#
# Sourced by lookup_docs.sh; split out to keep it under the 250-line
# cap. Sourced rather than run, so it inherits the caller's strict mode
# and the variables defined above the source line.
#==============================================================================
# Rust specific lookup
#==============================================================================
lookup_rust() {
local term="$1"
local result=""
local desc=""
if command -v rustup &>/dev/null; then
# Use rustup doc to get path
local rust_doc_path
rust_doc_path=$(rustup doc --path 2>/dev/null | head -1 | xargs dirname 2>/dev/null)
# Search in std docs
if [ -d "$rust_doc_path/std" ]; then
local found
found=$(find "$rust_doc_path/std" -name "*${term}*" -type f 2>/dev/null | head -1)
if [ -n "$found" ]; then
result="$found"
desc="Rust: $term"
fi
fi
fi
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}
#==============================================================================
# Go specific lookup
#==============================================================================
lookup_go() {
local term="$1"
local result=""
local desc=""
if command -v go &>/dev/null; then
# Check if it's a stdlib package
if go doc "$term" &>/dev/null; then
result="go doc $term"
desc="Go package: $term (use 'go doc $term' to view)"
fi
fi
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}
#==============================================================================
# Shell specific lookup
#==============================================================================
lookup_shell() {
local term="$1"
local doc_dir="$DOCS_DIR/shell"
local result=""
local desc=""
# Check bash builtins
if [ -f "$doc_dir/bash_builtins.txt" ]; then
if grep -q "=== $term ===" "$doc_dir/bash_builtins.txt" 2>/dev/null; then
result="$doc_dir/bash_builtins.txt"
desc="Bash builtin: $term"
fi
fi
# Check common commands
if [ -z "$result" ] && [ -f "$doc_dir/common_commands.txt" ]; then
if grep -q "^$term" "$doc_dir/common_commands.txt" 2>/dev/null; then
local cmd_desc
cmd_desc=$(grep "^$term" "$doc_dir/common_commands.txt" | head -1)
result="$doc_dir/common_commands.txt"
desc="Shell command: $cmd_desc"
fi
fi
# Try man page
if [ -z "$result" ]; then
local man_path
man_path=$(man -w "$term" 2>/dev/null)
if [ -n "$man_path" ]; then
result="man $term"
desc="Manual page: $term (use 'man $term' to view)"
fi
fi
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}

View File

@ -35,793 +35,22 @@ else
NC=''
fi
#==============================================================================
# Python-specific lookup
#==============================================================================
lookup_python() {
local term="$1"
local in_module="$2" # Optional: look for term within this module
local doc_dir="$DOCS_DIR/python"
local result=""
local desc=""
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
# shellcheck source=lib/docs_python_keywords.sh
source "$SCRIPT_DIR/lib/docs_python_keywords.sh"
# shellcheck source=lib/docs_python.sh
source "$SCRIPT_DIR/lib/docs_python.sh"
# shellcheck source=lib/docs_cpp.sh
source "$SCRIPT_DIR/lib/docs_cpp.sh"
# shellcheck source=lib/docs_js.sh
source "$SCRIPT_DIR/lib/docs_js.sh"
# shellcheck source=lib/docs_rust.sh
source "$SCRIPT_DIR/lib/docs_rust.sh"
# shellcheck source=lib/docs_all.sh
source "$SCRIPT_DIR/lib/docs_all.sh"
# Normalize term (preserve case for True/False/None)
local term_lower
term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]')
# If looking for a term within a specific module
if [ -n "$in_module" ]; then
local module_lower
module_lower=$(echo "$in_module" | tr '[:upper:]' '[:lower:]')
if [ -f "$doc_dir/library/${module_lower}.html" ]; then
# Find anchor for the specific item in the module
local anchor
anchor=$(grep -oP "id=\"[^\"]*${term}[^\"]*\"" "$doc_dir/library/${module_lower}.html" 2>/dev/null | head -1 | sed 's/id="//;s/"//')
if [ -n "$anchor" ]; then
result="$doc_dir/library/${module_lower}.html#$anchor"
desc="Python: $in_module.$term"
else
# Just link to the module
result="$doc_dir/library/${module_lower}.html"
desc="Python: $term in module $in_module"
fi
echo "$result|$desc"
return 0
fi
fi
#--------------------------------------------------------------------------
# PRIORITY 1: Python keywords - map to exact documentation locations
#--------------------------------------------------------------------------
# Compound statements (reference/compound_stmts.html)
case "$term_lower" in
if | elif | else)
result="$doc_dir/reference/compound_stmts.html#if"
desc="Python: if statement"
;;
for)
result="$doc_dir/reference/compound_stmts.html#for"
desc="Python: for statement"
;;
while)
result="$doc_dir/reference/compound_stmts.html#while"
desc="Python: while statement"
;;
def)
result="$doc_dir/reference/compound_stmts.html#def"
desc="Python: function definition"
;;
class)
result="$doc_dir/reference/compound_stmts.html#class"
desc="Python: class definition"
;;
try | except | finally)
result="$doc_dir/reference/compound_stmts.html#try"
desc="Python: try statement"
;;
with)
result="$doc_dir/reference/compound_stmts.html#with"
desc="Python: with statement"
;;
async)
result="$doc_dir/reference/compound_stmts.html#async"
desc="Python: async definition"
;;
match | case)
result="$doc_dir/reference/compound_stmts.html#match"
desc="Python: match statement"
;;
esac
# Simple statements (reference/simple_stmts.html)
if [ -z "$result" ]; then
case "$term_lower" in
return)
result="$doc_dir/reference/simple_stmts.html#return"
desc="Python: return statement"
;;
pass)
result="$doc_dir/reference/simple_stmts.html#pass"
desc="Python: pass statement"
;;
break)
result="$doc_dir/reference/simple_stmts.html#break"
desc="Python: break statement"
;;
continue)
result="$doc_dir/reference/simple_stmts.html#continue"
desc="Python: continue statement"
;;
import | from)
result="$doc_dir/reference/simple_stmts.html#import"
desc="Python: import statement"
;;
raise)
result="$doc_dir/reference/simple_stmts.html#raise"
desc="Python: raise statement"
;;
assert)
result="$doc_dir/reference/simple_stmts.html#assert"
desc="Python: assert statement"
;;
yield)
result="$doc_dir/reference/simple_stmts.html#yield"
desc="Python: yield expression"
;;
del)
result="$doc_dir/reference/simple_stmts.html#del"
desc="Python: del statement"
;;
global)
result="$doc_dir/reference/simple_stmts.html#global"
desc="Python: global statement"
;;
nonlocal)
result="$doc_dir/reference/simple_stmts.html#nonlocal"
desc="Python: nonlocal statement"
;;
type)
result="$doc_dir/reference/simple_stmts.html#type"
desc="Python: type alias statement"
;;
esac
fi
# Expressions/operators (reference/expressions.html)
if [ -z "$result" ]; then
case "$term_lower" in
and)
result="$doc_dir/reference/expressions.html#and"
desc="Python: and operator"
;;
or)
result="$doc_dir/reference/expressions.html#or"
desc="Python: or operator"
;;
not)
result="$doc_dir/reference/expressions.html#not"
desc="Python: not operator"
;;
in)
result="$doc_dir/reference/expressions.html#in"
desc="Python: in operator"
;;
is)
result="$doc_dir/reference/expressions.html#is"
desc="Python: is operator"
;;
lambda)
result="$doc_dir/reference/expressions.html#lambda"
desc="Python: lambda expression"
;;
await)
result="$doc_dir/reference/expressions.html#await"
desc="Python: await expression"
;;
esac
fi
# Built-in constants (library/constants.html) - case-sensitive!
if [ -z "$result" ]; then
case "$term" in
True | False)
result="$doc_dir/library/constants.html#$term"
desc="Python: $term constant"
;;
None)
result="$doc_dir/library/constants.html#None"
desc="Python: None constant"
;;
Ellipsis)
result="$doc_dir/library/constants.html#Ellipsis"
desc="Python: Ellipsis constant"
;;
NotImplemented)
result="$doc_dir/library/constants.html#NotImplemented"
desc="Python: NotImplemented constant"
;;
esac
fi
# Verify file exists for keyword lookups
if [ -n "$result" ] && [ ! -f "${result%%#*}" ]; then
result=""
desc=""
fi
#--------------------------------------------------------------------------
# PRIORITY 2: Check if it's a module (pathlib, os, sys, etc.)
#--------------------------------------------------------------------------
if [ -z "$result" ] && [ -f "$doc_dir/library/${term_lower}.html" ]; then
result="$doc_dir/library/${term_lower}.html"
desc="Python module: $term"
fi
#--------------------------------------------------------------------------
# PRIORITY 3: Built-in functions (library/functions.html)
#--------------------------------------------------------------------------
if [ -z "$result" ] && [ -f "$doc_dir/library/functions.html" ]; then
if grep -q "id=\"$term_lower\"" "$doc_dir/library/functions.html" 2>/dev/null; then
result="$doc_dir/library/functions.html#$term_lower"
desc="Python built-in function: $term"
fi
fi
#--------------------------------------------------------------------------
# PRIORITY 4: Built-in types (library/stdtypes.html)
#--------------------------------------------------------------------------
if [ -z "$result" ]; then
case "$term_lower" in
str | string)
result="$doc_dir/library/stdtypes.html#str"
desc="Python: str type"
;;
int | integer)
result="$doc_dir/library/stdtypes.html#int"
desc="Python: int type"
;;
float)
result="$doc_dir/library/stdtypes.html#float"
desc="Python: float type"
;;
list)
result="$doc_dir/library/stdtypes.html#list"
desc="Python: list type"
;;
dict | dictionary)
result="$doc_dir/library/stdtypes.html#dict"
desc="Python: dict type"
;;
set)
result="$doc_dir/library/stdtypes.html#set"
desc="Python: set type"
;;
tuple)
result="$doc_dir/library/stdtypes.html#tuple"
desc="Python: tuple type"
;;
bool | boolean)
result="$doc_dir/library/stdtypes.html#boolean-values"
desc="Python: bool type"
;;
bytes)
result="$doc_dir/library/stdtypes.html#bytes"
desc="Python: bytes type"
;;
esac
fi
#--------------------------------------------------------------------------
# PRIORITY 5: Check for class/function in module docs (exact id match)
#--------------------------------------------------------------------------
if [ -z "$result" ]; then
local found_in
# Look for exact id match first
found_in=$(grep -l "id=\"$term\"" "$doc_dir/library/"*.html 2>/dev/null | head -1)
if [ -n "$found_in" ]; then
result="$found_in#$term"
local module
module=$(basename "$found_in" .html)
desc="Python: $term in module $module"
fi
fi
#--------------------------------------------------------------------------
# PRIORITY 6: Search in index
#--------------------------------------------------------------------------
if [ -z "$result" ] && [ -f "$INDEX_DIR/python_index.txt" ]; then
local index_match
index_match=$(grep -i "^$term " "$INDEX_DIR/python_index.txt" 2>/dev/null | head -1)
if [ -n "$index_match" ]; then
result=$(echo "$index_match" | cut -d' ' -f2-)
desc="Python: $term (from index)"
fi
fi
# NO full-text search fallback - it produces garbage results
# If we can't find a specific doc, return nothing (will fall back to online)
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}
#==============================================================================
# C/C++ specific lookup
#==============================================================================
lookup_cpp() {
local term="$1"
local doc_dir="$DOCS_DIR/c_cpp"
local result=""
local desc=""
# Resolve symlink if present (system package installs to c_cpp/system/)
[ -L "$doc_dir/system" ] && doc_dir="$doc_dir/system"
# Common C headers
case "$term" in
stdio.h | stdio)
[ -f "$doc_dir/reference/cstdio/index.html" ] && result="$doc_dir/reference/cstdio/index.html"
[ -f "$doc_dir/en/c/io.html" ] && result="$doc_dir/en/c/io.html"
desc="C standard I/O header"
;;
stdlib.h | stdlib)
[ -f "$doc_dir/reference/cstdlib/index.html" ] && result="$doc_dir/reference/cstdlib/index.html"
[ -f "$doc_dir/en/c/memory.html" ] && result="$doc_dir/en/c/memory.html"
desc="C standard library header"
;;
string.h | cstring)
[ -f "$doc_dir/reference/cstring/index.html" ] && result="$doc_dir/reference/cstring/index.html"
desc="C string handling header"
;;
math.h | cmath)
[ -f "$doc_dir/reference/cmath/index.html" ] && result="$doc_dir/reference/cmath/index.html"
desc="C math header"
;;
esac
# C++ STL containers
case "$term" in
vector)
[ -f "$doc_dir/reference/vector/index.html" ] && result="$doc_dir/reference/vector/index.html"
[ -f "$doc_dir/en/cpp/container/vector.html" ] && result="$doc_dir/en/cpp/container/vector.html"
desc="C++ std::vector container"
;;
map)
[ -f "$doc_dir/reference/map/index.html" ] && result="$doc_dir/reference/map/index.html"
desc="C++ std::map container"
;;
string)
[ -f "$doc_dir/reference/string/index.html" ] && result="$doc_dir/reference/string/index.html"
desc="C++ std::string"
;;
iostream)
[ -f "$doc_dir/reference/iostream/index.html" ] && result="$doc_dir/reference/iostream/index.html"
desc="C++ iostream header"
;;
esac
# C keywords
case "$term" in
if | else | for | while | do | switch | case | break | continue | return | goto)
[ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html"
[ -f "$doc_dir/en/cpp/language/$term.html" ] && result="$doc_dir/en/cpp/language/$term.html"
desc="C/C++ keyword: $term"
;;
int | char | float | double | void | long | short | unsigned | signed)
[ -f "$doc_dir/en/c/language/type.html" ] && result="$doc_dir/en/c/language/type.html"
desc="C/C++ type: $term"
;;
struct | union | enum | typedef)
[ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html"
desc="C/C++ keyword: $term"
;;
esac
# Search in files if not found (use -L to follow symlinks)
if [ -z "$result" ]; then
local found
found=$(find -L "$doc_dir" -name "*${term}*" -type f 2>/dev/null | head -1)
if [ -n "$found" ]; then
result="$found"
desc="C/C++: $term"
fi
fi
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}
#==============================================================================
# JavaScript/MDN specific lookup
# Searches the cloned MDN content repository
#==============================================================================
lookup_js() {
local term="$1"
local mdn_dir="$DOCS_DIR/mdn-content/files/en-us"
# Normalize term for searching
local term_lower
term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]')
# Handle common statement aliases (MDN uses if...else, try...catch, etc.)
local statement_aliases=(
"if:if...else"
"else:if...else"
"try:try...catch"
"catch:try...catch"
"finally:try...catch"
"do:do...while"
"while:while"
"for:for"
"switch:switch"
"case:switch"
"default:switch"
)
for alias in "${statement_aliases[@]}"; do
local key="${alias%%:*}"
local value="${alias##*:}"
if [ "$term_lower" = "$key" ]; then
local stmt_dir="$mdn_dir/web/javascript/reference/statements/$value"
if [ -d "$stmt_dir" ] && [ -f "$stmt_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$stmt_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$stmt_dir/index.md|${title:-$term}"
return 0
fi
fi
done
# Handle boolean/null literals
case "$term_lower" in
true | false)
local bool_dir="$mdn_dir/web/javascript/reference/global_objects/boolean"
if [ -d "$bool_dir" ] && [ -f "$bool_dir/index.md" ]; then
echo "$bool_dir/index.md|Boolean ($term)"
return 0
fi
;;
null)
local null_dir="$mdn_dir/web/javascript/reference/operators/null"
if [ -d "$null_dir" ] && [ -f "$null_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$null_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$null_dir/index.md|${title:-null}"
return 0
fi
;;
undefined)
local undef_dir="$mdn_dir/web/javascript/reference/global_objects/undefined"
if [ -d "$undef_dir" ] && [ -f "$undef_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$undef_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$undef_dir/index.md|${title:-undefined}"
return 0
fi
;;
esac
# Search JavaScript reference directory structure (priority order)
local search_dirs=(
"$mdn_dir/web/javascript/reference/statements"
"$mdn_dir/web/javascript/reference/operators"
"$mdn_dir/web/javascript/reference/global_objects"
"$mdn_dir/web/javascript/reference/functions"
"$mdn_dir/web/javascript/reference/classes"
)
for search_dir in "${search_dirs[@]}"; do
if [ -d "$search_dir" ]; then
# Look for exact directory match (MDN uses directories with index.md)
local found_dir
found_dir=$(find "$search_dir" -maxdepth 2 -type d -iname "$term" 2>/dev/null | head -1)
if [ -n "$found_dir" ] && [ -f "$found_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found_dir/index.md|${title:-$term}"
return 0
fi
fi
done
# Search Web APIs - prioritize *_api directories for common terms
if [ -d "$mdn_dir/web/api" ]; then
# First try <term>_api directory (e.g., fetch_api, console_api)
local api_dir="$mdn_dir/web/api/${term_lower}_api"
if [ -d "$api_dir" ] && [ -f "$api_dir/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$api_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$api_dir/index.md|${title:-$term API}"
return 0
fi
# Then try exact top-level API interface (e.g., Console, Document, Element)
local found
found=$(find "$mdn_dir/web/api" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found/index.md|${title:-$term}"
return 0
fi
# Try window/<term> for global functions like alert, confirm, etc.
local window_method="$mdn_dir/web/api/window/${term_lower}"
if [ -d "$window_method" ] && [ -f "$window_method/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$window_method/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$window_method/index.md|${title:-Window.$term()}"
return 0
fi
# Search nested API methods
found=$(find "$mdn_dir/web/api" -maxdepth 3 -type d -iname "$term" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found/index.md|${title:-$term}"
return 0
fi
fi
# Now try partial matches in Global Objects (e.g., Array.from, Object.keys)
if [ -d "$mdn_dir/web/javascript/reference/global_objects" ]; then
local found
found=$(find "$mdn_dir/web/javascript/reference/global_objects" -maxdepth 2 -type d -iname "*${term}*" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found/index.md|${title:-$term}"
return 0
fi
fi
# Glossary as last resort
if [ -d "$mdn_dir/glossary" ]; then
local found
found=$(find "$mdn_dir/glossary" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found/index.md" ]; then
local title
title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"')
echo "$found/index.md|${title:-$term}"
return 0
fi
fi
return 1
}
#==============================================================================
# Rust specific lookup
#==============================================================================
lookup_rust() {
local term="$1"
local result=""
local desc=""
if command -v rustup &>/dev/null; then
# Use rustup doc to get path
local rust_doc_path
rust_doc_path=$(rustup doc --path 2>/dev/null | head -1 | xargs dirname 2>/dev/null)
# Search in std docs
if [ -d "$rust_doc_path/std" ]; then
local found
found=$(find "$rust_doc_path/std" -name "*${term}*" -type f 2>/dev/null | head -1)
if [ -n "$found" ]; then
result="$found"
desc="Rust: $term"
fi
fi
fi
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}
#==============================================================================
# Go specific lookup
#==============================================================================
lookup_go() {
local term="$1"
local result=""
local desc=""
if command -v go &>/dev/null; then
# Check if it's a stdlib package
if go doc "$term" &>/dev/null; then
result="go doc $term"
desc="Go package: $term (use 'go doc $term' to view)"
fi
fi
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}
#==============================================================================
# Shell specific lookup
#==============================================================================
lookup_shell() {
local term="$1"
local doc_dir="$DOCS_DIR/shell"
local result=""
local desc=""
# Check bash builtins
if [ -f "$doc_dir/bash_builtins.txt" ]; then
if grep -q "=== $term ===" "$doc_dir/bash_builtins.txt" 2>/dev/null; then
result="$doc_dir/bash_builtins.txt"
desc="Bash builtin: $term"
fi
fi
# Check common commands
if [ -z "$result" ] && [ -f "$doc_dir/common_commands.txt" ]; then
if grep -q "^$term" "$doc_dir/common_commands.txt" 2>/dev/null; then
local cmd_desc
cmd_desc=$(grep "^$term" "$doc_dir/common_commands.txt" | head -1)
result="$doc_dir/common_commands.txt"
desc="Shell command: $cmd_desc"
fi
fi
# Try man page
if [ -z "$result" ]; then
local man_path
man_path=$(man -w "$term" 2>/dev/null)
if [ -n "$man_path" ]; then
result="man $term"
desc="Manual page: $term (use 'man $term' to view)"
fi
fi
if [ -n "$result" ]; then
echo "$result|$desc"
fi
}
#==============================================================================
# Generic lookup (searches all languages)
#==============================================================================
lookup_all() {
local term="$1"
# Try each language
for lang in python cpp js rust go shell; do
local result
result=$(lookup_$lang "$term" 2>/dev/null)
if [ -n "$result" ]; then
echo "$lang: $result"
fi
done
}
#==============================================================================
# Parse Python import and lookup the actual imported item
#==============================================================================
parse_python_import() {
local import_line="$1"
# Handle "from X import Y" format
if [[ $import_line =~ ^from[[:space:]]+([^[:space:]]+)[[:space:]]+import[[:space:]]+(.+) ]]; then
local module="${BASH_REMATCH[1]}"
local items="${BASH_REMATCH[2]}"
# Clean up items (remove parentheses, commas, etc.)
items=$(echo "$items" | sed 's/[(),]//g' | awk '{print $1}')
# Output: module and first imported item
echo "$module|$items"
return 0
fi
# Handle "import X" format
if [[ $import_line =~ ^import[[:space:]]+([^[:space:],]+) ]]; then
local module="${BASH_REMATCH[1]}"
echo "$module|"
return 0
fi
return 1
}
#==============================================================================
# Smart lookup for imports
#==============================================================================
lookup_import() {
local import_line="$1"
local lang="$2"
case "$lang" in
python)
local parsed
parsed=$(parse_python_import "$import_line")
if [ -n "$parsed" ]; then
local module item
module=$(echo "$parsed" | cut -d'|' -f1)
item=$(echo "$parsed" | cut -d'|' -f2)
# For "from X import Y", look up Y within module X's documentation
if [ -n "$item" ] && [ -n "$module" ]; then
local result
# Pass both item and module to lookup_python
result=$(lookup_python "$item" "$module")
if [ -n "$result" ]; then
echo "$result"
return 0
fi
fi
# Fall back to module documentation
lookup_python "$module"
fi
;;
c_cpp)
# Extract header name from #include <header> or #include "header"
local header
header=$(echo "$import_line" | sed -E 's/#include\s*[<"]([^">]+)[">]/\1/' | sed 's/\.h$//')
lookup_cpp "$header"
;;
javascript | typescript)
# Extract module from import/require
local module=""
# Match: from "module" or from 'module'
module=$(echo "$import_line" | grep -oP "from\s+['\"]\\K[^'\"]+")
if [ -z "$module" ]; then
# Match: require("module") or require('module')
module=$(echo "$import_line" | grep -oP "require\\(['\"]\\K[^'\"]+")
fi
[ -n "$module" ] && lookup_js "$module"
;;
*)
echo "Unknown language: $lang"
;;
esac
}
#==============================================================================
# Extract documentation content
#==============================================================================
extract_doc_content() {
local file="$1"
local term="$2"
local max_lines="${3:-20}"
if [[ $file == *.html ]]; then
# Extract text from HTML, find section about term
if command -v html2text &>/dev/null; then
html2text "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines"
elif command -v lynx &>/dev/null; then
lynx -dump -nolist "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines"
else
# Basic extraction
sed 's/<[^>]*>//g' "$file" | grep -A"$max_lines" -i "$term" | head -"$max_lines"
fi
elif [[ $file == *.json ]]; then
# Pretty print JSON section
grep -A5 "\"$term\"" "$file" 2>/dev/null
else
# Plain text
grep -A"$max_lines" -i "$term" "$file" | head -"$max_lines"
fi
}
#==============================================================================
# Main
#==============================================================================
usage() {
cat <<EOF
Usage: $0 <term> [language] [options]
Search offline documentation for a term.
Languages: python, cpp, c_cpp, js, javascript, rust, go, shell, all
Options:
--open Open the documentation file (requires xdg-open)
--extract Extract and display relevant content
--import Parse and lookup an import statement
--batch Process multiple terms from a file
Examples:
$0 Path python # Find Path in Python docs
$0 vector cpp # Find vector in C++ docs
$0 map # Find map in all languages
$0 --import "from pathlib import Path" python
$0 --batch imports.txt python
EOF
}
main() {
if [ $# -eq 0 ]; then

View File

@ -89,914 +89,19 @@ require_non_root() {
fi
}
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] [COMMAND]
Root BL9000 phone from Arch Linux using Magisk.
Commands:
install-deps Install required dependencies (adb, fastboot, tools)
install-mtk Install MTKClient for MediaTek boot extraction
check Check device connection and prerequisites
backup Backup phone data before unlocking bootloader
unlock Unlock bootloader (WARNING: wipes all data!)
extract-mtk Extract boot.img using MTKClient (for MediaTek devices)
patch Patch boot.img with Magisk (manual step on phone)
flash [IMG] Flash patched boot image (uses magisk_patched.img or specified file)
auto-root Automated: extract -> patch -> flash in one command
root Extract boot, patch with Magisk, and flash
full Run complete rooting process (deps + unlock + root)
clean Remove temporary working directory
help Show this message
Options:
-h, --help Show this message
--work-dir DIR Set working directory (default: $WORK_DIR)
--boot-img FILE Use existing boot.img instead of extracting from device
Examples:
$SCRIPT_NAME install-deps # Install required tools
$SCRIPT_NAME install-mtk # Install MTKClient for boot extraction
$SCRIPT_NAME check # Verify device connection
$SCRIPT_NAME backup # Backup phone data first!
$SCRIPT_NAME extract-mtk # Extract boot.img with MTKClient
$SCRIPT_NAME auto-root # Automated rooting (extract + patch + flash)
$SCRIPT_NAME full # Complete rooting process
$SCRIPT_NAME root # Root only (assumes bootloader unlocked)
WARNING: Unlocking the bootloader will ERASE ALL DATA on your phone!
Make sure to back up important data before proceeding.
EOF
}
install_dependencies() {
print_header "Installing Dependencies"
local packages=()
local missing=()
# Check for required commands
if ! command -v adb > /dev/null 2>&1; then
packages+=("android-tools")
missing+=("adb")
fi
if ! command -v fastboot > /dev/null 2>&1 && ! pacman -Q android-tools > /dev/null 2>&1; then
packages+=("android-tools")
missing+=("fastboot")
fi
if ! command -v unzip > /dev/null 2>&1; then
packages+=("unzip")
missing+=("unzip")
fi
if ! command -v curl > /dev/null 2>&1; then
packages+=("curl")
missing+=("curl")
fi
if ! command -v python3 > /dev/null 2>&1; then
packages+=("python")
missing+=("python3")
fi
if ! command -v git > /dev/null 2>&1; then
packages+=("git")
missing+=("git")
fi
# Check for libusb and fuse2 (needed for mtkclient)
if ! pacman -Q libusb > /dev/null 2>&1; then
packages+=("libusb")
missing+=("libusb")
fi
if ! pacman -Q fuse2 > /dev/null 2>&1; then
packages+=("fuse2")
missing+=("fuse2")
fi
# Check for python-protobuf (needed for boot image tools)
if ! python3 -c "import google.protobuf" 2> /dev/null; then
packages+=("python-protobuf")
missing+=("python-protobuf")
fi
if [[ ${#missing[@]} -eq 0 ]]; then
log "All dependencies are already installed."
return 0
fi
log "Missing dependencies: ${missing[*]}"
# Remove duplicates
readarray -t packages < <(printf '%s\n' "${packages[@]}" | sort -u)
if ! confirm "Install missing packages: ${packages[*]}?"; then
die "Cannot proceed without required dependencies."
fi
log "Installing packages: ${packages[*]}"
sudo pacman -S --needed --noconfirm "${packages[@]}" || die "Failed to install dependencies"
log "Dependencies installed successfully."
}
setup_udev_rules() {
print_header "Setting Up USB Access"
local udev_file="/etc/udev/rules.d/51-android.rules"
local mtk_udev_dir="${WORK_DIR}/mtkclient/mtkclient/Setup/Linux"
# Install MTKClient udev rules if mtkclient is present
if [[ -d "${WORK_DIR}/mtkclient" ]]; then
log "Installing MTKClient udev rules..."
if [[ -d $mtk_udev_dir ]]; then
sudo cp "$mtk_udev_dir"/*.rules /etc/udev/rules.d/ 2> /dev/null || warn "Failed to copy MTKClient rules"
fi
fi
if [[ -f $udev_file ]]; then
log "Android udev rules already exist at $udev_file"
else
if ! confirm "Create udev rules for Android device access?"; then
warn "Skipping udev rules. You may need to run commands with sudo."
return 0
fi
log "Creating Android udev rules..."
# Create comprehensive udev rules for Android devices
sudo tee "$udev_file" > /dev/null << 'EOF'
# Android Debug Bridge (ADB) devices
# Add your device's vendor ID if not listed
# Google
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="adbusers"
# MediaTek (common in BL9000)
SUBSYSTEM=="usb", ATTR{idVendor}=="0e8d", MODE="0666", GROUP="adbusers"
# Generic catch-all for Android devices
SUBSYSTEM=="usb", ATTR{idVendor}=="*", ATTR{idProduct}=="*", MODE="0666", GROUP="adbusers", SYMLINK+="android%n"
EOF
fi
# Create adbusers group if it doesn't exist
if ! getent group adbusers > /dev/null; then
sudo groupadd -r adbusers
log "Created adbusers group"
fi
# Add current user to adbusers and plugdev groups
if ! groups "$USER" | grep -q '\badbusers\b'; then
sudo usermod -aG adbusers "$USER"
log "Added $USER to adbusers group"
fi
if ! getent group plugdev > /dev/null; then
sudo groupadd -r plugdev
fi
if ! groups "$USER" | grep -q '\bplugdev\b'; then
sudo usermod -aG plugdev "$USER"
log "Added $USER to plugdev group"
fi
if ! getent group dialout > /dev/null; then
sudo groupadd -r dialout
fi
if ! groups "$USER" | grep -q '\bdialout\b'; then
sudo usermod -aG dialout "$USER"
log "Added $USER to dialout group"
warn "You need to log out and back in for group membership to take effect."
warn "Alternatively, run: newgrp dialout"
fi
# Reload udev rules
sudo udevadm control --reload-rules
sudo udevadm trigger
log "USB access configured successfully."
}
backup_device_data() {
print_header "Backing Up Device Data"
local backup_dir
backup_dir="${WORK_DIR}/backup_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$backup_dir"
log "Backup directory: $backup_dir" # Check device connection first
if ! adb get-state > /dev/null 2>&1; then
error "Device not connected. Please connect your device first."
return 1
fi
log "Starting comprehensive backup process..."
# 1. Backup internal storage (DCIM, Pictures, Documents, Downloads, etc.)
log "Backing up internal storage (this may take a while)..."
local storage_dirs=("DCIM" "Pictures" "Documents" "Download" "Music" "Movies" "WhatsApp" "Telegram")
for dir in "${storage_dirs[@]}"; do
if adb shell "[ -d /sdcard/$dir ]" 2> /dev/null; then
log " → Backing up /sdcard/$dir..."
if adb pull "/sdcard/$dir" "$backup_dir/$dir" 2>&1 | grep -v "^$"; then
log " ✓ $dir backed up successfully"
else
warn " ⚠ Could not backup $dir (may be empty or inaccessible)"
fi
fi
done
# 2. Backup SMS/MMS (if possible)
log "Backing up SMS/MMS database..."
if adb shell "su -c 'cp /data/data/com.android.providers.telephony/databases/mmssms.db /sdcard/mmssms.db'" 2> /dev/null; then
adb pull /sdcard/mmssms.db "$backup_dir/mmssms.db" 2> /dev/null && log " ✓ SMS/MMS backed up"
adb shell "rm /sdcard/mmssms.db" 2> /dev/null || true
else
warn " ⚠ SMS/MMS backup requires root (skipping)"
fi
# 3. Backup contacts
log "Backing up contacts..."
if adb shell "su -c 'cp /data/data/com.android.providers.contacts/databases/contacts2.db /sdcard/contacts2.db'" 2> /dev/null; then
adb pull /sdcard/contacts2.db "$backup_dir/contacts2.db" 2> /dev/null && log " ✓ Contacts backed up"
adb shell "rm /sdcard/contacts2.db" 2> /dev/null || true
else
warn " ⚠ Contacts backup requires root (skipping)"
fi
# 4. Backup call logs
log "Backing up call logs..."
if adb shell "su -c 'cp /data/data/com.android.providers.contacts/databases/calllog.db /sdcard/calllog.db'" 2> /dev/null; then
adb pull /sdcard/calllog.db "$backup_dir/calllog.db" 2> /dev/null && log " ✓ Call logs backed up"
adb shell "rm /sdcard/calllog.db" 2> /dev/null || true
else
warn " ⚠ Call logs backup requires root (skipping)"
fi
# 5. Backup app list
log "Backing up installed apps list..."
adb shell "pm list packages -f" > "$backup_dir/installed_apps.txt"
log " ✓ App list saved to installed_apps.txt"
# 6. Backup APKs for user-installed apps (optional, can be large)
if confirm "Backup APK files for installed apps? (This can take a long time and use lots of space)"; then
log "Backing up user-installed APKs..."
local apk_dir="$backup_dir/apks"
mkdir -p "$apk_dir"
# Get user-installed packages
local user_apps
user_apps=$(adb shell "pm list packages -3 -f" | sed 's/package://' | cut -d'=' -f2)
local count=0
while IFS= read -r pkg; do
if [[ -n $pkg ]]; then
log " → Backing up $pkg..."
local apk_path
apk_path=$(adb shell "pm path $pkg" | head -n1 | sed 's/package://')
if [[ -n $apk_path ]]; then
adb pull "$apk_path" "$apk_dir/${pkg}.apk" > /dev/null 2>&1 && count=$((count + 1))
fi
fi
done <<< "$user_apps"
log " ✓ Backed up $count APK files"
fi
# 7. Full ADB backup (app data, if device supports it)
log "Creating full ADB backup (app data)..."
if confirm "Create full ADB backup? (You'll need to confirm on your device)"; then
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " On your phone: Tap 'Back up my data' when prompted"
echo " You can set a password or leave it blank"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
if adb backup -apk -shared -all -system -f "$backup_dir/full_backup.ab"; then
log " ✓ Full ADB backup completed"
log " Note: Restore with: adb restore full_backup.ab"
else
warn " ⚠ ADB backup failed or was cancelled"
fi
fi
# 8. Backup device info
log "Saving device information..."
{
echo "Device Backup Information"
echo "========================="
echo "Date: $(date)"
echo
echo "Device Model: $(adb shell getprop ro.product.model | tr -d '\r\n')"
echo "Android Version: $(adb shell getprop ro.build.version.release | tr -d '\r\n')"
echo "Build Number: $(adb shell getprop ro.build.display.id | tr -d '\r\n')"
echo "Security Patch: $(adb shell getprop ro.build.version.security_patch | tr -d '\r\n')"
echo "Serial: $(adb shell getprop ro.serialno | tr -d '\r\n')"
echo
echo "Installed Apps:"
adb shell "pm list packages -3" | sed 's/package:/ - /'
} > "$backup_dir/device_info.txt"
log " ✓ Device info saved"
# Summary
local backup_size
backup_size=$(du -sh "$backup_dir" 2> /dev/null | cut -f1 || echo "unknown")
echo
echo -e "${GREEN}╔═══════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Backup Completed Successfully! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════╝${NC}"
echo
log "Backup location: $backup_dir"
log "Backup size: $backup_size"
echo
echo "What was backed up:"
echo " ✓ Photos (DCIM)"
echo " ✓ Pictures"
echo " ✓ Documents"
echo " ✓ Downloads"
echo " ✓ Music"
echo " ✓ Movies"
echo " ✓ WhatsApp data (if present)"
echo " ✓ Telegram data (if present)"
echo " ✓ Installed apps list"
echo " ✓ Device information"
if [[ -f "$backup_dir/full_backup.ab" ]]; then
echo " ✓ Full app data backup"
fi
if [[ -d "$backup_dir/apks" ]]; then
echo " ✓ APK files"
fi
echo
log "Keep this backup safe! You'll need it to restore your data after rooting."
return 0
}
check_device() {
print_header "Checking Device Connection"
log "Starting ADB server..."
adb start-server > /dev/null 2>&1 || true
log "Waiting for device..."
if ! adb wait-for-device; then
error "Failed to detect device via ADB."
echo
echo "Troubleshooting steps:"
echo "1. Make sure USB debugging is enabled on your phone"
echo " Settings → About Phone → Tap Build Number 7 times"
echo " Settings → Developer Options → Enable USB Debugging"
echo "2. Connect your phone via USB cable"
echo "3. Accept the 'Allow USB debugging' prompt on your phone"
echo "4. Run: adb devices"
echo
return 1
fi
local device_info
device_info=$(adb devices -l | grep -v "List of devices" | grep -v "^$" | head -n1)
if [[ -z $device_info ]]; then
error "No device detected"
return 1
fi
log "Device connected: $device_info"
# Check device properties
local model
model=$(adb shell getprop ro.product.model 2> /dev/null | tr -d '\r\n' || echo "Unknown")
log "Model: $model"
local android_version
android_version=$(adb shell getprop ro.build.version.release 2> /dev/null | tr -d '\r\n' || echo "Unknown")
log "Android version: $android_version"
local battery_level
battery_level=$(adb shell dumpsys battery | grep level | awk '{print $2}' | tr -d '\r\n' || echo "Unknown")
log "Battery level: ${battery_level}%"
if [[ $battery_level != "Unknown" && $battery_level -lt 50 ]]; then
warn "Battery level is below 50%. Charge your phone before proceeding."
if ! confirm "Continue anyway?"; then
return 1
fi
fi
# Check if bootloader is unlocked
local unlock_status
unlock_status=$(adb shell getprop ro.boot.verifiedbootstate 2> /dev/null | tr -d '\r\n' || echo "unknown")
if [[ $unlock_status == "orange" || $unlock_status == "red" ]]; then
log "Bootloader unlock status: ${GREEN}UNLOCKED${NC}"
else
warn "Bootloader appears to be LOCKED. You'll need to unlock it to root."
fi
# Check if OEM unlocking is enabled
local oem_unlock
oem_unlock=$(adb shell getprop sys.oem_unlock_allowed 2> /dev/null | tr -d '\r\n' || echo "unknown")
if [[ $oem_unlock == "1" ]]; then
log "OEM unlocking: ${GREEN}ENABLED${NC}"
else
warn "OEM unlocking is not enabled in Developer Options."
echo "Enable it at: Settings → Developer Options → OEM unlocking"
fi
return 0
}
unlock_bootloader() {
print_header "Unlocking Bootloader"
echo
echo -e "${RED}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}║ WARNING ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ Unlocking the bootloader will ERASE ALL DATA on your phone! ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ This includes: ║${NC}"
echo -e "${RED}║ - All apps and app data ║${NC}"
echo -e "${RED}║ - Photos, videos, and files ║${NC}"
echo -e "${RED}║ - System settings ║${NC}"
echo -e "${RED}║ - Everything else on internal storage ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ Make sure you have backed up important data! ║${NC}"
echo -e "${RED}╚═══════════════════════════════════════════════════════════════╝${NC}"
echo
if ! confirm "Have you backed up all important data and want to proceed?"; then
log "Bootloader unlock cancelled by user."
return 1
fi
if ! confirm "Are you ABSOLUTELY SURE? This cannot be undone!"; then
log "Bootloader unlock cancelled by user."
return 1
fi
log "Rebooting device to bootloader..."
adb reboot bootloader || die "Failed to reboot to bootloader"
log "Waiting for fastboot mode..."
sleep 5
if ! fastboot devices | grep -q .; then
error "Device not detected in fastboot mode."
echo
echo "If the device doesn't enter fastboot automatically:"
echo "1. Power off the phone completely"
echo "2. Hold Volume Down + Power buttons simultaneously"
echo "3. Release when you see the bootloader/fastboot screen"
echo "4. Run: fastboot devices"
echo
return 1
fi
log "Device in fastboot mode"
# Check current bootloader status
local bl_status
bl_status=$(fastboot getvar unlocked 2>&1 | grep "unlocked:" | awk '{print $2}' || echo "unknown")
if [[ $bl_status == "yes" ]]; then
log "Bootloader is already unlocked."
fastboot reboot
return 0
fi
log "Attempting to unlock bootloader..."
# Try different unlock commands (varies by device)
if fastboot flashing unlock 2>&1 | grep -qi "okay\|finished"; then
log "Bootloader unlock command sent successfully."
elif fastboot oem unlock 2>&1 | grep -qi "okay\|finished"; then
log "Bootloader unlock command sent successfully."
else
error "Bootloader unlock command may have failed."
echo
echo "On your phone:"
echo "1. Use volume buttons to select 'Unlock the bootloader'"
echo "2. Press power button to confirm"
echo
if ! confirm "Did you complete the unlock on the device?"; then
fastboot reboot
return 1
fi
fi
log "Rebooting device..."
fastboot reboot || true
log "Bootloader unlocked successfully!"
log "Device will now boot up and perform factory reset..."
log "Waiting for device to come back online..."
sleep 10
adb wait-for-device || true
log "Please complete the initial setup on your phone, then re-enable USB debugging."
echo
return 0
}
download_magisk() {
print_header "Downloading Magisk"
local magisk_apk="$WORK_DIR/magisk.apk"
if [[ -f $magisk_apk ]]; then
log "Magisk APK already downloaded at $magisk_apk"
return 0
fi
log "Downloading latest Magisk APK..."
if ! curl -L -o "$magisk_apk" "$MAGISK_APK_URL"; then
error "Failed to download Magisk APK"
return 1
fi
log "Magisk downloaded successfully: $magisk_apk"
return 0
}
install_mtkclient() {
print_header "Installing MTKClient"
local mtk_dir="${WORK_DIR}/mtkclient"
if [[ -d $mtk_dir && -f "$mtk_dir/mtk.py" ]]; then
log "MTKClient already installed at $mtk_dir"
return 0
fi
if ! confirm "Install MTKClient for MediaTek boot image extraction?"; then
return 1
fi
log "Cloning MTKClient repository..."
if ! git clone https://github.com/bkerler/mtkclient "$mtk_dir"; then
error "Failed to clone MTKClient"
return 1
fi
log "Installing MTKClient Python dependencies..."
cd "$mtk_dir"
python3 -m pip install --user -r requirements.txt || warn "Some dependencies may have failed to install"
python3 -m pip install --user . || warn "MTKClient installation may be incomplete"
cd - > /dev/null
log "MTKClient installed successfully"
return 0
}
extract_boot_with_mtkclient() {
print_header "Extracting Boot with MTKClient"
local mtk_dir="${WORK_DIR}/mtkclient"
local boot_img="$WORK_DIR/boot.img"
local boot_a_img="$WORK_DIR/boot_a.img"
local vbmeta_a_img="$WORK_DIR/vbmeta_a.img"
if [[ ! -d $mtk_dir ]]; then
error "MTKClient not installed. Run: $SCRIPT_NAME install-mtk"
return 1
fi
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " MTKClient Boot ROM Mode Instructions"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
echo "BEFORE continuing, you MUST:"
echo
echo " 1. Power off your phone COMPLETELY"
echo " 2. DISCONNECT the USB cable from your phone"
echo " 3. Have the USB cable ready in your hand"
echo
echo "When you press Enter:"
echo
echo " 4. Press and hold BOTH Volume buttons (Up + Down)"
echo " 5. While holding BOTH buttons, connect USB cable"
echo " 6. Keep holding until device is detected (may take 5-10 seconds)"
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
if ! confirm "Phone is OFF and USB DISCONNECTED - ready to proceed?"; then
return 1
fi
log "Starting MTKClient NOW - quickly enter BROM mode!"
log "Hold BOTH volume buttons and connect USB cable..."
echo
cd "$mtk_dir"
# Activate venv and extract boot and vbmeta
if [[ ! -d "$mtk_dir/venv" ]]; then
error "MTKClient virtual environment not found. Run: $SCRIPT_NAME install-mtk"
cd - > /dev/null
return 1
fi
# shellcheck source=/dev/null
source venv/bin/activate
# BL9000 uses A/B partitions, so extract boot_a and vbmeta_a
if python3 mtk.py r boot_a,vbmeta_a "$boot_a_img,$vbmeta_a_img"; then
log "Boot_a and vbmeta_a extracted successfully!"
# Copy boot_a.img to boot.img for Magisk compatibility
cp "$boot_a_img" "$boot_img"
BOOT_IMG="$boot_img"
log "Boot image ready for patching: $BOOT_IMG"
# Reset device
log "Resetting device..."
python3 mtk.py reset || warn "Failed to reset device, please reboot manually"
# Deactivate venv if function exists
type deactivate &> /dev/null && deactivate
cd - > /dev/null
return 0
else
# Deactivate venv if function exists
type deactivate &> /dev/null && deactivate
error "Failed to extract boot image with MTKClient"
cd - > /dev/null
return 1
fi
}
extract_boot_image() {
print_header "Extracting Boot Image"
local boot_img="$WORK_DIR/boot.img"
if [[ -n ${BOOT_IMG:-} && -f $BOOT_IMG ]]; then
log "Using provided boot image: $BOOT_IMG"
cp "$BOOT_IMG" "$boot_img"
BOOT_IMG="$boot_img"
return 0
fi
log "Attempting to extract boot image from device..."
# Method 1: Try MTKClient first (best for MediaTek devices)
if [[ -d "${WORK_DIR}/mtkclient" ]]; then
log "Trying MTKClient extraction..."
if extract_boot_with_mtkclient; then
return 0
fi
warn "MTKClient extraction failed, trying ADB methods..."
fi
# Method 2: Try to pull boot partition directly via ADB
local boot_partition
boot_partition=$(adb shell "find /dev/block -name boot | head -n1" 2> /dev/null | tr -d '\r\n' || echo "")
if [[ -n $boot_partition ]]; then
log "Found boot partition: $boot_partition"
if adb pull "$boot_partition" "$boot_img" 2> /dev/null; then
log "Boot image extracted successfully"
BOOT_IMG="$boot_img"
return 0
fi
fi
# Method 3: Try to get boot partition via by-name
boot_partition=$(adb shell "ls /dev/block/by-name/boot*" 2> /dev/null | head -n1 | tr -d '\r\n' || echo "")
if [[ -n $boot_partition ]]; then
log "Found boot partition: $boot_partition"
if adb shell "su -c 'dd if=$boot_partition of=/sdcard/boot.img'" 2> /dev/null &&
adb pull /sdcard/boot.img "$boot_img" 2> /dev/null; then
adb shell rm /sdcard/boot.img 2> /dev/null || true
log "Boot image extracted successfully"
BOOT_IMG="$boot_img"
return 0
fi
fi
error "Failed to extract boot image automatically."
echo
echo "Manual extraction options:"
echo "1. Use MTKClient: $SCRIPT_NAME extract-mtk"
echo "2. Extract boot.img from your device's firmware package"
echo "3. Get boot.img from device manufacturer's official ROM"
echo
echo "Then run: $SCRIPT_NAME root --boot-img /path/to/boot.img"
echo
return 1
}
patch_boot_with_magisk() {
print_header "Patching Boot Image with Magisk"
if [[ ! -f ${BOOT_IMG:-} ]]; then
die "Boot image not found: ${BOOT_IMG:-none}"
fi
local magisk_apk="$WORK_DIR/magisk.apk"
if [[ ! -f $magisk_apk ]]; then
die "Magisk APK not found. Run download step first."
fi
log "Checking if device is connected..."
if ! adb devices | grep -q "device$"; then
die "No device detected. Make sure USB debugging is enabled and device is connected."
fi
log "Installing Magisk APK on device..."
if ! adb install -r "$magisk_apk" 2> /dev/null; then
warn "Magisk APK installation failed (may already be installed)"
fi
log "Pushing boot image to device..."
adb push "$BOOT_IMG" /sdcard/Download/boot.img || die "Failed to push boot image"
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " MANUAL STEP REQUIRED"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
echo "On your phone:"
echo "1. Open the Magisk app"
echo "2. Tap 'Install' next to Magisk"
echo "3. Select 'Select and Patch a File'"
echo "4. Navigate to Downloads and select boot.img"
echo "5. Tap 'Let's Go' and wait for patching to complete"
echo "6. The patched file will be saved as magisk_patched_*.img"
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
if ! confirm "Have you completed patching the boot image in Magisk app?"; then
error "Patching cancelled by user"
return 1
fi
log "Pulling patched boot image from device..."
local patched_img
patched_img=$(adb shell "ls /sdcard/Download/magisk_patched_*.img 2>/dev/null" | tr -d '\r\n' | head -n1 || echo "")
if [[ -z $patched_img ]]; then
error "Could not find patched boot image on device."
echo "Please ensure the patching completed successfully in Magisk app."
return 1
fi
PATCHED_BOOT_IMG="$WORK_DIR/magisk_patched.img"
if ! adb pull "$patched_img" "$PATCHED_BOOT_IMG"; then
error "Failed to pull patched boot image"
return 1
fi
log "Patched boot image saved to: $PATCHED_BOOT_IMG"
return 0
}
flash_patched_boot() {
print_header "Flashing Patched Boot Image"
if [[ ! -f ${PATCHED_BOOT_IMG:-} ]]; then
die "Patched boot image not found: ${PATCHED_BOOT_IMG:-none}"
fi
echo
echo -e "${YELLOW}This will flash the patched boot image to your device.${NC}"
echo "Device uses A/B partitions - will flash to boot_a"
echo
if ! confirm "Proceed with flashing?"; then
log "Flashing cancelled by user"
return 1
fi
log "Rebooting to bootloader..."
adb reboot bootloader || die "Failed to reboot to bootloader"
log "Waiting for fastboot mode..."
sleep 5
if ! sudo fastboot devices | grep -q .; then
die "Device not detected in fastboot mode"
fi
log "Flashing patched boot image to boot_a..."
if ! sudo fastboot flash boot_a "$PATCHED_BOOT_IMG"; then
error "Failed to flash boot image"
return 1
fi
log "Flashed successfully!"
log "Rebooting device..."
sudo fastboot reboot
log "Waiting for device to boot..."
sleep 10
adb wait-for-device || true
echo
echo -e "${GREEN}╔═══════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Root Process Complete! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════╝${NC}"
echo
echo "Your BL9000 phone should now be rooted with Magisk!"
echo
echo "Next steps:"
echo "1. Open the Magisk app on your phone"
echo "2. Verify that it shows 'Installed' for both Magisk and App"
echo "3. Grant root access to apps as needed"
echo "4. Install Magisk modules if desired"
echo
echo "Note: Some banking and secure apps may not work with root."
echo " Use Magisk's DenyList feature to hide root from specific apps."
echo
return 0
}
clean_work_dir() {
if [[ -d $WORK_DIR ]]; then
log "Removing working directory: $WORK_DIR"
rm -rf "$WORK_DIR"
log "Cleaned successfully"
else
log "Work directory doesn't exist: $WORK_DIR"
fi
}
run_full_process() {
print_header "BL9000 Full Root Process"
log "Starting complete rooting process..."
install_dependencies || die "Failed to install dependencies"
setup_udev_rules || true
echo
if ! confirm "Continue to device check?"; then
die "Process cancelled by user"
fi
check_device || die "Device check failed"
echo
if ! confirm "Continue to backup device data?"; then
die "Process cancelled by user"
fi
backup_device_data || warn "Backup failed or incomplete"
echo
if ! confirm "Continue to bootloader unlock?"; then
die "Process cancelled by user"
fi
unlock_bootloader || die "Bootloader unlock failed"
echo
log "Please complete device setup and re-enable USB debugging, then press Enter..."
read -r
check_device || die "Device check failed after unlock"
download_magisk || die "Failed to download Magisk"
extract_boot_image || die "Failed to extract boot image"
patch_boot_with_magisk || die "Failed to patch boot image"
flash_patched_boot || die "Failed to flash patched boot"
log "Full root process completed successfully!"
}
run_root_only() {
print_header "BL9000 Root Process (Skip Unlock)"
log "Starting root process (assuming bootloader is already unlocked)..."
check_device || die "Device check failed"
download_magisk || die "Failed to download Magisk"
extract_boot_image || die "Failed to extract boot image"
patch_boot_with_magisk || die "Failed to patch boot image"
flash_patched_boot || die "Failed to flash patched boot"
log "Root process completed successfully!"
}
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
# shellcheck source=lib/bl9000_usage.sh
source "$SCRIPT_DIR/lib/bl9000_usage.sh"
# shellcheck source=lib/bl9000_deps.sh
source "$SCRIPT_DIR/lib/bl9000_deps.sh"
# shellcheck source=lib/bl9000_backup.sh
source "$SCRIPT_DIR/lib/bl9000_backup.sh"
# shellcheck source=lib/bl9000_device.sh
source "$SCRIPT_DIR/lib/bl9000_device.sh"
# shellcheck source=lib/bl9000_mtkclient.sh
source "$SCRIPT_DIR/lib/bl9000_mtkclient.sh"
# shellcheck source=lib/bl9000_magisk.sh
source "$SCRIPT_DIR/lib/bl9000_magisk.sh"
main() {
require_non_root