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