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