Add chrootless mode and extract and custom variants

This commit is contained in:
Johannes 'josch' Schauer 2018-10-22 17:05:56 +02:00
parent 07f0e53081
commit 7534a7607f
Signed by untrusted user: josch
GPG key ID: F2CBA5C78FBD83E1

View file

@ -586,11 +586,22 @@ sub setup {
print $conf "Dir::Etc::TrustedParts \"/etc/apt/trusted.gpg.d\";\n"; print $conf "Dir::Etc::TrustedParts \"/etc/apt/trusted.gpg.d\";\n";
close $conf; close $conf;
foreach my $dir ('/etc/apt/apt.conf.d', '/etc/apt/sources.list.d', {
'/etc/apt/preferences.d', '/var/cache/apt', my @directories = ('/etc/apt/apt.conf.d', '/etc/apt/sources.list.d',
'/var/lib/apt/lists/partial', '/var/lib/dpkg', '/etc/apt/preferences.d', '/var/cache/apt',
'/etc/dpkg/dpkg.cfg.d/') { '/var/lib/apt/lists/partial', '/var/lib/dpkg',
make_path("$options->{root}/$dir") or die "failed to create $dir: $!"; '/etc/dpkg/dpkg.cfg.d/');
# if dpkg and apt operate from the outside we need some more
# directories because dpkg and apt might not even be installed inside
# the chroot
if ($options->{mode} eq 'chrootless') {
push @directories, ('/var/log/apt', '/var/lib/dpkg/triggers',
'/var/lib/dpkg/info', '/var/lib/dpkg/alternatives',
'/var/lib/dpkg/updates');
}
foreach my $dir (@directories) {
make_path("$options->{root}/$dir") or die "failed to create $dir: $!";
}
} }
# We put certain configuration items in their own configuration file # We put certain configuration items in their own configuration file
@ -638,6 +649,8 @@ sub setup {
} }
if (scalar @{$options->{dpkgopts}} > 0) { if (scalar @{$options->{dpkgopts}} > 0) {
# FIXME: in chrootless mode, dpkg will only read the configuration
# from the host
open my $fh, '>', "$options->{root}/etc/dpkg/dpkg.cfg.d/99mmdebstrap" or die "cannot open /etc/dpkg/dpkg.cfg.d/99mmdebstrap: $!"; open my $fh, '>', "$options->{root}/etc/dpkg/dpkg.cfg.d/99mmdebstrap" or die "cannot open /etc/dpkg/dpkg.cfg.d/99mmdebstrap: $!";
foreach my $opt (@{$options->{dpkgopts}}) { foreach my $opt (@{$options->{dpkgopts}}) {
if (-r $opt) { if (-r $opt) {
@ -673,6 +686,36 @@ sub setup {
close $fh; close $fh;
} }
# allow network access from within
copy("/etc/resolv.conf", "$options->{root}/etc/resolv.conf") or die "cannot copy /etc/resolv.conf: $!";
copy("/etc/hostname", "$options->{root}/etc/hostname") or die "cannot copy /etc/hostname: $!";
if ($options->{havemknod}) {
foreach my $file (@devfiles) {
my ($fname, $mode, $type, $linkname, $devmajor, $devminor) = @{$file};
if ($type == 0) { # normal file
die "type 0 not implemented";
} elsif ($type == 1) { # hardlink
die "type 1 not implemented";
} elsif ($type == 2) { # symlink
symlink $linkname, "$options->{root}/$fname" or die "cannot create symlink $fname";
next; # chmod cannot work on symlinks
} elsif ($type == 3) { # character special
0 == system('mknod', "$options->{root}/$fname", 'c', $devmajor, $devminor) or die "mknod failed: $?";
} elsif ($type == 4) { # block special
0 == system('mknod', "$options->{root}/$fname", 'b', $devmajor, $devminor) or die "mknod failed: $?";
} elsif ($type == 5) { # directory
make_path "$options->{root}/$fname", { error => \my $err };
if (@$err) {
die "cannot create $fname";
}
} else {
die "unsupported type: $type";
}
chmod $mode, "$options->{root}/$fname" or die "cannot chmod $fname: $!";
}
}
# we tell apt about the configuration via a config file passed via the # we tell apt about the configuration via a config file passed via the
# APT_CONFIG environment variable instead of using the --option command # APT_CONFIG environment variable instead of using the --option command
# line arguments because configuration settings like Dir::Etc have already # line arguments because configuration settings like Dir::Etc have already
@ -713,7 +756,30 @@ sub setup {
# apt and libapt treats apt as essential. If we want to install less # apt and libapt treats apt as essential. If we want to install less
# (essential variant) then we have to compute the package set ourselves. # (essential variant) then we have to compute the package set ourselves.
# Same if we want to install priority based variants. # Same if we want to install priority based variants.
if (any { $_ eq $options->{variant} } ('essential', 'standard', 'important', 'required', 'buildd', 'minbase')) { if (any { $_ eq $options->{variant} } ('extract', 'custom')) {
print STDERR "I: downloading packages with apt...\n";
run_apt_progress ('apt-get', '--yes',
'-oApt::Get::Download-Only=true',
'install', keys %pkgs_to_install);
} elsif ($options->{variant} eq 'apt') {
# if we just want to install Essential:yes packages, apt and their
# dependencies then we can make use of libapt treating apt as
# implicitly essential. An upgrade with the (currently) empty status
# file will trigger an installation of the essential packages plus apt.
#
# 2018-09-02, #debian-dpkg on OFTC, times in UTC+2
# 23:39 < josch> I'll just put it in my script and if it starts
# breaking some time I just say it's apt's fault. :P
# 23:42 < DonKult> that is how it usually works, so yes, do that :P (<-
# and please add that line next to it so you can
# remind me in 5+ years that I said that after I wrote
# in the bugreport: "Are you crazy?!? Nobody in his
# right mind would even suggest depending on it!")
print STDERR "I: downloading packages with apt...\n";
run_apt_progress ('apt-get', '--yes',
'-oApt::Get::Download-Only=true',
'dist-upgrade');
} elsif (any { $_ eq $options->{variant} } ('essential', 'standard', 'important', 'required', 'buildd', 'minbase')) {
my %ess_pkgs; my %ess_pkgs;
open(my $pipe_apt, '-|', 'apt-get', 'indextargets', '--format', '$(FILENAME)', 'Created-By: Packages') or die "cannot start apt-get indextargets: $!"; open(my $pipe_apt, '-|', 'apt-get', 'indextargets', '--format', '$(FILENAME)', 'Created-By: Packages') or die "cannot start apt-get indextargets: $!";
while (my $fname = <$pipe_apt>) { while (my $fname = <$pipe_apt>) {
@ -790,24 +856,6 @@ sub setup {
run_apt_progress ('apt-get', '--yes', run_apt_progress ('apt-get', '--yes',
'-oApt::Get::Download-Only=true', '-oApt::Get::Download-Only=true',
'install', keys %ess_pkgs); 'install', keys %ess_pkgs);
} elsif ($options->{variant} eq 'apt') {
# if we just want to install Essential:yes packages, apt and their
# dependencies then we can make use of libapt treating apt as
# implicitly essential. An upgrade with the (currently) empty status
# file will trigger an installation of the essential packages plus apt.
#
# 2018-09-02, #debian-dpkg on OFTC, times in UTC+2
# 23:39 < josch> I'll just put it in my script and if it starts
# breaking some time I just say it's apt's fault. :P
# 23:42 < DonKult> that is how it usually works, so yes, do that :P (<-
# and please add that line next to it so you can
# remind me in 5+ years that I said that after I wrote
# in the bugreport: "Are you crazy?!? Nobody in his
# right mind would even suggest depending on it!")
print STDERR "I: downloading packages with apt...\n";
run_apt_progress ('apt-get', '--yes',
'-oApt::Get::Download-Only=true',
'dist-upgrade');
} else { } else {
die "unknown variant: $options->{variant}"; die "unknown variant: $options->{variant}";
} }
@ -841,345 +889,346 @@ sub setup {
die "nothing got downloaded"; die "nothing got downloaded";
} }
print STDERR "I: extracting archives...\n"; if ($options->{mode} eq 'chrootless') {
print_progress 0.0; print STDERR "I: installing packages...\n";
my $counter = 0; # FIXME: the dpkg config from the host is parsed before the command
my $total = scalar @essential_pkgs; # line arguments are parsed and might break this mode
foreach my $deb (@essential_pkgs) { my @chrootless_opts = (
$counter += 1; '-oDPkg::Options::=--force-not-root',
# not using dpkg-deb --extract as that would replace the '-oDPkg::Options::=--force-script-chrootless',
# merged-usr symlinks with plain directories '-oDPkg::Options::=--root=' . $options->{root},
pipe my $rfh, my $wfh; '-oDPkg::Options::=--log=' . "$options->{root}/var/log/dpkg.log");
my $pid1 = fork() // die "fork() failed: $!"; run_apt_progress ('apt-get', '--yes', @chrootless_opts,
if ($pid1 == 0) { 'install', (map { "$options->{root}/$_" } @essential_pkgs));
open(STDOUT, '>&', $wfh); if (any { $_ eq $options->{variant} } ('extract', 'custom')) {
exec 'dpkg-deb', '--fsys-tarfile', "$options->{root}/$deb"; # nothing to do
} } elsif (any { $_ eq $options->{variant} } ('essential', 'apt', 'standard', 'important', 'required', 'buildd', 'minbase')) {
my $pid2 = fork() // die "fork() failed: $!"; if (%pkgs_to_install) {
if ($pid2 == 0) { run_apt_progress ('apt-get', '--yes', @chrootless_opts,
open(STDIN, '<&', $rfh); 'install', keys %pkgs_to_install);
exec 'tar', '-C', $options->{root}, '--keep-directory-symlink', '--extract', '--file', '-';
}
waitpid($pid1, 0);
$? == 0 or die "dpkg-deb --fsys-tarfile failed: $?";
waitpid($pid2, 0);
$? == 0 or die "tar --extract failed: $?";
print_progress ($counter/$total*100);
}
print_progress "done";
if ($options->{mode} eq 'fakechroot') {
# FIXME: if trouble arises, look into /etc/fakechroot/*.env for
# more interesting variables to set
$ENV{FAKECHROOT_CMD_SUBST} = join ':', (
'/bin/mount=/bin/true',
'/usr/bin/ldd=/usr/bin/ldd.fakechroot',
'/usr/bin/mkfifo=/bin/true',
'/usr/sbin/ldconfig=/bin/true',
);
}
# make sure that APT_CONFIG is not set when executing anything inside the
# chroot
my @chrootcmd = ('env', '--unset=APT_CONFIG');
if ($options->{mode} eq 'proot') {
# FIXME: proot currently cannot install apt because of https://github.com/proot-me/PRoot/issues/147
push @chrootcmd, ('proot', '--root-id', '--bind=/dev', "--rootfs=$options->{root}", '--cwd=/');
} elsif (any { $_ eq $options->{mode} } ('root', 'unshare', 'fakechroot')) {
push @chrootcmd, ('/usr/sbin/chroot', $options->{root});
} else {
die "unknown mode: $options->{mode}";
}
# copy qemu-user-static binary into chroot or setup proot with --qemu
if (defined $options->{qemu}) {
if ($options->{mode} eq 'proot') {
push @chrootcmd, "--qemu=qemu-$options->{qemu}";
} elsif ($options->{mode} eq 'fakechroot') {
# The binfmt support on the outside is used, so qemu needs to know
# where it has to look for shared libraries
$ENV{QEMU_LD_PREFIX} = $options->{root};
# Make sure that the fakeroot and fakechroot shared libraries
# exist for the right architecture
open my $fh, '-|', 'dpkg-architecture', '-a', $options->{nativearch}, '-qDEB_HOST_MULTIARCH' // die "failed to fork(): $!";
chomp (my $deb_host_multiarch = do { local $/; <$fh> });
close $fh;
if ($? != 0 or !$deb_host_multiarch) {
die "dpkg-architecture failed: $?";
} }
my $fakechrootdir = "/usr/lib/$deb_host_multiarch/fakechroot";
if (!-e "$fakechrootdir/libfakechroot.so") {
die "$fakechrootdir/libfakechroot.so doesn't exist. Install libfakechroot:$options->{nativearch} outside the chroot";
}
my $fakerootdir = "/usr/lib/$deb_host_multiarch/libfakeroot";
if (!-e "$fakerootdir/libfakeroot-sysv.so") {
die "$fakerootdir/libfakeroot-sysv.so doesn't exist. Install libfakeroot:$options->{nativearch} outside the chroot";
}
# fakechroot only fills LD_LIBRARY_PATH with the directories of
# the host's architecture. We append the directories of the chroot
# architecture.
$ENV{LD_LIBRARY_PATH} .= ":$fakechrootdir:$fakerootdir";
} elsif (any { $_ eq $options->{mode} } ('root', 'unshare')) {
# other modes require a static qemu-user binary
my $qemubin = "/usr/bin/qemu-$options->{qemu}-static";
if (!-e $qemubin) {
die "cannot find $qemubin";
}
copy $qemubin, "$options->{root}/$qemubin" or die "cannot copy $qemubin: $!";
} else { } else {
die "unknown mode: $options->{mode}"; die "unknown variant: $options->{variant}";
} }
} } elsif (any { $_ eq $options->{mode} } ('root', 'unshare', 'fakechroot', 'proot')) {
print STDERR "I: extracting archives...\n";
print_progress 0.0;
my $counter = 0;
my $total = scalar @essential_pkgs;
foreach my $deb (@essential_pkgs) {
$counter += 1;
# not using dpkg-deb --extract as that would replace the
# merged-usr symlinks with plain directories
pipe my $rfh, my $wfh;
my $pid1 = fork() // die "fork() failed: $!";
if ($pid1 == 0) {
open(STDOUT, '>&', $wfh);
exec 'dpkg-deb', '--fsys-tarfile', "$options->{root}/$deb";
}
my $pid2 = fork() // die "fork() failed: $!";
if ($pid2 == 0) {
open(STDIN, '<&', $rfh);
exec 'tar', '-C', $options->{root}, '--keep-directory-symlink', '--extract', '--file', '-';
}
waitpid($pid1, 0);
$? == 0 or die "dpkg-deb --fsys-tarfile failed: $?";
waitpid($pid2, 0);
$? == 0 or die "tar --extract failed: $?";
print_progress ($counter/$total*100);
}
print_progress "done";
if ($options->{havemknod}) { if ($options->{variant} eq 'extract') {
foreach my $file (@devfiles) { # nothing else to do
my ($fname, $mode, $type, $linkname, $devmajor, $devminor) = @{$file}; } elsif (any { $_ eq $options->{variant} } ('custom', 'essential', 'apt', 'standard', 'important', 'required', 'buildd', 'minbase')) {
if ($type == 0) { # normal file if ($options->{mode} eq 'fakechroot') {
die "type 0 not implemented"; # FIXME: if trouble arises, look into /etc/fakechroot/*.env for
} elsif ($type == 1) { # hardlink # more interesting variables to set
die "type 1 not implemented"; $ENV{FAKECHROOT_CMD_SUBST} = join ':', (
} elsif ($type == 2) { # symlink '/bin/mount=/bin/true',
symlink $linkname, "$options->{root}/$fname" or die "cannot create symlink $fname"; '/usr/bin/ldd=/usr/bin/ldd.fakechroot',
next; # chmod cannot work on symlinks '/usr/bin/mkfifo=/bin/true',
} elsif ($type == 3) { # character special '/usr/sbin/ldconfig=/bin/true',
0 == system('mknod', "$options->{root}/$fname", 'c', $devmajor, $devminor) or die "mknod failed: $?"; );
} elsif ($type == 4) { # block special }
0 == system('mknod', "$options->{root}/$fname", 'b', $devmajor, $devminor) or die "mknod failed: $?";
} elsif ($type == 5) { # directory # make sure that APT_CONFIG is not set when executing anything inside the
make_path "$options->{root}/$fname", { error => \my $err }; # chroot
if (@$err) { my @chrootcmd = ('env', '--unset=APT_CONFIG');
die "cannot create $fname"; if ($options->{mode} eq 'proot') {
} # FIXME: proot currently cannot install apt because of https://github.com/proot-me/PRoot/issues/147
push @chrootcmd, ('proot', '--root-id', '--bind=/dev', "--rootfs=$options->{root}", '--cwd=/');
} elsif (any { $_ eq $options->{mode} } ('root', 'unshare', 'fakechroot')) {
push @chrootcmd, ('/usr/sbin/chroot', $options->{root});
} else { } else {
die "unsupported type: $type"; die "unknown mode: $options->{mode}";
} }
chmod $mode, "$options->{root}/$fname" or die "cannot chmod $fname: $!";
}
}
# install the extracted packages properly # copy qemu-user-static binary into chroot or setup proot with --qemu
# we need --force-depends because dpkg does not take Pre-Depends into if (defined $options->{qemu}) {
# account and thus doesn't install them in the right order if ($options->{mode} eq 'proot') {
print STDERR "I: installing packages...\n"; push @chrootcmd, "--qemu=qemu-$options->{qemu}";
run_dpkg_progress [@essential_pkgs], @chrootcmd, 'dpkg', '--install', '--force-depends'; } elsif ($options->{mode} eq 'fakechroot') {
# The binfmt support on the outside is used, so qemu needs to know
# if the path-excluded option was added to the dpkg config, reinstall all # where it has to look for shared libraries
# packages $ENV{QEMU_LD_PREFIX} = $options->{root};
if (-e "$options->{root}/etc/dpkg/dpkg.cfg.d/99mmdebstrap") { # Make sure that the fakeroot and fakechroot shared libraries
open(my $fh, '<', "$options->{root}/etc/dpkg/dpkg.cfg.d/99mmdebstrap") or die "cannot open /etc/dpkg/dpkg.cfg.d/99mmdebstrap: $!"; # exist for the right architecture
my $num_matches = grep /^path-exclude=/, <$fh>; open my $fh, '-|', 'dpkg-architecture', '-a', $options->{nativearch}, '-qDEB_HOST_MULTIARCH' // die "failed to fork(): $!";
close $fh; chomp (my $deb_host_multiarch = do { local $/; <$fh> });
if ($num_matches > 0) {
# without --skip-same-version, dpkg will install the given
# packages even though they are already installed
print STDERR "I: re-installing packages because of path-exclude...\n";
run_dpkg_progress [@essential_pkgs], @chrootcmd, 'dpkg', '--install';
}
}
foreach my $deb (@essential_pkgs) {
unlink "$options->{root}/$deb" or die "cannot unlink $deb";
}
if (%pkgs_to_install) {
# some packages have to be installed from the outside before anything
# can be installed from the inside.
#
# we do not need to install any *-archive-keyring packages inside the
# chroot prior to installing the packages, because the keyring is only
# used when doing "apt-get update" and that was already done at the
# beginning using key material from the outside. Since the apt cache
# is already filled and we are not calling "apt-get update" again, the
# keyring can be installed later during installation. But: if it's not
# installed during installation, then we might end up with a fully
# installed system without keyrings that are valid for its
# sources.list.
my %pkgs_to_install_from_outside;
# install apt if necessary
if ($options->{variant} ne 'apt') {
$pkgs_to_install_from_outside{apt} = ();
}
# since apt will be run inside the chroot, make sure that
# apt-transport-https and ca-certificates gets installed first if any
# mirror is a https URI
open(my $pipe_apt, '-|', 'apt-get', 'indextargets', '--format', '$(URI)', 'Created-By: Packages') or die "cannot start apt-get indextargets: $!";
while (my $uri = <$pipe_apt>) {
if ($uri =~ /^https:\/\//) {
# FIXME: support for https is part of apt >= 1.5
$pkgs_to_install_from_outside{'apt-transport-https'} = ();
$pkgs_to_install_from_outside{'ca-certificates'} = ();
last;
} elsif ($uri =~ /^tor(\+[a-z]+)*:\/\//) {
# tor URIs can be tor+http://, tor+https:// or even
# tor+mirror+file://
$pkgs_to_install_from_outside{'apt-transport-tor'} = ();
last;
}
}
close $pipe_apt;
$? == 0 or die "apt-get indextargets failed";
if (%pkgs_to_install_from_outside) {
print STDERR 'I: downloading ' . (join ', ', keys %pkgs_to_install_from_outside) . "...\n";
run_apt_progress ('apt-get', '--yes',
'-oApt::Get::Download-Only=true',
'install', (keys %pkgs_to_install_from_outside));
my @debs_to_install;
my $apt_archives = "/var/cache/apt/archives/";
opendir my $dh, "$options->{root}/$apt_archives" or die "cannot read $apt_archives";
while (my $deb = readdir $dh) {
if ($deb !~ /\.deb$/) {
next;
}
$deb = "$apt_archives/$deb";
if (!-f "$options->{root}/$deb") {
next;
}
push @debs_to_install, $deb;
}
close $dh;
if (scalar @debs_to_install == 0) {
die "nothing got downloaded";
}
# we need --force-depends because dpkg does not take Pre-Depends
# into account and thus doesn't install them in the right order
print STDERR 'I: installing ' . (join ', ', keys %pkgs_to_install_from_outside) . "...\n";
run_dpkg_progress [@debs_to_install], @chrootcmd, 'dpkg', '--install', '--force-depends';
foreach my $deb (@debs_to_install) {
unlink "$options->{root}/$deb" or die "cannot unlink $deb";
}
}
# if more than essential should be installed, make the system look
# more like a real one by creating or bind-mounting the device nodes
foreach my $file (@devfiles) {
my ($fname, $mode, $type, $linkname, $devmajor, $devminor) = @{$file};
next if $fname eq './dev/';
if ($type == 0) { # normal file
die "type 0 not implemented";
} elsif ($type == 1) { # hardlink
die "type 1 not implemented";
} elsif ($type == 2) { # symlink
if (!$options->{havemknod}) {
symlink $linkname, "$options->{root}/$fname" or die "cannot create symlink $fname";
}
} elsif ($type == 3 or $type == 4) { # character/block special
if (!$options->{havemknod}) {
open my $fh, '>', "$options->{root}/$fname" or die "cannot open $options->{root}/$fname: $!";
close $fh; close $fh;
0 == system('mount', '-o', 'bind', "/$fname", "$options->{root}/$fname") or die "mount failed: $?"; if ($? != 0 or !$deb_host_multiarch) {
} die "dpkg-architecture failed: $?";
} elsif ($type == 5) { # directory
if (!$options->{havemknod}) {
make_path "$options->{root}/$fname";
chmod $mode, "$options->{root}/$fname" or die "cannot chmod $fname: $!";
}
0 == system('mount', '-o', 'bind', "/$fname", "$options->{root}/$fname") or die "mount failed: $?";
} else {
die "unsupported type: $type";
}
}
# We can only mount /proc and /sys after extracting the essential
# set because if we mount it before, then base-files will not be able
# to extract those
if ($options->{mode} eq 'unshare') {
# without the network namespace unshared, we cannot mount a new
# sysfs. Since we need network, we just bind-mount.
#
# we have to rbind because just using bind results in "wrong fs
# type, bad option, bad superblock" error
0 == system('mount', '-o', 'rbind', '/sys', "$options->{root}/sys") or die "mount failed: $?";
} elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) {
0 == system('mount', '-t', 'sysfs', '-o', 'nosuid,nodev,noexec', 'sys', "$options->{root}/sys") or die "mount failed: $?";
} else {
die "unknown mode: $options->{mode}";
}
0 == system('mount', '-t', 'proc', 'proc', "$options->{root}/proc") or die "mount failed: $?";
# prevent daemons from starting
{
open my $fh, '>', "$options->{root}/usr/sbin/policy-rc.d" or die "cannot open policy-rc.d: $!";
print $fh "#!/bin/sh\n";
print $fh "exit 101\n";
close $fh;
chmod 0755, "$options->{root}/usr/sbin/policy-rc.d" or die "cannot chmod policy-rc.d: $!";
}
{
move("$options->{root}/sbin/start-stop-daemon", "$options->{root}/sbin/start-stop-daemon.REAL") or die "cannot move start-stop-daemon";
open my $fh, '>', "$options->{root}/sbin/start-stop-daemon" or die "cannot open policy-rc.d: $!";
print $fh "#!/bin/sh\n";
print $fh "echo \"Warning: Fake start-stop-daemon called, doing nothing\">&2\n";
close $fh;
chmod 0755, "$options->{root}/sbin/start-stop-daemon" or die "cannot chmod start-stop-daemon: $!";
}
# allow network access from within
copy("/etc/resolv.conf", "$options->{root}/etc/resolv.conf") or die "cannot copy /etc/resolv.conf: $!";
copy("/etc/hostname", "$options->{root}/etc/hostname") or die "cannot copy /etc/hostname: $!";
print STDERR "I: installing remaining packages inside the chroot...\n";
run_apt_progress @chrootcmd, 'apt-get', '--yes', 'install', keys %pkgs_to_install;
# cleanup
move("$options->{root}/sbin/start-stop-daemon.REAL", "$options->{root}/sbin/start-stop-daemon") or die "cannot move start-stop-daemon";
unlink "$options->{root}/usr/sbin/policy-rc.d" or die "cannot unlink policy-rc.d";
foreach my $file (@devfiles) {
my ($fname, undef, $type, $linkname, undef, undef) = @{$file};
next if $fname eq './dev/';
if ($type == 0) { # normal file
die "type 0 not implemented";
} elsif ($type == 1) { # hardlink
die "type 1 not implemented";
} elsif ($type == 2) { # symlink
if (!$options->{havemknod}) {
unlink "$options->{root}/$fname" or die "cannot unlink $fname: $!";
}
} elsif ($type == 3 or $type == 4) { # character/block special
if (!$options->{havemknod}) {
if ($options->{mode} eq 'unshare') {
0 == system('umount', '--no-mtab', "$options->{root}/$fname") or die "umount failed: $?";
} elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) {
0 == system('umount', "$options->{root}/$fname") or die "umount failed: $?";
} else {
die "unknown mode: $options->{mode}";
} }
unlink "$options->{root}/$fname"; my $fakechrootdir = "/usr/lib/$deb_host_multiarch/fakechroot";
} if (!-e "$fakechrootdir/libfakechroot.so") {
} elsif ($type == 5) { # directory die "$fakechrootdir/libfakechroot.so doesn't exist. Install libfakechroot:$options->{nativearch} outside the chroot";
if ($options->{mode} eq 'unshare') { }
0 == system('umount', '--no-mtab', "$options->{root}/$fname") or die "umount failed: $?"; my $fakerootdir = "/usr/lib/$deb_host_multiarch/libfakeroot";
} elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) { if (!-e "$fakerootdir/libfakeroot-sysv.so") {
0 == system('umount', "$options->{root}/$fname") or die "umount failed: $?"; die "$fakerootdir/libfakeroot-sysv.so doesn't exist. Install libfakeroot:$options->{nativearch} outside the chroot";
}
# fakechroot only fills LD_LIBRARY_PATH with the directories of
# the host's architecture. We append the directories of the chroot
# architecture.
$ENV{LD_LIBRARY_PATH} .= ":$fakechrootdir:$fakerootdir";
} elsif (any { $_ eq $options->{mode} } ('root', 'unshare')) {
# other modes require a static qemu-user binary
my $qemubin = "/usr/bin/qemu-$options->{qemu}-static";
if (!-e $qemubin) {
die "cannot find $qemubin";
}
copy $qemubin, "$options->{root}/$qemubin" or die "cannot copy $qemubin: $!";
} else {
die "unknown mode: $options->{mode}";
}
}
# install the extracted packages properly
# we need --force-depends because dpkg does not take Pre-Depends into
# account and thus doesn't install them in the right order
print STDERR "I: installing packages...\n";
run_dpkg_progress [@essential_pkgs], @chrootcmd, 'dpkg', '--install', '--force-depends';
# if the path-excluded option was added to the dpkg config, reinstall all
# packages
if (-e "$options->{root}/etc/dpkg/dpkg.cfg.d/99mmdebstrap") {
open(my $fh, '<', "$options->{root}/etc/dpkg/dpkg.cfg.d/99mmdebstrap") or die "cannot open /etc/dpkg/dpkg.cfg.d/99mmdebstrap: $!";
my $num_matches = grep /^path-exclude=/, <$fh>;
close $fh;
if ($num_matches > 0) {
# without --skip-same-version, dpkg will install the given
# packages even though they are already installed
print STDERR "I: re-installing packages because of path-exclude...\n";
run_dpkg_progress [@essential_pkgs], @chrootcmd, 'dpkg', '--install';
}
}
foreach my $deb (@essential_pkgs) {
unlink "$options->{root}/$deb" or die "cannot unlink $deb";
}
if (%pkgs_to_install) {
# some packages have to be installed from the outside before anything
# can be installed from the inside.
#
# we do not need to install any *-archive-keyring packages inside the
# chroot prior to installing the packages, because the keyring is only
# used when doing "apt-get update" and that was already done at the
# beginning using key material from the outside. Since the apt cache
# is already filled and we are not calling "apt-get update" again, the
# keyring can be installed later during installation. But: if it's not
# installed during installation, then we might end up with a fully
# installed system without keyrings that are valid for its
# sources.list.
my %pkgs_to_install_from_outside;
# install apt if necessary
if ($options->{variant} ne 'apt') {
$pkgs_to_install_from_outside{apt} = ();
}
# since apt will be run inside the chroot, make sure that
# apt-transport-https and ca-certificates gets installed first if any
# mirror is a https URI
open(my $pipe_apt, '-|', 'apt-get', 'indextargets', '--format', '$(URI)', 'Created-By: Packages') or die "cannot start apt-get indextargets: $!";
while (my $uri = <$pipe_apt>) {
if ($uri =~ /^https:\/\//) {
# FIXME: support for https is part of apt >= 1.5
$pkgs_to_install_from_outside{'apt-transport-https'} = ();
$pkgs_to_install_from_outside{'ca-certificates'} = ();
last;
} elsif ($uri =~ /^tor(\+[a-z]+)*:\/\//) {
# tor URIs can be tor+http://, tor+https:// or even
# tor+mirror+file://
$pkgs_to_install_from_outside{'apt-transport-tor'} = ();
last;
}
}
close $pipe_apt;
$? == 0 or die "apt-get indextargets failed";
if (%pkgs_to_install_from_outside) {
print STDERR 'I: downloading ' . (join ', ', keys %pkgs_to_install_from_outside) . "...\n";
run_apt_progress ('apt-get', '--yes',
'-oApt::Get::Download-Only=true',
'install', (keys %pkgs_to_install_from_outside));
my @debs_to_install;
my $apt_archives = "/var/cache/apt/archives/";
opendir my $dh, "$options->{root}/$apt_archives" or die "cannot read $apt_archives";
while (my $deb = readdir $dh) {
if ($deb !~ /\.deb$/) {
next;
}
$deb = "$apt_archives/$deb";
if (!-f "$options->{root}/$deb") {
next;
}
push @debs_to_install, $deb;
}
close $dh;
if (scalar @debs_to_install == 0) {
die "nothing got downloaded";
}
# we need --force-depends because dpkg does not take Pre-Depends
# into account and thus doesn't install them in the right order
print STDERR 'I: installing ' . (join ', ', keys %pkgs_to_install_from_outside) . "...\n";
run_dpkg_progress [@debs_to_install], @chrootcmd, 'dpkg', '--install', '--force-depends';
foreach my $deb (@debs_to_install) {
unlink "$options->{root}/$deb" or die "cannot unlink $deb";
}
}
# if more than essential should be installed, make the system look
# more like a real one by creating or bind-mounting the device nodes
foreach my $file (@devfiles) {
my ($fname, $mode, $type, $linkname, $devmajor, $devminor) = @{$file};
next if $fname eq './dev/';
if ($type == 0) { # normal file
die "type 0 not implemented";
} elsif ($type == 1) { # hardlink
die "type 1 not implemented";
} elsif ($type == 2) { # symlink
if (!$options->{havemknod}) {
symlink $linkname, "$options->{root}/$fname" or die "cannot create symlink $fname";
}
} elsif ($type == 3 or $type == 4) { # character/block special
if (!$options->{havemknod}) {
open my $fh, '>', "$options->{root}/$fname" or die "cannot open $options->{root}/$fname: $!";
close $fh;
0 == system('mount', '-o', 'bind', "/$fname", "$options->{root}/$fname") or die "mount failed: $?";
}
} elsif ($type == 5) { # directory
if (!$options->{havemknod}) {
make_path "$options->{root}/$fname";
chmod $mode, "$options->{root}/$fname" or die "cannot chmod $fname: $!";
}
0 == system('mount', '-o', 'bind', "/$fname", "$options->{root}/$fname") or die "mount failed: $?";
} else {
die "unsupported type: $type";
}
}
# We can only mount /proc and /sys after extracting the essential
# set because if we mount it before, then base-files will not be able
# to extract those
if ($options->{mode} eq 'unshare') {
# without the network namespace unshared, we cannot mount a new
# sysfs. Since we need network, we just bind-mount.
#
# we have to rbind because just using bind results in "wrong fs
# type, bad option, bad superblock" error
0 == system('mount', '-o', 'rbind', '/sys', "$options->{root}/sys") or die "mount failed: $?";
} elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) {
0 == system('mount', '-t', 'sysfs', '-o', 'nosuid,nodev,noexec', 'sys', "$options->{root}/sys") or die "mount failed: $?";
} else {
die "unknown mode: $options->{mode}";
}
0 == system('mount', '-t', 'proc', 'proc', "$options->{root}/proc") or die "mount failed: $?";
# prevent daemons from starting
{
open my $fh, '>', "$options->{root}/usr/sbin/policy-rc.d" or die "cannot open policy-rc.d: $!";
print $fh "#!/bin/sh\n";
print $fh "exit 101\n";
close $fh;
chmod 0755, "$options->{root}/usr/sbin/policy-rc.d" or die "cannot chmod policy-rc.d: $!";
}
{
move("$options->{root}/sbin/start-stop-daemon", "$options->{root}/sbin/start-stop-daemon.REAL") or die "cannot move start-stop-daemon";
open my $fh, '>', "$options->{root}/sbin/start-stop-daemon" or die "cannot open policy-rc.d: $!";
print $fh "#!/bin/sh\n";
print $fh "echo \"Warning: Fake start-stop-daemon called, doing nothing\">&2\n";
close $fh;
chmod 0755, "$options->{root}/sbin/start-stop-daemon" or die "cannot chmod start-stop-daemon: $!";
}
print STDERR "I: installing remaining packages inside the chroot...\n";
run_apt_progress @chrootcmd, 'apt-get', '--yes', 'install', keys %pkgs_to_install;
# cleanup
move("$options->{root}/sbin/start-stop-daemon.REAL", "$options->{root}/sbin/start-stop-daemon") or die "cannot move start-stop-daemon";
unlink "$options->{root}/usr/sbin/policy-rc.d" or die "cannot unlink policy-rc.d";
foreach my $file (@devfiles) {
my ($fname, undef, $type, $linkname, undef, undef) = @{$file};
next if $fname eq './dev/';
if ($type == 0) { # normal file
die "type 0 not implemented";
} elsif ($type == 1) { # hardlink
die "type 1 not implemented";
} elsif ($type == 2) { # symlink
if (!$options->{havemknod}) {
unlink "$options->{root}/$fname" or die "cannot unlink $fname: $!";
}
} elsif ($type == 3 or $type == 4) { # character/block special
if (!$options->{havemknod}) {
if ($options->{mode} eq 'unshare') {
0 == system('umount', '--no-mtab', "$options->{root}/$fname") or die "umount failed: $?";
} elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) {
0 == system('umount', "$options->{root}/$fname") or die "umount failed: $?";
} else {
die "unknown mode: $options->{mode}";
}
unlink "$options->{root}/$fname";
}
} elsif ($type == 5) { # directory
if ($options->{mode} eq 'unshare') {
0 == system('umount', '--no-mtab', "$options->{root}/$fname") or die "umount failed: $?";
} elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) {
0 == system('umount', "$options->{root}/$fname") or die "umount failed: $?";
} else {
die "unknown mode: $options->{mode}";
}
if (!$options->{havemknod}) {
rmdir "$options->{root}/$fname" or die "cannot rmdir $fname: $!";
}
} else {
die "unsupported type: $type";
}
}
# naturally we have to clean up after ourselves in sudo mode where we
# do a real mount. But we also need to unmount in unshare mode because
# otherwise, even with the --one-file-system tar option, the
# permissions of the mount source will be stored and not the mount
# target (the directory)
if ($options->{mode} eq 'unshare') {
# since we cannot write to /etc/mtab we need --no-mtab
# unmounting /sys only seems to be successful with --lazy
0 == system('umount', '--no-mtab', '--lazy', "$options->{root}/sys") or die "umount failed: $?";
0 == system('umount', '--no-mtab', "$options->{root}/proc") or die "umount failed: $?";
} elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) {
0 == system('umount', "$options->{root}/sys") or die "umount failed: $?";
0 == system('umount', "$options->{root}/proc") or die "umount failed: $?";
} else { } else {
die "unknown mode: $options->{mode}"; die "unknown mode: $options->{mode}";
} }
if (!$options->{havemknod}) {
rmdir "$options->{root}/$fname" or die "cannot rmdir $fname: $!";
}
} else {
die "unsupported type: $type";
} }
}
# naturally we have to clean up after ourselves in sudo mode where we
# do a real mount. But we also need to unmount in unshare mode because
# otherwise, even with the --one-file-system tar option, the
# permissions of the mount source will be stored and not the mount
# target (the directory)
if ($options->{mode} eq 'unshare') {
# since we cannot write to /etc/mtab we need --no-mtab
# unmounting /sys only seems to be successful with --lazy
0 == system('umount', '--no-mtab', '--lazy', "$options->{root}/sys") or die "umount failed: $?";
0 == system('umount', '--no-mtab', "$options->{root}/proc") or die "umount failed: $?";
} elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) {
0 == system('umount', "$options->{root}/sys") or die "umount failed: $?";
0 == system('umount', "$options->{root}/proc") or die "umount failed: $?";
} else { } else {
die "unknown mode: $options->{mode}"; die "unknown variant: $options->{variant}";
} }
} else {
die "unknown mode: $options->{mode}";
} }
# clean up temporary configuration file # clean up temporary configuration file
@ -1246,8 +1295,8 @@ sub main() {
'aptopt=s@' => \$options->{aptopts}, 'aptopt=s@' => \$options->{aptopts},
) or pod2usage(-exitval => 2, -verbose => 1); ) or pod2usage(-exitval => 2, -verbose => 1);
my @valid_variants = ('essential', 'apt', 'required', 'minbase', 'buildd', my @valid_variants = ('extract', 'custom', 'essential', 'apt', 'required',
'important', 'debootstrap', '-', 'standard'); 'minbase', 'buildd', 'important', 'debootstrap', '-', 'standard');
if (none { $_ eq $options->{variant}} @valid_variants) { if (none { $_ eq $options->{variant}} @valid_variants) {
die "invalid variant. Choose from " . (join ', ', @valid_variants); die "invalid variant. Choose from " . (join ', ', @valid_variants);
} }
@ -1264,7 +1313,8 @@ sub main() {
if ($options->{mode} eq 'sudo') { if ($options->{mode} eq 'sudo') {
$options->{mode} = 'root'; $options->{mode} = 'root';
} }
my @valid_modes = ('auto', 'root', 'unshare', 'fakechroot', 'proot'); my @valid_modes = ('auto', 'root', 'unshare', 'fakechroot', 'proot',
'chrootless');
if (none { $_ eq $options->{mode} } @valid_modes) { if (none { $_ eq $options->{mode} } @valid_modes) {
die "invalid mode. Choose from " . (join ', ', @valid_modes); die "invalid mode. Choose from " . (join ', ', @valid_modes);
} }
@ -1492,6 +1542,8 @@ sub main() {
} }
exit 1; exit 1;
} }
} elsif ($options->{mode} eq 'chrootless') {
# nothing to do
} else { } else {
die "unknown mode: $options->{mode}"; die "unknown mode: $options->{mode}";
} }
@ -1502,6 +1554,9 @@ sub main() {
$options->{maketar} = 0; $options->{maketar} = 0;
if (scalar @tar_compress_opts > 0 or $options->{target} =~ /\.tar$/ or $options->{target} eq '-') { if (scalar @tar_compress_opts > 0 or $options->{target} =~ /\.tar$/ or $options->{target} eq '-') {
$options->{maketar} = 1; $options->{maketar} = 1;
if (any { $_ eq $options->{variant} } ('extract', 'custom') and $options->{mode} eq 'fakechroot') {
print STDERR "I: creating a tarball in fakechroot mode might fail in extract and custom variants because there might be no tar inside the chroot\n";
}
} }
if ($options->{maketar}) { if ($options->{maketar}) {
@ -1572,7 +1627,7 @@ sub main() {
} \@idmap; } \@idmap;
waitpid $pid, 0; waitpid $pid, 0;
$? == 0 or die "havemknod failed"; $? == 0 or die "havemknod failed";
} elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) { } elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot', 'chrootless')) {
$options->{havemknod} = havemknod($options->{root}); $options->{havemknod} = havemknod($options->{root});
} else { } else {
die "unknown mode: $options->{mode}"; die "unknown mode: $options->{mode}";
@ -1639,7 +1694,7 @@ sub main() {
exit 0; exit 0;
} \@idmap; } \@idmap;
} elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) { } elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot', 'chrootless')) {
$pid = fork() // die "fork() failed: $!"; $pid = fork() // die "fork() failed: $!";
if ($pid == 0) { if ($pid == 0) {
close $rfh; close $rfh;
@ -1668,7 +1723,7 @@ sub main() {
# proot requires tar to run inside proot or otherwise # proot requires tar to run inside proot or otherwise
# permissions will be completely off # permissions will be completely off
0 == system('proot', '--root-id', "--rootfs=$options->{root}", '--cwd=/', 'tar', @taropts, '--exclude=./dev', '-C', '/', '.') or die "tar failed: $?"; 0 == system('proot', '--root-id', "--rootfs=$options->{root}", '--cwd=/', 'tar', @taropts, '--exclude=./dev', '-C', '/', '.') or die "tar failed: $?";
} elsif (any { $_ eq $options->{mode} } ('root')) { } elsif (any { $_ eq $options->{mode} } ('root', 'chrootless')) {
0 == system('tar', @taropts, '-C', $options->{root}, '.') or die "tar failed: $?"; 0 == system('tar', @taropts, '-C', $options->{root}, '.') or die "tar failed: $?";
} else { } else {
die "unknown mode: $options->{mode}"; die "unknown mode: $options->{mode}";
@ -1719,7 +1774,7 @@ sub main() {
} \@idmap; } \@idmap;
waitpid $pid, 0; waitpid $pid, 0;
$? == 0 or die "remove_tree failed"; $? == 0 or die "remove_tree failed";
} elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) { } elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot', 'chrootless')) {
# without unshare, we use the system's rm to recursively remove the # without unshare, we use the system's rm to recursively remove the
# temporary directory just to make sure that we do not accidentally # temporary directory just to make sure that we do not accidentally
# remove more than we should by using --one-file-system. # remove more than we should by using --one-file-system.
@ -1797,10 +1852,10 @@ Print this help text and exit.
=item B<--variant> =item B<--variant>
Choose which package set to install. Valid variant names are B<essential>, Choose which package set to install. Valid variant names are B<extract>,
B<apt>, B<required>, B<minbase>, B<buildd>, B<important>, B<debootstrap>, B<custom>, B<essential>, B<apt>, B<required>, B<minbase>, B<buildd>,
B<->, and B<standard>. The default variant is B<required>. See the section B<important>, B<debootstrap>, B<->, and B<standard>. The default variant is
B<VARIANTS> for more information. B<required>. See the section B<VARIANTS> for more information.
=item B<--mode> =item B<--mode>
@ -1837,9 +1892,15 @@ Example: --dpkgopt="path-exclude=/usr/share/man/*"
=item B<--include> =item B<--include>
Comma separated list of packages which will be installed in addition to the Comma separated list of packages which will be installed in addition to the
packages installed by the specified variant. This option is incompatible with packages installed by the specified variant. The direct and indirect hard
the essential variant because apt inside the chroot is needed to install extra dependencies will also be installed. The behaviour of this option depends on
packages. the selected variant. The B<extract> and B<custom> variants install no packages
by default, so for these variants, the packages specified by this option will
be the only ones that get either extracted or installed by dpkg, respectively.
For all other variants, apt is used to install the additional packages. The
B<essential> variant does not include apt and thus, the include option will
only work when the B<chrootless> mode is selected and thus apt from the outside
can be used.
=item B<--components> =item B<--components>
@ -1897,17 +1958,37 @@ permissions are only retained while proot is still running, this will lead to
wrong permissions in the final directory and tarball. This mode is useful if wrong permissions in the final directory and tarball. This mode is useful if
you plan to use the chroot with proot. you plan to use the chroot with proot.
=item B<chrootless>
Uses the dpkg option C<--force-script-chrootless> to install packages into
B<TARGET> without dpkg and apt inside B<target> but using apt and dpkg from
the machine running mmdebstrap. Maintainer scripts are run without chrooting
into B<TARGET> and rely on their dependencies being installed on the machine
running mmdebstrap.
=back =back
=head1 VARIANTS =head1 VARIANTS
All package sets also include the hard dependencies (but not recommends) of All package sets also include the direct and indirect hard dependencies (but
the selected package sets. The variants B<minbase>, B<buildd> and B<->, not recommends) of the selected package sets. The variants B<minbase>,
resemble the package sets that debootstrap would install with the same B<buildd> and B<->, resemble the package sets that debootstrap would install
I<--variant> argument. with the same I<--variant> argument.
=over 8 =over 8
=item B<extract>
Installs nothing by default (not even C<Essential:yes> packages). Packages
given by the C<--include> option are extracted but will not be installed.
=item B<custom>
Installs nothing by default (not even C<Essential:yes> packages). Packages
given by the C<--include> option will be installed. If another mode than
B<chrootless> was selected and dpkg was not part of the included package set,
then this variant will fail because it cannot configure the packages.
=item B<essential> =item B<essential>
C<Essential:yes> packages. C<Essential:yes> packages.