#!/usr/bin/env bash # Build and deploy OpenWRT images using shell-style device profiles, via source code or the official Image Builder. # Copyright 2022-26 Bryan C. Roessler # Apache 2.0 License # See README and ./profiles for device configuration # Set default release : "${DEFAULT_RELEASE:=${RELEASE:="25.12.5"}}" # do find all replace # @internal usage() { debug "${FUNCNAME[0]}" cat <<-'EOF' Build and deploy OpenWRT images using convenient profiles. USAGE: openwrtbuilder [OPTIONS] [-p PROFILE]... OPTIONS --profile,-p PROFILE --release,-r,--version,-v RELEASE ("snapshot", "25.12.4") Default: From profile or hardcoded RELEASE --buildroot,-b PATH Default: location of openwrtbuilder script --cpus,-c NUM Default: # of host CPUS minus 1 --mode,-m imagebuilder|source Default: imagebuilder --clean clean|targetclean|dirclean|distclean Optional clean step for source mode --ssh-upgrade HOST Examples: root@192.168.1.1, root@router.lan --ssh-backup SSH_PATH Enabled by default for --ssh-upgrade --flash,-f DEVICE Example: /dev/sdX --mirror PREFIX Mirror prefix for feeds (default: github.com/openwrt) --dl-timeout SECONDS Per-file max download time for source builds (default: 300) --depends Force dependency installation --yes,-y Assume yes for all questions (non-interactive) --debug,-d --help,-h EXAMPLES openwrtbuilder -p r4s -r snapshot openwrtbuilder -p ax6000 -r 23.05.0-rc3 --mode source --debug openwrtbuilder -p rpi4 -r 25.12.4 --flash /dev/sdX openwrtbuilder -p linksys -r snapshot --ssh-upgrade root@192.168.1.1 EOF } # @internal init() { debug "${FUNCNAME[0]}" declare -g ID RPM_MGR SCRIPT_DIR DL_TOOL ((DEBUG)) || echo "To enable debugging output, use --debug or -d" # Save the script directory # https://stackoverflow.com/a/4774063 SCRIPT_DIR=$(readlink -f "$(dirname "${BASH_SOURCE[0]}")") if [[ -e "/etc/os-release" ]]; then source "/etc/os-release" else echo "/etc/os-release not found" echo "Your OS is unsupported" usage exit 1 fi debug "Detected host platform: $ID" # normalize distro ID case "$ID" in debian|arch) ;; centos|fedora) if command -v dnf &>/dev/null; then RPM_MGR="dnf" elif command -v yum &>/dev/null; then RPM_MGR="yum" fi ;; rhel) ID="centos" ;; linuxmint|neon|*ubuntu*) ID="ubuntu" ;; *suse*) ID="suse" ;; raspbian) ID="debian" ;; *) echo "Autodetecting distro, this may be unreliable" for cmd in dnf yum apt pacman; do if command -v "$cmd" &>/dev/null; then case "$cmd" in dnf) ID="fedora"; RPM_MGR="dnf" ;; yum) ID="centos"; RPM_MGR="yum" ;; apt) ID="ubuntu" ;; pacman) ID="arch" ;; esac break fi done [[ -z $ID ]] && return 1 ;; esac debug "Using host platform: $ID" # Set distro-specific functions case "$ID" in fedora|centos) pkg_install(){ sudo "$RPM_MGR" install -y "$@"; } ;; debian|ubuntu) pkg_install(){ sudo apt-get install --ignore-missing -y -q0 "$@"; } ;; suse) pkg_install(){ sudo zypper --non-interactive -q install --force --no-confirm "$@"; } ;; arch) pkg_install(){ sudo pacman -S --noconfirm --needed "$@"; } ;; esac if command -v axel &>/dev/null; then DL_TOOL="axel" elif command -v curl &>/dev/null; then DL_TOOL="curl" else echo "Downloading the Image Builder requires axel or curl" return 1 fi } # @description Arguments parse_input() { debug "${FUNCNAME[0]}" "$*" declare -ga PROFILES declare -gi RESET=0 YES=0 DEBUG=0 FORCE_DEPENDS=0 CPUS=0 declare -g USER_RELEASE SSH_UPGRADE_PATH SSH_BACKUP_PATH FLASH_DEV USER_MODE USER_CLEAN USER_MIRROR USER_DL_TIMEOUT local long_opts='release:,version:,profile:,buildroot:,cpus:,mode:,clean:,mirror:,dl-timeout:' long_opts+='ssh-upgrade:,ssh-backup:,flash:,reset,depends,yes,debug,help' if _input=$(getopt -o +r:v:p:b:m:c:f:ydh -l $long_opts -- "$@"); then eval set -- "$_input" while true; do case "$1" in --release|-r|--version|-v) shift; USER_RELEASE="$1" ;; --profile|-p) shift; PROFILES+=("$1") ;; --buildroot|-b) shift; BUILD_ROOT="$1" ;; --mode|-m) shift; USER_MODE="$1" ;; --clean) shift; USER_CLEAN="$1" ;; --cpus|-c) shift; CPUS="$1" ;; --ssh-upgrade) shift; SSH_UPGRADE_PATH="$1" ;; --ssh-backup) shift; SSH_BACKUP_PATH="$1" ;; --flash|-f) shift; FLASH_DEV="$1" ;; --mirror) shift; USER_MIRROR="$1" ;; --dl-timeout) shift; USER_DL_TIMEOUT="$1" ;; --reset) RESET=1 ;; --depends) FORCE_DEPENDS=1 ;; --yes|-y) YES=1 ;; --debug|-d) echo "Debugging on"; DEBUG=1 ;; --help|-h) usage; exit 0 ;; --) shift; break ;; esac shift done else echo "Incorrect options provided" usage; exit 1 fi } # @description Install build dependencies on major distros # @arg $1 string Build mode ("source" or "imagebuilder") install_dependencies() { debug "${FUNCNAME[0]}" local mode="$1" local -a pkg_list local lock_file # Set appropriate lock file based on mode if [[ "$mode" == "source" ]]; then lock_file="$BUILD_ROOT/.dependencies_source.lock" elif [[ "$mode" == "imagebuilder" ]]; then lock_file="$BUILD_ROOT/.dependencies_imagebuilder.lock" fi [[ -f $lock_file ]] && debug "$lock_file lock file exists, skipping dependency install" && return 0 if [[ "$mode" == "source" ]]; then # For building from source code see: # https://openwrt.org/docs/guide-developer/toolchain/install-buildsystem case "$ID" in fedora|centos) pkg_list+=( bzip2 clang # for qosify diffutils gcc gcc-c++ git golang llvm15-libs # for qosify make ncurses-devel patch perl perl-Data-Dumper perl-File-Compare perl-File-Copy perl-FindBin perl-IPC-Cmd perl-JSON-PP perl-Thread-Queue perl-Time-Piece perl-base python3 python3-devel python3-pyelftools python3-setuptools quilt rsync swig tar unzip wget which ) ;; debian|ubuntu) pkg_list+=( build-essential clang file flex g++ gawk gcc-multilib gettext git golang liblzma-dev libncurses5-dev libssl-dev python3-distutils quilt rsync patch unzip wget zlib1g-dev ) ;; arch) pkg_list+=( autoconf automake base-devel bash binutils bison bzip2 clang fakeroot file findutils flex gawk gcc gettext git golang grep groff gzip libelf libtool libxslt m4 make ncurses net-snmp openssl patch pkgconf python quilt rsync sed texinfo time unzip util-linux wget which xz zlib ) ;; *) debug "Unsupported OS, skipping dependency install"; return 1 ;; esac elif [[ "$mode" == "imagebuilder" ]]; then # For Imagebuilder case "$ID" in fedora|centos) pkg_list+=( @c-development @development-tools @development-libs perl-FindBin zlib-static elfutils-libelf-devel gawk unzip file wget python3 axel perl-IPC-Cmd zstd) ;; debian|ubuntu) pkg_list+=( build-essential libncurses5-dev libncursesw5-dev zlib1g-dev gawk git gettext libssl-dev xsltproc wget unzip python axel zstd) ;; *) debug "Unsupported OS, skipping dependency install"; return 1 ;; esac fi pkg_install "${pkg_list[@]}" && echo "${pkg_list[@]}" > "$lock_file" } # @description Normalize release and set worktree reference # @arg $1 string Raw release input # @arg $2 string Build mode ("source" or "imagebuilder") # @returns string Normalized release and reference normalize_and_ref() { local input="$1" mode="$2" local rel ref branch tag case "$input" in snapshot|latest|main|master) rel="snapshot" ref="main" ;; v[0-9][0-9].[0-9][0-9].*|[0-9][0-9].[0-9][0-9].*) # strip optional leading ā€œvā€ rel="${input#v}" if [[ "$mode" == "source" ]]; then branch="openwrt-${rel%.*}" tag="v$rel" if ask_ok "Use branch $branch HEAD (y) or tag $tag (n)?"; then ref="$branch" else ref="$tag" fi else ref="$rel" fi ;; *) if [[ "$mode" == "source" ]]; then # arbitrary commit-ish allowed rel="$input" ref="$input" else echo "Error: invalid release '$input'" >&2 exit 1 fi ;; esac printf '%s %s' "$rel" "$ref" } # @description Acquires the OpenWRT Image Builder get_imagebuilder() { debug "${FUNCNAME[0]}" "$*" local -a url_file_pairs=("$@") for ((i=0; i<${#url_file_pairs[@]}; i+=2)); do local url="${url_file_pairs[i]}" local file="${url_file_pairs[i+1]}" # Check if file exists and ask user to remove and redownload if [[ -f $file ]] && ! ask_ok "Use existing $file?"; then execute rm -f "$file" fi # Download the file if it doesn't exist if [[ ! -f "$file" ]]; then echo "Downloading $url to $file using $DL_TOOL" execute "$DL_TOOL" "-o" "$file" "$url" fi done } ssh_backup() { debug "${FUNCNAME[0]}" local date hostname backup_fname printf -v date '%(%Y-%m-%d-%H-%M-%S)T' hostname=$(ssh -qt "$SSH_BACKUP_PATH" echo -n \$HOSTNAME) backup_fname="backup-$hostname-$date.tar.gz" [[ -d "$FILES_DIR" ]] || execute mkdir -p "$FILES_DIR" # Make backup archive on remote if ! execute ssh -t "$SSH_BACKUP_PATH" sysupgrade -b "/tmp/$backup_fname"; then echo "SSH backup failed" exit 1 fi # Move backup archive locally if ! execute rsync -avz --remove-source-files "$SSH_BACKUP_PATH:/tmp/$backup_fname" "$BUILD_DIR/"; then echo "Could not copy SSH backup" exit 1 fi # Extract backup archive if ! execute tar -C "$FILES_DIR" -xzf "$BUILD_DIR/$backup_fname"; then echo "Could not extract SSH backup" exit 1 fi execute rm "$BUILD_DIR/$backup_fname" } make_images() { debug "${FUNCNAME[0]}" local -a make_opts; ((DEBUG)) && make_opts+=("V=sc") local rootfs_partsize # Image Builder accepts ROOTFS_PARTSIZE, not CONFIG_TARGET_ROOTFS_PARTSIZE. # Parse profile KCONFIGS (scalar string) and extract only this value. for kconfig in $KCONFIGS; do case "$kconfig" in CONFIG_TARGET_ROOTFS_PARTSIZE=*) rootfs_partsize="${kconfig#*=}" ;; esac done local -a make_cmd=(make "${make_opts[@]}" image BIN_DIR="$BIN_DIR" PROFILE="$DEVICE" PACKAGES="$PACKAGES" ROOTFS_FILESYSTEM="$FILESYSTEM" FILES="$FILES_DIR" --directory="$BUILD_DIR" --jobs="$JOBS") [[ -n "$rootfs_partsize" ]] && make_cmd+=("ROOTFS_PARTSIZE=$rootfs_partsize") # Reuse the existing output # TODO Disable for now since it was causing issues # if [[ -d "$BIN_DIR" ]]; then # if ask_ok "$BIN_DIR exists. Rebuild?"; then # execute rm -rf "$BIN_DIR" # else # return 0 # fi # fi # Debug manually so we can log output debug "${make_cmd[*]}" "${make_cmd[@]}" 2>&1 | tee "$BUILD_DIR/make.log" return "${PIPESTATUS[0]}" # bash-specific way to return exit code of piped command } flash_images() { debug "${FUNCNAME[0]}" local img_gz="$1" local dev="$2" local img="${img_gz%.gz}" if [[ ! -b "$dev" ]]; then echo "The device specified by --flash could not be found" return 1 fi [[ -f $img_gz ]] || { echo "$img_gz does not exist"; return 1; } execute gunzip -qfk "$img_gz" [[ -f "$img" ]] || { echo "Extracted image '$img' is missing"; return 1; } if execute sudo dd if="$img" of="$dev" bs=2M conv=fsync; then sync if command -v partprobe &>/dev/null; then execute sudo partprobe "$dev" || debug "partprobe failed for $dev" fi echo "Image flashed successfully!" else echo "dd failed!" return 1 fi } ssh_upgrade() { debug "${FUNCNAME[0]}" local img_gz="$1" local ssh_path="$2" local img_fname="${img_gz##*/}" [[ -f $img_gz ]] || { echo "$img_gz is missing, check build output"; return 1; } echo "Copying '$img_gz' to $ssh_path/tmp/$img_fname" if ! execute scp "$img_gz" "$ssh_path:/tmp/$img_fname"; then echo "Could not copy $img_gz to $ssh_path:/tmp/$img_fname" return 1 fi echo "Executing remote sysupgrade" # This may result in weird exit code from closing the ssh connection # shellcheck disable=SC2029 ssh "$ssh_path" "sysupgrade -F /tmp/$img_fname" return 0 } # @description Builds OpenWRT from source code using the the default buildbot as base from_source() { debug "${FUNCNAME[0]}" local src_url="https://github.com/openwrt/openwrt.git" local seed_file="$BUILD_DIR/.config" local worktree_meta="$SRC_DIR/.git/worktrees/source-$REF" local pkg kconfig commit description rootfs_kconfig local cherrypick url_branch branch url remote feed_dir repo_name local -a make_opts preprocess_cherrypick() { local spec="$1" url_branch="${spec%:*}" if [[ "$url_branch" == *"@"* ]]; then url="${url_branch%@*}" branch="${url_branch#*@}" else url="$url_branch" branch="" fi commit="${spec##*:}" repo_name="${url%.git}" repo_name="${repo_name##*/}" remote="$repo_name" remote=${remote//[^A-Za-z0-9._-]/_} [[ -z $remote ]] && remote="cherry" } apply_cherrypick() { local repo_dir="$1" if ! git -C "$repo_dir" remote | grep -q "^$remote$"; then execute git -C "$repo_dir" remote add "$remote" "$url" else execute git -C "$repo_dir" remote set-url "$remote" "$url" fi execute git -C "$repo_dir" fetch "$remote" "$branch" if ! git -C "$repo_dir" cat-file -e "$commit^{commit}" 2>/dev/null; then debug "Commit $commit not found after fetching from $remote; skipping" return 0 fi execute git -C "$repo_dir" merge-base --is-ancestor "$commit" HEAD || execute git -C "$repo_dir" cherry-pick "$commit" } echo "Building from source is under development" # Convert filesystem to corresponding KCONFIG value case "${FILESYSTEM,,}" in squashfs) rootfs_kconfig="SQUASHFS" ;; ext4|ext4fs) rootfs_kconfig="EXT4FS" ;; ubifs) rootfs_kconfig="UBIFS" ;; erofs) rootfs_kconfig="EROFS" ;; jffs2) rootfs_kconfig="JFFS2" ;; all) rootfs_kconfig="all";; *) echo "Error: unsupported filesystem '$FILESYSTEM' for source mode"; return 1 ;; esac # Default KCONFIG options for all profiles (can be overridden by profile-specific KCONFIGS) local -a kconfigs=( "CONFIG_TARGET_${TARGET%%/*}=y" "CONFIG_TARGET_${TARGET//\//_}=y" "CONFIG_TARGET_PROFILE=DEVICE_$DEVICE" "CONFIG_TARGET_${TARGET//\//_}_DEVICE_$DEVICE=y" "CONFIG_TARGET_ROOTFS_SQUASHFS=n" "CONFIG_TARGET_ROOTFS_EXT4FS=n" "CONFIG_TARGET_ROOTFS_UBIFS=n" "CONFIG_TARGET_ROOTFS_EROFS=n" "CONFIG_TARGET_ROOTFS_JFFS2=n" "CONFIG_TARGET_MULTI_PROFILE=n" "CONFIG_BUILDBOT=n" "CONFIG_ALL_KMODS=n" "CONFIG_ALL_NONSHARED=n" "CONFIG_DEVEL=n" "CONFIG_COLLECT_KERNEL_DEBUG=n" "CONFIG_SDK=n" "CONFIG_SDK_LLVM_BPF=n" "CONFIG_IB=n" "CONFIG_MAKE_TOOLCHAIN=n" "CONFIG_TARGET_PER_DEVICE_ROOTFS=n" ) # Add KCONFIG for selected filesystem(s) if [[ $rootfs_kconfig == "all" ]]; then kconfigs+=("CONFIG_TARGET_ROOTFS_SQUASHFS=y" "CONFIG_TARGET_ROOTFS_EXT4FS=y" "CONFIG_TARGET_ROOTFS_UBIFS=y" "CONFIG_TARGET_ROOTFS_EROFS=y" "CONFIG_TARGET_ROOTFS_JFFS2=y") else kconfigs+=("CONFIG_TARGET_ROOTFS_${rootfs_kconfig}=y") fi # Remove all build directories and worktrees if --reset if ((RESET)); then if [[ -d "$BUILD_DIR" || -d "$worktree_meta" ]]; then execute git -C "$SRC_DIR" worktree remove --force --force "$BUILD_DIR" [[ -d "$BUILD_DIR" ]] && execute rm -rf "$BUILD_DIR" [[ -d "$worktree_meta" ]] && execute rm -rf "$worktree_meta" fi [[ -d "$BUILD_DIR" ]] && execute rm -rf "$BUILD_DIR" fi # Fetch or clone source repo (no local merges) if [[ -d "$SRC_DIR" ]]; then execute git -C "$SRC_DIR" fetch origin --tags --prune else execute mkdir -p "$SRC_DIR" execute git clone "$src_url" "$SRC_DIR" fi # Reuse worktree if present; otherwise create it (support branches and tags) if git -C "$BUILD_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then execute git -C "$BUILD_DIR" fetch origin --tags --prune execute git -C "$BUILD_DIR" reset --hard "origin/$REF" || \ execute git -C "$BUILD_DIR" reset --hard "$REF" || \ execute git -C "$BUILD_DIR" checkout --detach "$REF" else execute git -C "$SRC_DIR" worktree prune --verbose # Prefer local tag/branch if present, otherwise use remote-tracking branch if ! execute git -C "$SRC_DIR" worktree add --detach "$BUILD_DIR" "$REF"; then execute git -C "$SRC_DIR" worktree add --detach "$BUILD_DIR" "origin/$REF" fi fi # Apply OpenWrt-targeted cherrypicks before branch merges. for cherrypick in $CHERRYPICKS; do preprocess_cherrypick "$cherrypick" [[ "$repo_name" == "openwrt" ]] || continue apply_cherrypick "$BUILD_DIR" done # Merge entire branches for branch in $BRANCHES; do url_branch="$branch" branch="" url="$url_branch" if [[ "$url_branch" == *"@"* ]]; then url="${url_branch%@*}" branch="${url_branch#*@}" fi remote="${url%.git}" remote="${remote##*/}" remote=${remote//[^A-Za-z0-9._-]/_} [[ -z $remote ]] && remote="merge" if ! git -C "$BUILD_DIR" remote | grep -q "^$remote$"; then execute git -C "$BUILD_DIR" remote add "$remote" "$url" else execute git -C "$BUILD_DIR" remote set-url "$remote" "$url" fi if [[ -n $branch ]]; then execute git -C "$BUILD_DIR" fetch "$remote" "$branch" echo "Merging $remote/$branch into $(git -C "$BUILD_DIR" rev-parse --abbrev-ref HEAD)" execute git -C "$BUILD_DIR" merge --allow-unrelated-histories -m "Merge $remote/$branch" "$remote/$branch" || \ { debug "Merge conflict or failed for $remote/$branch"; return 1; } else debug "Cannot merge: no branch specified in entry '$branch'" continue fi done # Print commit info commit=$(git -C "$BUILD_DIR" rev-parse HEAD) description=$(git -C "$BUILD_DIR" describe --always --dirty) echo "Current commit hash: $commit" echo "Git worktree description: $description" ((DEBUG)) && git --no-pager -C "$BUILD_DIR" log -1 # Enter worktree execute pushd "$BUILD_DIR" || return 1 # Begin OpenWRT build process ((DEBUG)) && make_opts+=("V=sc") # Cleanup build environment: heavy clean only when --reset was used earlier # make clean # compiled output # make targetclean # compiled output, toolchain # make dirclean # compiled output, toolchain, build tools # make distclean # compiled output, toolchain, build tools, .config, feeds, .ccache if [[ -n $CLEAN ]]; then execute make "${make_opts[@]}" "-j1" "$CLEAN" else debug "Skipping cleanup step" fi # Use a custom (faster) mirror local mirror_prefix="${USER_MIRROR:-github.com/openwrt}" execute sed -i -E "s;git.openwrt.org/(feed|project);${mirror_prefix};" feeds.conf.default # Update package feed if ! ./scripts/feeds update -a -f || ! ./scripts/feeds install -a -f; then echo "Error: feeds update/install failed" return 1 fi # Apply feed-targeted cherrypicks after feeds are checked out/updated. for cherrypick in $CHERRYPICKS; do preprocess_cherrypick "$cherrypick" [[ "$repo_name" == "openwrt" ]] && continue feed_dir="$BUILD_DIR/feeds/$repo_name" if [[ ! -d "$feed_dir/.git" ]]; then debug "Feed repo '$repo_name' not found at $feed_dir; skipping" continue fi apply_cherrypick "$feed_dir" done # Apply custom files overlay for source builds. # execute rm -rf "$BUILD_DIR/files" # if [[ -d "$FILES_DIR" ]]; then # execute mkdir -p "$BUILD_DIR/files" # execute rsync -a "$FILES_DIR/" "$BUILD_DIR/files/" # fi # Add custom packages for pkg in $PACKAGES; do if [[ $pkg == -* ]]; then kconfigs+=("CONFIG_PACKAGE_${pkg#-}=n") # remove package else kconfigs+=("CONFIG_PACKAGE_$pkg=y") # add package fi done # Add profile kconfig options # $KCONFIGS is a scalar, use a loop to split and avoid SC2068 for kconfig in $KCONFIGS; do kconfigs+=("$kconfig") done # Reset and write options to config seed file [[ -f $seed_file ]] && execute rm -f "$seed_file" for kconfig in "${kconfigs[@]}"; do debug "Writing $kconfig to $seed_file" echo "$kconfig" >> "$seed_file" done # Expand seed into full config execute make "${make_opts[@]}" "-j1" defconfig # Run serial make download for better reliability # CURL_EXTRA_ARGS is picked up by OpenWRT's scripts/download.pl CURL_EXTRA_ARGS="--max-time ${USER_DL_TIMEOUT:-300}" execute make "${make_opts[@]}" "-j1" download # (Optional) Disable multicore make world # ((DEBUG)) && make_opts+=("-j1") || make_opts+=("-j$JOBS)") make_opts+=("-j$JOBS") # Make image if ! ionice -c2 -n7 nice -n19 make "${make_opts[@]}" BIN_DIR="$BIN_DIR" world 2>&1 | tee "$BUILD_DIR/make.log"; then echo "Error: make failed (see $BUILD_DIR/make.log)" return 1 fi execute popd || return 1 # Symlink output images to root of BIN_DIR (match Image Builder behavior) shopt -s nullglob for image in "$BIN_DIR/targets/${TARGET}/"*.{img,img.gz,ubi}; do execute ln -fs "$image" "$BIN_DIR/${image##*/}" done shopt -u nullglob return 0 } # @section Helper functions # @internal debug() { ((DEBUG)) && echo "Debug: $*"; } ask_ok() { ((YES)) && return local r read -r -p "$* [y/N]: " r r=${r,,} [[ "$r" =~ ^(yes|y)$ ]] } extract() { local archive="$1" local out_dir="$2" if ! execute tar -axf "$archive" -C "$out_dir" --strip-components 1; then echo "Extraction failed" return 1 fi } verify() { local file_to_check="$1" sum_file="$2" local checksum command -v sha256sum &>/dev/null || return 1 [[ -f $sum_file && -f $file_to_check ]] || return 1 checksum=$(grep "${file_to_check##*/}" "$sum_file" | cut -f1 -d' ') echo -n "$checksum $file_to_check" | sha256sum --check --status } load() { local source_file="$1" # shellcheck disable=SC1090 [[ -f $source_file ]] && source "$source_file" } execute() { if debug "$*"; then "$@" else "$@" &>/dev/null fi } # @description The openwrtbuilder main function # @internal main() { debug "${FUNCNAME[0]}" init load "$SCRIPT_DIR/profiles" parse_input "$@" # Fallback to SCRIPT_DIR if BUILD_ROOT has not been set declare -g BUILD_ROOT="${BUILD_ROOT:=$SCRIPT_DIR}" declare -g BACKUP_DIR="$SCRIPT_DIR/backups" # This could be dangerous if [[ $BUILD_ROOT == "/" ]]; then echo "Invalid --buildroot" exit 1 fi for dir in "$BUILD_ROOT/src" "$BUILD_ROOT/bin"; do [[ -d "$dir" ]] || execute mkdir -p "$dir" done # Allow --reset without a profile if ((RESET)) && [[ ${#PROFILES} -lt 1 ]]; then for d in "$BUILD_ROOT/src" "$BUILD_ROOT/bin"; do ask_ok "Remove $d?" && execute rm -rf "$d" done exit $? fi # Remove dependency lock files for --depends if ((FORCE_DEPENDS)); then [[ -f "$BUILD_ROOT/.dependencies_source.lock" ]] && rm -f "$BUILD_ROOT/.dependencies_source.lock" [[ -f "$BUILD_ROOT/.dependencies_imagebuilder.lock" ]] && rm -f "$BUILD_ROOT/.dependencies_imagebuilder.lock" fi # Run selected profiles for PROFILE in "${PROFILES[@]}"; do debug "Running profile: $PROFILE" if [[ ! ${!PROFILE@a} = A ]]; then echo "Profile '$PROFILE' does not exist" return 1 fi # Store profile in P_ARR nameref and set global profile vars local -n P_ARR="$PROFILE" declare -g REPO="${P_ARR[repo]:-}" declare -g FILESYSTEM="${P_ARR[filesystem]:-squashfs}" declare -g TARGET="${P_ARR[target]}" declare -g DEVICE="${P_ARR[device]}" declare -g MODE="${USER_MODE:-${P_ARR[mode]:-imagebuilder}}" declare -g CLEAN="${USER_CLEAN:-${P_ARR[clean]:-}}" declare -g PACKAGES="${P_ARR[packages]:-}" # scalar declare -g CHERRYPICKS="${P_ARR[cherrypicks]:-}" # scalar declare -g BRANCHES="${P_ARR[branches]:-}" # scalar declare -g KCONFIGS="${P_ARR[kconfigs]:-}" # scalar declare -g FILES_DIR="${P_ARR[files]:-$BUILD_ROOT/src/files}" if [[ ! -d "$FILES_DIR" ]]; then if [[ -v P_ARR[files] ]]; then echo "Profile '$PROFILE' files directory does not exist: $FILES_DIR" return 1 fi execute mkdir -p "$FILES_DIR" fi install_dependencies "$MODE" # Set number of parallel jobs for make and imagebuilder declare -gi JOBS if ((CPUS)); then JOBS="$CPUS" # user overide (--cpus) else JOBS=$(nproc || echo 4) # fallback to quad-core if nproc fails ((JOBS > 1)) && JOBS=$((JOBS - 1)) # leave one CPU free fi # Normalize RELEASE and set REF committish local raw_release="${USER_RELEASE:=${P_ARR[release]:=$DEFAULT_RELEASE}}" declare -g RELEASE REF read -r RELEASE REF < <(normalize_and_ref "$raw_release" "$MODE") declare -g SRC_DIR="$BUILD_ROOT/src/.openwrt" declare -g BUILD_DIR="$BUILD_ROOT/src/$PROFILE/$REF-$MODE" declare -g BIN_DIR="$BUILD_ROOT/bin/$PROFILE/$REF-$MODE" if [[ "$RELEASE" == "snapshot" ]]; then local url_prefix="https://downloads.openwrt.org/snapshots/targets/$TARGET" local url_filename="openwrt-imagebuilder-${TARGET//\//-}.Linux-x86_64.tar.zst" local img_fname="openwrt-${TARGET//\//-}-$DEVICE-$FILESYSTEM" else local url_prefix="https://downloads.openwrt.org/releases/$RELEASE/targets/$TARGET" local url_filename="openwrt-imagebuilder-$RELEASE-${TARGET//\//-}.Linux-x86_64.tar.zst" local img_fname="openwrt-$RELEASE-${TARGET//\//-}-$DEVICE-$FILESYSTEM" fi local imagebuilder_url="$url_prefix/$url_filename" local imagebuilder_file="$BUILD_DIR/$url_filename" local imagebuilder_sha256_url="$url_prefix/sha256sums" local imagebuilder_sha256_file="$BUILD_DIR/sha256sums" if [[ "$MODE" == "source" ]]; then declare -g SYSUPGRADEIMGGZ="$BIN_DIR/targets/$TARGET/$img_fname-sysupgrade.img.gz" else declare -g SYSUPGRADEIMGGZ="$BUILD_DIR/$img_fname-sysupgrade.img.gz" fi # Backup existing output directory if [[ -d "$BIN_DIR" ]]; then local timestamp timestamp=$(date +%y%m%d%H%M) execute mkdir -p "$BACKUP_DIR/$PROFILE/$REF-$MODE-$timestamp" execute rsync -a --delete --exclude 'packages/' "$BIN_DIR/" "$BACKUP_DIR/$PROFILE/$REF-$MODE-$timestamp/" fi if [[ "$MODE" == "source" ]]; then from_source || return $? elif [[ "$MODE" == "imagebuilder" ]]; then [[ -d $BUILD_DIR ]] || mkdir -p "$BUILD_DIR" get_imagebuilder "$imagebuilder_url" "$imagebuilder_file" "$imagebuilder_sha256_url" "$imagebuilder_sha256_file" && verify "$imagebuilder_file" "$imagebuilder_sha256_file" && extract "$imagebuilder_file" "$BUILD_DIR" || return $? # Add external repositories for the Image Builder build if [[ -n $REPO ]]; then if ! grep -q "$REPO" "$BUILD_DIR/repositories.conf"; then echo "$REPO" >> "$BUILD_DIR/repositories.conf" fi sed -i '/option check_signature/d' "$BUILD_DIR/repositories.conf" fi make_images # Verify output image for stock builds (in testing) if [[ ! -v P_ARR[packages] || -z ${P_ARR[packages]} ]]; then shopt -s nullglob local -a outfiles=("$BIN_DIR"/*.img.gz "$BIN_DIR"/*.img) shopt -u nullglob for outfile in "${outfiles[@]}"; do verify "$outfile" "$imagebuilder_sha256_file" || return 1 done fi #copyFiles fi [[ -v SSH_BACKUP_PATH ]] && ssh_backup [[ -v SSH_UPGRADE_PATH ]] && ssh_upgrade "$SYSUPGRADEIMGGZ" "$SSH_UPGRADE_PATH" [[ -v FLASH_DEV ]] && flash_images "$SYSUPGRADEIMGGZ" "$FLASH_DEV" done } # Roughly turn debugging on for pre-init # Reset and reparse in parse_input() with getopt [[ " $* " =~ ( --debug | -d ) ]] && DEBUG=1 main "$@" exit