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