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