The qmail rpm package

Sam Isaacson (sbi@nbcs.rutgers.edu)

Qmail has the most complex spec file of any package currently in the rpm repository. Its unique installer exemplifies several practices to be avoided when writing software intended for packaging. This document outlines the construction of the qmail-1.03 rpm.

The qmail build process

Qmail does not use a traditional GNU "configure ; make ; make install", imake, or "perl Makefile.PL ; make ; make install" installation. Instead qmail builds and runs its own installer when "make" is run. Essentially qmail installs itself by running the function hier() in hier.c:

  c(auto_qmail,"doc","INSTALL.alias",auto_uido,auto_gidq,0644);
  c(auto_qmail,"doc","INSTALL.ctl",auto_uido,auto_gidq,0644);
  c(auto_qmail,"doc","INSTALL.ids",auto_uido,auto_gidq,0644);
  c(auto_qmail,"doc","INSTALL.maildir",auto_uido,auto_gidq,0644);
  c(auto_qmail,"doc","INSTALL.mbox",auto_uido,auto_gidq,0644);
  c(auto_qmail,"doc","INSTALL.vsm",auto_uido,auto_gidq,0644);
  c(auto_qmail,"doc","TEST.deliver",auto_uido,auto_gidq,0644);
  c(auto_qmail,"doc","TEST.receive",auto_uido,auto_gidq,0644);
  c(auto_qmail,"doc","REMOVE.sendmail",auto_uido,auto_gidq,0644);
  c(auto_qmail,"doc","REMOVE.binmail",auto_uido,auto_gidq,0644);
  c(auto_qmail,"doc","PIC.local2alias",auto_uido,auto_gidq,0644);
  c(auto_qmail,"doc","PIC.local2ext",auto_uido,auto_gidq,0644);
  c(auto_qmail,"doc","PIC.local2local",auto_uido,auto_gidq,0644);

 -- An excerpt from hier.c
The variable auto_qmail contains the installation prefix for qmail (i.e. "/usr/local/qmail"). The variables auto_uid* and auto_gid*, defined in auto_uids.c, contain the uids and gids for the qmail-specific users and groups, respectively. Finally, the function c() (in addition to other functions not seen here: h(), d(), p(), and z()), located in install.c, installs the file or directory specified and sets its permissions and owner. The auto_* variables are set at compile-time, as qmail assumes it will be installed on the same machine on which it is compiled.

The qmail patches

Unfortunately we cannot change auto_qmail to $RPM_BUILD_ROOT/... and properly install qmail with rpm, since the installation location is hardcoded in several of qmail's executables. Moreover, qmail attempts to chown() its files in install; this fails if the packager is not root. To remedy this situation we first patch install to put the files in the buildroot instead of their final locations and to forget chown() calls:

#include "substdio.h"
#include "strerr.h"
#include "error.h"
#include "open.h"
#include "readwrite.h"
#include "exit.h"

#include <stdio.h>
#include <stdlib.h>

/* To fit the install process into RPM we need to ignore the chown()
 * and chmod() calls (for now) and install in a special build root.
 * Also we will log all the files installed for RPM's convenience.
 * Effectively home is constant so we replace it with RPM's build root:
 */

char build_rooted_home[] = "/var/tmp/qmail-root/usr/local/qmail";
FILE *rpm_log;
#define LOG(d,n,u,g)  fprintf(rpm_log, "%s:%s:%i:%i\n", d, n, u, g)

extern void hier();

#define FATAL "install: fatal: "

int fdsourcedir = -1;

void h(home,uid,gid,mode)
char *home;
int uid;
int gid;
int mode;
{
  if (mkdir(build_rooted_home,0700) == -1)
    if (errno != error_exist)
      strerr_die4sys(111,FATAL,"unable to mkdir ",build_rooted_home,": ");
/*  if (chown(home,uid,gid) == -1)
    strerr_die4sys(111,FATAL,"unable to chown ",home,": "); */
  if (chmod(build_rooted_home,mode) == -1)
    strerr_die4sys(111,FATAL,"unable to chmod ",build_rooted_home,": ");
}

