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