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

Installing from scratch with debs



I recently got a new hard drive and wanted to reinstall Debian on it.
The machine tracks unstable, so I figured it would be easier just to
download all the relevant .debs and dpkg -i them into the target file
system, than make boot floppies for Potato and then re-upgrade.

Installing debs onto a bare system proved to be harder than I'd
thought.  Obviously you start with the Essential packages.  But the
moment you try 'dpkg --root=/mnt --install libc6_2.2.1-1_i386.deb' you
discover five or so serious problems:

1. dpkg won't do anything at all if /var/lib/dpkg doesn't exist and
   doesn't contain two files (status, available) and two directories
   (info, updates).  They can all be empty, but you have to create
   them by hand.  I don't see why dpkg doesn't create these if they
   don't exist.

2. Preinst scripts won't run chrooted into a bare file system; there's
   nothing to run them with.  I tried dpkg-deb -x'ing enough packages
   that a minimal preinst could run - bash, libc6, fileutils - only to
   hit the next problem:

3. All the Essential packages have incomplete dependency lists.  This
   isn't surprising; their dep lists are intended to make upgrades of
   complete systems work.  Their true dep lists are much longer, and
   contain lots of cycles.  You rapidly get into a tangle where A
   won't unpack because it pre-depends on B, but B won't configure
   because its postinst expects A to be present.  They also have a
   fair number of dependencies on packages which are not Essential,
   and you have to get those too.

4. dpkg-deb -x doesn't update the status file or extract the control
   files, which means dpkg has no idea that certain packages should be
   hit by -a --configure.

5. makedev is not Essential, and not much depends on it, but it's the
   only package which will create /dev - if you leave it out, you get
   no /dev, and all kinds of things mysteriously break.  This is
   easily worked around by doing (cd /mnt/dev && MAKEDEV generic) by
   hand, first, if you remember.

I solved all these problems by writing a shell script that would
extract a .deb into a specified directory, ignoring all dependency
fields, and not running preinst scripts.  (It appears that none of the
Essential preinsts need to do anything when installing from scratch
with a modern dpkg.)  Then it would extract the control files,
generate a .list file, and fake a status file entry, leaving the
package in the 'unpacked' state.  (To find out what Status: line
corresponded to 'iU' in dpkg -l, I had to dig through the apt
sources.  Why is the status file format undocumented?)

I ran this script for all the Essential packages and the transitive
closure of their dependencies.  In case you're curious, these are all
the packages which are not Essential but included in the transitive
closure:

 libc6
 libcap1
 libdb2
 libncurses5
 libpam-modules
 libpam-runtime
 libpam0g
 libreadline4
 libstdc++2.10-glibc2.2
 mawk

I'm not sure why base-files wants awk.

Once this had been done, 'dpkg -a --configure' succeeded, leaving me
with a minimal but functional installation.  Then I downloaded all the
other packages I want, and did 'dpkg -iGE *.deb' to install them.  I
believe this would have worked in principle.  dpkg presently has a
bug, which causes it to segfault after unpacking five to ten packages.
I think this is the same bug that is being discussed right now on
debian-dpkg.  I also think it has something to do with the length of
the command line.  Note that -iGE will skip over hundreds of packages
without a problem; the crash only happens after it starts unpacking
things.

You can't just repeat the command, because the packages that it
unpacked have not yet been configured, and for some reason dpkg will
try to unpack them again instead of just configuring them and moving
on.  It will therefore crash again, in the same place.
dpkg -a --configure doesn't necessarily work, either, because it may
not have gotten to all the dependencies of the unpacked packages.  You
have to find those and install them individually, then configure
pending packages, then try to install *.deb again.  Repeat ad
nauseam.  After about an hour of this I got everything installed.

I couldn't use apt to do this, because for some mysterious reason,
even if you provide apt with a complete set of Packages and Release
files, pkgcache.bin, and all the debs in /var/cache/apt/archives, and
say --no-download, it still tries to do DNS lookups.  These fail
because there's no /etc/resolv.conf yet (in my configuration, that
gets created by the DHCP client - which is running outside the chroot,
but apt inside doesn't know that.)

