#!/usr/bin/perl

# Copyright (C) 2006-2012  Neil Williams <codehelp@debian.org>
#
# This package is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

use strict;
use warnings;
use Cwd qw (realpath getcwd);
use File::Basename;
use POSIX qw(locale_h);
use Locale::gettext;
use Config::IniFiles;
use vars qw / $our_version $prog $configfile $config $suite $tarname
 $milestone $name $tarball $key $branch $arch $dir $dryrun $verbose
 $etcdir @dirs @touch $config_str $dpkgdir $codename $extralist
 $rootpkg $targetpkg $location $components $basedir $updatescript
 $mountpoint $keyring $fpr /;
setlocale(LC_MESSAGES, "");
textdomain("ladder");
$prog = basename($0);
$our_version = &scripts_version();
$basedir = "/var/lib/ladder/";
$arch = 'armel';
$dir = "${basedir}rootfs/";
$etcdir = "${dir}etc/apt/"; # sources below ./$name/
use Data::Dumper;
while( @ARGV ) {
	$_= shift( @ARGV );
	last if m/^--$/;
	if (!/^-/) {
		unshift(@ARGV,$_);
		last;
	} elsif (/^(-\?|-h|--help|--version)$/) {
		&usageversion();
		exit (0);
	} elsif (/^(-m|--milestone)$/) {
		$milestone = shift;
	} elsif (/^(-c|--configfile)$/) {
		$configfile = realpath(shift);
	} elsif (/^(-t|--tarball)$/) {
		$tarball = realpath (shift);
	} elsif (/^(-a|--arch)$/) {
		$arch = shift;
	} elsif (/^(-n|--dry-run)$/) {
		$dryrun++;
	} elsif (/^(-v|--verbose)$/) {
		$verbose++;
	} else {
		die "$prog: "._g("Unknown option")." $_.\n";
	}
}
if (not defined $configfile) {
	$configfile = "/etc/ladder.d/$milestone.conf"
		if (-r "/etc/ladder.d/$milestone.conf");
}
if ((not defined $configfile) or (not defined $tarball)) {
	&usageversion;
	exit 0;
}
my $dmsg = sprintf (_g("Need a configuration file - use %s -f\n"), $prog);
die ($dmsg) if (not -r "$configfile");
$config     = new Config::IniFiles( -file => $configfile );
&check_config ($config);

# Translators: fields are programname, version string, include file, milestone name.
printf (_g("%s %s using %s for %s\n"), $prog, $our_version, $configfile, $name);
print   _g("in dry-run mode") if (defined $dryrun);
printf (_g("Suite:\t\t%s\n"),$suite);
printf (_g("Codename:\t%s\n"), $codename);
printf (_g("Mirror:\t\t%s\n"), $location);
printf (_g("Target package:\t%s\n"), $targetpkg);

if (defined $key) {
	# see #663479 needs to be the default keyring of root user
	my $gpg = system ("gpg --homedir ${basedir}keys/ --no-default-keyring --no-options -q --no-permission-warning --list-secret-keys >/dev/null 2>&1");
	$gpg = $gpg >> 8;
	if ($gpg != 0) {
		my $dmsg = sprintf(_g("secret key for '%s' is not available to the root user.\n"), $key);
		die ($dmsg);
	}
}

