#!/bin/bash # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # Code shared by update packaging scripts. # Author: Darin Fisher # # ----------------------------------------------------------------------------- # Preserve a value set by the parent so it survives in the child processes # spawned by run_parallel_tasks() QUIET=${QUIET:-0} # By default just assume that these tools exist on our path MAR=${MAR:-mar} MBSDIFF=${MBSDIFF:-mbsdiff} XZ=${XZ:-xz} $XZ --version > /dev/null 2>&1 if [ $? -ne 0 ]; then # If $XZ is not set and not found on the path then this is probably # running on a windows buildbot. Some of the Windows build systems have # xz.exe in topsrcdir/xz/. Look in the places this would be in both a # mozilla-central and comm-central build. XZ="$(dirname "$(dirname "$(dirname "$0")")")/xz/xz.exe" $XZ --version > /dev/null 2>&1 if [ $? -ne 0 ]; then XZ="$(dirname "$(dirname "$(dirname "$(dirname "$0")")")")/xz/xz.exe" $XZ --version > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "xz was not found on this system!" echo "exiting" exit 1 fi fi fi # Ensure that we're always using the right compression settings. # -T1 (single-threaded) is required: multi-threaded xz produces non-deterministic # output, which would break both the reproducibility of MARs and the content-hash # cache in process_update_task(). See get_cache_tag() for how changes here are # accounted for in the cache. export XZ_OPT="-T1 -7e" # ----------------------------------------------------------------------------- # Helper routines notice() { echo "$*" 1>&2 } verbose_notice() { if [ $QUIET -eq 0 ]; then notice "$*" fi } get_file_size() { info=($(ls -ln "$1")) echo ${info[4]} } copy_perm() { reference="$1" target="$2" if [ -x "$reference" ]; then chmod 0755 "$target" else chmod 0644 "$target" fi } make_add_instruction() { f="$1" filev3="$2" # Used to log to the console if [ $4 ]; then forced=" (forced)" else forced= fi # Changed by Zotero for -e is_extension=$(echo "$f" | grep -c 'distribution/extensions/.*/') || true if [ $is_extension = "1" ]; then # Use the subdirectory of the extensions folder as the file to test # before performing this add instruction. testdir=$(echo "$f" | sed 's/\(.*distribution\/extensions\/[^\/]*\)\/.*/\1/') verbose_notice " add-if \"$testdir\" \"$f\"" echo "add-if \"$testdir\" \"$f\"" >> "$filev3" else verbose_notice " add \"$f\"$forced" echo "add \"$f\"" >> "$filev3" fi } check_for_add_if_not_update() { add_if_not_file_chk="$1" if [[ "$(basename "$add_if_not_file_chk")" = "channel-prefs.js" || \ "$add_if_not_file_chk" =~ (^|/)ChannelPrefs\.framework/ || \ "$(basename "$add_if_not_file_chk")" = "update-settings.ini" || \ "$add_if_not_file_chk" =~ (^|/)UpdateSettings\.framework/ ]]; then ## "true" return 0; fi ## "false" return 1; } make_add_if_not_instruction() { f="$1" filev3="$2" verbose_notice " add-if-not \"$f\" \"$f\"" echo "add-if-not \"$f\" \"$f\"" >> "$filev3" } make_patch_instruction() { f="$1" filev3="$2" # Changed by Zotero for -e is_extension=$(echo "$f" | grep -c 'distribution/extensions/.*/') || true if [ $is_extension = "1" ]; then # Use the subdirectory of the extensions folder as the file to test # before performing this add instruction. testdir=$(echo "$f" | sed 's/\(.*distribution\/extensions\/[^\/]*\)\/.*/\1/') verbose_notice " patch-if \"$testdir\" \"$f.patch\" \"$f\"" echo "patch-if \"$testdir\" \"$f.patch\" \"$f\"" >> "$filev3" else verbose_notice " patch \"$f.patch\" \"$f\"" echo "patch \"$f.patch\" \"$f\"" >> "$filev3" fi } append_remove_instructions() { dir="$1" filev3="$2" if [ -f "$dir/removed-files" ]; then listfile="$dir/removed-files" elif [ -f "$dir/Contents/Resources/removed-files" ]; then listfile="$dir/Contents/Resources/removed-files" fi if [ -n "$listfile" ]; then # Changed by Zotero: Use subshell and disable filename globbing to prevent bash from expanding # entries in removed-files with paths from the root (e.g., 'xulrunner/*') ( set -f # Map spaces to pipes so that we correctly handle filenames with spaces. files=($(cat "$listfile" | tr " " "|" | sort -r)) num_files=${#files[*]} for ((i=0; $i<$num_files; i=$i+1)); do # Map pipes back to whitespace and remove carriage returns f=$(echo ${files[$i]} | tr "|" " " | tr -d '\r') # Trim whitespace f=$(echo $f) # Exclude blank lines. if [ -n "$f" ]; then # Exclude comments if [ ! $(echo "$f" | grep -c '^#') = 1 ]; then if [ $(echo "$f" | grep -c '/$') = 1 ]; then verbose_notice " rmdir \"$f\"" echo "rmdir \"$f\"" >> "$filev3" elif [ $(echo "$f" | grep -c '/\*$') = 1 ]; then # Remove the * f=$(echo "$f" | sed -e 's:\*$::') verbose_notice " rmrfdir \"$f\"" echo "rmrfdir \"$f\"" >> "$filev3" else verbose_notice " remove \"$f\"" echo "remove \"$f\"" >> "$filev3" fi fi fi done ) fi } # Return the number of parallel jobs to use for diffing/compression, from # UPDATE_PACKAGING_JOBS or the number of cores get_parallel_jobs() { if [ -n "${UPDATE_PACKAGING_JOBS:-}" ]; then echo "$UPDATE_PACKAGING_JOBS" return 0 fi nproc 2>/dev/null && return 0 sysctl -n hw.ncpu 2>/dev/null && return 0 echo 1 } # Run per-file tasks from a task file in parallel by re-invoking the calling # script with --run-task for each line. Tasks are ordered largest-file-first so # that long-running jobs (e.g., diffing xul) start as early as possible. # # $1 - task file, with one "" task per line # $2 - directory to measure file sizes against for job ordering # $3 - script to re-invoke (callers pass "$0" and handle --run-task before # option parsing by calling process_update_task) # # Callers must export the variables needed by process_update_task(). run_parallel_tasks() { local taskfile="$1" local basedir="$2" local script="$3" local jobs if [ ! -s "$taskfile" ]; then return 0 fi # xargs requires a resolvable command path script="$(cd "$(dirname "$script")" && pwd)/$(basename "$script")" # Compute the cache tag once here and export it so the per-file child # processes inherit it rather than recomputing it for every file if [ -n "${UPDATE_CACHE_DIR:-}" ] && [ -z "${UPDATE_CACHE_TAG:-}" ]; then UPDATE_CACHE_TAG="$(get_cache_tag)" export UPDATE_CACHE_TAG fi jobs=$(get_parallel_jobs) notice "Processing $(wc -l < "$taskfile" | tr -d ' ') files with $jobs parallel jobs" (cd "$basedir" && du -ak .) > "$taskfile.sizes" awk -F'\t' 'NR==FNR { sizes[substr($2, 3)] = $1; next } { print sizes[$2] "\t" $0 }' \ "$taskfile.sizes" "$taskfile" \ | sort -rn \ | cut -f 2- \ | tr '\n' '\000' \ | xargs -0 -n 1 -P "$jobs" "$script" --run-task } # ----------------------------------------------------------------------------- # Diff and compression cache # # Building updates repeats the same expensive work: the same file contents get # xz-compressed once per architecture, per FROM version, and per release, and # the same before/after file pair gets binary-diffed again and again. When # UPDATE_CACHE_DIR is set, we cache the results by content hash and reuse them # across builds. Two kinds of entry are stored: # # full--.xz A single file compressed with xz, keyed # on the file's contents. Reused whenever # the same content shows up again, in any # architecture, version, or release. # # patch---.xz The compressed binary diff between a # file's old and new contents, keyed on # both. Reused whenever the same # before/after pair recurs (e.g., a file # unchanged across several FROM versions # being patched to the same TO). # # Both entries are written even when only one is ultimately packaged, so a file # that loses the patch-vs-full size comparison in one build still seeds the # cache for the next. is get_cache_tag(), so a toolchain/settings change # starts a fresh namespace rather than reusing stale output. Entries are touched # on use and expire via the time-based cleanup in build_autoupdate.sh. # ----------------------------------------------------------------------------- # Print a SHA-256 content hash to use as a cache key hash_file() { if command -v sha256sum > /dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' else shasum -a 256 "$1" | awk '{print $1}' fi } # Print a SHA-256 hash of stdin hash_stdin() { if command -v sha256sum > /dev/null 2>&1; then sha256sum | awk '{print $1}' else shasum -a 256 | awk '{print $1}' fi } # Print a short tag identifying the toolchain and options that produce cached # files, so that entries are invalidated when any of them change. The cache # keys files by input content only, on the assumption that the same inputs # always compress/diff to the same output; this tag makes that assumption hold # across xz/mbsdiff upgrades and XZ_OPT/BCJ_OPTIONS changes. (A change to any # input invalidates both the full and patch caches, which is more than strictly # necessary but keeps this simple; a stale build is just a cold rebuild.) # Stale-tag entries are removed by the time-based cleanup in build_autoupdate.sh. get_cache_tag() { local mbsdiff_bin mbsdiff_bin="$(command -v "$MBSDIFF" 2>/dev/null)" { $XZ --version 2>/dev/null | head -1 echo "XZ_OPT=$XZ_OPT" echo "BCJ_OPTIONS=${BCJ_OPTIONS:-}" # mbsdiff has no --version, so identify it by its binary contents if [ -n "$mbsdiff_bin" ]; then hash_file "$mbsdiff_bin" else echo "MBSDIFF=$MBSDIFF" fi } | hash_stdin | cut -c 1-16 } # Atomically store file $2 in the cache as $1 cache_store() { mkdir -p "$(dirname "$1")" cp "$2" "$1.tmp.$$" && mv -f "$1.tmp.$$" "$1" } # xz-compress $1 to $2, reusing a cached result when one exists (see the cache # overview above) compress_full_file() { local src="$1" local dest="$2" local key="" if [ -n "${UPDATE_CACHE_DIR:-}" ]; then key="$UPDATE_CACHE_DIR/full-${UPDATE_CACHE_TAG:-}-$(hash_file "$src").xz" if [ -f "$key" ]; then # Keep entries that are still in use from expiring touch -c "$key" 2>/dev/null || true cp "$key" "$dest" return 0 fi fi $XZ $XZ_OPT --compress $BCJ_OPTIONS --lzma2 --format=xz --check=crc64 --force --stdout "$src" > "$dest" if [ -n "$key" ]; then cache_store "$key" "$dest" fi } # Process a single task line from run_parallel_tasks(): # # full - compress the new file into the work directory # diff - if the file changed, generate a binary diff and keep the smaller of # the compressed patch ($f.patch) and the compressed file ($f) # # Expects newdir and workdir in the environment, plus olddir for diff tasks. # The results are assembled into the manifest serially by the caller based on # which files exist in the work directory. Diffs and compressed files are # cached when enabled -- see the cache overview above. process_update_task() { local task="$1" local tab=$(printf '\t') local type="${task%%"$tab"*}" local f="${task#*"$tab"}" local oldfile_path newfile_path patch_path patchsize fullsize full_pid patchkey if [ "$type" = "diff" ]; then if diff "$olddir/$f" "$newdir/$f" > /dev/null; then # Unchanged return 0 fi mkdir -p "$(dirname "$workdir/$f")" verbose_notice "diffing \"$f\"" # Compress the full new file in the background while diffing, since both # are needed to decide which to package compress_full_file "$newdir/$f" "$workdir/$f" & full_pid=$! patchkey="" if [ -n "${UPDATE_CACHE_DIR:-}" ]; then patchkey="$UPDATE_CACHE_DIR/patch-${UPDATE_CACHE_TAG:-}-$(hash_file "$olddir/$f")-$(hash_file "$newdir/$f").xz" fi if [ -n "$patchkey" ] && [ -f "$patchkey" ]; then # Keep entries that are still in use from expiring touch -c "$patchkey" 2>/dev/null || true cp "$patchkey" "$workdir/$f.patch.xz" else # mbsdiff doesn't like POSIX paths on Windows if [ ${WIN_NATIVE:-0} -eq 1 ]; then oldfile_path=$(cygpath -m "$olddir/$f") newfile_path=$(cygpath -m "$newdir/$f") patch_path=$(cygpath -m "$workdir/$f.patch") else oldfile_path="$olddir/$f" newfile_path="$newdir/$f" patch_path="$workdir/$f.patch" fi $MBSDIFF "$oldfile_path" "$newfile_path" "$patch_path" $XZ $XZ_OPT --compress --lzma2 --format=xz --check=crc64 --force "$workdir/$f.patch" if [ -n "$patchkey" ]; then cache_store "$patchkey" "$workdir/$f.patch.xz" fi fi wait $full_pid copy_perm "$newdir/$f" "$workdir/$f" patchsize=$(get_file_size "$workdir/$f.patch.xz") fullsize=$(get_file_size "$workdir/$f") if [ $patchsize -lt $fullsize ]; then mv -f "$workdir/$f.patch.xz" "$workdir/$f.patch" rm -f "$workdir/$f" else rm -f "$workdir/$f.patch.xz" fi else mkdir -p "$(dirname "$workdir/$f")" compress_full_file "$newdir/$f" "$workdir/$f" copy_perm "$newdir/$f" "$workdir/$f" fi } # List all files in the current directory, stripping leading "./" # Pass a variable name and it will be filled as an array. list_files() { count=0 temp_filelist=$(mktemp) find . -type f \ ! -name "update.manifest" \ ! -name "updatev2.manifest" \ ! -name "updatev3.manifest" \ | sed 's/\.\/\(.*\)/\1/' \ | sort -r > "${temp_filelist}" while read file; do eval "${1}[$count]=\"$file\"" # Changed for Zotero to avoid eval as 1 #(( count++ )) (( ++count )) done < "${temp_filelist}" rm "${temp_filelist}" } # List all directories in the current directory, stripping leading "./" list_dirs() { count=0 temp_dirlist=$(mktemp) find . -type d \ ! -name "." \ ! -name ".." \ | sed 's/\.\/\(.*\)/\1/' \ | sort -r > "${temp_dirlist}" while read dir; do eval "${1}[$count]=\"$dir\"" # Changed for Zotero #(( count++ )) (( ++count )) done < "${temp_dirlist}" rm "${temp_dirlist}" }