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