#!/bin/bash # # REQUIRED: ReferenceDir must already exist. It may be created by untaring a # previous distribution. # # Error handling set -e # Exit on error set -o pipefail # Version information OLD_BUILD=10 OLD_VERSION="2.14.0.$OLD_BUILD" NEW_BUILD=11 NEW_VERSION="2.14.0.$NEW_BUILD" # Directory structure CHANGES_DIR=mux REFERENCE_DIR=mux2.14_$OLD_BUILD DISTRO_DIR=mux2.14 NEW_DIR=mux2.14_$NEW_BUILD echo "Checking for required tools..." if ! command -v xdelta3 &> /dev/null; then echo "xdelta3 is required but not found. Please install using apt-cyg install xdelta3" exit 1 fi # Validate the table-of-contents lists before touching anything. # # The lists are the whole packaging model: a distribution is the PREVIOUS # release plus whatever these name. A list that contradicts itself or names # a file that no longer exists produces a wrong tarball SILENTLY -- every # checksum and every signature over the result still verifies, because the # archive is intact. It is just incomplete. That is how 2.14.0.9 and # 2.14.0.10 shipped a win32 source zip missing six sources netmux.vcxproj # requires: they were on the copy list AND the delete list, and the delete # runs second. # # Two invariants, both cheap, both failing loudly: # 1. removed n (patchable u unpatched) = 0 -- a file cannot both ship and # be deleted; whichever the script does last wins by accident. # 2. everything listed to ship exists in the tree -- catches paths left # behind by a move. # # The stronger check (every source the BUILD SYSTEM names is present in the # output) cannot run here: it needs the packaged tree, and it is # platform-specific -- Makefile.am on Unix, netmux.vcxproj on Windows. That # seam is exactly where the six hid, so run it against the built artifact. # validate_toc_lists() { local dist_type=$1 local errors=0 local ship_list conflict f ship_list=$(cat "win32/TOC.$dist_type.patchable" "win32/TOC.$dist_type.unpatched" \ | sed 's/\r$//' | grep -v '^[[:space:]]*$' | sort -u) conflict=$(comm -12 <(echo "$ship_list") \ <(sed 's/\r$//' "win32/TOC.$dist_type.removed" \ | grep -v '^[[:space:]]*$' | sort -u)) if [ -n "$conflict" ]; then echo "ERROR: win32/TOC.$dist_type.* lists both ship and delete these files:" echo "$conflict" | sed 's/^/ /' echo " A file on the removed list is deleted after it is copied, so it" echo " never ships. Remove it from one list or the other." errors=$((errors + 1)) fi while IFS= read -r f; do [ -z "$f" ] && continue if [ ! -e "$CHANGES_DIR/$f" ]; then echo "ERROR: win32/TOC.$dist_type.* lists '$f', which is not in $CHANGES_DIR/." errors=$((errors + 1)) fi done <<< "$ship_list" return $errors } echo "Validating table-of-contents lists..." toc_errors=0 for dist_type in src bin; do validate_toc_lists "$dist_type" || toc_errors=$((toc_errors + $?)) done if [ "$toc_errors" -ne 0 ]; then echo "Refusing to build a distribution from inconsistent file lists." exit 1 fi echo " TOC lists consistent." # Function to clean files clean_files() { local dir=$1 shift for file in "$@"; do # A blank line in a TOC.removed list arrives here as an empty # element (mapfile -t preserves it; dounix.sh's unquoted word # splitting silently drops it, which is why the same blank line # is harmless there and fatal here). "$dir/" exists, so without # this guard the loop runs rm on the distribution root itself: # plain rm refuses with "Is a directory" and set -e aborts the # release mid-build. The rm -rf variant of this would not refuse. [ -z "$file" ] && continue if [ -e "$dir/$file" ]; then echo "Removing: $dir/$file" rm "$dir/$file" fi done } # Function to check if a file is binary is_binary() { if file --mime "$1" | grep -q "charset=binary"; then return 0 # True, it's binary else return 1 # False, it's not binary fi } # Write a sorted newline-delimited file list without adding a blank line for empty arrays. write_sorted_list() { local output_file=$1 shift if [ "$#" -eq 0 ]; then : > "$output_file" else printf '%s\n' "$@" | sort > "$output_file" fi } # Function to process source and create patches process_distribution() { local dist_type=$1 # "src" or "bin" echo "Building Windows $dist_type distribution..." # Read file lists local -a patchable_files=() local -a unpatched_files=() local -a remove_files=() mapfile -t patchable_files < "win32/TOC.$dist_type.patchable" mapfile -t unpatched_files < "win32/TOC.$dist_type.unpatched" mapfile -t remove_files < "win32/TOC.$dist_type.removed" # Prepare directories rm -rf "${NEW_DIR}_$dist_type" "$DISTRO_DIR" "patches_$dist_type" cp -r "${REFERENCE_DIR}_$dist_type" "$DISTRO_DIR" cp -r "${REFERENCE_DIR}_$dist_type" "${NEW_DIR}_$dist_type" mkdir -p "patches_$dist_type/bin" "patches_$dist_type/text" # Copy patchable files echo "Copying patchable files for Windows $dist_type..." for file in "${patchable_files[@]}"; do mkdir -p "$(dirname "${NEW_DIR}_$dist_type/$file")" cp "$CHANGES_DIR/$file" "${NEW_DIR}_$dist_type/$file" done # Special Windows configurations for source if [ "$dist_type" = "src" ]; then cp "$CHANGES_DIR/include/autoconf-win32.h" "${NEW_DIR}_$dist_type/include/autoconf.h" cp "$CHANGES_DIR/modules/autoconf-win32.h" "${NEW_DIR}_$dist_type/modules/autoconf.h" fi # Remove files echo "Cleaning up removed files for Windows $dist_type..." clean_files "${NEW_DIR}_$dist_type" "${remove_files[@]}" chmod -R u+rw "$DISTRO_DIR" "${NEW_DIR}_$dist_type" # Create a master patch manifest echo "# Windows $dist_type patches for MUX $OLD_VERSION to $NEW_VERSION" > "patches_$dist_type/MANIFEST.txt" echo "# Generated on $(date)" >> "patches_$dist_type/MANIFEST.txt" echo "" >> "patches_$dist_type/MANIFEST.txt" echo "# Binary files (xdelta3 format):" >> "patches_$dist_type/MANIFEST.txt" # Process all files to create appropriate patches echo "Generating patches for Windows $dist_type..." # Track files for manifest local -a binary_files=() local -a text_files=() local -a new_files=() local -a removed_files=() # Create patches for changed files and track new/removed files while IFS= read -r src_file; do rel_path=${src_file#$DISTRO_DIR/} new_file="${NEW_DIR}_$dist_type/$rel_path" if [ -f "$new_file" ]; then # Check if files differ if ! cmp -s "$src_file" "$new_file"; then if is_binary "$src_file" || is_binary "$new_file"; then # Binary file - use xdelta3 mkdir -p "patches_$dist_type/bin/$(dirname "$rel_path")" xdelta3 -e -s "$src_file" "$new_file" "patches_$dist_type/bin/$rel_path.vcdiff" binary_files+=("$rel_path") else # Text file - use standard diff mkdir -p "patches_$dist_type/text/$(dirname "$rel_path")" diff -u "$src_file" "$new_file" > "patches_$dist_type/text/$rel_path.patch" 2>/dev/null || diff_rc=$? if [ "${diff_rc:-0}" -ge 2 ]; then echo "Error: diff failed for $rel_path (exit $diff_rc)" >&2 exit 1 fi text_files+=("$rel_path") fi fi else # File was removed removed_files+=("$rel_path") fi done < <(find "$DISTRO_DIR" -type f) # Detect new files while IFS= read -r new_file; do rel_path=${new_file#${NEW_DIR}_$dist_type/} src_file="$DISTRO_DIR/$rel_path" if [ ! -f "$src_file" ]; then # File was added - copy directly if is_binary "$new_file"; then mkdir -p "patches_$dist_type/bin/$(dirname "$rel_path")" cp "$new_file" "patches_$dist_type/bin/$rel_path.new" else mkdir -p "patches_$dist_type/text/$(dirname "$rel_path")" cp "$new_file" "patches_$dist_type/text/$rel_path.new" fi new_files+=("$rel_path") fi done < <(find "${NEW_DIR}_$dist_type" -type f) # Create metadata files for patch application write_sorted_list "patches_$dist_type/binary_files.txt" "${binary_files[@]}" write_sorted_list "patches_$dist_type/text_files.txt" "${text_files[@]}" write_sorted_list "patches_$dist_type/new_files.txt" "${new_files[@]}" write_sorted_list "patches_$dist_type/removed_files.txt" "${removed_files[@]}" # Complete the manifest while IFS= read -r file; do [ -n "$file" ] || continue echo "bin/$file.vcdiff" >> "patches_$dist_type/MANIFEST.txt" done < "patches_$dist_type/binary_files.txt" echo "" >> "patches_$dist_type/MANIFEST.txt" echo "# Text files (unified diff format):" >> "patches_$dist_type/MANIFEST.txt" while IFS= read -r file; do [ -n "$file" ] || continue echo "text/$file.patch" >> "patches_$dist_type/MANIFEST.txt" done < "patches_$dist_type/text_files.txt" echo "" >> "patches_$dist_type/MANIFEST.txt" echo "# New files:" >> "patches_$dist_type/MANIFEST.txt" while IFS= read -r file; do [ -n "$file" ] || continue if is_binary "${NEW_DIR}_$dist_type/$file"; then echo "bin/$file.new" >> "patches_$dist_type/MANIFEST.txt" else echo "text/$file.new" >> "patches_$dist_type/MANIFEST.txt" fi done < "patches_$dist_type/new_files.txt" echo "" >> "patches_$dist_type/MANIFEST.txt" echo "# Removed files:" >> "patches_$dist_type/MANIFEST.txt" while IFS= read -r file; do [ -n "$file" ] || continue echo "$file" >> "patches_$dist_type/MANIFEST.txt" done < "patches_$dist_type/removed_files.txt" # Create patch application script cat > "patches_$dist_type/apply_patch.bat" < /dev/null; then if [ -e "mux-$OLD_VERSION-$NEW_VERSION.win32.$dist_type.patch.7z" ]; then rm "mux-$OLD_VERSION-$NEW_VERSION.win32.$dist_type.patch.7z" fi 7z a "mux-$OLD_VERSION-$NEW_VERSION.win32.$dist_type.patch.7z" "patches_$dist_type/" else echo "7z command not found, skipping .7z archive creation" fi # Build complete distribution rm -rf "$DISTRO_DIR" # Copy unpatched files echo "Copying unpatched files for Windows $dist_type..." for file in "${unpatched_files[@]}"; do mkdir -p "$(dirname "${NEW_DIR}_$dist_type/$file")" cp "$CHANGES_DIR/$file" "${NEW_DIR}_$dist_type/$file" done # Final cleanup echo "Final cleanup of removed files for Windows $dist_type..." clean_files "${NEW_DIR}_$dist_type" "${remove_files[@]}" cp -r "${NEW_DIR}_$dist_type" "$DISTRO_DIR" # Create full distribution archives echo "Creating Windows $dist_type full distribution archives..." # ZIP format (primary Windows format) if [ -e "mux-$NEW_VERSION.win32.$dist_type.zip" ]; then rm "mux-$NEW_VERSION.win32.$dist_type.zip" fi zip -r "mux-$NEW_VERSION.win32.$dist_type.zip" "$DISTRO_DIR" # 7z format (also common on Windows) if command -v 7z &> /dev/null; then if [ -e "mux-$NEW_VERSION.win32.$dist_type.7z" ]; then rm "mux-$NEW_VERSION.win32.$dist_type.7z" fi 7z a "mux-$NEW_VERSION.win32.$dist_type.7z" "$DISTRO_DIR" else echo "7z command not found, skipping .7z archive creation" fi # Generate checksums echo "Generating checksums for Windows $dist_type..." sha256sum "mux-$OLD_VERSION-$NEW_VERSION.win32.$dist_type.patch.zip" > "mux-$OLD_VERSION-$NEW_VERSION.win32.$dist_type.patch.zip.sha256" sha256sum "mux-$NEW_VERSION.win32.$dist_type.zip" > "mux-$NEW_VERSION.win32.$dist_type.zip.sha256" if command -v 7z &> /dev/null; then sha256sum "mux-$OLD_VERSION-$NEW_VERSION.win32.$dist_type.patch.7z" > "mux-$OLD_VERSION-$NEW_VERSION.win32.$dist_type.patch.7z.sha256" sha256sum "mux-$NEW_VERSION.win32.$dist_type.7z" > "mux-$NEW_VERSION.win32.$dist_type.7z.sha256" fi # Generate a simple README with instructions cat > "patches_$dist_type/README.txt" < get_xdelta3.bat < get_patch.bat <