mkdir ($dir);
if (defined $tarball) {
	if (not -f $tarball) {
		my $dmsg = sprintf(_g("Failed to find %s: %s\n"), $tarball, $!);
		die ($dmsg);
	}
	my $tar = system ("cd ${dir} ; tar -xzf $tarball");
	$tar = $tar >> 8;
	if ($tar != 0) {
		my $dmsg = sprintf(_g("Failed to decompress rootfs tarball %s: %s\n"), $tarball, $!);
		die ($dmsg);
	}
} else {
	exit 0;
}
# tarball checks
system ("mkdir -p ${etcdir}sources.list.d/") if (not -d "${etcdir}sources.list.d/");
system ("mkdir -p ${etcdir}preferences.d/")  if (not -d "${etcdir}preferences.d/");
system ("mkdir -p $dir/var/lib/apt/lists")   if (not -d "$dir/var/lib/apt/lists");
system ("mkdir -p $dir/var/lib/apt/lists/partial") if (not -d "$dir/var/lib/apt/lists/partial");
system ("mkdir -p $dir/var/cache/apt/archives")    if (not -d "$dir/var/cache/apt/archives");
system ("mkdir -p $dir/var/cache/apt/archives/partial") if (not -d "$dir/var/cache/apt/archives/partial");
@dirs = qw/ alternatives info parts updates/;
@touch = qw/ available diversions statoverride status lock/;
$dpkgdir = "var/lib/dpkg/";
system ("mkdir -p ${dir}$dpkgdir");
foreach my $dpkgd (@dirs) {
	if (not -d "${dir}${dpkgdir}$dpkgd") {
		mkdir ("${dir}${dpkgdir}$dpkgd");
	}
}
foreach my $file (@touch) {
	open(F, ">${dir}${dpkgdir}/$file");
	close F;
}
utime(time, time, "${dir}etc/shells") or
	(open(F, ">${dir}etc/shells") && close F );

if (not -d "${dir}etc/network") {
	mkdir ("${dir}etc/network");
}

if (not -d "${dir}dev") {
	mkdir ("${dir}dev");
}

$config_str  = (defined $verbose) ? '' : ' -q ';
$config_str .= " -o Apt::Get::Download-Only=true";
$config_str .= " -y -o Apt::Architecture=$arch";
$config_str .= " -o Apt::Install-Recommends=false";
$config_str .= " -o Dir::Etc=${etcdir}";
$config_str .= " -o Dir::Etc::Trusted=${etcdir}trusted.gpg";
$config_str .= " -o Dir::Etc::TrustedParts=${etcdir}trusted.gpg.d";
$config_str .= " -o Dir::Etc::SourceList=${etcdir}sources.list";
$config_str .= " -o Dir::Etc::SourceParts=${etcdir}sources.list.d/";
$config_str .= " -o Dir::State=${dir}var/lib/apt";
$config_str .= " -o Dir::State::Status=${dir}${dpkgdir}status";
$config_str .= " -o Dir::Cache=${dir}var/cache/apt/";

