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