Port-amd64 archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

improved etcupdate (was: update 8.0_RC1->8.0)



EF> If you need to do this on a set of machines and get tired of answering 
EF> the same questions over and over again, I have a patch to add 
EF> record/replay support to etcupdate.
SB> Sounds useful.  Yes, I have eight machines running NetBSD, and
SB> updating to a new release can be a real chore.
I'll attach my version for NetBSD 8. Please note that I didn't actually run 
this version myself, only a former one I originally wrote for NetBSD 6.

The attached version also includes an updated fix for install/50838 (the whole 
NEED_XXX logic looks broken to me but the PR is untouched for five years) 
plus maybe some DESTDIR fixes (I implemented PREFIX/DESTDIR before NetBSD did 
and NetBSD and me overlooked different places where you need to add it).
It also adds a way to bootstrap into automatic mode and a switch to 
automatically add missing files/directories.

The idea is that you run -o file on one machine, answer the questions 
manually, transfer file to the other machines and run -i file there. 
If you have to merge, the logic only records the fact, not the details of 
the merge -- you need to re-enter them.
.\"	$NetBSD: etcupdate.8,v $
.\"
.\" Copyright (c) 2001-2008 The NetBSD Foundation, Inc.
.\" All rights reserved.
.\"
.\" This code is derived from software contributed to The NetBSD Foundation
.\" by Martti Kuparinen.
.\"
.\" Redistribution and use in source and binary forms, with or without
.\" modification, are permitted provided that the following conditions
.\" are met:
.\" 1. Redistributions of source code must retain the above copyright
.\"    notice, this list of conditions and the following disclaimer.
.\" 2. Redistributions in binary form must reproduce the above copyright
.\"    notice, this list of conditions and the following disclaimer in the
.\"    documentation and/or other materials provided with the distribution.
.\"
.\" THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
.\" ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
.\" TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
.\" PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
.\" BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
.\" CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
.\" SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
.\" INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
.\" CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
.\" ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
.\" POSSIBILITY OF SUCH DAMAGE.
.\"
.Dd June 12, 2014
.Dt ETCUPDATE 8
.Os
.Sh NAME
.Nm etcupdate
.Nd update the configuration and startup files in
.Pa /etc
.Sh SYNOPSIS
.Nm
.Op Fl abhlmv
.Op Fl d Ar destdir
.Op Fl p Ar pager
.Op Fl s Brq Ar srcdir | Ar tgzdir | Ar tgzfile
.Op Fl t Ar temproot
.Op Fl w Ar width
.Op Fl o Ar file
.Op Fl i Ar file
.Op Fl r Ar dir
.Sh DESCRIPTION
.Nm
is a tool that lets the administrator update the configuration and
startup files in
.Pa /etc
(and some other directories like
.Pa /dev ,
.Pa /root
and
.Pa /var )
without having to manually check and modify every file.
The administrator should run this script after performing an operating
system update (e.g. after running
.Pa make build
in
.Pa /usr/src
or after extracting new binary distribution files)
to update to the latest configuration and startup files.
.Pp
.Nm
compares the new configuration files against the currently installed files.
The user is given the option of installing, merging or deleting each
modified or missing file.
The user can also view the differences between the files.
By default, it shows the differences in the unified diff format.
The default format can be toggled to show the differences
in unified, context, or side by side formats or an user-defined
command may be used to view differences.
(And if
.Nm wdiff
is installed, it can also show differences on a word by word basis.)
.Pp
.Nm
also detects if the user installs certain special files and performs
corresponding tasks like remaking device nodes or rebuilding a database
from the
.Xr aliases 5
file.
Finally,
.Nm
runs
.Xr postinstall 8
to check the results.
.Pp
.Nm
needs a clean set of new configuration files to compare the
existing files against.
These files, called the
.Dq reference files
in this manual, may be derived from either a source or binary
distribution of
.Nx .
.Pp
If the user is updating from sources (which is the default mode),
.Nm
will first create a copy of the reference files
by running
.Pa make distribution
in
.Pa /usr/src/etc ,
installing the files to a so-called
.Pa temproot .
(See usage of the
.Fl s Ar srcdir
and
.Fl t Ar temproot
options later in this manual page.)
Although this is the default mode, it is not recommended
(see the
.Dq BUGS
section).
.Pp
Instead of using sources, it is recommended that the user should extract
one or more binary distribution sets in a special location and use those
as the reference files (see usage of the
.Fl s Ar tgzdir
option later in this manual page),
or specify one or more binary distribution sets directly
(see usage of the
.Fl s Ar tgzfile
option later in this manual page).
.Pp
The following options are available:
.Bl -tag -width XXtXtemprootX
.It Fl a
.Nm
can automatically update files which have not been modified locally.
The
.Fl a
flag instructs
.Nm
to store MD5 checksums in
.Pa /var/etcupdate
and use these checksums to determine if there have been any
local modifications.
.It Fl b
``bootstrap'' automatic
.Pf ( Fl a )
mode by initializing the checksums in
.Pa /var/etcupdate
from the reference files instead of updating anything.
Use this if you never ran in
.Fl a
mode before or lost your
.Pa /var/etcupdate
directory. 
Note that unless you just ran a normal
.Nm
before, in this mode, the
.Fl s
arguments must refer to the ``old'' configuration files, i.e.\&
those the currently installed ones are based on.
.It Fl m
Automatically create missing directories
and install missing files.
.It Fl d Ar destdir
Use
.Ar destdir
instead of
.Pa /
as the top of the file system hierarchy to be updated.
For example,
.Ar destdir Ns Pa /etc
will be used instead of
.Pa /etc .
.It Fl h
Prints a help text.
.It Fl i Ar file
Read (input) the per-file decisions to be taken drom
.Pa file .
The contents of
.Pa file
is typically the output from a
.Nm Fl o Ar file
run on another machine, but may also be crafted manually.
The format is one line per file, starting with a single character
#/d/i/m for comment, keep (``Don't install''), overwrite (``Install'') or merge,
whitspace, file name and optionally a per-file comment starting with whitespace.
Note that in case of a merge,
.Pa file
only gives the information that a file has to be merged,
the merge itself has still be to performed interactively.
.It Fl l
Automatically skip files with unchanged RCS IDs.
This has the effect of leaving alone files that have been altered
locally but which have not been changed in the
reference files.
Since this works using RCS IDs, files without RCS IDs will not be
skipped even if only modified locally.
This flag may be used together with the
.Fl a
flag described above.
.It Fl o Ar file
Write (output) the per-file decisions taken to 
.Pa file
in a format suitable to be read by the
.Fl i
option.
This can be used to record the decisions taken on a fully interactive run of
.Nm
and re-use that information on a subsequent invocation of
.Nm
on another machine.
Note that in case of a merge,
.Pa file
only records the fact that a merge was performed, not the details of the merge
operation.
.It Fl p Ar pager
The pager to use when displaying files.
By default this is
.Xr more 1
but it can be changed either with this option
or by defining the
.Ev PAGER
variable.
.It Fl r Ar dir
Install all files and directories under the
.Pa dir
directory. This is mainly useful for testing purposes.
Note that the
.Xr newaliases 1
command can't be run in this mode because it can't be given the name of the
configuration directory.
.It Fl s Brq Ar srcdir | Ar tgzdir | Ar tgzfile
The location of the reference files, or the
.Nx
source files used to create the reference files.
This may be specified in one of three ways:
.Bl -tag -width XXsXtgzfileXX
.It Fl s Ar srcdir
The top level directory of the
.Nx
source tree.
By default this is
.Pa /usr/src
but it can be changed either with this option
or the
.Ev SRCDIR
variable.
The reference files will be created by running
.Dq "make distribution"
in the
.Ar srcdir Ns Pa /etc
directory.
Note that
.Ar srcdir
should refer to the top of the source directory tree;
earlier versions of
.Nm
expected
.Ar srcdir
to refer to the
.Pa etc
subdirectory within the source tree.
.It Fl s Ar tgzdir
A directory in which reference files have been
extracted from a binary distribution of
.Nx .
The files that are distributed in the
.Dq Pa etc.tgz
set file must be present.
The files that are distributed in the
.Dq Pa xetc.tgz
set file are optional.
The reference files from the specified directory will be copied to the
.Pa temproot
directory.
.It Fl s Ar tgzfile
The location of a set file
(or
.Dq "tgz file" )
such as
.Dq Pa etc.tgz
or
.Dq Pa xetc.tgz
from a binary distribution of
.Nx .
Each set file is a compressed archive containing reference files,
which will be extracted to the
.Pa temproot
directory.
Multiple
.Fl s
options may be used to specify multiple set files.
The
.Dq Pa etc.tgz
set file must be specified.
The
.Dq Pa xetc.tgz
set file is optional.
.El
.It Fl t Ar temproot
Specifies the location of the
.Pa temproot
directory.
This directory will be used for a temporary copy of
the reference files created by running
.Dq "make distribution"
in the source directory specified by
.Fl s Ar srcdir ,
or a temporary copy of the reference files extracted from
the binary sets specified by
.Fl s Ar tgzfile ,
or a temporary copy of the reference files from the directory specified by
.Fl s Ar tempdir .
By default this is
.Pa /tmp/temproot
but can be changed either with this option or the
.Ev TEMPROOT
environment variable.
.It Fl v
Makes
.Nm
verbose about its actions.
.It Fl w Ar width
Sets screen width used during interactive merge.
By default this is the number of columns
.Xr stty 1
reports but it can be changed either with this
option or by defining the
.Ev WIDTH
variable.
This is useful for
.Xr xterm 1
users with wider shell windows.
.El
.Sh ENVIRONMENT
.Bl -tag -width IGNOREFILESXX
.It Ev TEMPROOT
Sets a default value for
.Pa temproot .
See
.Fl t
above.
.It Ev SRCDIR
The location of the
.Nx
sources files.
See
.Fl s
above.
.It Ev PAGER
The pager to use when displaying files.
See
.Fl p
above.
.It Ev WIDTH
The screen width used during interactive merge.
See
.Fl w
above.
.It Ev IGNOREFILES
A list of files that
.Nm
should ignore.
Files listed in this
variable will never be considered for updating by
.Nm .
.El
.Sh FILES
The environment variables can also be defined in the following configuration
files.
The user's personal configuration file settings override the global
settings.
.Pp
/etc/etcupdate.conf
.Pp
~~/.etcupdaterc
.Sh EXAMPLES
You have just upgraded your
.Nx
host from 3.0 to 4.0 and now it's time
to update the configuration files as well.
To update the configuration files from the sources (if you have the
.Pa /usr/src/etc
directory):
.Pp
.Dl etcupdate
.Pp
The default location of the source files is
.Pa /usr/src
but this may be overridden with the
.Fl s Ar srcdir
command line argument:
.Pp
.Dl etcupdate -s /some/where/src
.Pp
To update the configuration files from binary distribution sets
do something like this:
.Pp
.Dl etcupdate -s /some/where/etc.tgz -s /some/where/xetc.tgz
.Pp
or like this:
.Pp
.Dl mkdir /tmp/temproot
.Dl cd /tmp/temproot
.Dl tar -xpzf /some/where/etc.tgz
.Dl tar -xpzf /some/where/xetc.tgz
.Dl etcupdate -s /tmp/temproot
.Pp
You have modified only few files in the
.Pa /etc
directory so you would like install most of the updates without being asked.
To automatically update the unmodified configuration files:
.Pp
.Dl etcupdate -a
.Pp
To get a better idea what's going on, use the
.Fl v
flag:
.Pp
.Dl etcupdate -v
.Sh SEE ALSO
.Xr cmp 1 ,
.Xr more 1 ,
.Xr rcs 1 ,
.Xr sdiff 1 ,
.Xr stty 1 ,
.Xr aliases 5 ,
.Xr postinstall 8
.Sh HISTORY
The
.Nm
command appeared in
.Nx 1.6 .
.Pp
In
.Nx 4.0 ,
the
.Fl s Ar tgzfile
option was added, the
.Fl b Ar tempdir
option was converted to
.Fl s Ar tgzdir ,
and the
.Fl s Ar srcdir
option was changed to refer to the top of the
source directory tree rather than to the
.Pa etc
subdirectory.
.Pp
In
.Nx 5.0 ,
the ability to specify multiple colon-separated files with a single
.Fl s
option was deprecated,
and options deprecated in
.Nx 4.0
were removed.
.Pp
In
.Nx 7.0 ,
the ability to specify multiple colon-separated files with a single
.Fl s
option was removed (multiple
.Fl s
options must be used instead),
and the
.Fl d Ar destdir
option was added.
.Sh AUTHORS
The script was written by
.An Martti Kuparinen
.Aq martti%NetBSD.org@localhost
and improved by several other
.Nx
users.
.Pp
The idea for this script (including code fragments, variable names etc.)
came from the
.Fx
mergemaster (by Douglas Barton).
Unlike the
.Fx
mergemaster, this does not use CVS version tags by default to compare if
the files need to be updated.
Files are compared with
.Xr cmp 1
as this is more reliable and the only way if the version numbers are the
same even though the files are different.
.\" when exactly are the version the same even though the file changes?
.\" .Pp
.Sh BUGS
If a source directory is specified via the
.Dq Fl s Ar srcdir
option (or if the
.Pa /usr/src
directory is used by default), then
.Nm
will run
.Dq "make distribution"
in the
.Pa etc
subdirectory of the source directory, but it will not use the same
options or environment variables that would be used during a full build
of the operating system.
For this reason, use of the
.Dq Fl s Ar srcdir
option is not recommended, and use of the
.Dq Fl s Ar tgzdir
or
.Dq Fl s Ar tgzfile
options is recommended.
.\" .Pp
.\" Because of the use of
.\" .Xr cmp 1
.\" to compare files, rather than CVS versions, files that are locally changed
.\" from the distribution are always considered needing to be updated.
#!/bin/sh
#
# $NetBSD: etcupdate,v $
#
# Copyright (c) 2001-2008 The NetBSD Foundation, Inc.
# All rights reserved.
#
# This code is derived from software contributed to The NetBSD Foundation
# by Martti Kuparinen.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
#
# This script helps you to update the configuration files in /etc
# after an operating system upgrade. Instead of running "make distribution"
# in /usr/src/etc (and losing your current configuration) you can easily
# see the modifications and either install the new version or merge the
# changes in to your current configuration files.
#
# This script was written by Martti Kuparinen <martti%NetBSD.org@localhost> and
# improved by several other NetBSD users.
#
# The idea for this script (including code fragments, variable names etc.)
# came from the FreeBSD mergemaster (by Douglas Barton).
#
PATH="/sbin:/usr/sbin:/bin:/usr/bin:${PATH}"

