#!/bin/sh # # $NetBSD: etcupdate,v 1.48 2008/10/22 15:20:05 apb Exp $ # # 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> 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##*/}" 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 one or more "-s" args N_SRC_ARGS=0 # number of "-s" args AUTOMATIC=false LOCALSKIP=false MACHINE="${MACHINE:="$(uname -m)"}" export MACHINE MACHINE_ARCH="${MACHINE_ARCH:="$(uname -p)"}" export MACHINE_ARCH # Settings for post-installation procedures NEED_MAKEDEV=false NEED_MTREE=false NEED_NEWALIASES=false NEED_PWD_MKDB=false NEED_SERVICES_MKDB=false 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", or a colon-separated list of 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) -w width Screen width (default: 80) -a Automatically update unmodified files -l Automatically skip files with strictly local changes (this option has no effect on files lacking RCS Ids) -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 for arg in "$@" ; do # Append a space if necessary result="${result}${result:+ }" # Convert each embedded ' to '\'', # then insert ' at the beginning of the first line, # and append ' at the end of the last line. result="${result}$(printf "%s\n" "$arg" | \ sed -e "s/'/'\\\\''/g" -e "1s/^/'/" -e "\$s/\$/'/")" done printf "%s\n" "$result" } install_dir() { # $1 = target directory if yesno "Create ${1}"; then verbose "Creating ${1}" mkdir -p "${1}" || exit 1 NEED_MTREE=true fi } install_file() { # $1 = target file # Install the new file verbose "Installing ${1}" cp -p "${TEMPROOT}${1}" "${1}" && rm -f "${TEMPROOT}${1}" # Check if this was a special file case "${1}" in /dev/MAKEDEV) NEED_MAKEDEV=true ;; /dev/MAKEDEV.local) NEED_MAKEDEV=true ;; /etc/mail/aliases) NEED_NEWALIASES=true ;; /etc/master.passwd) NEED_PWD_MKDB=true ;; /etc/services) NEED_SERVICES_MKDB=true ;; esac } install_checksum() { # $1 = target file ${AUTOMATIC} || return D="$(dirname "${1}")" mkdir -p "/var/etcupdate/${D}" md5 "${1}" > "/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" } diff_and_merge_file() { # $1 = target file if cmp -s "${TEMPROOT}${1}" "${1}"; then verbose "===> ${1} (ok)" rm -f "${TEMPROOT}${1}" install_checksum "${1}" return fi if ${AUTOMATIC} && [ -f "/var/etcupdate/${1}" ]; then SUM1="$(md5 "${1}")" SUM2="$(cat "/var/etcupdate/${1}")" if [ "${SUM1}" = "${SUM2}" ]; then install_file "${1}" install_checksum "${1}" return fi fi if ${LOCALSKIP}; then ID1="$(ident -q "${TEMPROOT}${1}" | sed -n 2p)" ID1="${ID1:-0}" ID2="$(ident -q "${1}" | sed -n 2p)" ID2="${ID2:-1}" if [ "${ID1}" = "${ID2}" ]; then verbose "===> ${1} (ok:RCS)" rm -f "${TEMPROOT}${1}" return fi fi clear if [ ! -f "${1}" ]; then verbose "===> ${1} (missing)" DOES_EXIST=false else verbose "===> ${1} (modified)" verbose "" DOES_EXIST=true diff -u "${1}" "${TEMPROOT}${1}" | ${PAGER} 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 # Ask the user if (s)he wants to install the new # version or perform a more complicated manual work. echo "" echo -n "File: ${1}" if [ ! -f "${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 case "${ANSWER}" in [dD]) verbose "Removing ${TEMPROOT}${1}" rm -f "${TEMPROOT}${1}" STAY_HERE=false ;; [iI]) install_file "${1}" if ! ${ALREADY_MERGED}; then install_checksum "${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 \ "${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} "${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 "._etcupdate_${TEMPROOT}${1}" } # 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 ahlp:s:t:vw: i do case "${i}" in a) AUTOMATIC=true ;; h) usage ;; l) LOCALSKIP=true ;; 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}" elif [ -f "${arg%%:*}" -a -f "${arg##*:}" -a ! -f "${arg}" ] then # Backward compatibility: allow arg to refer # to a colon-separated list of tgz files. # Remove this after NetBSD-5.0 is released. cat <<EOF *** WARNING: The "-s tgzfile1:tgzfile2" option is deprecated. Please use "-s tgzfile1 -s tgzfile2" in future. EOF BINARYMODE=true BINARYTGZMODE=true TGZLIST="${TGZLIST}${TGZLIST:+ }$( \ IFS=: eval shell_quote \${arg} )" else echo "*** Nonexistent 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 [ "$(id -u)" -ne 0 ]; then echo "*** ERROR: You MUST be root" exit 1 fi if [ "${N_SRC_ARGS}" -gt 1 ] && ( ${SOURCEMODE} || ${BINARYDIRMODE} ); then echo "*** ERROR: Multiple -s args are allowed only with tgz files" usage fi 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 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 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 echo "*** Populating ${TEMPROOT} from ${BINARYDIR} (copying)" cp -RPp "${BINARYDIR}"/* "${TEMPROOT}"/ [ $? -ne 0 ] && exit 1 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 # 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 "${D}" ] && install_dir "${D}" <&3 done 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 "" fi if yesno "Remove ${TEMPROOT}"; then echo "*** Removing ${TEMPROOT}" rm -rf "${TEMPROOT}" else echo "*** Keeping ${TEMPROOT}" 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 ${NEED_PWD_MKDB}; then if yesno "Do you want to rebuild the password databases from the" \ "new master.passwd" then verbose "Running pwd_mkdb" pwd_mkdb -p "/etc/master.passwd" else echo "" echo "*** You MUST rebuild the password databases to make" \ "the changes visible" echo "*** This is done by running \"pwd_mkdb -p" \ "/etc/master.passwd\" as root" echo "" fi fi if ${NEED_SERVICES_MKDB}; then if yesno "Do you want to rebuild the services databases from the" \ "new /etc/services" then verbose "Running services_mkdb" services_mkdb else echo "" echo "*** You SHOULD rebuild the services databases to make" \ "the changes visible" echo "*** This is done by running \"services_mkdb\" as root" echo "" fi fi if ${NEED_MTREE}; then if yesno "You have created new directories. Run mtree to set" \ "permissions" then (cd / && mtree -Udef /etc/mtree/NetBSD.dist) fi fi if ${NEED_MAKEDEV}; then if yesno "Do you want to rebuild the device nodes in /dev"; then verbose "Running MAKEDEV in /dev" (cd "/dev" && ./MAKEDEV all) else echo "" echo "*** You SHOULD rebuild the device nodes in /dev" echo "*** This is done by running \"(cd /dev &&" \ "./MAKEDEV all)\" as root". echo "" fi fi if ${NEED_NEWALIASES}; then if yesno "Do you want to rebuild the mail alias database"; then verbose "Running newaliases" newaliases else echo "" echo "*** You MUST rebuild the mail alias database to make" \ "the changes visible" echo "*** This is done by running \"newaliases\" as root" echo "" fi fi if [ -x /usr/sbin/postinstall ]; then echo "*** Running /usr/sbin/postinstall" eval "/usr/sbin/postinstall ${SRC_ARGLIST} check" fi echo "*** All done"