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