# Default settings
PROG="${0##*/}"
DESTDIR="${DESTDIR:=}"	# must not have a trailing slash
DESTDIR_BRE=""		# basic regex to match ${DESTDIR}
TEMPROOT="${TEMPROOT:=/tmp/temproot}"
PAGER="${PAGER:=/usr/bin/more}"
SWIDTH="$(stty size | awk '{w=$2}END{if(w==0){w=80}print w}')"
WIDTH="${WIDTH:="${SWIDTH}"}"
DIFF_COMMAND="diff -u"
VERBOSE=false
CONTINUE=false
SOURCEMODE=false	# true for "-s source_dir"
SRCDIR=			# directory for SOURCEMODE
BINARYMODE=false	# true for both BINARYDIRMODE and BINARYTGZMODE
BINARYDIRMODE=false	# true for "-s extracted_dir"
BINARYDIR=		# directory name for BINARYDIRMODE
BINARYTGZMODE=false	# true for "-s etc.tgz"
TGZLIST=		# quoted list list of files for BINARYTGZMODE
SRC_ARGLIST=		# quoted list of "-s" args
N_SRC_ARGS=0		# number of "-s" args
AUTOMATIC=false
AUTO_MISSING=false
BOOT_AUTOMATIC=false
LOCALSKIP=false
MACHINE="${MACHINE:="$(uname -m)"}"
export MACHINE
MACHINE_ARCH="${MACHINE_ARCH:="$(uname -p)"}"
export MACHINE_ARCH
OUTFILE=		# file name argument to -o
INFILE=			# file name argument to -i
KEEP_FILES=" "		# files to keep (Don't install)
OVERWRITE_FILES=" "	# files to overwrite (Install)
MERGE_FILES=" "		# files to merge

