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