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