void d(home,subdir,uid,gid,mode)
char *home;
char *subdir;
int uid;
int gid;
int mode;
{
  LOG(subdir,"",uid,gid);
  if (chdir(build_rooted_home) == -1)
    strerr_die4sys(111,FATAL,"unable to switch to ",build_rooted_home,": ");
  if (mkdir(subdir,0700) == -1)
    if (errno != error_exist)
      strerr_die6sys(111,FATAL,"unable to mkdir ",build_rooted_home,"/",subdir,"
: ");
/*  if (chown(subdir,uid,gid) == -1)
    strerr_die6sys(111,FATAL,"unable to chown ",home,"/",subdir,": "); */
  if (chmod(subdir,mode) == -1)
    strerr_die6sys(111,FATAL,"unable to chmod ",build_rooted_home,"/",subdir,": 
");
}

void p(home,fifo,uid,gid,mode)
char *home;
char *fifo;
int uid;
int gid;
int mode;
{
  LOG("",fifo,uid,gid);
  if (chdir(build_rooted_home) == -1)
    strerr_die4sys(111,FATAL,"unable to switch to ",build_rooted_home,": ");
  if (fifo_make(fifo,0700) == -1)
    if (errno != error_exist)
      strerr_die6sys(111,FATAL,"unable to mkfifo ",build_rooted_home,"/",fifo,":
 ");
/*  if (chown(fifo,uid,gid) == -1)
    strerr_die6sys(111,FATAL,"unable to chown ",home,"/",fifo,": "); */
  if (chmod(fifo,mode) == -1)
    strerr_die6sys(111,FATAL,"unable to chmod ",build_rooted_home,"/",fifo,": ")
;
}

char inbuf[SUBSTDIO_INSIZE];
char outbuf[SUBSTDIO_OUTSIZE];
substdio ssin;
substdio ssout;

void c(home,subdir,file,uid,gid,mode)
char *home;
char *subdir;
char *file;
int uid;
int gid;
int mode;
{
  int fdin;
  int fdout;

  LOG(subdir,file,uid,gid);
  if (fchdir(fdsourcedir) == -1)
    strerr_die2sys(111,FATAL,"unable to switch back to source directory: ");

  fdin = open_read(file);
  if (fdin == -1)
    strerr_die4sys(111,FATAL,"unable to read ",file,": ");
  substdio_fdbuf(&ssin,read,fdin,inbuf,sizeof inbuf);

  if (chdir(build_rooted_home) == -1)
    strerr_die4sys(111,FATAL,"unable to switch to ",build_rooted_home,": ");
  if (chdir(subdir) == -1)
    strerr_die6sys(111,FATAL,"unable to switch to ",build_rooted_home,"/",subdir
,": ");

  fdout = open_trunc(file);
  if (fdout == -1)
    strerr_die6sys(111,FATAL,"unable to write .../",subdir,"/",file,": ");
  substdio_fdbuf(&ssout,write,fdout,outbuf,sizeof outbuf);

  switch(substdio_copy(&ssout,&ssin)) {
    case -2:
      strerr_die4sys(111,FATAL,"unable to read ",file,": ");
    case -3:
      strerr_die6sys(111,FATAL,"unable to write .../",subdir,"/",file,": ");
  }

  close(fdin);
  if (substdio_flush(&ssout) == -1)
    strerr_die6sys(111,FATAL,"unable to write .../",subdir,"/",file,": ");
  if (fsync(fdout) == -1)
    strerr_die6sys(111,FATAL,"unable to write .../",subdir,"/",file,": ");
  if (close(fdout) == -1) /* NFS silliness */
    strerr_die6sys(111,FATAL,"unable to write .../",subdir,"/",file,": ");

/*  if (chown(file,uid,gid) == -1)
    strerr_die6sys(111,FATAL,"unable to chown .../",subdir,"/",file,": "); */
  if (chmod(file,mode) == -1)
    strerr_die6sys(111,FATAL,"unable to chmod .../",subdir,"/",file,": ");
}

void z(home,file,len,uid,gid,mode)
char *home;
char *file;
int len;
int uid;
int gid;
int mode;
{
  int fdout;

  LOG("",file,uid,gid);
  if (chdir(build_rooted_home) == -1)
    strerr_die4sys(111,FATAL,"unable to switch to ",build_rooted_home,": ");

  fdout = open_trunc(file);
  if (fdout == -1)
    strerr_die6sys(111,FATAL,"unable to write ",build_rooted_home,"/",file,": ")
;
  substdio_fdbuf(&ssout,write,fdout,outbuf,sizeof outbuf);

  while (len-- > 0)
    if (substdio_put(&ssout,"",1) == -1)
      strerr_die6sys(111,FATAL,"unable to write ",build_rooted_home,"/",file,": 
");

  if (substdio_flush(&ssout) == -1)
    strerr_die6sys(111,FATAL,"unable to write ",build_rooted_home,"/",file,": ")
;
  if (fsync(fdout) == -1)
    strerr_die6sys(111,FATAL,"unable to write ",build_rooted_home,"/",file,": ")
;
  if (close(fdout) == -1) /* NFS silliness */
    strerr_die6sys(111,FATAL,"unable to write ",build_rooted_home,"/",file,": ")
;

/*  if (chown(file,uid,gid) == -1)
    strerr_die6sys(111,FATAL,"unable to chown ",home,"/",file,": "); */
  if (chmod(file,mode) == -1)
    strerr_die6sys(111,FATAL,"unable to chmod ",build_rooted_home,"/",file,": ")
;
}

void main()
{
  fdsourcedir = open_read(".");
  if (fdsourcedir == -1)
    strerr_die2sys(111,FATAL,"unable to open current directory: ");
  if (!(rpm_log = fopen("RPM_MANIFEST", "a"))) {
    printf("Error opening RPM_MANIFEST for writing\n");
    exit(1);
  }

  umask(077);
  hier();

  fclose(rpm_log);
  _exit(0);
}

 -- The patched install.c

First, we replace auto_home with the variable build_rooted_home, set in this file to the temporary installation directory. We also comment out the chown() calls so that install can be run by non-root users. Finally, we log all the files installed, along with their eventual uids and gids, in RPM_MANIFEST. Since qmail does not use stdio, including stdio.h risks clobbering qmail functions; fortunately this works for qmail 1.03.

To install the files in the temporary location, it is possible to leave install.c alone and patch hier.c instead. This approach seems easier, since it requires only textual substitution. However, the install functions will still attempt to chown() files; either one must change the uids specified in hier() (and change them back in the specfile) or run rpm -ba as root. Moreover, hier() probably will change more than the install functions as new versions of qmail are released.

The qmail spec file

RPM can make packages based on file lists generated at compile-time. This is precisely the technique used in the qmail spec file (other examples include the perl rpm, which is considerably simpler):

# This is heavily inspired by the qmail spec from qmail's site. 

URL: ftp://koobera.math.uic.edu/www/qmail.html
Summary: qmail Mail Transfer Agent
Name: qmail 
Version: 1.03
Release: 3
Group: Utilities/System
Copyright: Check with djb@koobera.math.uic.edu
Source0: qmail-%{version}.tar.gz
Source1: rutgers-qmail-additions.tar.gz
Patch: qmail.patch
Buildroot: /var/tmp/qmail-root
Conflicts: sendmail exim smail
Provides: MTA smtpdaemon
Requires: patch

%description
qmail is a small, fast, secure replacement for the sendmail package,
which is the program that actually receives, routes, and delivers
electronic mail.  *** Note: Be sure and read the documentation as there
are some small but very significant differences between sendmail and
qmail and the programs that interact with them.

%prep
%setup -q
%setup -D -T -a 1
# Patch 0 fixes the hier.c file so install works correctly.
%patch  -p1
perl -i -p -e 's(/var/qmail)(/usr/local/qmail)' conf-qmail
perl -i -p -e 's/cc/gcc/' conf-cc
perl -i -p -e 's/cc/gcc/' conf-ld


%install
rm -rf $RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT/usr/local/qmail/bin
mkdir -p $RPM_BUILD_ROOT/etc/qmail

# Two builds are done here to make both binaries:

# make secure
perl -i -p -e 's/002/022/' conf-patrn
make 
make setup

mv $RPM_BUILD_ROOT/usr/local/qmail/bin/qmail-local \
   $RPM_BUILD_ROOT/usr/local/qmail/bin/qmail-local.secure
for i in control alias users ; do
    mv $RPM_BUILD_ROOT/usr/local/qmail/$i $RPM_BUILD_ROOT/etc/qmail/$i
done
 
# make insecure
perl -i -p -e 's/022/000/' conf-patrn
make
mv qmail-local $RPM_BUILD_ROOT/usr/local/qmail/bin/qmail-local.insecure
cp config config-fast $RPM_BUILD_ROOT/usr/local/qmail/bin

(cd files && find . | cpio -pdmu $RPM_BUILD_ROOT)
perl -e '
  %qmailu = ( 30296 => "alias", 30297 => "qmaild", 30298 => "qmaill",
              0 => "root", 30300 => "qmailp", 30301 => "qmailq",
              30302 => "qmailr", 30303 => "qmails" ); 
  %qmailg = ( 2036 => "nofiles", 2035 => "qmail" );
  %moved  = ( "/usr/local/qmail/control" => "/etc/qmail/control",
              "/usr/local/qmail/alias"   => "/etc/qmail/alias",
              "/usr/local/qmail/users"   => "/etc/qmail/users" );
  open(MANIFEST, "RPM_MANIFEST")   or die "Cannot open RPM_MANIFEST: $!";
  open(FILELIST, "> RPM_FILELIST") or die "Cannot open RPM_FILELIST: $!";
  print FILELIST "\%defattr(-,root,root)\n";
  while (<MANIFEST>) {
      chomp; @f = split /:/;
      $fn = "/usr/local/qmail" . ($f[0] eq "" ? "" : "/$f[0]")
                               . ($f[1] eq "" ? "" : "/$f[1]");
      $fn = $moved{$fn} if (defined $moved{$fn});
      next if ($fn =~ m(bin/qmail-local));
      print FILELIST "\%attr(-,$qmailu{ $f[2] },$qmailg{ $f[3] }) ";
      print FILELIST "%dir " if ($f[1] eq "");
      print FILELIST "$fn\n";
      $seen{$fn} = 1;
  }
  close MANIFEST;
  open(REST, "find /var/tmp/qmail-root/ \! -type d |")
      or die "Pipe error: $!";
  while (<REST>) {
      chomp; s(/var/tmp/qmail-root)();
      unless ($seen{$_}) {
          print FILELIST "\%attr(755,root,qmail) " if (m/qmail-local/);
          print FILELIST "$_\n";
      }
  }
  close FILELIST;'

%clean
rm -rf $RPM_BUILD_ROOT

%pre
echo "Checking uids..."
if [   "id -a qmailq | grep \
        \"uid=30301(qmailq) gid=199(users) groups=2035(qmail)\"" \
    -a "id -a qmailr | grep \
        \"uid=30302(qmailr) gid=199(users) groups=2035(qmail)\"" \
    -a "id -a qmails | grep \
       \"uid=30303(qmails) gid=199(users) groups=2035(qmail)\"" ]; then
    echo "okay."
else
    echo -e "FAILED!" \
"\nCheck id -a qmailq == uid=30301(qmailq) gid=199(users) groups=2035(qmail)" \
"\n      id -a qmailr == uid=30302(qmailr) gid=199(users) groups=2035(qmail)" \
"\n      id -a qmails == uid=30303(qmails) gid=199(users) groups=2035(qmail)" \
"\nYou may need to recompile qmail.\n"
sleep 60
fi

%post
PATH=/usr/bin
QHOME=/usr/local/qmail

# Shamelessly cribbed from the tint package

addlink () {
    # $1 is the target name
    # $2 is the real location
    [ ! -d "$1" ] && [ ! -h "$1" ] && [ ! -f "$1" ] && ln -s "$2" "$1"
}

# people may want to move these around

addlink $QHOME/boot/rc          rutgers
addlink $QHOME/bin/qmail-local  qmail-local.secure

addlink $QHOME/control          /etc/qmail/control
addlink $QHOME/alias            /etc/qmail/alias
addlink $QHOME/users            /etc/qmail/users

echo "Don't forget /usr/local/qmail/bin/setup for first time installs"
echo "Don't forget /usr/local/qmail/bin/finish_qmail to enable qmail"

%preun

QHOME=/usr/local/qmail
for i in boot/rc bin/qmail-local control alias users ; do
    rm $QHOME/$i
done

%files -f RPM_FILELIST

 -- The qmail spec file

The key to this approach is the perl script above. The script converts RPM_MANIFEST into RPM_FILELIST, which the spec file uses for its file list. Each line in RPM_MANIFEST (generated by the installer) consists of 4 entries seperated by colons:

queue/remote/21::30303:2035
queue/remote/22::30303:2035
queue/lock::30301:2035
:queue/lock/tcpto:30302:2035
:queue/lock/sendmutex:30303:2035
:queue/lock/trigger:30303:2035
boot:home:0:2035
boot:home+df:0:2035
boot:proc:0:2035
boot:proc+df:0:2035

 -- an excerpt from RPM_MANIFEST
An empty first entry indicates that the file specified is located in /usr/local/qmail; an empty second entry indicates that the file is a directory. The third and fourth entries are the uid and gid of the file, respectively.

Transforming RPM_MANIFEST into a file list for RPM is straightforward. Since RPM will not install files with numeric uids or gids, the third and fourth columns must be converted back to the qmail-specific usernames and group names. Since the contents of the directories are listed separately from the directories, the directories must be marked with "%dir" (so their contents are not packaged automatically). Finally, the spec file must gather all the extra files created. RPM sets the owner and group of these extra files to "root" (excepting bin/qmail-local.*).

In addition to fixing the installer, there are a number of Rutgers-specific modifications that the qmail specfile addresses. These are all contained in rutgers-qmail-additions.tar.gz and are (nearly) untarred in place.


$Id: building_qmail.html,v 1.1.1.1 2001/12/14 20:38:47 sbi Exp $