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