usage() {
	cat << EOF >&2

Usage: ${PROG} [options]

Options:

  -p pager     Which pager to use              (default: /usr/bin/more)
  -s {srcdir|tgzfile|tempdir}                  (default: /usr/src)
               Location of the source files.  This may be any of the
               following:
               * A directory that contains a NetBSD source tree;
               * A distribution set file such as "etc.tgz" or
                 "xetc.tgz".  Pass multiple -s options to specify
                 multiple such files;
               * A temporary directory in which one or both of
                 "etc.tgz" and "xetc.tgz" have been extracted.
  -t temproot  Where to store temporary files  (default: /tmp/temproot)
  -d destdir   Destination prefix to check.    (default: empty)
  -w width     Screen width                    (default: 80)
  -a           Automatically update unmodified files
  -m           Automatically create/install missing directories/files
  -b           Bootstrap automatic mode: Initialize reference checksums
               from -s arguments
  -l           Automatically skip files with strictly local changes
               (this option has no effect on files lacking RCS Ids)
  -o file      Write (output) per-file decisions taken to file
  -i file      Read (input) per-file decisions to be taken from file
  -h           This help text
  -v           Be more verbose

EOF
	exit 1
}

verbose() {
	# $* = message to display if in verbose mode

	${VERBOSE} && echo "${@}"
}

