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