openwrtbuilder 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. #!/usr/bin/env bash
  2. #
  3. # Copyright 2022 Bryan C. Roessler
  4. #
  5. # Build and flash/upgrade OpenWRT devices
  6. #
  7. # Apache 2.0 License
  8. # Set default release
  9. : "${RELEASE:="22.03.2"}"
  10. printHelp() {
  11. debug "${FUNCNAME[0]}"
  12. cat <<-'EOF'
  13. USAGE:
  14. openwrtbuilder [[OPTION] [VALUE]]...
  15. Run and deploy OpenWRT imagebuilder.
  16. OPTIONS
  17. --profile, -p PROFILE
  18. --info, -i PROFILE
  19. --list-profiles, -l
  20. --release, -r RELEASE
  21. --builddir, -b PATH
  22. --ssh-upgrade HOST
  23. Example: root@192.168.1.1
  24. --ssh-backup SSH_PATH
  25. (For testing, enabled by default for --ssh-upgrade)
  26. --flash, -f DEVICE
  27. Example: /dev/sdX
  28. --reset
  29. Cleanup all source and output files
  30. --debug, -d
  31. --help, -h
  32. EOF
  33. }
  34. readInput() {
  35. debug "${FUNCNAME[0]}"
  36. unset RESET
  37. if _input=$(getopt -o +r:v:p:i:lb:f:dh -l release:,profile:,info:,list-profiles,builddir:,ssh-upgrade:,ssh-backup:,flash:,reset,debug,help -- "$@"); then
  38. eval set -- "$_input"
  39. while true; do
  40. case "$1" in
  41. --release|-r)
  42. shift && RELEASE="$1"
  43. ;;
  44. --profile|-p)
  45. shift && PROFILE="$1"
  46. ;;
  47. --info|-i)
  48. shift && PROFILE="$1" && PROFILE_INFO=1 && exit $?
  49. ;;
  50. --list-profiles|-l)
  51. listProfiles && exit $?
  52. ;;
  53. --builddir|-b)
  54. shift && BUILDDIR="$1"
  55. ;;
  56. --ssh-upgrade)
  57. shift && SSH_UPGRADE_PATH="$1"
  58. ;;
  59. --ssh-backup)
  60. shift && SSH_BACKUP_PATH="$1"
  61. ;;
  62. --flash|-f)
  63. shift && FLASH_DEV="$1"
  64. ;;
  65. --reset)
  66. RESET=1
  67. ;;
  68. --debug|-d)
  69. echo "Debugging on"
  70. DEBUG=1
  71. ;;
  72. --help|-h)
  73. printHelp && exit 0
  74. ;;
  75. --)
  76. shift
  77. break
  78. ;;
  79. esac
  80. shift
  81. done
  82. else
  83. echo "Incorrect options provided"
  84. printHelp && exit 1
  85. fi
  86. }
  87. listProfiles() {
  88. debug "${FUNCNAME[0]}"
  89. grep "declare -Ag" "$PFILE" | cut -d" " -f3
  90. }
  91. installHostDependencies() {
  92. debug "${FUNCNAME[0]}"
  93. local -a _pkg_list
  94. local _pkg_cmd
  95. source /etc/os-release
  96. if [[ "$ID" =~ ^(fedora)$ ]]; then
  97. _pkg_list=(\
  98. "@c-development" \
  99. "@development-tools" \
  100. "@development-libs" \
  101. "perl-FindBin" \
  102. "zlib-static" \
  103. "elfutils-libelf-devel" \
  104. "gawk" \
  105. "unzip" \
  106. "file" \
  107. "wget" \
  108. "python3" \
  109. "python2" \
  110. "axel" \
  111. )
  112. _pkg_cmd="dnf"
  113. elif [[ "$ID" =~ ^(debian|ubuntu)$ ]]; then
  114. _pkg_list=(\
  115. "build-essential" \
  116. "libncurses5-dev" \
  117. "libncursesw5-dev" \
  118. "zlib1g-dev" \
  119. "gawk" \
  120. "git" \
  121. "gettext" \
  122. "libssl-dev" \
  123. "xsltproc" \
  124. "wget" \
  125. "unzip" \
  126. "python" \
  127. "axel" \
  128. )
  129. _pkg_cmd="apt-get"
  130. fi
  131. echo "Installing dependencies"
  132. debug "sudo $_pkg_cmd -y install ${_pkg_list[*]}"
  133. if ! sudo "$_pkg_cmd" -y install "${_pkg_list[@]}" > /dev/null 2>&1; then
  134. echo "Warning: Problem installing dependencies"
  135. return 1
  136. fi
  137. }
  138. getImageBuilder() {
  139. debug "${FUNCNAME[0]}"
  140. local _url _filename _dl_tool
  141. if [[ "${P_ARR[release]}" == "snapshot" ]]; then
  142. _filename="openwrt-imagebuilder-${P_ARR[target]//\//-}.Linux-x86_64.tar.xz"
  143. _url="https://downloads.openwrt.org/snapshots/targets/${P_ARR[target]}/$_filename"
  144. if [[ -f "${P_ARR[source_archive]}" ]]; then
  145. if askOk "Update ImageBuilder snapshot?"; then
  146. rm -f "${P_ARR[source_archive]}"
  147. else
  148. return 0
  149. fi
  150. fi
  151. else
  152. _filename="openwrt-imagebuilder-${P_ARR[release]}-${P_ARR[target]//\//-}.Linux-x86_64.tar.xz"
  153. _url="https://downloads.openwrt.org/releases/${P_ARR[release]}/targets/${P_ARR[target]}/$_filename"
  154. [[ -f "${P_ARR[source_archive]}" ]] && return 0 # Reuse existing ImageBuilders
  155. fi
  156. # Make sources directory if it does not exist
  157. [[ ! -d "$BUILDDIR/sources" ]] && mkdir -p "$BUILDDIR/sources"
  158. if hash axel &>/dev/null; then
  159. _dl_tool="axel"
  160. elif hash curl &>/dev/null; then
  161. _dl_tool="curl"
  162. else
  163. echo "Downloading the ImageBuilder requires axel or curl!"
  164. return 1
  165. fi
  166. #_dl_tool="curl" # TODO remove
  167. echo "Downloading imagebuilder archive using $_dl_tool"
  168. debug "$_dl_tool -o ${P_ARR[source_archive]} $_url"
  169. if ! "$_dl_tool" -o "${P_ARR[source_archive]}" "$_url" > /dev/null 2>&1; then
  170. echo "Could not download imagebuilder archive"
  171. exit 1
  172. fi
  173. if [[ ! -f "${P_ARR[source_archive]}" ]]; then
  174. echo "Archive missing"
  175. exit 1
  176. fi
  177. echo "Extracting image archive"
  178. [[ ! -d "${P_ARR[source_dir]}" ]] && mkdir -p "${P_ARR[source_dir]}"
  179. debug "tar -xf ${P_ARR[source_archive]} -C ${P_ARR[source_dir]} --strip-components 1"
  180. if ! tar -xf "${P_ARR[source_archive]}" -C "${P_ARR[source_dir]}" --strip-components 1; then
  181. echo "Extraction failed"
  182. exit 1
  183. fi
  184. }
  185. addRepos() {
  186. debug "${FUNCNAME[0]}"
  187. if [[ -v P_ARR[repo] ]]; then
  188. if ! grep -q "${P_ARR[repo]}" "${P_ARR[source_dir]}/repositories.conf"; then
  189. echo "${P_ARR[repo]}" >> "${P_ARR[source_dir]}/repositories.conf"
  190. fi
  191. sed -i '/option check_signature/d' "${P_ARR[source_dir]}/repositories.conf"
  192. fi
  193. }
  194. sshBackup() {
  195. debug "${FUNCNAME[0]}"
  196. local _date _hostname _backup_fname
  197. [[ -d "$FILESDIR" ]] || mkdir -p "$FILESDIR"
  198. printf -v _date '%(%Y-%m-%d-%H-%M-%S)T'
  199. _hostname=$(ssh -qt "$SSH_BACKUP_PATH" echo -n \$HOSTNAME)
  200. _backup_fname="backup-$_hostname-$_date.tar.gz"
  201. # Make backup archive on remote
  202. debug "ssh -t $SSH_BACKUP_PATH sysupgrade -b /tmp/$_backup_fname"
  203. if ! ssh -t "$SSH_BACKUP_PATH" "sysupgrade -b /tmp/$_backup_fname"; then
  204. echo "SSH backup failed"
  205. exit 1
  206. fi
  207. # Move backup archive locally
  208. debug "rsync -avz --remove-source-files $SSH_BACKUP_PATH:/tmp/$_backup_fname $BUILDDIR/"
  209. if ! rsync -avz --remove-source-files "$SSH_BACKUP_PATH":"/tmp/$_backup_fname" "$BUILDDIR/"; then
  210. echo "Could not copy SSH backup"
  211. exit 1
  212. fi
  213. # Extract backup archive
  214. debug "tar -C $FILESDIR -xzf $BUILDDIR/$_backup_fname"
  215. if ! tar -C "$FILESDIR" -xzf "$BUILDDIR/$_backup_fname"; then
  216. echo "Could not extract SSH backup"
  217. exit 1
  218. fi
  219. rm "$BUILDDIR/$_backup_fname"
  220. }
  221. makeImage() {
  222. debug "${FUNCNAME[0]}"
  223. declare _nprocs
  224. # Reuse the existing output
  225. if [[ -d "${P_ARR[out_bin_dir]}" ]]; then
  226. if askOk "${P_ARR[out_bin_dir]} exists. Rebuild?"; then
  227. rm -rf "${P_ARR[out_bin_dir]}"
  228. else
  229. return 0
  230. fi
  231. fi
  232. [[ ! -d "${P_ARR[out_bin_dir]}" ]] && mkdir -p "${P_ARR[out_bin_dir]}"
  233. # build image
  234. debug "make image BIN_DIR=${P_ARR[out_bin_dir]} PROFILE=${P_ARR[profile]} PACKAGES=${P_ARR[packages]} FILES=$FILESDIR --directory=${P_ARR[source_dir]} > make.log"
  235. if ! make image BIN_DIR="${P_ARR[out_bin_dir]}" PROFILE="${P_ARR[profile]}" PACKAGES="${P_ARR[packages]}" FILES="$FILESDIR" --directory="${P_ARR[source_dir]}" > make.log; then
  236. echo "Make image failed!"
  237. exit 1
  238. fi
  239. }
  240. flashImage() {
  241. debug "${FUNCNAME[0]}"
  242. local _umount
  243. if [[ ! -e "$FLASH_DEV" ]]; then
  244. echo "The device specified by --flash could not be found"
  245. exit 1
  246. fi
  247. # TODO Roughly chooses the correct image
  248. if [[ -f "${P_ARR[factory_img_gz]}" ]]; then
  249. img_gz="${P_ARR[factory_img_gz]}"
  250. img="${P_ARR[factory_img]}"
  251. elif [[ -f "${P_ARR[sysupgrade_img_gz]}" ]]; then
  252. img_gz="${P_ARR[sysupgrade_img_gz]}"
  253. img="${P_ARR[sysupgrade_img]}"
  254. else
  255. return 1
  256. fi
  257. debug "$img_gz $img"
  258. debug "gunzip -qfk $img_gz"
  259. gunzip -qfk "$img_gz"
  260. echo "Unmounting target device $FLASH_DEV partitions"
  261. _umount=( "$FLASH_DEV"?* )
  262. debug "umount ${_umount[*]}"
  263. sudo umount "${_umount[@]}"
  264. debug "sudo dd if=\"$img\" of=\"$FLASH_DEV\" bs=2M conv=fsync"
  265. if sudo dd if="$img" of="$FLASH_DEV" bs=2M conv=fsync; then
  266. sync
  267. echo "Image flashed sucessfully!"
  268. else
  269. echo "dd failed!"
  270. exit 1
  271. fi
  272. }
  273. sshUpgrade() {
  274. debug "${FUNCNAME[0]}"
  275. echo "Copying \"${P_ARR[sysupgrade_bin_gz]}\" to $SSH_UPGRADE_PATH/tmp/"
  276. debug "scp \"${P_ARR[sysupgrade_bin_gz]}\" \"$SSH_UPGRADE_PATH\":\"/tmp/${P_ARR[sysupgrade_bin_gz_fname]}\""
  277. # shellcheck disable=SC2140
  278. if ! scp "${P_ARR[sysupgrade_bin_gz]}" "$SSH_UPGRADE_PATH":"/tmp/${P_ARR[sysupgrade_bin_gz_fname]}"; then
  279. echo "Could not access the --ssh-upgrade PATH"
  280. exit 1
  281. fi
  282. echo "Executing remote sysupgrade"
  283. debug "ssh \"$SSH_UPGRADE_PATH\" \"sysupgrade -F /tmp/${P_ARR[sysupgrade_bin_gz_fname]}\""
  284. # shellcheck disable=SC2029
  285. ssh "$SSH_UPGRADE_PATH" "sysupgrade -F /tmp/${P_ARR[sysupgrade_bin_gz_fname]}"
  286. }
  287. debug() { (( DEBUG )) && echo "Running: $*"; }
  288. askOk() {
  289. local _response
  290. read -r -p "$* [y/N]" _response
  291. _response=${_response,,}
  292. [[ ! "$_response" =~ ^(yes|y)$ ]] && return 1
  293. return 0
  294. }
  295. reset() {
  296. debug "${FUNCNAME[0]}"
  297. askOk "Remove $FILESDIR $BUILDDIR/sources $BUILDDIR/bin?" || exit $?
  298. debug "rm -rf $FILESDIR $BUILDDIR/sources $BUILDDIR/bin"
  299. rm -rf "$FILESDIR" "${BUILDDIR:?}/sources" "${BUILDDIR:?}/bin"
  300. }
  301. loadProfiles() {
  302. debug "${FUNCNAME[0]}"
  303. declare -g SCRIPTDIR PFILE
  304. # https://stackoverflow.com/a/4774063
  305. SCRIPTDIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit $? ; pwd -P )"
  306. PFILE="$SCRIPTDIR/profiles"
  307. # shellcheck source=./profiles
  308. ! source "$PFILE" && echo "profiles file missing!" && return 1
  309. }
  310. main() {
  311. debug "${FUNCNAME[0]}"
  312. loadProfiles
  313. readInput "$@"
  314. [[ ! ${!PROFILE@a} = A ]] && echo "Profile does not exist" && return 1
  315. declare -gn P_ARR="$PROFILE"
  316. declare _out_prefix
  317. : "${BUILDDIR:=$SCRIPTDIR}"
  318. : "${FILESDIR:=$BUILDDIR/files}"
  319. : "${P_ARR[release]:=$RELEASE}"
  320. : "${P_ARR[source_archive]:=$BUILDDIR/sources/${P_ARR[profile]}-${P_ARR[release]}.tar.xz}"
  321. : "${P_ARR[source_dir]:=${P_ARR[source_archive]%.tar.xz}}"
  322. : "${P_ARR[out_bin_dir]:=$BUILDDIR/bin/${P_ARR[profile]}-${P_ARR[release]}}"
  323. if [[ "${P_ARR[release]}" == "snapshot" ]]; then
  324. _out_prefix="${P_ARR[out_bin_dir]}/openwrt-${P_ARR[target]//\//-}-${P_ARR[profile]}"
  325. else
  326. _out_prefix="${P_ARR[out_bin_dir]}/openwrt-${P_ARR[release]}-${P_ARR[target]//\//-}-${P_ARR[profile]}"
  327. fi
  328. : "${P_ARR[factory_img]:=$_out_prefix-${P_ARR[filesystem]}-factory.img}"
  329. : "${P_ARR[factory_img_gz]:=${P_ARR[factory_img]}.gz}"
  330. : "${P_ARR[sysupgrade_img]:=$_out_prefix-${P_ARR[filesystem]}-sysupgrade.img}"
  331. : "${P_ARR[sysupgrade_img_gz]:=${P_ARR[sysupgrade_img]}.gz}"
  332. : "${P_ARR[sysupgrade_bin]:=$_out_prefix-${P_ARR[filesystem]}-sysupgrade.img}"
  333. : "${P_ARR[sysupgrade_bin_fname]:=${P_ARR[sysupgrade_bin]##*/}}"
  334. : "${P_ARR[sysupgrade_bin_gz]:=${P_ARR[sysupgrade_bin]}.gz}"
  335. : "${P_ARR[sysupgrade_bin_gz_fname]:=${P_ARR[sysupgrade_bin_gz]##*/}}"
  336. (( RESET )) && reset
  337. if (( DEBUG )) || (( PROFILE_INFO )); then
  338. for x in "${!P_ARR[@]}"; do printf "[%s]=%s\n" "$x" "${P_ARR[$x]}"; done
  339. fi
  340. installHostDependencies
  341. getImageBuilder
  342. addRepos
  343. #copyFiles
  344. [[ -v SSH_BACKUP_PATH ]] && sshBackup
  345. if makeImage; then
  346. [[ -v SSH_UPGRADE_PATH ]] && sshUpgrade
  347. [[ -v FLASH_DEV ]] && flashImage
  348. fi
  349. }
  350. main "$@"
  351. exit