#!/bin/bash # Prepare an OpenWrt build tree. # # Layout produced by this script: # before /openwrt-build-helper/... this helper checkout # first run /openwrt/ the OpenWrt source root # /openwrt/helper/... this checkout, moved in # later runs the same /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 [] stable release tag, e.g. 25.12.5 or v25.12.5 # without a value: newest stable tag (rc tags excluded) # --branch 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 use as the OpenWrt root instead of /openwrt # --skip-feeds do not run scripts/feeds update/install # --skip-defconfig do not run make defconfig # --repo-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 /openwrt/helper. A symlink # is left behind at the original path so that shells, editors and agents whose # working directory still points at /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 "" 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