diff options
author | jhb <jhb@FreeBSD.org> | 2010-07-10 02:29:51 +0800 |
---|---|---|
committer | jhb <jhb@FreeBSD.org> | 2010-07-10 02:29:51 +0800 |
commit | 098838b5cb6b0abfd674bd129da998d10ba93571 (patch) | |
tree | 0c456ebb0518e8eb49192d94ac78db3aa5451e5a /sysutils/etcupdate/src | |
parent | eb6a03e6177d3ca72f51c2b226a8bb24c4384294 (diff) | |
download | freebsd-ports-gnome-098838b5cb6b0abfd674bd129da998d10ba93571.tar.gz freebsd-ports-gnome-098838b5cb6b0abfd674bd129da998d10ba93571.tar.zst freebsd-ports-gnome-098838b5cb6b0abfd674bd129da998d10ba93571.zip |
Add etcupdate 0.1, manage updates to /etc automatically.
Reviewed by: dougb
Diffstat (limited to 'sysutils/etcupdate/src')
-rw-r--r-- | sysutils/etcupdate/src/etcupdate.8 | 769 | ||||
-rw-r--r-- | sysutils/etcupdate/src/etcupdate.sh | 1671 |
2 files changed, 2440 insertions, 0 deletions
diff --git a/sysutils/etcupdate/src/etcupdate.8 b/sysutils/etcupdate/src/etcupdate.8 new file mode 100644 index 000000000000..205d2faf4529 --- /dev/null +++ b/sysutils/etcupdate/src/etcupdate.8 @@ -0,0 +1,769 @@ +.\" Copyright (c) 2010 Advanced Computing Technologies LLC +.\" Written by: John H. Baldwin <jhb@FreeBSD.org> +.\" All rights reserved. +.\" +.\" 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 AUTHOR 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 AUTHOR 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. +.\" +.\" $FreeBSD$ +.\" +.Dd April 26, 2010 +.Dt ETCUPDATE 8 +.Os +.Sh NAME +.Nm etcupdate +.Nd "manage updates to system files not updated by installworld" +.Sh SYNOPSIS +.Nm +.Op Fl nBF +.Op Fl d Ar workdir +.Op Fl r | Fl s Ar source | Fl t Ar tarball +.Op Fl A Ar patterns +.Op Fl D Ar destdir +.Op Fl I Ar patterns +.Op Fl L Ar logfile +.Op Fl M Ar options +.Nm +.Cm build +.Op Fl B +.Op Fl d Ar workdir +.Op Fl s Ar source +.Op Fl L Ar logfile +.Op Fl M Ar options +.Ar tarball +.Nm +.Cm diff +.Op Fl d Ar workdir +.Op Fl D Ar destdir +.Op Fl I Ar patterns +.Op Fl L Ar logfile +.Nm +.Cm extract +.Op Fl B +.Op Fl d Ar workdir +.Op Fl s Ar source | Fl t Ar tarball +.Op Fl L Ar logfile +.Op Fl M Ar options +.Nm +.Cm resolve +.Op Fl d Ar workdir +.Op Fl D Ar destdir +.Op Fl L Ar logfile +.Nm +.Cm status +.Op Fl d Ar workdir +.Sh DESCRIPTION +The +.Nm +utility is a tool for managing updates to files that are not updated as +part of +.Sq make installworld +such as files in +.Pa /etc . +It manages updates by doing a three-way merge of changes made to these +files against the local versions. +It is also designed to minimize the amount of user intervention with +the goal of simplifying upgrades for clusters of machines. +.Pp +To perform a three-way merge, +.Nm +keeps copies of the current and previous versions of files that it manages. +These copies are stored in two trees known as the +.Dq current +and +.Dq previous +trees. +During a merge, +.Nm +compares the +.Dq current +and +.Dq previous +copies of each file to determine which changes need to be merged into the +local version of each file. +If a file can be updated without generating a conflict, +.Nm +will update the file automatically. +If the local changes to a file conflict with the changes made to a file in +the source tree, +then a merge conflict is generated. +The conflict must be resolved after the merge has finished. +The +.Nm +utility will not perform a new merge until all conflicts from an earlier +merge are resolved. +.Sh MODES +.Pp +The +.Nm +utility supports several modes of operation. +The mode is specified via an optional command argument. +If present, the command must be the first argument on the command line. +If a command is not specified, the default mode is used. +.Ss Default Mode +The default mode merges changes from the source tree to the destination +directory. +First, +it updates the +.Dq current +and +.Dq previous +trees. +Next, +it compares the two trees merging changes into the destination directory. +Finally, +it displays warnings for any conditions it could not handle automatically. +.Pp +If the +.Fl r +option is not specified, +then the first step taken is to update the +.Dq current +and +.Dq previous +trees. +If a +.Dq current +tree already exists, +then that tree is saved as the +.Dq previous +tree. +An older +.Dq previous +tree is removed if it exists. +By default the new +.Dq current +tree is built from a source tree. +However, +if a tarball is specified via the +.Fl t +option, +then the tree is extracted from that tarball instead. +.Pp +Next, +.Nm +compares the files in the +.Dq current +and +.Dq previous +trees. +If a file was removed from the +.Dq current +tree, +then it will be removed from the destination directory only if it +does not have any local modifications. +If a file was added to the +.Dq current +tree, +then it will be copied to the destination directory only if it +would not clobber an existing file. +If a file is changed in the +.Dq current +tree, +then +.Nm +will attempt to merge the changes into the version of the file in the +destination directory. +If the merge encounters conflicts, +then a version of the file with conflict markers will be saved for +future resolution. +If the merge does not encounter conflicts, +then the merged version of the file will be saved in the destination +directory. +If +.Nm +is not able to safely merge in changes to a file other than a merge conflict, +it will generate a warning. +.Pp +For each file that is updated a line will be output with a leading character +to indicate the action taken. +The possible actions follow: +.Pp +.Bl -tag -width "A" -compact -offset indent +.It A +Added +.It C +Conflict +.It D +Deleted +.It M +Merged +.It U +Updated +.El +.Pp +Finally, +if any warnings were encountered they are displayed after the merge has +completed. +.Pp +Note that for certain files +.Nm +will perform post-install actions any time that the file is updated. +Specifically, +.Xr pwd_mkdb 8 +is invoked if +.Pa /etc/master.passwd +is changed, +.Xr cap_mkdb 1 +is invoked to update +.Pa /etc/login.conf.db +if +.Pa /etc/login.conf +is changed, +and +.Xr newaliases 1 +is invoked if +.Pa /etc/mail/aliases +is changed. +One exception is that if +.Pa /etc/mail/aliases +is changed and the destination directory is not the default, +then a warning will be issued instead. +This is due to a limitation of the +.Xr newaliases 1 +command. +.Ss Build Mode +The +.Cm build +mode is used to build a tarball that contains a snapshot of a +.Dq current +tree. +This tarball can be used by the default and extract modes. +Using a tarball can allow +.Nm +to perform a merge without requiring a source tree that matches the +currently installed world. +The +.Fa tarball +argument specifies the name of the file to create. +The file will be a +.Xr tar 5 +file compressed with +.Xr bzip2 1 . +.Ss Diff Mode +The +.Cm diff +mode compares the versions of files in the destination directory to the +.Dq current +tree and generates a unified format diff of the changes. +This can be used to determine which files have been locally modified and how. +Note that +.Nm +does not manage files that are not maintained in the source tree such as +.Pa /etc/fstab +and +.Pa /etc/rc.conf . +.Ss Extract Mode +The +.Cm extract +mode generates a new +.Dq current +tree. +Unlike the default mode, +it does not save any existing +.Dq current +tree and does not modify any existing +.Dq previous +tree. +The new +.Dq current +tree can either be built from a source tree or extracted from a tarball. +.Ss Resolve Mode +The +.Cm resolve +mode is used to resolve any conflicts encountered during a merge. +In this mode, +.Nm +iterates over any existing conflicts prompting the user for actions to take +on each conflicted file. +For each file, the following actions are available: +.Pp +.Bl -tag -width "(tf) theirs-full" -compact +.It (p) postpone +Ignore this conflict for now. +.It (df) diff-full +Show all changes made to the merged file as a unified diff. +.It (e) edit +Change the merged file in an editor. +.It (r) resolved +Install the merged version of the file into the destination directory. +.It (mf) mine-full +Use the version of the file in the destination directory and ignore any +changes made to the file in the +.Dq current +tree. +.It (tf) theirs-full +Use the version of the file from the +.Dq current +tree and discard any local changes made to the file. +.It (h) help +Display the list of commands. +.El +.Ss Status Mode +The +.Cm status +mode shows a summary of the results of the most recent merge. +First it lists any files for which there are unresolved conflicts. +Next it lists any warnings generated during the last merge. +If the last merge did not generate any conflicts or warnings, +then nothing will be output. +.Sh OPTIONS +The following options are available. +Note that most options do not apply to all modes. +.Bl -tag -width ".Fl d Ar workdir" +.It Fl B +Do not build generated files in a private object tree. +Instead, +reuse the generated files from a previously built object tree that matches +the source tree. +This can be useful to avoid gratuitous conflicts in sendmail configuration +files when bootstrapping. +It can also be useful for building a tarball that matches a specific +world build. +.It Fl d Ar workdir +Specify an alternate directory to use as the work directory. +The work directory is used to store the +.Dq current +and +.Dq previous +trees as well as unresolved conflicts. +The default work directory is +.Pa /var/db/etcupdate . +.It Fl A Ar patterns +Always install the new version of any files that match any of the patterns +listed in +.Ar patterns . +Each pattern is evaluated as an +.Xr sh 1 +shell pattern. +This option may be specified multiple times to specify multiple patterns. +Multiple space-separated patterns may also be specified in a single +option. +Note that ignored files specified via the +.Ev IGNORE_FILES +variable or the +.Fl I +option will not be installed. +.It Fl D Ar destdir +Specify an alternate destination directory as the target of a merge. +This is analagous to the +.Dv DESTDIR +variable used with +.Sq make installworld . +The default destination directory is an empty string which results in +merges updating +.Pa /etc +on the local machine. +.It Fl F +Ignore changes in the FreeBSD ID string when comparing files in the +destination directory to files in either of the +.Dq current +or +.Dq previous +trees. +In +.Cm diff +mode, +this reduces noise due to FreeBSD ID string changes in the output. +During an update this can simplify handling for harmless conflicts caused +by FreeBSD ID string changes. +.Pp +Specifically, +if a file in the destination directory is identical to the same file in the +.Dq previous +tree modulo the FreeBSD ID string, +then the file is treated as if it was unmodified and the +.Dq current +version of the file will be installed. +Similarly, +if a file in the destination directory is identical to the same file in the +.Dq current +tree modulo the FreeBSD ID string, +then the +.Dq current +version of the file will be installed to update the ID string. +If the +.Dq previous +and +.Dq current +versions of the file are identical, +then +.Nm +will not change the file in the destination directory. +.Pp +Due to limitations in the +.Xr diff 1 +command, +this option may not have an effect if there are other changes in a file that +are close to the FreeBSD ID string. +.It Fl I Ar patterns +Ignore any files that match any of the patterns listed in +.Ar patterns . +No warnings or other messages will be generated for those files during a +merge. +Each pattern is evaluated as an +.Xr sh 1 +shell pattern. +This option may be specified multiple times to specify multiple patterns. +Multiple space-separated patterns may also be specified in a single +option. +.It Fl L Ar logfile +Specify an alternate path for the log file. +The +.Nm +utility logs each command that it invokes along with the standard output +and standard error to this file. +By default the log file is stored in a file named +.Pa log +in the work directory. +.It Fl M Ar options +Pass +.Ar options +as additional parameters to +.Xr make 1 +when building a +.Dq current +tree. +This can be used for to set the +.Dv TARGET +or +.Dv TARGET_ARCH +variables for a cross-build. +.It Fl n +Enable +.Dq dry-run +mode. +Do not merge any changes to the destination directory. +Instead, +report what actions would be taken during a merge. +Note that the existing +.Dq current +and +.Dq previous +trees will not be changed. +If the +.Fl r +option is not specified, +then a temporary +.Dq current +tree will be extracted to perform the comparison. +.It Fl r +Do not update the +.Dq current +and +.Dq previous +trees during a merge. +This can be used to +.Dq re-run +a previous merge operation. +.It Fl s Ar source +Specify an alternate source tree to use when building or extracting a +.Dq current +tree. +The default source tree is +.Pa /usr/src . +.It Fl t Ar tarball +Extract a new +.Dq current +tree from a tarball previously generated by the +.Cm build +command rather than building the tree from a source tree. +.El +.Sh CONFIG FILE +The +.Nm +utility can also be configured by setting variables in an optional +configuration file named +.Pa /etc/etcupdate.conf . +Note that command line options override settings in the configuration file. +The configuration file is executed by +.Xr sh 1 , +so it uses that syntax to set configuration variables. +The following variables can be set: +.Bl -tag -width ".Ev ALWAYS_INSTALL" +.It Ev ALWAYS_INSTALL +Always install files that match any of the patterns listed in this variable +similar to the +.Fl A +option. +.It Ev DESTDIR +Specify an alternate destination directory similar to the +.Fl D +option. +.It Ev EDITOR +Specify a program to edit merge conflicts. +.It Ev FREEBSD_ID +Ignore changes in the FreeBSD ID string similar to the +.Fl F +option. +This is enabled by setting the variable to a non-empty value. +.It Ev IGNORE_FILES +Ignore files that match any of the patterns listed in this variable +similar to the +.Fl I +option. +.It Ev LOGFILE +Specify an alternate path for the log file similar to the +.Fl L +option. +.It Ev MAKE_OPTIONS +Pass additional options to +.Xr make 1 +when building a +.Dq current +tree similar to the +.Fl M +option. +.It Ev SRCDIR +Specify an alternate source tree similar to the +.Fl s +option. +.It Ev WORKDIR +Specify an alternate work directory similar to the +.Fl d +option. +.El +.Sh ENVIRONMENT +The +.Nm +utility uses the program identified in the +.Ev EDITOR +environment variable to edit merge conflicts. +If +.Ev EDITOR +is not set, +.Xr vi 1 +is used as the default editor. +.Sh FILES +.Bl -tag -width ".Pa /var/db/etcupdate/log" -compact +.It Pa /etc/etcupdate.conf +Optional config file. +.It Pa /var/db/etcupdate +Default work directory used to store trees and other data. +.It Pa /var/db/etcupdate/log +Default log file. +.El +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +If the source tree matches the currently installed world, +then the following can be used to bootstrap +.Nm +so that it can be used for future upgrades: +.Pp +.Dl "etcupdate extract" +.Pp +To merge changes after an upgrade via the buildworld and installworld process: +.Pp +.Dl "etcupdate" +.Pp +To resolve any conflicts generated during a merge: +.Pp +.Dl "etcupdate resolve" +.Sh DIAGNOSTICS +The following warning messages may be generated during a merge. +Note that several of these warnings cover obscure cases that should occur +rarely if at all in practice. +For example, +if a file changes from a file to a directory in the +.Dq current +tree +and the file was modified in the destination directory, +then a warning will be triggered. +In general, +when a warning references a pathname, +the corresponding file in the destination directory is not changed by a +merge operation. +.Bl -diag +.It "Directory mismatch: <path> (<type>)" +An attempt was made to create a directory at +.Pa path +but an existing file of type +.Dq type +already exists for that path name. +.It "Modified link changed: <file> (<old> became <new>)" +The target of a symbolic link named +.Pa file +was changed from +.Dq old +to +.Dq new +in the +.Dq current +tree. +The symbolic link has been modified to point to a target that is neither +.Dq old +nor +.Dq new +in the destination directory. +.It "Modified mismatch: <file> (<new> vs <dest>)" +A file named +.Pa file +of type +.Dq new +was modified in the +.Dq current +tree, +but the file exists as a different type +.Dq dest +in the destination directory. +.It "Modified <type> changed: <file> (<old> became <new>)" +A file named +.Pa file +changed type from +.Dq old +in the +.Dq previous +tree to type +.Dq new +in the +.Dq current +tree. +The file in the destination directory of type +.Dq type +has been modified, +so it could not be merged automatically. +.It "Modified <type> remains: <file>" +The file of type +.Dq type +named +.Pa file +has been removed from the +.Dq current +tree, +but it has been locally modified. +The modified version of the file remains in the destination directory. +.It "Needs update: /etc/mail/aliases.db (required manual update via newaliases(1))" +The file +.Pa /etc/mail/aliases +was updated during a merge with a non-empty destination directory. +Due to a limitation of the +.Xr newaliases 1 +command, +.Nm +was not able to automatically update the corresponding aliases database. +.It "New file mismatch: <file> (<new> vs <dest>)" +A new file named +.Pa file +of type +.Dq new +has been added to the +.Dq current +tree. +A file of that name already exists in the destination directory, +but it is of a different type +.Dq dest . +.It "New link conflict: <file> (<new> vs <dest>)" +A symbolic link named +.Pa file +has been added to the +.Dq current +tree that links to +.Dq new . +A symbolic link of the same name already exists in the destination +directory, +but it links to a different target +.Dq dest . +.It "Non-empty directory remains: <file>" +The directory +.Pa file +was removed from the +.Dq current +tree, +but it contains additional files in the destination directory. +These additional files as well as the directory remain. +.It "Remove mismatch: <file> (<old> became <new>)" +A file named +.Pa file +changed from type +.Dq old +in the +.Dq previous +tree to type +.Dq new +in the +.Dq current +tree, +but it has been removed in the destination directory. +.It "Removed file changed: <file>" +A file named +.Pa file +was modified in the +.Dq current +tree, +but it has been removed in the destination directory. +.It "Removed link changed: <file> (<old> became <new>)" +The target of a symbolic link named +.Pa file +was changed from +.Dq old +to +.Dq new +in the +.Dq current +tree, +but it has been removed in the destination directory. +.El +.Sh SEE ALSO +.Xr cap_mkdb 1 , +.Xr diff 1 , +.Xr make 1 , +.Xr newaliases 1 , +.Xr sh 1 , +.Xr pwd_mkdb 8 +.\".Sh HISTORY +.Sh AUTHORS +The +.Nm +utility was written by +.An John Baldwin Aq jhb@FreeBSD.org . +.Sh BUGS +Rerunning a merge does not automatically delete conflicts left over from a +previous merge. +Any conflicts must be resolved before the merge can be rerun. +It it is not clear if this is a feature or a bug. +.Pp +There is no way to easily automate conflict resolution for specific files. +For example, one can imagine a syntax along the lines of +.Pp +.Dl "etcupdate resolve tf /some/file" +.Pp +to resolve a specific conflict in an automated fashion. +.Pp +It might be nice to have something like a +.Sq revert +command to replace a locally modified version of a file with the stock +version of the file. +For example: +.Pp +.Dl "etcupdate revert /etc/mail/freebsd.cf" +.Pp +Bootstrapping +.Nm +often results in gratuitous diffs in +.Pa /etc/mail/*.cf +that cause conflicts in the first merge. +If an object tree that matches the source tree is present when bootstrapping, +then passing the +.Fl B +flag to the +.Cm extract +command can work around this. diff --git a/sysutils/etcupdate/src/etcupdate.sh b/sysutils/etcupdate/src/etcupdate.sh new file mode 100644 index 000000000000..a8956f632e00 --- /dev/null +++ b/sysutils/etcupdate/src/etcupdate.sh @@ -0,0 +1,1671 @@ +#!/bin/sh -e +# +# Copyright (c) 2010 Advanced Computing Technologies LLC +# Written by: John H. Baldwin <jhb@FreeBSD.org> +# All rights reserved. +# +# 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 AUTHOR 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 AUTHOR 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. +# +# $FreeBSD$ + +# This is a tool to manage updating files that are not updated as part +# of 'make installworld' such as files in /etc. Unlike other tools, +# this one is specifically tailored to assisting with mass upgrades. +# To that end it does not require user intervention while running. +# +# Theory of operation: +# +# The most reliable way to update changes to files that have local +# modifications is to perform a three-way merge between the original +# unmodified file, the new version of the file, and the modified file. +# This requires having all three versions of the file available when +# performing an update. +# +# To that end, etcupdate uses a strategy where the current unmodified +# tree is kept in WORKDIR/current and the previous unmodified tree is +# kept in WORKDIR/old. When performing a merge, a new tree is built +# if needed and then the changes are merged into DESTDIR. Any files +# with unresolved conflicts after the merge are left in a tree rooted +# at WORKDIR/conflicts. +# +# To provide extra flexibility, etcupdate can also build tarballs of +# root trees that can later be used. It can also use a tarball as the +# source of a new tree instead of building it from /usr/src. + +# Global settings. These can be adjusted by config files and in some +# cases by command line options. + +# TODO: +# - automatable conflict resolution +# - a 'revert' command to make a file "stock" +# - invoke /etc/rc.d/motd if /etc/motd changes? + +usage() +{ + cat <<EOF +usage: etcupdate [-nBF] [-d workdir] [-r | -s source | -t tarball] [-A patterns] + [-D destdir] [-I patterns] [-L logfile] [-M options] + etcupdate build [-B] [-d workdir] [-s source] [-L logfile] [-M options] + <tarball> + etcupdate diff [-d workdir] [-D destdir] [-I patterns] [-L logfile] + etcupdate extract [-B] [-d workdir] [-s source | -t tarball] [-L logfile] + [-M options] + etcupdate resolve [-d workdir] [-D destdir] [-L logfile] + etcupdate status [-d workdir] +EOF + exit 1 +} + +# Used to write a message prepended with '>>>' to the logfile. +log() +{ + echo ">>>" "$@" >&3 +} + +# Used for assertion conditions that should never happen. +panic() +{ + echo "PANIC:" "$@" + exit 10 +} + +# Used to write a warning message. These are saved to the WARNINGS +# file with " " prepended. +warn() +{ + echo -n " " >> $WARNINGS + echo "$@" >> $WARNINGS +} + +# Output a horizontal rule using the passed-in character. Matches the +# length used for Index lines in CVS and SVN diffs. +# +# $1 - character +rule() +{ + jot -b "$1" -s "" 67 +} + +# Output a text description of a specified file's type. +# +# $1 - file pathname. +file_type() +{ + stat -f "%HT" $1 | tr "[:upper:]" "[:lower:]" +} + +# Returns true (0) if a file exists +# +# $1 - file pathname. +exists() +{ + [ -e $1 -o -L $1 ] +} + +# Returns true (0) if a file should be ignored, false otherwise. +# +# $1 - file pathname +ignore() +{ + local pattern - + + set -o noglob + for pattern in $IGNORE_FILES; do + set +o noglob + case $1 in + $pattern) + return 0 + ;; + esac + set -o noglob + done + + # Ignore /.cshrc and /.profile if they are hardlinked to the + # same file in /root. This ensures we only compare those + # files once in that case. + case $1 in + /.cshrc|/.profile) + if [ ${DESTDIR}$1 -ef ${DESTDIR}/root$1 ]; then + return 0 + fi + ;; + *) + ;; + esac + + return 1 +} + +# Returns true (0) if the new version of a file should always be +# installed rather than attempting to do a merge. +# +# $1 - file pathname +always_install() +{ + local pattern - + + set -o noglob + for pattern in $ALWAYS_INSTALL; do + set +o noglob + case $1 in + $pattern) + return 0 + ;; + esac + set -o noglob + done + + return 1 +} + +# Build a new tree +# +# $1 - directory to store new tree in +build_tree() +{ + local make + + make="make $MAKE_OPTIONS" + + log "Building tree at $1 with $make" + mkdir -p $1/usr/obj >&3 2>&1 + (cd $SRCDIR; $make DESTDIR=$1 distrib-dirs) >&3 2>&1 + if [ $? -ne 0 ]; then + echo "Failed to build tree at $1" + return 1 + fi + + if ! [ -n "$nobuild" ]; then + (cd $SRCDIR; \ + MAKEOBJDIRPREFIX=$1/usr/obj $make _obj SUBDIR_OVERRIDE=etc && + MAKEOBJDIRPREFIX=$1/usr/obj $make everything SUBDIR_OVERRIDE=etc && + MAKEOBJDIRPREFIX=$1/usr/obj $make DESTDIR=$1 distribution) >&3 2>&1 + >&3 2>&1 + else + (cd $SRCDIR; $make DESTDIR=$1 distribution) >&3 2>&1 + fi + if [ $? -ne 0 ]; then + echo "Failed to build tree at $1" + return 1 + fi + chflags -R noschg $1 >&3 2>&1 + rm -rf $1/usr/obj >&3 2>&1 + + # Purge auto-generated files. Only the source files need to + # be updated after which these files are regenerated. + rm -f $1/etc/*.db $1/etc/passwd >&3 2>&1 + + # Remove empty files. These just clutter the output of 'diff'. + find $1 -type f -size 0 -delete >&3 2>&1 + + # Trim empty directories. + find -d $1 -type d -empty -delete >&3 2>&1 + return 0 +} + +# Generate a new NEWTREE tree. If tarball is set, then the tree is +# extracted from the tarball. Otherwise the tree is built from a +# source tree. +extract_tree() +{ + # If we have a tarball, extract that into the new directory. + if [ -n "$tarball" ]; then + (mkdir -p $NEWTREE && tar xf $tarball -C $NEWTREE) >&3 2>&1 + if [ $? -ne 0 ]; then + echo "Failed to extract new tree." + remove_tree $NEWTREE + exit 1 + fi + else + if ! build_tree $NEWTREE; then + remove_tree $NEWTREE + exit 1 + fi + fi +} + +# Forcefully remove a tree. Returns true (0) if the operation succeeds. +# +# $1 - path to tree +remove_tree() +{ + + rm -rf $1 >&3 2>&1 + if [ -e $1 ]; then + chflags -R noschg $1 >&3 2>&1 + rm -rf $1 >&3 2>&1 + fi + [ ! -e $1 ] +} + +# Return values for compare() +COMPARE_EQUAL=0 +COMPARE_ONLYFIRST=1 +COMPARE_ONLYSECOND=2 +COMPARE_DIFFTYPE=3 +COMPARE_DIFFLINKS=4 +COMPARE_DIFFFILES=5 + +# Compare two files/directories/symlinks. Note that this does not +# recurse into subdirectories. Instead, if two nodes are both +# directories, they are assumed to be equivalent. +# +# Returns true (0) if the nodes are identical. If only one of the two +# nodes are present, return one of the COMPARE_ONLY* constants. If +# the nodes are different, return one of the COMPARE_DIFF* constants +# to indicate the type of difference. +# +# $1 - first node +# $2 - second node +compare() +{ + local first second + + # If the first node doesn't exist, then check for the second + # node. Note that -e will fail for a symbolic link that + # points to a missing target. + if ! exists $1; then + if exists $2; then + return $COMPARE_ONLYSECOND + else + return $COMPARE_EQUAL + fi + elif ! exists $2; then + return $COMPARE_ONLYFIRST + fi + + # If the two nodes are different file types fail. + first=`stat -f "%Hp" $1` + second=`stat -f "%Hp" $2` + if [ "$first" != "$second" ]; then + return $COMPARE_DIFFTYPE + fi + + # If both are symlinks, compare the link values. + if [ -L $1 ]; then + first=`readlink $1` + second=`readlink $2` + if [ "$first" = "$second" ]; then + return $COMPARE_EQUAL + else + return $COMPARE_DIFFLINKS + fi + fi + + # If both are files, compare the file contents. + if [ -f $1 ]; then + if cmp -s $1 $2; then + return $COMPARE_EQUAL + else + return $COMPARE_DIFFFILES + fi + fi + + # As long as the two nodes are the same type of file, consider + # them equivalent. + return $COMPARE_EQUAL +} + +# Returns true (0) if the only difference between two regular files is a +# change in the FreeBSD ID string. +# +# $1 - path of first file +# $2 - path of second file +fbsdid_only() +{ + + diff -qI '\$FreeBSD.*\$' $1 $2 >/dev/null 2>&1 +} + +# This is a wrapper around compare that will return COMPARE_EQUAL if +# the only difference between two regular files is a change in the +# FreeBSD ID string. It only makes this adjustment if the -F flag has +# been specified. +# +# $1 - first node +# $2 - second node +compare_fbsdid() +{ + local cmp + + compare $1 $2 + cmp=$? + + if [ -n "$FREEBSD_ID" -a "$cmp" -eq $COMPARE_DIFFFILES ] && \ + fbsdid_only $1 $2; then + return $COMPARE_EQUAL + fi + + return $cmp +} + +# Returns true (0) if a directory is empty. +# +# $1 - pathname of the directory to check +empty_dir() +{ + local contents + + contents=`ls -A $1` + [ -z "$contents" ] +} + +# Returns true (0) if one directories contents are a subset of the +# other. This will recurse to handle subdirectories and compares +# individual files in the trees. Its purpose is to quiet spurious +# directory warnings for dryrun invocations. +# +# $1 - first directory (sub) +# $2 - second directory (super) +dir_subset() +{ + local contents file + + if ! [ -d $1 -a -d $2 ]; then + return 1 + fi + + # Ignore files that are present in the second directory but not + # in the first. + contents=`ls -A $1` + for file in $contents; do + if ! compare $1/$file $2/$file; then + return 1 + fi + + if [ -d $1/$file ]; then + if ! dir_subset $1/$file $2/$file; then + return 1 + fi + fi + done + return 0 +} + +# Returns true (0) if a directory in the destination tree is empty. +# If this is a dryrun, then this returns true as long as the contents +# of the directory are a subset of the contents in the old tree +# (meaning that the directory would be empty in a non-dryrun when this +# was invoked) to quiet spurious warnings. +# +# $1 - pathname of the directory to check relative to DESTDIR. +empty_destdir() +{ + + if [ -n "$dryrun" ]; then + dir_subset $DESTDIR/$1 $OLDTREE/$1 + return + fi + + empty_dir $DESTDIR/$1 +} + +# Output a diff of two directory entries with the same relative name +# in different trees. Note that as with compare(), this does not +# recurse into subdirectories. If the nodes are identical, nothing is +# output. +# +# $1 - first tree +# $2 - second tree +# $3 - node name +# $4 - label for first tree +# $5 - label for second tree +diffnode() +{ + local first second file old new diffargs + + if [ -n "$FREEBSD_ID" ]; then + diffargs="-I \\\$FreeBSD.*\\\$" + else + diffargs="" + fi + + compare_fbsdid $1/$3 $2/$3 + case $? in + $COMPARE_EQUAL) + ;; + $COMPARE_ONLYFIRST) + echo + echo "Removed: $3" + echo + ;; + $COMPARE_ONLYSECOND) + echo + echo "Added: $3" + echo + ;; + $COMPARE_DIFFTYPE) + first=`file_type $1/$3` + second=`file_type $2/$3` + echo + echo "Node changed from a $first to a $second: $3" + echo + ;; + $COMPARE_DIFFLINKS) + first=`readlink $1/$file` + second=`readlink $2/$file` + echo + echo "Link changed: $file" + rule "=" + echo "-$first" + echo "+$second" + echo + ;; + $COMPARE_DIFFFILES) + echo "Index: $3" + rule "=" + diff -u $diffargs -L "$3 ($4)" $1/$3 -L "$3 ($5)" $2/$3 + ;; + esac +} + +# Create missing parent directories of a node in a target tree +# preserving the owner, group, and permissions from a specified +# template tree. +# +# $1 - template tree +# $2 - target tree +# $3 - pathname of the node (relative to both trees) +install_dirs() +{ + local args dir + + dir=`dirname $3` + + # Nothing to do if the parent directory exists. This also + # catches the degenerate cases when the path is just a simple + # filename. + if [ -d ${2}$dir ]; then + return 0 + fi + + # If non-directory file exists with the desired directory + # name, then fail. + if exists ${2}$dir; then + # If this is a dryrun and we are installing the + # directory in the DESTDIR and the file in the DESTDIR + # matches the file in the old tree, then fake success + # to quiet spurious warnings. + if [ -n "$dryrun" -a "$2" = "$DESTDIR" ]; then + if compare $OLDTREE/$dir $DESTDIR/$dir; then + return 0 + fi + fi + + args=`file_type ${2}$dir` + warn "Directory mismatch: ${2}$dir ($args)" + return 1 + fi + + # Ensure the parent directory of the directory is present + # first. + if ! install_dirs $1 "$2" $dir; then + return 1 + fi + + # Format attributes from template directory as install(1) + # arguments. + args=`stat -f "-o %Su -g %Sg -m %0Mp%0Lp" $1/$dir` + + log "install -d $args ${2}$dir" + if [ -z "$dryrun" ]; then + install -d $args ${2}$dir >&3 2>&1 + fi + return 0 +} + +# Perform post-install fixups for a file. This largely consists of +# regenerating any files that depend on the newly installed file. +# +# $1 - pathname of the updated file (relative to DESTDIR) +post_install_file() +{ + case $1 in + /etc/mail/aliases) + # Grr, newaliases only works for an empty DESTDIR. + if [ -z "$DESTDIR" ]; then + log "newaliases" + if [ -z "$dryrun" ]; then + newaliases >&3 2>&1 + fi + else + NEWALIAS_WARN=yes + fi + ;; + /etc/login.conf) + log "cap_mkdb ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + cap_mkdb ${DESTDIR}$1 >&3 2>&1 + fi + ;; + /etc/master.passwd) + log "pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1 \ + >&3 2>&1 + fi + ;; + esac +} + +# Install the "new" version of a file. Returns true if it succeeds +# and false otherwise. +# +# $1 - pathname of the file to install (relative to DESTDIR) +install_new() +{ + + if ! install_dirs $NEWTREE "$DESTDIR" $1; then + return 1 + fi + log "cp -Rp ${NEWTREE}$1 ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + cp -Rp ${NEWTREE}$1 ${DESTDIR}$1 >&3 2>&1 + fi + post_install_file $1 + return 0 +} + +# Install the "resolved" version of a file. Returns true if it succeeds +# and false otherwise. +# +# $1 - pathname of the file to install (relative to DESTDIR) +install_resolved() +{ + + # This should always be present since the file is already + # there (it caused a conflict). However, it doesn't hurt to + # just be safe. + if ! install_dirs $NEWTREE "$DESTDIR" $1; then + return 1 + fi + + log "cp -Rp ${CONFLICTS}$1 ${DESTDIR}$1" + cp -Rp ${CONFLICTS}$1 ${DESTDIR}$1 >&3 2>&1 + post_install_file $1 + return 0 +} + +# Generate a conflict file when a "new" file conflicts with an +# existing file in DESTDIR. +# +# $1 - pathname of the file that conflicts (relative to DESTDIR) +new_conflict() +{ + + if [ -n "$dryrun" ]; then + return + fi + + install_dirs $NEWTREE $CONFLICTS $1 + diff --changed-group-format='<<<<<<< (local) +%<======= +%>>>>>>>> (stock) +' $DESTDIR/$1 $NEWTREE/$1 > $CONFLICTS/$1 +} + +# Remove the "old" version of a file. +# +# $1 - pathname of the old file to remove (relative to DESTDIR) +remove_old() +{ + log "rm -f ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + rm -f ${DESTDIR}$1 >&3 2>&1 + fi + echo " D $1" +} + +# Update a file that has no local modifications. +# +# $1 - pathname of the file to update (relative to DESTDIR) +update_unmodified() +{ + local new old + + # If the old file is a directory, then remove it with rmdir + # (this should only happen if the file has changed its type + # from a directory to a non-directory). If the directory + # isn't empty, then fail. This will be reported as a warning + # later. + if [ -d $DESTDIR/$1 ]; then + if empty_destdir $1; then + log "rmdir ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + rmdir ${DESTDIR}$1 >&3 2>&1 + fi + else + return 1 + fi + + # If both the old and new files are regular files, leave the + # existing file. This avoids breaking hard links for /.cshrc + # and /.profile. Otherwise, explicitly remove the old file. + elif ! [ -f ${DESTDIR}$1 -a -f ${NEWTREE}$1 ]; then + log "rm -f ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + rm -f ${DESTDIR}$1 >&3 2>&1 + fi + fi + + # If the new file is a directory, note that the old file has + # been removed, but don't do anything else for now. The + # directory will be installed if needed when new files within + # that directory are installed. + if [ -d $NEWTREE/$1 ]; then + if empty_dir $NEWTREE/$1; then + echo " D $file" + else + echo " U $file" + fi + elif install_new $1; then + echo " U $file" + fi + return 0 +} + +# Update the FreeBSD ID string in a locally modified file to match the +# FreeBSD ID string from the "new" version of the file. +# +# $1 - pathname of the file to update (relative to DESTDIR) +update_freebsdid() +{ + local new dest file + + # If the FreeBSD ID string is removed from the local file, + # there is nothing to do. In this case, treat the file as + # updated. Otherwise, if either file has more than one + # FreeBSD ID string, just punt and let the user handle the + # conflict manually. + new=`grep -c '\$FreeBSD.*\$' ${NEWTREE}$1` + dest=`grep -c '\$FreeBSD.*\$' ${DESTDIR}$1` + if [ "$dest" -eq 0 ]; then + return 0 + fi + if [ "$dest" -ne 1 -o "$dest" -ne 1 ]; then + return 1 + fi + + # If the FreeBSD ID string in the new file matches the FreeBSD ID + # string in the local file, there is nothing to do. + new=`grep '\$FreeBSD.*\$' ${NEWTREE}$1` + dest=`grep '\$FreeBSD.*\$' ${DESTDIR}$1` + if [ "$new" = "$dest" ]; then + return 0 + fi + + # Build the new file in three passes. First, copy all the + # lines preceding the FreeBSD ID string from the local version + # of the file. Second, append the FreeBSD ID string line from + # the new version. Finally, append all the lines after the + # FreeBSD ID string from the local version of the file. + file=`mktemp $WORKDIR/etcupdate-XXXXXXX` + awk '/\$FreeBSD.*\$/ { exit } { print }' ${DESTDIR}$1 >> $file + awk '/\$FreeBSD.*\$/ { print }' ${NEWTREE}$1 >> $file + awk '/\$FreeBSD.*\$/ { ok = 1; next } { if (ok) print }' \ + ${DESTDIR}$1 >> $file + + # As an extra sanity check, fail the attempt if the updated + # version of the file has any differences aside from the + # FreeBSD ID string. + if ! fbsdid_only ${DESTDIR}$1 $file; then + rm -f $file + return 1 + fi + + log "cp $file ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + cp $file ${DESTDIR}$1 >&3 2>&1 + fi + rm -f $file + post_install_file $1 + echo " M $1" + return 0 +} + +# Attempt to update a file that has local modifications. This routine +# only handles regular files. If the 3-way merge succeeds without +# conflicts, the updated file is installed. If the merge fails, the +# merged version with conflict markers is left in the CONFLICTS tree. +# +# $1 - pathname of the file to merge (relative to DESTDIR) +merge_file() +{ + local res + + # Try the merge to see if there is a conflict. + merge -q -p ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 >/dev/null 2>&3 + res=$? + case $res in + 0) + # No conflicts, so just redo the merge to the + # real file. + log "merge ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1" + if [ -z "$dryrun" ]; then + merge ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 + fi + post_install_file $1 + echo " M $1" + ;; + 1) + # Conflicts, save a version with conflict markers in + # the conflicts directory. + if [ -z "$dryrun" ]; then + install_dirs $NEWTREE $CONFLICTS $1 + log "cp -Rp ${DESTDIR}$1 ${CONFLICTS}$1" + cp -Rp ${DESTDIR}$1 ${CONFLICTS}$1 >&3 2>&1 + merge -A -q -L "yours" -L "original" -L "new" \ + ${CONFLICTS}$1 ${OLDTREE}$1 ${NEWTREE}$1 + fi + echo " C $1" + ;; + *) + panic "merge failed with status $res" + ;; + esac +} + +# Returns true if a file contains conflict markers from a merge conflict. +# +# $1 - pathname of the file to resolve (relative to DESTDIR) +has_conflicts() +{ + + egrep -q '^(<{7}|\|{7}|={7}|>{7}) ' $CONFLICTS/$1 +} + +# Attempt to resolve a conflict. The user is prompted to choose an +# action for each conflict. If the user edits the file, they are +# prompted again for an action. The process is very similar to +# resolving conflicts after an update or merge with Perforce or +# Subversion. The prompts are modelled on a subset of the available +# commands for resolving conflicts with Subversion. +# +# $1 - pathname of the file to resolve (relative to DESTDIR) +resolve_conflict() +{ + local command junk + + echo "Resolving conflict in '$1':" + edit= + while true; do + # Only display the resolved command if the file + # doesn't contain any conflicts. + echo -n "Select: (p) postpone, (df) diff-full, (e) edit," + if ! has_conflicts $1; then + echo -n " (r) resolved," + fi + echo + echo -n " (h) help for more options: " + read command + case $command in + df) + diff -u ${DESTDIR}$1 ${CONFLICTS}$1 + ;; + e) + $EDITOR ${CONFLICTS}$1 + ;; + h) + cat <<EOF + (p) postpone - ignore this conflict for now + (df) diff-full - show all changes made to merged file + (e) edit - change merged file in an editor + (r) resolved - accept merged version of file + (mf) mine-full - accept local version of entire file (ignore new changes) + (tf) theirs-full - accept new version of entire file (lose local changes) + (h) help - show this list +EOF + ;; + mf) + # For mine-full, just delete the + # merged file and leave the local + # version of the file as-is. + rm ${CONFLICTS}$1 + return + ;; + p) + return + ;; + r) + # If the merged file has conflict + # markers, require confirmation. + if has_conflicts $1; then + echo "File '$1' still has conflicts," \ + "are you sure? (y/n) " + read junk + if [ "$junk" != "y" ]; then + continue + fi + fi + + if ! install_resolved $1; then + panic "Unable to install merged" \ + "version of $1" + fi + rm ${CONFLICTS}$1 + return + ;; + tf) + # For theirs-full, install the new + # version of the file over top of the + # existing file. + if ! install_new $1; then + panic "Unable to install new" \ + "version of $1" + fi + rm ${CONFLICTS}$1 + return + ;; + *) + echo "Invalid command." + ;; + esac + done +} + +# Handle a file that has been removed from the new tree. If the file +# does not exist in DESTDIR, then there is nothing to do. If the file +# exists in DESTDIR and is identical to the old version, remove it +# from DESTDIR. Otherwise, whine about the conflict but leave the +# file in DESTDIR. To handle directories, this uses two passes. The +# first pass handles all non-directory files. The second pass handles +# just directories and removes them if they are empty. +# +# If -F is specified, and the only difference in the file in DESTDIR +# is a change in the FreeBSD ID string, then remove the file. +# +# $1 - pathname of the file (relative to DESTDIR) +handle_removed_file() +{ + local dest file + + file=$1 + if ignore $file; then + log "IGNORE: removed file $file" + return + fi + + compare_fbsdid $DESTDIR/$file $OLDTREE/$file + case $? in + $COMPARE_EQUAL) + if ! [ -d $DESTDIR/$file ]; then + remove_old $file + fi + ;; + $COMPARE_ONLYFIRST) + panic "Removed file now missing" + ;; + $COMPARE_ONLYSECOND) + # Already removed, nothing to do. + ;; + $COMPARE_DIFFTYPE|$COMPARE_DIFFLINKS|$COMPARE_DIFFFILES) + dest=`file_type $DESTDIR/$file` + warn "Modified $dest remains: $file" + ;; + esac +} + +# Handle a directory that has been removed from the new tree. Only +# remove the directory if it is empty. +# +# $1 - pathname of the directory (relative to DESTDIR) +handle_removed_directory() +{ + local dir + + dir=$1 + if ignore $dir; then + log "IGNORE: removed dir $dir" + return + fi + + if [ -d $DESTDIR/$dir -a -d $OLDTREE/$dir ]; then + if empty_destdir $dir; then + log "rmdir ${DESTDIR}$dir" + if [ -z "$dryrun" ]; then + rmdir ${DESTDIR}$dir >/dev/null 2>&1 + fi + echo " D $dir" + else + warn "Non-empty directory remains: $dir" + fi + fi +} + +# Handle a file that exists in both the old and new trees. If the +# file has not changed in the old and new trees, there is nothing to +# do. If the file in the destination directory matches the new file, +# there is nothing to do. If the file in the destination directory +# matches the old file, then the new file should be installed. +# Everything else becomes some sort of conflict with more detailed +# handling. +# +# $1 - pathname of the file (relative to DESTDIR) +handle_modified_file() +{ + local cmp dest file new newdestcmp old + + file=$1 + if ignore $file; then + log "IGNORE: modified file $file" + return + fi + + compare $OLDTREE/$file $NEWTREE/$file + cmp=$? + if [ $cmp -eq $COMPARE_EQUAL ]; then + return + fi + + if [ $cmp -eq $COMPARE_ONLYFIRST -o $cmp -eq $COMPARE_ONLYSECOND ]; then + panic "Changed file now missing" + fi + + compare $NEWTREE/$file $DESTDIR/$file + newdestcmp=$? + if [ $newdestcmp -eq $COMPARE_EQUAL ]; then + return + fi + + # If the only change in the new file versus the destination + # file is a change in the FreeBSD ID string and -F is + # specified, just install the new file. + if [ -n "$FREEBSD_ID" -a $newdestcmp -eq $COMPARE_DIFFFILES ] && \ + fbsdid_only $NEWTREE/$file $DESTDIR/$file; then + if update_unmodified $file; then + return + else + panic "Updating FreeBSD ID string failed" + fi + fi + + # If the local file is the same as the old file, install the + # new file. If -F is specified and the only local change is + # in the FreeBSD ID string, then install the new file as well. + if compare_fbsdid $OLDTREE/$file $DESTDIR/$file; then + if update_unmodified $file; then + return + fi + fi + + # If the only change in the new file versus the old file is a + # change in the FreeBSD ID string and -F is specified, just + # update the FreeBSD ID string in the local file. + if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \ + fbsdid_only $OLDTREE/$file $NEWTREE/$file; then + if update_freebsdid $file; then + continue + fi + fi + + # If the file was removed from the dest tree, just whine. + if [ $newdestcmp -eq $COMPARE_ONLYFIRST ]; then + # If the removed file matches an ALWAYS_INSTALL glob, + # then just install the new version of the file. + if always_install $file; then + log "ALWAYS: adding $file" + if ! [ -d $NEWTREE/$file ]; then + if install_new $file; then + echo " A $file" + fi + fi + return + fi + + case $cmp in + $COMPARE_DIFFTYPE) + old=`file_type $OLDTREE/$file` + new=`file_type $NEWTREE/$file` + warn "Remove mismatch: $file ($old became $new)" + ;; + $COMPARE_DIFFLINKS) + old=`readlink $OLDTREE/$file` + new=`readlink $NEWTREE/$file` + warn \ + "Removed link changed: $file (\"$old\" became \"$new\")" + ;; + $COMPARE_DIFFFILES) + warn "Removed file changed: $file" + ;; + esac + return + fi + + # Treat the file as unmodified and force install of the new + # file if it matches an ALWAYS_INSTALL glob. If the update + # attempt fails, then fall through to the normal case so a + # warning is generated. + if always_install $file; then + log "ALWAYS: updating $file" + if update_unmodified $file; then + return + fi + fi + + # If the file changed types between the old and new trees but + # the files in the new and dest tree are both of the same + # type, treat it like an added file just comparing the new and + # dest files. + if [ $cmp -eq $COMPARE_DIFFTYPE ]; then + case $newdestcmp in + $COMPARE_DIFFLINKS) + new=`readlink $NEWTREE/$file` + dest=`readlink $DESTDIR/$file` + warn \ + "New link conflict: $file (\"$new\" vs \"$dest\")" + return + ;; + $COMPARE_DIFFFILES) + new_conflict $file + echo " C $file" + return + ;; + esac + else + # If the file has not changed types between the old + # and new trees, but it is a different type in + # DESTDIR, then just warn. + if [ $newdestcmp -eq $COMPARE_DIFFTYPE ]; then + new=`file_type $NEWTREE/$file` + dest=`file_type $DESTDIR/$file` + warn "Modified mismatch: $file ($new vs $dest)" + return + fi + fi + + case $cmp in + $COMPARE_DIFFTYPE) + old=`file_type $OLDTREE/$file` + new=`file_type $NEWTREE/$file` + dest=`file_type $DESTDIR/$file` + warn "Modified $dest changed: $file ($old became $new)" + ;; + $COMPARE_DIFFLINKS) + old=`readlink $OLDTREE/$file` + new=`readlink $NEWTREE/$file` + warn \ + "Modified link changed: $file (\"$old\" became \"$new\")" + ;; + $COMPARE_DIFFFILES) + merge_file $file + ;; + esac +} + +# Handle a file that has been added in the new tree. If the file does +# not exist in DESTDIR, simply copy the file into DESTDIR. If the +# file exists in the DESTDIR and is identical to the new version, do +# nothing. Otherwise, generate a diff of the two versions of the file +# and mark it as a conflict. +# +# $1 - pathname of the file (relative to DESTDIR) +handle_added_file() +{ + local cmp dest file new + + file=$1 + if ignore $file; then + log "IGNORE: added file $file" + return + fi + + compare $DESTDIR/$file $NEWTREE/$file + cmp=$? + case $cmp in + $COMPARE_EQUAL) + return + ;; + $COMPARE_ONLYFIRST) + panic "Added file now missing" + ;; + $COMPARE_ONLYSECOND) + # Ignore new directories. They will be + # created as needed when non-directory nodes + # are installed. + if ! [ -d $NEWTREE/$file ]; then + if install_new $file; then + echo " A $file" + fi + fi + return + ;; + esac + + + # Treat the file as unmodified and force install of the new + # file if it matches an ALWAYS_INSTALL glob. If the update + # attempt fails, then fall through to the normal case so a + # warning is generated. + if always_install $file; then + log "ALWAYS: updating $file" + if update_unmodified $file; then + return + fi + fi + + case $cmp in + $COMPARE_DIFFTYPE) + new=`file_type $NEWTREE/$file` + dest=`file_type $DESTDIR/$file` + warn "New file mismatch: $file ($new vs $dest)" + ;; + $COMPARE_DIFFLINKS) + new=`readlink $NEWTREE/$file` + dest=`readlink $DESTDIR/$file` + warn "New link conflict: $file (\"$new\" vs \"$dest\")" + ;; + $COMPARE_DIFFFILES) + # If the only change in the new file versus + # the destination file is a change in the + # FreeBSD ID string and -F is specified, just + # install the new file. + if [ -n "$FREEBSD_ID" ] && \ + fbsdid_only $NEWTREE/$file $DESTDIR/$file; then + if update_unmodified $file; then + return + else + panic \ + "Updating FreeBSD ID string failed" + fi + fi + + new_conflict $file + echo " C $file" + ;; + esac +} + +# Main routines for each command + +# Build a new tree and save it in a tarball. +build_cmd() +{ + local dir + + if [ $# -ne 1 ]; then + echo "Missing required tarball." + echo + usage + fi + + log "build command: $1" + + # Create a temporary directory to hold the tree + dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX` + if [ $? -ne 0 ]; then + echo "Unable to create temporary directory." + exit 1 + fi + if ! build_tree $dir; then + remove_tree $dir + exit 1 + fi + if ! tar cfj $1 -C $dir . >&3 2>&1; then + echo "Failed to create tarball ." + remove_tree $dir + exit 1 + fi + remove_tree $dir +} + +# Output a diff comparing the tree at DESTDIR to the current +# unmodified tree. Note that this diff does not include files that +# are present in DESTDIR but not in the unmodified tree. +diff_cmd() +{ + local file + + if [ $# -ne 0 ]; then + usage + fi + + # Requires an unmodified tree to diff against. + if ! [ -d $NEWTREE ]; then + echo "Reference tree to diff against unavailable." + exit 1 + fi + + # Unfortunately, diff alone does not quite provide the right + # level of options that we want, so improvise. + for file in `(cd $NEWTREE; find .) | sed -e 's/^\.//'`; do + if ignore $file; then + continue + fi + + diffnode $NEWTREE "$DESTDIR" $file "stock" "local" + done +} + +# Just extract a new tree into NEWTREE either by building a tree or +# extracting a tarball. This can be used to bootstrap updates by +# initializing the current "stock" tree to match the currently +# installed system. +# +# Unlike 'update', this command does not rotate or preserve an +# existing NEWTREE, it just replaces any existing tree. +extract_cmd() +{ + + if [ $# -ne 0 ]; then + usage + fi + + log "extract command: tarball=$tarball" + + if [ -d $NEWTREE ]; then + if ! remove_tree $NEWTREE; then + echo "Unable to remove current tree." + exit 1 + fi + fi + + extract_tree +} + +# Resolve conflicts left from an earlier merge. +resolve_cmd() +{ + local conflicts + + if [ $# -ne 0 ]; then + usage + fi + + if ! [ -d $CONFLICTS ]; then + return + fi + + conflicts=`(cd $CONFLICTS; find . ! -type d) | sed -e 's/^\.//'` + for file in $conflicts; do + resolve_conflict $file + done + + if [ -n "$NEWALIAS_WARN" ]; then + warn "Needs update: /etc/mail/aliases.db" \ + "(requires manual update via newaliases(1))" + echo + echo "Warnings:" + echo " Needs update: /etc/mail/aliases.db" \ + "(requires manual update via newaliases(1))" + fi +} + +# Report a summary of the previous merge. Specifically, list any +# remaining conflicts followed by any warnings from the previous +# update. +status_cmd() +{ + + if [ $# -ne 0 ]; then + usage + fi + + if [ -d $CONFLICTS ]; then + (cd $CONFLICTS; find . ! -type d) | sed -e 's/^\./ C /' + fi + if [ -s $WARNINGS ]; then + echo "Warnings:" + cat $WARNINGS + fi +} + +# Perform an actual merge. The new tree can either already exist (if +# rerunning a merge), be extracted from a tarball, or generated from a +# source tree. +update_cmd() +{ + local dir + + if [ $# -ne 0 ]; then + usage + fi + + log "update command: rerun=$rerun tarball=$tarball" + + if [ `id -u` -ne 0 ]; then + echo "Must be root to update a tree." + exit 1 + fi + + # Enforce a sane umask + umask 022 + + # XXX: Should existing conflicts be ignored and removed during + # a rerun? + + # Trim the conflicts tree. Whine if there is anything left. + if [ -e $CONFLICTS ]; then + find -d $CONFLICTS -type d -empty -delete >&3 2>&1 + rmdir $CONFLICTS >&3 2>&1 + fi + if [ -d $CONFLICTS ]; then + echo "Conflicts remain from previous update, aborting." + exit 1 + fi + + if [ -z "$rerun" ]; then + # For a dryrun that is not a rerun, do not rotate the existing + # stock tree. Instead, extract a tree to a temporary directory + # and use that for the comparison. + if [ -n "$dryrun" ]; then + dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX` + if [ $? -ne 0 ]; then + echo "Unable to create temporary directory." + exit 1 + fi + OLDTREE=$NEWTREE + NEWTREE=$dir + + # Rotate the existing stock tree to the old tree. + elif [ -d $NEWTREE ]; then + # First, delete the previous old tree if it exists. + if ! remove_tree $OLDTREE; then + echo "Unable to remove old tree." + exit 1 + fi + + # Move the current stock tree. + if ! mv $NEWTREE $OLDTREE >&3 2>&1; then + echo "Unable to rename current stock tree." + exit 1 + fi + fi + + if ! [ -d $OLDTREE ]; then + cat <<EOF +No previous tree to compare against, a sane comparison is not possible. +EOF + log "No previous tree to compare against." + if [ -n "$dir" ]; then + rmdir $dir + fi + exit 1 + fi + + # Populate the new tree. + extract_tree + fi + + # Build lists of nodes in the old and new trees. + (cd $OLDTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/old.files + (cd $NEWTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/new.files + + # Split the files up into three groups using comm. + comm -23 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/removed.files + comm -13 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/added.files + comm -12 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/both.files + + # Initialize conflicts and warnings handling. + rm -f $WARNINGS + mkdir -p $CONFLICTS + + # The order for the following sections is important. In the + # odd case that a directory is converted into a file, the + # existing subfiles need to be removed if possible before the + # file is converted. Similarly, in the case that a file is + # converted into a directory, the file needs to be converted + # into a directory if possible before the new files are added. + + # First, handle removed files. + for file in `cat $WORKDIR/removed.files`; do + handle_removed_file $file + done + + # For the directory pass, reverse sort the list to effect a + # depth-first traversal. This is needed to ensure that if a + # directory with subdirectories is removed, the entire + # directory is removed if there are no local modifications. + for file in `sort -r $WORKDIR/removed.files`; do + handle_removed_directory $file + done + + # Second, handle files that exist in both the old and new + # trees. + for file in `cat $WORKDIR/both.files`; do + handle_modified_file $file + done + + # Finally, handle newly added files. + for file in `cat $WORKDIR/added.files`; do + handle_added_file $file + done + + if [ -n "$NEWALIAS_WARN" ]; then + warn "Needs update: /etc/mail/aliases.db" \ + "(requires manual update via newaliases(1))" + fi + + if [ -s $WARNINGS ]; then + echo "Warnings:" + cat $WARNINGS + fi + + if [ -n "$dir" ]; then + if [ -z "$dryrun" -o -n "$rerun" ]; then + panic "Should not have a temporary directory" + fi + + remove_tree $dir + fi +} + +# Determine which command we are executing. A command may be +# specified as the first word. If one is not specified then 'update' +# is assumed as the default command. +command="update" +if [ $# -gt 0 ]; then + case "$1" in + build|diff|extract|status|resolve) + command="$1" + shift + ;; + -*) + # If first arg is an option, assume the + # default command. + ;; + *) + usage + ;; + esac +fi + +# Set default variable values. + +# Where the "old" and "new" trees are stored. +WORKDIR=/var/db/etcupdate + +# The path to the source tree used to build trees. +SRCDIR=/usr/src + +# The destination directory where the modified files live. +DESTDIR= + +# Ignore changes in the FreeBSD ID string. +FREEBSD_ID= + +# Files that should always have the new version of the file installed. +ALWAYS_INSTALL= + +# Files to ignore and never update during a merge. +IGNORE_FILES= + +# Flags to pass to 'make' when building a tree. +MAKE_OPTIONS= + +# Include a config file if it exists. Note that command line options +# override any settings in the config file. More details are in the +# manual, but in general the following variables can be set: +# - ALWAYS_INSTALL +# - DESTDIR +# - EDITOR +# - FREEBSD_ID +# - IGNORE_FILES +# - LOGFILE +# - MAKE_OPTIONS +# - SRCDIR +# - WORKDIR +if [ -r /etc/etcupdate.conf ]; then + . /etc/etcupdate.conf +fi + +# Parse command line options +tarball= +rerun= +always= +dryrun= +ignore= +nobuild= +while getopts "d:nrs:t:A:BD:FI:L:M:" option; do + case "$option" in + d) + WORKDIR=$OPTARG + ;; + n) + dryrun=YES + ;; + r) + rerun=YES + ;; + s) + SRCDIR=$OPTARG + ;; + t) + tarball=$OPTARG + ;; + A) + # To allow this option to be specified + # multiple times, accumulate command-line + # specified patterns in an 'always' variable + # and use that to overwrite ALWAYS_INSTALL + # after parsing all options. Need to be + # careful here with globbing expansion. + set -o noglob + always="$always $OPTARG" + set +o noglob + ;; + B) + nobuild=YES + ;; + D) + DESTDIR=$OPTARG + ;; + F) + FREEBSD_ID=YES + ;; + I) + # To allow this option to be specified + # multiple times, accumulate command-line + # specified patterns in an 'ignore' variable + # and use that to overwrite IGNORE_FILES after + # parsing all options. Need to be careful + # here with globbing expansion. + set -o noglob + ignore="$ignore $OPTARG" + set +o noglob + ;; + L) + LOGFILE=$OPTARG + ;; + M) + MAKE_OPTIONS="$OPTARG" + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) + +# Allow -A command line options to override ALWAYS_INSTALL set from +# the config file. +set -o noglob +if [ -n "$always" ]; then + ALWAYS_INSTALL="$always" +fi + +# Allow -I command line options to override IGNORE_FILES set from the +# config file. +if [ -n "$ignore" ]; then + IGNORE_FILES="$ignore" +fi +set +o noglob + +# Log file for verbose output from program that are run. The log file +# is opened on fd '3'. +LOGFILE=${LOGFILE:-$WORKDIR/log} + +# The path of the "old" tree +OLDTREE=$WORKDIR/old + +# The path of the "new" tree +NEWTREE=$WORKDIR/current + +# The path of the "conflicts" tree where files with merge conflicts are saved. +CONFLICTS=$WORKDIR/conflicts + +# The path of the "warnings" file that accumulates warning notes from an update. +WARNINGS=$WORKDIR/warnings + +# Use $EDITOR for resolving conflicts. If it is not set, default to vi. +EDITOR=${EDITOR:-/usr/bin/vi} + +# Handle command-specific argument processing such as complaining +# about unsupported options. Since the configuration file is always +# included, do not complain about extra command line arguments that +# may have been set via the config file rather than the command line. +case $command in + update) + if [ -n "$rerun" -a -n "$tarball" ]; then + echo "Only one of -r or -t can be specified." + echo + usage + fi + ;; + build|diff|resolve|status) + if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" ]; then + usage + fi + ;; + extract) + if [ -n "$dryrun" -o -n "$rerun" ]; then + usage + fi + ;; +esac + +# Open the log file. Don't truncate it if doing a minor operation so +# that a minor operation doesn't lose log info from a major operation. +if ! mkdir -p $WORKDIR 2>/dev/null; then + echo "Failed to create work directory $WORKDIR" +fi + +case $command in + diff|resolve|status) + exec 3>>$LOGFILE + ;; + *) + exec 3>$LOGFILE + ;; +esac + +${command}_cmd "$@" |