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,12 +586,23 @@ 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', {
my @directories = ('/etc/apt/apt.conf.d', '/etc/apt/sources.list.d',
'/etc/apt/preferences.d', '/var/cache/apt', '/etc/apt/preferences.d', '/var/cache/apt',
'/var/lib/apt/lists/partial', '/var/lib/dpkg', '/var/lib/apt/lists/partial', '/var/lib/dpkg',
'/etc/dpkg/dpkg.cfg.d/') { '/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: $!"; 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
# because they have to be valid for apt invocation from outside as well as # because they have to be valid for apt invocation from outside as well as
@ -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,6 +889,28 @@ sub setup {
die "nothing got downloaded"; die "nothing got downloaded";
} }
if ($options->{mode} eq 'chrootless') {
print STDERR "I: installing packages...\n";
# FIXME: the dpkg config from the host is parsed before the command
# line arguments are parsed and might break this mode
my @chrootless_opts = (
'-oDPkg::Options::=--force-not-root',
'-oDPkg::Options::=--force-script-chrootless',
'-oDPkg::Options::=--root=' . $options->{root},
'-oDPkg::Options::=--log=' . "$options->{root}/var/log/dpkg.log");
run_apt_progress ('apt-get', '--yes', @chrootless_opts,
'install', (map { "$options->{root}/$_" } @essential_pkgs));
if (any { $_ eq $options->{variant} } ('extract', 'custom')) {
# nothing to do
} elsif (any { $_ eq $options->{variant} } ('essential', 'apt', 'standard', 'important', 'required', 'buildd', 'minbase')) {
if (%pkgs_to_install) {
run_apt_progress ('apt-get', '--yes', @chrootless_opts,
'install', keys %pkgs_to_install);
}
} else {
die "unknown variant: $options->{variant}";
}
} elsif (any { $_ eq $options->{mode} } ('root', 'unshare', 'fakechroot', 'proot')) {
print STDERR "I: extracting archives...\n"; print STDERR "I: extracting archives...\n";
print_progress 0.0; print_progress 0.0;
my $counter = 0; my $counter = 0;
@ -868,6 +938,9 @@ sub setup {
} }
print_progress "done"; print_progress "done";
if ($options->{variant} eq 'extract') {
# nothing else to do
} elsif (any { $_ eq $options->{variant} } ('custom', 'essential', 'apt', 'standard', 'important', 'required', 'buildd', 'minbase')) {
if ($options->{mode} eq 'fakechroot') { if ($options->{mode} eq 'fakechroot') {
# FIXME: if trouble arises, look into /etc/fakechroot/*.env for # FIXME: if trouble arises, look into /etc/fakechroot/*.env for
# more interesting variables to set # more interesting variables to set
@ -931,32 +1004,6 @@ sub setup {
} }
} }
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: $!";
}
}
# install the extracted packages properly # install the extracted packages properly
# we need --force-depends because dpkg does not take Pre-Depends into # we need --force-depends because dpkg does not take Pre-Depends into
# account and thus doesn't install them in the right order # account and thus doesn't install them in the right order
@ -1116,10 +1163,6 @@ sub setup {
chmod 0755, "$options->{root}/sbin/start-stop-daemon" or die "cannot chmod start-stop-daemon: $!"; 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"; print STDERR "I: installing remaining packages inside the chroot...\n";
run_apt_progress @chrootcmd, 'apt-get', '--yes', 'install', keys %pkgs_to_install; run_apt_progress @chrootcmd, 'apt-get', '--yes', 'install', keys %pkgs_to_install;
@ -1181,6 +1224,12 @@ sub setup {
die "unknown mode: $options->{mode}"; die "unknown mode: $options->{mode}";
} }
} }
} else {
die "unknown variant: $options->{variant}";
}
} else {
die "unknown mode: $options->{mode}";
}
# clean up temporary configuration file # clean up temporary configuration file
unlink "$options->{root}/etc/apt/apt.conf.d/00mmdebstrap" or die "failed to unlink /etc/apt/apt.conf.d/00mmdebstrap: $!"; unlink "$options->{root}/etc/apt/apt.conf.d/00mmdebstrap" or die "failed to unlink /etc/apt/apt.conf.d/00mmdebstrap: $!";
@ -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.