# repo root dir
mkdir "${basedir}repo";
mkdir "${basedir}repo/conf";
my $label = "$name ladder repository";
my $desc = (defined $branch) ? "$branch - $suite" : "ladder for $suite";
my $msg = sprintf(_g("Cannot write %s/conf/distributions"), "${basedir}repo");
open (DIST, ">${basedir}repo/conf/distributions") or die ($msg." :$!");
# the labels are part of the syntax and cannot be translated.
print DIST "Origin: Debian\n";
print DIST "Label: $label\n";
print DIST "Suite: $suite\n";
print DIST "Codename: $codename\nVersion: 0.1\n";
print DIST "Architectures: $arch\n";
print DIST "Components: main\n"; #  even if the originals had components, this does not.
print DIST "SignWith: $key\n" if (defined $key);
print DIST "Description: $desc\n";
close (DIST);
my $vstr = (defined $verbose) ? "-v" : '--silent';
my $keystr = (defined $key) ? "--gnupghome ${basedir}keys/" : "";
system ("reprepro $keystr $vstr -b ${basedir}repo removematched $suite '*'");
my $ret = system ("reprepro $keystr $vstr -b ${basedir}repo export");
$ret >>= 8;
die ("reprepro $keystr ${basedir}keys $vstr -b ${basedir}repo export") if ($ret);
system ("reprepro $keystr $vstr -b ${basedir}repo createsymlinks");
# now empty the apt cache (if any) from the tarball.
system ("apt-get $config_str clean");
symlink ("${basedir}repo/dists", "${basedir}$name/dists") or die("$!");
symlink ("${basedir}repo/pool", "${basedir}$name/pool") or die("$!");
# populate
printf _g("Source:") . " deb $location $suite $components\n" if (defined $verbose);
printf _g("Apt configuration:") ." $config_str\n" if (defined $verbose);
# the sources to use to create the ladder step
open (SRC, ">${dir}etc/apt/sources.list.d/ladder-${name}.list") or die ("$!");
print SRC "deb $location $suite $components\n";
close (SRC);
system ("apt-get $config_str update");
system ("apt-get $config_str install $targetpkg $extralist");
if (not defined $dryrun) {
	system ("apt-get $config_str update");
	system ("apt-get $config_str dist-upgrade");
}
mkdir "${basedir}$name/list.d";
# the .list file for the upgrade on-device
# does not need to be in the normal /etc/ directory.
open (SOURCE, ">${basedir}$name/ladder.list") or die ("$!");
print SOURCE "deb copy:///$name/ $suite main\n";
close (SOURCE);
# create an update script, if configured
mkdir "${basedir}$name/list.d";
if (defined $updatescript) {
	open (SCR, ">${basedir}$name/ladder-${name}.sh") or die ("$!");
	my $root = (defined $rootpkg) ? $rootpkg : "";
	my $script = <<END;
#!/bin/sh

set -e
DIR=$mountpoint
ROOTPKG=$root
CONFIG=-y -o Dir::Etc::SourceList=\${DIR}/ladder.list -o Dir::Etc::SourceParts=\${DIR}/list.d/
apt-key add \${DIR}/pubkey.asc
apt-get \${CONFIG} update
if [ -n "\$ROOTPKG" ]; then
    apt-get \${CONFIG} --purge autoremove \$ROOTPKG
fi
apt-get \${CONFIG} dist-upgrade
apt-get \${CONFIG} autoclean
END
	print SCR $script;
	close (SCR);
	chmod (0755, "${basedir}$name/ladder-${name}.sh");
}
opendir (DEBS, "${dir}var/cache/apt/archives/") or die ("$!");
my @list = grep(/\.deb$/, readdir DEBS);
closedir (DEBS);
$tarname = "ladder-${name}.tgz";
if (not defined $dryrun) {
	foreach my $pkg (@list) {
		system ("reprepro $keystr --silent -b ${basedir}repo includedeb $codename ${dir}var/cache/apt/archives/$pkg 2>/dev/null");
	}
	unlink "${basedir}$tarname" if (-f "${basedir}$tarname");
	# optimise the directory layout to simplify the call.
	my $script = (-f "${basedir}$name/ladder-$name.sh") ? "./$name/ladder-$name.sh" : "";
	system ("cd ${basedir} ; tar -chzf ./$tarname ./${name}/*")
		if (-d "${basedir}${name}/pool");
	printf (_g("Created %s\n"), "${basedir}${tarname}") if (-f "${basedir}${tarname}");
} else {
	print _g("Package list:\n");
	print join ("\n", sort @list);
	print "\n";
	# Translators: string is the name of the final tarball.
	printf (_g("%s not modified.\n"), "${basedir}${tarname}") if (-f "${basedir}${tarname}");
}
system ("rm -rf ${basedir}rootfs/*");
system ("rm -rf ${basedir}${name}/*");
system ("rm -rf ${basedir}keys/*");
system ("rm -rf ${basedir}repo/*");
system ("rmdir ${basedir}repo/");
system ("rmdir ${basedir}keys/");
system ("rmdir ${basedir}rootfs/");
system ("rmdir ${basedir}${name}");
exit 0;

# sub routines

