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