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