yesno() {
	# $* = message to display

	echo -n "${@}? (y/[n]) "
	read ANSWER
	case "${ANSWER}" in
		y|Y)
			return 0
			;;
		*)
			return 1
			;;
	esac
}

# Quote args to make them safe in the shell.
# Usage: quotedlist="$(shell_quote args...)"
#
# After building up a quoted list, use it by evaling it inside
# double quotes, like this:
#    eval "set -- $quotedlist"
# or like this:
#    eval "\$command $quotedlist \$filename"
#
shell_quote()
{(
	local result=''
	local arg qarg
	LC_COLLATE=C ; export LC_COLLATE # so [a-zA-Z0-9] works in ASCII
	for arg in "$@" ; do
		case "${arg}" in
		'')
			qarg="''"
			;;
		*[!-./a-zA-Z0-9]*)
			# Convert each embedded ' to '\'',
			# then insert ' at the beginning of the first line,
			# and append ' at the end of the last line.
			# Finally, elide unnecessary '' pairs at the
			# beginning and end of the result and as part of
			# '\'''\'' sequences that result from multiple
			# adjacent quotes in he input.
			qarg="$(printf "%s\n" "$arg" | \
			    ${SED:-sed} -e "s/'/'\\\\''/g" \
				-e "1s/^/'/" -e "\$s/\$/'/" \
				-e "1s/^''//" -e "\$s/''\$//" \
				-e "s/'''/'/g"
				)"
			;;
		*)
			# Arg is not the empty string, and does not contain
			# any unsafe characters.  Leave it unchanged for
			# readability.
			qarg="${arg}"
			;;
		esac
		result="${result}${result:+ }${qarg}"
	done
	printf "%s\n" "$result"
)}

# Convert arg $1 to a basic regular expression (as in sed)
# that will match the arg.  This works by inserting backslashes
# before characters that are special in basic regular expressions.
# It also inserts backslashes before the extra characters specified
# in $2 (which defaults to "/,").
# XXX: Does not handle embedded newlines.
# Usage: regex="$(bre_quote "${string}")"
bre_quote()
{
	local arg="$1"
	local extra="${2-/,}"
	printf "%s\n" "${arg}" | sed -e 's/[][^$.*\\'"${extra}"']/\\&/g'
}

install_dir() {
	# $1 = target directory (relative to ${DESTDIR})

	touch "${TEMPROOT}"/.etcupdate.NEED_ANYTHING
	if ${AUTO_MISSING} || yesno "Create ${DESTDIR}${1}"; then
		verbose "Creating ${DESTDIR}${1}"
		mkdir -p "${DESTDIR}${1}" || exit 1
		touch "${TEMPROOT}"/.etcupdate.NEED_MTREE
	fi
}

install_file() {
	# $1 = target file (relative to ${DESTDIR})

	touch "${TEMPROOT}"/.etcupdate.NEED_ANYTHING
	# Install the new file
	verbose "Installing ${DESTDIR}${1}"
	cp -p "${TEMPROOT}${1}" "${DESTDIR}${1}" && rm -f "${TEMPROOT}${1}"

	# Check if this was a special file
	case "${1}" in
	/dev/MAKEDEV)
		touch "${TEMPROOT}"/.etcupdate.NEED_MAKEDEV
		;;
	/dev/MAKEDEV.local)
		touch "${TEMPROOT}"/.etcupdate.NEED_MAKEDEV
		;;
	/etc/mail/aliases)
		touch "${TEMPROOT}"/.etcupdate.NEED_NEWALIASES
		;;
	/etc/master.passwd)
		touch "${TEMPROOT}"/.etcupdate.NEED_PWD_MKDB
		;;
	/etc/services)
		touch "${TEMPROOT}"/.etcupdate.NEED_SERVICES_MKDB
		;;
	esac
}

install_checksum() {
	# $1 = target file (relative to ${DESTDIR})

	${AUTOMATIC} || return

	touch "${TEMPROOT}"/.etcupdate.NEED_ANYTHING
	D="$(dirname "${1}")"
	mkdir -p "${DESTDIR}/var/etcupdate/${D}"
	verbose "Saving MD5 checksum for ${DESTDIR}${1} to" \
	    "${DESTDIR}/var/etcupdate/${1}"
	# The sed part of the following pipeline changes things like
	# "MD5 (/path/to/dest/dir/etc/filename) = abc123" to
	# "MD5 (/etc/filename) = abc123".
	md5 "${DESTDIR}${1}" | sed -e "s,(${DESTDIR_BRE},(," \
	    > "${DESTDIR}/var/etcupdate/${1}"
}

