3 # Copyright (c) 2010 Advanced Computing Technologies LLC
4 # Written by: John H. Baldwin <jhb@FreeBSD.org>
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions
10 # 1. Redistributions of source code must retain the above copyright
11 # notice, this list of conditions and the following disclaimer.
12 # 2. Redistributions in binary form must reproduce the above copyright
13 # notice, this list of conditions and the following disclaimer in the
14 # documentation and/or other materials provided with the distribution.
16 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
17 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
20 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30 # This is a tool to manage updating files that are not updated as part
31 # of 'make installworld' such as files in /etc. Unlike other tools,
32 # this one is specifically tailored to assisting with mass upgrades.
33 # To that end it does not require user intervention while running.
35 # Theory of operation:
37 # The most reliable way to update changes to files that have local
38 # modifications is to perform a three-way merge between the original
39 # unmodified file, the new version of the file, and the modified file.
40 # This requires having all three versions of the file available when
41 # performing an update.
43 # To that end, etcupdate uses a strategy where the current unmodified
44 # tree is kept in WORKDIR/current and the previous unmodified tree is
45 # kept in WORKDIR/old. When performing a merge, a new tree is built
46 # if needed and then the changes are merged into DESTDIR. Any files
47 # with unresolved conflicts after the merge are left in a tree rooted
48 # at WORKDIR/conflicts.
50 # To provide extra flexibility, etcupdate can also build tarballs of
51 # root trees that can later be used. It can also use a tarball as the
52 # source of a new tree instead of building it from /usr/src.
54 # Global settings. These can be adjusted by config files and in some
55 # cases by command line options.
58 # - automatable conflict resolution
59 # - a 'revert' command to make a file "stock"
64 usage: etcupdate [-npBF] [-d workdir] [-r | -s source | -t tarball]
65 [-A patterns] [-D destdir] [-I patterns] [-L logfile]
67 etcupdate build [-B] [-d workdir] [-s source] [-L logfile] [-M options]
69 etcupdate diff [-d workdir] [-D destdir] [-I patterns] [-L logfile]
70 etcupdate extract [-B] [-d workdir] [-s source | -t tarball] [-L logfile]
72 etcupdate resolve [-p] [-d workdir] [-D destdir] [-L logfile]
73 etcupdate status [-d workdir] [-D destdir]
78 # Used to write a message prepended with '>>>' to the logfile.
84 # Used for assertion conditions that should never happen.
91 # Used to write a warning message. These are saved to the WARNINGS
92 # file with " " prepended.
95 echo -n " " >> $WARNINGS
96 echo "$@" >> $WARNINGS
99 # Output a horizontal rule using the passed-in character. Matches the
100 # length used for Index lines in CVS and SVN diffs.
108 # Output a text description of a specified file's type.
110 # $1 - file pathname.
113 stat -f "%HT" $1 | tr "[:upper:]" "[:lower:]"
116 # Returns true (0) if a file exists
118 # $1 - file pathname.
124 # Returns true (0) if a file should be ignored, false otherwise.
132 for pattern in $IGNORE_FILES; do
142 # Ignore /.cshrc and /.profile if they are hardlinked to the
143 # same file in /root. This ensures we only compare those
144 # files once in that case.
147 if [ ${DESTDIR}$1 -ef ${DESTDIR}/root$1 ]; then
158 # Returns true (0) if the new version of a file should always be
159 # installed rather than attempting to do a merge.
167 for pattern in $ALWAYS_INSTALL; do
182 # $1 - directory to store new tree in
185 local destdir dir file make
187 make="make $MAKE_OPTIONS"
189 log "Building tree at $1 with $make"
190 mkdir -p $1/usr/obj >&3 2>&1
191 destdir=`realpath $1`
193 if [ -n "$preworld" ]; then
194 # Build a limited tree that only contains files that are
195 # crucial to installworld.
196 for file in $PREWORLD_FILES; do
198 mkdir -p $1/$dir >&3 2>&1 || return 1
199 cp -p $SRCDIR/$file $1/$file || return 1
201 elif ! [ -n "$nobuild" ]; then
202 (cd $SRCDIR; $make DESTDIR=$destdir distrib-dirs &&
203 MAKEOBJDIRPREFIX=$destdir/usr/obj $make _obj SUBDIR_OVERRIDE=etc &&
204 MAKEOBJDIRPREFIX=$destdir/usr/obj $make everything SUBDIR_OVERRIDE=etc &&
205 MAKEOBJDIRPREFIX=$destdir/usr/obj $make DESTDIR=$destdir distribution) \
208 (cd $SRCDIR; $make DESTDIR=$destdir distrib-dirs &&
209 $make DESTDIR=$destdir distribution) >&3 2>&1 || return 1
211 chflags -R noschg $1 >&3 2>&1 || return 1
212 rm -rf $1/usr/obj >&3 2>&1 || return 1
214 # Purge auto-generated files. Only the source files need to
215 # be updated after which these files are regenerated.
216 rm -f $1/etc/*.db $1/etc/passwd >&3 2>&1 || return 1
218 # Remove empty files. These just clutter the output of 'diff'.
219 find $1 -type f -size 0 -delete >&3 2>&1 || return 1
221 # Trim empty directories.
222 find -d $1 -type d -empty -delete >&3 2>&1 || return 1
226 # Generate a new NEWTREE tree. If tarball is set, then the tree is
227 # extracted from the tarball. Otherwise the tree is built from a
233 # If we have a tarball, extract that into the new directory.
234 if [ -n "$tarball" ]; then
236 if [ -n "$preworld" ]; then
237 files="$PREWORLD_FILES"
239 if ! (mkdir -p $NEWTREE && tar xf $tarball -C $NEWTREE $files) \
241 echo "Failed to extract new tree."
246 if ! build_tree $NEWTREE; then
247 echo "Failed to build new tree."
254 # Forcefully remove a tree. Returns true (0) if the operation succeeds.
262 chflags -R noschg $1 >&3 2>&1
268 # Return values for compare()
276 # Compare two files/directories/symlinks. Note that this does not
277 # recurse into subdirectories. Instead, if two nodes are both
278 # directories, they are assumed to be equivalent.
280 # Returns true (0) if the nodes are identical. If only one of the two
281 # nodes are present, return one of the COMPARE_ONLY* constants. If
282 # the nodes are different, return one of the COMPARE_DIFF* constants
283 # to indicate the type of difference.
291 # If the first node doesn't exist, then check for the second
292 # node. Note that -e will fail for a symbolic link that
293 # points to a missing target.
296 return $COMPARE_ONLYSECOND
298 return $COMPARE_EQUAL
300 elif ! exists $2; then
301 return $COMPARE_ONLYFIRST
304 # If the two nodes are different file types fail.
305 first=`stat -f "%Hp" $1`
306 second=`stat -f "%Hp" $2`
307 if [ "$first" != "$second" ]; then
308 return $COMPARE_DIFFTYPE
311 # If both are symlinks, compare the link values.
315 if [ "$first" = "$second" ]; then
316 return $COMPARE_EQUAL
318 return $COMPARE_DIFFLINKS
322 # If both are files, compare the file contents.
324 if cmp -s $1 $2; then
325 return $COMPARE_EQUAL
327 return $COMPARE_DIFFFILES
331 # As long as the two nodes are the same type of file, consider
333 return $COMPARE_EQUAL
336 # Returns true (0) if the only difference between two regular files is a
337 # change in the FreeBSD ID string.
339 # $1 - path of first file
340 # $2 - path of second file
344 diff -qI '\$FreeBSD.*\$' $1 $2 >/dev/null 2>&1
347 # This is a wrapper around compare that will return COMPARE_EQUAL if
348 # the only difference between two regular files is a change in the
349 # FreeBSD ID string. It only makes this adjustment if the -F flag has
361 if [ -n "$FREEBSD_ID" -a "$cmp" -eq $COMPARE_DIFFFILES ] && \
362 fbsdid_only $1 $2; then
363 return $COMPARE_EQUAL
369 # Returns true (0) if a directory is empty.
371 # $1 - pathname of the directory to check
380 # Returns true (0) if one directories contents are a subset of the
381 # other. This will recurse to handle subdirectories and compares
382 # individual files in the trees. Its purpose is to quiet spurious
383 # directory warnings for dryrun invocations.
385 # $1 - first directory (sub)
386 # $2 - second directory (super)
391 if ! [ -d $1 -a -d $2 ]; then
395 # Ignore files that are present in the second directory but not
398 for file in $contents; do
399 if ! compare $1/$file $2/$file; then
403 if [ -d $1/$file ]; then
404 if ! dir_subset $1/$file $2/$file; then
412 # Returns true (0) if a directory in the destination tree is empty.
413 # If this is a dryrun, then this returns true as long as the contents
414 # of the directory are a subset of the contents in the old tree
415 # (meaning that the directory would be empty in a non-dryrun when this
416 # was invoked) to quiet spurious warnings.
418 # $1 - pathname of the directory to check relative to DESTDIR.
422 if [ -n "$dryrun" ]; then
423 dir_subset $DESTDIR/$1 $OLDTREE/$1
427 empty_dir $DESTDIR/$1
430 # Output a diff of two directory entries with the same relative name
431 # in different trees. Note that as with compare(), this does not
432 # recurse into subdirectories. If the nodes are identical, nothing is
438 # $4 - label for first tree
439 # $5 - label for second tree
442 local first second file old new diffargs
444 if [ -n "$FREEBSD_ID" ]; then
445 diffargs="-I \\\$FreeBSD.*\\\$"
450 compare_fbsdid $1/$3 $2/$3
465 first=`file_type $1/$3`
466 second=`file_type $2/$3`
468 echo "Node changed from a $first to a $second: $3"
472 first=`readlink $1/$file`
473 second=`readlink $2/$file`
475 echo "Link changed: $file"
484 diff -u $diffargs -L "$3 ($4)" $1/$3 -L "$3 ($5)" $2/$3
489 # Create missing parent directories of a node in a target tree
490 # preserving the owner, group, and permissions from a specified
495 # $3 - pathname of the node (relative to both trees)
502 # Nothing to do if the parent directory exists. This also
503 # catches the degenerate cases when the path is just a simple
505 if [ -d ${2}$dir ]; then
509 # If non-directory file exists with the desired directory
511 if exists ${2}$dir; then
512 # If this is a dryrun and we are installing the
513 # directory in the DESTDIR and the file in the DESTDIR
514 # matches the file in the old tree, then fake success
515 # to quiet spurious warnings.
516 if [ -n "$dryrun" -a "$2" = "$DESTDIR" ]; then
517 if compare $OLDTREE/$dir $DESTDIR/$dir; then
522 args=`file_type ${2}$dir`
523 warn "Directory mismatch: ${2}$dir ($args)"
527 # Ensure the parent directory of the directory is present
529 if ! install_dirs $1 "$2" $dir; then
533 # Format attributes from template directory as install(1)
535 args=`stat -f "-o %Su -g %Sg -m %0Mp%0Lp" $1/$dir`
537 log "install -d $args ${2}$dir"
538 if [ -z "$dryrun" ]; then
539 install -d $args ${2}$dir >&3 2>&1
544 # Perform post-install fixups for a file. This largely consists of
545 # regenerating any files that depend on the newly installed file.
547 # $1 - pathname of the updated file (relative to DESTDIR)
552 # Grr, newaliases only works for an empty DESTDIR.
553 if [ -z "$DESTDIR" ]; then
555 if [ -z "$dryrun" ]; then
563 log "cap_mkdb ${DESTDIR}$1"
564 if [ -z "$dryrun" ]; then
565 cap_mkdb ${DESTDIR}$1 >&3 2>&1
569 log "pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1"
570 if [ -z "$dryrun" ]; then
571 pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1 \
576 # /etc/rc.d/motd hardcodes the /etc/motd path.
577 # Don't warn about non-empty DESTDIR's since this
578 # change is only cosmetic anyway.
579 if [ -z "$DESTDIR" ]; then
580 log "sh /etc/rc.d/motd start"
581 if [ -z "$dryrun" ]; then
582 sh /etc/rc.d/motd start >&3 2>&1
589 # Install the "new" version of a file. Returns true if it succeeds
590 # and false otherwise.
592 # $1 - pathname of the file to install (relative to DESTDIR)
596 if ! install_dirs $NEWTREE "$DESTDIR" $1; then
599 log "cp -Rp ${NEWTREE}$1 ${DESTDIR}$1"
600 if [ -z "$dryrun" ]; then
601 cp -Rp ${NEWTREE}$1 ${DESTDIR}$1 >&3 2>&1
607 # Install the "resolved" version of a file. Returns true if it succeeds
608 # and false otherwise.
610 # $1 - pathname of the file to install (relative to DESTDIR)
614 # This should always be present since the file is already
615 # there (it caused a conflict). However, it doesn't hurt to
617 if ! install_dirs $NEWTREE "$DESTDIR" $1; then
621 log "cp -Rp ${CONFLICTS}$1 ${DESTDIR}$1"
622 cp -Rp ${CONFLICTS}$1 ${DESTDIR}$1 >&3 2>&1
627 # Generate a conflict file when a "new" file conflicts with an
628 # existing file in DESTDIR.
630 # $1 - pathname of the file that conflicts (relative to DESTDIR)
634 if [ -n "$dryrun" ]; then
638 install_dirs $NEWTREE $CONFLICTS $1
639 diff --changed-group-format='<<<<<<< (local)
642 ' $DESTDIR/$1 $NEWTREE/$1 > $CONFLICTS/$1
645 # Remove the "old" version of a file.
647 # $1 - pathname of the old file to remove (relative to DESTDIR)
650 log "rm -f ${DESTDIR}$1"
651 if [ -z "$dryrun" ]; then
652 rm -f ${DESTDIR}$1 >&3 2>&1
657 # Update a file that has no local modifications.
659 # $1 - pathname of the file to update (relative to DESTDIR)
664 # If the old file is a directory, then remove it with rmdir
665 # (this should only happen if the file has changed its type
666 # from a directory to a non-directory). If the directory
667 # isn't empty, then fail. This will be reported as a warning
669 if [ -d $DESTDIR/$1 ]; then
670 if empty_destdir $1; then
671 log "rmdir ${DESTDIR}$1"
672 if [ -z "$dryrun" ]; then
673 rmdir ${DESTDIR}$1 >&3 2>&1
679 # If both the old and new files are regular files, leave the
680 # existing file. This avoids breaking hard links for /.cshrc
681 # and /.profile. Otherwise, explicitly remove the old file.
682 elif ! [ -f ${DESTDIR}$1 -a -f ${NEWTREE}$1 ]; then
683 log "rm -f ${DESTDIR}$1"
684 if [ -z "$dryrun" ]; then
685 rm -f ${DESTDIR}$1 >&3 2>&1
689 # If the new file is a directory, note that the old file has
690 # been removed, but don't do anything else for now. The
691 # directory will be installed if needed when new files within
692 # that directory are installed.
693 if [ -d $NEWTREE/$1 ]; then
694 if empty_dir $NEWTREE/$1; then
699 elif install_new $1; then
705 # Update the FreeBSD ID string in a locally modified file to match the
706 # FreeBSD ID string from the "new" version of the file.
708 # $1 - pathname of the file to update (relative to DESTDIR)
713 # If the FreeBSD ID string is removed from the local file,
714 # there is nothing to do. In this case, treat the file as
715 # updated. Otherwise, if either file has more than one
716 # FreeBSD ID string, just punt and let the user handle the
718 new=`grep -c '\$FreeBSD.*\$' ${NEWTREE}$1`
719 dest=`grep -c '\$FreeBSD.*\$' ${DESTDIR}$1`
720 if [ "$dest" -eq 0 ]; then
723 if [ "$dest" -ne 1 -o "$dest" -ne 1 ]; then
727 # If the FreeBSD ID string in the new file matches the FreeBSD ID
728 # string in the local file, there is nothing to do.
729 new=`grep '\$FreeBSD.*\$' ${NEWTREE}$1`
730 dest=`grep '\$FreeBSD.*\$' ${DESTDIR}$1`
731 if [ "$new" = "$dest" ]; then
735 # Build the new file in three passes. First, copy all the
736 # lines preceding the FreeBSD ID string from the local version
737 # of the file. Second, append the FreeBSD ID string line from
738 # the new version. Finally, append all the lines after the
739 # FreeBSD ID string from the local version of the file.
740 file=`mktemp $WORKDIR/etcupdate-XXXXXXX`
741 awk '/\$FreeBSD.*\$/ { exit } { print }' ${DESTDIR}$1 >> $file
742 awk '/\$FreeBSD.*\$/ { print }' ${NEWTREE}$1 >> $file
743 awk '/\$FreeBSD.*\$/ { ok = 1; next } { if (ok) print }' \
744 ${DESTDIR}$1 >> $file
746 # As an extra sanity check, fail the attempt if the updated
747 # version of the file has any differences aside from the
749 if ! fbsdid_only ${DESTDIR}$1 $file; then
754 log "cp $file ${DESTDIR}$1"
755 if [ -z "$dryrun" ]; then
756 cp $file ${DESTDIR}$1 >&3 2>&1
764 # Attempt to update a file that has local modifications. This routine
765 # only handles regular files. If the 3-way merge succeeds without
766 # conflicts, the updated file is installed. If the merge fails, the
767 # merged version with conflict markers is left in the CONFLICTS tree.
769 # $1 - pathname of the file to merge (relative to DESTDIR)
774 # Try the merge to see if there is a conflict.
775 merge -q -p ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 >/dev/null 2>&3
779 # No conflicts, so just redo the merge to the
781 log "merge ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1"
782 if [ -z "$dryrun" ]; then
783 merge ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1
789 # Conflicts, save a version with conflict markers in
790 # the conflicts directory.
791 if [ -z "$dryrun" ]; then
792 install_dirs $NEWTREE $CONFLICTS $1
793 log "cp -Rp ${DESTDIR}$1 ${CONFLICTS}$1"
794 cp -Rp ${DESTDIR}$1 ${CONFLICTS}$1 >&3 2>&1
795 merge -A -q -L "yours" -L "original" -L "new" \
796 ${CONFLICTS}$1 ${OLDTREE}$1 ${NEWTREE}$1
801 panic "merge failed with status $res"
806 # Returns true if a file contains conflict markers from a merge conflict.
808 # $1 - pathname of the file to resolve (relative to DESTDIR)
812 egrep -q '^(<{7}|\|{7}|={7}|>{7}) ' $CONFLICTS/$1
815 # Attempt to resolve a conflict. The user is prompted to choose an
816 # action for each conflict. If the user edits the file, they are
817 # prompted again for an action. The process is very similar to
818 # resolving conflicts after an update or merge with Perforce or
819 # Subversion. The prompts are modelled on a subset of the available
820 # commands for resolving conflicts with Subversion.
822 # $1 - pathname of the file to resolve (relative to DESTDIR)
827 echo "Resolving conflict in '$1':"
830 # Only display the resolved command if the file
831 # doesn't contain any conflicts.
832 echo -n "Select: (p) postpone, (df) diff-full, (e) edit,"
833 if ! has_conflicts $1; then
834 echo -n " (r) resolved,"
837 echo -n " (h) help for more options: "
841 diff -u ${DESTDIR}$1 ${CONFLICTS}$1
844 $EDITOR ${CONFLICTS}$1
848 (p) postpone - ignore this conflict for now
849 (df) diff-full - show all changes made to merged file
850 (e) edit - change merged file in an editor
851 (r) resolved - accept merged version of file
852 (mf) mine-full - accept local version of entire file (ignore new changes)
853 (tf) theirs-full - accept new version of entire file (lose local changes)
854 (h) help - show this list
858 # For mine-full, just delete the
859 # merged file and leave the local
860 # version of the file as-is.
868 # If the merged file has conflict
869 # markers, require confirmation.
870 if has_conflicts $1; then
871 echo "File '$1' still has conflicts," \
872 "are you sure? (y/n) "
874 if [ "$junk" != "y" ]; then
879 if ! install_resolved $1; then
880 panic "Unable to install merged" \
887 # For theirs-full, install the new
888 # version of the file over top of the
890 if ! install_new $1; then
891 panic "Unable to install new" \
898 echo "Invalid command."
904 # Handle a file that has been removed from the new tree. If the file
905 # does not exist in DESTDIR, then there is nothing to do. If the file
906 # exists in DESTDIR and is identical to the old version, remove it
907 # from DESTDIR. Otherwise, whine about the conflict but leave the
908 # file in DESTDIR. To handle directories, this uses two passes. The
909 # first pass handles all non-directory files. The second pass handles
910 # just directories and removes them if they are empty.
912 # If -F is specified, and the only difference in the file in DESTDIR
913 # is a change in the FreeBSD ID string, then remove the file.
915 # $1 - pathname of the file (relative to DESTDIR)
916 handle_removed_file()
921 if ignore $file; then
922 log "IGNORE: removed file $file"
926 compare_fbsdid $DESTDIR/$file $OLDTREE/$file
929 if ! [ -d $DESTDIR/$file ]; then
934 panic "Removed file now missing"
937 # Already removed, nothing to do.
939 $COMPARE_DIFFTYPE|$COMPARE_DIFFLINKS|$COMPARE_DIFFFILES)
940 dest=`file_type $DESTDIR/$file`
941 warn "Modified $dest remains: $file"
946 # Handle a directory that has been removed from the new tree. Only
947 # remove the directory if it is empty.
949 # $1 - pathname of the directory (relative to DESTDIR)
950 handle_removed_directory()
956 log "IGNORE: removed dir $dir"
960 if [ -d $DESTDIR/$dir -a -d $OLDTREE/$dir ]; then
961 if empty_destdir $dir; then
962 log "rmdir ${DESTDIR}$dir"
963 if [ -z "$dryrun" ]; then
964 rmdir ${DESTDIR}$dir >/dev/null 2>&1
968 warn "Non-empty directory remains: $dir"
973 # Handle a file that exists in both the old and new trees. If the
974 # file has not changed in the old and new trees, there is nothing to
975 # do. If the file in the destination directory matches the new file,
976 # there is nothing to do. If the file in the destination directory
977 # matches the old file, then the new file should be installed.
978 # Everything else becomes some sort of conflict with more detailed
981 # $1 - pathname of the file (relative to DESTDIR)
982 handle_modified_file()
984 local cmp dest file new newdestcmp old
987 if ignore $file; then
988 log "IGNORE: modified file $file"
992 compare $OLDTREE/$file $NEWTREE/$file
994 if [ $cmp -eq $COMPARE_EQUAL ]; then
998 if [ $cmp -eq $COMPARE_ONLYFIRST -o $cmp -eq $COMPARE_ONLYSECOND ]; then
999 panic "Changed file now missing"
1002 compare $NEWTREE/$file $DESTDIR/$file
1004 if [ $newdestcmp -eq $COMPARE_EQUAL ]; then
1008 # If the only change in the new file versus the destination
1009 # file is a change in the FreeBSD ID string and -F is
1010 # specified, just install the new file.
1011 if [ -n "$FREEBSD_ID" -a $newdestcmp -eq $COMPARE_DIFFFILES ] && \
1012 fbsdid_only $NEWTREE/$file $DESTDIR/$file; then
1013 if update_unmodified $file; then
1016 panic "Updating FreeBSD ID string failed"
1020 # If the local file is the same as the old file, install the
1021 # new file. If -F is specified and the only local change is
1022 # in the FreeBSD ID string, then install the new file as well.
1023 if compare_fbsdid $OLDTREE/$file $DESTDIR/$file; then
1024 if update_unmodified $file; then
1029 # If the file was removed from the dest tree, just whine.
1030 if [ $newdestcmp -eq $COMPARE_ONLYFIRST ]; then
1031 # If the removed file matches an ALWAYS_INSTALL glob,
1032 # then just install the new version of the file.
1033 if always_install $file; then
1034 log "ALWAYS: adding $file"
1035 if ! [ -d $NEWTREE/$file ]; then
1036 if install_new $file; then
1043 # If the only change in the new file versus the old
1044 # file is a change in the FreeBSD ID string and -F is
1045 # specified, don't warn.
1046 if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \
1047 fbsdid_only $OLDTREE/$file $NEWTREE/$file; then
1053 old=`file_type $OLDTREE/$file`
1054 new=`file_type $NEWTREE/$file`
1055 warn "Remove mismatch: $file ($old became $new)"
1058 old=`readlink $OLDTREE/$file`
1059 new=`readlink $NEWTREE/$file`
1061 "Removed link changed: $file (\"$old\" became \"$new\")"
1064 warn "Removed file changed: $file"
1070 # Treat the file as unmodified and force install of the new
1071 # file if it matches an ALWAYS_INSTALL glob. If the update
1072 # attempt fails, then fall through to the normal case so a
1073 # warning is generated.
1074 if always_install $file; then
1075 log "ALWAYS: updating $file"
1076 if update_unmodified $file; then
1081 # If the only change in the new file versus the old file is a
1082 # change in the FreeBSD ID string and -F is specified, just
1083 # update the FreeBSD ID string in the local file.
1084 if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \
1085 fbsdid_only $OLDTREE/$file $NEWTREE/$file; then
1086 if update_freebsdid $file; then
1091 # If the file changed types between the old and new trees but
1092 # the files in the new and dest tree are both of the same
1093 # type, treat it like an added file just comparing the new and
1095 if [ $cmp -eq $COMPARE_DIFFTYPE ]; then
1098 new=`readlink $NEWTREE/$file`
1099 dest=`readlink $DESTDIR/$file`
1101 "New link conflict: $file (\"$new\" vs \"$dest\")"
1111 # If the file has not changed types between the old
1112 # and new trees, but it is a different type in
1113 # DESTDIR, then just warn.
1114 if [ $newdestcmp -eq $COMPARE_DIFFTYPE ]; then
1115 new=`file_type $NEWTREE/$file`
1116 dest=`file_type $DESTDIR/$file`
1117 warn "Modified mismatch: $file ($new vs $dest)"
1124 old=`file_type $OLDTREE/$file`
1125 new=`file_type $NEWTREE/$file`
1126 dest=`file_type $DESTDIR/$file`
1127 warn "Modified $dest changed: $file ($old became $new)"
1130 old=`readlink $OLDTREE/$file`
1131 new=`readlink $NEWTREE/$file`
1133 "Modified link changed: $file (\"$old\" became \"$new\")"
1141 # Handle a file that has been added in the new tree. If the file does
1142 # not exist in DESTDIR, simply copy the file into DESTDIR. If the
1143 # file exists in the DESTDIR and is identical to the new version, do
1144 # nothing. Otherwise, generate a diff of the two versions of the file
1145 # and mark it as a conflict.
1147 # $1 - pathname of the file (relative to DESTDIR)
1150 local cmp dest file new
1153 if ignore $file; then
1154 log "IGNORE: added file $file"
1158 compare $DESTDIR/$file $NEWTREE/$file
1165 panic "Added file now missing"
1167 $COMPARE_ONLYSECOND)
1168 # Ignore new directories. They will be
1169 # created as needed when non-directory nodes
1171 if ! [ -d $NEWTREE/$file ]; then
1172 if install_new $file; then
1181 # Treat the file as unmodified and force install of the new
1182 # file if it matches an ALWAYS_INSTALL glob. If the update
1183 # attempt fails, then fall through to the normal case so a
1184 # warning is generated.
1185 if always_install $file; then
1186 log "ALWAYS: updating $file"
1187 if update_unmodified $file; then
1194 new=`file_type $NEWTREE/$file`
1195 dest=`file_type $DESTDIR/$file`
1196 warn "New file mismatch: $file ($new vs $dest)"
1199 new=`readlink $NEWTREE/$file`
1200 dest=`readlink $DESTDIR/$file`
1201 warn "New link conflict: $file (\"$new\" vs \"$dest\")"
1204 # If the only change in the new file versus
1205 # the destination file is a change in the
1206 # FreeBSD ID string and -F is specified, just
1207 # install the new file.
1208 if [ -n "$FREEBSD_ID" ] && \
1209 fbsdid_only $NEWTREE/$file $DESTDIR/$file; then
1210 if update_unmodified $file; then
1214 "Updating FreeBSD ID string failed"
1224 # Main routines for each command
1226 # Build a new tree and save it in a tarball.
1231 if [ $# -ne 1 ]; then
1232 echo "Missing required tarball."
1237 log "build command: $1"
1239 # Create a temporary directory to hold the tree
1240 dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX`
1241 if [ $? -ne 0 ]; then
1242 echo "Unable to create temporary directory."
1245 if ! build_tree $dir; then
1246 echo "Failed to build tree."
1250 if ! tar cfj $1 -C $dir . >&3 2>&1; then
1251 echo "Failed to create tarball."
1258 # Output a diff comparing the tree at DESTDIR to the current
1259 # unmodified tree. Note that this diff does not include files that
1260 # are present in DESTDIR but not in the unmodified tree.
1265 if [ $# -ne 0 ]; then
1269 # Requires an unmodified tree to diff against.
1270 if ! [ -d $NEWTREE ]; then
1271 echo "Reference tree to diff against unavailable."
1275 # Unfortunately, diff alone does not quite provide the right
1276 # level of options that we want, so improvise.
1277 for file in `(cd $NEWTREE; find .) | sed -e 's/^\.//'`; do
1278 if ignore $file; then
1282 diffnode $NEWTREE "$DESTDIR" $file "stock" "local"
1286 # Just extract a new tree into NEWTREE either by building a tree or
1287 # extracting a tarball. This can be used to bootstrap updates by
1288 # initializing the current "stock" tree to match the currently
1291 # Unlike 'update', this command does not rotate or preserve an
1292 # existing NEWTREE, it just replaces any existing tree.
1296 if [ $# -ne 0 ]; then
1300 log "extract command: tarball=$tarball"
1302 if [ -d $NEWTREE ]; then
1303 if ! remove_tree $NEWTREE; then
1304 echo "Unable to remove current tree."
1312 # Resolve conflicts left from an earlier merge.
1317 if [ $# -ne 0 ]; then
1321 if ! [ -d $CONFLICTS ]; then
1325 if ! [ -d $NEWTREE ]; then
1326 echo "The current tree is not present to resolve conflicts."
1330 conflicts=`(cd $CONFLICTS; find . ! -type d) | sed -e 's/^\.//'`
1331 for file in $conflicts; do
1332 resolve_conflict $file
1335 if [ -n "$NEWALIAS_WARN" ]; then
1336 warn "Needs update: /etc/mail/aliases.db" \
1337 "(requires manual update via newaliases(1))"
1340 echo " Needs update: /etc/mail/aliases.db" \
1341 "(requires manual update via newaliases(1))"
1345 # Report a summary of the previous merge. Specifically, list any
1346 # remaining conflicts followed by any warnings from the previous
1351 if [ $# -ne 0 ]; then
1355 if [ -d $CONFLICTS ]; then
1356 (cd $CONFLICTS; find . ! -type d) | sed -e 's/^\./ C /'
1358 if [ -s $WARNINGS ]; then
1364 # Perform an actual merge. The new tree can either already exist (if
1365 # rerunning a merge), be extracted from a tarball, or generated from a
1371 if [ $# -ne 0 ]; then
1375 log "update command: rerun=$rerun tarball=$tarball preworld=$preworld"
1377 if [ `id -u` -ne 0 ]; then
1378 echo "Must be root to update a tree."
1382 # Enforce a sane umask
1385 # XXX: Should existing conflicts be ignored and removed during
1388 # Trim the conflicts tree. Whine if there is anything left.
1389 if [ -e $CONFLICTS ]; then
1390 find -d $CONFLICTS -type d -empty -delete >&3 2>&1
1391 rmdir $CONFLICTS >&3 2>&1
1393 if [ -d $CONFLICTS ]; then
1394 echo "Conflicts remain from previous update, aborting."
1398 if [ -z "$rerun" ]; then
1399 # For a dryrun that is not a rerun, do not rotate the existing
1400 # stock tree. Instead, extract a tree to a temporary directory
1401 # and use that for the comparison.
1402 if [ -n "$dryrun" ]; then
1403 dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX`
1404 if [ $? -ne 0 ]; then
1405 echo "Unable to create temporary directory."
1409 # A pre-world dryrun has already set OLDTREE to
1410 # point to the current stock tree.
1411 if [ -z "$preworld" ]; then
1416 # For a pre-world update, blow away any pre-existing
1418 elif [ -n "$preworld" ]; then
1419 if ! remove_tree $NEWTREE; then
1420 echo "Unable to remove pre-world tree."
1424 # Rotate the existing stock tree to the old tree.
1425 elif [ -d $NEWTREE ]; then
1426 # First, delete the previous old tree if it exists.
1427 if ! remove_tree $OLDTREE; then
1428 echo "Unable to remove old tree."
1432 # Move the current stock tree.
1433 if ! mv $NEWTREE $OLDTREE >&3 2>&1; then
1434 echo "Unable to rename current stock tree."
1439 if ! [ -d $OLDTREE ]; then
1441 No previous tree to compare against, a sane comparison is not possible.
1443 log "No previous tree to compare against."
1444 if [ -n "$dir" ]; then
1450 # Populate the new tree.
1454 # Build lists of nodes in the old and new trees.
1455 (cd $OLDTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/old.files
1456 (cd $NEWTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/new.files
1458 # Split the files up into three groups using comm.
1459 comm -23 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/removed.files
1460 comm -13 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/added.files
1461 comm -12 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/both.files
1463 # Initialize conflicts and warnings handling.
1467 # Ignore removed files for the pre-world case. A pre-world
1468 # update uses a stripped-down tree.
1469 if [ -n "$preworld" ]; then
1470 > $WORKDIR/removed.files
1473 # The order for the following sections is important. In the
1474 # odd case that a directory is converted into a file, the
1475 # existing subfiles need to be removed if possible before the
1476 # file is converted. Similarly, in the case that a file is
1477 # converted into a directory, the file needs to be converted
1478 # into a directory if possible before the new files are added.
1480 # First, handle removed files.
1481 for file in `cat $WORKDIR/removed.files`; do
1482 handle_removed_file $file
1485 # For the directory pass, reverse sort the list to effect a
1486 # depth-first traversal. This is needed to ensure that if a
1487 # directory with subdirectories is removed, the entire
1488 # directory is removed if there are no local modifications.
1489 for file in `sort -r $WORKDIR/removed.files`; do
1490 handle_removed_directory $file
1493 # Second, handle files that exist in both the old and new
1495 for file in `cat $WORKDIR/both.files`; do
1496 handle_modified_file $file
1499 # Finally, handle newly added files.
1500 for file in `cat $WORKDIR/added.files`; do
1501 handle_added_file $file
1504 if [ -n "$NEWALIAS_WARN" ]; then
1505 warn "Needs update: /etc/mail/aliases.db" \
1506 "(requires manual update via newaliases(1))"
1509 if [ -s $WARNINGS ]; then
1514 if [ -n "$dir" ]; then
1515 if [ -z "$dryrun" -o -n "$rerun" ]; then
1516 panic "Should not have a temporary directory"
1523 # Determine which command we are executing. A command may be
1524 # specified as the first word. If one is not specified then 'update'
1525 # is assumed as the default command.
1527 if [ $# -gt 0 ]; then
1529 build|diff|extract|status|resolve)
1534 # If first arg is an option, assume the
1543 # Set default variable values.
1545 # The path to the source tree used to build trees.
1548 # The destination directory where the modified files live.
1551 # Ignore changes in the FreeBSD ID string.
1554 # Files that should always have the new version of the file installed.
1557 # Files to ignore and never update during a merge.
1560 # Flags to pass to 'make' when building a tree.
1563 # Include a config file if it exists. Note that command line options
1564 # override any settings in the config file. More details are in the
1565 # manual, but in general the following variables can be set:
1575 if [ -r /etc/etcupdate.conf ]; then
1576 . /etc/etcupdate.conf
1579 # Parse command line options
1587 while getopts "d:nprs:t:A:BD:FI:L:M:" option; do
1608 # To allow this option to be specified
1609 # multiple times, accumulate command-line
1610 # specified patterns in an 'always' variable
1611 # and use that to overwrite ALWAYS_INSTALL
1612 # after parsing all options. Need to be
1613 # careful here with globbing expansion.
1615 always="$always $OPTARG"
1628 # To allow this option to be specified
1629 # multiple times, accumulate command-line
1630 # specified patterns in an 'ignore' variable
1631 # and use that to overwrite IGNORE_FILES after
1632 # parsing all options. Need to be careful
1633 # here with globbing expansion.
1635 ignore="$ignore $OPTARG"
1642 MAKE_OPTIONS="$OPTARG"
1650 shift $((OPTIND - 1))
1652 # Allow -A command line options to override ALWAYS_INSTALL set from
1655 if [ -n "$always" ]; then
1656 ALWAYS_INSTALL="$always"
1659 # Allow -I command line options to override IGNORE_FILES set from the
1661 if [ -n "$ignore" ]; then
1662 IGNORE_FILES="$ignore"
1666 # Where the "old" and "new" trees are stored.
1667 WORKDIR=${WORKDIR:-$DESTDIR/var/db/etcupdate}
1669 # Log file for verbose output from program that are run. The log file
1670 # is opened on fd '3'.
1671 LOGFILE=${LOGFILE:-$WORKDIR/log}
1673 # The path of the "old" tree
1674 OLDTREE=$WORKDIR/old
1676 # The path of the "new" tree
1677 NEWTREE=$WORKDIR/current
1679 # The path of the "conflicts" tree where files with merge conflicts are saved.
1680 CONFLICTS=$WORKDIR/conflicts
1682 # The path of the "warnings" file that accumulates warning notes from an update.
1683 WARNINGS=$WORKDIR/warnings
1685 # Use $EDITOR for resolving conflicts. If it is not set, default to vi.
1686 EDITOR=${EDITOR:-/usr/bin/vi}
1688 # Files that need to be updated before installworld.
1689 PREWORLD_FILES="etc/master.passwd etc/group"
1691 # Handle command-specific argument processing such as complaining
1692 # about unsupported options. Since the configuration file is always
1693 # included, do not complain about extra command line arguments that
1694 # may have been set via the config file rather than the command line.
1697 if [ -n "$rerun" -a -n "$tarball" ]; then
1698 echo "Only one of -r or -t can be specified."
1702 if [ -n "$rerun" -a -n "$preworld" ]; then
1703 echo "Only one of -p or -r can be specified."
1709 if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" -o \
1710 -n "$preworld" ]; then
1715 if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" ]; then
1720 if [ -n "$dryrun" -o -n "$rerun" -o -n "$preworld" ]; then
1726 # Pre-world mode uses a different set of trees. It leaves the current
1727 # tree as-is so it is still present for a full etcupdate run after the
1728 # world install is complete. Instead, it installs a few critical files
1729 # into a separate tree.
1730 if [ -n "$preworld" ]; then
1732 NEWTREE=$WORKDIR/preworld
1735 # Open the log file. Don't truncate it if doing a minor operation so
1736 # that a minor operation doesn't lose log info from a major operation.
1737 if ! mkdir -p $WORKDIR 2>/dev/null; then
1738 echo "Failed to create work directory $WORKDIR"
1742 diff|resolve|status)