Compare commits

...

14 commits

Author SHA1 Message Date
9682e74385
release 1.2.0 2022-09-05 06:26:40 +02:00
b0caeeef54
bump dates to 2022 2022-09-05 06:25:24 +02:00
d209fb0c11
reformat with perltidy 2022-09-05 06:21:17 +02:00
f4a3865c00
Remove support for proot.
The proot mode was broken from the start because in contrast to
fakechroot, no ownership information can be retained across multiple
invocations of proot. Since mmdebstrap started using apt from the
outside by setting DPkg::Chroot-Directory in mmdebstrap 0.8.0 proot mode
was finally completely broken because proot cannot wrap the chroot call
done by apt. Users of proot are recommended to run mmdebstrap in
fakechroot mode and then use proot with the resulting directory.
2022-09-05 06:21:17 +02:00
892e568496
coverage.sh: split up linting conditional 2022-09-05 06:21:17 +02:00
b85df6b8f2
coverage.sh: idshift 2022-09-05 06:21:17 +02:00
7e8931578b
Store --include option values in MMDEBSTRAP_INCLUDE for hooks 2022-09-05 06:21:07 +02:00
e1f0b0fa40
ensure operator precedence using more parenthesis 2022-09-02 23:38:53 +02:00
0ff2bef84c
tests/as-debootstrap-unshare-wrapper: redirect output of cmp and diff to stderr to preserve output order 2022-09-02 23:36:44 +02:00
e875bca7fb
support apt patterns and paths with commas and whitespace for the --include option 2022-09-02 23:35:56 +02:00
0af22912f7
also delete everything in /run and add --skip=cleanup/run 2022-09-02 23:29:52 +02:00
add9412a47
add --skip=chroot/mount and --skip=chroot/mount/dev, --skip=chroot/mount/proc, --skip=chroot/mount/sys 2022-09-02 23:27:27 +02:00
e61e352f67
add --skip=chroot/start-stop-daemon and --skip=chroot/policy-rc.d 2022-09-02 23:25:48 +02:00
18c1e9bbc5
multiple skip options can be passed by separating them by comma or whitespace 2022-09-02 23:23:53 +02:00
11 changed files with 418 additions and 322 deletions

View file

@ -1,3 +1,18 @@
1.2.0 (2022-09-05)
------------------
- remove proot mode
- error out if stdout is an interactive terminal
- replace taridshift by tarfilter --idshift
- tarfilter: add --transform option
- multiple --skip options can be separated by comma or whitespace
- also cleanup the contents of /run
- support apt patterns and paths with commas and whitespace in --include
- hooks: store the values of the --include option in MMDEBSTRAP_INCLUDE
- add new --skip options: chroot/start-stop-daemon, chroot/policy-rc.d
chroot/mount, chroot/mount/dev, chroot/mount/proc, chroot/mount/sys,
cleanup/run
1.1.0 (2022-07-26)
----------------

View file

