Add chrootless mode and extract and custom variants

pull/1/head
parent 07f0e53081
commit 7534a7607f
Signed by: josch
GPG Key ID: F2CBA5C78FBD83E1

@ -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";
if ($options->{havemknod}) { print_progress 0.0;
foreach my $file (@devfiles) { my $counter = 0;
my ($fname, $mode, $type, $linkname, $devmajor, $devminor) = @{$file}; my $total = scalar @essential_pkgs;
if ($type == 0) { # normal file foreach my $deb (@essential_pkgs) {
die "type 0 not implemented"; $counter += 1;
} elsif ($type == 1) { # hardlink # not using dpkg-deb --extract as that would replace the
die "type 1 not implemented"; # merged-usr symlinks with plain directories
} elsif ($type == 2) { # symlink pipe my $rfh, my $wfh;
symlink $linkname, "$options->{root}/$fname" or die "cannot create symlink $fname"; my $pid1 = fork() // die "fork() failed: $!";
next; # chmod cannot work on symlinks if ($pid1 == 0) {
} elsif ($type == 3) { # character special open(STDOUT, '>&', $wfh);
0 == system('mknod', "$options->{root}/$fname", 'c', $devmajor, $devminor) or die "mknod failed: $?"; exec 'dpkg-deb', '--fsys-tarfile', "$options->{root}/$deb";
} 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: $!"; my $pid2 = fork() // die "fork() failed: $!";
} if ($pid2 == 0) {
} open(STDIN, '<&', $rfh);
exec 'tar', '-C', $options->{root}, '--keep-directory-symlink', '--extract', '--file', '-';
# install the extracted packages properly }
# we need --force-depends because dpkg does not take Pre-Depends into waitpid($pid1, 0);
# account and thus doesn't install them in the right order $? == 0 or die "dpkg-deb --fsys-tarfile failed: $?";
print STDERR "I: installing packages...\n"; waitpid($pid2, 0);
run_dpkg_progress [@essential_pkgs], @chrootcmd, 'dpkg', '--install', '--force-depends'; $? == 0 or die "tar --extract failed: $?";
print_progress ($counter/$total*100);
# 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';
} }
} print_progress "done";
foreach my $deb (@essential_pkgs) {
unlink "$options->{root}/$deb" or die "cannot unlink $deb";
}
if (%pkgs_to_install) { if ($options->{variant} eq 'extract') {
# some packages have to be installed from the outside before anything # nothing else to do
# can be installed from the inside. } elsif (any { $_ eq $options->{variant} } ('custom', 'essential', 'apt', 'standard', 'important', 'required', 'buildd', 'minbase')) {
# if ($options->{mode} eq 'fakechroot') {
# we do not need to install any *-archive-keyring packages inside the # FIXME: if trouble arises, look into /etc/fakechroot/*.env for
# chroot prior to installing the packages, because the keyring is only # more interesting variables to set
# used when doing "apt-get update" and that was already done at the $ENV{FAKECHROOT_CMD_SUBST} = join ':', (
# beginning using key material from the outside. Since the apt cache '/bin/mount=/bin/true',
# is already filled and we are not calling "apt-get update" again, the '/usr/bin/ldd=/usr/bin/ldd.fakechroot',
# keyring can be installed later during installation. But: if it's not '/usr/bin/mkfifo=/bin/true',
# installed during installation, then we might end up with a fully '/usr/sbin/ldconfig=/bin/true',
# 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 # make sure that APT_CONFIG is not set when executing anything inside the
# apt-transport-https and ca-certificates gets installed first if any # chroot
# mirror is a https URI my @chrootcmd = ('env', '--unset=APT_CONFIG');
open(my $pipe_apt, '-|', 'apt-get', 'indextargets', '--format', '$(URI)', 'Created-By: Packages') or die "cannot start apt-get indextargets: $!"; if ($options->{mode} eq 'proot') {
while (my $uri = <$pipe_apt>) { # FIXME: proot currently cannot install apt because of https://github.com/proot-me/PRoot/issues/147
if ($uri =~ /^https:\/\//) { push @chrootcmd, ('proot', '--root-id', '--bind=/dev', "--rootfs=$options->{root}", '--cwd=/');
# FIXME: support for https is part of apt >= 1.5 } elsif (any { $_ eq $options->{mode} } ('root', 'unshare', 'fakechroot')) {
$pkgs_to_install_from_outside{'apt-transport-https'} = (); push @chrootcmd, ('/usr/sbin/chroot', $options->{root});
$pkgs_to_install_from_outside{'ca-certificates'} = (); } else {
last; die "unknown mode: $options->{mode}";
} 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; # copy qemu-user-static binary into chroot or setup proot with --qemu
$? == 0 or die "apt-get indextargets failed"; if (defined $options->{qemu}) {
if ($options->{mode} eq 'proot') {
if (%pkgs_to_install_from_outside) { push @chrootcmd, "--qemu=qemu-$options->{qemu}";
print STDERR 'I: downloading ' . (join ', ', keys %pkgs_to_install_from_outside) . "...\n"; } elsif ($options->{mode} eq 'fakechroot') {
run_apt_progress ('apt-get', '--yes', # The binfmt support on the outside is used, so qemu needs to know
'-oApt::Get::Download-Only=true', # where it has to look for shared libraries
'install', (keys %pkgs_to_install_from_outside)); $ENV{QEMU_LD_PREFIX} = $options->{root};
my @debs_to_install; # Make sure that the fakeroot and fakechroot shared libraries
my $apt_archives = "/var/cache/apt/archives/"; # exist for the right architecture
opendir my $dh, "$options->{root}/$apt_archives" or die "cannot read $apt_archives"; open my $fh, '-|', 'dpkg-architecture', '-a', $options->{nativearch}, '-qDEB_HOST_MULTIARCH' // die "failed to fork(): $!";
while (my $deb = readdir $dh) { chomp (my $deb_host_multiarch = do { local $/; <$fh> });
if ($deb !~ /\.deb$/) { close $fh;
next; if ($? != 0 or !$deb_host_multiarch) {
} die "dpkg-architecture failed: $?";
$deb = "$apt_archives/$deb"; }
if (!-f "$options->{root}/$deb") { my $fakechrootdir = "/usr/lib/$deb_host_multiarch/fakechroot";
next; 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 {
die "unknown mode: $options->{mode}";
} }
push @debs_to_install, $deb;
} }
close $dh;
if (scalar @debs_to_install == 0) { # install the extracted packages properly
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 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';
}
} }
# we need --force-depends because dpkg does not take Pre-Depends
# into account and thus doesn't install them in the right order foreach my $deb (@essential_pkgs) {
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"; unlink "$options->{root}/$deb" or die "cannot unlink $deb";
} }
}
# if more than essential should be installed, make the system look if (%pkgs_to_install) {
# more like a real one by creating or bind-mounting the device nodes # some packages have to be installed from the outside before anything
foreach my $file (@devfiles) { # can be installed from the inside.
my ($fname, $mode, $type, $linkname, $devmajor, $devminor) = @{$file}; #
next if $fname eq './dev/'; # we do not need to install any *-archive-keyring packages inside the
if ($type == 0) { # normal file # chroot prior to installing the packages, because the keyring is only
die "type 0 not implemented"; # used when doing "apt-get update" and that was already done at the
} elsif ($type == 1) { # hardlink # beginning using key material from the outside. Since the apt cache
die "type 1 not implemented"; # is already filled and we are not calling "apt-get update" again, the
} elsif ($type == 2) { # symlink # keyring can be installed later during installation. But: if it's not
if (!$options->{havemknod}) { # installed during installation, then we might end up with a fully
symlink $linkname, "$options->{root}/$fname" or die "cannot create symlink $fname"; # 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} = ();
} }
} elsif ($type == 3 or $type == 4) { # character/block special
if (!$options->{havemknod}) { # since apt will be run inside the chroot, make sure that
open my $fh, '>', "$options->{root}/$fname" or die "cannot open $options->{root}/$fname: $!"; # apt-transport-https and ca-certificates gets installed first if any
close $fh; # mirror is a https URI
0 == system('mount', '-o', 'bind', "/$fname", "$options->{root}/$fname") or die "mount failed: $?"; 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;
}
} }
} elsif ($type == 5) { # directory close $pipe_apt;
if (!$options->{havemknod}) { $? == 0 or die "apt-get indextargets failed";
make_path "$options->{root}/$fname";
chmod $mode, "$options->{root}/$fname" or die "cannot chmod $fname: $!"; 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";
}
} }
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 # if more than essential should be installed, make the system look
{ # more like a real one by creating or bind-mounting the device nodes
open my $fh, '>', "$options->{root}/usr/sbin/policy-rc.d" or die "cannot open policy-rc.d: $!"; foreach my $file (@devfiles) {
print $fh "#!/bin/sh\n"; my ($fname, $mode, $type, $linkname, $devmajor, $devminor) = @{$file};
print $fh "exit 101\n"; next if $fname eq './dev/';
close $fh; if ($type == 0) { # normal file
chmod 0755, "$options->{root}/usr/sbin/policy-rc.d" or die "cannot chmod policy-rc.d: $!"; die "type 0 not implemented";
} } elsif ($type == 1) { # hardlink
die "type 1 not implemented";
{ } elsif ($type == 2) { # symlink
move("$options->{root}/sbin/start-stop-daemon", "$options->{root}/sbin/start-stop-daemon.REAL") or die "cannot move start-stop-daemon"; if (!$options->{havemknod}) {
open my $fh, '>', "$options->{root}/sbin/start-stop-daemon" or die "cannot open policy-rc.d: $!"; symlink $linkname, "$options->{root}/$fname" or die "cannot create symlink $fname";
print $fh "#!/bin/sh\n"; }
print $fh "echo \"Warning: Fake start-stop-daemon called, doing nothing\">&2\n"; } elsif ($type == 3 or $type == 4) { # character/block special
close $fh; if (!$options->{havemknod}) {
chmod 0755, "$options->{root}/sbin/start-stop-daemon" or die "cannot chmod start-stop-daemon: $!"; 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: $?";
# allow network access from within }
copy("/etc/resolv.conf", "$options->{root}/etc/resolv.conf") or die "cannot copy /etc/resolv.conf: $!"; } elsif ($type == 5) { # directory
copy("/etc/hostname", "$options->{root}/etc/hostname") or die "cannot copy /etc/hostname: $!"; if (!$options->{havemknod}) {
make_path "$options->{root}/$fname";
print STDERR "I: installing remaining packages inside the chroot...\n"; chmod $mode, "$options->{root}/$fname" or die "cannot chmod $fname: $!";
run_apt_progress @chrootcmd, 'apt-get', '--yes', 'install', keys %pkgs_to_install; }
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: $?";
# cleanup # prevent daemons from starting
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"; 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: $!";
}
foreach my $file (@devfiles) { {
my ($fname, undef, $type, $linkname, undef, undef) = @{$file}; move("$options->{root}/sbin/start-stop-daemon", "$options->{root}/sbin/start-stop-daemon.REAL") or die "cannot move start-stop-daemon";
next if $fname eq './dev/'; open my $fh, '>', "$options->{root}/sbin/start-stop-daemon" or die "cannot open policy-rc.d: $!";
if ($type == 0) { # normal file print $fh "#!/bin/sh\n";
die "type 0 not implemented"; print $fh "echo \"Warning: Fake start-stop-daemon called, doing nothing\">&2\n";
} elsif ($type == 1) { # hardlink close $fh;
die "type 1 not implemented"; chmod 0755, "$options->{root}/sbin/start-stop-daemon" or die "cannot chmod start-stop-daemon: $!";
} 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}) { print STDERR "I: installing remaining packages inside the chroot...\n";
if ($options->{mode} eq 'unshare') { run_apt_progress @chrootcmd, 'apt-get', '--yes', 'install', keys %pkgs_to_install;
0 == system('umount', '--no-mtab', "$options->{root}/$fname") or die "umount failed: $?";
} elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) { # cleanup
0 == system('umount', "$options->{root}/$fname") or die "umount failed: $?"; 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 { } else {
die "unknown mode: $options->{mode}"; die "unsupported type: $type";
} }
unlink "$options->{root}/$fname";
} }
} elsif ($type == 5) { # directory # 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') { if ($options->{mode} eq 'unshare') {
0 == system('umount', '--no-mtab', "$options->{root}/$fname") or die "umount failed: $?"; # 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')) { } elsif (any { $_ eq $options->{mode} } ('root', 'fakechroot', 'proot')) {
0 == system('umount', "$options->{root}/$fname") or die "umount failed: $?"; 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.

Loading…
Cancel
Save