Level: Intermediate Dan Poirier (poirier@us.ibm.com), Software engineer, IBM
01 Feb 2002 RPM is a widely-used tool for delivering software for Linux. Users can easily install an RPM-packaged product. In this article, third in a series, Dan explains how to run scripts when your package is installed or uninstalled, or when other packages are installed or uninstalled.
It's often useful to be able to execute
commands when your program is installed or uninstalled on a user's
machine. For example, you might need to edit a system configuration file
to enable a new service, or need to define a new user to own the program
that you're installing. To get the most out of the advice and examples in this article, take a look at Part 1 and Part 2 of this series, which show you how to use RPM and how to distribute your work, respectively. How install and uninstall scripts work
Install and uninstall scripts seem simple, but a few surprises
in how they work can cause big problems. Here are the basics. You can add any of the following four sections to
your .spec file, which lists shell scripts to run at various points in the
install life of your package:
-
%pre
- Runs before the package is installed
-
%post
- Runs after the package is installed
-
%preun
- Runs before the package is uninstalled
-
%postun
- Runs after the package is uninstalled
In particular, note the difference between %install and these sections. %install runs on your development machine when you
are building the RPM; it should install the product on your development
machine, or into a build root. These sections, on the other hand, specify
what will run on the user's machine when the user is installing or
uninstalling your RPM package. Here's an example, building on the previous articles. We want to use
install-info to add GNU indent's info file to
the directory. Listing 1. indent-4.spec
# Simplistic example of install scripts - do not use
Summary: GNU indent
Name: indent
Version: 2.2.6
Release: 4
Source0: %{name}-%{version}.tar.gz
License: GPL
Group: Development/Tools
BuildRoot: %{_builddir}/%{name}-root
%description
The GNU indent program reformats C code to any of a variety of
formatting standards, or you can define your own.
%prep
%setup -q
%build
./configure
make
%install
rm -rf $RPM_BUILD_ROOT
make DESTDIR=$RPM_BUILD_ROOT install
%post
if [ -x /sbin/install-info ]; then
/sbin/install-info /usr/local/info/indent.info /usr/local/info/dir
fi
%preun
if [ -x /sbin/install-info ]; then
/sbin/install-info --delete /usr/local/info/indent.info /usr/local/info/dir
fi
%clean
rm -rf $RPM_BUILD_ROOT
%files
%defattr(-,root,root)
/usr/local/bin/indent
%doc /usr/local/info/indent.info
%doc %attr(0444,root,root) /usr/local/man/man1/indent.1
%doc COPYING AUTHORS README NEWS
|
Note first that we check for the install-info tool before trying to use it. We don't
want our install to fail just because we cannot provide a link to the
product documentation. But there might be cases where you would like an installation to
fail. A good technique is to use the %pre
script to check for installation prerequisites that are more complex than
what RPM can support directly. If the prerequisites are not met, the
script exits with a non-zero status, and RPM does not continue with
the installation. Notice also that we are careful to undo our install script
using an uninstall script.
Not so simple: upgrades complicate everything
Now let's turn to upgrades. The instructions in the
previous section work fine if users only install and delete your package;
they fail badly during upgrades. Here's how RPM performs an upgrade:
- Run %pre of new package
- Install new files
- Run %post of new package
- Run %preun of old package
- Delete any old files not overwritten by newer ones
- Run %postun of old package
If we do an upgrade using our previous example, the last thing that RPM
runs is the %postun script, which removes all the work we did in our
install script! This is not exactly what the average RPM-using developer would have
guessed. I won't try to explain the reasons for this, just what you have
to do about it. Luckily, there is a way for a script to tell if the package is being
installed, deleted, or upgraded -- sort of. Each script is passed a
single command-line argument, a number. This is supposed to tell the
script how many copies of the package will be installed after the
current package has finished installing or uninstalling. Rather than trying to figure this out, it's probably easier to just
look at the values that get passed in various cases. Here are the actual values passed during an install:
- Run %pre of new package (1)
- Install new files
- Run %post of new package (1)
Here are the values passed during an upgrade:
- Run %pre of new package (2)
- Install new files
- Run %post of new package (2)
- Run %preun of old package (1)
- Delete any old files not overwritten by newer ones
- Run %postun of old package (1)
Here are the values passed during a delete:
- Run %preun of old package (0)
- Delete files
- Run %postun of old package (0)
You can test this yourself by adding something like the next example to
your package. Then make a new package with a slightly higher release
number, and you can install the first, then upgrade to the second, and
finally uninstall it, to see all the possibilities. And of course, you
will always want to try this several times yourself before releasing any
RPM on an unsuspecting public. Listing 2. Test order and arguments for script execution
%pre
echo This is pre for %{version}-%{release}: arg=$1
%post
echo This is post for %{version}-%{release}: arg=$1
%preun
echo This is preun for %{version}-%{release}: arg=$1
%postun
echo This is postun for %{version}-%{release}: arg=$1
|
Here's another example, this time coping correctly with the upgrade
process: Listing 3. indent-5.spec
Summary: GNU indent
Name: indent
Version: 2.2.6
Release: 5
Source0: %{name}-%{version}.tar.gz
License: GPL
Group: Development/Tools
BuildRoot: %{_builddir}/%{name}-root
%description
The GNU indent program reformats C code to any of a variety of
formatting standards, or you can define your own.
%prep
%setup -q
%build
./configure
make
%install
rm -rf $RPM_BUILD_ROOT
make DESTDIR=$RPM_BUILD_ROOT install
%post
if [ "$1" = "1" ] ; then # first install
if [ -x /sbin/install-info ]; then
/sbin/install-info /usr/local/info/indent.info /usr/local/info/dir
fi
fi
%preun
if [ "$1" = "0" ] ; then # last uninstall
if [ -x /sbin/install-info ]; then
/sbin/install-info --delete /usr/local/info/indent.info /usr/local/info/dir
fi
fi
%clean
rm -rf $RPM_BUILD_ROOT
%files
%defattr(-,root,root)
/usr/local/bin/indent
%doc /usr/local/info/indent.info
%doc %attr(0444,root,root) /usr/local/man/man1/indent.1
%doc COPYING AUTHORS README NEWS
|
Now the info link will be removed only when the package is being
completely deleted.
Triggers -- running scripts when other packages are installed or uninstalled
Suppose you want to run some code in your package when other
packages are installed or uninstalled. You can do it with trigger
scripts. Why would you want to do this? Usually because your package uses the
services of one or more other packages, or provides services to one or
more other packages. Here's an example. Suppose you're packaging up a nifty add-on tool for
the Emacs and Xemacs editors. It can work with either or both, but you
need to do some minor configuration depending on which editors are
installed. At install time, you can test for Emacs and Xemacs and configure your
tool to be accessible by the available editors. But what happens if the
user installs Xemacs later? Your tool won't be usable in Xemacs unless
the user uninstalls and reinstalls your tool. Wouldn't it be better if
your package could tell RPM, "let me know if Xemacs gets installed"? That's the idea of trigger scripts. You can add this to your .spec
file: Listing 4. Trigger example
%triggerin -- emacs
# Insert code here to run if your package is already installed,
# then emacs is installed,
# OR if emacs is already installed, then your package is installed
%triggerin -- xemacs
# Insert code here to run if your package is already installed,
# then xemacs is installed,
# OR if xemacs is already installed, then your package is installed
%triggerun -- emacs
# insert code here to run if your package is already installed,
# then emacs is uninstalled
%triggerun -- xemacs
# insert code here to run if your package is already installed,
# then xemacs is uninstalled
%postun
# Insert code here to run if your package is uninstalled
|
Trigger scripts are passed two arguments. The first argument is
the number of instances of your package that will be installed when
your trigger script has completed running. The second argument is the
number of instances of the triggering package that will be
installed when your trigger script has completed running. From the triggers file in the RPM
distribution, here's the complete ordering of script executions and file
installs and uninstalls during an RPM upgrade: Listing 5. Script ordering
new-%pre for new version of package being installed
... (all new files are installed)
new-%post for new version of package being installed
any-%triggerin (%triggerin from other packages set off by new install)
new-%triggerin
old-%triggerun
any-%triggerun (%triggerun from other packages set off by old uninstall)
old-%preun for old version of package being removed
... (all old files are removed)
old-%postun for old version of package being removed
old-%triggerpostun
any-%triggerpostun (%triggerpostun from other packages set off by old un
install)
|
Advanced scripting Alternate interpreters
Ordinarily, all install-time scripts and trigger scripts are run using the
/bin/sh shell program. If you prefer another
scripting language, say Perl, you can tell RPM that your script should be
run using another interpreter by adding -p
interpreter to the script line. For example: Listing 6. Example of alternate interpreters
%post -p /usr/bin/perl
# Perl script here
%triggerun -p /usr/bin/perl -- xemacs
# Another Perl script here
|
Note that this does not apply to the build-time scripts
of RPM, such as %install. RPM variables
RPM expands RPM variables in your scripts before storing them into the RPM
package file, which can be useful sometimes. For example, you can define
your own variables near the top of your .spec file, and then refer to them
using %{variable_name} all through your .spec
file -- and even in your scripts: Listing 7. Example of RPM variables
...
%define foo_dir /usr/lib/foo
...
%install
cp install.time.message $RPM_BUILD_ROOT/%{foo_dir}
%files
%{foo_dir}/install.time.message
%post
/bin/cat %{foo_dir}/install.time.message
|
Things to avoid
You might be tempted to do some things at install time that turn out to be
a bad idea. For example, any attempt to interact with the user probably
won't work very well. RPM is designed to allow batch installs, during
which no user is necessarily present. If an RPM package stops during an install
to ask a question, and no one sees the question, the
install will appear to just hang forever. Another thing you probably want to avoid is starting any services.
During a complete installation, you can't be sure if everything that your
program needs is there already (for example, there might not be any
network yet); also, if every RPM service tried to start during a complete
operating system install, the entire install would probably take
significantly longer. What you can do in such cases is print a message telling the user about
any required configuration or service that needs to be started. If a user
is installing your RPM package manually, he or she will see it; if it's part of a
larger batch install, it won't hurt anything, and the machine will almost
certainly be rebooted at the end, starting your service.
Things to remember
If your package installs an init script, you can use chkconfig to arrange for your service to be started
and stopped in the appropriate run-levels. Though you can accomplish the
same thing by installing the necessary symbolic links directly as part of
your package, getting them right is a pesky detail that you might prefer
to leave to chkconfig. A number of services run under a specific user id for security; if
yours does, you will need to create that user on the system if it doesn't
already exist. If your package installs any GNU info files, they won't be visible in
the Info directory unless you use the install-info tool to add them at install time. Before uninstalling, you should almost certainly attempt to stop any
services your package might be running (but be sure the uninstall won't
fail if the service isn't running). At uninstall, of course you should reverse most changes you might have
made to the system at install-time. But give a little thought to your
actions; uninstalling an RPM package should not, for example, accidentally
delete any user-created files. So you might be better off not trying to
remove a user ID, or deleting entire directory trees.
Resources -
Part 1 in this RPM series
introduces the process for building software packages with RPM, and Part 2 explains how to build without root, patch software, and distribute RPM.
- Some Linux distributions use Debian's APT tool for package
management.
- The RPM Web site
has pointers to many useful resources. The RPM e-mail list is a good place to ask questions.
-
Maximum RPM
is a book about using RPM. It has become quite outdated,
but an effort is underway to update it.
- The RPM HOWTO
is also getting somewhat dated. It covers some of the same ground
as this article.
- Eric S. Raymond's
Software Release Practice HOWTO document
is not specific to RPM or Linux. But it does give many good tips on releasing software
in a way that makes it easy for users to use and programmers to
contribute fixes and improvements.
-
The Free Software Foundation
is the source for GNU Indent and many other useful software packages -- including glibc and emacs.
- Browse more Linux resources on developerWorks.
- Browse more Open source resources on developerWorks.
About the author  | |  | Dan Poirier is an Advisory Software Engineer at IBM. He currently works in Research Triangle
Park, North Carolina, on network appliances that run Linux. Contact Dan at poirier@us.ibm.com.
|
Rate this page
|