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