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