# Initialise the DIFF_EXTRA_OPTIONS variable.
init_diff_extra_options() {
	#
	# Start with a few options that are always available.
	#
	DIFF_EXTRA_OPTIONS=\
"  su  Show differences in unified format (\"diff -u\")
  sc  Show differences in context format (\"diff -c\")
  ss  Show differences side by side (\"sdiff -w${WIDTH}\")"
	#
	# wdiff is not part of the base system, but the
	# user might have installed it from pkgsrc.  It is
	# useful to show differences on a word by word basis
	# instead of line by line.  If it is executable
	# then offer to use it in the menu.
	#
	if (wdiff /dev/null /dev/null) >/dev/null 2>&1 ; then
		DIFF_EXTRA_OPTIONS="${DIFF_EXTRA_OPTIONS}
  sw  Show differences word by word (\"wdiff -n -l\")"
	fi
	#
	# End with an option to use a user-specified diff-like command.
	#
	DIFF_EXTRA_OPTIONS="${DIFF_EXTRA_OPTIONS}
  scommand Show differences using the specified diff-like command"
}

record_action() {
	# $1 = action, $2 = target file
	test -n "${OUTFILE}" || return
	printf "%c %s\n" "$1" "$2" >>${OUTFILE}
}

diff_and_merge_file() {
	# $1 = target file (relative to ${DESTDIR})

	if cmp -s "${TEMPROOT}${1}" "${DESTDIR}${1}"; then
		verbose "===> ${1} (ok)"
		rm -f "${TEMPROOT}${1}"
		install_checksum "${1}"
		return
	fi

	if ${AUTOMATIC} && [ -f "${DESTDIR}/var/etcupdate/${1}" ]; then
		SUM1="$(md5 "${DESTDIR}${1}")"
		SUM2="$(cat "${DESTDIR}/var/etcupdate/${1}")"
		if [ "${SUM1}" = "${SUM2}" ]; then
			install_file "${1}"
			install_checksum "${1}"
			return
		fi
	fi

	if ${AUTO_MISSING} && [ ! -e "${DESTDIR}${1}" ]; then
		install_file "${1}"
		install_checksum "${1}"
		return
	fi

	if ${LOCALSKIP}; then
		ID1="$(ident -q "${TEMPROOT}${1}" | sed -n 2p)"
		ID1="${ID1:-0}"
		ID2="$(ident -q "${DESTDIR}${1}" | sed -n 2p)"
		ID2="${ID2:-1}"
		if [ "${ID1}" = "${ID2}" ]; then
			verbose "===> ${1} (ok:RCS)"
			rm -f "${TEMPROOT}${1}"
			return
		fi
	fi

	AUTOANSWER=""
	case ${KEEP_FILES} in
		*\ ${1}\ *) 
			verbose "===> ${DESTDIR}${1} (KEEP)"
			AUTOANSWER="d"
			;;
	esac
	case ${OVERWRITE_FILES} in
		*\ ${1}\ *) 
			verbose "===> ${DESTDIR}${1} (OVERWRITE)"
			AUTOANSWER="i"
			;;
	esac
	case ${MERGE_FILES} in
		*\ ${1}\ *) 
			verbose "===> ${DESTDIR}${1} (MERGE)"
			AUTOANSWER="m"
			;;
	esac

	if [ -z "${AUTOANSWER}" ]; then
		clear
		if [ ! -f "${DESTDIR}${1}" ]; then
			verbose "===> ${DESTDIR}${1} (missing)"
			DOES_EXIST=false
		else
			verbose "===> ${DESTDIR}${1} (modified)"
			verbose ""
			DOES_EXIST=true
			diff -u "${DESTDIR}${1}" "${TEMPROOT}${1}" | ${PAGER}
		fi
	fi

	STAY_HERE=true
	ALREADY_MERGED=false

	# Determine name for the backup file (/foo/._etcupdate.bar)
	D="$(dirname  "${TEMPROOT}${1}")"
	F="$(basename "${TEMPROOT}${1}")"
	B="${D}/.etcupdate.${F}"
	F="${D}/${F}"

	while ${STAY_HERE}; do

		if [ -n "${AUTOANSWER}" ]; then
			ANSWER="${AUTOANSWER}"
			AUTOANSWER=""
		else
			# Ask the user if (s)he wants to install the new
			# version or perform a more complicated manual work.
			echo ""
			echo -n "File: ${DESTDIR}${1}"
			if [ ! -f "${DESTDIR}${1}" ]; then
				echo -n " (missing)"
			else
				echo -n " (modified)"
			fi
			echo ""
			echo ""
			echo "Please select one of the following operations:"
			echo ""
			if ! ${DOES_EXIST}; then
				cat << EOF
  d  Don't install the missing file
  i  Install the missing file
  v  Show the missing file

EOF
			elif ! ${ALREADY_MERGED}; then
				cat << EOF
  d  Don't install the new file (keep your old file)
  i  Install the new file (overwrites your local modifications!)
  m  Merge the currently installed and new files
  s  Show the differences between the currently installed and new files
${DIFF_EXTRA_OPTIONS}
  v  Show the new file

EOF
			else
				cat << EOF
  d  Don't install the merged file (keep your old file)
  i  Install the merged file (overwrites your old file)
  m  Merge again (your old file against the result from the previous merge)
  s  Show the differences between the currently installed and new merged files
${DIFF_EXTRA_OPTIONS}
  u  Undo merge (start again with the original version of the new file)
  v  Show the merged file

EOF
			fi
			echo -n "What do you want to do? [Leave it for later] "
			read ANSWER
		fi
		case "${ANSWER}" in

		[dD])
			verbose "Removing ${TEMPROOT}${1}"
			rm -f "${TEMPROOT}${1}"
			record_action "d" "${1}"
			STAY_HERE=false
			;;
		[iI])
			install_file "${1}"
			if ! ${ALREADY_MERGED}; then
				record_action "i" "${1}"
				install_checksum "${1}"
			else
				record_action "m" "${1}"
			fi
			STAY_HERE=false
			;;
		[mM])
			${DOES_EXIST} || continue
			[ ! -f "${B}" ] && cp "${F}" "${B}"
			cp "${TEMPROOT}${1}" "${TEMPROOT}${1}.merged"
			sdiff -o "${TEMPROOT}${1}.merged"	\
				--width=${WIDTH}		\
				--suppress-common-lines --text	\
				"${DESTDIR}${1}" "${TEMPROOT}${1}"
			mv -f "${TEMPROOT}${1}.merged" "${TEMPROOT}${1}"
			ALREADY_MERGED=true
			;;
		[sS]*)
			${DOES_EXIST} || continue
			case "${ANSWER}" in
			[sS])	: no change ;;
			[sS]u)	DIFF_COMMAND="diff -u" ;;
			[sS]c)	DIFF_COMMAND="diff -c" ;;
			[sS]s)	DIFF_COMMAND="sdiff -w${WIDTH}" ;;
			[sS]w)	DIFF_COMMAND="wdiff -n -l" ;;
			[sS]*)	DIFF_COMMAND="${ANSWER#?}" ;;
			esac
			${DIFF_COMMAND} "${DESTDIR}${1}" "${TEMPROOT}${1}" \
				| ${PAGER}
			;;
		[uU])
			if [ -f "${B}" ]; then
				echo "*** Restoring ${F}"
				mv -f "${B}" "${F}"
			fi
			ALREADY_MERGED=false
			;;
		[vV])
			${PAGER} "${TEMPROOT}${1}"
			;;
		"")
			STAY_HERE=false
			;;
		*)
			echo "*** Invalid selection!"
			;;
		esac
	done
	rm -f "$B"
}