sub usageversion {
	printf STDERR (_g("
%s - create a milestone repository
version %s

Syntax: %s [OPTIONS] -m MILESTONE -t TARBALL
        %s -?|-h|--help|--version

Commands:
-c|--config PATH:              config file for this migration [required]
-m|--milestone MILESTONE :     shortcut to config file
-t|--tarball PATH:             rootfs tarball to upgrade [required]
-n|--dry-run:                  unpack tarball and list the needed packages.
-a|--arch ARCHITECTURE:        architecture of the packages [default=armel]

-?|-h|--help|--version:        print this help message and exit

Options:
-n|--dry-run:                  check which packages would be processed

The specified config file dictates the target branch to achieve from
the rootfs tarball specified in the file. Normally, this will be from
one milestone to the adjacent milestone. Run %s repeatedly to create
multiple steps. If the config file exists in /etc/ladder.d/, the
milestone name can be used as a shortcut.

The initial tarball may be a clean build (in which case, ensure that
the milestone is the first software release. If the tarball contains 
released software, this should normally be a default install of the 
software release immediately prior to the specified milestone as some 
packages may migrate data formats and other mechanisms between 
releases and skipping a release is not usually supported.

%s works in the /var/lib/ladder directory, unpacking the tarball into
./rootfs and creating the repostitory in a directory named after the
milestone. The tarball will be unpacked even in dry-run mode.

"), $prog, $our_version, $prog, $prog, $prog, $prog)
	or die "$0: failed to write usage: $!\n";
}

sub scripts_version {
	my $query = `dpkg-query -W -f='\${Version}' ladder`;
	(defined $query) ? return $query : return "";
}

sub check_config {
	my $conf = shift;
	my @sections = $conf->Sections;
	$name = $sections[0];
	my @msg=();
	$suite      = $config->val(lc($name), 'suite');
	$branch     = $config->val(lc($name), 'branch');
	$codename   = $config->val(lc($name), 'codename');
	$rootpkg    = $config->val(lc($name), 'rootpackage');
	$targetpkg  = $config->val(lc($name), 'targetpackage');
	$location   = $config->val(lc($name), 'location');
	$components = $config->val(lc($name), 'components');
	$key        = $config->val(lc($name), 'key');
	$extralist  = $config->val(lc($name), 'extrapackages');
	$mountpoint = $config->val(lc($name), 'mountpoint');
	my $update  = $config->val(lc($name), 'updatescript');
	my $keydir  = $config->val(lc($name), 'keyringdir');
	if (defined $key) {
		if (not defined $keydir or not -d $keydir) {
			$keydir = "/root/.gnupg/";
		}
		my $fprcmd = "gpg -q --no-permission-warning --with-colons --homedir ${keydir} ".
			"--no-default-keyring --fingerprint $key 2>/dev/null";
		my @output = `$fprcmd`;
		foreach my $line (@output) {
			next unless $line =~ /^fpr/;
			$line =~ /^fpr:+(\S+):$/ and $fpr = $1;
		}
		if (defined $fpr) {
			mkdir "${basedir}keys" if (not -d "${basedir}keys");
			mkdir "${basedir}$name" if (not -d "${basedir}$name");
			# location of exported public key for update script
			mkdir "${basedir}$name/keys" if (not -d "${basedir}$name/keys"); 
			# export the specified secret and public key and then import it
			system ("gpg --no-options -q --no-permission-warning --homedir ${keydir} ".
			"--no-default-keyring --export-secret-key $key > ${basedir}keys/seckey.gpg 2>/dev/null");
			system ("gpg --no-options -a -q --no-permission-warning --homedir ${keydir} ".
			"--no-default-keyring --export $key > ${basedir}$name/keys/pubkey.asc 2>/dev/null");
			system ("gpg --no-options -q --no-permission-warning --homedir ${basedir}keys/ ".
			"--no-default-keyring --import ${basedir}keys/seckey.gpg ".
			"${basedir}$name/keys/pubkey.asc >/dev/null 2>&1");
		} else {
			undef $key;
			undef $keydir;
		}
	}
	$updatescript++ if (defined $update and $update eq "true");
	push @msg, sprintf(_g("Suite has not been specified in %s."),$configfile)
		if((not defined $suite) or $suite eq '');
	undef $branch if((not defined $branch) or $branch eq '');
	$codename   = "${name}-release" if((not defined $codename) or $codename eq '');
	undef $rootpkg if((not defined $rootpkg) or $rootpkg eq '');
	push @msg, sprintf(_g("Target package has not been specified in %s."),$configfile)
		if((not defined $targetpkg) or $targetpkg eq '');
	push @msg, sprintf(_g("Location (mirror) has not been specified in %s."), $configfile)
		if((not defined $location) or $location eq '');
	$components = "main" if((not defined $components) or $components eq '');
	undef $key if((not defined $key) or $key eq '');
	$extralist = '' if (not defined $extralist);
	die (join("\n", @msg)."\n") if (scalar @msg > 0);
}

sub _g {
	return gettext(shift);
}

# POD content

=pod

=head1 NAME

Ladder - creates migration repositories for software release sets

=head1 Description

Ladder creates a SecureApt repository to migrate production devices
from one release milestone to the next. The repository contains all
binary packages which would be installed to upgrade the target package
of the specified release, including base packages. Source packages are
not included as this would make the final tarball much larger than
necessary. Sources should remain available via the main repositories.

For the purposes of C<ladder>, the bare installation / rootfs should
be considered to always precede the first software release. Subsequent
steps can then be based on the tarball of the previous milestone.

Note that if using C<multistrap> or a foreign architecture
C<debootstrap>, ensure that the rootfs inside the tarball is 
B<configured> and repacked before being used with C<ladder>. i.e.
use the production tarball rather than the build system tarball.

Ladder checks the installed package list from the production tarball
for that release, calculates the packages needed to migrate to the
specified milestone and prepares a repository containing those packages,
including all dependencies.

If the specified package list and the specified milestone are B<NOT>
contiguous, errors can result if some of the contained packages need
to migrate between data formats. For most cases, create a ladder step
for each software release and upgrade devices in the same sequence.
C<ladder> steps can be chained by modifying the update scripts.

=head1 Config files

Ladder configuration files live in F</etc/ladder.d/> and need to be
named after the release described. e.g. F</etc/ladder.d/internal.conf>.

A minimal file to upgrade to Debian sid could look like:

 [sid]
 suite=unstable
 location=http://ftp.uk.debian.org/debian
 targetpackage=apt

A more comprehensive config file could look like:

 [internal]
 suite=interim
 codename=milestone
 branch=software_release_4
 key=0xDEADBEEF
 keyringdir=keys
 location=copy:///srv/repo
 rootpackage=libfoo3
 targetpackage=metapackage
 extrapackages=bar baz other
 updatescript=true
 mountpoint=/media/ladder

(It is possible to list more than one package, as a space separated list.
Commas or other markers will not be parsed by apt.)

The section name (e.g. internal in the example above) is used as the
milestone name, which can differ from the suite name and the branch
name.

For more information on the key and keyringdir options, see the section
on SecureApt below.

=head1 Requirements

The rootfs is expected to carry some existing apt sources, the location
specified in the config file should be the one additional source which
provides the updated packages and the expectation is that this will
have a different suite name to the suite configured in the rootfs. If
the location and suite are the same, C<apt> will print messages about
duplicate source lists but these messages can be ignored.

In order for apt to calculate the packages needed for the update,
B<all> repositories which are enabled in the rootfs tarball B<including>
the location specified in the config file B<must be accessible> on
the machine running C<ladder>.

=head1 Deployment of ladder tarballs

The final tarball contains an example apt source showing the syntax
which would be suitable for use with the packaged repository. The
full path will need to be specified in the final sources list file.
e.g.

 deb copy:///milestone suite main
 
May need to be modified to:

 deb copy:///media/usb0/milestone suite main 

The example source is packaged as F<ladder.list> in the tarball.

The key should normally already be part of a keyring package and
installed on the devices. If not, an exported copy of the public key
is also included in the tarball which can be included into the device
keyring using C<apt-key> (which needs to be run as root):

 apt-key add /path/milestone/ladder.gpg

Some scripting / programming support will be needed to make this
process seamless on-device, in particular to provide the knowledge
of the actual sequence of milestone names, but this is beyond the scope
of C<ladder>, if only because the ladder tarball needs to be unpacked
first.

If the system is set with some standard apt sources already, the upgrade
will need to only allow C<apt-get> to see the ladder repository (because
the normal network connection isn't available, so the update would fail).
To do this, use apt command line options to reset the location of the
SourceList and SourceParts:

 apt-get -o Dir::Etc::SourceList=ladder.list -o Dir::Etc::SourceParts=./dir/ update

(F<./dir/> should be an empty directory - or a directory containing
empty F<.list> files and nothing else.)

The only requirements to use the ladder tarball are to create the
relevant source list file, ensure the key is available and then call
apt-get update; apt-get upgrade. There is no need for perl, reprepro
or anything else used by C<ladder> itself.

=head1 Example update script

If the configuration file includes the C<updatescript> option an
example script will be included, listing the value of the C<rootpackage>
option to be removed. If the C<mountpoint> option is set, the F<DIR>
variable will be set in the example script as well. (You may need to
invest time in a C<udev> rule as part of your rootfs to get a known
mount point but such rules are beyond the scope of this documentation.)

 #!/bin/sh
 
 set -e
 DIR=/media/ladder
 ROOTPKG=
 CONFIG=-y -o Dir::Etc::SourceList=${DIR}/ladder.list -o Dir::Etc::SourceParts=${DIR}/list.d/
 apt-key add ${DIR}/pubkey.asc
 apt-get ${CONFIG} update
 if [ -n "$ROOTPKG" ]; then
    apt-get ${CONFIG} --purge autoremove $ROOTPKG
 fi
 apt-get ${CONFIG} dist-upgrade
 apt-get ${CONFIG} autoclean

=head1 SecureApt

Signing a ladder step repository requires that the secret key is usable
without a passphrase and that the secret key is accessible to the root
user, either directly or via sudo.

As with anything related to GnuPG, protecting the secret key is the sole
responsibility of the key owner. It is recommended that ladder steps
are only created in a secure environment comparable with that used
to generate the keys. The same requirements apply to the machines which
use the secret key to sign the internal milestone repositories, so it
may be appropriate to create ladder steps on those machines.

=over

=item Specifying a keyring directory and key ID

If C<keyringdir> is used, the specified directory must contain the
public and secret keyrings which contain the specified C<key>. C<ladder>
will then make both the secret key and public key accessible to the
root user using a temporary keyring in F</var/lib/ladder/keys>. Only
the key available in F</var/lib/ladder/keys> will be available to the
repository signing process. C<ladder> only needs to be able to read the
secret and public keyrings of the F<keyringdir> specified. Ensure that
the secret key is available - B<without a passphrase> - or the repository
will not be signed.

=item Using just a key ID

If C<keyringdir> is B<not> used, the user must ensure that the key is
available to the root user as ladder requires sudo/root
to be able to use apt. Ensure that the specified secret key is
available - B<without a passphrase> - to the root user or the repository
will not be signed.

 sudo gpg --list-secret-key KEYID

Using C<keyringdir> is generally the easiest option.

=back

If the key is not available, the repository simply won't be signed
and devices would need to pass the AllowUnauthenticated option to
C<apt-get> when using the ladder repository. B<ladder does not
add the unauthenticated option to generated upgrade scripts!> You
can tell a SecureApt repository by the presence of the F<Release.gpg>
file.

It is possible to auto-generate GnuPG keys but C<ladder> does not
support this currently. The main problem is entropy - generating
a new GnuPG (or SSH) key requires a lot of entropy, especially
as default key lengths increase. It is a lot easier to ensure high
entropy when the key generation process is interactive.

=head2 Keyring packages are recommended

With careful planning, the security of the step upgrades can be much
improved by modifying the update scripts to B<not> add the signing
key using C<apt-key add> but instead to provide a keyring package in
the rootfs itself which contains the public key which will be used to
sign the next milestone. This is how Debian arranges keys - the release
of milestone A is not made until the key which will be used to sign
milestone B has the corresponding public key already included in the
keyring package in milestone A.

Such keyring packages themselves need to be in the milestone repository
because then the keyring package itself is protected by SecureApt.

Note that keyring packages will make it harder to use the downgrade
solution explained below, hence the need for planning.

=head2 Key expiries

Key expiry dates will complicate C<ladder> usage, especially if
downgrades are to be available. If a device was released on milestone D
and needs to be downgraded to milestone B, you will have problems if
the key used to sign milestone B has since expired. Equally, repairing
or servicing a device running milestone B becomes problematic if the
key for milestone B has expired whilst the device was in use.

Avoid using expiry dates on keys unless you are very, very confident
that a particular milestone will not be in use after a certain date.

=head2 Key management

Keys can be revoked but this relies on the devices which need to
verify that key being able to download the revocation certificate and
then to still have a usable key available for the upgrade. Consider
revoking the key for milestone B in the version of the keyring
package released with milestone D (milestone C still needs it to be able
to upgrade). This allows keys to be revoked on-device but still be
usable should it become necessary to repair, service or downgrade.

If a key is compromised, then unless the keyring package in any one
milestone still includes a usable key, there may be no way of securely
upgrading devices without manually adding a replacement key. Take care
of your secret keys.

=head1 Steps and milestones

Ladder - as with Debian - only works forwards. Downgrades are not
supported. If the rootfs tarball contains an existing apt source which
contains packages B<NEWER> than the requested milestone, then the
packages downloaded will be for the existing apt source, not the
milestone. Check the output with the C<-n|--dry-run> option.

However, judicious use of the C<rootpackage> option can assist with
limited downgrades - especially when the software being downgraded is
under your own control. The generated B<updatescript> can use 
C<apt-get --purge autoremove> on the root package. Specifying a core
library or special platform dependency package here can allow the rootfs
to be returned to a pristine state. The required milestone can then be
installed as if from a clean base. This is not quite the same as an
explicit downgrade but is a much more reliable mechanism as it provides
the equivalent rootfs to when the original milestone was created.

For these reasons, B<always> keep a copy of the original clean rootfs
which has no complicating apt sources.

If your root package is a shared library, you can specify multiple
root packages in the config file so that all released SONAME versions
are removed. Use only spaces to separate packages in the config file.

If you are using keyring packages, ensure that a suitable keyring
package is available to the ladder step which purges the root package.
To be able to upgrade to the end milestone from a purged rootfs, the
keyring package first needs to be upgraded to include the key used to
sign the end milestone (although the upgraded keyring package is free
to include revoked copies of intermediary keys, if appropriate).

=head1 Output

Ladder works in the F</var/lib/ladder> directory, unpacking the tarball
into F<./rootfs> and creating the repository in a directory named
after the milestone.

Results will be F</var/lib/ladder/ladder-$name.tgz>

=head1 Support

C<ladder> was written with a specific purpose in mind but is available
in Debian in the hope it will be useful for other situations as well.
If there are specific situations where C<ladder> could be extended to
be more useful for others, let me know using the Debian bug tracking
system: F<bugs.debian.org/ladder>.

Note that C<reprepro> already has B<snapshot> support which is not
the same as a C<ladder> of milestones. Snapshots include full sources
and ancillary packages which are not needed on-device and are intended
for build systems and developer use - ladder milestones are intended to
provide a small repository which can be used on machines after
production.

=cut
