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