# Set the environment for make.
set_makeenv() {
	#
	# INSTALL_DONE=1 prevents installation of unwanted
	# files (things that are not part of the etc set).
	# BUILD=1 allows building of files that are wanted.
	#
	MAKE_ENV=" 			\
		NETBSDSRCDIR=$(shell_quote "${SRCDIR}")	\
		DESTDIR=$(shell_quote "${TEMPROOT}")	\
		MAKE=make		\
		MTREE=mtree		\
		TOOL_MTREE=mtree	\
		INSTALL_DONE=1		\
		BUILD=1			\
		USETOOLS=never"
}

#
# main()
#

# Read global configuration
GLOBALRC="/etc/${PROG}.conf"
[ -r ${GLOBALRC} ] && . ${GLOBALRC}

# Read user configuration
USERRC="${HOME}/.{PROG}rc"
[ -r ${USERRC} ] && . ${USERRC}

# Read command line arguments
while getopts abd:hi:lmo:p:s:t:vw: i
do
	case "${i}" in
	a)
		AUTOMATIC=true
		;;
	b)
		BOOT_AUTOMATIC=true
		;;
	d)
		DESTDIR="${OPTARG}"
		;;
	h)
		usage
		;;
	i)
		INFILE="${OPTARG}"
		;;
	l)
		LOCALSKIP=true
		;;
	m)
		AUTO_MISSING=true
		;;
	o)
		OUTFILE="${OPTARG}"
		;;
	p)
		PAGER="${OPTARG}"
		;;
	s)
		# Three cases:
		# -s tgzfile       (may be repeated)
		# -s srcdir        (may not be repeated)
		# -s extracted_dir (may not be repeated)
		arg="${OPTARG}"
		qarg="$(shell_quote "${OPTARG}")"
		N_SRC_ARGS=$(( N_SRC_ARGS + 1 ))
		SRC_ARGLIST="${SRC_ARGLIST}${SRC_ARGLIST:+ }-s ${qarg}"
		if [ -f "${arg}" ]; then
			# arg refers to a *.tgz file.
			# This may happen twice, for both etc.tgz and
			# xetc.tgz, so we build up a list in TGZLIST.
			BINARYMODE=true
			BINARYTGZMODE=true
			TGZLIST="${TGZLIST}${TGZLIST:+ }${qarg}"
		elif [ -d "${arg}" ] && [ -f "${arg}/etc/Makefile" ]; then
			# arg refers to a source directory
			SOURCEMODE=true
			SRCDIR="${arg}"
		elif [ -d "${arg}" ] && [ -d "${arg}/etc" ] \
			&& ! [ -f "${arg}/etc/Makefile" ]
		then
			# arg refers to a directory where the
			# sets have already been extracted
			BINARYMODE=true
			BINARYDIRMODE=true
			BINARYDIR="${arg}"
		else
			echo "*** Nonexistent or invalid file or directory" \
			     "for -s ${arg}"
			usage
		fi
		;;
	t)
		TEMPROOT="${OPTARG}"
		;;
	v)
		VERBOSE=true
		;;
	w)
		WIDTH="${OPTARG}"
		;;
	*)
		# getopts should already have printed an error message
		usage
		break
		;;
	esac
done

# Last minute sanity checks
if [ -z "${DESTDIR}" ] && [ "$(id -u)" -ne 0 ]; then
	echo "*** ERROR: You MUST be root"
	exit 1
fi
DESTDIR="${DESTDIR%/}" # remove trailing slash, if any.  result might be "".
DESTDIR_BRE="$(bre_quote "${DESTDIR}")"
if [ "${N_SRC_ARGS}" -gt 1 ] && ( ${SOURCEMODE} || ${BINARYDIRMODE} ); then
	echo "*** ERROR: Multiple -s args are allowed only with tgz files"
	usage
