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