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