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