1#!/bin/sh 2# 3# SPDX-License-Identifier: BSD-2-Clause-FreeBSD 4# 5# Copyright (c) 2010-2013 Hudson River Trading LLC 6# Written by: John H. Baldwin <jhb@FreeBSD.org> 7# All rights reserved. 8# 9# Redistribution and use in source and binary forms, with or without 10# modification, are permitted provided that the following conditions 11# are met: 12# 1. Redistributions of source code must retain the above copyright 13# notice, this list of conditions and the following disclaimer. 14# 2. Redistributions in binary form must reproduce the above copyright 15# notice, this list of conditions and the following disclaimer in the 16# documentation and/or other materials provided with the distribution. 17# 18# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 22# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 24# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 25# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 26# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 27# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 28# SUCH DAMAGE. 29# 30# $FreeBSD$ 31 32# This is a tool to manage updating files that are not updated as part 33# of 'make installworld' such as files in /etc. Unlike other tools, 34# this one is specifically tailored to assisting with mass upgrades. 35# To that end it does not require user intervention while running. 36# 37# Theory of operation: 38# 39# The most reliable way to update changes to files that have local 40# modifications is to perform a three-way merge between the original 41# unmodified file, the new version of the file, and the modified file. 42# This requires having all three versions of the file available when 43# performing an update. 44# 45# To that end, etcupdate uses a strategy where the current unmodified 46# tree is kept in WORKDIR/current and the previous unmodified tree is 47# kept in WORKDIR/old. When performing a merge, a new tree is built 48# if needed and then the changes are merged into DESTDIR. Any files 49# with unresolved conflicts after the merge are left in a tree rooted 50# at WORKDIR/conflicts. 51# 52# To provide extra flexibility, etcupdate can also build tarballs of 53# root trees that can later be used. It can also use a tarball as the 54# source of a new tree instead of building it from /usr/src. 55 56# Global settings. These can be adjusted by config files and in some 57# cases by command line options. 58 59# TODO: 60# - automatable conflict resolution 61 62usage() 63{ 64 cat <<EOF 65usage: etcupdate [-npBF] [-d workdir] [-r | -s source | -t tarball] 66 [-A patterns] [-D destdir] [-I patterns] [-L logfile] 67 [-M options] 68 etcupdate build [-B] [-d workdir] [-s source] [-L logfile] [-M options] 69 <tarball> 70 etcupdate diff [-d workdir] [-D destdir] [-I patterns] [-L logfile] 71 etcupdate extract [-B] [-d workdir] [-s source | -t tarball] 72 [-D destdir] [-L logfile] [-M options] 73 etcupdate resolve [-p] [-d workdir] [-D destdir] [-L logfile] 74 etcupdate revert [-d workdir] [-D destdir] [-L logfile] file ... 75 etcupdate status [-d workdir] [-D destdir] 76EOF 77 exit 1 78} 79 80# Used to write a message prepended with '>>>' to the logfile. 81log() 82{ 83 echo ">>>" "$@" >&3 84} 85 86# Used for assertion conditions that should never happen. 87panic() 88{ 89 echo "PANIC:" "$@" 90 exit 10 91} 92 93# Used to write a warning message. These are saved to the WARNINGS 94# file with " " prepended. 95warn() 96{ 97 echo -n " " >> $WARNINGS 98 echo "$@" >> $WARNINGS 99} 100 101# Output a horizontal rule using the passed-in character. Matches the 102# length used for Index lines in CVS and SVN diffs. 103# 104# $1 - character 105rule() 106{ 107 jot -b "$1" -s "" 67 108} 109 110# Output a text description of a specified file's type. 111# 112# $1 - file pathname. 113file_type() 114{ 115 stat -f "%HT" $1 | tr "[:upper:]" "[:lower:]" 116} 117 118# Returns true (0) if a file exists 119# 120# $1 - file pathname. 121exists() 122{ 123 [ -e $1 -o -L $1 ] 124} 125 126# Returns true (0) if a file should be ignored, false otherwise. 127# 128# $1 - file pathname 129ignore() 130{ 131 local pattern - 132 133 set -o noglob 134 for pattern in $IGNORE_FILES; do 135 set +o noglob 136 case $1 in 137 $pattern) 138 return 0 139 ;; 140 esac 141 set -o noglob 142 done 143 144 # Ignore /.cshrc and /.profile if they are hardlinked to the 145 # same file in /root. This ensures we only compare those 146 # files once in that case. 147 case $1 in 148 /.cshrc|/.profile) 149 if [ ${DESTDIR}$1 -ef ${DESTDIR}/root$1 ]; then 150 return 0 151 fi 152 ;; 153 *) 154 ;; 155 esac 156 157 return 1 158} 159 160# Returns true (0) if the new version of a file should always be 161# installed rather than attempting to do a merge. 162# 163# $1 - file pathname 164always_install() 165{ 166 local pattern - 167 168 set -o noglob 169 for pattern in $ALWAYS_INSTALL; do 170 set +o noglob 171 case $1 in 172 $pattern) 173 return 0 174 ;; 175 esac 176 set -o noglob 177 done 178 179 return 1 180} 181 182# Build a new tree. This runs inside a subshell to trap SIGINT. 183# 184# $1 - directory to store new tree in 185build_tree() 186( 187 local destdir dir file make 188 189 make="make $MAKE_OPTIONS -DNO_FILEMON" 190 191 log "Building tree at $1 with $make" 192 193 exec >&3 2>&1 194 trap 'return 1' INT 195 196 mkdir -p $1/usr/obj 197 destdir=`realpath $1` 198 199 if [ -n "$preworld" ]; then 200 # Build a limited tree that only contains files that are 201 # crucial to installworld. 202 for file in $PREWORLD_FILES; do 203 name=$(basename $file) 204 mkdir -p $1/etc || return 1 205 cp -p $SRCDIR/$file $1/etc/$name || return 1 206 done 207 elif ! [ -n "$nobuild" ]; then 208 (cd $SRCDIR; $make DESTDIR=$destdir distrib-dirs && 209 MAKEOBJDIRPREFIX=$destdir/usr/obj $make _obj SUBDIR_OVERRIDE=etc && 210 MAKEOBJDIRPREFIX=$destdir/usr/obj $make everything SUBDIR_OVERRIDE=etc && 211 MAKEOBJDIRPREFIX=$destdir/usr/obj $make DESTDIR=$destdir distribution) || \ 212 return 1 213 else 214 (cd $SRCDIR; $make DESTDIR=$destdir distrib-dirs && 215 $make DESTDIR=$destdir distribution) || return 1 216 fi 217 chflags -R noschg $1 || return 1 218 rm -rf $1/usr/obj || return 1 219 220 # Purge auto-generated files. Only the source files need to 221 # be updated after which these files are regenerated. 222 rm -f $1/etc/*.db $1/etc/passwd $1/var/db/services.db || return 1 223 224 # Remove empty files. These just clutter the output of 'diff'. 225 find $1 -type f -size 0 -delete || return 1 226 227 # Trim empty directories. 228 find -d $1 -type d -empty -delete || return 1 229 return 0 230) 231 232# Generate a new tree. If tarball is set, then the tree is 233# extracted from the tarball. Otherwise the tree is built from a 234# source tree. 235# 236# $1 - directory to store new tree in 237extract_tree() 238{ 239 local files 240 241 # If we have a tarball, extract that into the new directory. 242 if [ -n "$tarball" ]; then 243 files= 244 if [ -n "$preworld" ]; then 245 files="$PREWORLD_FILES" 246 fi 247 if ! (mkdir -p $1 && tar xf $tarball -C $1 $files) \ 248 >&3 2>&1; then 249 echo "Failed to extract new tree." 250 remove_tree $1 251 exit 1 252 fi 253 else 254 if ! build_tree $1; then 255 echo "Failed to build new tree." 256 remove_tree $1 257 exit 1 258 fi 259 fi 260} 261 262# Forcefully remove a tree. Returns true (0) if the operation succeeds. 263# 264# $1 - path to tree 265remove_tree() 266{ 267 268 rm -rf $1 >&3 2>&1 269 if [ -e $1 ]; then 270 chflags -R noschg $1 >&3 2>&1 271 rm -rf $1 >&3 2>&1 272 fi 273 [ ! -e $1 ] 274} 275 276# Return values for compare() 277COMPARE_EQUAL=0 278COMPARE_ONLYFIRST=1 279COMPARE_ONLYSECOND=2 280COMPARE_DIFFTYPE=3 281COMPARE_DIFFLINKS=4 282COMPARE_DIFFFILES=5 283 284# Compare two files/directories/symlinks. Note that this does not 285# recurse into subdirectories. Instead, if two nodes are both 286# directories, they are assumed to be equivalent. 287# 288# Returns true (0) if the nodes are identical. If only one of the two 289# nodes are present, return one of the COMPARE_ONLY* constants. If 290# the nodes are different, return one of the COMPARE_DIFF* constants 291# to indicate the type of difference. 292# 293# $1 - first node 294# $2 - second node 295compare() 296{ 297 local first second 298 299 # If the first node doesn't exist, then check for the second 300 # node. Note that -e will fail for a symbolic link that 301 # points to a missing target. 302 if ! exists $1; then 303 if exists $2; then 304 return $COMPARE_ONLYSECOND 305 else 306 return $COMPARE_EQUAL 307 fi 308 elif ! exists $2; then 309 return $COMPARE_ONLYFIRST 310 fi 311 312 # If the two nodes are different file types fail. 313 first=`stat -f "%Hp" $1` 314 second=`stat -f "%Hp" $2` 315 if [ "$first" != "$second" ]; then 316 return $COMPARE_DIFFTYPE 317 fi 318 319 # If both are symlinks, compare the link values. 320 if [ -L $1 ]; then 321 first=`readlink $1` 322 second=`readlink $2` 323 if [ "$first" = "$second" ]; then 324 return $COMPARE_EQUAL 325 else 326 return $COMPARE_DIFFLINKS 327 fi 328 fi 329 330 # If both are files, compare the file contents. 331 if [ -f $1 ]; then 332 if cmp -s $1 $2; then 333 return $COMPARE_EQUAL 334 else 335 return $COMPARE_DIFFFILES 336 fi 337 fi 338 339 # As long as the two nodes are the same type of file, consider 340 # them equivalent. 341 return $COMPARE_EQUAL 342} 343 344# Returns true (0) if the only difference between two regular files is a 345# change in the FreeBSD ID string. 346# 347# $1 - path of first file 348# $2 - path of second file 349fbsdid_only() 350{ 351 352 diff -qI '\$FreeBSD.*\$' $1 $2 >/dev/null 2>&1 353} 354 355# This is a wrapper around compare that will return COMPARE_EQUAL if 356# the only difference between two regular files is a change in the 357# FreeBSD ID string. It only makes this adjustment if the -F flag has 358# been specified. 359# 360# $1 - first node 361# $2 - second node 362compare_fbsdid() 363{ 364 local cmp 365 366 compare $1 $2 367 cmp=$? 368 369 if [ -n "$FREEBSD_ID" -a "$cmp" -eq $COMPARE_DIFFFILES ] && \ 370 fbsdid_only $1 $2; then 371 return $COMPARE_EQUAL 372 fi 373 374 return $cmp 375} 376 377# Returns true (0) if a directory is empty. 378# 379# $1 - pathname of the directory to check 380empty_dir() 381{ 382 local contents 383 384 contents=`ls -A $1` 385 [ -z "$contents" ] 386} 387 388# Returns true (0) if one directories contents are a subset of the 389# other. This will recurse to handle subdirectories and compares 390# individual files in the trees. Its purpose is to quiet spurious 391# directory warnings for dryrun invocations. 392# 393# $1 - first directory (sub) 394# $2 - second directory (super) 395dir_subset() 396{ 397 local contents file 398 399 if ! [ -d $1 -a -d $2 ]; then 400 return 1 401 fi 402 403 # Ignore files that are present in the second directory but not 404 # in the first. 405 contents=`ls -A $1` 406 for file in $contents; do 407 if ! compare $1/$file $2/$file; then 408 return 1 409 fi 410 411 if [ -d $1/$file ]; then 412 if ! dir_subset $1/$file $2/$file; then 413 return 1 414 fi 415 fi 416 done 417 return 0 418} 419 420# Returns true (0) if a directory in the destination tree is empty. 421# If this is a dryrun, then this returns true as long as the contents 422# of the directory are a subset of the contents in the old tree 423# (meaning that the directory would be empty in a non-dryrun when this 424# was invoked) to quiet spurious warnings. 425# 426# $1 - pathname of the directory to check relative to DESTDIR. 427empty_destdir() 428{ 429 430 if [ -n "$dryrun" ]; then 431 dir_subset $DESTDIR/$1 $OLDTREE/$1 432 return 433 fi 434 435 empty_dir $DESTDIR/$1 436} 437 438# Output a diff of two directory entries with the same relative name 439# in different trees. Note that as with compare(), this does not 440# recurse into subdirectories. If the nodes are identical, nothing is 441# output. 442# 443# $1 - first tree 444# $2 - second tree 445# $3 - node name 446# $4 - label for first tree 447# $5 - label for second tree 448diffnode() 449{ 450 local first second file old new diffargs 451 452 if [ -n "$FREEBSD_ID" ]; then 453 diffargs="-I \\\$FreeBSD.*\\\$" 454 else 455 diffargs="" 456 fi 457 458 compare_fbsdid $1/$3 $2/$3 459 case $? in 460 $COMPARE_EQUAL) 461 ;; 462 $COMPARE_ONLYFIRST) 463 echo 464 echo "Removed: $3" 465 echo 466 ;; 467 $COMPARE_ONLYSECOND) 468 echo 469 echo "Added: $3" 470 echo 471 ;; 472 $COMPARE_DIFFTYPE) 473 first=`file_type $1/$3` 474 second=`file_type $2/$3` 475 echo 476 echo "Node changed from a $first to a $second: $3" 477 echo 478 ;; 479 $COMPARE_DIFFLINKS) 480 first=`readlink $1/$file` 481 second=`readlink $2/$file` 482 echo 483 echo "Link changed: $file" 484 rule "=" 485 echo "-$first" 486 echo "+$second" 487 echo 488 ;; 489 $COMPARE_DIFFFILES) 490 echo "Index: $3" 491 rule "=" 492 diff -u $diffargs -L "$3 ($4)" $1/$3 -L "$3 ($5)" $2/$3 493 ;; 494 esac 495} 496 497# Run one-off commands after an update has completed. These commands 498# are not tied to a specific file, so they cannot be handled by 499# post_install_file(). 500post_update() 501{ 502 local args 503 504 # None of these commands should be run for a pre-world update. 505 if [ -n "$preworld" ]; then 506 return 507 fi 508 509 # If /etc/localtime exists and is not a symlink and /var/db/zoneinfo 510 # exists, run tzsetup -r to refresh /etc/localtime. 511 if [ -f ${DESTDIR}/etc/localtime -a \ 512 ! -L ${DESTDIR}/etc/localtime ]; then 513 if [ -f ${DESTDIR}/var/db/zoneinfo ]; then 514 if [ -n "${DESTDIR}" ]; then 515 args="-C ${DESTDIR}" 516 else 517 args="" 518 fi 519 log "tzsetup -r ${args}" 520 if [ -z "$dryrun" ]; then 521 tzsetup -r ${args} >&3 2>&1 522 fi 523 else 524 warn "Needs update: /etc/localtime (required" \ 525 "manual update via tzsetup(8))" 526 fi 527 fi 528} 529 530# Create missing parent directories of a node in a target tree 531# preserving the owner, group, and permissions from a specified 532# template tree. 533# 534# $1 - template tree 535# $2 - target tree 536# $3 - pathname of the node (relative to both trees) 537install_dirs() 538{ 539 local args dir 540 541 dir=`dirname $3` 542 543 # Nothing to do if the parent directory exists. This also 544 # catches the degenerate cases when the path is just a simple 545 # filename. 546 if [ -d ${2}$dir ]; then 547 return 0 548 fi 549 550 # If non-directory file exists with the desired directory 551 # name, then fail. 552 if exists ${2}$dir; then 553 # If this is a dryrun and we are installing the 554 # directory in the DESTDIR and the file in the DESTDIR 555 # matches the file in the old tree, then fake success 556 # to quiet spurious warnings. 557 if [ -n "$dryrun" -a "$2" = "$DESTDIR" ]; then 558 if compare $OLDTREE/$dir $DESTDIR/$dir; then 559 return 0 560 fi 561 fi 562 563 args=`file_type ${2}$dir` 564 warn "Directory mismatch: ${2}$dir ($args)" 565 return 1 566 fi 567 568 # Ensure the parent directory of the directory is present 569 # first. 570 if ! install_dirs $1 "$2" $dir; then 571 return 1 572 fi 573 574 # Format attributes from template directory as install(1) 575 # arguments. 576 args=`stat -f "-o %Su -g %Sg -m %0Mp%0Lp" $1/$dir` 577 578 log "install -d $args ${2}$dir" 579 if [ -z "$dryrun" ]; then 580 install -d $args ${2}$dir >&3 2>&1 581 fi 582 return 0 583} 584 585# Perform post-install fixups for a file. This largely consists of 586# regenerating any files that depend on the newly installed file. 587# 588# $1 - pathname of the updated file (relative to DESTDIR) 589post_install_file() 590{ 591 case $1 in 592 /etc/mail/aliases) 593 # Grr, newaliases only works for an empty DESTDIR. 594 if [ -z "$DESTDIR" ]; then 595 log "newaliases" 596 if [ -z "$dryrun" ]; then 597 newaliases >&3 2>&1 598 fi 599 else 600 NEWALIAS_WARN=yes 601 fi 602 ;; 603 /usr/share/certs/trusted/* | /usr/share/certs/untrusted/*) 604 log "certctl rehash" 605 if [ -z "$dryrun" ]; then 606 env DESTDIR=${DESTDIR} certctl rehash >&3 2>&1 607 fi 608 ;; 609 /etc/login.conf) 610 log "cap_mkdb ${DESTDIR}$1" 611 if [ -z "$dryrun" ]; then 612 cap_mkdb ${DESTDIR}$1 >&3 2>&1 613 fi 614 ;; 615 /etc/master.passwd) 616 log "pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1" 617 if [ -z "$dryrun" ]; then 618 pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1 \ 619 >&3 2>&1 620 fi 621 ;; 622 /etc/motd) 623 # /etc/rc.d/motd hardcodes the /etc/motd path. 624 # Don't warn about non-empty DESTDIR's since this 625 # change is only cosmetic anyway. 626 if [ -z "$DESTDIR" ]; then 627 log "sh /etc/rc.d/motd start" 628 if [ -z "$dryrun" ]; then 629 sh /etc/rc.d/motd start >&3 2>&1 630 fi 631 fi 632 ;; 633 /etc/services) 634 log "services_mkdb -q -o $DESTDIR/var/db/services.db" \ 635 "${DESTDIR}$1" 636 if [ -z "$dryrun" ]; then 637 services_mkdb -q -o $DESTDIR/var/db/services.db \ 638 ${DESTDIR}$1 >&3 2>&1 639 fi 640 ;; 641 esac 642} 643 644# Install the "new" version of a file. Returns true if it succeeds 645# and false otherwise. 646# 647# $1 - pathname of the file to install (relative to DESTDIR) 648install_new() 649{ 650 651 if ! install_dirs $NEWTREE "$DESTDIR" $1; then 652 return 1 653 fi 654 log "cp -Rp ${NEWTREE}$1 ${DESTDIR}$1" 655 if [ -z "$dryrun" ]; then 656 cp -Rp ${NEWTREE}$1 ${DESTDIR}$1 >&3 2>&1 657 fi 658 post_install_file $1 659 return 0 660} 661 662# Install the "resolved" version of a file. Returns true if it succeeds 663# and false otherwise. 664# 665# $1 - pathname of the file to install (relative to DESTDIR) 666install_resolved() 667{ 668 669 # This should always be present since the file is already 670 # there (it caused a conflict). However, it doesn't hurt to 671 # just be safe. 672 if ! install_dirs $NEWTREE "$DESTDIR" $1; then 673 return 1 674 fi 675 676 # Use cat rather than cp to preserve metadata 677 log "cat ${CONFLICTS}$1 > ${DESTDIR}$1" 678 cat ${CONFLICTS}$1 > ${DESTDIR}$1 2>&3 679 post_install_file $1 680 return 0 681} 682 683# Generate a conflict file when a "new" file conflicts with an 684# existing file in DESTDIR. 685# 686# $1 - pathname of the file that conflicts (relative to DESTDIR) 687new_conflict() 688{ 689 690 if [ -n "$dryrun" ]; then 691 return 692 fi 693 694 install_dirs $NEWTREE $CONFLICTS $1 695 diff --changed-group-format='<<<<<<< (local) 696%<======= 697%>>>>>>>> (stock) 698' $DESTDIR/$1 $NEWTREE/$1 > $CONFLICTS/$1 699} 700 701# Remove the "old" version of a file. 702# 703# $1 - pathname of the old file to remove (relative to DESTDIR) 704remove_old() 705{ 706 log "rm -f ${DESTDIR}$1" 707 if [ -z "$dryrun" ]; then 708 rm -f ${DESTDIR}$1 >&3 2>&1 709 fi 710 echo " D $1" 711} 712 713# Update a file that has no local modifications. 714# 715# $1 - pathname of the file to update (relative to DESTDIR) 716update_unmodified() 717{ 718 local new old 719 720 # If the old file is a directory, then remove it with rmdir 721 # (this should only happen if the file has changed its type 722 # from a directory to a non-directory). If the directory 723 # isn't empty, then fail. This will be reported as a warning 724 # later. 725 if [ -d $DESTDIR/$1 ]; then 726 if empty_destdir $1; then 727 log "rmdir ${DESTDIR}$1" 728 if [ -z "$dryrun" ]; then 729 rmdir ${DESTDIR}$1 >&3 2>&1 730 fi 731 else 732 return 1 733 fi 734 735 # If both the old and new files are regular files, leave the 736 # existing file. This avoids breaking hard links for /.cshrc 737 # and /.profile. Otherwise, explicitly remove the old file. 738 elif ! [ -f ${DESTDIR}$1 -a -f ${NEWTREE}$1 ]; then 739 log "rm -f ${DESTDIR}$1" 740 if [ -z "$dryrun" ]; then 741 rm -f ${DESTDIR}$1 >&3 2>&1 742 fi 743 fi 744 745 # If the new file is a directory, note that the old file has 746 # been removed, but don't do anything else for now. The 747 # directory will be installed if needed when new files within 748 # that directory are installed. 749 if [ -d $NEWTREE/$1 ]; then 750 if empty_dir $NEWTREE/$1; then 751 echo " D $file" 752 else 753 echo " U $file" 754 fi 755 elif install_new $1; then 756 echo " U $file" 757 fi 758 return 0 759} 760 761# Update the FreeBSD ID string in a locally modified file to match the 762# FreeBSD ID string from the "new" version of the file. 763# 764# $1 - pathname of the file to update (relative to DESTDIR) 765update_freebsdid() 766{ 767 local new dest file 768 769 # If the FreeBSD ID string is removed from the local file, 770 # there is nothing to do. In this case, treat the file as 771 # updated. Otherwise, if either file has more than one 772 # FreeBSD ID string, just punt and let the user handle the 773 # conflict manually. 774 new=`grep -c '\$FreeBSD.*\$' ${NEWTREE}$1` 775 dest=`grep -c '\$FreeBSD.*\$' ${DESTDIR}$1` 776 if [ "$dest" -eq 0 ]; then 777 return 0 778 fi 779 if [ "$dest" -ne 1 -o "$dest" -ne 1 ]; then 780 return 1 781 fi 782 783 # If the FreeBSD ID string in the new file matches the FreeBSD ID 784 # string in the local file, there is nothing to do. 785 new=`grep '\$FreeBSD.*\$' ${NEWTREE}$1` 786 dest=`grep '\$FreeBSD.*\$' ${DESTDIR}$1` 787 if [ "$new" = "$dest" ]; then 788 return 0 789 fi 790 791 # Build the new file in three passes. First, copy all the 792 # lines preceding the FreeBSD ID string from the local version 793 # of the file. Second, append the FreeBSD ID string line from 794 # the new version. Finally, append all the lines after the 795 # FreeBSD ID string from the local version of the file. 796 file=`mktemp $WORKDIR/etcupdate-XXXXXXX` 797 awk '/\$FreeBSD.*\$/ { exit } { print }' ${DESTDIR}$1 >> $file 798 awk '/\$FreeBSD.*\$/ { print }' ${NEWTREE}$1 >> $file 799 awk '/\$FreeBSD.*\$/ { ok = 1; next } { if (ok) print }' \ 800 ${DESTDIR}$1 >> $file 801 802 # As an extra sanity check, fail the attempt if the updated 803 # version of the file has any differences aside from the 804 # FreeBSD ID string. 805 if ! fbsdid_only ${DESTDIR}$1 $file; then 806 rm -f $file 807 return 1 808 fi 809 810 log "cp $file ${DESTDIR}$1" 811 if [ -z "$dryrun" ]; then 812 cp $file ${DESTDIR}$1 >&3 2>&1 813 fi 814 rm -f $file 815 post_install_file $1 816 echo " M $1" 817 return 0 818} 819 820# Attempt to update a file that has local modifications. This routine 821# only handles regular files. If the 3-way merge succeeds without 822# conflicts, the updated file is installed. If the merge fails, the 823# merged version with conflict markers is left in the CONFLICTS tree. 824# 825# $1 - pathname of the file to merge (relative to DESTDIR) 826merge_file() 827{ 828 local res 829 830 # Try the merge to see if there is a conflict. 831 diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > /dev/null 2>&3 832 res=$? 833 case $res in 834 0) 835 # No conflicts, so just redo the merge to the 836 # real file. 837 log "diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1" 838 if [ -z "$dryrun" ]; then 839 temp=$(mktemp -t etcupdate) 840 diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > ${temp} 841 # Use "cat >" to preserve metadata. 842 cat ${temp} > ${DESTDIR}$1 843 rm -f ${temp} 844 fi 845 post_install_file $1 846 echo " M $1" 847 ;; 848 1) 849 # Conflicts, save a version with conflict markers in 850 # the conflicts directory. 851 if [ -z "$dryrun" ]; then 852 install_dirs $NEWTREE $CONFLICTS $1 853 log "diff3 -m ${DESTDIR}$1 ${CONFLICTS}$1" 854 diff3 -m -L "yours" -L "original" -L "new" \ 855 ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > \ 856 ${CONFLICTS}$1 857 fi 858 echo " C $1" 859 ;; 860 *) 861 panic "merge failed with status $res" 862 ;; 863 esac 864} 865 866# Returns true if a file contains conflict markers from a merge conflict. 867# 868# $1 - pathname of the file to resolve (relative to DESTDIR) 869has_conflicts() 870{ 871 872 egrep -q '^(<{7}|\|{7}|={7}|>{7}) ' $CONFLICTS/$1 873} 874 875# Attempt to resolve a conflict. The user is prompted to choose an 876# action for each conflict. If the user edits the file, they are 877# prompted again for an action. The process is very similar to 878# resolving conflicts after an update or merge with Perforce or 879# Subversion. The prompts are modelled on a subset of the available 880# commands for resolving conflicts with Subversion. 881# 882# $1 - pathname of the file to resolve (relative to DESTDIR) 883resolve_conflict() 884{ 885 local command junk 886 887 echo "Resolving conflict in '$1':" 888 edit= 889 while true; do 890 # Only display the resolved command if the file 891 # doesn't contain any conflicts. 892 echo -n "Select: (p) postpone, (df) diff-full, (e) edit," 893 if ! has_conflicts $1; then 894 echo -n " (r) resolved," 895 fi 896 echo 897 echo -n " (h) help for more options: " 898 read command 899 case $command in 900 df) 901 diff -u ${DESTDIR}$1 ${CONFLICTS}$1 902 ;; 903 e) 904 $EDITOR ${CONFLICTS}$1 905 ;; 906 h) 907 cat <<EOF 908 (p) postpone - ignore this conflict for now 909 (df) diff-full - show all changes made to merged file 910 (e) edit - change merged file in an editor 911 (r) resolved - accept merged version of file 912 (mf) mine-full - accept local version of entire file (ignore new changes) 913 (tf) theirs-full - accept new version of entire file (lose local changes) 914 (h) help - show this list 915EOF 916 ;; 917 mf) 918 # For mine-full, just delete the 919 # merged file and leave the local 920 # version of the file as-is. 921 rm ${CONFLICTS}$1 922 return 923 ;; 924 p) 925 return 926 ;; 927 r) 928 # If the merged file has conflict 929 # markers, require confirmation. 930 if has_conflicts $1; then 931 echo "File '$1' still has conflicts," \ 932 "are you sure? (y/n) " 933 read junk 934 if [ "$junk" != "y" ]; then 935 continue 936 fi 937 fi 938 939 if ! install_resolved $1; then 940 panic "Unable to install merged" \ 941 "version of $1" 942 fi 943 rm ${CONFLICTS}$1 944 return 945 ;; 946 tf) 947 # For theirs-full, install the new 948 # version of the file over top of the 949 # existing file. 950 if ! install_new $1; then 951 panic "Unable to install new" \ 952 "version of $1" 953 fi 954 rm ${CONFLICTS}$1 955 return 956 ;; 957 *) 958 echo "Invalid command." 959 ;; 960 esac 961 done 962} 963 964# Handle a file that has been removed from the new tree. If the file 965# does not exist in DESTDIR, then there is nothing to do. If the file 966# exists in DESTDIR and is identical to the old version, remove it 967# from DESTDIR. Otherwise, whine about the conflict but leave the 968# file in DESTDIR. To handle directories, this uses two passes. The 969# first pass handles all non-directory files. The second pass handles 970# just directories and removes them if they are empty. 971# 972# If -F is specified, and the only difference in the file in DESTDIR 973# is a change in the FreeBSD ID string, then remove the file. 974# 975# $1 - pathname of the file (relative to DESTDIR) 976handle_removed_file() 977{ 978 local dest file 979 980 file=$1 981 if ignore $file; then 982 log "IGNORE: removed file $file" 983 return 984 fi 985 986 compare_fbsdid $DESTDIR/$file $OLDTREE/$file 987 case $? in 988 $COMPARE_EQUAL) 989 if ! [ -d $DESTDIR/$file ]; then 990 remove_old $file 991 fi 992 ;; 993 $COMPARE_ONLYFIRST) 994 panic "Removed file now missing" 995 ;; 996 $COMPARE_ONLYSECOND) 997 # Already removed, nothing to do. 998 ;; 999 $COMPARE_DIFFTYPE|$COMPARE_DIFFLINKS|$COMPARE_DIFFFILES) 1000 dest=`file_type $DESTDIR/$file` 1001 warn "Modified $dest remains: $file" 1002 ;; 1003 esac 1004} 1005 1006# Handle a directory that has been removed from the new tree. Only 1007# remove the directory if it is empty. 1008# 1009# $1 - pathname of the directory (relative to DESTDIR) 1010handle_removed_directory() 1011{ 1012 local dir 1013 1014 dir=$1 1015 if ignore $dir; then 1016 log "IGNORE: removed dir $dir" 1017 return 1018 fi 1019 1020 if [ -d $DESTDIR/$dir -a -d $OLDTREE/$dir ]; then 1021 if empty_destdir $dir; then 1022 log "rmdir ${DESTDIR}$dir" 1023 if [ -z "$dryrun" ]; then 1024 rmdir ${DESTDIR}$dir >/dev/null 2>&1 1025 fi 1026 echo " D $dir" 1027 else 1028 warn "Non-empty directory remains: $dir" 1029 fi 1030 fi 1031} 1032 1033# Handle a file that exists in both the old and new trees. If the 1034# file has not changed in the old and new trees, there is nothing to 1035# do. If the file in the destination directory matches the new file, 1036# there is nothing to do. If the file in the destination directory 1037# matches the old file, then the new file should be installed. 1038# Everything else becomes some sort of conflict with more detailed 1039# handling. 1040# 1041# $1 - pathname of the file (relative to DESTDIR) 1042handle_modified_file() 1043{ 1044 local cmp dest file new newdestcmp old 1045 1046 file=$1 1047 if ignore $file; then 1048 log "IGNORE: modified file $file" 1049 return 1050 fi 1051 1052 compare $OLDTREE/$file $NEWTREE/$file 1053 cmp=$? 1054 if [ $cmp -eq $COMPARE_EQUAL ]; then 1055 return 1056 fi 1057 1058 if [ $cmp -eq $COMPARE_ONLYFIRST -o $cmp -eq $COMPARE_ONLYSECOND ]; then 1059 panic "Changed file now missing" 1060 fi 1061 1062 compare $NEWTREE/$file $DESTDIR/$file 1063 newdestcmp=$? 1064 if [ $newdestcmp -eq $COMPARE_EQUAL ]; then 1065 return 1066 fi 1067 1068 # If the only change in the new file versus the destination 1069 # file is a change in the FreeBSD ID string and -F is 1070 # specified, just install the new file. 1071 if [ -n "$FREEBSD_ID" -a $newdestcmp -eq $COMPARE_DIFFFILES ] && \ 1072 fbsdid_only $NEWTREE/$file $DESTDIR/$file; then 1073 if update_unmodified $file; then 1074 return 1075 else 1076 panic "Updating FreeBSD ID string failed" 1077 fi 1078 fi 1079 1080 # If the local file is the same as the old file, install the 1081 # new file. If -F is specified and the only local change is 1082 # in the FreeBSD ID string, then install the new file as well. 1083 if compare_fbsdid $OLDTREE/$file $DESTDIR/$file; then 1084 if update_unmodified $file; then 1085 return 1086 fi 1087 fi 1088 1089 # If the file was removed from the dest tree, just whine. 1090 if [ $newdestcmp -eq $COMPARE_ONLYFIRST ]; then 1091 # If the removed file matches an ALWAYS_INSTALL glob, 1092 # then just install the new version of the file. 1093 if always_install $file; then 1094 log "ALWAYS: adding $file" 1095 if ! [ -d $NEWTREE/$file ]; then 1096 if install_new $file; then 1097 echo " A $file" 1098 fi 1099 fi 1100 return 1101 fi 1102 1103 # If the only change in the new file versus the old 1104 # file is a change in the FreeBSD ID string and -F is 1105 # specified, don't warn. 1106 if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \ 1107 fbsdid_only $OLDTREE/$file $NEWTREE/$file; then 1108 return 1109 fi 1110 1111 case $cmp in 1112 $COMPARE_DIFFTYPE) 1113 old=`file_type $OLDTREE/$file` 1114 new=`file_type $NEWTREE/$file` 1115 warn "Remove mismatch: $file ($old became $new)" 1116 ;; 1117 $COMPARE_DIFFLINKS) 1118 old=`readlink $OLDTREE/$file` 1119 new=`readlink $NEWTREE/$file` 1120 warn \ 1121 "Removed link changed: $file (\"$old\" became \"$new\")" 1122 ;; 1123 $COMPARE_DIFFFILES) 1124 warn "Removed file changed: $file" 1125 ;; 1126 esac 1127 return 1128 fi 1129 1130 # Treat the file as unmodified and force install of the new 1131 # file if it matches an ALWAYS_INSTALL glob. If the update 1132 # attempt fails, then fall through to the normal case so a 1133 # warning is generated. 1134 if always_install $file; then 1135 log "ALWAYS: updating $file" 1136 if update_unmodified $file; then 1137 return 1138 fi 1139 fi 1140 1141 # If the only change in the new file versus the old file is a 1142 # change in the FreeBSD ID string and -F is specified, just 1143 # update the FreeBSD ID string in the local file. 1144 if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \ 1145 fbsdid_only $OLDTREE/$file $NEWTREE/$file; then 1146 if update_freebsdid $file; then 1147 continue 1148 fi 1149 fi 1150 1151 # If the file changed types between the old and new trees but 1152 # the files in the new and dest tree are both of the same 1153 # type, treat it like an added file just comparing the new and 1154 # dest files. 1155 if [ $cmp -eq $COMPARE_DIFFTYPE ]; then 1156 case $newdestcmp in 1157 $COMPARE_DIFFLINKS) 1158 new=`readlink $NEWTREE/$file` 1159 dest=`readlink $DESTDIR/$file` 1160 warn \ 1161 "New link conflict: $file (\"$new\" vs \"$dest\")" 1162 return 1163 ;; 1164 $COMPARE_DIFFFILES) 1165 new_conflict $file 1166 echo " C $file" 1167 return 1168 ;; 1169 esac 1170 else 1171 # If the file has not changed types between the old 1172 # and new trees, but it is a different type in 1173 # DESTDIR, then just warn. 1174 if [ $newdestcmp -eq $COMPARE_DIFFTYPE ]; then 1175 new=`file_type $NEWTREE/$file` 1176 dest=`file_type $DESTDIR/$file` 1177 warn "Modified mismatch: $file ($new vs $dest)" 1178 return 1179 fi 1180 fi 1181 1182 case $cmp in 1183 $COMPARE_DIFFTYPE) 1184 old=`file_type $OLDTREE/$file` 1185 new=`file_type $NEWTREE/$file` 1186 dest=`file_type $DESTDIR/$file` 1187 warn "Modified $dest changed: $file ($old became $new)" 1188 ;; 1189 $COMPARE_DIFFLINKS) 1190 old=`readlink $OLDTREE/$file` 1191 new=`readlink $NEWTREE/$file` 1192 warn \ 1193 "Modified link changed: $file (\"$old\" became \"$new\")" 1194 ;; 1195 $COMPARE_DIFFFILES) 1196 merge_file $file 1197 ;; 1198 esac 1199} 1200 1201# Handle a file that has been added in the new tree. If the file does 1202# not exist in DESTDIR, simply copy the file into DESTDIR. If the 1203# file exists in the DESTDIR and is identical to the new version, do 1204# nothing. Otherwise, generate a diff of the two versions of the file 1205# and mark it as a conflict. 1206# 1207# $1 - pathname of the file (relative to DESTDIR) 1208handle_added_file() 1209{ 1210 local cmp dest file new 1211 1212 file=$1 1213 if ignore $file; then 1214 log "IGNORE: added file $file" 1215 return 1216 fi 1217 1218 compare $DESTDIR/$file $NEWTREE/$file 1219 cmp=$? 1220 case $cmp in 1221 $COMPARE_EQUAL) 1222 return 1223 ;; 1224 $COMPARE_ONLYFIRST) 1225 panic "Added file now missing" 1226 ;; 1227 $COMPARE_ONLYSECOND) 1228 # Ignore new directories. They will be 1229 # created as needed when non-directory nodes 1230 # are installed. 1231 if ! [ -d $NEWTREE/$file ]; then 1232 if install_new $file; then 1233 echo " A $file" 1234 fi 1235 fi 1236 return 1237 ;; 1238 esac 1239 1240 1241 # Treat the file as unmodified and force install of the new 1242 # file if it matches an ALWAYS_INSTALL glob. If the update 1243 # attempt fails, then fall through to the normal case so a 1244 # warning is generated. 1245 if always_install $file; then 1246 log "ALWAYS: updating $file" 1247 if update_unmodified $file; then 1248 return 1249 fi 1250 fi 1251 1252 case $cmp in 1253 $COMPARE_DIFFTYPE) 1254 new=`file_type $NEWTREE/$file` 1255 dest=`file_type $DESTDIR/$file` 1256 warn "New file mismatch: $file ($new vs $dest)" 1257 ;; 1258 $COMPARE_DIFFLINKS) 1259 new=`readlink $NEWTREE/$file` 1260 dest=`readlink $DESTDIR/$file` 1261 warn "New link conflict: $file (\"$new\" vs \"$dest\")" 1262 ;; 1263 $COMPARE_DIFFFILES) 1264 # If the only change in the new file versus 1265 # the destination file is a change in the 1266 # FreeBSD ID string and -F is specified, just 1267 # install the new file. 1268 if [ -n "$FREEBSD_ID" ] && \ 1269 fbsdid_only $NEWTREE/$file $DESTDIR/$file; then 1270 if update_unmodified $file; then 1271 return 1272 else 1273 panic \ 1274 "Updating FreeBSD ID string failed" 1275 fi 1276 fi 1277 1278 new_conflict $file 1279 echo " C $file" 1280 ;; 1281 esac 1282} 1283 1284# Main routines for each command 1285 1286# Build a new tree and save it in a tarball. 1287build_cmd() 1288{ 1289 local dir 1290 1291 if [ $# -ne 1 ]; then 1292 echo "Missing required tarball." 1293 echo 1294 usage 1295 fi 1296 1297 log "build command: $1" 1298 1299 # Create a temporary directory to hold the tree 1300 dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX` 1301 if [ $? -ne 0 ]; then 1302 echo "Unable to create temporary directory." 1303 exit 1 1304 fi 1305 if ! build_tree $dir; then 1306 echo "Failed to build tree." 1307 remove_tree $dir 1308 exit 1 1309 fi 1310 if ! tar cfj $1 -C $dir . >&3 2>&1; then 1311 echo "Failed to create tarball." 1312 remove_tree $dir 1313 exit 1 1314 fi 1315 remove_tree $dir 1316} 1317 1318# Output a diff comparing the tree at DESTDIR to the current 1319# unmodified tree. Note that this diff does not include files that 1320# are present in DESTDIR but not in the unmodified tree. 1321diff_cmd() 1322{ 1323 local file 1324 1325 if [ $# -ne 0 ]; then 1326 usage 1327 fi 1328 1329 # Requires an unmodified tree to diff against. 1330 if ! [ -d $NEWTREE ]; then 1331 echo "Reference tree to diff against unavailable." 1332 exit 1 1333 fi 1334 1335 # Unfortunately, diff alone does not quite provide the right 1336 # level of options that we want, so improvise. 1337 for file in `(cd $NEWTREE; find .) | sed -e 's/^\.//'`; do 1338 if ignore $file; then 1339 continue 1340 fi 1341 1342 diffnode $NEWTREE "$DESTDIR" $file "stock" "local" 1343 done 1344} 1345 1346# Just extract a new tree into NEWTREE either by building a tree or 1347# extracting a tarball. This can be used to bootstrap updates by 1348# initializing the current "stock" tree to match the currently 1349# installed system. 1350# 1351# Unlike 'update', this command does not rotate or preserve an 1352# existing NEWTREE, it just replaces any existing tree. 1353extract_cmd() 1354{ 1355 1356 if [ $# -ne 0 ]; then 1357 usage 1358 fi 1359 1360 log "extract command: tarball=$tarball" 1361 1362 # Create a temporary directory to hold the tree 1363 dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX` 1364 if [ $? -ne 0 ]; then 1365 echo "Unable to create temporary directory." 1366 exit 1 1367 fi 1368 1369 extract_tree $dir 1370 1371 if [ -d $NEWTREE ]; then 1372 if ! remove_tree $NEWTREE; then 1373 echo "Unable to remove current tree." 1374 remove_tree $dir 1375 exit 1 1376 fi 1377 fi 1378 1379 if ! mv $dir $NEWTREE >&3 2>&1; then 1380 echo "Unable to rename temp tree to current tree." 1381 remove_tree $dir 1382 exit 1 1383 fi 1384} 1385 1386# Resolve conflicts left from an earlier merge. 1387resolve_cmd() 1388{ 1389 local conflicts 1390 1391 if [ $# -ne 0 ]; then 1392 usage 1393 fi 1394 1395 if ! [ -d $CONFLICTS ]; then 1396 return 1397 fi 1398 1399 if ! [ -d $NEWTREE ]; then 1400 echo "The current tree is not present to resolve conflicts." 1401 exit 1 1402 fi 1403 1404 conflicts=`(cd $CONFLICTS; find . ! -type d) | sed -e 's/^\.//'` 1405 for file in $conflicts; do 1406 resolve_conflict $file 1407 done 1408 1409 if [ -n "$NEWALIAS_WARN" ]; then 1410 warn "Needs update: /etc/mail/aliases.db" \ 1411 "(requires manual update via newaliases(1))" 1412 echo 1413 echo "Warnings:" 1414 echo " Needs update: /etc/mail/aliases.db" \ 1415 "(requires manual update via newaliases(1))" 1416 fi 1417} 1418 1419# Restore files to the stock version. Only files with a local change 1420# are restored from the stock version. 1421revert_cmd() 1422{ 1423 local cmp file 1424 1425 if [ $# -eq 0 ]; then 1426 usage 1427 fi 1428 1429 for file; do 1430 log "revert $file" 1431 1432 if ! [ -e $NEWTREE/$file ]; then 1433 echo "File $file does not exist in the current tree." 1434 exit 1 1435 fi 1436 if [ -d $NEWTREE/$file ]; then 1437 echo "File $file is a directory." 1438 exit 1 1439 fi 1440 1441 compare $DESTDIR/$file $NEWTREE/$file 1442 cmp=$? 1443 if [ $cmp -eq $COMPARE_EQUAL ]; then 1444 continue 1445 fi 1446 1447 if update_unmodified $file; then 1448 # If this file had a conflict, clean up the 1449 # conflict. 1450 if [ -e $CONFLICTS/$file ]; then 1451 if ! rm $CONFLICTS/$file >&3 2>&1; then 1452 echo "Failed to remove conflict " \ 1453 "for $file". 1454 fi 1455 fi 1456 fi 1457 done 1458} 1459 1460# Report a summary of the previous merge. Specifically, list any 1461# remaining conflicts followed by any warnings from the previous 1462# update. 1463status_cmd() 1464{ 1465 1466 if [ $# -ne 0 ]; then 1467 usage 1468 fi 1469 1470 if [ -d $CONFLICTS ]; then 1471 (cd $CONFLICTS; find . ! -type d) | sed -e 's/^\./ C /' 1472 fi 1473 if [ -s $WARNINGS ]; then 1474 echo "Warnings:" 1475 cat $WARNINGS 1476 fi 1477} 1478 1479# Perform an actual merge. The new tree can either already exist (if 1480# rerunning a merge), be extracted from a tarball, or generated from a 1481# source tree. 1482update_cmd() 1483{ 1484 local dir new old 1485 1486 if [ $# -ne 0 ]; then 1487 usage 1488 fi 1489 1490 log "update command: rerun=$rerun tarball=$tarball preworld=$preworld" 1491 1492 if [ `id -u` -ne 0 ]; then 1493 echo "Must be root to update a tree." 1494 exit 1 1495 fi 1496 1497 # Enforce a sane umask 1498 umask 022 1499 1500 # XXX: Should existing conflicts be ignored and removed during 1501 # a rerun? 1502 1503 # Trim the conflicts tree. Whine if there is anything left. 1504 if [ -e $CONFLICTS ]; then 1505 find -d $CONFLICTS -type d -empty -delete >&3 2>&1 1506 rmdir $CONFLICTS >&3 2>&1 1507 fi 1508 if [ -d $CONFLICTS ]; then 1509 echo "Conflicts remain from previous update, aborting." 1510 exit 1 1511 fi 1512 1513 # Save tree names to use for rotation later. 1514 old=$OLDTREE 1515 new=$NEWTREE 1516 if [ -z "$rerun" ]; then 1517 # Extract the new tree to a temporary directory. The 1518 # trees are only rotated after a successful update to 1519 # avoid races if an update command is interrupted 1520 # before it completes. 1521 dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX` 1522 if [ $? -ne 0 ]; then 1523 echo "Unable to create temporary directory." 1524 exit 1 1525 fi 1526 1527 # Populate the new tree. 1528 extract_tree $dir 1529 1530 # Compare the new tree against the previous tree. For 1531 # the preworld case OLDTREE already points to the 1532 # current stock tree. 1533 if [ -z "$preworld" ]; then 1534 OLDTREE=$NEWTREE 1535 fi 1536 NEWTREE=$dir 1537 fi 1538 1539 if ! [ -d $OLDTREE ]; then 1540 cat <<EOF 1541No previous tree to compare against, a sane comparison is not possible. 1542EOF 1543 log "No previous tree to compare against." 1544 if [ -n "$dir" ]; then 1545 if [ -n "$rerun" ]; then 1546 panic "Should not have a temporary directory" 1547 fi 1548 remove_tree $dir 1549 fi 1550 exit 1 1551 fi 1552 1553 # Build lists of nodes in the old and new trees. 1554 (cd $OLDTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/old.files 1555 (cd $NEWTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/new.files 1556 1557 # Split the files up into three groups using comm. 1558 comm -23 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/removed.files 1559 comm -13 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/added.files 1560 comm -12 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/both.files 1561 1562 # Initialize conflicts and warnings handling. 1563 rm -f $WARNINGS 1564 mkdir -p $CONFLICTS 1565 1566 # Ignore removed files for the pre-world case. A pre-world 1567 # update uses a stripped-down tree. 1568 if [ -n "$preworld" ]; then 1569 > $WORKDIR/removed.files 1570 fi 1571 1572 # The order for the following sections is important. In the 1573 # odd case that a directory is converted into a file, the 1574 # existing subfiles need to be removed if possible before the 1575 # file is converted. Similarly, in the case that a file is 1576 # converted into a directory, the file needs to be converted 1577 # into a directory if possible before the new files are added. 1578 1579 # First, handle removed files. 1580 for file in `cat $WORKDIR/removed.files`; do 1581 handle_removed_file $file 1582 done 1583 1584 # For the directory pass, reverse sort the list to effect a 1585 # depth-first traversal. This is needed to ensure that if a 1586 # directory with subdirectories is removed, the entire 1587 # directory is removed if there are no local modifications. 1588 for file in `sort -r $WORKDIR/removed.files`; do 1589 handle_removed_directory $file 1590 done 1591 1592 # Second, handle files that exist in both the old and new 1593 # trees. 1594 for file in `cat $WORKDIR/both.files`; do 1595 handle_modified_file $file 1596 done 1597 1598 # Finally, handle newly added files. 1599 for file in `cat $WORKDIR/added.files`; do 1600 handle_added_file $file 1601 done 1602 1603 if [ -n "$NEWALIAS_WARN" ]; then 1604 warn "Needs update: /etc/mail/aliases.db" \ 1605 "(requires manual update via newaliases(1))" 1606 fi 1607 1608 # Run any special one-off commands after an update has completed. 1609 post_update 1610 1611 if [ -s $WARNINGS ]; then 1612 echo "Warnings:" 1613 cat $WARNINGS 1614 fi 1615 1616 # If this was a dryrun, remove the temporary tree if we built 1617 # a new one. 1618 if [ -n "$dryrun" ]; then 1619 if [ -n "$dir" ]; then 1620 if [ -n "$rerun" ]; then 1621 panic "Should not have a temporary directory" 1622 fi 1623 remove_tree $dir 1624 fi 1625 return 1626 fi 1627 1628 # Finally, rotate any needed trees. 1629 if [ "$new" != "$NEWTREE" ]; then 1630 if [ -n "$rerun" ]; then 1631 panic "Should not have a temporary directory" 1632 fi 1633 if [ -z "$dir" ]; then 1634 panic "Should have a temporary directory" 1635 fi 1636 1637 # Rotate the old tree if needed 1638 if [ "$old" != "$OLDTREE" ]; then 1639 if [ -n "$preworld" ]; then 1640 panic "Old tree should be unchanged" 1641 fi 1642 1643 if ! remove_tree $old; then 1644 echo "Unable to remove previous old tree." 1645 exit 1 1646 fi 1647 1648 if ! mv $OLDTREE $old >&3 2>&1; then 1649 echo "Unable to rename old tree." 1650 exit 1 1651 fi 1652 fi 1653 1654 # Rotate the new tree. Remove a previous pre-world 1655 # tree if it exists. 1656 if [ -d $new ]; then 1657 if [ -z "$preworld" ]; then 1658 panic "New tree should be rotated to old" 1659 fi 1660 if ! remove_tree $new; then 1661 echo "Unable to remove previous pre-world tree." 1662 exit 1 1663 fi 1664 fi 1665 1666 if ! mv $NEWTREE $new >&3 2>&1; then 1667 echo "Unable to rename current tree." 1668 exit 1 1669 fi 1670 fi 1671} 1672 1673# Determine which command we are executing. A command may be 1674# specified as the first word. If one is not specified then 'update' 1675# is assumed as the default command. 1676command="update" 1677if [ $# -gt 0 ]; then 1678 case "$1" in 1679 build|diff|extract|status|resolve|revert) 1680 command="$1" 1681 shift 1682 ;; 1683 -*) 1684 # If first arg is an option, assume the 1685 # default command. 1686 ;; 1687 *) 1688 usage 1689 ;; 1690 esac 1691fi 1692 1693# Set default variable values. 1694 1695# The path to the source tree used to build trees. 1696SRCDIR=/usr/src 1697 1698# The destination directory where the modified files live. 1699DESTDIR= 1700 1701# Ignore changes in the FreeBSD ID string. 1702FREEBSD_ID= 1703 1704# Files that should always have the new version of the file installed. 1705ALWAYS_INSTALL= 1706 1707# Files to ignore and never update during a merge. 1708IGNORE_FILES= 1709 1710# Flags to pass to 'make' when building a tree. 1711MAKE_OPTIONS= 1712 1713# Include a config file if it exists. Note that command line options 1714# override any settings in the config file. More details are in the 1715# manual, but in general the following variables can be set: 1716# - ALWAYS_INSTALL 1717# - DESTDIR 1718# - EDITOR 1719# - FREEBSD_ID 1720# - IGNORE_FILES 1721# - LOGFILE 1722# - MAKE_OPTIONS 1723# - SRCDIR 1724# - WORKDIR 1725if [ -r /etc/etcupdate.conf ]; then 1726 . /etc/etcupdate.conf 1727fi 1728 1729# Parse command line options 1730tarball= 1731rerun= 1732always= 1733dryrun= 1734ignore= 1735nobuild= 1736preworld= 1737while getopts "d:nprs:t:A:BD:FI:L:M:" option; do 1738 case "$option" in 1739 d) 1740 WORKDIR=$OPTARG 1741 ;; 1742 n) 1743 dryrun=YES 1744 ;; 1745 p) 1746 preworld=YES 1747 ;; 1748 r) 1749 rerun=YES 1750 ;; 1751 s) 1752 SRCDIR=$OPTARG 1753 ;; 1754 t) 1755 tarball=$OPTARG 1756 ;; 1757 A) 1758 # To allow this option to be specified 1759 # multiple times, accumulate command-line 1760 # specified patterns in an 'always' variable 1761 # and use that to overwrite ALWAYS_INSTALL 1762 # after parsing all options. Need to be 1763 # careful here with globbing expansion. 1764 set -o noglob 1765 always="$always $OPTARG" 1766 set +o noglob 1767 ;; 1768 B) 1769 nobuild=YES 1770 ;; 1771 D) 1772 DESTDIR=$OPTARG 1773 ;; 1774 F) 1775 FREEBSD_ID=YES 1776 ;; 1777 I) 1778 # To allow this option to be specified 1779 # multiple times, accumulate command-line 1780 # specified patterns in an 'ignore' variable 1781 # and use that to overwrite IGNORE_FILES after 1782 # parsing all options. Need to be careful 1783 # here with globbing expansion. 1784 set -o noglob 1785 ignore="$ignore $OPTARG" 1786 set +o noglob 1787 ;; 1788 L) 1789 LOGFILE=$OPTARG 1790 ;; 1791 M) 1792 MAKE_OPTIONS="$OPTARG" 1793 ;; 1794 *) 1795 echo 1796 usage 1797 ;; 1798 esac 1799done 1800shift $((OPTIND - 1)) 1801 1802# Allow -A command line options to override ALWAYS_INSTALL set from 1803# the config file. 1804set -o noglob 1805if [ -n "$always" ]; then 1806 ALWAYS_INSTALL="$always" 1807fi 1808 1809# Allow -I command line options to override IGNORE_FILES set from the 1810# config file. 1811if [ -n "$ignore" ]; then 1812 IGNORE_FILES="$ignore" 1813fi 1814set +o noglob 1815 1816# Where the "old" and "new" trees are stored. 1817WORKDIR=${WORKDIR:-$DESTDIR/var/db/etcupdate} 1818 1819# Log file for verbose output from program that are run. The log file 1820# is opened on fd '3'. 1821LOGFILE=${LOGFILE:-$WORKDIR/log} 1822 1823# The path of the "old" tree 1824OLDTREE=$WORKDIR/old 1825 1826# The path of the "new" tree 1827NEWTREE=$WORKDIR/current 1828 1829# The path of the "conflicts" tree where files with merge conflicts are saved. 1830CONFLICTS=$WORKDIR/conflicts 1831 1832# The path of the "warnings" file that accumulates warning notes from an update. 1833WARNINGS=$WORKDIR/warnings 1834 1835# Use $EDITOR for resolving conflicts. If it is not set, default to vi. 1836EDITOR=${EDITOR:-/usr/bin/vi} 1837 1838# Files that need to be updated before installworld. 1839PREWORLD_FILES="etc/master.passwd etc/group" 1840 1841# Handle command-specific argument processing such as complaining 1842# about unsupported options. Since the configuration file is always 1843# included, do not complain about extra command line arguments that 1844# may have been set via the config file rather than the command line. 1845case $command in 1846 update) 1847 if [ -n "$rerun" -a -n "$tarball" ]; then 1848 echo "Only one of -r or -t can be specified." 1849 echo 1850 usage 1851 fi 1852 if [ -n "$rerun" -a -n "$preworld" ]; then 1853 echo "Only one of -p or -r can be specified." 1854 echo 1855 usage 1856 fi 1857 ;; 1858 build|diff|status|revert) 1859 if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" -o \ 1860 -n "$preworld" ]; then 1861 usage 1862 fi 1863 ;; 1864 resolve) 1865 if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" ]; then 1866 usage 1867 fi 1868 ;; 1869 extract) 1870 if [ -n "$dryrun" -o -n "$rerun" -o -n "$preworld" ]; then 1871 usage 1872 fi 1873 ;; 1874esac 1875 1876# Pre-world mode uses a different set of trees. It leaves the current 1877# tree as-is so it is still present for a full etcupdate run after the 1878# world install is complete. Instead, it installs a few critical files 1879# into a separate tree. 1880if [ -n "$preworld" ]; then 1881 OLDTREE=$NEWTREE 1882 NEWTREE=$WORKDIR/preworld 1883fi 1884 1885# Open the log file. Don't truncate it if doing a minor operation so 1886# that a minor operation doesn't lose log info from a major operation. 1887if ! mkdir -p $WORKDIR 2>/dev/null; then 1888 echo "Failed to create work directory $WORKDIR" 1889fi 1890 1891case $command in 1892 diff|resolve|revert|status) 1893 exec 3>>$LOGFILE 1894 ;; 1895 *) 1896 exec 3>$LOGFILE 1897 ;; 1898esac 1899 1900${command}_cmd "$@" 1901