#!/usr/bin/env bash # # Copyright 2022-23 Bryan C. Roessler # # Build and deploy OpenWRT images # # Apache 2.0 License # Set default release : "${RELEASE:="22.03.3"}" printHelp() { debug "${FUNCNAME[0]}" cat <<-'EOF' Create and deploy OpenWRT images using the Image Builder. USAGE: openwrtbuilder [OPTION [VALUE]] -p PROFILE [-p PROFILE2]... OPTIONS --profile, -p PROFILE --info, -i (print profile info) --list-profiles, -l --release, -r, --version, -v RELEASE_VERSION ("snapshot", "22.03.3", etc.) --buildroot, -b PATH --ssh-upgrade HOST Example: root@192.168.1.1 --ssh-backup SSH_PATH (Enabled by default for --ssh-upgrade) --flash, -f DEVICE Example: /dev/sdX --reset Cleanup all source and output files --debug, -d --help, -h EOF } readInput() { debug "${FUNCNAME[0]}" unset RESET declare -ga PROFILES declare -g PROFILE_INFO if _input=$(getopt -o +r:v:p:i:lb:sf:dh -l release:,version:,profile:,info:,list-profiles,buildroot:,from-source,ssh-upgrade:,ssh-backup:,flash:,reset,debug,help -- "$@"); then eval set -- "$_input" while true; do case "$1" in --release|-r|--version|-v) shift && declare -g USER_RELEASE="$1" ;; --profile|-p) shift && PROFILES+=("$1") ;; --info|-i) PROFILE_INFO=1 ;; --list-profiles|-l) listProfiles && exit $? ;; --buildroot|-b) shift && BUILDROOT="$1" ;; --from-source|-s) FROM_SOURCE=1 ;; --ssh-upgrade) shift && SSH_UPGRADE_PATH="$1" ;; --ssh-backup) shift && SSH_BACKUP_PATH="$1" ;; --flash|-f) shift && FLASH_DEV="$1" ;; --reset) RESET=1 ;; --debug|-d) echo "Debugging on" DEBUG=1 ;; --help|-h) printHelp && exit 0 ;; --) shift break ;; esac shift done else echo "Incorrect options provided" printHelp && exit 1 fi } listProfiles() { debug "${FUNCNAME[0]}" grep "declare -Ag" "$PFILE" | cut -d" " -f3 } installDependencies() { debug "${FUNCNAME[0]}" declare -a pkg_list # TODO please contribute your platform here if (( FROM_SOURCE )); then # For building from source with make # https://openwrt.org/docs/guide-developer/toolchain/install-buildsystem case "$ID" in fedora|centos) pkg_list+=( "bash-completion" "bzip2" "gcc" "gcc-c++" "git" "make" "ncurses-devel" "patch" "rsync" "tar" "unzip" "wget" "which" "diffutils" "python2" "python3" "perl-base" "perl-Data-Dumper" "perl-File-Compare" "perl-File-Copy" "perl-FindBin" "perl-Thread-Queue" ) ;; debian|ubuntu) pkg_list+=( "build-essential" "clang" "flex" "g++" "gawk" "gcc-multilib" "gettext" "git" "libncurses5-dev" "libssl-dev" "python3-distutils" "rsync" "unzip" "zlib1g-dev" "file" "wget" ) ;; arch) pkg_list+=( "base-devel" "autoconf" "automake" "bash" "binutils" "bison" "bzip2" "fakeroot" "file" "findutils" "flex" "gawk" "gcc" "gettext" "git" "grep" "groff" "gzip" "libelf" "libtool" "libxslt" "m4" "make" "ncurses" "openssl" "patch" "pkgconf" "python" "rsync" "sed" "texinfo" "time" "unzip" "util-linux" "wget" "which" "zlib" ) ;; esac else # 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" "python2" "axel" ) ;; debian|ubuntu) pkg_list+=( "build-essential" "libncurses5-dev" "libncursesw5-dev" "zlib1g-dev" "gawk" "git" "gettext" "libssl-dev" "xsltproc" "wget" "unzip" "python" "axel" ) ;; esac fi pkg_install "${pkg_list[@]}" } getImageBuilder() { debug "${FUNCNAME[0]}" declare dl_tool if [[ -f "${P_ARR[ib_archive]}" ]]; then if askOk "Redownload ImageBuilder archive?"; then rm -f "${P_ARR[ib_archive]}" else return 0 fi fi if hash axel &>/dev/null; then dl_tool="axel" elif hash curl &>/dev/null; then dl_tool="curl" else echo "Downloading the ImageBuilder requires axel or curl!" return 1 fi echo "Downloading imagebuilder archive using $dl_tool" debug "$dl_tool -o ${P_ARR[ib_archive]} ${P_ARR[ib_url]}" if ! "$dl_tool" -o "${P_ARR[ib_archive]}" "${P_ARR[ib_url]}"; then echo "Could not download imagebuilder archive" exit 1 fi if [[ ! -f "${P_ARR[ib_archive]}" ]]; then echo "Archive missing" exit 1 fi # if hash sha256sum &>/dev/null; then # echo "Verifying checksums" # debug "$dl_tool -s "${P_ARR[SHA256_URL]}" | grep $filename | cut -f1 -d' '" # sha256sum=$($dl_tool -s "${P_ARR[SHA256_URL]}" |grep "$filename" |cut -f1 -d' ') # debug "Downloaded sha256sum: $sha256sum" # fi echo "Extracting image archive" [[ ! -d "${P_ARR[sources_dir]}" ]] && mkdir -p "${P_ARR[sources_dir]}" debug "tar -xf ${P_ARR[ib_archive]} -C ${P_ARR[sources_dir]} --strip-components 1" if ! tar -xf "${P_ARR[ib_archive]}" -C "${P_ARR[sources_dir]}" --strip-components 1; then echo "Extraction failed" exit 1 fi } addRepos() { debug "${FUNCNAME[0]}" if [[ -v P_ARR[repo] ]]; then if ! grep -q "${P_ARR[repo]}" "${P_ARR[sources_dir]}/repositories.conf"; then echo "${P_ARR[repo]}" >> "${P_ARR[sources_dir]}/repositories.conf" fi sed -i '/option check_signature/d' "${P_ARR[sources_dir]}/repositories.conf" fi } sshBackup() { debug "${FUNCNAME[0]}" local _date _hostname _backup_fname [[ -d "$FILESDIR" ]] || mkdir -p "$FILESDIR" 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" # Make backup archive on remote debug "ssh -t $SSH_BACKUP_PATH sysupgrade -b /tmp/$_backup_fname" if ! ssh -t "$SSH_BACKUP_PATH" "sysupgrade -b /tmp/$_backup_fname"; then echo "SSH backup failed" exit 1 fi # Move backup archive locally debug "rsync -avz --remove-source-files $SSH_BACKUP_PATH:/tmp/$_backup_fname ${P_ARR[build_dir]}/" if ! rsync -avz --remove-source-files "$SSH_BACKUP_PATH":"/tmp/$_backup_fname" "${P_ARR[build_dir]}/"; then echo "Could not copy SSH backup" exit 1 fi # Extract backup archive debug "tar -C $FILESDIR -xzf ${P_ARR[build_dir]}/$_backup_fname" if ! tar -C "$FILESDIR" -xzf "${P_ARR[build_dir]}/$_backup_fname"; then echo "Could not extract SSH backup" exit 1 fi rm "${P_ARR[build_dir]}/$_backup_fname" } makeImage() { debug "${FUNCNAME[0]}" # Reuse the existing output if [[ -d "${P_ARR[bin_dir]}" ]]; then if askOk "${P_ARR[bin_dir]} exists. Rebuild?"; then rm -rf "${P_ARR[bin_dir]}" else return 0 fi fi [[ ! -d "${P_ARR[bin_dir]}" ]] && mkdir -p "${P_ARR[bin_dir]}" if ! make image \ BIN_DIR="${P_ARR[bin_dir]}" \ PROFILE="${P_ARR[profile]}" \ PACKAGES="${P_ARR[packages]}" \ FILES="$FILESDIR" \ --directory="${P_ARR[sources_dir]}" \ --jobs=$(( $(nproc) - 1 )) \ > make.log; then echo "Make image failed!" exit 1 fi } flashImage() { debug "${FUNCNAME[0]}" local _umount if [[ ! -e "$FLASH_DEV" ]]; then echo "The device specified by --flash could not be found" exit 1 fi # TODO Roughly chooses the correct image if [[ -f "${P_ARR[factory_img_gz]}" ]]; then img_gz="${P_ARR[factory_img_gz]}" img="${P_ARR[factory_img]}" elif [[ -f "${P_ARR[sysupgrade_img_gz]}" ]]; then img_gz="${P_ARR[sysupgrade_img_gz]}" img="${P_ARR[sysupgrade_img]}" else return 1 fi debug "$img_gz $img" debug "gunzip -qfk $img_gz" gunzip -qfk "$img_gz" echo "Unmounting target device $FLASH_DEV partitions" _umount=( "$FLASH_DEV"?* ) debug "umount ${_umount[*]}" sudo umount "${_umount[@]}" debug "sudo dd if=\"$img\" of=\"$FLASH_DEV\" bs=2M conv=fsync" if sudo dd if="$img" of="$FLASH_DEV" bs=2M conv=fsync; then sync echo "Image flashed sucessfully!" else echo "dd failed!" exit 1 fi } sshUpgrade() { debug "${FUNCNAME[0]}" echo "Copying \"${P_ARR[sysupgrade_bin_gz]}\" to $SSH_UPGRADE_PATH/tmp/" debug "scp \"${P_ARR[sysupgrade_bin_gz]}\" \"$SSH_UPGRADE_PATH\":\"/tmp/${P_ARR[sysupgrade_bin_gz_fname]}\"" # shellcheck disable=SC2140 if ! scp "${P_ARR[sysupgrade_bin_gz]}" "$SSH_UPGRADE_PATH":"/tmp/${P_ARR[sysupgrade_bin_gz_fname]}"; then echo "Could not access the --ssh-upgrade PATH" exit 1 fi echo "Executing remote sysupgrade" debug "ssh \"$SSH_UPGRADE_PATH\" \"sysupgrade -F /tmp/${P_ARR[sysupgrade_bin_gz_fname]}\"" # shellcheck disable=SC2029 ssh "$SSH_UPGRADE_PATH" "sysupgrade -F /tmp/${P_ARR[sysupgrade_bin_gz_fname]}" } fromSource() { debug "${FUNCNAME[0]}" declare src_url="https://github.com/openwrt/openwrt.git" declare src_dir="${P_ARR[build_dir]}/sources/openwrt" declare -a pkg_list echo "Building from source is under development" if [[ ! -d "$src_dir" ]]; then mkdir -p "$src_dir" git clone "$src_url" "$src_dir" fi pushd "$src_dir" || return 1 if [[ ${P_ARR['release']} == "snapshot" ]]; then git checkout master else git checkout "v${P_ARR[release]}" fi ./scripts/feeds update -a ./scripts/feeds install -a # Grab the release config.seed k_options=$(curl -s https://downloads.openwrt.org/releases/22.03.3/targets/rockchip/armv8/config.buildinfo) debug "$k_options" make distclean make download make -j"$(nproc)" world popd || return 1 exit # TODO exit here for fromSource() testing } debug() { (( DEBUG )) && echo "Debug: $*"; } askOk() { local _response read -r -p "$* [y/N]" _response _response=${_response,,} [[ ! "$_response" =~ ^(yes|y)$ ]] && return 1 return 0 } resetAll() { debug "${FUNCNAME[0]}" askOk "Remove ${BUILDROOT}/sources and ${BUILDROOT}/bin?" || exit $? debug "rm -rf ${BUILDROOT}/sources ${BUILDROOT}/bin" find "${BUILDROOT}/sources" "${BUILDROOT}/bin" \ ! -dir "$FILESDIR" -type f -delete + #rm -rf "${BUILDROOT}/sources" "${BUILDROOT}/bin" } resetProfile() { debug "${FUNCNAME[0]}" askOk "Remove ${P_ARR[sources_dir]} ${P_ARR[bin_dir]}?" || exit $? debug "rm -rf ${P_ARR[sources_dir]} ${P_ARR[bin_dir]}" rm -rf "${P_ARR[sources_dir]}" "${P_ARR[bin_dir]}" } loadProfiles() { debug "${FUNCNAME[0]}" declare -g PFILE # https://stackoverflow.com/a/4774063 PFILE="$SCRIPTDIR/profiles" # shellcheck source=./profiles ! source "$PFILE" && echo "profiles file missing!" && return 1 } init() { debug "${FUNCNAME[0]}" declare -g ID RPM_MGR SCRIPTDIR debug || echo "To enable debugging output, use --debug or -d" # Save the script directory SCRIPTDIR="$(cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit $? ; pwd -P)" if [[ -e "/etc/os-release" ]]; then source "/etc/os-release" else err "/etc/os-release not found" err "Your OS is unsupported" printHelp exit 1 fi debug "Detected host platform: $ID $VERSION_ID" # normalize distro ID case "$ID" in debian|arch) ;; centos|fedora) if hash dnf &>/dev/null; then RPM_MGR="dnf" elif hash 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 and --compat may also be required" if hash dnf &>/dev/null; then ID="fedora" RPM_MGR="dnf" elif hash yum &>/dev/null; then ID="centos" RPM_MGR="yum" elif hash apt &>/dev/null; then ID="ubuntu" elif hash pacman &>/dev/null; then ID="arch" else return 1 fi ;; esac debug "Using host platform: $ID $VERSION_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 -y -q0 "$@"; } ;; suse) pkg_install(){ sudo zypper --non-interactive -q install --force --no-confirm "$@"; } ;; arch) pkg_install(){ sudo pacman -S --noconfirm --needed "$@"; } ;; esac } main() { debug "${FUNCNAME[0]}" init loadProfiles readInput "$@" # Allow --reset without a profile if [[ ${#PROFILES} -lt 1 ]]; then if (( RESET )); then resetAll exit else echo "No profile supplied" && return 1 fi fi installDependencies for profile in "${PROFILES[@]}"; do debug "Profile: $profile" [[ ! ${!profile@a} = A ]] && echo "Profile does not exist" && return 1 # Set profile vars in the P_ARR array declare -gn P_ARR="$profile" # Fallback to SCRIPTDIR if BUILDROOT has not been set BUILDROOT="${BUILDROOT:=$SCRIPTDIR}" FILESDIR="${FILESDIR:=$BUILDROOT/files}" # precedence: user input>profile>env>hardcode P_ARR[release]="${USER_RELEASE:=${P_ARR[release]:=$RELEASE}}" P_ARR[build_dir]="$BUILDROOT/${P_ARR[profile]}-${P_ARR[release]}" P_ARR[sources_dir]="$BUILDROOT/sources" P_ARR[ib_archive]="${P_ARR[sources_dir]}/${P_ARR[profile]}-${P_ARR[release]}.tar.xz" P_ARR[bin_dir]="${P_ARR[build_dir]}" # shellcheck disable=SC2154 # TODO: I don't knwo why shellcheck is catching this if [[ "${P_ARR[release]}" == "snapshot" ]]; then P_ARR[out_prefix]="${P_ARR[bin_dir]}/openwrt-${P_ARR[target]//\//-}-${P_ARR[profile]}" P_ARR[url_prefix]="https://downloads.openwrt.org/snapshots/targets/${P_ARR[target]}" P_ARR[url_filename]="openwrt-imagebuilder-${P_ARR[target]//\//-}.Linux-x86_64.tar.xz" P_ARR[ib_url]="${P_ARR[url_prefix]}/${P_ARR[url_filename]}" else P_ARR[out_prefix]="${P_ARR[bin_dir]}/openwrt-${P_ARR[release]}-${P_ARR[target]//\//-}-${P_ARR[profile]}" P_ARR[url_prefix]="https://downloads.openwrt.org/releases/${P_ARR[release]}/targets/${P_ARR[target]}" P_ARR[url_filename]="openwrt-imagebuilder-${P_ARR[release]}-${P_ARR[target]//\//-}.Linux-x86_64.tar.xz" P_ARR[ib_url]="${P_ARR[url_prefix]}/${P_ARR[url_filename]}" fi P_ARR[factory_img]="${P_ARR[out_prefix]}-${P_ARR[filesystem]}-factory.img" P_ARR[factory_img_gz]="${P_ARR[factory_img]}.gz" P_ARR[sysupgrade_img]="${P_ARR[out_prefix]}-${P_ARR[filesystem]}-sysupgrade.img" P_ARR[sysupgrade_img_gz]="${P_ARR[sysupgrade_img]}.gz" #P_ARR[sysupgrade_bin]=$out_prefix-${P_ARR[filesystem]}-sysupgrade.img #P_ARR[sysupgrade_bin_fname]=${P_ARR[sysupgrade_bin]##*/} P_ARR[sysupgrade_bin_gz]="${P_ARR[sysupgrade_bin]}.gz" P_ARR[sysupgrade_bin_gz_fname]="${P_ARR[sysupgrade_bin_gz]##*/}" P_ARR[SHA256_URL]="${P_ARR[url_prefix]}/sha256sums" P_ARR[CONFIG_SEED]="${P_ARR[url_prefix]}" if (( DEBUG )) || (( PROFILE_INFO )); then echo "Profile settings:" for x in "${!P_ARR[@]}"; do printf "%s=%s\n" "$x" "${P_ARR[$x]}"; done fi (( RESET )) && resetProfile # Experimental (( FROM_SOURCE )) && fromSource getImageBuilder addRepos #copyFiles [[ -v SSH_BACKUP_PATH ]] && sshBackup if makeImage; then [[ -v SSH_UPGRADE_PATH ]] && sshUpgrade [[ -v FLASH_DEV ]] && flashImage fi done } main "$@" exit