fi
case "${TEMPROOT}" in
/*) : OK ;;
*)  new="${PWD:-$(pwd)}/${TEMPROOT}"
    echo "*** NOTE: Using TEMPROOT \"${new}\" instead of \"${TEMPROOT}\""
    TEMPROOT="${new}"
    ;;
esac
if ${BINARYDIRMODE}; then
	SRCDIR="${TEMPROOT}"
fi
if ${BINARYTGZMODE}; then
	SRCDIR="${TEMPROOT}"
fi
if [ "${N_SRC_ARGS}" -eq 0 ]; then
	# default if no "-s" option was specified
	SOURCEMODE=true
	SRCDIR="/usr/src"
	SRC_ARGLIST="-s $(shell_quote "${SRCDIR}")"
fi
if [ -z "${SRCDIR}" -o -z "${TEMPROOT}" ]; then
	echo "*** ERROR: One of the following variables is undefined"
	echo ""
	echo "SRCDIR=\"${SRCDIR}\""
	echo "TEMPROOT=\"${TEMPROOT}\""
	echo ""
	exit 1
fi
if [ -r "${TEMPROOT}" ]; then
	if ${BOOT_AUTOMATIC}; then
		echo ""
		echo "*** ERROR: ${TEMPROOT} already exists"
		echo ""
		exit 1
	else
		echo ""
		echo "*** WARNING: ${TEMPROOT} already exists"
		echo ""
		if yesno "Continue previously aborted update"; then
			CONTINUE=true
		elif yesno "Remove the old ${TEMPROOT}"; then
			echo "*** Removing ${TEMPROOT}"
			rm -rf "${TEMPROOT}"
		fi
	fi
fi

if ! ${CONTINUE}; then
	# Create the temporary root directory
	echo "*** Creating ${TEMPROOT}"
	mkdir -p "${TEMPROOT}"
	if [ ! -d "${TEMPROOT}" ]; then
		echo "*** ERROR: Unable to create ${TEMPROOT}"
		exit 1
	fi
	# Are we using the sources or binaries?
	if ${BINARYTGZMODE}; then
		# Populate ${TEMPROOT} from ${TGZLIST}
		eval "set -- ${TGZLIST}"
		for tgz in "$@"; do
			if [ ! -f "${tgz}" ]; then
				echo "*** ERROR: Unable to find ${tgz}"
				exit 1
			fi
			echo "*** Populating ${TEMPROOT} from ${tgz}"
			tar -zxpf "${tgz}" -C "${TEMPROOT}"
			[ $? -ne 0 ] && exit 1
		done
	elif ${BINARYDIRMODE}; then
		# Populate ${TEMPROOT} from ${SRCDIR} by copying.
		# Copy only the files that belong to the etc and xetc sets.
		echo "*** Populating ${TEMPROOT} from ${BINARYDIR} (copying)"
		for setname in etc xetc; do
			mtreefile="${BINARYDIR}/etc/mtree/set.${setname}"
			if ${VERBOSE}; then vflag="-v"; else vflag=""; fi
			if [ -f "${mtreefile}" ]; then
				echo "*** Copying files belonging to" \
				     "${setname} set"
				(cd "${BINARYDIR}" \
				 && pax -rwdM ${vflag} "${TEMPROOT%/}/."
				) <"${mtreefile}"
				[ $? -ne 0 ] && exit 1
			else
				echo "*** Not copying files belonging to" \
				     "${setname} set: ${mtreefile} not found"
			fi
		done
	elif ${SOURCEMODE}; then
		# Populate ${TEMPROOT} from ${SRCDIR} by running make
		if [ ! -f "${SRCDIR}/etc/Makefile" ]; then
			echo "*** ERROR: Unable to find ${SRCDIR}/etc/Makefile"
			exit 1
		fi
		set_makeenv
		echo "*** Populating ${TEMPROOT} from ${SRCDIR} (make distribution)"
		cd ${SRCDIR}/etc
		if ! ${VERBOSE}; then
			eval "${MAKE_ENV} make distribution > /dev/null"
		else
			eval "${MAKE_ENV} make distribution"
		fi
		[ $? -ne 0 ] && exit 1
	fi
	if ! [ -f "${TEMPROOT}/etc/mtree/set.etc" ]; then
		echo "*** ERROR: Files from the etc.tgz set are missing"
		exit 1
	fi
	if [ ! -f "${TEMPROOT}/dev/MAKEDEV" ]; then
		echo ""
		echo "*** WARNING: ${TEMPROOT}/dev/MAKEDEV not found"
		echo "Make sure you update /dev/MAKEDEV later and run"
		echo "(cd /dev && ./MAKEDEV all) to rebuild the device nodes"
		echo ""
	fi

	# Ignore the following files during comparision
	rm -f "${TEMPROOT}"/etc/passwd
	rm -f "${TEMPROOT}"/etc/pwd.db
	rm -f "${TEMPROOT}"/etc/spwd.db
	find "${TEMPROOT}" -type f -size 0 -exec rm {} \;

	# Ignore files we're told to ignore
	if [ ! -z "${IGNOREFILES}" ]; then
		echo "*** Ignoring files: ${IGNOREFILES}"
		for file in ${IGNOREFILES}; do
			rm -f "${TEMPROOT}"${file}
		done
	fi

	if ${BOOT_AUTOMATIC}; then
		echo "*** Initializing checksums"
		find "${TEMPROOT}" -type f  -a ! -name \*.etcupdate.\* | \
		while read i; do
			F="${i#"${TEMPROOT}"}"
			D="$(dirname "${F}")"
			mkdir -p "${DESTDIR}/var/etcupdate/${D}"
			verbose "Saving original MD5 checksum for ${F} to ${DESTDIR}/var/etcupdate/${F}"
			# unfortunately, the files in /var/etcupdate contain the plain output of the md5 command, including the filename
			SUM1="$(md5 "${i}")"
			SUM1="${SUM1##*= }"
			printf "MD5 (%s) = %s\n" "$F" "$SUM1" > "${DESTDIR}/var/etcupdate/${F}"
		done
		echo "*** Removing ${TEMPROOT}"
		rm -rf "${TEMPROOT}"
		echo "*** Done"
		exit 0
	fi

	# Are there any new directories?
	echo "*** Checking for new directories"
	exec 3<&0
	find "${TEMPROOT}" -type d | \
	while read i; do
		D="${i#"${TEMPROOT}"}"
		[ "x${i}" = "x${TEMPROOT}" ] && continue
		[ ! -d "${DESTDIR}${D}" ] && install_dir "${D}" <&3
	done
fi

if [ -n "${OUTFILE}" ]; then
	echo "# $0 $(date)" >${OUTFILE}
fi

if [ -n "${INFILE}" ]; then
	while read cmd file comment; do
		case ${cmd} in
			\#) ;;
			d) KEEP_FILES="${KEEP_FILES}${file} " ;;
			i) OVERWRITE_FILES="${OVERWRITE_FILES}${file} " ;;
			m) MERGE_FILES="${MERGE_FILES}${file} " ;;
			*) echo "*** ERROR: unknown command $cmd in ${INFILE}"; exit 1 ;;
		esac
	done <${INFILE}
fi

# Start the comparision
echo "*** Checking for added/modified files"
init_diff_extra_options
exec 3<&0
find "${TEMPROOT}" -type f  -a ! -name \*.etcupdate.\* | \
while read i; do
	D="${i#"${TEMPROOT}"}"
	diff_and_merge_file "${D}" <&3
done

# Do we have files which were not processed?
REMAINING="$(find "${TEMPROOT}" -type f -a ! -name \*.etcupdate.\*)"
if [ ! -z "${REMAINING}" ]; then
	echo ""
	echo "*** The following files need your attention:"
	echo ""
	echo "${REMAINING}" | sed -e 's/^/  /'
	echo ""
elif test -f "${TEMPROOT}"/.etcupdate.NEED_ANYTHING; then
	rm "${TEMPROOT}"/.etcupdate.NEED_ANYTHING
	echo ""
	echo "*** No changes were needed"
	echo ""
fi

# Clean up after "make distribution"
if ${SOURCEMODE}; then
	echo "*** Cleaning up in ${SRCDIR}/etc"
	set_makeenv
	cd ${SRCDIR}/etc
	if ! ${VERBOSE}; then
		eval "${MAKE_ENV} make clean > /dev/null"
	else
		eval "${MAKE_ENV} make clean"
	fi
fi

# Do some post-installation tasks
if test -f "${TEMPROOT}"/.etcupdate.NEED_PWD_MKDB; then
	rm "${TEMPROOT}"/.etcupdate.NEED_PWD_MKDB
	pwd_mkdb_cmd="$(shell_quote \
	    pwd_mkdb ${DESTDIR:+-d "${DESTDIR}"} \
	    -p "${DESTDIR}/etc/master.passwd")"
	if yesno "Do you want to rebuild the password databases from the" \
	         "new ${DESTDIR}/etc/master.passwd"
	then
		verbose "Running pwd_mkdb"
		eval "${pwd_mkdb_cmd}"
	else
		echo ""
		echo "*** You MUST rebuild the password databases to make" \
		     "the changes visible"
		echo "*** This is done by running \"${pwd_mkdb_cmd}\" as root"
		echo ""
	fi
fi

if ! test -f "${TEMPROOT}"/.etcupdate.NEED_SERVICES_MKDB; then
	if test -e "${DESTDIR}/var/db/services.db" \
	   -a ! -e "${DESTDIR}/var/db/services.cdb"; then
		touch "${TEMPROOT}"/.etcupdate.NEED_SERVICES_MKDB
	fi
fi

if test -f "${TEMPROOT}"/.etcupdate.NEED_SERVICES_MKDB; then
	rm "${TEMPROOT}"/.etcupdate.NEED_SERVICES_MKDB
	services_mkdb_cmd="$(shell_quote services_mkdb -V cdb \
	    -o "${DESTDIR}/var/db/services.cdb" \
	    "${DESTDIR}/etc/services")"
	if yesno "Do you want to rebuild the services databases from the" \
	         "new ${DESTDIR}/etc/services"
	then
		verbose "Running services_mkdb"
		eval "${services_mkdb_cmd}"
	else
		echo ""
		echo "*** You SHOULD rebuild the services databases to make" \
		     "the changes visible"
		echo "*** This is done by running \"${services_mkdb_cmd}\"" \
		     "as root"
		echo ""
	fi
fi
if test -f "${TEMPROOT}"/.etcupdate.NEED_MTREE; then
	rm "${TEMPROOT}"/.etcupdate.NEED_MTREE
	if yesno "You have created new directories. Run mtree to set" \
	         "permissions"
	then
		(cd "${DESTDIR:-/}" && \
		    mtree -Udef "${DESTDIR}/etc/mtree/NetBSD.dist")
	fi
fi
if test -f "${TEMPROOT}"/.etcupdate.NEED_MAKEDEV; then
	rm "${TEMPROOT}"/.etcupdate.NEED_MAKEDEV
	makedev_cmd="($(shell_quote cd "${DESTDIR}/dev") && ./MAKEDEV all)"
	if yesno "Do you want to rebuild the device nodes in ${DESTDIR}/dev"
	then
		verbose "Running MAKEDEV in /dev"
		eval "${makedev_cmd}"
	else
		echo ""
		echo "*** You SHOULD rebuild the device nodes in" \
		     "${DESTDIR}/dev"
		echo "*** This is done by running \"${makedev_cmd}\" as root."
		echo ""
	fi
fi
if test -f "${TEMPROOT}"/.etcupdate.NEED_NEWALIASES; then
	rm "${TEMPROOT}"/.etcupdate.NEED_NEWALIASES
	newaliases_cmd="newaliases"
	# XXX newaliases doesn't work with DESTDIR.
	# We could check whether the system configuration is
	# sufficiently standard, and then run postalias(1) with the
	# right args to make it work, but changes to /etc/mail/aliases
	# are so rare that it doesn't seem worth the effort of checking
	# that the system's mail configuration is standard.
	if [ -z "${DESTDIR}" ] && \
	    yesno "Do you want to rebuild the mail alias database"
	then
		verbose "Running newaliases"
		eval "${newaliases_cmd}"
	else
		echo ""
		echo "*** You MUST rebuild the mail alias database to make" \
		     "the changes visible"
		echo "*** This is done by running \"${newaliases_cmd}\" as root"
		if [ -n "${DESTDIR}" ]; then
		    postalias_cmd="$(shell_quote \
			postalias "hash:${DESTDIR}/etc/mail/aliases")"
		    echo "*** but it won't work with DESTDIR=${DESTDIR}."
		    echo "*** If you use postfix(1) with the default" \
			 "configuration, then you can try"
		    echo "*** running \"${postalias_cmd}\" as root."
		fi
		echo ""
	fi
fi

if yesno "Remove ${TEMPROOT}"; then
	echo "*** Removing ${TEMPROOT}"
	rm -rf "${TEMPROOT}"
else
	echo "*** Keeping ${TEMPROOT}"
fi

if [ -x "${DESTDIR}/usr/sbin/postinstall" ]; then
	postinstall_cmd="$(shell_quote "${DESTDIR}/usr/sbin/postinstall" \
	    ${DESTDIR:+-d "${DESTDIR}"}) ${SRC_ARGLIST} check"
	echo "*** Running ${DESTDIR}/usr/sbin/postinstall"
	eval "${postinstall_cmd}"
fi
echo "*** All done"


Home | Main Index | Thread Index | Old Index