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