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