@ -34,7 +34,7 @@ Summary:
- chroot with apt in 11 seconds
- gzipped tarball with apt is 27M small
- bit-by-bit reproducible output
- unprivileged operation using Linux user namespaces, fakechroot or proot
- unprivileged operation using Linux user namespaces or fakechroot
- can operate on filesystems mounted with nodev
- foreign architecture chroots with qemu-user
- variant installing only Essential:yes packages and dependencies
@ -78,9 +78,9 @@ privileges to create a file (the chroot tarball) in one's home directory.
Thus, mmdebstrap provides multiple options to create a chroot tarball with the
right permissions **without superuser privileges**. This avoids a whole class
of bugs like #921815. Depending on what is available, it uses either Linux user
namespaces, fakechroot or proot. Debootstrap supports fakechroot but will not
namespaces or fakechroot. Debootstrap supports fakechroot but will not
create a tarball with the right permissions by itself. Support for Linux user
namespaces and proot is missing (see bugs #829134 and #698347, respectively).
namespaces is missing (see #829134).
When creating a chroot tarball with debootstrap, the temporary chroot directory
cannot be on a filesystem that has been mounted with nodev. In unprivileged

View file

@ -2,7 +2,7 @@
set -eu
if [ -e ./mmdebstrap -a -e ./taridshift -a -e ./tarfilter -a -e ./coverage.py ]; then
if [ -e ./mmdebstrap ]; then
TMPFILE=$(mktemp)
perltidy < ./mmdebstrap > "$TMPFILE"
ret=0
@ -20,10 +20,11 @@ if [ -e ./mmdebstrap -a -e ./taridshift -a -e ./tarfilter -a -e ./coverage.py ];
fi
perlcritic --severity 4 --verbose 8 ./mmdebstrap
black --check ./taridshift ./tarfilter ./coverage.py
fi
[ -e ./tarfilter ] && black --check ./tarfilter
[ -e ./coverage.py ] && black --check ./coverage.py
mirrordir="./shared/cache/debian"
if [ ! -e "$mirrordir" ]; then
@ -113,4 +114,4 @@ END
echo
fi
rm shared/test.sh shared/tar1.txt shared/tar2.txt shared/pkglist.txt shared/doc-debian.tar.list shared/mmdebstrap shared/taridshift shared/tarfilter shared/proxysolver
rm shared/test.sh shared/tar1.txt shared/tar2.txt shared/pkglist.txt shared/doc-debian.tar.list shared/mmdebstrap shared/tarfilter shared/proxysolver

View file

@ -264,6 +264,11 @@ Test: not-having-to-install-apt-in-include-because-a-hook-did-it-before
Test: remove-start-stop-daemon-and-policy-rc-d-in-hook
Test: skip-start-stop-daemon-policy-rc
Test: skip-mount
Modes: unshare
Test: compare-output-with-pre-seeded-var-cache-apt-archives
Needs-QEMU: true
Variants: any

View file

@ -1,6 +1,6 @@
#!/usr/bin/perl
#
# © 2018 - 2021 Johannes Schauer Marin Rodrigues <josch@mister-muffin.de>
# © 2018 - 2022 Johannes Schauer Marin Rodrigues <josch@mister-muffin.de>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
@ -23,7 +23,7 @@
use strict;
use warnings;
our $VERSION = '1.1.0';
our $VERSION = '1.2.0';
use English;
use Getopt::Long;
@ -1104,8 +1104,12 @@ sub run_chroot {
}
} elsif ($type == 3 or $type == 4) {
# character/block special
if ((any { $_ eq $options->{mode} } ('root', 'unshare'))
&& !$options->{canmount}) {
if (
any { $_ =~ '^chroot/mount(?:/dev)?$' }
@{ $options->{skip} }
) {
info "skipping chroot/mount/dev as requested";
} elsif (!$options->{canmount}) {
warning "skipping bind-mounting ./dev/$fname";
} elsif (!$options->{havemknod}) {
if (!-d "$options->{root}/dev") {
@ -1160,15 +1164,21 @@ sub run_chroot {
"$options->{root}/dev/$fname")
or error "mount ./dev/$fname failed: $?";
}
} elsif ($type == 5
&& (any { $_ eq $options->{mode} } ('root', 'unshare'))
&& !$options->{canmount}) {
} elsif ($type == 5) {
# directory
if (
any { $_ =~ '^chroot/mount(?:/dev)?$' }
@{ $options->{skip} }
) {
info "skipping chroot/mount/dev as requested";
} elsif (!$options->{canmount}) {
warning "skipping bind-mounting ./dev/$fname";
} elsif ($type == 5) { # directory
} else {
if (!-d "$options->{root}/dev") {
warning(
"skipping creation of ./dev/$fname because the"
. " /dev directory is missing in the target");
. " /dev directory is missing in the target"
);
next;
}
if (!-e "/dev/$fname" && $fname ne "pts/") {
@ -1192,7 +1202,8 @@ sub run_chroot {
};
if (-e "$options->{root}/dev/$fname") {
if (!-d "$options->{root}/dev/$fname") {
error "./dev/$fname already exists but is not"
error
"./dev/$fname already exists but is not"
. " a directory";
}
} else {
@ -1209,8 +1220,8 @@ sub run_chroot {
} @$err
));
} elsif ($num_created == 0) {
error
"cannot create $options->{root}/dev/$fname";
error( "cannot create $options->{root}"
. "/dev/$fname");
}
}
chmod $mode, "$options->{root}/dev/$fname"
@ -1251,17 +1262,13 @@ sub run_chroot {
"$options->{root}/dev/$fname")
or error "mount ./dev/$fname failed: $?";
}
}
} else {
error "unsupported type: $type";
}
}
} elsif (
any { $_ eq $options->{mode} }
('proot', 'fakechroot', 'chrootless')
) {
# we cannot mount in fakechroot and proot mode
# in proot mode we have /dev bind-mounted already through
# --bind=/dev
} elsif (any { $_ eq $options->{mode} } ('fakechroot', 'chrootless')) {
# we cannot mount in fakechroot mode
} else {
error "unknown mode: $options->{mode}";
}
@ -1269,6 +1276,10 @@ sub run_chroot {
# set because if we mount it before, then base-files will not be able
# to extract those
if ( (any { $_ eq $options->{mode} } ('root', 'unshare'))
&& (any { $_ =~ '^chroot/mount(?:/sys)?$' } @{ $options->{skip} }))
{
info "skipping chroot/mount/sys as requested";
} elsif ((any { $_ eq $options->{mode} } ('root', 'unshare'))
&& !$options->{canmount}) {
warning "skipping mount sysfs";
} elsif ((any { $_ eq $options->{mode} } ('root', 'unshare'))
@ -1333,17 +1344,17 @@ sub run_chroot {
# type, bad option, bad superblock" error
0 == system('mount', '-o', 'rbind', '/sys', "$options->{root}/sys")
or error "mount /sys failed: $?";
} elsif (
any { $_ eq $options->{mode} }
('proot', 'fakechroot', 'chrootless')
) {
# we cannot mount in fakechroot and proot mode
# in proot mode we have /proc bind-mounted already through
# --bind=/proc
} elsif (any { $_ eq $options->{mode} } ('fakechroot', 'chrootless')) {
# we cannot mount in fakechroot mode
} else {
error "unknown mode: $options->{mode}";
}
if ((any { $_ eq $options->{mode} } ('root', 'unshare'))
if (
(any { $_ eq $options->{mode} } ('root', 'unshare'))
&& (any { $_ =~ '^chroot/mount(?:/proc)?$' } @{ $options->{skip} })
) {
info "skipping chroot/mount/proc as requested";
} elsif ((any { $_ eq $options->{mode} } ('root', 'unshare'))
&& !$options->{canmount}) {
warning "skipping mount proc";
} elsif ((any { $_ eq $options->{mode} } ('root', 'unshare'))
@ -1408,13 +1419,8 @@ sub run_chroot {
};
0 == system('mount', '-t', 'proc', 'proc', "$options->{root}/proc")
or error "mount /proc failed: $?";
} elsif (
any { $_ eq $options->{mode} }
('proot', 'fakechroot', 'chrootless')
) {
# we cannot mount in fakechroot and proot mode
# in proot mode we have /sys bind-mounted already through
# --bind=/sys
} elsif (any { $_ eq $options->{mode} } ('fakechroot', 'chrootless')) {
# we cannot mount in fakechroot mode
} else {
error "unknown mode: $options->{mode}";
}
@ -1426,6 +1432,9 @@ sub run_chroot {
# existing inside the chroot
#
# See #911290 for more problems of this interface
if (any { $_ eq 'chroot/policy-rc.d' } @{ $options->{skip} }) {
info "skipping chroot/policy-rc.d as requested";
} else {
if (-d "$options->{root}/usr/sbin/") {
open my $fh, '>', "$options->{root}/usr/sbin/policy-rc.d"
or error "cannot open policy-rc.d: $!";
@ -1435,11 +1444,16 @@ sub run_chroot {
chmod 0755, "$options->{root}/usr/sbin/policy-rc.d"
or error "cannot chmod policy-rc.d: $!";
}
}
# the file might not exist if it was removed in a hook
if (any { $_ eq 'chroot/start-stop-daemon' } @{ $options->{skip} }) {
info "skipping chroot/start-stop-daemon as requested";
} else {
if (-f "$options->{root}/sbin/start-stop-daemon") {
if (-e "$options->{root}/sbin/start-stop-daemon.REAL") {
error "$options->{root}/sbin/start-stop-daemon.REAL already"
error
"$options->{root}/sbin/start-stop-daemon.REAL already"
. " exists";
}
move(
@ -1449,26 +1463,36 @@ sub run_chroot {
open my $fh, '>', "$options->{root}/sbin/start-stop-daemon"
or error "cannot open start-stop-daemon: $!";
print $fh "#!/bin/sh\n";
print $fh "echo \"Warning: Fake start-stop-daemon called, doing"
print $fh
"echo \"Warning: Fake start-stop-daemon called, doing"
. " nothing\">&2\n";
close $fh;
chmod 0755, "$options->{root}/sbin/start-stop-daemon"
or error "cannot chmod start-stop-daemon: $!";
}
}
&{$cmd}();
# cleanup
if (any { $_ eq 'chroot/start-stop-daemon' } @{ $options->{skip} }) {
info "skipping chroot/start-stop-daemon as requested";
} else {
if (-e "$options->{root}/sbin/start-stop-daemon.REAL") {
move(
"$options->{root}/sbin/start-stop-daemon.REAL",
"$options->{root}/sbin/start-stop-daemon"
) or error "cannot move start-stop-daemon: $!";
}
}
if (any { $_ eq 'chroot/policy-rc.d' } @{ $options->{skip} }) {
info "skipping chroot/policy-rc.d as requested";
} else {
if (-f "$options->{root}/usr/sbin/policy-rc.d") {
unlink "$options->{root}/usr/sbin/policy-rc.d"
or error "cannot unlink policy-rc.d: $!";
}
}
};
@ -1530,6 +1554,20 @@ sub run_hooks {
# Store the verbosity of mmdebstrap so that hooks can be just as verbose
# as the mmdebstrap invocation that called them.
push @env_opts, ("MMDEBSTRAP_VERBOSITY=" . $verbosity_level);
# Store the packages given via --include in an environment variable so that
# hooks can, for example, make .deb files available inside the chroot.
{
my @escaped_includes = @{ $options->{include} };
foreach my $incl (@escaped_includes) {
# We have to encode commas so that values containing commas can
# be stored in the list. Since we encode using percent-encoding
# (urlencoding) we also have to encode the percent sign.
$incl =~ s/%/%25/g;
$incl =~ s/,/%2C/g;
}
push @env_opts,
("MMDEBSTRAP_INCLUDE=" . (join ",", @escaped_includes));
}
my $runner = sub {
foreach my $script (@{ $options->{"${name}_hook"} }) {
@ -1542,13 +1580,11 @@ sub run_hooks {
)\ /x
) {
info "running special hook: $script";
if (
any { $_ eq $options->{variant} } ('extract', 'custom')
and any { $_ eq $options->{mode} }
('fakechroot', 'proot') and $name ne 'setup'
) {
if ((any { $_ eq $options->{variant} } ('extract', 'custom'))
and $options->{mode} eq 'fakechroot'
and $name ne 'setup') {
info "the copy-in, copy-out, tar-in and tar-out commands"
. " in fakechroot mode or proot mode might fail in"
. " in fakechroot mode might fail in"
. " extract and custom variants because there might be"
. " no tar inside the chroot";
}
@ -2074,14 +2110,14 @@ sub run_setup() {
copy($tmpfile, \*STDERR);
}
if (none { $_ eq $options->{mode} } ('fakechroot', 'proot')) {
if ($options->{mode} ne 'fakechroot') {
# Apt dropping privileges to another user than root is not useful in
# fakechroot and proot mode because all users are faked and thus there
# is no real privilege difference anyways. We could set
# APT::Sandbox::User "root" in fakechroot and proot mode but we don't
# because if we would, then /var/cache/apt/archives/partial/ and
# /var/lib/apt/lists/partial/ would not be owned by the _apt user
# if mmdebstrap was run in fakechroot or proot mode.
# fakechroot mode because all users are faked and thus there is no real
# privilege difference anyways. We could set APT::Sandbox::User "root"
# in fakechroot mode but we don't because if we would, then
# /var/cache/apt/archives/partial/ and /var/lib/apt/lists/partial/
# would not be owned by the _apt user if mmdebstrap was run in
# fakechroot mode.
#
# when apt-get update is run by the root user, then apt will attempt to
# drop privileges to the _apt user. This will fail if the _apt user
@ -2493,29 +2529,15 @@ sub run_prepare {
# make sure that APT_CONFIG and TMPDIR are not set when executing
# anything inside the chroot
my @chrootcmd = ('env', '--unset=APT_CONFIG', '--unset=TMPDIR');
if ($options->{mode} eq 'proot') {
push @chrootcmd,
(
'proot', '--root-id',
'--bind=/dev', '--bind=/proc',
'--bind=/sys', "--rootfs=$options->{root}",
'--cwd=/'
);
} elsif (
any { $_ eq $options->{mode} }
('root', 'unshare', 'fakechroot')
) {
if (any { $_ eq $options->{mode} } ('root', 'unshare', 'fakechroot')) {
push @chrootcmd, ('chroot', $options->{root});
} else {
error "unknown mode: $options->{mode}";
}
# copy qemu-user-static binary into chroot or setup proot with
# --qemu
# copy qemu-user-static binary into chroot
if (defined $options->{qemu}) {
if ($options->{mode} eq 'proot') {
push @chrootcmd, "--qemu=qemu-$options->{qemu}";
} elsif ($options->{mode} eq 'fakechroot') {
if ($options->{mode} eq 'fakechroot') {
# Make sure that the fakeroot and fakechroot shared
# libraries exist for the right architecture
open my $fh, '-|', 'dpkg-architecture', '-a',
@ -2601,29 +2623,21 @@ sub run_prepare {
}
# some versions of coreutils use the renameat2 system call in mv.
# This breaks certain versions of fakechroot and proot. Here we do
# This breaks certain versions of fakechroot. Here we do
# a sanity check and warn the user in case things might break.
if (any { $_ eq $options->{mode} } ('fakechroot', 'proot')
if ($options->{mode} eq 'fakechroot'
and -e "$options->{root}/bin/mv") {
mkdir "$options->{root}/000-move-me"
or error "cannot create directory: $!";
my $ret = system @chrootcmd, '/bin/mv', '/000-move-me',
'/001-delete-me';
if ($ret != 0) {
if ($options->{mode} eq 'proot') {
info "the /bin/mv binary inside the chroot doesn't"
. " work under proot";
info "this is likely due to missing support for"
. " renameat2 in proot";
info "see https://github.com/proot-me/PRoot/issues/147";
} else {
info "the /bin/mv binary inside the chroot doesn't"
. " work under fakechroot";
info "with certain versions of coreutils and glibc,"
. " this is due to missing support for renameat2 in"
. " fakechroot";
info "see https://github.com/dex4er/fakechroot/issues/60";
}
info "expect package post installation scripts not to work";
rmdir "$options->{root}/000-move-me"
or error "cannot rmdir: $!";
@ -2693,10 +2707,8 @@ sub run_essential() {
'--force-depends'
],
PKGS => [map { "$options->{root}/$_" } @{$essential_pkgs}] });
} elsif (
any { $_ eq $options->{mode} }
('root', 'unshare', 'fakechroot', 'proot')
) {
} elsif (any { $_ eq $options->{mode} } ('root', 'unshare', 'fakechroot'))
{
# 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
@ -2816,10 +2828,8 @@ sub run_install() {
PKGS => [@pkgs_to_install],
});
}
} elsif (
any { $_ eq $options->{mode} }
('root', 'unshare', 'fakechroot', 'proot')
) {
} elsif (any { $_ eq $options->{mode} } ('root', 'unshare', 'fakechroot'))
{
if ($options->{variant} ne 'custom'
and scalar @pkgs_to_install > 0) {
# Advantage of running apt on the outside instead of inside the
@ -3001,10 +3011,31 @@ sub run_cleanup() {
}
}
if (any { $_ eq 'cleanup/run' } @{ $options->{skip} }) {
info "skipping cleanup/run as requested";
} else {
# remove any possible leftovers in /run
if (-d "$options->{root}/run") {
opendir(my $dh, "$options->{root}/run")
or error "Can't opendir($options->{root}/run): $!";
while (my $entry = readdir $dh) {
# skip the "." and ".." entries
next if $entry eq ".";
next if $entry eq "..";
debug "deleting files in /run: $entry";
0 == system(
'rm', '--interactive=never',
'--recursive', '--preserve-root',
'--one-file-system', "$options->{root}/run/$entry"
) or error "rm failed: $?";
}
closedir($dh);
}
}
if (any { $_ eq 'cleanup/tmp' } @{ $options->{skip} }) {
info "skipping cleanup/tmp as requested";
} else {
# remove any possible leftovers in /tmp but warn about it
# remove any possible leftovers in /tmp
if (-d "$options->{root}/tmp") {
opendir(my $dh, "$options->{root}/tmp")
or error "Can't opendir($options->{root}/tmp): $!";
@ -3012,7 +3043,7 @@ sub run_cleanup() {
# skip the "." and ".." entries
next if $entry eq ".";
next if $entry eq "..";
warning "deleting files in /tmp: $entry";
debug "deleting files in /tmp: $entry";
0 == system(
'rm', '--interactive=never',
'--recursive', '--preserve-root',
@ -3105,26 +3136,12 @@ sub hookhelper {
. 'delete=atime,delete=ctime'
);
if ($hook eq 'setup') {
if ($mode eq 'proot') {
# since we cannot run tar inside the chroot under proot during
# the setup hook because the chroot is empty, we have to run
# tar from the outside, which leads to all files being owned
# by the user running mmdebstrap. To let the ownership
# information not be completely off, we force all files be
# owned by the root user.
push @tarcmd, '--owner=0', '--group=0';
}
} elsif (any { $_ eq $hook } ('extract', 'essential', 'customize')) {
if ($mode eq 'fakechroot') {
# Fakechroot requires tar to run inside the chroot or
# otherwise absolute symlinks will include the path to the
# root directory
push @cmdprefix, 'chroot', $root;
} elsif ($mode eq 'proot') {
# proot requires tar to run inside proot or otherwise
# permissions will be completely off
push @cmdprefix, 'proot', '--root-id', "--rootfs=$root",
'--cwd=/', "--qemu=$qemu";
} elsif (any { $_ eq $mode } ('root', 'chrootless', 'unshare')) {
# not chrooting in this case
} else {
@ -3155,7 +3172,7 @@ sub hookhelper {
any { $_ eq $hook }
('extract', 'essential', 'customize')
) {
if (any { $_ eq $mode } ('fakechroot', 'proot')) {
if ($mode eq 'fakechroot') {
# tar will run inside the chroot
$directory = $outpath;
} elsif (
@ -3170,10 +3187,10 @@ sub hookhelper {
error "unknown hook: $hook";
}
# if chrooted_realpath was used and if neither fakechroot or
# proot were used (absolute symlinks will be broken) we can
# if chrooted_realpath was used and if fakechroot
# was used (absolute symlinks will be broken) we can
# check and potentially fail early if the target does not exist
if (none { $_ eq $mode } ('fakechroot', 'proot')) {
if ($mode ne 'fakechroot') {
my $dirtocheck = $directory;
if ($command eq 'upload') {
# check the parent directory instead
@ -3302,7 +3319,7 @@ sub hookhelper {
any { $_ eq $hook }
('extract', 'essential', 'customize')
) {
if (any { $_ eq $mode } ('fakechroot', 'proot')) {
if ($mode eq 'fakechroot') {
# tar will run inside the chroot
$directory = $ARGV[$i];
} elsif (
@ -3317,10 +3334,10 @@ sub hookhelper {
error "unknown hook: $hook";
}
# if chrooted_realpath was used and if neither fakechroot or
# proot were used (absolute symlinks will be broken) we can
# if chrooted_realpath was used and if fakechroot
# was used (absolute symlinks will be broken) we can
# check and potentially fail early if the source does not exist
if (none { $_ eq $mode } ('fakechroot', 'proot')) {
if ($mode ne 'fakechroot') {
if (!-e $directory) {
error "path does not exist: $directory";
}
@ -4247,6 +4264,26 @@ sub main() {
'variant=s' => \$options->{variant},
'include=s' => sub {
my ($opt_name, $opt_value) = @_;
my $sanitize_path = sub {
my $pkg = shift;
$pkg = abs_path($pkg);
if (!defined $pkg) {
error "cannot resolve absolute path of $pkg: $!";
}
if (!-f $pkg) {
error "$pkg is not an existing file";
}
return $pkg;
};
if ($opt_value =~ /^[?~!(]/) {
# Treat option as a single apt pattern and don't split by comma
# or whitespace -- append it verbatim.
push @{ $options->{include} }, $opt_value;
} elsif ($opt_value =~ /^\.?\.?\//) {
# Treat option as a single path name and don't split by comma
# or whitespace -- append the normalized path.
push @{ $options->{include} }, sanitize_path($opt_value);
} else {
for my $pkg (split /[,\s]+/, $opt_value) {
# strip leading and trailing whitespace
$pkg =~ s/^\s+|\s+$//g;
@ -4254,8 +4291,17 @@ sub main() {
if ($pkg eq '') {
next;
}
# Make paths canonical absolute paths, resolve symlinks
# and check if it's an existing file.
if ($pkg =~ /^\.?\.?\//) {
$pkg = sanitize_path($pkg);
}
push @{ $options->{include} }, $pkg;
}
}
# We are not sorting or otherwise normalizing the order of
# arguments to apt because package order matters for "apt install"
# since https://salsa.debian.org/apt-team/apt/-/merge_requests/256
},
'architectures=s@' => \$options->{architectures},
'mode=s' => \$options->{mode},
@ -4355,8 +4401,18 @@ sub main() {
# here prepare for long suffering in dependency hell.
'simulate' => \$options->{dryrun},
'dry-run' => \$options->{dryrun},
'skip=s@' => \$options->{skip},
) or pod2usage(-exitval => 2, -verbose => 1);
'skip=s' => sub {
my ($opt_name, $opt_value) = @_;
for my $skip (split /[,\s]+/, $opt_value) {
# strip leading and trailing whitespace
$skip =~ s/^\s+|\s+$//g;
# skip if the remainder is an empty string
if ($skip eq '') {
next;
}
push @{ $options->{skip} }, $skip;
}
}) or pod2usage(-exitval => 2, -verbose => 1);
if (defined($logfile)) {
open(STDERR, '>', $logfile) or error "cannot open $logfile: $!";
@ -4400,8 +4456,7 @@ sub main() {
if ($options->{mode} eq 'sudo') {
$options->{mode} = 'root';
}
my @valid_modes
= ('auto', 'root', 'unshare', 'fakechroot', 'proot', 'chrootless');
my @valid_modes = ('auto', 'root', 'unshare', 'fakechroot', 'chrootless');
if (none { $_ eq $options->{mode} } @valid_modes) {
error "invalid mode. Choose from " . (join ', ', @valid_modes);
}
@ -4525,9 +4580,6 @@ sub main() {
@prefix = ($EXECUTABLE_NAME, '-MDevel::Cover=-silent,-nogcov');
}
exec 'fakechroot', 'fakeroot', @prefix, $PROGRAM_NAME, @ARGVORIG;
} elsif (can_execute 'proot') {
# and lastly, proot
$options->{mode} = 'proot';
} else {
error "unable to pick chroot mode automatically";
}
@ -4536,10 +4588,6 @@ sub main() {
if ($EFFECTIVE_USER_ID != 0) {
error "need to be root";
}
} elsif ($options->{mode} eq 'proot') {
if (!can_execute 'proot') {
error "need working proot binary";
}
} elsif ($options->{mode} eq 'fakechroot') {
if (&{$check_fakechroot_running}()) {
# fakechroot is already running
@ -4616,7 +4664,7 @@ sub main() {
0 == syscall &SYS_capget, $hdrp, $datap
or error "capget failed: $!";
my ($effective, undef) = unpack "LLLLLL", $datap;
if (($effective >> $CAP_SYS_ADMIN) & 1 != 1) {
if ((($effective >> $CAP_SYS_ADMIN) & 1) != 1) {
warning
"cannot mount because CAP_SYS_ADMIN is not in the effective set";
$options->{canmount} = 0;
@ -5271,9 +5319,9 @@ sub main() {
if (any { $_ eq $format } ('tar', 'squashfs', 'ext2', 'null')) {
if ($format ne 'null') {
if (any { $_ eq $options->{variant} } ('extract', 'custom')
and any { $_ eq $options->{mode} } ('fakechroot', 'proot')) {
and $options->{mode} eq 'fakechroot') {
info "creating a tarball or squashfs image or ext2 image in"
. " fakechroot mode or proot mode might fail in extract and"
. " fakechroot mode might fail in extract and"
. " custom variants because there might be no tar inside the"
. " chroot";
}
@ -5411,7 +5459,7 @@ sub main() {
$? == 0 or error "havemknod failed";
} elsif (
any { $_ eq $options->{mode} }
('root', 'fakechroot', 'proot', 'chrootless')
('root', 'fakechroot', 'chrootless')
) {
$options->{havemknod} = havemknod($options->{root});
} else {
@ -5572,7 +5620,7 @@ sub main() {
);
} elsif (
any { $_ eq $options->{mode} }
('root', 'fakechroot', 'proot', 'chrootless')
('root', 'fakechroot', 'chrootless')
) {
$pid = fork() // error "fork() failed: $!";
if ($pid == 0) {
@ -5635,18 +5683,6 @@ sub main() {
0 == system('chroot', $options->{root}, 'tar',
@taropts, '-C', '/', '.')
or error "tar failed: $?";
} elsif ($options->{mode} eq 'proot') {
# proot requires tar to run inside proot or otherwise
# permissions will be completely off
my @qemuopt = ();
if (defined $options->{qemu}) {
push @qemuopt, "--qemu=qemu-$options->{qemu}";
push @taropts, "--exclude=./host-rootfs";
}
0 == system('proot', '--root-id',
"--rootfs=$options->{root}", '--cwd=/', @qemuopt,
'tar', @taropts, '-C', '/', '.')
or error "tar failed: $?";
} elsif (
any { $_ eq $options->{mode} }
('root', 'chrootless')
@ -5884,15 +5920,11 @@ sub main() {
}
} elsif (
any { $_ eq $options->{mode} }
('root', 'fakechroot', 'proot', 'chrootless')
('root', 'fakechroot', 'chrootless')
) {
# without unshare, we use the system's rm to recursively remove the
# temporary directory just to make sure that we do not accidentally
# remove more than we should by using --one-file-system.
#
# --interactive=never is needed when in proot mode, the
# write-protected file /apt/apt.conf.d/01autoremove-kernels is to
# be removed.
0 == system('rm', '--interactive=never', '--recursive',
'--preserve-root', '--one-file-system', $options->{root})
or error "rm failed: $?";
@ -6013,7 +6045,7 @@ B<debootstrap>. See the section B<VARIANTS> for more information.
Choose how to perform the chroot operation and create a filesystem with
ownership information different from the current user. Valid mode I<name>s are
B<auto>, B<sudo>, B<root>, B<unshare>, B<fakeroot>, B<fakechroot>, B<proot> and
B<auto>, B<sudo>, B<root>, B<unshare>, B<fakeroot>, B<fakechroot> and
B<chrootless>. The default mode is B<auto>. See the section B<MODES> for more
information.
@ -6118,16 +6150,35 @@ 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. Package names are directly passed to apt and thus, you
can use apt features like C<pkg/suite>, C<pkg=version>, C<pkg->, use a glob or
regex for C<pkg> or use apt patterns. See apt(8) for the supported
syntax. The option can be specified multiple times and the packages are
concatenated in the order in which they are given on the command line. If
later list items are repeated, then they get dropped so that the resulting
package list is free of duplicates. So the following are equivalent:
regex for C<pkg>, use apt patterns or pass a path to a .deb package file. See
apt(8) for the supported syntax.
The option can be specified multiple times and the packages are concatenated in
the order in which they are given on the command line. If later list items are
repeated, then they get dropped so that the resulting package list is free of
duplicates. So the following are equivalent:
--include="pkg1/stable pkg2=1.0 pkg3-"
--include=pkg1/stable,pkg2=1.0,pkg3-
--include=pkg1/stable,pkg2=1.0,pkg3-,,,
--incl=pkg1/stable --incl="pkg2=1.0 pkg3-" --incl=pkg2=1.0,pkg3-
Since the list of packages is separated by comma or whitespace, it is not
possible to mix apt patterns or .deb package file paths containing either
commas or whitespace with normal package names. If you do, your patterns and
paths will be split by comma and whitespace as well and become useless. To pass
such a pattern or package file path, put them into their own B<--include>
option. If the argument to B<--include> starts with an apt pattern or with a
file path, then it will not be split:
--include="?or(?priority(required), ?priority(important))"
--include="./path/to/deb with spaces/and,commas/foo.deb"
Specifically, all arguments to B<--include> that start with a C<?>, C<!>, C<~>,
C<(>, C</>, C<./> or C<../> are not split and treated as single arguments to
apt. To add more packages, use multiple B<--include> options. To disable this
detection of patterns and paths, start the argument to B<--include> with a
comma or whitespace.
=item B<--components>=I<comp1>[,I<comp2>,...]
Comma or whitespace separated list of components like main, contrib and
@ -6278,7 +6329,8 @@ B<mmdebstrap> tries hard to implement sensible defaults and will try to stop
you before shooting yourself in the foot. This option is for when you are sure
you know what you are doing and allows one to skip certain actions and safety
checks. See section B<OPERATION> for a list of possible arguments and their
context.
context. The option can be specified multiple times or you can separate
multiple values by comma or whitespace.
=item B<-q,--quiet>, B<-s,--silent>
@ -6318,8 +6370,7 @@ This mode automatically selects a fitting mode. If the effective user id is the
one of the superuser, then the B<sudo> mode is chosen. Otherwise, the
B<unshare> mode is picked if the system has the sysctl
C<kernel.unprivileged_userns_clone> set to C<1>. Should that not be the case
and if the fakechroot binary exists, the B<fakechroot> mode is chosen. Lastly,
the B<proot> mode is used if the proot binary exists.
and if the fakechroot binary exists, the B<fakechroot> mode is chosen.
=item B<sudo>, B<root>
@ -6371,15 +6422,6 @@ package B<initramfs-tools> until version 0.132. This mode will also not work
with a different libc inside the chroot than on the outside. See the section
B<LIMITATIONS> in B<fakechroot(1)>.
=item B<proot>
This mode will carry out all calls to chroot with proot instead. Since
ownership information is only retained while proot is still running, this will
lead to wrong ownership information in the final directory (everything will be
owned by the user that executed B<mmdebstrap>) and tarball (everything will be
owned by the root user). Extended attributes are not retained. This mode is
useful if you plan to use the chroot with proot.
=item B<chrootless>
Uses the dpkg option C<--force-script-chrootless> to install packages into
@ -6387,8 +6429,8 @@ I<TARGET> without dpkg and apt inside I<TARGET> but using apt and dpkg from the
machine running B<mmdebstrap>. Maintainer scripts are run without chrooting
into I<TARGET> and rely on their dependencies being installed on the machine
running B<mmdebstrap>. Only very few packages support this mode. Namely, as of
2021, not all essential packages support it, mainly due to missing support in
debconf. See https://wiki.debian.org/Teams/Dpkg/Spec/InstallBootstrap or the
2022, not all essential packages support it. See
https://wiki.debian.org/Teams/Dpkg/Spec/InstallBootstrap or the
dpkg-root-support usertag of debian-dpkg@lists.debian.org in the Debian bug
tracking system. B<WARNING>: if this option is used carelessly with packages
that do not support C<DPKG_ROOT>, this mode can result in undesired changes to
@ -6572,19 +6614,20 @@ B<TMPDIR>. Furthermore, C<MMDEBSTRAP_MODE> will store the mode set by
B<--mode>, C<MMDEBSTRAP_HOOK> stores which hook is currently run (setup,
extract, essential, customize) and C<MMDEBSTRAP_VERBOSITY> stores the numerical
verbosity level (0 for no output, 1 for normal, 2 for verbose and 3 for debug
output).
output). The C<MMDEBSTRAP_INCLUDE> variable stores the list of packages, apt
patterns or file paths given by the B<--include> option, separated by a comma
and with commas and percent signs in the option values urlencoded.
In special hooks, the paths inside the chroot are relative to the root
directory of the chroot. The path on the outside is relative to current
directory of the original B<mmdebstrap> invocation. The path inside the chroot
must already exist. Paths outside the chroot are created as necessary.
In B<fakechroot> and B<proot> mode, C<tar>, or C<sh> and C<cat> have to be run
inside the chroot or otherwise, symlinks will be wrongly resolved and/or
permissions will be off. This means that the special hooks might fail in
B<fakechroot> and B<proot> mode for the B<setup> hook or for the B<extract> and
B<custom> variants if no C<tar> or C<sh> and C<cat> is available inside the
chroot.
In B<fakechroot> mode, C<tar>, or C<sh> and C<cat> have to be run inside the
chroot or otherwise, symlinks will be wrongly resolved and/or permissions will
be off. This means that the special hooks might fail in B<fakechroot> mode for
the B<setup> hook or for the B<extract> and B<custom> variants if no C<tar> or
C<sh> and C<cat> is available inside the chroot.
=over 8
@ -6754,6 +6797,18 @@ out in B<extract> mode.
Run B<--customize-hook> options and all F<customize*> scripts in B<--hook-dir>.
This step is not carried out in B<extract> mode.
Whenever B<mmdebstrap> does a chroot call in B<root> or B<unshare> modes, it
will mount relevant device nodes, F</proc> and F</sys> into the chroot and
unmount them afterwards. This can be disabled using B<--skip=chroot/mount> or
specifically by B<--skip=chroot/mount/dev>, B<--skip=chroot/mount/proc> and
B<--skip=chroot/mount/sys>, respectively.
For each command that is run inside the chroot, B<mmdebstrap> will disable
running services by temporarily moving F</usr/sbin/policy-rc.d> and
F</sbin/start-stop-daemon> if they exist. This can be disabled with
B<--skip=chroot/policy-rc.d> and B<--skip=chroot/start-stop-daemon>,
respectively.
=item B<cleanup>
Performs cleanup tasks, unless B<--skip=cleanup> is used:
@ -6784,6 +6839,8 @@ Performs cleanup tasks, unless B<--skip=cleanup> is used:
=back
=item * Remove everything in F</run> inside the chroot. This can be disabled using B<--skip=cleanup/run>.
=item * Remove everything in F</tmp> inside the chroot. This can be disabled using B<--skip=cleanup/tmp>.
=back
@ -7074,7 +7131,7 @@ This section lists some differences to debootstrap.
=item * Default mirrors for stable releases include updates and security mirror
=item * Multiple ways to operate as non-root: fakechroot, proot, unshare
=item * Multiple ways to operate as non-root: fakechroot and unshare
=item * twice as fast

View file

@ -20,7 +20,7 @@ tar --xattrs --xattrs-include='*' -C /tmp/debian-debootstrap -xf "cache/debian-u
# delete the directory
tar -C /tmp/debian-debootstrap -cf dev1.tar ./dev
tar -C /tmp/debian-mm -cf dev2.tar ./dev
cmp dev1.tar dev2.tar
cmp dev1.tar dev2.tar >&2
rm dev1.tar dev2.tar
rm -r /tmp/debian-debootstrap/dev /tmp/debian-mm/dev
@ -34,6 +34,9 @@ rm /tmp/debian-debootstrap/var/log/dpkg.log \
/tmp/debian-debootstrap/var/log/alternatives.log \
/tmp/debian-mm/var/log/bootstrap.log
# clear out /run
rm -r /tmp/debian-debootstrap/run/*
# debootstrap doesn't clean apt
rm /tmp/debian-debootstrap/var/lib/apt/lists/127.0.0.1_debian_dists_unstable_main_binary-{{ HOSTARCH }}_Packages \
/tmp/debian-debootstrap/var/lib/apt/lists/127.0.0.1_debian_dists_unstable_Release \
@ -44,7 +47,7 @@ rm /tmp/debian-mm/var/cache/apt/archives/lock
rm /tmp/debian-mm/var/lib/apt/lists/lock
# check if the file content differs
diff --no-dereference --recursive /tmp/debian-debootstrap /tmp/debian-mm
diff --no-dereference --recursive /tmp/debian-debootstrap /tmp/debian-mm >&2
# check permissions, ownership, symlink targets, modification times using tar
# mtimes of directories created by mmdebstrap will differ, thus we equalize them first
@ -64,7 +67,7 @@ tar --full-time --verbose -tf /tmp/root2.tar > /tmp/root2.tar.list
# will slightly differ from each other in the sub-second precision (last
# decimals) so the tarballs will not be identical, so we use diff to compare
# content and tar to compare attributes
diff -u /tmp/root1.tar.list /tmp/root2.tar.list
diff -u /tmp/root1.tar.list /tmp/root2.tar.list >&2
rm /tmp/root1.tar /tmp/root2.tar /tmp/root1.tar.list /tmp/root2.tar.list
rm /tmp/debian-mm.tar

View file

@ -95,13 +95,8 @@ fi
if [ -e /tmp/debian-{{ DIST }}-mm/etc/apt/apt.conf.d/01autoremove-kernels ]; then
rm /tmp/debian-{{ DIST }}-mm/etc/apt/apt.conf.d/01autoremove-kernels
fi
# who creates /run/mount?
if [ -e "/tmp/debian-{{ DIST }}-debootstrap/run/mount/utab" ]; then
rm "/tmp/debian-{{ DIST }}-debootstrap/run/mount/utab"
fi
if [ -e "/tmp/debian-{{ DIST }}-debootstrap/run/mount" ]; then
rmdir "/tmp/debian-{{ DIST }}-debootstrap/run/mount"
fi
# clear out /run
rm -r /tmp/debian-{{ DIST }}-debootstrap/run/*
# debootstrap doesn't clean apt
rm /tmp/debian-{{ DIST }}-debootstrap/var/lib/apt/lists/127.0.0.1_debian_dists_{{ DIST }}_main_binary-{{ HOSTARCH }}_Packages \
/tmp/debian-{{ DIST }}-debootstrap/var/lib/apt/lists/127.0.0.1_debian_dists_{{ DIST }}_Release \

View file

@ -21,7 +21,6 @@ prefix=
$prefix {{ CMD }} --mode={{ MODE }} --variant=apt --architectures=arm64 {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
# we ignore differences between architectures by ignoring some files
# and renaming others
# in proot mode, some extra files are put there by proot
{ tar -tf /tmp/debian-chroot.tar \
| grep -v '^\./lib/ld-linux-aarch64\.so\.1$' \
| grep -v '^\./lib/aarch64-linux-gnu/ld-linux-aarch64\.so\.1$' \
@ -40,6 +39,5 @@ $prefix {{ CMD }} --mode={{ MODE }} --variant=apt --architectures=arm64 {{ DIST
| grep -v '^\./usr/share/doc/[^/]\+/changelog\(\.Debian\)\?\.amd64\.gz$' \
| grep -v '^\./usr/share/man/man8/i386\.8\.gz$' \
| grep -v '^\./usr/share/man/man8/x86_64\.8\.gz$';
[ "{{ MODE }}" = "proot" ] && printf "./etc/ld.so.preload\n";
} | sort | diff -u - tar2.txt
rm /tmp/debian-chroot.tar

12
tests/skip-mount Normal file
View file

@ -0,0 +1,12 @@
#!/bin/sh
set -eu
export LC_ALL=C.UTF-8
[ "{{ MODE }}" = "unshare" ]
trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
{{ CMD }} --mode=unshare --variant=apt \
--skip=chroot/mount/proc,chroot/mount/sys \
--customize-hook='mountpoint "$1"/dev/null' \
--customize-hook='if mountpoint "$1"/sys; then exit 1; fi' \
--customize-hook='if mountpoint "$1"/proc; then exit 1; fi' \
{{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -

View file

@ -0,0 +1,10 @@
#!/bin/sh
set -eu
export LC_ALL=C.UTF-8
trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
{{ CMD }} --mode={{ MODE }} --variant=apt \
--skip=chroot/start-stop-daemon,chroot/policy-rc.d \
--customize-hook='test ! -e "$1/sbin/start-stop-daemon.REAL"' \
--customize-hook='test ! -e "$1/usr/sbin/policy-rc.d"' \
{{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -

View file

@ -19,7 +19,7 @@ prefix=
[ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && prefix="runuser -u user --"
[ "{{ MODE }}" = "fakechroot" ] && prefix="$prefix fakechroot fakeroot"
symlinktarget=/real
case {{ MODE }} in fakechroot|proot) symlinktarget='$1/real';; esac
[ "{{ MODE }}" = "fakechroot" ] && symlinktarget='$1/real'
echo copy-in-setup > /tmp/copy-in-setup
echo copy-in-essential > /tmp/copy-in-essential
echo copy-in-customize > /tmp/copy-in-customize