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