Packages providing daemons tend to start them in their postinst.
That's fine in most cases, but not when you're installing into a
chroot tree and the same daemon is already running outside.  They try
to bind their TCP socket(s), fail, and die.  Fortunately most
postinsts don't consider this a fatal error.

base-config has serious bugs at present.  Passwords are echoed to the
screen, and it does some ridiculous grep loop over the password file
which takes about five minutes to complete.  With no indication of
what's happening.  I thought it had wedged.  Finally, it gets
seriously confused when it hits the bit about running tasksel or
dselect, and errors out.  I wound up setting up shadow MD5 passwords
and accounts by hand - this is not hard.

I understand that installer generation has to jump through some of the
same hoops, so I hope my experience provides some useful insights.
I've attached the unpacking script.

zw
#! /bin/sh

# dpkg-extract: extract a .deb archive into a virgin system.
# This is the same as dpkg -i except that:
# - preinst and postinst scripts are not run
# - the available file is not updated
# - the package is left in the "unconfigured" state
# - dependencies are ignored

# Usage: dpkg-extract package.deb /path/to/target/filesystem

deb=$1
targ=$2

emsg () {
    echo "$@" >&2
    exit 1
}

[ -n "$deb" ] && [ -n "$targ" ] ||
    emsg "usage: $0 package.deb /path/to/target/filesystem"


[ -f "$deb" ] || emsg "$deb: file not found"
[ -d "$targ" ] || emsg "$targ: no such directory"

pkg=${deb%%_*}
dpkgdir=$targ/var/lib/dpkg
scratch=tmp.$$

# Create the skeletal ROOT/var/lib/dpkg if it doesn't already
# exist (it may well not).
set -e
umask 022

[ -d $dpkgdir ] || mkdir -p $targ/var/lib/dpkg
[ -d $dpkgdir/updates ] || mkdir $targ/var/lib/dpkg/updates
[ -d $dpkgdir/info ] || mkdir $targ/var/lib/dpkg/info
[ -f $dpkgdir/status ] || touch $dpkgdir/status
[ -f $dpkgdir/available ] || touch $dpkgdir/available

# Likewise ROOT/dev, or lots of stuff will break.
# ??? check for devfs...
[ -d $targ/dev ] || mkdir $targ/dev
[ -c $targ/dev/null ] || (cd $targ/dev; /sbin/MAKEDEV generic)

# Extract the control files and generate the list file.
mkdir $scratch
dpkg-deb -e $deb $scratch
dpkg-deb -c $deb | awk '{print $6}' > $scratch/list.T

# Make sure no file in the list exists in the target directory.
# (Directories may exist already.)  We do this before editing the list
# file, so we still know what's a directory and what isn't.
abort=
while read file; do
    case $file in
	*/) ;;
	*)  if [ -e $targ/$file ]; then
		echo "$file exists below $targ already" >&2
		abort=yes
	    fi
	    ;;
    esac
done < $scratch/list.T

# Now edit list and control to match what dpkg wants to see in
# info/pkg.list and status, respectively.

# "Status: install ok unpacked" is believed to be the appropriate
# string to make dpkg -a --configure DTRT later.  (The format of the
# status file, and the exact semantics of the Status: line, are
# documented nowhere.  Down, not across.)

cd $scratch
mv control control.T
sed 's|^\./$|/.|; s|/$||; s|^\.||' list.T >list
sed '/^Architecture: /d
     /^Package: /a\
Status: install ok unpacked
' control.T >control
rm control.T list.T

# Make sure the package info files don't already exist in $dpkgdir/info.
for file in *; do
    mv $file $pkg.$file
    if [ -e $dpkgdir/info/$pkg.$file ]; then
	echo "$pkg.$file exists in $dpkgdir/info already" >&2
	abort=yes
    fi
done

# Bail out now if any of the above checks failed.
[ -z "$abort" ] || exit 1

# Finally, update the package info database, and unpack the package.
cd ..

cat $scratch/$pkg.control >> $dpkgdir/status
echo >> $dpkgdir/status
rm $scratch/$pkg.control

cp $scratch/* $dpkgdir/info
rm -rf $scratch

dpkg-deb -x $deb $targ

Reply to: