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