prepare-openwrt-env.sh - Dispatch on the package manager instead of guessing from the distro version: apt, dnf/yum, pacman, zypper and apk are all supported. - Probe every package against the running release and skip the ones that no longer exist. Previously a single dropped package (on Ubuntu 26.04 arm64, gcc-multilib-s390x-linux-gnu) failed the whole apt command and set -e aborted the script, so nothing was installed at all. - Align the package lists with the official build system guide, adding libelf-dev, python3-dev, bc, xsltproc, zstd, u-boot-tools and others. - Make Go opt-in via --with-go rather than forced on arm64. - Add -y/--yes, -n/--dry-run and -h/--help; allow running as root. prepare-openwrt.sh - Split read-only discovery from mutation. Versions are resolved over git ls-remote before anything is moved, so aborting at the prompt leaves the helper checkout untouched. - Resume an interrupted first run instead of failing forever: a root with .git but no source tree was previously unrecoverable without a manual rm -rf. Also restore the worktree when HEAD already points at the target ref, where checkout is a no-op. - Detect the relocated layout by content rather than directory name, so renaming the root no longer triggers a second, nested relocation. - Replace the staged in-place relocation with a resumable content move, removing the window that could strand the checkout in a hidden dir. - Filter release candidates before sorting; sort -V orders v25.12.0-rc1 after v25.12.0, so the newest stable tag could be an rc. - Follow upstream: snapshots use main (master as fallback) and the dead -SNAPSHOT tag lookup is gone. - Use a partial clone (--filter=blob:none) by default. - Guard version switches on a dirty tree and back up .config first. - Add /helper/ to .git/info/exclude and warn that git clean -xdff still deletes it, since -x bypasses ignore rules by design. - Leave a symlink at the old checkout path so shells and editors whose working directory still points there keep working. - Add --stable/--branch/--snapshot plus -y, -n, --force, --full-clone, --allow-rc, --root, --skip-feeds, --skip-defconfig, --repo-url and --no-compat-link. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1025 lines
31 KiB
Bash
Executable File
1025 lines
31 KiB
Bash
Executable File
#!/bin/bash
|
||
# Prepare an OpenWrt build tree.
|
||
#
|
||
# Layout produced by this script:
|
||
# before <parent>/openwrt-build-helper/... this helper checkout
|
||
# first run <parent>/openwrt/ the OpenWrt source root
|
||
# <parent>/openwrt/helper/... this checkout, moved in
|
||
# later runs the same <parent>/openwrt/ is updated in place
|
||
#
|
||
# Nothing on disk is touched before the plan is printed and confirmed, so
|
||
# aborting at the prompt leaves the helper checkout exactly where it was.
|
||
#
|
||
# Usage: ./prepare-openwrt.sh [options]
|
||
#
|
||
# Version selection (mutually exclusive; omit all for an interactive menu):
|
||
# --stable [<ver>] stable release tag, e.g. 25.12.5 or v25.12.5
|
||
# without a value: newest stable tag (rc tags excluded)
|
||
# --branch <name> release branch: 24.10, openwrt-24.10, main, ...
|
||
# --snapshot newest snapshot (the 'main' branch)
|
||
#
|
||
# Behaviour:
|
||
# -y, --yes non-interactive, accept defaults, never prompt
|
||
# -n, --dry-run print the plan and every action, change nothing
|
||
# -f, --force override the safety guards (dirty tree, stray files)
|
||
# --full-clone fetch full history (default: --filter=blob:none)
|
||
# --allow-rc let --stable with no value pick a release candidate
|
||
# --root <dir> use <dir> as the OpenWrt root instead of <parent>/openwrt
|
||
# --skip-feeds do not run scripts/feeds update/install
|
||
# --skip-defconfig do not run make defconfig
|
||
# --repo-url <url> override the upstream URL (advanced / testing)
|
||
# --no-compat-link do not leave a symlink at the old checkout path
|
||
# -h, --help show this help
|
||
#
|
||
# After the first run the checkout lives at <parent>/openwrt/helper. A symlink
|
||
# is left behind at the original path so that shells, editors and agents whose
|
||
# working directory still points at <parent>/openwrt-build-helper keep working.
|
||
#
|
||
# Note: with the default partial clone, switching to another tag or branch
|
||
# later fetches the missing blobs on demand and therefore needs network.
|
||
#
|
||
# Author: Zhe Yuan
|
||
|
||
set -euo pipefail
|
||
umask 022
|
||
|
||
# --- Configuration ---
|
||
REPO_URL="https://github.com/openwrt/openwrt.git"
|
||
SNAPSHOT_BRANCH="main"
|
||
SNAPSHOT_FALLBACK="master"
|
||
|
||
# --- Options ---
|
||
VERSION_MODE=""
|
||
VERSION_INPUT=""
|
||
ASSUME_YES=false
|
||
DRY_RUN=false
|
||
FORCE=false
|
||
FULL_CLONE=false
|
||
ALLOW_RC=false
|
||
ROOT_OVERRIDE=""
|
||
SKIP_FEEDS=false
|
||
SKIP_DEFCONFIG=false
|
||
COMPAT_LINK=true
|
||
|
||
# --- Discovered state ---
|
||
SCRIPT_INVOKED_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||
# Physical path: layout decisions must see through a compatibility symlink,
|
||
# otherwise re-running through the old path would relocate an already
|
||
# relocated checkout.
|
||
SCRIPT_DIR="$(cd -- "$SCRIPT_INVOKED_DIR" && pwd -P)"
|
||
COMPAT_LINK_PATH=""
|
||
WORK_ROOT=""
|
||
HELPER_DIR=""
|
||
RELOCATE_MODE="none" # none | move | inplace
|
||
ROOT_STATE=""
|
||
TARGET_KIND="" # tag | branch
|
||
TARGET_REF=""
|
||
CURRENT_REF=""
|
||
REMOTE_TAGS=""
|
||
REMOTE_HEADS=""
|
||
LOCK_DIR=""
|
||
CONFIG_BACKUP=""
|
||
|
||
# --- Output helpers ---
|
||
info() { echo "[INFO] $*"; }
|
||
warn() { echo "[WARN] $*"; }
|
||
err() { echo "[ERROR] $*" >&2; }
|
||
|
||
# Wrap every mutating command so that --dry-run prints instead of running.
|
||
run() {
|
||
if [ "$DRY_RUN" = true ]; then
|
||
printf '[DRY-RUN]'
|
||
printf ' %q' "$@"
|
||
echo
|
||
return 0
|
||
fi
|
||
"$@"
|
||
}
|
||
|
||
usage() {
|
||
sed -n '2,42p' "$0" | sed 's/^# \{0,1\}//;s/^#$//'
|
||
}
|
||
|
||
set_version_mode() {
|
||
if [ -n "$VERSION_MODE" ]; then
|
||
err "--stable, --branch and --snapshot are mutually exclusive."
|
||
exit 1
|
||
fi
|
||
VERSION_MODE="$1"
|
||
}
|
||
|
||
# --- Argument parsing ---
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--stable)
|
||
set_version_mode stable
|
||
# Optional value: only consume $2 when it is not another option.
|
||
if [ $# -gt 1 ] && [ -n "${2:-}" ] && [ "${2#-}" = "$2" ]; then
|
||
VERSION_INPUT="$2"
|
||
shift
|
||
fi
|
||
;;
|
||
--branch)
|
||
set_version_mode branch
|
||
if [ $# -lt 2 ] || [ -z "${2:-}" ]; then
|
||
err "--branch requires a branch name."
|
||
exit 1
|
||
fi
|
||
VERSION_INPUT="$2"
|
||
shift
|
||
;;
|
||
--snapshot) set_version_mode snapshot ;;
|
||
-y|--yes) ASSUME_YES=true ;;
|
||
-n|--dry-run) DRY_RUN=true ;;
|
||
-f|--force) FORCE=true ;;
|
||
--full-clone) FULL_CLONE=true ;;
|
||
--allow-rc) ALLOW_RC=true ;;
|
||
--skip-feeds) SKIP_FEEDS=true ;;
|
||
--skip-defconfig) SKIP_DEFCONFIG=true ;;
|
||
--no-compat-link) COMPAT_LINK=false ;;
|
||
--root)
|
||
if [ $# -lt 2 ] || [ -z "${2:-}" ]; then
|
||
err "--root requires a directory."
|
||
exit 1
|
||
fi
|
||
ROOT_OVERRIDE="$2"
|
||
shift
|
||
;;
|
||
--repo-url)
|
||
if [ $# -lt 2 ] || [ -z "${2:-}" ]; then
|
||
err "--repo-url requires a URL."
|
||
exit 1
|
||
fi
|
||
REPO_URL="$2"
|
||
shift
|
||
;;
|
||
-h|--help) usage; exit 0 ;;
|
||
*)
|
||
err "Unknown option: $1"
|
||
echo "Run '$0 --help' for usage." >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
# A closed stdin must not make `read` spin or hang.
|
||
if [ ! -t 0 ]; then
|
||
ASSUME_YES=true
|
||
fi
|
||
|
||
echo "========================================"
|
||
echo " OpenWrt Source Preparation"
|
||
echo "========================================"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Generic helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# Resolve a directory to its physical path, returning non-zero instead of
|
||
# letting a bare `cd` abort the whole script under `set -e`.
|
||
abspath() {
|
||
local p="$1"
|
||
[ -d "$p" ] || return 1
|
||
( cd -- "$p" 2>/dev/null && pwd -P ) || return 1
|
||
}
|
||
|
||
is_git_root() {
|
||
local d="$1" top resolved
|
||
[ -e "$d/.git" ] || return 1
|
||
top="$(git -C "$d" rev-parse --show-toplevel 2>/dev/null)" || return 1
|
||
resolved="$(abspath "$d")" || return 1
|
||
[ "$top" = "$resolved" ]
|
||
}
|
||
|
||
# Content-only check: does not require the directory to be a git repository.
|
||
is_openwrt_tree() {
|
||
[ -f "$1/feeds.conf.default" ] && [ -f "$1/Makefile" ] && [ -d "$1/scripts" ]
|
||
}
|
||
|
||
is_openwrt_repo() {
|
||
is_git_root "$1" && is_openwrt_tree "$1"
|
||
}
|
||
|
||
has_openwrt_origin() {
|
||
local u
|
||
u="$(git -C "$1" remote get-url origin 2>/dev/null)" || return 1
|
||
case "$u" in
|
||
*openwrt/openwrt*|"$REPO_URL") return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
is_helper_checkout() {
|
||
local d="$1"
|
||
|
||
[ -f "$d/prepare-openwrt.sh" ] || return 1
|
||
|
||
# Complete checkout.
|
||
if [ -f "$d/prepare-openwrt-env.sh" ] && [ -f "$d/add-external-repos.sh" ]; then
|
||
return 0
|
||
fi
|
||
|
||
# Half-finished in-place move: some scripts already sit in helper/, so the
|
||
# full signature is gone. Recognizing this is what makes the move resumable.
|
||
if [ -d "$d/helper" ] &&
|
||
{ [ -f "$d/helper/prepare-openwrt-env.sh" ] ||
|
||
[ -f "$d/helper/add-external-repos.sh" ]; }; then
|
||
return 0
|
||
fi
|
||
|
||
[ -f "$d/prepare-openwrt-env.sh" ] || [ -f "$d/add-external-repos.sh" ]
|
||
}
|
||
|
||
require_sort_v() {
|
||
if [ "$(printf 'v1.10\nv1.9\n' | sort -V | head -n 1)" != "v1.9" ]; then
|
||
err "This script needs GNU 'sort -V' for version ordering."
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
confirm() {
|
||
# confirm "<question>" <default: yes|no>
|
||
local question="$1" default="$2" reply=""
|
||
|
||
if [ "$ASSUME_YES" = true ]; then
|
||
return 0
|
||
fi
|
||
|
||
if [ "$default" = "yes" ]; then
|
||
read -r -p "$question [Y/n]: " reply || reply=""
|
||
case "${reply:-}" in
|
||
[nN]|[nN][oO]) return 1 ;;
|
||
*) return 0 ;;
|
||
esac
|
||
else
|
||
read -r -p "$question [y/N]: " reply || reply=""
|
||
case "${reply:-}" in
|
||
[yY]|[yY][eE][sS]) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
fi
|
||
}
|
||
|
||
release_lock() {
|
||
if [ -n "$LOCK_DIR" ] && [ -d "$LOCK_DIR" ]; then
|
||
rmdir "$LOCK_DIR" 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
acquire_lock() {
|
||
[ "$DRY_RUN" = true ] && return 0
|
||
LOCK_DIR="$WORK_ROOT/.prepare-openwrt.lock"
|
||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||
LOCK_DIR=""
|
||
err "Another prepare-openwrt.sh seems to be running in '$WORK_ROOT'."
|
||
err "If that is wrong, remove '$WORK_ROOT/.prepare-openwrt.lock'."
|
||
exit 1
|
||
fi
|
||
trap release_lock EXIT
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase A: layout discovery (read-only)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# Echoes one of:
|
||
# absent empty openwrt-complete openwrt-partial foreign-git nonempty
|
||
# notdir unreadable
|
||
classify_root() {
|
||
local d="$1" entry name
|
||
|
||
[ -e "$d" ] || { echo absent; return; }
|
||
[ -d "$d" ] || { echo notdir; return; }
|
||
{ [ -r "$d" ] && [ -x "$d" ]; } || { echo unreadable; return; }
|
||
|
||
if is_openwrt_repo "$d"; then echo openwrt-complete; return; fi
|
||
|
||
if is_git_root "$d"; then
|
||
if has_openwrt_origin "$d"; then
|
||
echo openwrt-partial
|
||
else
|
||
echo foreign-git
|
||
fi
|
||
return
|
||
fi
|
||
|
||
# Any real content other than the helper checkout and well-known junk?
|
||
while IFS= read -r entry; do
|
||
name="$(basename "$entry")"
|
||
case "$name" in
|
||
helper|.DS_Store|Thumbs.db|.directory|.prepare-openwrt.lock) continue ;;
|
||
*) echo nonempty; return ;;
|
||
esac
|
||
done < <(find "$d" -mindepth 1 -maxdepth 1)
|
||
|
||
echo empty
|
||
}
|
||
|
||
list_root_clutter() {
|
||
local d="$1" entry name
|
||
while IFS= read -r entry; do
|
||
name="$(basename "$entry")"
|
||
case "$name" in
|
||
helper|.DS_Store|Thumbs.db|.directory|.prepare-openwrt.lock) continue ;;
|
||
*) echo " $name" ;;
|
||
esac
|
||
done < <(find "$d" -mindepth 1 -maxdepth 1)
|
||
}
|
||
|
||
check_helper_target() {
|
||
local t="$HELPER_DIR" existing source
|
||
|
||
[ -e "$t" ] || return 0
|
||
|
||
if [ ! -d "$t" ]; then
|
||
err "Helper destination '$t' exists but is not a directory."
|
||
err "Move or remove it, then re-run."
|
||
exit 1
|
||
fi
|
||
|
||
existing="$(abspath "$t")" || { err "Cannot resolve '$t'."; exit 1; }
|
||
source="$(abspath "$SCRIPT_DIR")" || { err "Cannot resolve '$SCRIPT_DIR'."; exit 1; }
|
||
[ "$existing" = "$source" ] && return 0
|
||
|
||
if is_helper_checkout "$t"; then
|
||
err "A different helper checkout already exists at '$t'."
|
||
else
|
||
err "'$t' exists and is not a helper checkout."
|
||
fi
|
||
err "Move or remove it, then re-run."
|
||
exit 1
|
||
}
|
||
|
||
discover_layout() {
|
||
local parent
|
||
parent="$(dirname -- "$SCRIPT_DIR")"
|
||
|
||
if [ -n "$ROOT_OVERRIDE" ]; then
|
||
# Escape hatch for every ambiguous case.
|
||
if [ -d "$ROOT_OVERRIDE" ]; then
|
||
WORK_ROOT="$(abspath "$ROOT_OVERRIDE")" ||
|
||
{ err "Cannot resolve --root '$ROOT_OVERRIDE'."; exit 1; }
|
||
else
|
||
case "$ROOT_OVERRIDE" in
|
||
/*) WORK_ROOT="$ROOT_OVERRIDE" ;;
|
||
*) WORK_ROOT="$PWD/$ROOT_OVERRIDE" ;;
|
||
esac
|
||
fi
|
||
|
||
if [ "$SCRIPT_DIR" = "$WORK_ROOT/helper" ] || ! is_helper_checkout "$SCRIPT_DIR"; then
|
||
HELPER_DIR="$WORK_ROOT/helper"
|
||
RELOCATE_MODE="none"
|
||
elif [ "$SCRIPT_DIR" = "$WORK_ROOT" ]; then
|
||
HELPER_DIR="$WORK_ROOT/helper"
|
||
RELOCATE_MODE="inplace"
|
||
else
|
||
HELPER_DIR="$WORK_ROOT/helper"
|
||
RELOCATE_MODE="move"
|
||
fi
|
||
|
||
elif is_helper_checkout "$SCRIPT_DIR" && root_is_parent "$parent"; then
|
||
# Already normalized. Detected by CONTENT first, so renaming the parent
|
||
# to e.g. openwrt-24.10 does not trigger a second relocation.
|
||
WORK_ROOT="$parent"
|
||
HELPER_DIR="$SCRIPT_DIR"
|
||
RELOCATE_MODE="none"
|
||
info "Reusing the existing layout at '$WORK_ROOT'."
|
||
|
||
elif is_openwrt_repo "$SCRIPT_DIR"; then
|
||
WORK_ROOT="$SCRIPT_DIR"
|
||
HELPER_DIR="$SCRIPT_DIR/helper"
|
||
RELOCATE_MODE="none"
|
||
warn "Running from inside an OpenWrt source root; not relocating anything."
|
||
|
||
elif is_helper_checkout "$SCRIPT_DIR"; then
|
||
WORK_ROOT="$parent/openwrt"
|
||
HELPER_DIR="$WORK_ROOT/helper"
|
||
if [ "$SCRIPT_DIR" = "$WORK_ROOT" ]; then
|
||
RELOCATE_MODE="inplace"
|
||
else
|
||
RELOCATE_MODE="move"
|
||
fi
|
||
|
||
else
|
||
err "'$SCRIPT_DIR' does not look like a helper checkout."
|
||
err "Refusing to relocate an unexpected directory. Use --root to override."
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$RELOCATE_MODE" = "inplace" ]; then
|
||
# The root currently IS the helper checkout (its own git repo included).
|
||
# Classifying it would reject it as a foreign repository; after the
|
||
# contents move down into helper/ the root is empty by construction.
|
||
ROOT_STATE="inplace-helper"
|
||
else
|
||
ROOT_STATE="$(classify_root "$WORK_ROOT")"
|
||
fi
|
||
|
||
case "$ROOT_STATE" in
|
||
notdir)
|
||
err "OpenWrt root '$WORK_ROOT' exists but is not a directory."
|
||
exit 1
|
||
;;
|
||
unreadable)
|
||
err "OpenWrt root '$WORK_ROOT' exists but cannot be read (permissions)."
|
||
exit 1
|
||
;;
|
||
foreign-git)
|
||
err "'$WORK_ROOT' is a Git repository, but its origin is not OpenWrt."
|
||
err "Choose another root with --root, or move that repository away."
|
||
exit 1
|
||
;;
|
||
openwrt-partial)
|
||
warn "'$WORK_ROOT' has a Git repository but no source tree."
|
||
warn "A previous run was interrupted; resuming it."
|
||
;;
|
||
nonempty)
|
||
err "OpenWrt root '$WORK_ROOT' contains unexpected files:"
|
||
list_root_clutter "$WORK_ROOT" >&2
|
||
if [ "$FORCE" != true ]; then
|
||
err "Refusing to use it. Re-run with --force to proceed anyway."
|
||
exit 1
|
||
fi
|
||
warn "--force given; proceeding despite the files above."
|
||
;;
|
||
esac
|
||
|
||
case "$RELOCATE_MODE" in
|
||
move)
|
||
check_helper_target
|
||
;;
|
||
inplace)
|
||
# helper/ is created by the move itself and may be partially populated
|
||
# from an interrupted run, so only rule out a non-directory here.
|
||
if [ -e "$HELPER_DIR" ] && [ ! -d "$HELPER_DIR" ]; then
|
||
err "Helper destination '$HELPER_DIR' exists but is not a directory."
|
||
exit 1
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
# A repo created inside another worktree confuses the outer repository.
|
||
local outer=""
|
||
if outer="$(git -C "$(dirname -- "$WORK_ROOT")" rev-parse --show-toplevel 2>/dev/null)"; then
|
||
if [ -n "$outer" ] && [ "$outer" != "$WORK_ROOT" ]; then
|
||
warn "'$WORK_ROOT' will live inside the Git worktree at '$outer'."
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# True when $1 is already the OpenWrt root for this helper checkout.
|
||
root_is_parent() {
|
||
local p="$1"
|
||
is_openwrt_repo "$p" ||
|
||
has_openwrt_origin "$p" ||
|
||
is_openwrt_tree "$p" ||
|
||
[ "$(basename -- "$p")" = "openwrt" ]
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase B: remote ref discovery (read-only, network, no local repo needed)
|
||
# ---------------------------------------------------------------------------
|
||
discover_remote_refs() {
|
||
info "Reading refs from $REPO_URL ..."
|
||
|
||
if ! REMOTE_TAGS="$(git ls-remote --tags --refs "$REPO_URL" 'v*' 2>&1 |
|
||
awk '{print $2}' | sed 's#refs/tags/##')"; then
|
||
err "Cannot read tags from '$REPO_URL'."
|
||
err "$REMOTE_TAGS"
|
||
exit 1
|
||
fi
|
||
|
||
if ! REMOTE_HEADS="$(git ls-remote --heads "$REPO_URL" 2>&1 |
|
||
awk '{print $2}' | sed 's#refs/heads/##')"; then
|
||
err "Cannot read branches from '$REPO_URL'."
|
||
err "$REMOTE_HEADS"
|
||
exit 1
|
||
fi
|
||
|
||
if [ -z "$REMOTE_HEADS" ]; then
|
||
err "'$REPO_URL' reports no branches; is the URL correct?"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
remote_has_head() {
|
||
printf '%s\n' "$REMOTE_HEADS" | grep -qxF "$1"
|
||
}
|
||
|
||
remote_has_tag() {
|
||
printf '%s\n' "$REMOTE_TAGS" | grep -qxF "$1"
|
||
}
|
||
|
||
latest_stable_tag() {
|
||
local pattern='^v[0-9]+\.[0-9]+\.[0-9]+$'
|
||
[ "$ALLOW_RC" = true ] && pattern='^v[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$'
|
||
# Filter release candidates BEFORE sorting: `sort -V` orders v25.12.0-rc1
|
||
# after v25.12.0, so filtering afterwards would still let an rc win.
|
||
printf '%s\n' "$REMOTE_TAGS" | grep -E "$pattern" | sort -V | tail -n 1
|
||
}
|
||
|
||
list_release_branches() {
|
||
printf '%s\n' "$REMOTE_HEADS" | grep -E '^openwrt-[0-9]+\.[0-9]+$' | sort -V
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase C: version selection and ref resolution
|
||
# ---------------------------------------------------------------------------
|
||
choose_version_interactive() {
|
||
local choice="" newest branches
|
||
|
||
newest="$(latest_stable_tag)"
|
||
branches="$(list_release_branches | tr '\n' ' ')"
|
||
|
||
echo
|
||
echo "Select the OpenWrt version to build:"
|
||
echo " 1) Stable release (newest: ${newest:-none found})"
|
||
echo " 2) Release branch (${branches:-none found}main)"
|
||
echo " 3) Snapshot (the '$SNAPSHOT_BRANCH' branch)"
|
||
read -r -p "Enter choice [1-3, default=1]: " choice || choice=""
|
||
|
||
case "${choice:-}" in
|
||
2)
|
||
VERSION_MODE="branch"
|
||
read -r -p "Enter branch (e.g. 25.12 or openwrt-25.12): " VERSION_INPUT || VERSION_INPUT=""
|
||
if [ -z "${VERSION_INPUT:-}" ]; then
|
||
err "No branch given."
|
||
exit 1
|
||
fi
|
||
;;
|
||
3)
|
||
VERSION_MODE="snapshot"
|
||
;;
|
||
*)
|
||
VERSION_MODE="stable"
|
||
read -r -p "Enter version (e.g. 25.12.5, empty for newest): " VERSION_INPUT || VERSION_INPUT=""
|
||
;;
|
||
esac
|
||
}
|
||
|
||
resolve_target_ref() {
|
||
local v candidate
|
||
|
||
case "$VERSION_MODE" in
|
||
snapshot)
|
||
TARGET_KIND="branch"
|
||
if remote_has_head "$SNAPSHOT_BRANCH"; then
|
||
TARGET_REF="$SNAPSHOT_BRANCH"
|
||
elif remote_has_head "$SNAPSHOT_FALLBACK"; then
|
||
warn "Branch '$SNAPSHOT_BRANCH' not found; using '$SNAPSHOT_FALLBACK'."
|
||
TARGET_REF="$SNAPSHOT_FALLBACK"
|
||
else
|
||
err "Neither '$SNAPSHOT_BRANCH' nor '$SNAPSHOT_FALLBACK' exists upstream."
|
||
exit 1
|
||
fi
|
||
;;
|
||
|
||
branch)
|
||
TARGET_KIND="branch"
|
||
v="$VERSION_INPUT"
|
||
case "$v" in
|
||
main|master|openwrt-*) candidate="$v" ;;
|
||
[0-9]*) candidate="openwrt-$v" ;;
|
||
*) candidate="$v" ;;
|
||
esac
|
||
|
||
if remote_has_head "$candidate"; then
|
||
TARGET_REF="$candidate"
|
||
elif remote_has_head "openwrt-$v"; then
|
||
TARGET_REF="openwrt-$v"
|
||
else
|
||
err "Branch not found upstream for '$v'."
|
||
err "Available release branches:"
|
||
list_release_branches | sed 's/^/ /' >&2
|
||
err " main"
|
||
exit 1
|
||
fi
|
||
;;
|
||
|
||
stable)
|
||
TARGET_KIND="tag"
|
||
if [ -z "${VERSION_INPUT:-}" ]; then
|
||
TARGET_REF="$(latest_stable_tag)"
|
||
if [ -z "$TARGET_REF" ]; then
|
||
err "No stable tag found upstream."
|
||
exit 1
|
||
fi
|
||
info "No version given; using the newest stable tag: $TARGET_REF"
|
||
else
|
||
v="$VERSION_INPUT"
|
||
case "$v" in v*) ;; *) v="v$v" ;; esac
|
||
if ! remote_has_tag "$v"; then
|
||
err "Version tag '$v' not found upstream."
|
||
err "Recent stable tags:"
|
||
printf '%s\n' "$REMOTE_TAGS" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' |
|
||
sort -V | tail -n 5 | sed 's/^/ /' >&2
|
||
exit 1
|
||
fi
|
||
TARGET_REF="$v"
|
||
fi
|
||
;;
|
||
|
||
*)
|
||
err "Internal error: no version mode selected."
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase C': switch safety (read-only)
|
||
# ---------------------------------------------------------------------------
|
||
current_ref_of() {
|
||
local d="$1"
|
||
git -C "$d" describe --tags --exact-match 2>/dev/null ||
|
||
git -C "$d" symbolic-ref --short HEAD 2>/dev/null ||
|
||
git -C "$d" rev-parse --short HEAD 2>/dev/null ||
|
||
true
|
||
}
|
||
|
||
has_build_state() {
|
||
local d="$1" p
|
||
for p in .config build_dir staging_dir bin tmp feeds; do
|
||
[ -e "$d/$p" ] && return 0
|
||
done
|
||
return 1
|
||
}
|
||
|
||
check_switch_safety() {
|
||
local dirty=""
|
||
|
||
is_git_root "$WORK_ROOT" || return 0
|
||
|
||
CURRENT_REF="$(current_ref_of "$WORK_ROOT")"
|
||
|
||
# Same ref means no switch, hence no risk. This is what keeps the guard
|
||
# from firing on every routine re-run.
|
||
[ -n "$CURRENT_REF" ] && [ "$CURRENT_REF" = "$TARGET_REF" ] && return 0
|
||
is_openwrt_tree "$WORK_ROOT" || return 0
|
||
|
||
# Untracked files are excluded on purpose: a built OpenWrt tree always has
|
||
# feeds/, build_dir/, staging_dir/, bin/, .config and helper/ untracked.
|
||
dirty="$(git -C "$WORK_ROOT" status --porcelain --untracked-files=no 2>/dev/null || true)"
|
||
|
||
if [ -n "$dirty" ]; then
|
||
warn "Tracked files in '$WORK_ROOT' have local modifications:"
|
||
git -C "$WORK_ROOT" status --short --untracked-files=no | sed 's/^/ /'
|
||
if [ "$FORCE" = true ]; then
|
||
warn "--force given; switching anyway."
|
||
elif [ "$ASSUME_YES" = true ]; then
|
||
err "Refusing to switch versions with local modifications under -y."
|
||
err "Commit or stash them, or re-run with --force."
|
||
exit 1
|
||
elif ! confirm "Switching keeps your changes only if they apply cleanly. Continue?" no; then
|
||
info "Aborted by user."
|
||
exit 0
|
||
fi
|
||
elif has_build_state "$WORK_ROOT"; then
|
||
warn "'$WORK_ROOT' contains build state from '$CURRENT_REF'."
|
||
if [ "$ASSUME_YES" != true ] &&
|
||
! confirm "Build state is kept and .config is backed up. Continue?" yes; then
|
||
info "Aborted by user."
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
if [ -f "$WORK_ROOT/.config" ]; then
|
||
CONFIG_BACKUP="$WORK_ROOT/.config.${CURRENT_REF//\//-}.bak"
|
||
fi
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase D: plan and confirmation
|
||
# ---------------------------------------------------------------------------
|
||
print_plan() {
|
||
local root_note helper_note clone_note
|
||
|
||
case "$ROOT_STATE" in
|
||
absent) root_note="(create)" ;;
|
||
empty) root_note="(use empty directory)" ;;
|
||
inplace-helper) root_note="(reuse this directory)" ;;
|
||
openwrt-complete) root_note="(update existing)" ;;
|
||
openwrt-partial) root_note="(resume interrupted run)" ;;
|
||
nonempty) root_note="(forced, contains other files)" ;;
|
||
*) root_note="" ;;
|
||
esac
|
||
|
||
case "$RELOCATE_MODE" in
|
||
none) helper_note="(already in place)" ;;
|
||
move) helper_note="(move from $SCRIPT_DIR)" ;;
|
||
inplace) helper_note="(move contents of $SCRIPT_DIR down one level)" ;;
|
||
esac
|
||
|
||
if [ "$FULL_CLONE" = true ]; then
|
||
clone_note="full history"
|
||
else
|
||
clone_note="partial (--filter=blob:none)"
|
||
fi
|
||
|
||
echo
|
||
echo "========================================"
|
||
echo " Planned actions"
|
||
echo "========================================"
|
||
echo " OpenWrt root : $WORK_ROOT $root_note"
|
||
echo " Helper : $HELPER_DIR $helper_note"
|
||
echo " Version : $TARGET_REF ($TARGET_KIND)"
|
||
echo " Currently at : ${CURRENT_REF:-(new checkout)}"
|
||
echo " Clone mode : $clone_note"
|
||
[ -n "$CONFIG_BACKUP" ] && echo " .config : back up to $(basename "$CONFIG_BACKUP")"
|
||
if [ "$RELOCATE_MODE" = "move" ] && [ "$COMPAT_LINK" = true ]; then
|
||
echo " Old path : $SCRIPT_DIR -> symlink to the new helper location"
|
||
fi
|
||
if [ "$SKIP_FEEDS" = true ]; then
|
||
echo " Feeds : skipped (--skip-feeds)"
|
||
else
|
||
echo " Feeds : update -a, install -a"
|
||
fi
|
||
if [ "$SKIP_DEFCONFIG" = true ]; then
|
||
echo " Post : skipped (--skip-defconfig)"
|
||
else
|
||
echo " Post : make defconfig"
|
||
fi
|
||
echo "========================================"
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase E: mutation
|
||
# ---------------------------------------------------------------------------
|
||
relocate_helper() {
|
||
local entry name
|
||
|
||
case "$RELOCATE_MODE" in
|
||
none)
|
||
return 0
|
||
;;
|
||
|
||
move)
|
||
# Single rename inside one parent directory: atomic, no crash window.
|
||
info "Moving the helper checkout to '$HELPER_DIR'."
|
||
run mkdir -p "$WORK_ROOT"
|
||
run mv -- "$SCRIPT_DIR" "$HELPER_DIR"
|
||
|
||
# Anything still pointing at the old path -- a shell, an editor, an
|
||
# agent session -- would otherwise be left on a dangling directory.
|
||
if [ "$COMPAT_LINK" = true ]; then
|
||
if [ "$DRY_RUN" = true ] || [ ! -e "$SCRIPT_DIR" ]; then
|
||
info "Leaving a compatibility symlink at '$SCRIPT_DIR'."
|
||
run ln -s -- "$HELPER_DIR" "$SCRIPT_DIR"
|
||
COMPAT_LINK_PATH="$SCRIPT_DIR"
|
||
else
|
||
warn "'$SCRIPT_DIR' still exists; skipping the compatibility symlink."
|
||
fi
|
||
fi
|
||
;;
|
||
|
||
inplace)
|
||
# The checkout is itself named 'openwrt'. Move its contents down one
|
||
# level instead of staging through a temporary sibling, so that an
|
||
# interrupt never strands anything outside the root.
|
||
if [ -z "$WORK_ROOT" ] || [ "$WORK_ROOT" = "/" ]; then
|
||
err "Refusing to reorganize '$WORK_ROOT'."
|
||
exit 1
|
||
fi
|
||
info "Moving the checkout contents into '$HELPER_DIR'."
|
||
run mkdir -p "$HELPER_DIR"
|
||
shopt -s dotglob nullglob
|
||
for entry in "$WORK_ROOT"/*; do
|
||
name="$(basename -- "$entry")"
|
||
[ "$name" = "helper" ] && continue
|
||
[ "$name" = ".prepare-openwrt.lock" ] && continue
|
||
if [ -e "$HELPER_DIR/$name" ]; then
|
||
warn "Skipping '$name': already present in helper/."
|
||
continue
|
||
fi
|
||
run mv -- "$entry" "$HELPER_DIR/"
|
||
done
|
||
shopt -u dotglob nullglob
|
||
;;
|
||
esac
|
||
}
|
||
|
||
ensure_origin_remote() {
|
||
local current=""
|
||
if current="$(git -C "$WORK_ROOT" remote get-url origin 2>/dev/null)"; then
|
||
if [ "$current" != "$REPO_URL" ]; then
|
||
info "Pointing remote 'origin' at '$REPO_URL'."
|
||
run git -C "$WORK_ROOT" remote set-url origin "$REPO_URL"
|
||
fi
|
||
else
|
||
run git -C "$WORK_ROOT" remote add origin "$REPO_URL"
|
||
fi
|
||
}
|
||
|
||
supports_partial_clone() {
|
||
local v
|
||
v="$(git version | awk '{print $3}')"
|
||
[ "$(printf '%s\n2.19.0\n' "$v" | sort -V | head -n 1)" = "2.19.0" ]
|
||
}
|
||
|
||
ensure_repo() {
|
||
# Fresh, resumed and complete roots all take this one path, so there is no
|
||
# separate resume branch that can rot.
|
||
if ! is_git_root "$WORK_ROOT"; then
|
||
info "Initializing a Git repository in '$WORK_ROOT'."
|
||
run mkdir -p "$WORK_ROOT"
|
||
run git -C "$WORK_ROOT" init -q
|
||
fi
|
||
|
||
ensure_origin_remote
|
||
|
||
if [ "$FULL_CLONE" != true ] && supports_partial_clone; then
|
||
info "Fetching (partial clone, blobs on demand)..."
|
||
run git -C "$WORK_ROOT" config remote.origin.promisor true
|
||
run git -C "$WORK_ROOT" config remote.origin.partialclonefilter blob:none
|
||
if ! run git -C "$WORK_ROOT" fetch --tags --prune --prune-tags \
|
||
--filter=blob:none origin; then
|
||
warn "Partial fetch failed; retrying with a full fetch."
|
||
run git -C "$WORK_ROOT" config --unset remote.origin.promisor || true
|
||
run git -C "$WORK_ROOT" config --unset remote.origin.partialclonefilter || true
|
||
run git -C "$WORK_ROOT" fetch --tags --prune --prune-tags origin
|
||
fi
|
||
else
|
||
if [ "$FULL_CLONE" != true ]; then
|
||
warn "git $(git version | awk '{print $3}') has no partial clone; fetching everything."
|
||
fi
|
||
info "Fetching (full history)..."
|
||
run git -C "$WORK_ROOT" fetch --tags --prune --prune-tags origin
|
||
fi
|
||
}
|
||
|
||
ensure_helper_excluded() {
|
||
local gitdir ex
|
||
|
||
[ "$DRY_RUN" = true ] && { echo "[DRY-RUN] append '/helper/' to .git/info/exclude"; return 0; }
|
||
|
||
gitdir="$(git -C "$WORK_ROOT" rev-parse --git-dir)"
|
||
case "$gitdir" in
|
||
/*) ;;
|
||
*) gitdir="$WORK_ROOT/$gitdir" ;;
|
||
esac
|
||
ex="$gitdir/info/exclude"
|
||
|
||
if [ -f "$ex" ] && grep -qxF '/helper/' "$ex"; then
|
||
return 0
|
||
fi
|
||
|
||
info "Marking /helper/ as ignored in $ex"
|
||
mkdir -p "$gitdir/info"
|
||
printf '\n# Added by prepare-openwrt.sh: the build-helper checkout lives here.\n/helper/\n' >> "$ex"
|
||
}
|
||
|
||
checkout_target() {
|
||
if [ -n "$CONFIG_BACKUP" ]; then
|
||
info "Backing up .config to $(basename "$CONFIG_BACKUP")"
|
||
run cp -a "$WORK_ROOT/.config" "$CONFIG_BACKUP"
|
||
fi
|
||
|
||
# One-line insurance in case upstream ever ships a top-level 'helper' path.
|
||
if [ "$DRY_RUN" != true ] &&
|
||
[ -n "$(git -C "$WORK_ROOT" ls-tree --name-only "$TARGET_REF" helper 2>/dev/null)" ]; then
|
||
err "The target ref '$TARGET_REF' contains a tracked 'helper' path."
|
||
err "It would collide with the helper checkout. Use --root for a different layout."
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$TARGET_KIND" = "tag" ]; then
|
||
info "Checking out tag $TARGET_REF ..."
|
||
run git -C "$WORK_ROOT" checkout --detach "refs/tags/$TARGET_REF"
|
||
else
|
||
info "Checking out branch $TARGET_REF ..."
|
||
if git -C "$WORK_ROOT" show-ref --verify --quiet "refs/heads/$TARGET_REF"; then
|
||
run git -C "$WORK_ROOT" checkout "$TARGET_REF"
|
||
run git -C "$WORK_ROOT" merge --ff-only "origin/$TARGET_REF"
|
||
else
|
||
run git -C "$WORK_ROOT" checkout -b "$TARGET_REF" --track "origin/$TARGET_REF"
|
||
fi
|
||
fi
|
||
|
||
restore_worktree_if_missing
|
||
}
|
||
|
||
# Checking out the ref that HEAD already points at is a no-op, so it does not
|
||
# bring back tracked files that were deleted by an interrupted run. Only ever
|
||
# runs when the source tree is actually missing, so it cannot clobber edits.
|
||
restore_worktree_if_missing() {
|
||
[ "$DRY_RUN" = true ] && return 0
|
||
is_openwrt_tree "$WORK_ROOT" && return 0
|
||
|
||
warn "Source tree is incomplete; restoring tracked files from $TARGET_REF."
|
||
run git -C "$WORK_ROOT" checkout --force HEAD -- .
|
||
|
||
if ! is_openwrt_tree "$WORK_ROOT"; then
|
||
err "'$WORK_ROOT' still has no OpenWrt source tree after restoring."
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase F: feeds and defconfig
|
||
# ---------------------------------------------------------------------------
|
||
update_feeds() {
|
||
[ "$SKIP_FEEDS" = true ] && { info "Skipping feeds (--skip-feeds)."; return 0; }
|
||
|
||
if [ "$DRY_RUN" != true ] && ! is_openwrt_tree "$WORK_ROOT"; then
|
||
err "'$WORK_ROOT' has no OpenWrt source tree."
|
||
exit 1
|
||
fi
|
||
|
||
info "Updating and installing all feeds..."
|
||
run env -C "$WORK_ROOT" ./scripts/feeds update -a
|
||
run env -C "$WORK_ROOT" ./scripts/feeds install -a
|
||
}
|
||
|
||
run_defconfig() {
|
||
[ "$SKIP_DEFCONFIG" = true ] && { info "Skipping defconfig (--skip-defconfig)."; return 0; }
|
||
info "Running make defconfig..."
|
||
run env -C "$WORK_ROOT" make defconfig
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase G: summary
|
||
# ---------------------------------------------------------------------------
|
||
print_summary() {
|
||
local version="$TARGET_REF"
|
||
|
||
if [ "$DRY_RUN" != true ]; then
|
||
version="$(git -C "$WORK_ROOT" describe --tags 2>/dev/null ||
|
||
git -C "$WORK_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "$TARGET_REF")"
|
||
fi
|
||
|
||
echo
|
||
echo "========================================"
|
||
echo "✅ OpenWrt source is ready."
|
||
echo " Version: $version"
|
||
echo " Root: $WORK_ROOT"
|
||
echo " Helper: $HELPER_DIR"
|
||
[ -n "$COMPAT_LINK_PATH" ] && echo " Old path: $COMPAT_LINK_PATH (symlink)"
|
||
echo "========================================"
|
||
echo
|
||
echo "⚠ helper/ lives inside the OpenWrt Git tree but is not tracked by it."
|
||
echo " 'git clean -xdff' in the OpenWrt root WILL DELETE it, .git and all."
|
||
echo " Use instead: git clean -xdf -e /helper"
|
||
echo
|
||
if [ -n "$COMPAT_LINK_PATH" ]; then
|
||
echo "ℹ '$COMPAT_LINK_PATH' is now a symlink to '$HELPER_DIR',"
|
||
echo " so shells and tools still sitting in the old directory keep working."
|
||
echo " Run 'cd .' there to refresh a stale shell."
|
||
echo
|
||
fi
|
||
echo "👉 Next steps:"
|
||
echo " cd $WORK_ROOT"
|
||
echo " ./helper/download-config.sh # optional: import a device config"
|
||
echo " make menuconfig"
|
||
echo " make -j\$(nproc) download world"
|
||
echo
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# main
|
||
# ---------------------------------------------------------------------------
|
||
require_sort_v
|
||
|
||
discover_layout
|
||
discover_remote_refs
|
||
|
||
if [ -z "$VERSION_MODE" ]; then
|
||
if [ "$ASSUME_YES" = true ]; then
|
||
VERSION_MODE="stable"
|
||
else
|
||
choose_version_interactive
|
||
fi
|
||
fi
|
||
|
||
resolve_target_ref
|
||
check_switch_safety
|
||
print_plan
|
||
|
||
if [ "$DRY_RUN" = true ]; then
|
||
echo
|
||
relocate_helper
|
||
ensure_repo
|
||
ensure_helper_excluded
|
||
echo "[DRY-RUN] checkout $TARGET_REF"
|
||
echo
|
||
info "Dry run complete; nothing was changed."
|
||
exit 0
|
||
fi
|
||
|
||
echo
|
||
if ! confirm "Proceed?" yes; then
|
||
info "Aborted by user."
|
||
exit 0
|
||
fi
|
||
|
||
echo
|
||
# Everything below this line mutates the filesystem.
|
||
relocate_helper
|
||
acquire_lock
|
||
ensure_repo
|
||
ensure_helper_excluded
|
||
checkout_target
|
||
update_feeds
|
||
run_defconfig
|
||
print_summary
|