#!/bin/bash # Freetz patch tool with optional auto-fix feature # # This script can either be dot-included via '. tools/freetz_patch' to get an # in-process definition of the 'modpatch' shell function or executed directly. # The execution mode will be determined by checking if $0 has the base name # 'freetz_patch'. # # The functionality has evolved from the original "modpatch" function in # Freetz's 'fwmod' script (cf. copyright information there). # The additional auto-fix feature for self-healing patches was developed by # Alexander Kriegisch (kriegaex), 2007-06-25. # MD5 extension added by cuma, 2011 helpmsg() { cat >&2 << EOF $freetz_patch_name - Freetz patch tool with optional auto-fix feature Usage: $freetz_patch_name [md5-source] target-dir - target directory to apply patch to patch-file - patch file name (unified context diff, patch level -p0) md5-dir - directory with patch files, named "**.patch" md5-source - from which file md5 is computed (optional) if this is not set, the first file in .patch is used Examples: $freetz_patch_name source/my-package patches/my.patch $freetz_patch_name another/package another.patch 2 $freetz_patch_name third/package patches-directory/ $freetz_patch_name fourth/package patches-directory/ somewhere/file_for_md5 Environment variables changing behaviour: FREETZ_VERBOSITY_LEVEL - verbose output, if >= 2 VERBOSE - verbose output, if == '-v' AUTO_FIX_PATCHES - try to auto-fix fuzzy non-md5 patches, if == 'y' EOF } modpatch() { if [ $# -ne 2 -a $# -ne 3 ]; then helpmsg return 1 fi # Check prerequisites for auto-fix if [ "$AUTO_FIX_PATCHES" == "y" ]; then for tool in lsdiff filterdiff; do which $tool > /dev/null || error 1 "$freetz_patch_name: tool $tool needed for auto-fix mode, please install" done fi local is_verbose="" if [ "$FREETZ_VERBOSITY_LEVEL" ] && [ "$FREETZ_VERBOSITY_LEVEL" -ge 2 ] || [ "$VERBOSE" == "-v" ]; then is_verbose="y" fi local _auto_fix_patches="$AUTO_FIX_PATCHES" local target_dir="$1" local patch_file="$2" ## md5 start if [ -d "$patch_file" ]; then _auto_fix_patches="" local patch_target="$3" if [ -z "$patch_target" ]; then patch_target="$(grep -m1 '^+++ ' $patch_file/*.patch 2>/dev/null | sed -r -n 's,.*patch:[+]{3} ([^\t]*)(\t.*)?,\1,p' | sort -u)" echo "$patch_target" | grep -q " " && patch_target="" [ -z "$patch_target" ] && error 2 "modpatch: Could not determine target file" fi [ -e "$target_dir"/"$patch_target" ] || error 2 "modpatch: Target file $patch_target does not exist" # System's md5sum is used if busybox applet is not available, eg while download of busybox (see #1535) local MD5SUM=$(dirname $0)/md5sum [ ! -x $MD5SUM ] && MD5SUM="$(which md5sum)" local target_md5="$($MD5SUM "$target_dir"/"$patch_target" | sed -n 's/\([a-f0-9]*\) .*$/\1/p')" local md5_patch="$(find "$patch_file" -name *${target_md5}*.patch)" if [ -z "$md5_patch" ]; then [ -n "$target_md5" ] && local md5_error="for $patch_target (MD5: $target_md5)" error 2 "modpatch: No matching patch found in $patch_file $md5_error" else patch_file="$md5_patch" fi fi ## md5 end [ ! -e $patch_file ] && error 2 "modpatch: Could not find patch-file $patch_file" local backup local do_fix if [ "$_auto_fix_patches" == "y" ]; then local output=$(patch --dry-run -d "$target_dir" -p0 < "$patch_file" 2> /dev/null) if [ $? -eq 0 ] && echo "$output" | grep -sqE '^Hunk '; then # Is any target file patched more than once per patch? -> skip auto-fix local multi_patch_files="$(lsdiff "$patch_file" | sort | uniq -cd)" if [ "$multi_patch_files" ]; then # Developer note: If you encounter this warning and want to make # a patch auto-fix-friendly, either split it into two patch files # (recommended because a patch should not contain multiple change # sets for one file) or consolidate multiple change sets into one. echo2 "warning: cannot auto-fix $patch_file, multiple change sets found for the following file(s):" >&2 echo2 "$multi_patch_files" >&2 else do_fix="y" backup="-b " fi fi fi local STATUS if [ "$is_verbose" ]; then echo2 "applying patch file $patch_file" patch --force ${backup}-d "$target_dir" -p0 --no-backup-if-mismatch < "$patch_file" 2>&1 | sed -e "s/^/${L2}/g" STATUS=${PIPESTATUS[0]} echo2 -- "----------------------------------------------------------------------" else patch --force ${backup}-d "$target_dir" -p0 --no-backup-if-mismatch < "$patch_file" > /dev/null STATUS=$? fi if [ $STATUS -gt 0 ]; then error 2 "modpatch: Error in patch-file $patch_file" elif [ "$_auto_fix_patches" == "y" ] && [ "$do_fix" == "y" ]; then # Create (temporary) clean patch file copy without any comments in between change sets filtered_file="$(mktemp _filtered_patch.XXXXXXXX)" || exit 1 filterdiff "$patch_file" > "$filtered_file" # Arrays of files to be patched and lines where patches begin in orginal and clean patch file IFS=$'\n' local files=( $(lsdiff -n "$patch_file" | sed -r 's/^[0-9]+[[:space:]]+(.*)/\1/') ) local patch_lines=( $(lsdiff -n "$patch_file" | grep -Eo '^[0-9]+') ) local filtered_lines=( $(lsdiff -n "$filtered_file" | grep -Eo '^[0-9]+') ) unset IFS rm -f "$filtered_file" # Number of files to be patched should be the same in original and clean patch file local file_count=${#files[@]} test $file_count = ${#patch_lines[@]} -a $file_count = ${#filtered_lines[@]} || exit 1 echo2 "auto-fixing fuzzy patch file $patch_file" #echo2 " file_count = $file_count" #echo2 " files = ${files[*]}" #echo2 " patch_lines = ${patch_lines[*]}" #echo2 " filtered_lines = ${filtered_lines[*]}" # Because we are going 'cd' soon, we need an absolute path to the patch file now local patch_file="$(readlink -f "$patch_file")" mv -f "$patch_file" "$patch_file.orig" cd "$target_dir" for (( i=0; i<$file_count; i++ )); do # Locate patch comment for each file local delta=$(( patch_lines[i] - filtered_lines[i] )) local hdr_len=$(( delta - delta_old )) local from=$(( patch_lines[i] - hdr_len )) local to=$(( from + hdr_len - 1 )) local delta_old=$delta #echo2 " $delta, $hdr_len, $from, $to" # Cut & paste comment (if any) from original into auto-fixed patch file if [ $to -ge $from ]; then eval "sed -n '$from,${to}p' \"\$patch_file.orig\" >> \"\$patch_file\"" fi local _file="${files[i]}" # If backup exists, but is unreadable and has a size of zero, it # was created as a placeholder by 'patch -b' previously and can be # safely deleted. It even must be deleted in order to avoid a diff # error because it is unreadable. if [ -f "$_file.orig" -a ! -r "$_file.orig" -a ! -s "$_file.orig" ]; then rm -f "$_file.orig" fi # Auto-fix possibly fuzzy patch by re-diffing patched file against original diff -Naur --label "$_file" --label "$_file" "$_file.orig" "$_file" >> "$patch_file" rm -f "$_file.orig" done cd - > /dev/null echo2 -- "----------------------------------------------------------------------" fi } # Include freetz_functions if not already done by fwmod if ! declare -F | cut -d ' ' -f 3 | grep -q echoX; then # Let's hope we find the right file no matter if we are sourced or # stand-alone if [ "$(basename "$0")" == "freetz_patch" ] && [ -f "$(dirname "$0")/freetz_functions" ]; then source "$(dirname "$0")/freetz_functions" elif [ "$TOOLS_DIR" ] && [ -f "$TOOLS_DIR/freetz_functions" ]; then source "$TOOLS_DIR/freetz_functions" elif [ -f tools/freetz_functions ]; then source tools/freetz_functions else echo "tools/freetz_functions not found" >&2 exit 1 fi fi # Direct script call? -> delegate parameters to shell function if [ "$(basename "$0")" == "freetz_patch" ]; then freetz_patch_name="freetz_patch" modpatch "$@" else freetz_patch_name="modpatch" fi # Strip one directory level from one patch file # # The only parameter is the name of the patch file to be stripped in place - # no backup! The patch file must be a unidiff, and no care is taken of spaces, # tabs or slashes within path names. # # Note: This is just an inofficial little add-on and not part of the official # Freetz patch procedure, use at own risk. The function might come in handy if # you wish to canonise a set of patch files so they can be applied with patch # level zero (-p0). strip_patch_level() { echo "Stripping one directory level from $1" sed -i -r 's/^(---|\+\+\+) [^/]+\//\1 /' "$1" }