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