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