#!/bin/sh # SPDX-License-Identifier: GPL-2.0 # # Copy firmware files based on WHENCE list # verbose=: compress=cat compext= destdir= num_jobs=1 usage() { echo "Usage: $0 [-v] [-jN] [--xz|--zstd] " } err() { printf "ERROR: %s\n" "$*" usage exit 1 } warn() { printf "WARNING: %s\n" "$*" } has_gnu_parallel() { command -v parallel > /dev/null || return 1 # moreutils parallel doesn't support --version; if it fails, not GNU parallel --version > /dev/null 2>&1 || return 1 # Use tolower() instead of IGNORECASE (gawk-only extension) parallel --version 2>/dev/null \ | awk '{if(tolower($0)~/gnu parallel/)found=1} END{exit !found}' } while test $# -gt 0; do case $1 in -v | --verbose) verbose=echo shift ;; -j*) num_jobs=$(echo "$1" | sed 's/-j//') num_jobs=${num_jobs:-1} if [ "$num_jobs" -gt 1 ] && ! has_gnu_parallel; then err "the GNU parallel command is required to use -j" fi parallel_args_file=$(mktemp) trap 'rm -f $parallel_args_file' EXIT INT QUIT TERM shift ;; --xz) if test "$compext" = ".zst"; then err "cannot mix XZ and ZSTD compression" fi compress="xz --compress --quiet --stdout --check=crc32" compext=".xz" shift ;; --zstd) if test "$compext" = ".xz"; then err "cannot mix XZ and ZSTD compression" fi compress="zstd --compress --quiet --stdout" compext=".zst" shift ;; -h|--help) usage exit 1 ;; -*) warn "ignoring option $1" shift ;; *) if test -n "$destdir"; then err "unknown command-line options: $*" fi destdir="$1" shift ;; esac done if test -z "$destdir"; then err "destination directory was not specified" fi if test -d "$destdir"; then # BusyBox find does not support -empty; count entries instead if [ "$(find "$destdir" | wc -l)" -gt 1 ]; then warn "destination folder is not empty." fi fi if test -e .git/config; then $verbose "Checking that WHENCE file is formatted properly" ./check_whence.py || err "check_whence.py has detected errors." fi # sed -E is GNU-only; rewrite with basic BRE substitutions instead. # File/RawFile lines: strip the keyword prefix and any surrounding quotes, # then emit "K\tFILENAME" (tab-separated) so filenames with spaces are safe. grep -E '^(RawFile|File):' WHENCE \ | sed 's/^RawFile:/RawFile/;s/^File:/File/;s/"//g' \ | awk '{k=$1; sub(/^[^ ]+ +/,""); printf "%s\t%s\n", k, $0}' \ | while IFS="$(printf '\t')" read -r k f; do install -d "$destdir/$(dirname "$f")" $verbose "copying/compressing file $f$compext" if test "$compress" != "cat" && test "$k" = "RawFile"; then $verbose "compression will be skipped for file $f" if [ "$num_jobs" -gt 1 ]; then printf 'cat\t%s\t%s\n' "$f" "$destdir/$f" >> "$parallel_args_file" else cat "$f" > "$destdir/$f" fi else if [ "$num_jobs" -gt 1 ]; then printf '%s\t%s\t%s\n' "$compress" "$f" "$destdir/$f$compext" >> "$parallel_args_file" else $compress "$f" > "$destdir/$f$compext" fi fi done if [ "$num_jobs" -gt 1 ]; then # Tab-separated columns: {1}=cmd+args {2}=src {3}=dest # parallel --colsep splits on the tab so spaces in paths are safe. parallel -j"$num_jobs" --colsep '\t' '{1} "{2}" > "{3}"' \ < "$parallel_args_file" printf '' > "$parallel_args_file" fi # Link lines look like: Link: some/name with spaces -> target with spaces # Split on ' -> ' as a unit using awk so spaces inside names are preserved. grep -E '^Link:' WHENCE \ | sed 's/^Link: *//' \ | awk '{ idx = index($0, " -> ") if (idx > 0) { printf "%s\t%s\n", substr($0,1,idx-1), substr($0,idx+4) } }' \ | while IFS="$(printf '\t')" read -r l t; do directory="$destdir/$(dirname "$l")" install -d "$directory" # BusyBox realpath lacks -m (allow non-existent) and -s (no symlink # resolution). Replicate by walking each component manually. target=$( cd "$directory" 2>/dev/null || exit 1 result=$(pwd) set -f IFS=/ for part in $t; do case "$part" in '') ;; '.') ;; '..') result=$(dirname "$result") ;; *) result="$result/$part" ;; esac done printf '%s\n' "$result" ) if test -e "$target"; then $verbose "creating link $l -> $t" if [ "$num_jobs" -gt 1 ]; then printf 'ln -s\t%s\t%s\n' "$t" "$destdir/$l" >> "$parallel_args_file" else ln -s "$t" "$destdir/$l" fi else $verbose "creating link $l$compext -> $t$compext" if [ "$num_jobs" -gt 1 ]; then printf 'ln -s\t%s\t%s\n' \ "$t$compext" "$destdir/$l$compext" >> "$parallel_args_file" else ln -s "$t$compext" "$destdir/$l$compext" fi fi done if [ "$num_jobs" -gt 1 ]; then parallel -j"$num_jobs" --colsep '\t' '{1} "{2}" "{3}"' \ < "$parallel_args_file" fi # Verify no broken symlinks. # BusyBox find has no -xtype; use -type l then resolve each target manually. # Relative targets are relative to the symlink's own directory. find "$destdir" -type l | while read -r lnk; do lnk_target=$(readlink "$lnk") case "$lnk_target" in /*) check="$lnk_target" ;; *) check="$(dirname "$lnk")/$lnk_target" ;; esac if ! test -e "$check"; then printf "ERROR: Broken symlink: %s -> %s\n" "$lnk" "$lnk_target" >&2 fi done find "$destdir" -type l | while read -r lnk; do lnk_target=$(readlink "$lnk") case "$lnk_target" in /*) check="$lnk_target" ;; *) check="$(dirname "$lnk")/$lnk_target" ;; esac test -e "$check" || exit 1 done || err "Broken symlinks found in $destdir" exit 0 # vim: et sw=4 sts=4 ts=4