Skip to content

Instantly share code, notes, and snippets.

@rdndds
Last active April 16, 2026 06:28
Show Gist options
  • Select an option

  • Save rdndds/5919604ec6e3ded0dfb87552e2b27f81 to your computer and use it in GitHub Desktop.

Select an option

Save rdndds/5919604ec6e3ded0dfb87552e2b27f81 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
# =========================================================
# HARD-CODED DEFAULTS
# =========================================================
TG_TOKEN="8745418982:AAFuxsUvacKh0eC1gtdPBoH6Ebsrf4TMle0"
TG_CHAT_ID="6665891737"
ENABLE_TELEGRAM=1
VERBOSE_TELEGRAM=1
DEFAULT_DEVICE="P13001L"
DEFAULT_LOCAL_MANIFEST_REPO="https://github.com/rdndds/local_manifest-P13001L"
DEFAULT_LOCAL_MANIFEST_BRANCH="lineage-23.2"
DEFAULT_BUILD_USERNAME="rd"
DEFAULT_BUILD_HOSTNAME="server"
LINEAGE_GAPPS_REPO="https://gitlab.com/MindTheGapps/vendor_gapps.git"
LINEAGE_GAPPS_BRANCH="baklava"
AXION_VARIANT="gms"
AXION_BUILD_TYPE="user"
DO_INIT=1
DO_SYNC=1
GENERATE_KEYS=1
P2R_ENABLED=1
UPLOAD_ROM=1
UPLOAD_RECOVERY=1
# =========================================================
# RUNTIME VARS
# =========================================================
ROM=""
DEVICE="$DEFAULT_DEVICE"
WORKDIR="${PWD}"
MANIFEST_URL=""
MANIFEST_BRANCH=""
LOCAL_MANIFEST_REPO="$DEFAULT_LOCAL_MANIFEST_REPO"
LOCAL_MANIFEST_BRANCH="$DEFAULT_LOCAL_MANIFEST_BRANCH"
LUNCH_TARGET=""
ROM_FILE_GLOB=""
START_TIME="$(date +%s)"
PYTHON_BIN=""
REPO_AVAILABLE=0
UV_AVAILABLE=0
P2R_AVAILABLE=0
UPLOADER_AVAILABLE=0
export PATH="$HOME/.local/bin:$PATH"
# =========================================================
# LOGGING / TELEGRAM
# =========================================================
log() {
printf '\n[%s] %s\n' "$(date '+%F %T')" "$*"
}
send_telegram() {
local msg="$1"
[[ "${ENABLE_TELEGRAM}" -eq 1 ]] || return 0
[[ -n "${TG_TOKEN:-}" && -n "${TG_CHAT_ID:-}" ]] || return 0
curl -fsS -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
--data-urlencode "chat_id=${TG_CHAT_ID}" \
--data-urlencode "text=${msg}" \
>/dev/null || true
}
announce() {
local msg="$1"
log "$msg"
[[ "${VERBOSE_TELEGRAM}" -eq 1 ]] && send_telegram "$msg"
}
warn() {
local msg="$1"
log "WARN: $msg"
[[ "${VERBOSE_TELEGRAM}" -eq 1 ]] && send_telegram "WARNING: $msg"
}
die() {
local msg="$1"
log "ERROR: $msg"
send_telegram "${ROM:-unknown} build for ${DEVICE:-unknown} failed: $msg"
exit 1
}
on_error() {
local exit_code=$?
local line_no="$1"
send_telegram "${ROM:-unknown} build for ${DEVICE:-unknown} failed at line ${line_no} (exit code: ${exit_code})"
echo "FAILED at line ${line_no} (exit code ${exit_code})" >&2
exit "$exit_code"
}
trap 'on_error $LINENO' ERR
usage() {
cat <<'EOF'
Usage:
build-rom.sh --rom <axion|lunaris|lineage> [options]
Required:
--rom NAME axion | lunaris | lineage
Optional:
--device NAME
--workdir PATH default: current directory
--manifest-url URL
--manifest-branch BRANCH
--local-manifest URL
--local-manifest-branch NAME
--token TOKEN
--chat-id ID
--build-username NAME
--build-hostname NAME
--axion-variant NAME
--axion-build-type NAME
--lineage-gapps-branch NAME
Flags:
--skip-init
--skip-sync
--skip-p2r
--skip-upload-rom
--skip-upload-recovery
--skip-keys
--no-telegram
-h, --help
Examples:
./build-rom.sh --rom axion
curl -fsSL "RAW_GIST_URL" | bash -s -- --rom lunaris
crave run --no-patch -- "curl -fsSL 'RAW_GIST_URL' | bash -s -- --rom lineage"
EOF
}
# =========================================================
# GENERIC HELPERS
# =========================================================
command_exists() {
command -v "$1" >/dev/null 2>&1
}
thread_count() {
if command_exists nproc; then
nproc --all
else
echo 8
fi
}
find_newest_file() {
local dir="$1"
local pattern="$2"
find "$dir" -maxdepth 1 -type f -name "$pattern" -printf '%T@ %p\n' 2>/dev/null \
| sort -nr \
| head -n 1 \
| cut -d' ' -f2-
}
find_newest_rom_zip() {
local dir="$1"
find "$dir" -maxdepth 1 -type f -name "*.zip" ! -name "*-recovery.zip" -printf '%T@ %p\n' 2>/dev/null \
| sort -nr \
| head -n 1 \
| cut -d' ' -f2-
}
clone_repo_fresh() {
local repo_url="$1"
local dest="$2"
local branch="${3:-}"
rm -rf "$dest"
if [[ -n "$branch" ]]; then
git clone --depth=1 -b "$branch" "$repo_url" "$dest"
else
git clone --depth=1 "$repo_url" "$dest"
fi
}
# =========================================================
# BEST-EFFORT INSTALLERS
# =========================================================
install_repo_launcher_best_effort() {
if command_exists repo; then
return 0
fi
command_exists curl || return 1
command_exists python3 || command_exists python || return 1
announce "repo missing, trying to install repo launcher"
mkdir -p "$HOME/.local/bin"
if curl -fsSL "https://storage.googleapis.com/git-repo-downloads/repo" -o "$HOME/.local/bin/repo"; then
chmod +x "$HOME/.local/bin/repo"
export PATH="$HOME/.local/bin:$PATH"
hash -r
fi
command_exists repo
}
install_uv_best_effort() {
if command_exists uv; then
export PATH="$HOME/.local/bin:$PATH"
return 0
fi
command_exists curl || return 1
announce "uv missing, trying to install uv"
mkdir -p "$HOME/.local/bin"
if curl -LsSf "https://astral.sh/uv/install.sh" | env UV_UNMANAGED_INSTALL="$HOME/.local/bin" sh; then
export PATH="$HOME/.local/bin:$PATH"
hash -r
fi
command_exists uv
}
ensure_core_requirements() {
command_exists bash || die "bash is required"
command_exists git || die "git is required"
command_exists curl || die "curl is required"
command_exists sed || die "sed is required"
command_exists grep || die "grep is required"
command_exists find || die "find is required"
command_exists sort || die "sort is required"
if command_exists python3; then
PYTHON_BIN="python3"
elif command_exists python; then
PYTHON_BIN="python"
else
die "python/python3 is required"
fi
export PYTHON_BIN
if install_repo_launcher_best_effort; then
REPO_AVAILABLE=1
else
die "repo is required and auto-install failed"
fi
if install_uv_best_effort; then
UV_AVAILABLE=1
else
UV_AVAILABLE=0
warn "uv could not be installed; p2r and uploader will be skipped"
fi
}
ensure_uv_tool_best_effort() {
local tool_name="$1"
local spec="$2"
[[ "${UV_AVAILABLE}" -eq 1 ]] || return 1
if command_exists "$tool_name"; then
return 0
fi
announce "Installing ${tool_name}"
if uv tool install "$spec"; then
export PATH="$HOME/.local/bin:$PATH"
hash -r
command_exists "$tool_name"
return $?
fi
return 1
}
prepare_optional_tools() {
if [[ "${P2R_ENABLED}" -eq 1 ]]; then
if ensure_uv_tool_best_effort p2r "git+https://github.com/rdndds/payload2recovery.git"; then
P2R_AVAILABLE=1
else
P2R_AVAILABLE=0
P2R_ENABLED=0
UPLOAD_RECOVERY=0
warn "p2r unavailable; recovery generation/upload disabled"
fi
fi
if [[ "${UPLOAD_ROM}" -eq 1 || "${UPLOAD_RECOVERY}" -eq 1 ]]; then
if ensure_uv_tool_best_effort uploader "git+https://github.com/rdndds/uploader.git"; then
UPLOADER_AVAILABLE=1
else
UPLOADER_AVAILABLE=0
warn "uploader unavailable; GoFile curl upload fallback will be used"
fi
fi
}
# =========================================================
# ARG PARSING
# =========================================================
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--rom) ROM="$2"; shift 2 ;;
--device) DEVICE="$2"; shift 2 ;;
--workdir) WORKDIR="$2"; shift 2 ;;
--manifest-url) MANIFEST_URL="$2"; shift 2 ;;
--manifest-branch) MANIFEST_BRANCH="$2"; shift 2 ;;
--local-manifest) LOCAL_MANIFEST_REPO="$2"; shift 2 ;;
--local-manifest-branch) LOCAL_MANIFEST_BRANCH="$2"; shift 2 ;;
--token) TG_TOKEN="$2"; shift 2 ;;
--chat-id) TG_CHAT_ID="$2"; shift 2 ;;
--build-username) export BUILD_USERNAME="$2"; shift 2 ;;
--build-hostname) export BUILD_HOSTNAME="$2"; shift 2 ;;
--axion-variant) AXION_VARIANT="$2"; shift 2 ;;
--axion-build-type) AXION_BUILD_TYPE="$2"; shift 2 ;;
--lineage-gapps-branch) LINEAGE_GAPPS_BRANCH="$2"; shift 2 ;;
--skip-init) DO_INIT=0; shift ;;
--skip-sync) DO_SYNC=0; shift ;;
--skip-p2r) P2R_ENABLED=0; UPLOAD_RECOVERY=0; shift ;;
--skip-upload-rom) UPLOAD_ROM=0; shift ;;
--skip-upload-recovery) UPLOAD_RECOVERY=0; shift ;;
--skip-keys) GENERATE_KEYS=0; shift ;;
--no-telegram) ENABLE_TELEGRAM=0; shift ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1" ;;
esac
done
}
set_rom_defaults() {
[[ -n "$ROM" ]] || die "--rom is required"
case "$ROM" in
axion)
MANIFEST_URL="${MANIFEST_URL:-https://github.com/AxionAOSP/android.git}"
MANIFEST_BRANCH="${MANIFEST_BRANCH:-lineage-23.2}"
ROM_FILE_GLOB="axion-*.zip"
;;
lunaris)
MANIFEST_URL="${MANIFEST_URL:-https://github.com/Lunaris-AOSP/android}"
MANIFEST_BRANCH="${MANIFEST_BRANCH:-16.2}"
LUNCH_TARGET="lineage_${DEVICE}-bp4a-user"
ROM_FILE_GLOB="Lunaris-AOSP-${DEVICE}-*.zip"
;;
lineage)
MANIFEST_URL="${MANIFEST_URL:-https://github.com/LineageOS/android.git}"
MANIFEST_BRANCH="${MANIFEST_BRANCH:-lineage-23.2}"
LUNCH_TARGET="lineage_${DEVICE}-bp4a-user"
ROM_FILE_GLOB="lineage-*.zip"
;;
*)
die "Unsupported ROM: ${ROM}"
;;
esac
export BUILD_USERNAME="${BUILD_USERNAME:-$DEFAULT_BUILD_USERNAME}"
export BUILD_HOSTNAME="${BUILD_HOSTNAME:-$DEFAULT_BUILD_HOSTNAME}"
}
# =========================================================
# SYNC / BUILD ENV
# =========================================================
is_crave_env() {
command_exists crave && [[ -x /opt/crave/resync.sh ]]
}
repo_init_if_needed() {
[[ "${DO_INIT}" -eq 1 ]] || return 0
announce "Initializing repo: ${MANIFEST_URL} [${MANIFEST_BRANCH}]"
repo init \
--depth=1 \
--no-repo-verify \
--git-lfs \
-u "$MANIFEST_URL" \
-b "$MANIFEST_BRANCH" \
-g default,-mips,-darwin,-notdefault
}
repo_sync_sources() {
[[ "${DO_SYNC}" -eq 1 ]] || return 0
if is_crave_env; then
announce "Crave detected, syncing with /opt/crave/resync.sh"
/opt/crave/resync.sh
else
announce "Crave not detected, syncing with repo sync"
repo sync \
-c \
--no-clone-bundle \
--no-tags \
--optimized-fetch \
--prune \
--force-sync \
-j"$(thread_count)"
fi
}
repo_setup() {
announce "Using workspace: ${WORKDIR}"
mkdir -p "$WORKDIR"
cd "$WORKDIR"
repo_init_if_needed
announce "Refreshing local manifests"
clone_repo_fresh "$LOCAL_MANIFEST_REPO" ".repo/local_manifests" "$LOCAL_MANIFEST_BRANCH"
if [[ "$ROM" == "lineage" ]]; then
announce "Refreshing vendor/gapps"
clone_repo_fresh "$LINEAGE_GAPPS_REPO" "vendor/gapps" "$LINEAGE_GAPPS_BRANCH"
fi
repo_sync_sources
}
prepare_env() {
announce "Loading build environment"
cd "$WORKDIR"
source build/envsetup.sh
}
generate_keys() {
[[ "${GENERATE_KEYS}" -eq 1 ]] || return 0
announce "Generating signing keys"
cd "$WORKDIR/vendor/lineage-priv/keys"
"$PYTHON_BIN" gen_keys.py
croot
}
# =========================================================
# BUILD STEPS
# =========================================================
build_axion() {
prepare_env
generate_keys
announce "Starting Axion build"
axion "$DEVICE" "$AXION_BUILD_TYPE" "$AXION_VARIANT"
ax -b
}
build_lunaris() {
prepare_env
generate_keys
announce "Starting Lunaris build"
lunch "$LUNCH_TARGET"
m bacon
}
build_lineage() {
prepare_env
generate_keys
announce "Starting Lineage build"
lunch "$LUNCH_TARGET"
m bacon
}
build_rom() {
case "$ROM" in
axion) build_axion ;;
lunaris) build_lunaris ;;
lineage) build_lineage ;;
*) die "No build function for ${ROM}" ;;
esac
}
# =========================================================
# ARTIFACT DISCOVERY
# =========================================================
find_rom_zip() {
local out_dir="$WORKDIR/out/target/product/$DEVICE"
local zip=""
zip="$(find_newest_file "$out_dir" "$ROM_FILE_GLOB")"
if [[ -z "$zip" ]]; then
zip="$(find_newest_rom_zip "$out_dir")"
fi
[[ -n "$zip" && -f "$zip" ]] || die "ROM zip not found in $out_dir"
printf '%s\n' "$zip"
}
generate_recovery_zip() {
local rom_zip="$1"
local out_dir="$WORKDIR/out/target/product/$DEVICE"
local recovery_out_dir="$out_dir/output"
local recovery_zip=""
[[ "${P2R_ENABLED}" -eq 1 ]] || return 1
[[ "${P2R_AVAILABLE}" -eq 1 ]] || return 1
announce "Generating recovery zip with p2r"
rm -rf "$recovery_out_dir"
p2r "$rom_zip"
recovery_zip="$(find_newest_file "$recovery_out_dir" "*-recovery.zip")"
[[ -n "$recovery_zip" && -f "$recovery_zip" ]] || die "Recovery zip not found in $recovery_out_dir"
printf '%s\n' "$recovery_zip"
}
# =========================================================
# UPLOADS
# =========================================================
parse_uploader_output() {
local out="$1"
local pd_url gf_url
pd_url="$(printf '%s\n' "$out" | sed -n 's/^Pixeldrain:[[:space:]]*\(https\?:\/\/[^[:space:]]*\).*$/\1/p' | tail -n 1)"
gf_url="$(printf '%s\n' "$out" | sed -n 's/^GoFile:[[:space:]]*\(https\?:\/\/[^[:space:]]*\).*$/\1/p' | tail -n 1)"
printf '%s|%s\n' "${pd_url:-}" "${gf_url:-}"
}
extract_gofile_url_from_json() {
local payload="$1"
printf '%s' "$payload" | "$PYTHON_BIN" - <<'PY'
import json, sys
def walk(obj):
if isinstance(obj, dict):
for k, v in obj.items():
if isinstance(v, str) and "gofile.io" in v:
print(v)
return True
for v in obj.values():
if walk(v):
return True
elif isinstance(obj, list):
for v in obj:
if walk(v):
return True
return False
raw = sys.stdin.read().strip()
if not raw:
sys.exit(0)
try:
data = json.loads(raw)
except Exception:
sys.exit(0)
if walk(data):
sys.exit(0)
def find_key(obj, name):
if isinstance(obj, dict):
if name in obj and isinstance(obj[name], str) and obj[name]:
return obj[name]
for v in obj.values():
found = find_key(v, name)
if found:
return found
elif isinstance(obj, list):
for v in obj:
found = find_key(v, name)
if found:
return found
return None
folder_id = find_key(data, "folderId") or find_key(data, "parentFolder")
if folder_id:
print(f"https://gofile.io/d/{folder_id}")
PY
}
gofile_upload_curl() {
local file="$1"
local label="$2"
local response url
announce "Uploading ${label} via GoFile curl fallback: $(basename "$file")"
response="$(curl -fsS -F "file=@${file}" "https://upload.gofile.io/uploadfile" || true)"
[[ -n "$response" ]] || return 1
url="$(extract_gofile_url_from_json "$response" || true)"
if [[ -z "$url" ]]; then
url="$(printf '%s\n' "$response" | grep -Eo 'https://gofile\.io/[A-Za-z0-9/_-]+' | head -n 1 || true)"
fi
[[ -n "$url" ]] || return 1
printf '|%s\n' "$url"
}
upload_file() {
local file="$1"
local label="$2"
local result out
if [[ "${UPLOADER_AVAILABLE}" -eq 1 ]]; then
announce "Uploading ${label} with uploader: $(basename "$file")"
out="$(uploader "$file" 2>&1 || true)"
printf '%s\n' "$out" >&2
result="$(parse_uploader_output "$out")"
if [[ -n "${result##|}" ]]; then
printf '%s\n' "$result"
return 0
fi
warn "uploader did not return links for ${label}; trying GoFile curl fallback"
fi
result="$(gofile_upload_curl "$file" "$label" || true)"
[[ -n "$result" ]] || return 1
printf '%s\n' "$result"
}
# =========================================================
# MESSAGE COMPOSER
# =========================================================
compose_success_message() {
local rom_zip="$1"
local rom_pd="${2:-}"
local rom_gf="${3:-}"
local recovery_zip="${4:-}"
local rec_pd="${5:-}"
local rec_gf="${6:-}"
local end_time elapsed duration
end_time="$(date +%s)"
elapsed="$((end_time - START_TIME))"
duration="$((elapsed / 60)) minute(s) and $((elapsed % 60)) second(s)"
local msg="${ROM} build for ${DEVICE} completed
ROM: $(basename "$rom_zip")"
[[ -n "$rom_pd" ]] && msg="${msg}
ROM Pixeldrain: $rom_pd"
[[ -n "$rom_gf" ]] && msg="${msg}
ROM GoFile: $rom_gf"
if [[ -n "$recovery_zip" ]]; then
msg="${msg}
Recovery: $(basename "$recovery_zip")"
[[ -n "$rec_pd" ]] && msg="${msg}
Recovery Pixeldrain: $rec_pd"
[[ -n "$rec_gf" ]] && msg="${msg}
Recovery GoFile: $rec_gf"
fi
msg="${msg}
Build time: $duration"
printf '%s\n' "$msg"
}
# =========================================================
# MAIN
# =========================================================
main() {
parse_args "$@"
set_rom_defaults
ensure_core_requirements
prepare_optional_tools
announce "Starting ${ROM} build for ${DEVICE}"
repo_setup
build_rom
announce "Searching for output files"
local rom_zip recovery_zip upload_result
local rom_pd="" rom_gf="" rec_pd="" rec_gf=""
rom_zip="$(find_rom_zip)"
announce "ROM zip found: $(basename "$rom_zip")"
recovery_zip=""
if [[ "${P2R_ENABLED}" -eq 1 && "${P2R_AVAILABLE}" -eq 1 ]]; then
recovery_zip="$(generate_recovery_zip "$rom_zip")"
announce "Recovery zip found: $(basename "$recovery_zip")"
else
announce "Recovery generation skipped"
fi
if [[ "${UPLOAD_ROM}" -eq 1 ]]; then
upload_result="$(upload_file "$rom_zip" "ROM" || true)"
rom_pd="${upload_result%%|*}"
rom_gf="${upload_result#*|}"
if [[ -n "$rom_pd" || -n "$rom_gf" ]]; then
announce "ROM upload finished"
else
warn "ROM upload failed"
fi
fi
if [[ "${UPLOAD_RECOVERY}" -eq 1 && -n "$recovery_zip" ]]; then
upload_result="$(upload_file "$recovery_zip" "Recovery" || true)"
rec_pd="${upload_result%%|*}"
rec_gf="${upload_result#*|}"
if [[ -n "$rec_pd" || -n "$rec_gf" ]]; then
announce "Recovery upload finished"
else
warn "Recovery upload failed"
fi
fi
local success_msg
success_msg="$(compose_success_message "$rom_zip" "$rom_pd" "$rom_gf" "$recovery_zip" "$rec_pd" "$rec_gf")"
send_telegram "$success_msg"
log "Build finished successfully"
printf '%s\n' "$success_msg"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment