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