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 -DNO_FILEMON" 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(8))" 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 diff3 -E -m ${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 "diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1" 824 if [ -z "$dryrun" ]; then 825 temp=$(mktemp -t etcupdate) 826 diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > ${temp} 827 mv -f ${temp} ${DESTDIR}$1 828 fi 829 post_install_file $1 830 echo " M $1" 831 ;; 832 1) 833 # Conflicts, save a version with conflict markers in 834 # the conflicts directory. 835 if [ -z "$dryrun" ]; then 836 install_dirs $NEWTREE $CONFLICTS $1 837 log "diff3 -m -A ${DESTDIR}$1 ${CONFLICTS}$1" 838 diff3 -m -A -L "yours" -L "original" -L "new" \ 839 ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > \ 840 ${CONFLICTS}$1 841 fi 842 echo " C $1" 843 ;; 844 *) 845 panic "merge failed with status $res" 846 ;; 847 esac 848} 849 850# Returns true if a file contains conflict markers from a merge conflict. 851# 852# $1 - pathname of the file to resolve (relative to DESTDIR) 853has_conflicts() 854{ 855 856 egrep -q '^(<{7}|\|{7}|={7}|>{7}) ' $CONFLICTS/$1 857} 858 859# Attempt to resolve a conflict. The user is prompted to choose an 860# action for each conflict. If the user edits the file, they are 861# prompted again for an action. The process is very similar to 862# resolving conflicts after an update or merge with Perforce or 863# Subversion. The prompts are modelled on a subset of the available 864# commands for resolving conflicts with Subversion. 865# 866# $1 - pathname of the file to resolve (relative to DESTDIR) 867resolve_conflict() 868{ 869 local command junk 870 871 echo "Resolving conflict in '$1':" 872 edit= 873 while true; do 874 # Only display the resolved command if the file 875 # doesn't contain any conflicts. 876 echo -n "Select: (p) postpone, (df) diff-full, (e) edit," 877 if ! has_conflicts $1; then 878 echo -n " (r) resolved," 879 fi 880 echo 881 echo -n " (h) help for more options: " 882 read command 883 case $command in 884 df) 885 diff -u ${DESTDIR}$1 ${CONFLICTS}$1 886 ;; 887 e) 888 $EDITOR ${CONFLICTS}$1 889 ;; 890 h) 891 cat <<EOF 892 (p) postpone - ignore this conflict for now 893 (df) diff-full - show all changes made to merged file 894 (e) edit - change merged file in an editor 895 (r) resolved - accept merged version of file 896 (mf) mine-full - accept local version of entire file (ignore new changes) 897 (tf) theirs-full - accept new version of entire file (lose local changes) 898 (h) help - show this list 899EOF 900 ;; 901 mf) 902 # For mine-full, just delete the 903 # merged file and leave the local 904 # version of the file as-is. 905 rm ${CONFLICTS}$1 906 return 907 ;; 908 p) 909 return 910 ;; 911 r) 912 # If the merged file has conflict 913 # markers, require confirmation. 914 if has_conflicts $1; then 915 echo "File '$1' still has conflicts," \ 916 "are you sure? (y/n) " 917 read junk 918 if [ "$junk" != "y" ]; then 919 continue 920 fi 921 fi 922 923 if ! install_resolved $1; then 924 panic "Unable to install merged" \ 925 "version of $1" 926 fi 927 rm ${CONFLICTS}$1 928 return 929 ;; 930 tf) 931 # For theirs-full, install the new 932 # version of the file over top of the 933 # existing file. 934 if ! install_new $1; then 935 panic "Unable to install new" \ 936 "version of $1" 937 fi 938 rm ${CONFLICTS}$1 939 return 940 ;; 941 *) 942 echo "Invalid command." 943 ;; 944 esac 945 done 946} 947 948# Handle a file that has been removed from the new tree. If the file 949# does not exist in DESTDIR, then there is nothing to do. If the file 950# exists in DESTDIR and is identical to the old version, remove it 951# from DESTDIR. Otherwise, whine about the conflict but leave the 952# file in DESTDIR. To handle directories, this uses two passes. The 953# first pass handles all non-directory files. The second pass handles 954# just directories and removes them if they are empty. 955# 956# If -F is specified, and the only difference in the file in DESTDIR 957# is a change in the FreeBSD ID string, then remove the file. 958# 959# $1 - pathname of the file (relative to DESTDIR) 960handle_removed_file() 961{ 962 local dest file 963 964 file=$1 965 if ignore $file; then 966 log "IGNORE: removed file $file" 967 return 968 fi 969 970 compare_fbsdid $DESTDIR/$file $OLDTREE/$file 971 case $? in 972 $COMPARE_EQUAL) 973 if ! [ -d $DESTDIR/$file ]; then 974 remove_old $file 975 fi 976 ;; 977 $COMPARE_ONLYFIRST) 978 panic "Removed file now missing" 979 ;; 980 $COMPARE_ONLYSECOND) 981 # Already removed, nothing to do. 982 ;; 983 $COMPARE_DIFFTYPE|$COMPARE_DIFFLINKS|$COMPARE_DIFFFILES) 984 dest=`file_type $DESTDIR/$file` 985 warn "Modified $dest remains: $file" 986 ;; 987 esac 988} 989 990# Handle a directory that has been removed from the new tree. Only 991# remove the directory if it is empty. 992# 993# $1 - pathname of the directory (relative to DESTDIR) 994handle_removed_directory() 995{ 996 local dir 997 998 dir=$1 999 if ignore $dir; then 1000 log "IGNORE: removed dir $dir" 1001 return 1002 fi 1003 1004 if [ -d $DESTDIR/$dir -a -d $OLDTREE/$dir ]; then 1005 if empty_destdir $dir; then 1006 log "rmdir ${DESTDIR}$dir" 1007 if [ -z "$dryrun" ]; then 1008 rmdir ${DESTDIR}$dir >/dev/null 2>&1 1009 fi 1010 echo " D $dir" 1011 else 1012 warn "Non-empty directory remains: $dir" 1013 fi 1014 fi 1015} 1016 1017# Handle a file that exists in both the old and new trees. If the 1018# file has not changed in the old and new trees, there is nothing to 1019# do. If the file in the destination directory matches the new file, 1020# there is nothing to do. If the file in the destination directory 1021# matches the old file, then the new file should be installed. 1022# Everything else becomes some sort of conflict with more detailed 1023# handling. 1024# 1025# $1 - pathname of the file (relative to DESTDIR) 1026handle_modified_file() 1027{ 1028 local cmp dest file new newdestcmp old 1029 1030 file=$1 1031 if ignore $file; then 1032 log "IGNORE: modified file $file" 1033 return 1034 fi 1035 1036 compare $OLDTREE/$file $NEWTREE/$file 1037 cmp=$? 1038 if [ $cmp -eq $COMPARE_EQUAL ]; then 1039 return 1040 fi 1041 1042 if [ $cmp -eq $COMPARE_ONLYFIRST -o $cmp -eq $COMPARE_ONLYSECOND ]; then 1043 panic "Changed file now missing" 1044 fi 1045 1046 compare $NEWTREE/$file $DESTDIR/$file 1047 newdestcmp=$? 1048 if [ $newdestcmp -eq $COMPARE_EQUAL ]; then 1049 return 1050 fi 1051 1052 # If the only change in the new file versus the destination 1053 # file is a change in the FreeBSD ID string and -F is 1054 # specified, just install the new file. 1055 if [ -n "$FREEBSD_ID" -a $newdestcmp -eq $COMPARE_DIFFFILES ] && \ 1056 fbsdid_only $NEWTREE/$file $DESTDIR/$file; then 1057 if update_unmodified $file; then 1058 return 1059 else 1060 panic "Updating FreeBSD ID string failed" 1061 fi 1062 fi 1063 1064 # If the local file is the same as the old file, install the 1065 # new file. If -F is specified and the only local change is 1066 # in the FreeBSD ID string, then install the new file as well. 1067 if compare_fbsdid $OLDTREE/$file $DESTDIR/$file; then 1068 if update_unmodified $file; then 1069 return 1070 fi 1071 fi 1072 1073 # If the file was removed from the dest tree, just whine. 1074 if [ $newdestcmp -eq $COMPARE_ONLYFIRST ]; then 1075 # If the removed file matches an ALWAYS_INSTALL glob, 1076 # then just install the new version of the file. 1077 if always_install $file; then 1078 log "ALWAYS: adding $file" 1079 if ! [ -d $NEWTREE/$file ]; then 1080 if install_new $file; then 1081 echo " A $file" 1082 fi 1083 fi 1084 return 1085 fi 1086 1087 # If the only change in the new file versus the old 1088 # file is a change in the FreeBSD ID string and -F is 1089 # specified, don't warn. 1090 if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \ 1091 fbsdid_only $OLDTREE/$file $NEWTREE/$file; then 1092 return 1093 fi 1094 1095 case $cmp in 1096 $COMPARE_DIFFTYPE) 1097 old=`file_type $OLDTREE/$file` 1098 new=`file_type $NEWTREE/$file` 1099 warn "Remove mismatch: $file ($old became $new)" 1100 ;; 1101 $COMPARE_DIFFLINKS) 1102 old=`readlink $OLDTREE/$file` 1103 new=`readlink $NEWTREE/$file` 1104 warn \ 1105 "Removed link changed: $file (\"$old\" became \"$new\")" 1106 ;; 1107 $COMPARE_DIFFFILES) 1108 warn "Removed file changed: $file" 1109 ;; 1110 esac 1111 return 1112 fi 1113 1114 # Treat the file as unmodified and force install of the new 1115 # file if it matches an ALWAYS_INSTALL glob. If the update 1116 # attempt fails, then fall through to the normal case so a 1117 # warning is generated. 1118 if always_install $file; then 1119 log "ALWAYS: updating $file" 1120 if update_unmodified $file; then 1121 return 1122 fi 1123 fi 1124 1125 # If the only change in the new file versus the old file is a 1126 # change in the FreeBSD ID string and -F is specified, just 1127 # update the FreeBSD ID string in the local file. 1128 if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \ 1129 fbsdid_only $OLDTREE/$file $NEWTREE/$file; then 1130 if update_freebsdid $file; then 1131 continue 1132 fi 1133 fi 1134 1135 # If the file changed types between the old and new trees but 1136 # the files in the new and dest tree are both of the same 1137 # type, treat it like an added file just comparing the new and 1138 # dest files. 1139 if [ $cmp -eq $COMPARE_DIFFTYPE ]; then 1140 case $newdestcmp in 1141 $COMPARE_DIFFLINKS) 1142 new=`readlink $NEWTREE/$file` 1143 dest=`readlink $DESTDIR/$file` 1144 warn \ 1145 "New link conflict: $file (\"$new\" vs \"$dest\")" 1146 return 1147 ;; 1148 $COMPARE_DIFFFILES) 1149 new_conflict $file 1150 echo " C $file" 1151 return 1152 ;; 1153 esac 1154 else 1155 # If the file has not changed types between the old 1156 # and new trees, but it is a different type in 1157 # DESTDIR, then just warn. 1158 if [ $newdestcmp -eq $COMPARE_DIFFTYPE ]; then 1159 new=`file_type $NEWTREE/$file` 1160 dest=`file_type $DESTDIR/$file` 1161 warn "Modified mismatch: $file ($new vs $dest)" 1162 return 1163 fi 1164 fi 1165 1166 case $cmp in 1167 $COMPARE_DIFFTYPE) 1168 old=`file_type $OLDTREE/$file` 1169 new=`file_type $NEWTREE/$file` 1170 dest=`file_type $DESTDIR/$file` 1171 warn "Modified $dest changed: $file ($old became $new)" 1172 ;; 1173 $COMPARE_DIFFLINKS) 1174 old=`readlink $OLDTREE/$file` 1175 new=`readlink $NEWTREE/$file` 1176 warn \ 1177 "Modified link changed: $file (\"$old\" became \"$new\")" 1178 ;; 1179 $COMPARE_DIFFFILES) 1180 merge_file $file 1181 ;; 1182 esac 1183} 1184 1185# Handle a file that has been added in the new tree. If the file does 1186# not exist in DESTDIR, simply copy the file into DESTDIR. If the 1187# file exists in the DESTDIR and is identical to the new version, do 1188# nothing. Otherwise, generate a diff of the two versions of the file 1189# and mark it as a conflict. 1190# 1191# $1 - pathname of the file (relative to DESTDIR) 1192handle_added_file() 1193{ 1194 local cmp dest file new 1195 1196 file=$1 1197 if ignore $file; then 1198 log "IGNORE: added file $file" 1199 return 1200 fi 1201 1202 compare $DESTDIR/$file $NEWTREE/$file 1203 cmp=$? 1204 case $cmp in 1205 $COMPARE_EQUAL) 1206 return 1207 ;; 1208 $COMPARE_ONLYFIRST) 1209 panic "Added file now missing" 1210 ;; 1211 $COMPARE_ONLYSECOND) 1212 # Ignore new directories. They will be 1213 # created as needed when non-directory nodes 1214 # are installed. 1215 if ! [ -d $NEWTREE/$file ]; then 1216 if install_new $file; then 1217 echo " A $file" 1218 fi 1219 fi 1220 return 1221 ;; 1222 esac 1223 1224 1225 # Treat the file as unmodified and force install of the new 1226 # file if it matches an ALWAYS_INSTALL glob. If the update 1227 # attempt fails, then fall through to the normal case so a 1228 # warning is generated. 1229 if always_install $file; then 1230 log "ALWAYS: updating $file" 1231 if update_unmodified $file; then 1232 return 1233 fi 1234 fi 1235 1236 case $cmp in 1237 $COMPARE_DIFFTYPE) 1238 new=`file_type $NEWTREE/$file` 1239 dest=`file_type $DESTDIR/$file` 1240 warn "New file mismatch: $file ($new vs $dest)" 1241 ;; 1242 $COMPARE_DIFFLINKS) 1243 new=`readlink $NEWTREE/$file` 1244 dest=`readlink $DESTDIR/$file` 1245 warn "New link conflict: $file (\"$new\" vs \"$dest\")" 1246 ;; 1247 $COMPARE_DIFFFILES) 1248 # If the only change in the new file versus 1249 # the destination file is a change in the 1250 # FreeBSD ID string and -F is specified, just 1251 # install the new file. 1252 if [ -n "$FREEBSD_ID" ] && \ 1253 fbsdid_only $NEWTREE/$file $DESTDIR/$file; then 1254 if update_unmodified $file; then 1255 return 1256 else 1257 panic \ 1258 "Updating FreeBSD ID string failed" 1259 fi 1260 fi 1261 1262 new_conflict $file 1263 echo " C $file" 1264 ;; 1265 esac 1266} 1267 1268# Main routines for each command 1269 1270# Build a new tree and save it in a tarball. 1271build_cmd() 1272{ 1273 local dir 1274 1275 if [ $# -ne 1 ]; then 1276 echo "Missing required tarball." 1277 echo 1278 usage 1279 fi 1280 1281 log "build command: $1" 1282 1283 # Create a temporary directory to hold the tree 1284 dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX` 1285 if [ $? -ne 0 ]; then 1286 echo "Unable to create temporary directory." 1287 exit 1 1288 fi 1289 if ! build_tree $dir; then 1290 echo "Failed to build tree." 1291 remove_tree $dir 1292 exit 1 1293 fi 1294 if ! tar cfj $1 -C $dir . >&3 2>&1; then 1295 echo "Failed to create tarball." 1296 remove_tree $dir 1297 exit 1 1298 fi 1299 remove_tree $dir 1300} 1301 1302# Output a diff comparing the tree at DESTDIR to the current 1303# unmodified tree. Note that this diff does not include files that 1304# are present in DESTDIR but not in the unmodified tree. 1305diff_cmd() 1306{ 1307 local file 1308 1309 if [ $# -ne 0 ]; then 1310 usage 1311 fi 1312 1313 # Requires an unmodified tree to diff against. 1314 if ! [ -d $NEWTREE ]; then 1315 echo "Reference tree to diff against unavailable." 1316 exit 1 1317 fi 1318 1319 # Unfortunately, diff alone does not quite provide the right 1320 # level of options that we want, so improvise. 1321 for file in `(cd $NEWTREE; find .) | sed -e 's/^\.//'`; do 1322 if ignore $file; then 1323 continue 1324 fi 1325 1326 diffnode $NEWTREE "$DESTDIR" $file "stock" "local" 1327 done 1328} 1329 1330# Just extract a new tree into NEWTREE either by building a tree or 1331# extracting a tarball. This can be used to bootstrap updates by 1332# initializing the current "stock" tree to match the currently 1333# installed system. 1334# 1335# Unlike 'update', this command does not rotate or preserve an 1336# existing NEWTREE, it just replaces any existing tree. 1337extract_cmd() 1338{ 1339 1340 if [ $# -ne 0 ]; then 1341 usage 1342 fi 1343 1344 log "extract command: tarball=$tarball" 1345 1346 if [ -d $NEWTREE ]; then 1347 if ! remove_tree $NEWTREE; then 1348 echo "Unable to remove current tree." 1349 exit 1 1350 fi 1351 fi 1352 1353 extract_tree 1354} 1355 1356# Resolve conflicts left from an earlier merge. 1357resolve_cmd() 1358{ 1359 local conflicts 1360 1361 if [ $# -ne 0 ]; then 1362 usage 1363 fi 1364 1365 if ! [ -d $CONFLICTS ]; then 1366 return 1367 fi 1368 1369 if ! [ -d $NEWTREE ]; then 1370 echo "The current tree is not present to resolve conflicts." 1371 exit 1 1372 fi 1373 1374 conflicts=`(cd $CONFLICTS; find . ! -type d) | sed -e 's/^\.//'` 1375 for file in $conflicts; do 1376 resolve_conflict $file 1377 done 1378 1379 if [ -n "$NEWALIAS_WARN" ]; then 1380 warn "Needs update: /etc/mail/aliases.db" \ 1381 "(requires manual update via newaliases(1))" 1382 echo 1383 echo "Warnings:" 1384 echo " Needs update: /etc/mail/aliases.db" \ 1385 "(requires manual update via newaliases(1))" 1386 fi 1387} 1388 1389# Report a summary of the previous merge. Specifically, list any 1390# remaining conflicts followed by any warnings from the previous 1391# update. 1392status_cmd() 1393{ 1394 1395 if [ $# -ne 0 ]; then 1396 usage 1397 fi 1398 1399 if [ -d $CONFLICTS ]; then 1400 (cd $CONFLICTS; find . ! -type d) | sed -e 's/^\./ C /' 1401 fi 1402 if [ -s $WARNINGS ]; then 1403 echo "Warnings:" 1404 cat $WARNINGS 1405 fi 1406} 1407 1408# Perform an actual merge. The new tree can either already exist (if 1409# rerunning a merge), be extracted from a tarball, or generated from a 1410# source tree. 1411update_cmd() 1412{ 1413 local dir 1414 1415 if [ $# -ne 0 ]; then 1416 usage 1417 fi 1418 1419 log "update command: rerun=$rerun tarball=$tarball preworld=$preworld" 1420 1421 if [ `id -u` -ne 0 ]; then 1422 echo "Must be root to update a tree." 1423 exit 1 1424 fi 1425 1426 # Enforce a sane umask 1427 umask 022 1428 1429 # XXX: Should existing conflicts be ignored and removed during 1430 # a rerun? 1431 1432 # Trim the conflicts tree. Whine if there is anything left. 1433 if [ -e $CONFLICTS ]; then 1434 find -d $CONFLICTS -type d -empty -delete >&3 2>&1 1435 rmdir $CONFLICTS >&3 2>&1 1436 fi 1437 if [ -d $CONFLICTS ]; then 1438 echo "Conflicts remain from previous update, aborting." 1439 exit 1 1440 fi 1441 1442 if [ -z "$rerun" ]; then 1443 # For a dryrun that is not a rerun, do not rotate the existing 1444 # stock tree. Instead, extract a tree to a temporary directory 1445 # and use that for the comparison. 1446 if [ -n "$dryrun" ]; then 1447 dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX` 1448 if [ $? -ne 0 ]; then 1449 echo "Unable to create temporary directory." 1450 exit 1 1451 fi 1452 1453 # A pre-world dryrun has already set OLDTREE to 1454 # point to the current stock tree. 1455 if [ -z "$preworld" ]; then 1456 OLDTREE=$NEWTREE 1457 fi 1458 NEWTREE=$dir 1459 1460 # For a pre-world update, blow away any pre-existing 1461 # NEWTREE. 1462 elif [ -n "$preworld" ]; then 1463 if ! remove_tree $NEWTREE; then 1464 echo "Unable to remove pre-world tree." 1465 exit 1 1466 fi 1467 1468 # Rotate the existing stock tree to the old tree. 1469 elif [ -d $NEWTREE ]; then 1470 # First, delete the previous old tree if it exists. 1471 if ! remove_tree $OLDTREE; then 1472 echo "Unable to remove old tree." 1473 exit 1 1474 fi 1475 1476 # Move the current stock tree. 1477 if ! mv $NEWTREE $OLDTREE >&3 2>&1; then 1478 echo "Unable to rename current stock tree." 1479 exit 1 1480 fi 1481 fi 1482 1483 if ! [ -d $OLDTREE ]; then 1484 cat <<EOF 1485No previous tree to compare against, a sane comparison is not possible. 1486EOF 1487 log "No previous tree to compare against." 1488 if [ -n "$dir" ]; then 1489 rmdir $dir 1490 fi 1491 exit 1 1492 fi 1493 1494 # Populate the new tree. 1495 extract_tree 1496 fi 1497 1498 # Build lists of nodes in the old and new trees. 1499 (cd $OLDTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/old.files 1500 (cd $NEWTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/new.files 1501 1502 # Split the files up into three groups using comm. 1503 comm -23 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/removed.files 1504 comm -13 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/added.files 1505 comm -12 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/both.files 1506 1507 # Initialize conflicts and warnings handling. 1508 rm -f $WARNINGS 1509 mkdir -p $CONFLICTS 1510 1511 # Ignore removed files for the pre-world case. A pre-world 1512 # update uses a stripped-down tree. 1513 if [ -n "$preworld" ]; then 1514 > $WORKDIR/removed.files 1515 fi 1516 1517 # The order for the following sections is important. In the 1518 # odd case that a directory is converted into a file, the 1519 # existing subfiles need to be removed if possible before the 1520 # file is converted. Similarly, in the case that a file is 1521 # converted into a directory, the file needs to be converted 1522 # into a directory if possible before the new files are added. 1523 1524 # First, handle removed files. 1525 for file in `cat $WORKDIR/removed.files`; do 1526 handle_removed_file $file 1527 done 1528 1529 # For the directory pass, reverse sort the list to effect a 1530 # depth-first traversal. This is needed to ensure that if a 1531 # directory with subdirectories is removed, the entire 1532 # directory is removed if there are no local modifications. 1533 for file in `sort -r $WORKDIR/removed.files`; do 1534 handle_removed_directory $file 1535 done 1536 1537 # Second, handle files that exist in both the old and new 1538 # trees. 1539 for file in `cat $WORKDIR/both.files`; do 1540 handle_modified_file $file 1541 done 1542 1543 # Finally, handle newly added files. 1544 for file in `cat $WORKDIR/added.files`; do 1545 handle_added_file $file 1546 done 1547 1548 if [ -n "$NEWALIAS_WARN" ]; then 1549 warn "Needs update: /etc/mail/aliases.db" \ 1550 "(requires manual update via newaliases(1))" 1551 fi 1552 1553 # Run any special one-off commands after an update has completed. 1554 post_update 1555 1556 if [ -s $WARNINGS ]; then 1557 echo "Warnings:" 1558 cat $WARNINGS 1559 fi 1560 1561 if [ -n "$dir" ]; then 1562 if [ -z "$dryrun" -o -n "$rerun" ]; then 1563 panic "Should not have a temporary directory" 1564 fi 1565 1566 remove_tree $dir 1567 fi 1568} 1569 1570# Determine which command we are executing. A command may be 1571# specified as the first word. If one is not specified then 'update' 1572# is assumed as the default command. 1573command="update" 1574if [ $# -gt 0 ]; then 1575 case "$1" in 1576 build|diff|extract|status|resolve) 1577 command="$1" 1578 shift 1579 ;; 1580 -*) 1581 # If first arg is an option, assume the 1582 # default command. 1583 ;; 1584 *) 1585 usage 1586 ;; 1587 esac 1588fi 1589 1590# Set default variable values. 1591 1592# The path to the source tree used to build trees. 1593SRCDIR=/usr/src 1594 1595# The destination directory where the modified files live. 1596DESTDIR= 1597 1598# Ignore changes in the FreeBSD ID string. 1599FREEBSD_ID= 1600 1601# Files that should always have the new version of the file installed. 1602ALWAYS_INSTALL= 1603 1604# Files to ignore and never update during a merge. 1605IGNORE_FILES= 1606 1607# Flags to pass to 'make' when building a tree. 1608MAKE_OPTIONS= 1609 1610# Include a config file if it exists. Note that command line options 1611# override any settings in the config file. More details are in the 1612# manual, but in general the following variables can be set: 1613# - ALWAYS_INSTALL 1614# - DESTDIR 1615# - EDITOR 1616# - FREEBSD_ID 1617# - IGNORE_FILES 1618# - LOGFILE 1619# - MAKE_OPTIONS 1620# - SRCDIR 1621# - WORKDIR 1622if [ -r /etc/etcupdate.conf ]; then 1623 . /etc/etcupdate.conf 1624fi 1625 1626# Parse command line options 1627tarball= 1628rerun= 1629always= 1630dryrun= 1631ignore= 1632nobuild= 1633preworld= 1634while getopts "d:nprs:t:A:BD:FI:L:M:" option; do 1635 case "$option" in 1636 d) 1637 WORKDIR=$OPTARG 1638 ;; 1639 n) 1640 dryrun=YES 1641 ;; 1642 p) 1643 preworld=YES 1644 ;; 1645 r) 1646 rerun=YES 1647 ;; 1648 s) 1649 SRCDIR=$OPTARG 1650 ;; 1651 t) 1652 tarball=$OPTARG 1653 ;; 1654 A) 1655 # To allow this option to be specified 1656 # multiple times, accumulate command-line 1657 # specified patterns in an 'always' variable 1658 # and use that to overwrite ALWAYS_INSTALL 1659 # after parsing all options. Need to be 1660 # careful here with globbing expansion. 1661 set -o noglob 1662 always="$always $OPTARG" 1663 set +o noglob 1664 ;; 1665 B) 1666 nobuild=YES 1667 ;; 1668 D) 1669 DESTDIR=$OPTARG 1670 ;; 1671 F) 1672 FREEBSD_ID=YES 1673 ;; 1674 I) 1675 # To allow this option to be specified 1676 # multiple times, accumulate command-line 1677 # specified patterns in an 'ignore' variable 1678 # and use that to overwrite IGNORE_FILES after 1679 # parsing all options. Need to be careful 1680 # here with globbing expansion. 1681 set -o noglob 1682 ignore="$ignore $OPTARG" 1683 set +o noglob 1684 ;; 1685 L) 1686 LOGFILE=$OPTARG 1687 ;; 1688 M) 1689 MAKE_OPTIONS="$OPTARG" 1690 ;; 1691 *) 1692 echo 1693 usage 1694 ;; 1695 esac 1696done 1697shift $((OPTIND - 1)) 1698 1699# Allow -A command line options to override ALWAYS_INSTALL set from 1700# the config file. 1701set -o noglob 1702if [ -n "$always" ]; then 1703 ALWAYS_INSTALL="$always" 1704fi 1705 1706# Allow -I command line options to override IGNORE_FILES set from the 1707# config file. 1708if [ -n "$ignore" ]; then 1709 IGNORE_FILES="$ignore" 1710fi 1711set +o noglob 1712 1713# Where the "old" and "new" trees are stored. 1714WORKDIR=${WORKDIR:-$DESTDIR/var/db/etcupdate} 1715 1716# Log file for verbose output from program that are run. The log file 1717# is opened on fd '3'. 1718LOGFILE=${LOGFILE:-$WORKDIR/log} 1719 1720# The path of the "old" tree 1721OLDTREE=$WORKDIR/old 1722 1723# The path of the "new" tree 1724NEWTREE=$WORKDIR/current 1725 1726# The path of the "conflicts" tree where files with merge conflicts are saved. 1727CONFLICTS=$WORKDIR/conflicts 1728 1729# The path of the "warnings" file that accumulates warning notes from an update. 1730WARNINGS=$WORKDIR/warnings 1731 1732# Use $EDITOR for resolving conflicts. If it is not set, default to vi. 1733EDITOR=${EDITOR:-/usr/bin/vi} 1734 1735# Files that need to be updated before installworld. 1736PREWORLD_FILES="etc/master.passwd etc/group" 1737 1738# Handle command-specific argument processing such as complaining 1739# about unsupported options. Since the configuration file is always 1740# included, do not complain about extra command line arguments that 1741# may have been set via the config file rather than the command line. 1742case $command in 1743 update) 1744 if [ -n "$rerun" -a -n "$tarball" ]; then 1745 echo "Only one of -r or -t can be specified." 1746 echo 1747 usage 1748 fi 1749 if [ -n "$rerun" -a -n "$preworld" ]; then 1750 echo "Only one of -p or -r can be specified." 1751 echo 1752 usage 1753 fi 1754 ;; 1755 build|diff|status) 1756 if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" -o \ 1757 -n "$preworld" ]; then 1758 usage 1759 fi 1760 ;; 1761 resolve) 1762 if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" ]; then 1763 usage 1764 fi 1765 ;; 1766 extract) 1767 if [ -n "$dryrun" -o -n "$rerun" -o -n "$preworld" ]; then 1768 usage 1769 fi 1770 ;; 1771esac 1772 1773# Pre-world mode uses a different set of trees. It leaves the current 1774# tree as-is so it is still present for a full etcupdate run after the 1775# world install is complete. Instead, it installs a few critical files 1776# into a separate tree. 1777if [ -n "$preworld" ]; then 1778 OLDTREE=$NEWTREE 1779 NEWTREE=$WORKDIR/preworld 1780fi 1781 1782# Open the log file. Don't truncate it if doing a minor operation so 1783# that a minor operation doesn't lose log info from a major operation. 1784if ! mkdir -p $WORKDIR 2>/dev/null; then 1785 echo "Failed to create work directory $WORKDIR" 1786fi 1787 1788case $command in 1789 diff|resolve|status) 1790 exec 3>>$LOGFILE 1791 ;; 1792 *) 1793 exec 3>$LOGFILE 1794 ;; 1795esac 1796 1797${command}_cmd "$@" 1798