3 # SPDX-License-Identifier: BSD-2-Clause
5 # Copyright (c) 2010-2013 Hudson River Trading LLC
6 # Written by: John H. Baldwin <jhb@FreeBSD.org>
9 # Redistribution and use in source and binary forms, with or without
10 # modification, are permitted provided that the following conditions
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.
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
31 # This is a tool to manage updating files that are not updated as part
32 # of 'make installworld' such as files in /etc. Unlike other tools,
33 # this one is specifically tailored to assisting with mass upgrades.
34 # To that end it does not require user intervention while running.
36 # Theory of operation:
38 # The most reliable way to update changes to files that have local
39 # modifications is to perform a three-way merge between the original
40 # unmodified file, the new version of the file, and the modified file.
41 # This requires having all three versions of the file available when
42 # performing an update.
44 # To that end, etcupdate uses a strategy where the current unmodified
45 # tree is kept in WORKDIR/current and the previous unmodified tree is
46 # kept in WORKDIR/old. When performing a merge, a new tree is built
47 # if needed and then the changes are merged into DESTDIR. Any files
48 # with unresolved conflicts after the merge are left in a tree rooted
49 # at WORKDIR/conflicts.
51 # To provide extra flexibility, etcupdate can also build tarballs of
52 # root trees that can later be used. It can also use a tarball as the
53 # source of a new tree instead of building it from /usr/src.
55 # Global settings. These can be adjusted by config files and in some
56 # cases by command line options.
59 # - automatable conflict resolution
64 usage: etcupdate [-npBFN] [-d workdir] [-r | -s source | -t tarball]
65 [-A patterns] [-D destdir] [-I patterns] [-L logfile]
66 [-M options] [-m make]
67 etcupdate build [-BN] [-d workdir] [-s source] [-L logfile] [-M options]
69 etcupdate diff [-d workdir] [-D destdir] [-I patterns] [-L logfile]
70 etcupdate extract [-BN] [-d workdir] [-s source | -t tarball]
71 [-D destdir] [-L logfile] [-M options] [-m make]
72 etcupdate resolve [-p] [-d workdir] [-D destdir] [-L logfile]
73 etcupdate revert [-d workdir] [-D destdir] [-L logfile] file ...
74 etcupdate status [-d workdir] [-D destdir]
79 # Used to write a message prepended with '>>>' to the logfile.
85 # Used for assertion conditions that should never happen.
92 # Used to write a warning message. These are saved to the WARNINGS
93 # file with " " prepended.
96 echo -n " " >> $WARNINGS
97 echo "$@" >> $WARNINGS
100 # Output a horizontal rule using the passed-in character. Matches the
101 # length used for Index lines in CVS and SVN diffs.
109 # Output a text description of a specified file's type.
111 # $1 - file pathname.
114 stat -f "%HT" $1 | tr "[:upper:]" "[:lower:]"
117 # Returns true (0) if a file exists
119 # $1 - file pathname.
125 # Returns true (0) if a file should be ignored, false otherwise.
133 for pattern in $IGNORE_FILES; do
143 # Ignore /.cshrc and /.profile if they are hardlinked to the
144 # same file in /root. This ensures we only compare those
145 # files once in that case.
148 if [ ${DESTDIR}$1 -ef ${DESTDIR}/root$1 ]; then
159 # Returns true (0) if the new version of a file should always be
160 # installed rather than attempting to do a merge.
168 for pattern in $ALWAYS_INSTALL; do
181 # Build a new tree. This runs inside a subshell to trap SIGINT.
183 # $1 - directory to store new tree in
186 local destdir dir file make autogenfiles metatmp
188 make="$MAKE_CMD $MAKE_OPTIONS -DNO_FILEMON"
190 if [ -n "$noroot" ]; then
191 make="$make -DNO_ROOT"
192 metatmp=`mktemp $WORKDIR/etcupdate-XXXXXXX`
194 trap "rm -f $metatmp; trap '' EXIT; return 1" INT
195 trap "rm -f $metatmp" EXIT
201 log "Building tree at $1 with $make"
206 destdir=`realpath $1`
208 if [ -n "$preworld" ]; then
209 # Build a limited tree that only contains files that are
210 # crucial to installworld.
211 for file in $PREWORLD_FILES; do
212 name=$(basename $file)
213 mkdir -p $1/etc || return 1
214 cp -p $SRCDIR/$file $1/etc/$name || return 1
219 if ! [ -n "$nobuild" ]; then
220 export MAKEOBJDIRPREFIX=$destdir/usr/obj
221 if [ -n "$($make -V.ALLTARGETS:Mbuildetc)" ]; then
222 $make buildetc || exit 1
224 $make _obj SUBDIR_OVERRIDE=etc || exit 1
225 $make everything SUBDIR_OVERRIDE=etc || exit 1
228 if [ -n "$($make -V.ALLTARGETS:Minstalletc)" ]; then
229 $make DESTDIR=$destdir installetc || exit 1
231 $make DESTDIR=$destdir distrib-dirs || exit 1
232 $make DESTDIR=$destdir distribution || exit 1
236 chflags -R noschg $1 || return 1
237 rm -rf $1/usr/obj || return 1
239 # Purge auto-generated files. Only the source files need to
240 # be updated after which these files are regenerated.
241 autogenfiles="./etc/*.db ./etc/passwd ./var/db/services.db"
242 (cd $1 && printf '%s\n' $autogenfiles >> $metatmp && \
243 rm -f $autogenfiles) || return 1
245 # Remove empty files. These just clutter the output of 'diff'.
246 (cd $1 && find . -type f -size 0 -delete -print >> $metatmp) || \
249 # Trim empty directories.
250 (cd $1 && find . -depth -type d -empty -delete -print >> $metatmp) || \
253 if [ -n "$noroot" ]; then
254 # Rewrite the METALOG to exclude the files (and directories)
255 # removed above. $metatmp contains the list of files to delete,
256 # and we append #METALOG# as a delimiter followed by the
257 # original METALOG. This lets us scan through $metatmp in awk
258 # building up a table of names to delete until we reach the
259 # delimiter, then emit all the entries of the original METALOG
260 # after it that aren't in that table. We also exclude ./usr/obj
261 # and its children explicitly for simplicity rather than
262 # building up that list (and in practice only ./usr/obj itself
263 # will be in the METALOG since nothing is installed there).
264 echo '#METALOG#' >> $metatmp || return 1
265 cat $1/METALOG >> $metatmp || return 1
266 awk '/^#METALOG#$/ { metalog = 1; next }
267 { f=$1; gsub(/\/\/+/, "/", f) }
268 !metalog { rm[f] = 1; next }
269 !rm[f] && f !~ /^\.\/usr\/obj(\/|$)/ { print }' \
270 $metatmp > $1/METALOG || return 1
276 # Generate a new tree. If tarball is set, then the tree is
277 # extracted from the tarball. Otherwise the tree is built from a
280 # $1 - directory to store new tree in
285 # If we have a tarball, extract that into the new directory.
286 if [ -n "$tarball" ]; then
288 if [ -n "$preworld" ]; then
289 files="$PREWORLD_FILES"
291 if ! (mkdir -p $1 && tar xf $tarball -C $1 $files) \
293 echo "Failed to extract new tree."
298 if ! build_tree $1; then
299 echo "Failed to build new tree."
306 # Forcefully remove a tree. Returns true (0) if the operation succeeds.
314 chflags -R noschg $1 >&3 2>&1
320 # Return values for compare()
328 # Compare two files/directories/symlinks. Note that this does not
329 # recurse into subdirectories. Instead, if two nodes are both
330 # directories, they are assumed to be equivalent.
332 # Returns true (0) if the nodes are identical. If only one of the two
333 # nodes are present, return one of the COMPARE_ONLY* constants. If
334 # the nodes are different, return one of the COMPARE_DIFF* constants
335 # to indicate the type of difference.
343 # If the first node doesn't exist, then check for the second
344 # node. Note that -e will fail for a symbolic link that
345 # points to a missing target.
348 return $COMPARE_ONLYSECOND
350 return $COMPARE_EQUAL
352 elif ! exists $2; then
353 return $COMPARE_ONLYFIRST
356 # If the two nodes are different file types fail.
357 first=`stat -f "%Hp" $1`
358 second=`stat -f "%Hp" $2`
359 if [ "$first" != "$second" ]; then
360 return $COMPARE_DIFFTYPE
363 # If both are symlinks, compare the link values.
367 if [ "$first" = "$second" ]; then
368 return $COMPARE_EQUAL
370 return $COMPARE_DIFFLINKS
374 # If both are files, compare the file contents.
376 if cmp -s $1 $2; then
377 return $COMPARE_EQUAL
379 return $COMPARE_DIFFFILES
383 # As long as the two nodes are the same type of file, consider
385 return $COMPARE_EQUAL
388 # Returns true (0) if the only difference between two regular files is a
389 # change in the FreeBSD ID string.
391 # $1 - path of first file
392 # $2 - path of second file
396 diff -qI '\$FreeBSD.*\$' $1 $2 >/dev/null 2>&1
399 # This is a wrapper around compare that will return COMPARE_EQUAL if
400 # the only difference between two regular files is a change in the
401 # FreeBSD ID string. It only makes this adjustment if the -F flag has
413 if [ -n "$FREEBSD_ID" -a "$cmp" -eq $COMPARE_DIFFFILES ] && \
414 fbsdid_only $1 $2; then
415 return $COMPARE_EQUAL
421 # Returns true (0) if a directory is empty.
423 # $1 - pathname of the directory to check
432 # Returns true (0) if one directories contents are a subset of the
433 # other. This will recurse to handle subdirectories and compares
434 # individual files in the trees. Its purpose is to quiet spurious
435 # directory warnings for dryrun invocations.
437 # $1 - first directory (sub)
438 # $2 - second directory (super)
443 if ! [ -d $1 -a -d $2 ]; then
447 # Ignore files that are present in the second directory but not
450 for file in $contents; do
451 if ! compare $1/$file $2/$file; then
455 if [ -d $1/$file ]; then
456 if ! dir_subset $1/$file $2/$file; then
464 # Returns true (0) if a directory in the destination tree is empty.
465 # If this is a dryrun, then this returns true as long as the contents
466 # of the directory are a subset of the contents in the old tree
467 # (meaning that the directory would be empty in a non-dryrun when this
468 # was invoked) to quiet spurious warnings.
470 # $1 - pathname of the directory to check relative to DESTDIR.
474 if [ -n "$dryrun" ]; then
475 dir_subset $DESTDIR/$1 $OLDTREE/$1
479 empty_dir $DESTDIR/$1
482 # Output a diff of two directory entries with the same relative name
483 # in different trees. Note that as with compare(), this does not
484 # recurse into subdirectories. If the nodes are identical, nothing is
490 # $4 - label for first tree
491 # $5 - label for second tree
494 local first second file old new diffargs
496 if [ -n "$FREEBSD_ID" ]; then
497 diffargs="-I \\\$FreeBSD.*\\\$"
502 compare_fbsdid $1/$3 $2/$3
517 first=`file_type $1/$3`
518 second=`file_type $2/$3`
520 echo "Node changed from a $first to a $second: $3"
524 first=`readlink $1/$file`
525 second=`readlink $2/$file`
527 echo "Link changed: $file"
536 diff -u $diffargs -L "$3 ($4)" $1/$3 -L "$3 ($5)" $2/$3
541 # Run one-off commands after an update has completed. These commands
542 # are not tied to a specific file, so they cannot be handled by
543 # post_install_file().
548 # None of these commands should be run for a pre-world update.
549 if [ -n "$preworld" ]; then
553 # If /etc/localtime exists and is not a symlink and /var/db/zoneinfo
554 # exists, run tzsetup -r to refresh /etc/localtime.
555 if [ -f ${DESTDIR}/etc/localtime -a \
556 ! -L ${DESTDIR}/etc/localtime ]; then
557 if [ -f ${DESTDIR}/var/db/zoneinfo ]; then
558 if [ -n "${DESTDIR}" ]; then
563 log "tzsetup -r ${args}"
564 if [ -z "$dryrun" ]; then
565 tzsetup -r ${args} >&3 2>&1
568 warn "Needs update: /etc/localtime (required" \
569 "manual update via tzsetup(8))"
574 # Create missing parent directories of a node in a target tree
575 # preserving the owner, group, and permissions from a specified
580 # $3 - pathname of the node (relative to both trees)
587 # Nothing to do if the parent directory exists. This also
588 # catches the degenerate cases when the path is just a simple
590 if [ -d ${2}$dir ]; then
594 # If non-directory file exists with the desired directory
596 if exists ${2}$dir; then
597 # If this is a dryrun and we are installing the
598 # directory in the DESTDIR and the file in the DESTDIR
599 # matches the file in the old tree, then fake success
600 # to quiet spurious warnings.
601 if [ -n "$dryrun" -a "$2" = "$DESTDIR" ]; then
602 if compare $OLDTREE/$dir $DESTDIR/$dir; then
607 args=`file_type ${2}$dir`
608 warn "Directory mismatch: ${2}$dir ($args)"
612 # Ensure the parent directory of the directory is present
614 if ! install_dirs $1 "$2" $dir; then
618 # Format attributes from template directory as install(1)
620 args=`stat -f "-o %Su -g %Sg -m %0Mp%0Lp" $1/$dir`
622 log "install -d $args ${2}$dir"
623 if [ -z "$dryrun" ]; then
624 install -d $args ${2}$dir >&3 2>&1
629 # Perform post-install fixups for a file. This largely consists of
630 # regenerating any files that depend on the newly installed file.
632 # $1 - pathname of the updated file (relative to DESTDIR)
637 # Grr, newaliases only works for an empty DESTDIR.
638 if [ -z "$DESTDIR" ]; then
640 if [ -z "$dryrun" ]; then
647 /usr/share/certs/trusted/* | /usr/share/certs/untrusted/*)
649 if [ -z "$dryrun" ]; then
650 env DESTDIR=${DESTDIR} certctl rehash >&3 2>&1
654 log "cap_mkdb ${DESTDIR}$1"
655 if [ -z "$dryrun" ]; then
656 cap_mkdb ${DESTDIR}$1 >&3 2>&1
660 log "pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1"
661 if [ -z "$dryrun" ]; then
662 pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1 \
667 # /etc/rc.d/motd hardcodes the /etc/motd path.
668 # Don't warn about non-empty DESTDIR's since this
669 # change is only cosmetic anyway.
670 if [ -z "$DESTDIR" ]; then
671 log "sh /etc/rc.d/motd start"
672 if [ -z "$dryrun" ]; then
673 sh /etc/rc.d/motd start >&3 2>&1
678 log "services_mkdb -q -o $DESTDIR/var/db/services.db" \
680 if [ -z "$dryrun" ]; then
681 services_mkdb -q -o $DESTDIR/var/db/services.db \
682 ${DESTDIR}$1 >&3 2>&1
688 # Install the "new" version of a file. Returns true if it succeeds
689 # and false otherwise.
691 # $1 - pathname of the file to install (relative to DESTDIR)
695 if ! install_dirs $NEWTREE "$DESTDIR" $1; then
698 log "cp -Rp ${NEWTREE}$1 ${DESTDIR}$1"
699 if [ -z "$dryrun" ]; then
700 cp -Rp ${NEWTREE}$1 ${DESTDIR}$1 >&3 2>&1
706 # Install the "resolved" version of a file. Returns true if it succeeds
707 # and false otherwise.
709 # $1 - pathname of the file to install (relative to DESTDIR)
713 # This should always be present since the file is already
714 # there (it caused a conflict). However, it doesn't hurt to
716 if ! install_dirs $NEWTREE "$DESTDIR" $1; then
720 # Use cat rather than cp to preserve metadata
721 log "cat ${CONFLICTS}$1 > ${DESTDIR}$1"
722 cat ${CONFLICTS}$1 > ${DESTDIR}$1 2>&3
727 # Generate a conflict file when a "new" file conflicts with an
728 # existing file in DESTDIR.
730 # $1 - pathname of the file that conflicts (relative to DESTDIR)
734 if [ -n "$dryrun" ]; then
738 install_dirs $NEWTREE $CONFLICTS $1
739 diff --changed-group-format='<<<<<<< (local)
742 ' $DESTDIR/$1 $NEWTREE/$1 > $CONFLICTS/$1
745 # Remove the "old" version of a file.
747 # $1 - pathname of the old file to remove (relative to DESTDIR)
750 log "rm -f ${DESTDIR}$1"
751 if [ -z "$dryrun" ]; then
752 rm -f ${DESTDIR}$1 >&3 2>&1
757 # Update a file that has no local modifications.
759 # $1 - pathname of the file to update (relative to DESTDIR)
764 # If the old file is a directory, then remove it with rmdir
765 # (this should only happen if the file has changed its type
766 # from a directory to a non-directory). If the directory
767 # isn't empty, then fail. This will be reported as a warning
769 if [ -d $DESTDIR/$1 ]; then
770 if empty_destdir $1; then
771 log "rmdir ${DESTDIR}$1"
772 if [ -z "$dryrun" ]; then
773 rmdir ${DESTDIR}$1 >&3 2>&1
779 # If both the old and new files are regular files, leave the
780 # existing file. This avoids breaking hard links for /.cshrc
781 # and /.profile. Otherwise, explicitly remove the old file.
782 elif ! [ -f ${DESTDIR}$1 -a -f ${NEWTREE}$1 ]; then
783 log "rm -f ${DESTDIR}$1"
784 if [ -z "$dryrun" ]; then
785 rm -f ${DESTDIR}$1 >&3 2>&1
789 # If the new file is a directory, note that the old file has
790 # been removed, but don't do anything else for now. The
791 # directory will be installed if needed when new files within
792 # that directory are installed.
793 if [ -d $NEWTREE/$1 ]; then
794 if empty_dir $NEWTREE/$1; then
799 elif install_new $1; then
805 # Update the FreeBSD ID string in a locally modified file to match the
806 # FreeBSD ID string from the "new" version of the file.
808 # $1 - pathname of the file to update (relative to DESTDIR)
813 # If the FreeBSD ID string is removed from the local file,
814 # there is nothing to do. In this case, treat the file as
815 # updated. Otherwise, if either file has more than one
816 # FreeBSD ID string, just punt and let the user handle the
818 new=`grep -c '\$FreeBSD.*\$' ${NEWTREE}$1`
819 dest=`grep -c '\$FreeBSD.*\$' ${DESTDIR}$1`
820 if [ "$dest" -eq 0 ]; then
823 if [ "$dest" -ne 1 -o "$dest" -ne 1 ]; then
827 # If the FreeBSD ID string in the new file matches the FreeBSD ID
828 # string in the local file, there is nothing to do.
829 new=`grep '\$FreeBSD.*\$' ${NEWTREE}$1`
830 dest=`grep '\$FreeBSD.*\$' ${DESTDIR}$1`
831 if [ "$new" = "$dest" ]; then
835 # Build the new file in three passes. First, copy all the
836 # lines preceding the FreeBSD ID string from the local version
837 # of the file. Second, append the FreeBSD ID string line from
838 # the new version. Finally, append all the lines after the
839 # FreeBSD ID string from the local version of the file.
840 file=`mktemp $WORKDIR/etcupdate-XXXXXXX`
841 awk '/\$FreeBSD.*\$/ { exit } { print }' ${DESTDIR}$1 >> $file
842 awk '/\$FreeBSD.*\$/ { print }' ${NEWTREE}$1 >> $file
843 awk '/\$FreeBSD.*\$/ { ok = 1; next } { if (ok) print }' \
844 ${DESTDIR}$1 >> $file
846 # As an extra sanity check, fail the attempt if the updated
847 # version of the file has any differences aside from the
849 if ! fbsdid_only ${DESTDIR}$1 $file; then
854 log "cp $file ${DESTDIR}$1"
855 if [ -z "$dryrun" ]; then
856 cp $file ${DESTDIR}$1 >&3 2>&1
864 # Attempt to update a file that has local modifications. This routine
865 # only handles regular files. If the 3-way merge succeeds without
866 # conflicts, the updated file is installed. If the merge fails, the
867 # merged version with conflict markers is left in the CONFLICTS tree.
869 # $1 - pathname of the file to merge (relative to DESTDIR)
874 # Try the merge to see if there is a conflict.
875 diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > /dev/null 2>&3
879 # No conflicts, so just redo the merge to the
881 log "diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1"
882 if [ -z "$dryrun" ]; then
883 temp=$(mktemp -t etcupdate)
884 diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > ${temp}
885 # Use "cat >" to preserve metadata.
886 cat ${temp} > ${DESTDIR}$1
893 # Conflicts, save a version with conflict markers in
894 # the conflicts directory.
895 if [ -z "$dryrun" ]; then
896 install_dirs $NEWTREE $CONFLICTS $1
897 log "diff3 -m ${DESTDIR}$1 ${CONFLICTS}$1"
898 diff3 -m -L "yours" -L "original" -L "new" \
899 ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > \
905 panic "merge failed with status $res"
910 # Returns true if a file contains conflict markers from a merge conflict.
912 # $1 - pathname of the file to resolve (relative to DESTDIR)
916 egrep -q '^(<{7}|\|{7}|={7}|>{7}) ' $CONFLICTS/$1
919 # Attempt to resolve a conflict. The user is prompted to choose an
920 # action for each conflict. If the user edits the file, they are
921 # prompted again for an action. The process is very similar to
922 # resolving conflicts after an update or merge with Perforce or
923 # Subversion. The prompts are modelled on a subset of the available
924 # commands for resolving conflicts with Subversion.
926 # $1 - pathname of the file to resolve (relative to DESTDIR)
931 echo "Resolving conflict in '$1':"
934 # Only display the resolved command if the file
935 # doesn't contain any conflicts.
936 echo -n "Select: (p) postpone, (df) diff-full, (e) edit,"
937 if ! has_conflicts $1; then
938 echo -n " (r) resolved,"
941 echo -n " (h) help for more options: "
945 diff -u ${DESTDIR}$1 ${CONFLICTS}$1
948 $EDITOR ${CONFLICTS}$1
952 (p) postpone - ignore this conflict for now
953 (df) diff-full - show all changes made to merged file
954 (e) edit - change merged file in an editor
955 (r) resolved - accept merged version of file
956 (mf) mine-full - accept local version of entire file (ignore new changes)
957 (tf) theirs-full - accept new version of entire file (lose local changes)
958 (h) help - show this list
962 # For mine-full, just delete the
963 # merged file and leave the local
964 # version of the file as-is.
972 # If the merged file has conflict
973 # markers, require confirmation.
974 if has_conflicts $1; then
975 echo "File '$1' still has conflicts," \
976 "are you sure? (y/n) "
978 if [ "$junk" != "y" ]; then
983 if ! install_resolved $1; then
984 panic "Unable to install merged" \
991 # For theirs-full, install the new
992 # version of the file over top of the
994 if ! install_new $1; then
995 panic "Unable to install new" \
1002 echo "Invalid command."
1008 # Handle a file that has been removed from the new tree. If the file
1009 # does not exist in DESTDIR, then there is nothing to do. If the file
1010 # exists in DESTDIR and is identical to the old version, remove it
1011 # from DESTDIR. Otherwise, whine about the conflict but leave the
1012 # file in DESTDIR. To handle directories, this uses two passes. The
1013 # first pass handles all non-directory files. The second pass handles
1014 # just directories and removes them if they are empty.
1016 # If -F is specified, and the only difference in the file in DESTDIR
1017 # is a change in the FreeBSD ID string, then remove the file.
1019 # $1 - pathname of the file (relative to DESTDIR)
1020 handle_removed_file()
1025 if ignore $file; then
1026 log "IGNORE: removed file $file"
1030 compare_fbsdid $DESTDIR/$file $OLDTREE/$file
1033 if ! [ -d $DESTDIR/$file ]; then
1038 panic "Removed file now missing"
1040 $COMPARE_ONLYSECOND)
1041 # Already removed, nothing to do.
1043 $COMPARE_DIFFTYPE|$COMPARE_DIFFLINKS|$COMPARE_DIFFFILES)
1044 dest=`file_type $DESTDIR/$file`
1045 warn "Modified $dest remains: $file"
1050 # Handle a directory that has been removed from the new tree. Only
1051 # remove the directory if it is empty.
1053 # $1 - pathname of the directory (relative to DESTDIR)
1054 handle_removed_directory()
1059 if ignore $dir; then
1060 log "IGNORE: removed dir $dir"
1064 if [ -d $DESTDIR/$dir -a -d $OLDTREE/$dir ]; then
1065 if empty_destdir $dir; then
1066 log "rmdir ${DESTDIR}$dir"
1067 if [ -z "$dryrun" ]; then
1068 rmdir ${DESTDIR}$dir >/dev/null 2>&1
1072 warn "Non-empty directory remains: $dir"
1077 # Handle a file that exists in both the old and new trees. If the
1078 # file has not changed in the old and new trees, there is nothing to
1079 # do. If the file in the destination directory matches the new file,
1080 # there is nothing to do. If the file in the destination directory
1081 # matches the old file, then the new file should be installed.
1082 # Everything else becomes some sort of conflict with more detailed
1085 # $1 - pathname of the file (relative to DESTDIR)
1086 handle_modified_file()
1088 local cmp dest file new newdestcmp old
1091 if ignore $file; then
1092 log "IGNORE: modified file $file"
1096 compare $OLDTREE/$file $NEWTREE/$file
1098 if [ $cmp -eq $COMPARE_EQUAL ]; then
1102 if [ $cmp -eq $COMPARE_ONLYFIRST -o $cmp -eq $COMPARE_ONLYSECOND ]; then
1103 panic "Changed file now missing"
1106 compare $NEWTREE/$file $DESTDIR/$file
1108 if [ $newdestcmp -eq $COMPARE_EQUAL ]; then
1112 # If the only change in the new file versus the destination
1113 # file is a change in the FreeBSD ID string and -F is
1114 # specified, just install the new file.
1115 if [ -n "$FREEBSD_ID" -a $newdestcmp -eq $COMPARE_DIFFFILES ] && \
1116 fbsdid_only $NEWTREE/$file $DESTDIR/$file; then
1117 if update_unmodified $file; then
1120 panic "Updating FreeBSD ID string failed"
1124 # If the local file is the same as the old file, install the
1125 # new file. If -F is specified and the only local change is
1126 # in the FreeBSD ID string, then install the new file as well.
1127 if compare_fbsdid $OLDTREE/$file $DESTDIR/$file; then
1128 if update_unmodified $file; then
1133 # If the file was removed from the dest tree, just whine.
1134 if [ $newdestcmp -eq $COMPARE_ONLYFIRST ]; then
1135 # If the removed file matches an ALWAYS_INSTALL glob,
1136 # then just install the new version of the file.
1137 if always_install $file; then
1138 log "ALWAYS: adding $file"
1139 if ! [ -d $NEWTREE/$file ]; then
1140 if install_new $file; then
1147 # If the only change in the new file versus the old
1148 # file is a change in the FreeBSD ID string and -F is
1149 # specified, don't warn.
1150 if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \
1151 fbsdid_only $OLDTREE/$file $NEWTREE/$file; then
1157 old=`file_type $OLDTREE/$file`
1158 new=`file_type $NEWTREE/$file`
1159 warn "Remove mismatch: $file ($old became $new)"
1162 old=`readlink $OLDTREE/$file`
1163 new=`readlink $NEWTREE/$file`
1165 "Removed link changed: $file (\"$old\" became \"$new\")"
1168 warn "Removed file changed: $file"
1174 # Treat the file as unmodified and force install of the new
1175 # file if it matches an ALWAYS_INSTALL glob. If the update
1176 # attempt fails, then fall through to the normal case so a
1177 # warning is generated.
1178 if always_install $file; then
1179 log "ALWAYS: updating $file"
1180 if update_unmodified $file; then
1185 # If the only change in the new file versus the old file is a
1186 # change in the FreeBSD ID string and -F is specified, just
1187 # update the FreeBSD ID string in the local file.
1188 if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \
1189 fbsdid_only $OLDTREE/$file $NEWTREE/$file; then
1190 if update_freebsdid $file; then
1195 # If the file changed types between the old and new trees but
1196 # the files in the new and dest tree are both of the same
1197 # type, treat it like an added file just comparing the new and
1199 if [ $cmp -eq $COMPARE_DIFFTYPE ]; then
1202 new=`readlink $NEWTREE/$file`
1203 dest=`readlink $DESTDIR/$file`
1205 "New link conflict: $file (\"$new\" vs \"$dest\")"
1215 # If the file has not changed types between the old
1216 # and new trees, but it is a different type in
1217 # DESTDIR, then just warn.
1218 if [ $newdestcmp -eq $COMPARE_DIFFTYPE ]; then
1219 new=`file_type $NEWTREE/$file`
1220 dest=`file_type $DESTDIR/$file`
1221 warn "Modified mismatch: $file ($new vs $dest)"
1228 old=`file_type $OLDTREE/$file`
1229 new=`file_type $NEWTREE/$file`
1230 dest=`file_type $DESTDIR/$file`
1231 warn "Modified $dest changed: $file ($old became $new)"
1234 old=`readlink $OLDTREE/$file`
1235 new=`readlink $NEWTREE/$file`
1237 "Modified link changed: $file (\"$old\" became \"$new\")"
1245 # Handle a file that has been added in the new tree. If the file does
1246 # not exist in DESTDIR, simply copy the file into DESTDIR. If the
1247 # file exists in the DESTDIR and is identical to the new version, do
1248 # nothing. Otherwise, generate a diff of the two versions of the file
1249 # and mark it as a conflict.
1251 # $1 - pathname of the file (relative to DESTDIR)
1254 local cmp dest file new
1257 if ignore $file; then
1258 log "IGNORE: added file $file"
1262 compare $DESTDIR/$file $NEWTREE/$file
1269 panic "Added file now missing"
1271 $COMPARE_ONLYSECOND)
1272 # Ignore new directories. They will be
1273 # created as needed when non-directory nodes
1275 if ! [ -d $NEWTREE/$file ]; then
1276 if install_new $file; then
1285 # Treat the file as unmodified and force install of the new
1286 # file if it matches an ALWAYS_INSTALL glob. If the update
1287 # attempt fails, then fall through to the normal case so a
1288 # warning is generated.
1289 if always_install $file; then
1290 log "ALWAYS: updating $file"
1291 if update_unmodified $file; then
1298 new=`file_type $NEWTREE/$file`
1299 dest=`file_type $DESTDIR/$file`
1300 warn "New file mismatch: $file ($new vs $dest)"
1303 new=`readlink $NEWTREE/$file`
1304 dest=`readlink $DESTDIR/$file`
1305 warn "New link conflict: $file (\"$new\" vs \"$dest\")"
1308 # If the only change in the new file versus
1309 # the destination file is a change in the
1310 # FreeBSD ID string and -F is specified, just
1311 # install the new file.
1312 if [ -n "$FREEBSD_ID" ] && \
1313 fbsdid_only $NEWTREE/$file $DESTDIR/$file; then
1314 if update_unmodified $file; then
1318 "Updating FreeBSD ID string failed"
1328 # Main routines for each command
1330 # Build a new tree and save it in a tarball.
1335 if [ $# -ne 1 ]; then
1336 echo "Missing required tarball."
1341 log "build command: $1"
1343 # Create a temporary directory to hold the tree
1344 dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX`
1345 if [ $? -ne 0 ]; then
1346 echo "Unable to create temporary directory."
1349 if ! build_tree $dir; then
1350 echo "Failed to build tree."
1354 if [ -n "$noroot" ]; then
1359 if ! tar cfj $1 -C $dir $tartree >&3 2>&1; then
1360 echo "Failed to create tarball."
1367 # Output a diff comparing the tree at DESTDIR to the current
1368 # unmodified tree. Note that this diff does not include files that
1369 # are present in DESTDIR but not in the unmodified tree.
1374 if [ $# -ne 0 ]; then
1378 # Requires an unmodified tree to diff against.
1379 if ! [ -d $NEWTREE ]; then
1380 echo "Reference tree to diff against unavailable."
1384 # Unfortunately, diff alone does not quite provide the right
1385 # level of options that we want, so improvise.
1386 for file in `(cd $NEWTREE; find .) | sed -e 's/^\.//'`; do
1387 if ignore $file; then
1391 diffnode $NEWTREE "$DESTDIR" $file "stock" "local"
1395 # Just extract a new tree into NEWTREE either by building a tree or
1396 # extracting a tarball. This can be used to bootstrap updates by
1397 # initializing the current "stock" tree to match the currently
1400 # Unlike 'update', this command does not rotate or preserve an
1401 # existing NEWTREE, it just replaces any existing tree.
1405 if [ $# -ne 0 ]; then
1409 log "extract command: tarball=$tarball"
1411 # Create a temporary directory to hold the tree
1412 dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX`
1413 if [ $? -ne 0 ]; then
1414 echo "Unable to create temporary directory."
1420 if [ -d $NEWTREE ]; then
1421 if ! remove_tree $NEWTREE; then
1422 echo "Unable to remove current tree."
1428 if ! mv $dir $NEWTREE >&3 2>&1; then
1429 echo "Unable to rename temp tree to current tree."
1435 # Resolve conflicts left from an earlier merge.
1440 if [ $# -ne 0 ]; then
1444 if ! [ -d $CONFLICTS ]; then
1448 if ! [ -d $NEWTREE ]; then
1449 echo "The current tree is not present to resolve conflicts."
1453 conflicts=`(cd $CONFLICTS; find . ! -type d) | sed -e 's/^\.//'`
1454 for file in $conflicts; do
1455 resolve_conflict $file
1458 if [ -n "$NEWALIAS_WARN" ]; then
1459 warn "Needs update: /etc/mail/aliases.db" \
1460 "(requires manual update via newaliases(1))"
1463 echo " Needs update: /etc/mail/aliases.db" \
1464 "(requires manual update via newaliases(1))"
1468 # Restore files to the stock version. Only files with a local change
1469 # are restored from the stock version.
1474 if [ $# -eq 0 ]; then
1481 if ! [ -e $NEWTREE/$file ]; then
1482 echo "File $file does not exist in the current tree."
1485 if [ -d $NEWTREE/$file ]; then
1486 echo "File $file is a directory."
1490 compare $DESTDIR/$file $NEWTREE/$file
1492 if [ $cmp -eq $COMPARE_EQUAL ]; then
1496 if update_unmodified $file; then
1497 # If this file had a conflict, clean up the
1499 if [ -e $CONFLICTS/$file ]; then
1500 if ! rm $CONFLICTS/$file >&3 2>&1; then
1501 echo "Failed to remove conflict " \
1509 # Report a summary of the previous merge. Specifically, list any
1510 # remaining conflicts followed by any warnings from the previous
1515 if [ $# -ne 0 ]; then
1519 if [ -d $CONFLICTS ]; then
1520 (cd $CONFLICTS; find . ! -type d) | sed -e 's/^\./ C /'
1522 if [ -s $WARNINGS ]; then
1528 # Perform an actual merge. The new tree can either already exist (if
1529 # rerunning a merge), be extracted from a tarball, or generated from a
1535 if [ $# -ne 0 ]; then
1539 log "update command: rerun=$rerun tarball=$tarball preworld=$preworld"
1541 if [ `id -u` -ne 0 ]; then
1542 echo "Must be root to update a tree."
1546 # Enforce a sane umask
1549 # XXX: Should existing conflicts be ignored and removed during
1552 # Trim the conflicts tree. Whine if there is anything left.
1553 if [ -e $CONFLICTS ]; then
1554 find -d $CONFLICTS -type d -empty -delete >&3 2>&1
1555 rmdir $CONFLICTS >&3 2>&1
1557 if [ -d $CONFLICTS ]; then
1558 echo "Conflicts remain from previous update, aborting."
1562 # Save tree names to use for rotation later.
1565 if [ -z "$rerun" ]; then
1566 # Extract the new tree to a temporary directory. The
1567 # trees are only rotated after a successful update to
1568 # avoid races if an update command is interrupted
1569 # before it completes.
1570 dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX`
1571 if [ $? -ne 0 ]; then
1572 echo "Unable to create temporary directory."
1576 # Populate the new tree.
1579 # Compare the new tree against the previous tree. For
1580 # the preworld case OLDTREE already points to the
1581 # current stock tree.
1582 if [ -z "$preworld" ]; then
1588 if ! [ -d $OLDTREE ]; then
1590 No previous tree to compare against, a sane comparison is not possible.
1592 log "No previous tree to compare against."
1593 if [ -n "$dir" ]; then
1594 if [ -n "$rerun" ]; then
1595 panic "Should not have a temporary directory"
1602 # Build lists of nodes in the old and new trees.
1603 (cd $OLDTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/old.files
1604 (cd $NEWTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/new.files
1606 # Split the files up into three groups using comm.
1607 comm -23 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/removed.files
1608 comm -13 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/added.files
1609 comm -12 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/both.files
1611 # Initialize conflicts and warnings handling.
1615 # Ignore removed files for the pre-world case. A pre-world
1616 # update uses a stripped-down tree.
1617 if [ -n "$preworld" ]; then
1618 > $WORKDIR/removed.files
1621 # The order for the following sections is important. In the
1622 # odd case that a directory is converted into a file, the
1623 # existing subfiles need to be removed if possible before the
1624 # file is converted. Similarly, in the case that a file is
1625 # converted into a directory, the file needs to be converted
1626 # into a directory if possible before the new files are added.
1628 # First, handle removed files.
1629 for file in `cat $WORKDIR/removed.files`; do
1630 handle_removed_file $file
1633 # For the directory pass, reverse sort the list to effect a
1634 # depth-first traversal. This is needed to ensure that if a
1635 # directory with subdirectories is removed, the entire
1636 # directory is removed if there are no local modifications.
1637 for file in `sort -r $WORKDIR/removed.files`; do
1638 handle_removed_directory $file
1641 # Second, handle files that exist in both the old and new
1643 for file in `cat $WORKDIR/both.files`; do
1644 handle_modified_file $file
1647 # Finally, handle newly added files.
1648 for file in `cat $WORKDIR/added.files`; do
1649 handle_added_file $file
1652 if [ -n "$NEWALIAS_WARN" ]; then
1653 warn "Needs update: /etc/mail/aliases.db" \
1654 "(requires manual update via newaliases(1))"
1657 # Run any special one-off commands after an update has completed.
1660 if [ -s $WARNINGS ]; then
1665 # If this was a dryrun, remove the temporary tree if we built
1667 if [ -n "$dryrun" ]; then
1668 if [ -n "$dir" ]; then
1669 if [ -n "$rerun" ]; then
1670 panic "Should not have a temporary directory"
1677 # Finally, rotate any needed trees.
1678 if [ "$new" != "$NEWTREE" ]; then
1679 if [ -n "$rerun" ]; then
1680 panic "Should not have a temporary directory"
1682 if [ -z "$dir" ]; then
1683 panic "Should have a temporary directory"
1686 # Rotate the old tree if needed
1687 if [ "$old" != "$OLDTREE" ]; then
1688 if [ -n "$preworld" ]; then
1689 panic "Old tree should be unchanged"
1692 if ! remove_tree $old; then
1693 echo "Unable to remove previous old tree."
1697 if ! mv $OLDTREE $old >&3 2>&1; then
1698 echo "Unable to rename old tree."
1703 # Rotate the new tree. Remove a previous pre-world
1704 # tree if it exists.
1705 if [ -d $new ]; then
1706 if [ -z "$preworld" ]; then
1707 panic "New tree should be rotated to old"
1709 if ! remove_tree $new; then
1710 echo "Unable to remove previous pre-world tree."
1715 if ! mv $NEWTREE $new >&3 2>&1; then
1716 echo "Unable to rename current tree."
1722 # Determine which command we are executing. A command may be
1723 # specified as the first word. If one is not specified then 'update'
1724 # is assumed as the default command.
1726 if [ $# -gt 0 ]; then
1728 build|diff|extract|status|resolve|revert)
1733 # If first arg is an option, assume the
1742 # Set default variable values.
1744 # The path to the source tree used to build trees.
1747 # The destination directory where the modified files live.
1750 # Ignore changes in the FreeBSD ID string.
1753 # Files that should always have the new version of the file installed.
1756 # Files to ignore and never update during a merge.
1759 # The path to the make binary
1762 # Flags to pass to 'make' when building a tree.
1765 # Include a config file if it exists. Note that command line options
1766 # override any settings in the config file. More details are in the
1767 # manual, but in general the following variables can be set:
1778 if [ -r /etc/etcupdate.conf ]; then
1779 . /etc/etcupdate.conf
1782 # Parse command line options
1791 while getopts "d:m:nprs:t:A:BD:FI:L:M:N" option; do
1815 # To allow this option to be specified
1816 # multiple times, accumulate command-line
1817 # specified patterns in an 'always' variable
1818 # and use that to overwrite ALWAYS_INSTALL
1819 # after parsing all options. Need to be
1820 # careful here with globbing expansion.
1822 always="$always $OPTARG"
1835 # To allow this option to be specified
1836 # multiple times, accumulate command-line
1837 # specified patterns in an 'ignore' variable
1838 # and use that to overwrite IGNORE_FILES after
1839 # parsing all options. Need to be careful
1840 # here with globbing expansion.
1842 ignore="$ignore $OPTARG"
1849 MAKE_OPTIONS="$OPTARG"
1860 shift $((OPTIND - 1))
1862 # Allow -A command line options to override ALWAYS_INSTALL set from
1865 if [ -n "$always" ]; then
1866 ALWAYS_INSTALL="$always"
1869 # Allow -I command line options to override IGNORE_FILES set from the
1871 if [ -n "$ignore" ]; then
1872 IGNORE_FILES="$ignore"
1876 # Where the "old" and "new" trees are stored.
1877 WORKDIR=${WORKDIR:-$DESTDIR/var/db/etcupdate}
1879 # Log file for verbose output from program that are run. The log file
1880 # is opened on fd '3'.
1881 LOGFILE=${LOGFILE:-$WORKDIR/log}
1883 # The path of the "old" tree
1884 OLDTREE=$WORKDIR/old
1886 # The path of the "new" tree
1887 NEWTREE=$WORKDIR/current
1889 # The path of the "conflicts" tree where files with merge conflicts are saved.
1890 CONFLICTS=$WORKDIR/conflicts
1892 # The path of the "warnings" file that accumulates warning notes from an update.
1893 WARNINGS=$WORKDIR/warnings
1895 # Use $EDITOR for resolving conflicts. If it is not set, default to vi.
1896 EDITOR=${EDITOR:-/usr/bin/vi}
1898 # Files that need to be updated before installworld.
1899 PREWORLD_FILES="etc/master.passwd etc/group"
1901 # Handle command-specific argument processing such as complaining
1902 # about unsupported options. Since the configuration file is always
1903 # included, do not complain about extra command line arguments that
1904 # may have been set via the config file rather than the command line.
1907 if [ -n "$rerun" -a -n "$tarball" ]; then
1908 echo "Only one of -r or -t can be specified."
1912 if [ -n "$rerun" -a -n "$preworld" ]; then
1913 echo "Only one of -p or -r can be specified."
1918 build|diff|status|revert)
1919 if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" -o \
1920 -n "$preworld" ]; then
1925 if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" ]; then
1930 if [ -n "$dryrun" -o -n "$rerun" -o -n "$preworld" ]; then
1936 # Pre-world mode uses a different set of trees. It leaves the current
1937 # tree as-is so it is still present for a full etcupdate run after the
1938 # world install is complete. Instead, it installs a few critical files
1939 # into a separate tree.
1940 if [ -n "$preworld" ]; then
1942 NEWTREE=$WORKDIR/preworld
1945 # Open the log file. Don't truncate it if doing a minor operation so
1946 # that a minor operation doesn't lose log info from a major operation.
1947 if ! mkdir -p $WORKDIR 2>/dev/null; then
1948 echo "Failed to create work directory $WORKDIR"
1952 diff|resolve|revert|status)