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