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