From cd5dfbbbf2435bae8fc34ac32ee7d716c24bada8 Mon Sep 17 00:00:00 2001 From: Johannes 'josch' Schauer Date: Sun, 30 Jul 2017 19:20:28 +0200 Subject: [PATCH] wip --- multistrap | 1546 ++++++++++++++++++---------------------------------- 1 file changed, 543 insertions(+), 1003 deletions(-) diff --git a/multistrap b/multistrap index a00fbe4..5d212af 100755 --- a/multistrap +++ b/multistrap @@ -19,10 +19,6 @@ package multistrap; -# This allows one to use this file as an application with main() as the entry -# point as well as a module to allow unit testing -__PACKAGE__->main() unless caller(); - use strict; use warnings; use IO::File; @@ -38,6 +34,181 @@ use Text::Wrap; use Getopt::Long; use Pod::Usage; +# Our two global variables must come before we call main() or they will end up +# being empty. + +our $general_spec = { + arch => { + type => 'string', + help => 'Native architecture'}, + directory => { + type => 'string', + help => 'Output directory' + }, + cleanup => { + type => 'bool', + default => 1, + help => 'remove apt cache data' + }, + noauth => { + type => 'bool', + default => 0, + help => 'Allow the use of unauthenticated repositories' + }, + unpack => { + type => 'bool', + default => 1, + help => 'Extract all downloaded archives' + }, + explicitsuite => { + type => 'bool', + default => 0, + help => 'Suite explicitly selected instead of using latest versions.' + }, + aptsources => { + type => 'section', + list => 1, + default => [], + help => '' + }, + bootstrap => { + type => 'section', + list => 1, + default => [], + help => '' + }, + omitrequired => { + type => 'bool', + default => 0, + help => '' + }, + addimportant => { + type => 'bool', + default => 0, + help => '' + }, + debootstrap => { + type => 'section', + list => 1, + default => [], + help => '' + }, + bootstrap => { + type => 'section', + list => 1, + default => [], + help => '' + }, + ignorenative => { + type => 'bool', + default => 0, + help => '' + }, + aptpreferences => { + type => 'string', + list => 1, + help => '' + }, + multiarch => { + type => 'stringlist', + list => 1, + default => [], + help => '' + }, + allowrecommends => { + type => 'bool', + default => 0, + help => '' + }, + aptdefaultrelease => { + type => 'string', + help => '' + }, + setupscript => { + type => 'string', + help => '' + }, + hookdir => { + type => 'string', + list => 1, + default => [], + help => '' + }, + tarballname => { + type => 'string', + help => '' + }, + debconfseed => { + type => 'string', + list => 1, + default => [], + help => '' + }, + omitpreinst => { + type => 'bool', + default => 0, + help => '' + }, +}; + +our $section_spec = { + packages => { + type => 'stringlist', + list => 1, + default => [], + help => '' + }, + source => { + type => 'string', + list => 1, + help => '' + }, + keyring => { + type => 'string', + list => 1, + default => [], + help => '' + }, + suite => { + type => 'string', + help => '' + }, + component => { + type => 'string', + help => '' + }, + omitdebsrc => { + type => 'bool', + default => 0, + help => '' + }, + architecture => { + type => 'string', + help => '' + }, + markauto => { + type => 'bool', + default => 0, + help => '' + }, + additional => { + type => 'stringlist', + list => 1, + default => [], + help => '' + }, + reinstall => { + type => 'stringlist', + list => 1, + default => [], + help => '' + }, +}; + +# This allows one to use this file as an application with main() as the entry +# point as well as a module to allow unit testing +__PACKAGE__->main() unless caller(); + sub main { setlocale(LC_MESSAGES, ""); textdomain("multistrap"); @@ -48,42 +219,70 @@ sub main { # option will become the key in the $options hash. # # --man is a hidden option (not documented) + # + # FIXME: allow passing configuration variables on the command line. Maybe + # like: -o general/noauth=true GetOptions ($options, 'help|h', 'man', 'simulate|dry-run', 'shortcut|s=s', - 'file|f=s', 'arch|a=s', 'directory|d=s', 'tidy-up!', 'source-dir=s', 'auth!' + 'file|f=s', 'arch|a=s', 'directory|d=s', 'tidy-up!', 'auth!' ) or pod2usage(2); - pod2usage(1) if ($options->{help}); - pod2usage(-exitval => 0, -verbose => 2) if ($options->{man}); - pod2usage(-message => "Mandatory argument -f or --file is missing.\n", - -exitval => 1, -verbose => 1) if (! exists $options->{file}); + if ($options->{help}) { + pod2usage(1); + } + if ($options->{man}) { + pod2usage(-exitval => 0, -verbose => 2); + } + if (!exists $options->{file} && !exists $options->{shortcut}) { + if (scalar(@ARGV) == 0) { + pod2usage(-message => "Mandatory argument -f or --file is missing.\n", + -exitval => 1, -verbose => 1) + } elsif (scalar(@ARGV) == 1) { + pod2usage(-message => "Target directory is missing.\n", + -exitval => 1, -verbose => 1) + } elsif (scalar(@ARGV) > 3) { + pod2usage(-message => "Too many positional arguments.\n", + -exitval => 1, -verbose => 1) + } + } else { + if (scalar(@ARGV) > 0) { + pod2usage(-message => "No positional arguments allowed with -f and -s.\n", + -exitval => 1, -verbose => 1) + } + } + if (exists $options->{directory} && scalar(@ARGV) > 0) { + pod2usage(-message => "Argument --directory is not allowed if positional arguments are used.\n", + -exitval => 1, -verbose => 1) + } if (exists $options->{shortcut} && exists $options->{file}) { - die (_g("Options --shortcut and --file are mutually exclusive\n")); + die (g_("Options --shortcut and --file are mutually exclusive\n")); } + # figure out the configuration file to use my $file; - if (exists $options->{shortcut} && defined $options->{shortcut}) { - # FIXME: use ~/.config/multistrap and XDG paths as well - my $short = "/usr/share/multistrap/".$options->{shortcut}.".conf"; - $file = $short if (-f $short); - $short = "/etc/multistrap.d/".$options->{shortcut}.".conf"; - $file = $short if (-f $short); - } elsif (exists $options->{file} && defined $options->{file}) { - } - if (not defined $file) { - die (sprintf (_g("Need a configuration file - use %s -f\n"), $progname)); + if ((exists $options->{shortcut} && defined $options->{shortcut}) || scalar(@ARGV) > 0) { + my $short; + if (exists $options->{shortcut} && defined $options->{shortcut}) { + $short = $options->{shortcut}; + } else { + $short = $ARGV[0]; + } + my $xdgconfig = $ENV{XDG_CONFIG_HOME} || "$ENV{HOME}/.config"; + # Now go through all the search paths + for my $p ("$xdgconfig/multistrap", '/etc/multistrap.d', '/usr/share/multistrap') { + if (-f "$p/$short.conf") { + $file = "$p/$short.conf"; + last; + } + } + } if (exists $options->{file} && defined $options->{file}) { + $file = $options->{file}; } - my $cachedir = "var/cache/apt/"; # archives - my $libdir = "var/lib/apt/"; # lists - my $etcdir = "etc/apt/"; # sources - my $dpkgdir = "var/lib/dpkg/"; # state - # The "config" is read from configuration files. # The "options" come from the command line. # The "settings" are the config and options together. my $config_tree = parse_ini($file); - my $config = get_config_from_tree($config_tree); - my $settings = resolve_settings(); + my $settings = settings_from_config_tree($config_tree); # Overwrite settings from the configuration file with settings from the # command line if (exists $options->{arch}) { @@ -95,68 +294,57 @@ sub main { if (exists $options->{'tidy-up'}) { $settings->{general}{cleanup} = $options->{'tidy-up'}; } - if (exists $options->{'source-dir'}) { - $settings->{general}{retainsources} = $options->{'source-dir'}; - } if (exists $options->{auth}) { $settings->{general}{noauth} = !$options->{auth}; } -# Translators: fields are programname, include file. + if (scalar @ARGV > 0) { + $settings->{general}{directory} = $ARGV[1]; + } my $host = `dpkg --print-architecture`; chomp($host); if ($settings->{general}{omitrequired} and $settings->{general}{addimportant}) { - warn("\n"._g("Error: Cannot set 'add Priority: important' when packages ". + die("\n".g_("Cannot set 'add Priority: important' when packages ". "of 'Priority: required' are being omitted.\n")); - if (defined $options->{simulate}) { - warn("\n"); - &dump_settings; - exit 0; - } - exit (7); } if (defined $options->{simulate}) { - &dump_settings; + &dump_settings($settings); exit 0; } my @debootstrap = uniq_sort (@{$settings->{general}{debootstrap}}, @{$settings->{general}{bootstrap}}); my @aptsources = uniq_sort (@{$settings->{general}{aptsources}}); # Translators: fields are: programname, configfile. - printf (_g("%s using %s\n"), $progname, $file); my $arch = $settings->{general}{arch}; if ((not defined $arch) or ($arch eq "")) { $arch = $host; - printf (_g("Defaulting architecture to native: %s\n"),$arch); + printf (g_("Defaulting architecture to native: %s\n"),$arch); } elsif ($arch eq $host) { - printf (_g("Defaulting architecture to native: %s\n"),$arch); + printf (g_("Defaulting architecture to native: %s\n"),$arch); } else { - printf (_g("Using foreign architecture: %s\n"), $arch); + printf (g_("Using foreign architecture: %s\n"), $arch); } - my $foreign; - $foreign++ if (($host ne $arch) or ($settings->{general}{ignorenative})); my $dir = $settings->{general}{directory}; - if (not defined $dir or not defined $arch) { - &dump_settings; - exit 3; + if (not defined $dir) { + die "Unpack directory not defined"; } - unless (keys %sources) { - my $msg = sprintf(_g("No sources defined for a foreign multistrap. - Using your existing apt sources. To use different sources, - list them with aptsources= in '%s'."), $file); - warn ("$progname: $msg\n"); - $warn_count++; - my $l = prepare_sources_list(); - $deflist = join ("", @$l); + if (not defined $arch) { + die "Architecture not defined"; } # Translators: fields are: programname, architecture, host architecture. - printf (_g("%s building %s multistrap on '%s'\n"), $progname, $arch, $host); + printf (g_("%s building %s multistrap on '%s'\n"), $progname, $arch, $host); if ($dir =~ /^$/) { - my $msg = _g("No directory specified!"); + my $msg = g_("No directory specified!"); die "$progname: $msg\n"; } &mkdir_fatal ($dir); $dir = realpath ($dir); $dir .= ($dir =~ m:/$:) ? '' : "/"; + + my $cachedir = "var/cache/apt/"; # archives + my $libdir = "var/lib/apt/"; # lists + my $etcdir = "etc/apt/"; # sources + my $dpkgdir = "var/lib/dpkg/"; # state + system_fatal ("mkdir -p " . shellescape("${dir}${cachedir}")) if (not -d "${dir}${cachedir}"); system_fatal ("mkdir -p " . shellescape("${dir}${libdir}")) if (not -d "${dir}${libdir}"); system_fatal ("mkdir -p " . shellescape("${dir}${dpkgdir}")) if (not -d "${dir}${dpkgdir}"); @@ -169,7 +357,8 @@ sub main { system_fatal ("mkdir -p " . shellescape("${dir}usr/share/info/")) if (not -d "${dir}usr/share/info/"); system_fatal ("touch " . shellescape("${dir}usr/share/info/dir")); - if (defined $preffile) { + if ($settings->{general}{aptpreferences}) { + my $preffile = $settings->{general}{aptpreferences}; open (PREF, "$preffile") or die ("$progname: $preffile $!"); my @prefs=; close (PREF); @@ -199,22 +388,19 @@ sub main { if (not -d "${dir}dev") { mkdir_fatal ("${dir}dev"); } - if (scalar (@foreignarches) > 0) { + if (scalar (@{$settings->{general}{multiarch}}) > 0) { open (VMA, ">${dir}${dpkgdir}arch"); print VMA "$host\n"; - foreach my $farch (@foreignarches) { + foreach my $farch (@{$settings->{general}{multiarch}}) { print VMA "$farch\n"; } close (VMA); } - &guard_lib64($dir); - system_fatal ("rm -rf " . shellescape("${dir}etc/apt/sources.list.d") . "/*"); unlink ("${dir}etc/apt/sources.list") if (-f "${dir}etc/apt/sources.list"); - foreach my $repo (sort keys %suites) { if (not -e "${dir}${cachedir}") { mkdir_fatal ("${dir}${cachedir}"); } @@ -233,71 +419,86 @@ sub main { if (not -e "${dir}${etcdir}apt.conf.d") { mkdir_fatal ("${dir}${etcdir}apt.conf.d"); } - } foreach my $aptsrc (@debootstrap) { - if (defined $deflist) { - open (SOURCES, ">>${dir}etc/apt/sources.list.d/multistrap.sources.list") - or die _g("Cannot open sources list"). $!; - print SOURCES $deflist; - close SOURCES; - } elsif (-d "${dir}etc/apt/") { + foreach my $mirror (@{$settings->{$aptsrc}{source}}) { open (SOURCES, ">>${dir}etc/apt/sources.list.d/multistrap-${aptsrc}.list") - or die _g("Cannot open sources list"). $!; - my $mirror = $sources{$aptsrc}; - my $suite = (exists $flatfile{$aptsrc}) ? "" : $suites{$aptsrc}; - my $component = (exists $flatfile{$aptsrc}) ? "" - : (defined $components{$aptsrc}) ? $components{$aptsrc} : "main"; - if (defined $mirror and defined $suite) { - if (scalar (@foreignarches) == 0) { + or die g_("Cannot open sources list"). $!; + my $suite = $settings->{$aptsrc}{suite}; + if (!$suite) { + die "Section $aptsrc is missing the suite entry"; + } + my $component = $settings->{$aptsrc}{component}; + if (!$component) { + die "Section $aptsrc is missing the component entry"; + } + if (scalar (@{$settings->{general}{multiarch}}) == 0) { print SOURCES "deb [arch=$arch] $mirror $suite $component\n"; } else { - foreach my $farch (@foreignarches) { + foreach my $farch (@{$settings->{general}{multiarch}}) { print SOURCES "deb [arch=$farch] $mirror $suite $component\n"; } } - print SOURCES "deb-src $mirror $suite $component\n" if (not defined $omitdebsrc{$aptsrc}); + print SOURCES "deb-src $mirror $suite $component\n" if (not $settings->{$aptsrc}{omitdebsrc}); close SOURCES; - } - } - } - my $k; - # FIXME: remove duplicates from %keyrings - foreach my $pkg (values %keyrings) { - next if (not defined $pkg); - next if ("" eq "$pkg"); - $k .= "$pkg "; + } } - if ((defined $k) and (not defined $noauth)) { - printf (_g("I: Downloading %s\n"), $k); - system ("apt-get -y download $k"); - foreach my $keyring_pkg (values %keyrings) { - next if (not defined $keyring_pkg); - my @files=(); - my $file = `find ./ -name "${keyring_pkg}_*_all.deb"|grep -m1 $keyring_pkg`; - chomp ($file); - if ($file eq "") { - my $msg = sprintf (_g("Unable to download keyring package: '%s'"),$dir); - die "$progname: $msg\n"; - } - my $xdir = `mktemp -d -t keyring.XXXXXX`; - chomp ($xdir); - system ("dpkg -X $file $xdir >/dev/null"); - if (-d "${xdir}/usr/share/keyrings") { - opendir (DIR, "${xdir}/usr/share/keyrings"); - @files=grep(!m:\.\.?$:,readdir DIR); - closedir (DIR); - } - foreach my $gpg (@files) { - next if ($gpg =~ /removed/); - File::Copy::copy "${xdir}/usr/share/keyrings/${gpg}", "${dir}${etcdir}trusted.gpg.d/"; - } - system ("rm -rf ${xdir}"); - # FIXME: if the globbing was too aggressive, then this - # will remove files that are needed later - unlink ($file); - } + if (!$settings->{general}{noauth}) { + # Flatten the list of keyrings per section into a single list + my @keyrings = (); + foreach my $aptsrc (@debootstrap) { + # Use realpath to allow removing duplicates + foreach my $k (@{$settings->{$aptsrc}{keyring}}) { + my $r = realpath $k; + if ($r eq "") { + die "Cannot resolve using realpath(): $k"; + } + push @keyrings, $r; + } + } + @keyrings = uniq_sort(@keyrings); + my @resolved_keyrings = (); + # Resolve all directories in the list to .gpg or .asc files + foreach my $k (@keyrings) { + if (-f $k) { + push @resolved_keyrings, $k; + } elsif (-d $k) { + opendir(KEYDIR, $k); + foreach my $f (readdir KEYDIR) { + my $fl = realpath("$k/$f"); + if ($fl eq "") { + die "Cannot resolve using realpath(): $k/$f"; + } + # realpath() will resolve symlinks but we must still make sure + # that we get regular files and no directories + if (not -f $fl) { + next; + } + # We check the extension of the original file (which might've + # been a symlink) instead of the file the link resolved to. + if ($f !~ /\.gpg/ && $f !~ /\.asc/) { + next; + } + push @resolved_keyrings, $fl; + } + closedir(KEYDIR); + } elsif (! -e $k) { + die "keyring does not exist: $k"; + } else { + die "keyring is neither directory nor file: $k"; + } + } + @resolved_keyrings = uniq_sort(@resolved_keyrings); + foreach my $k (@resolved_keyrings) { + File::Copy::copy $k, "${dir}${etcdir}trusted.gpg.d/"; + } } + # Apt reads the configuration file before it parses the command line + # arguments. Thus, at the point where the -o option tells it to parse a + # different configuration file, it already read in settings from the host. + # To prevent apt from ever reading the hosts configuration, we have to + # pass our custom apt directory through a config file passed via the + # APT_CONFIG environment variable. my $pre_config_str = ''; $pre_config_str .= "Dir::Etc \"${dir}${etcdir}\";\n"; $pre_config_str .= "Dir::Etc::Parts \"${dir}${etcdir}apt.conf.d/\";\n"; @@ -315,109 +516,174 @@ sub main { $config_str .= " -o Dir::Etc::TrustedParts=" . shellescape("${dir}${etcdir}trusted.gpg.d"); $config_str .= " -o Dir::Etc::Trusted=" . shellescape("${dir}${etcdir}trusted.gpg"); $config_str .= " -o Apt::Get::AllowUnauthenticated=true" - if (defined $noauth); + if ($settings->{general}{noauth}); $config_str .= " -o Apt::Get::Download-Only=true"; $config_str .= " -o Apt::Install-Recommends=false" - if (not defined $allow_recommends); + if (!$settings->{general}{allowrecommends}); $config_str .= " -o Dir=" . shellescape($dir); $config_str .= " -o Dir::Etc=" . shellescape("${dir}${etcdir}"); $config_str .= " -o Dir::Etc::Parts=" . shellescape("${dir}${etcdir}apt.conf.d/"); $config_str .= " -o Dir::Etc::PreferencesParts=" . shellescape("${dir}${etcdir}preferences.d/"); - $config_str .= " -o APT::Default-Release=" . shellescape($default_release) if (defined $default_release); + $config_str .= " -o APT::Default-Release=" . shellescape($settings->{general}{aptdefaultrelease}) if ($settings->{general}{aptdefaultrelease}); # if (not defined $preffile); - if (defined $deflist) { - my $sourcesname = "sources.list.d/multistrap.sources.list"; - $config_str .= " -o Dir::Etc::SourceList=" . shellescape("${dir}${etcdir}$sourcesname"); - } $config_str .= " -o Dir::State=" . shellescape("${dir}${libdir}"); $config_str .= " -o Dir::State::Status=" . shellescape("${dir}${dpkgdir}status"); $config_str .= " -o Dir::Cache=" . shellescape("${dir}${cachedir}"); my $apt_get = "APT_CONFIG=" . shellescape($tmp_apt_conf) . " apt-get $config_str"; my $apt_mark = "APT_CONFIG=" . shellescape($tmp_apt_conf) . " apt-mark $config_str"; - printf (_g("Getting package lists: %s update\n"), $apt_get); + printf (g_("Getting package lists: %s update\n"), $apt_get); my $retval = system ("$apt_get update"); $retval >>= 8; - die (sprintf (_g("apt update failed. Exit value: %d\n"), $retval)) + die (sprintf (g_("apt update failed. Exit value: %d\n"), $retval)) if ($retval != 0); - my @s = (); - $str = ""; - if (not defined $omitrequired) { - print _g("I: Calculating required packages.\n"); - my %required = &get_required_debs($libdir); - $str .= join (' ', keys %required); - if (defined $addimportant) { - my $imps = join (' ', sort keys %important); - printf(_g("I: Adding 'Priority: important': %s\n"), $imps); - $str .= " ".$imps; - } - chomp($str); - } - $str .= " "; - foreach my $sect (sort keys %packages) { - my @list = split (' ', $sect); - foreach my $pkg (@list) { - next if ($packages{$pkg} =~ /^\s*$/); - next if (!(grep(/^$sect$/i, @debootstrap))); - my @long=split (/ /, $packages{$sect}); - foreach my $l (@long) { - chomp ($l); - if (defined $explicit_suite and $suites{$sect}) { - # instruct apt to get packages from the specified - # suites (when the package exists in more than one). - $str .= " $l/$suites{$sect}" if ((defined $l) and ($l !~ /^\s*$/)); - } else { - $str .= " $l" if ((defined $l) and ($l !~ /^\s*$/)); - } - } + my @packages = (); + if (!$settings->{general}{omitrequired}) { + print g_("I: Calculating required packages.\n"); + # emulate required="$(get_debs Priority: required)" + # from debootstrap/functions + # needs to be run after the first apt-get install so that + # Packages files exist + my @essential=(); + my @required=(); + my @important=(); + my %listfiles=(); + # FIXME: use apt-get indextargets --format '$(FILENAME)' "Created-By: Packages" | xargs --delimiter=\\\\n /usr/lib/apt/apt-helper cat-file + my $dir = $settings->{general}{directory}; + opendir (PKGS, "${dir}/${libdir}lists/") + or die sprintf(g_("Cannot open %s directory. %s\n"), + "${dir}/${libdir}lists/", $!); + my @lists=grep(/_Packages$/, readdir (PKGS)); + closedir (PKGS); + foreach my $l (@lists) { + $listfiles{$l}++; + } + foreach my $file (keys %listfiles) { + # FIXME: instead of requiring libparse-debian-packages-perl, use + # Dpkg::Index + my $fh = IO::File->new("${dir}/${libdir}lists/$file"); + my $parser = Parse::Debian::Packages->new( $fh ); + while (my %package = $parser->next) { + if ((defined $package{'Essential'}) && $package{'Essential'} eq 'yes') { + push @essential, $package{'Package'}; + } + next if (not defined $package{'Priority'}); + if ($package{'Priority'} eq "required") { + push @required, $package{'Package'}; + } elsif ($package{'Priority'} eq "important") { + push @important, $package{'Package'}; + } + } + } + @essential = uniq_sort(@essential); + @required = uniq_sort(@required); + @important = uniq_sort(@important); + push @packages, @essential, @required; + if ($settings->{general}{addimportant}) { + printf(g_("I: Adding 'Priority: important': %s\n"), (join " ", @important)); + push @packages, @important } } - chomp($str); - foreach my $keystr (values %keyrings) { - next if (not defined $keystr); - $str .= " " . $keystr . " "; + foreach my $aptsrc (@debootstrap) { + my @pkgs = @{$settings->{$aptsrc}{packages}}; + if ($settings->{$aptsrc}{architecture}) { + my $foreign = $settings->{$aptsrc}{architecture}; + @pkgs = map {"$_:$foreign"} @pkgs; + } + if ($settings->{general}{explicitsuite} && $settings->{$aptsrc}{suite}) { + my $suite = $settings->{$aptsrc}{suite}; + @pkgs = map {"$_/$suite"} @pkgs; + } + push @packages, @pkgs; } - chomp($str); - @s = split (/ /, $str); - @s = uniq_sort (@s); - $str = join (' ', @s); - print "$apt_get -y install $str\n"; + @packages = uniq_sort(@packages); + print "$apt_get -y install " . (join ' ', @packages) . "\n"; $retval = 0; - $retval = system ("$apt_get -y install $str"); + $retval = system ("$apt_get -y install " . (join ' ', @packages)); $retval >>= 8; - die (sprintf (_g("apt download failed. Exit value: %d\n"),$retval)) + die (sprintf (g_("apt download failed. Exit value: %d\n"),$retval)) if ($retval != 0); - &force_unpack($cachedir, $libdir, $dpkgdir) if ($unpack eq "true"); - &mark_manual_install ($apt_mark, $cachedir, $str) if (defined $markauto); + #&force_unpack($settings, $cachedir, $libdir, $dpkgdir) if ($settings->{general}{unpack}); + #foreach my $seed (@{$settings->{general}{debconfseed}}) { + # if (not -f $seed) { + # die "Cannot find $seed\n"; + # } + # open (SEED, "$seed") or next; + # my @s=; + # close (SEED); + # my $sfile = basename($seed); + # printf (g_("I: Copying debconf preseed data to %s.\n"), $sfile); + # mkdir_fatal ("${dir}/tmp/preseeds"); + # open (SEED, ">${dir}tmp/preseeds/$sfile"); + # print SEED @s; + # close (SEED); + #} + + #if ($settings->{general}{markauto}) { + # printf (g_("Marking automatically installed packages... please wait\n")); + # my $dir = $settings->{general}{directory}; + # opendir (DEBS, "${dir}${cachedir}archives/") + # or die (g_("Cannot read apt archives directory.\n")); + # my @archives=grep(/.*\.deb$/, readdir DEBS); + # closedir (DEBS); + # my @all = map { + # my $escaped_path = shellescape("${dir}${cachedir}archives/$_"); + # `LC_ALL=C dpkg -f $escaped_path Package`; + # } @archives; + # chomp (@all); + # my @auto = grep {my $pkg = $_; ! grep /$pkg/, @packages} @all; + # printf(ngettext ("Found %d package to mark.\n", + # "Found %d packages to mark.\n", scalar @auto), scalar @auto); + # system ("$apt_mark auto " . join (" ", sort @auto)) if (scalar @auto > 0); + # printf (g_("Marking automatically installed packages completed.\n")); + #} + system ("touch " . shellescape("${dir}${libdir}lists/lock")); - if ((defined $setupsh) and (-x $setupsh)) { - $retval = 0; - $retval = system (shellescape($setupsh) . " " . shellescape($dir) . " $arch"); - $retval >>= 8; - if ($retval != 0) { - warn sprintf(_g("setupscript '%s' returned %d.\n"), $setupsh, $retval); - $warn_count++; - } - } + # run setupscript + #my $setupsh = $settings->{general}{setupscript}; + #if ((defined $setupsh) and (-x $setupsh)) { + # $retval = 0; + # $retval = system (shellescape($setupsh) . " " . shellescape($dir) . " $arch"); + # $retval >>= 8; + # if ($retval != 0) { + # die sprintf(g_("setupscript '%s' returned %d.\n"), $setupsh, $retval); + # } + #} # run first set of hooks - probably unnecessary re setupscript. - &run_download_hooks(sort @{$hooks{'D'}}) if (defined $hooks{'D'}); - my $err = &native if (not defined ($foreign) and $unpack eq "true"); - if (defined $err and $err != 0) { - warn (_g("Native mode configuration reported an error!\n")); - $warn_count++; + #&run_hooks($settings, "download"); + #if ($settings->{general}{arch} eq $host and $settings->{general}{unpack}) { + # &native($settings); + #} + my @sections = uniq_sort ( + @{$settings->{general}{debootstrap}}, + @{$settings->{general}{bootstrap}}, + @{$settings->{general}{aptsources}}); + foreach my $section (@sections) { + if (scalar @{$settings->{$section}{additional}} > 0) { + my $str = join (' ', @{$settings->{$section}{additional}}); + print "$apt_get -y install $str\n"; + system ("$apt_get -y install $str"); + #&force_unpack ($settings, $cachedir, $libdir, $dpkgdir, @{$settings->{$section}{additional}}) if ($settings->{general}{unpack}); + my $dir = $settings->{general}{directory}; + system ("touch " . shellescape("${dir}${libdir}lists/lock")); + my $host = `dpkg --print-architecture`; + if ($settings->{general}{arch} ne $host || $settings->{general}{ignorenative}) { + &native($settings); + } + } + } + if ($settings->{general}{cleanup}) { + &tidy_apt($settings, $cachedir, $libdir); + } else { + system ("$apt_get update") } - &add_extra_packages($apt_get, $cachedir, $libdir, $dpkgdir); - system ("cp " . shellescape($configsh) . " " . shellescape("$dir/")) if ((defined $configsh) and (-f $configsh)); - &handle_source_packages($apt_get, $cachedir, $dpkgdir); - (not defined $tidy) ? system ("$apt_get update") : &tidy_apt($cachedir, $libdir); - &guard_lib64($dir); # cleanly separate the bootstrap sources from the final apt sources. unlink ("${dir}etc/apt/sources.list.d/multistrap.sources.list") if (-f "${dir}etc/apt/sources.list.d/multistrap.sources.list"); opendir (LISTS, "${dir}etc/apt/sources.list.d/") - or die (_g("Cannot read apt sources list directory.\n")); + or die (g_("Cannot read apt sources list directory.\n")); my @sources=grep(m:^multistrap-.*\.list$:, readdir LISTS); closedir (LISTS); foreach my $filelist (@sources) { @@ -425,67 +691,45 @@ sub main { unlink ("${dir}etc/apt/sources.list.d/$filelist"); } foreach my $aptsrc (@aptsources) { - if (defined $deflist) { - open (SOURCES, ">>${dir}etc/apt/sources.list.d/multistrap.sources.list") - or die _g("Cannot open sources list"). $!; - print SOURCES $deflist; - close SOURCES; - } elsif (-d "${dir}etc/apt/") { open (SOURCES, ">>${dir}etc/apt/sources.list.d/multistrap-${aptsrc}.list") - or die _g("Cannot open sources list"). $!; - my $mirror = $sources{$aptsrc}; - my $suite = (exists $flatfile{$aptsrc}) ? "" : $suites{$aptsrc}; - my $component = (exists $flatfile{$aptsrc}) ? "" - : (defined $components{$aptsrc}) ? $components{$aptsrc} : "main"; + or die g_("Cannot open sources list"). $!; + my $mirror = $settings->{$aptsrc}{source}; + my $suite = $settings->{$aptsrc}{suite}; + my $component = $settings->{$aptsrc}{component}; if (defined $mirror and defined $suite) { - if (scalar (@foreignarches) == 0) { + if (scalar (@{$settings->{general}{multiarch}}) == 0) { print SOURCES "deb [arch=$arch] $mirror $suite $component\n"; } else { - foreach my $farch (@foreignarches) { + foreach my $farch (@{$settings->{general}{multiarch}}) { print SOURCES "deb [arch=$farch] $mirror $suite $component\n"; } } - print SOURCES "deb-src $mirror $suite $component\n" if (not defined $omitdebsrc{$aptsrc}); + print SOURCES "deb-src $mirror $suite $component\n" if (!$settings->{$aptsrc}{omitdebsrc}); close SOURCES; } - } } # altered the sources, so get apt to update. - (not defined $tidy) ? system ("$apt_get update") : &tidy_apt($cachedir, $libdir); -# run second set of hooks - &run_completion_hooks(sort @{$hooks{'A'}}) if (defined ($hooks{'A'})); - unlink $tmp_apt_conf; - if (not defined $warn_count) { - printf (_g("\nMultistrap system installed successfully in %s.\n"), $dir); + if ($settings->{general}{cleanup}) { + &tidy_apt($settings, $cachedir, $libdir); } else { - my $plural = sprintf(ngettext ("\nMultistrap system reported %d error in %s.\n", - "\nMultistrap system reported %d errors in %s.\n", $warn_count), $warn_count, $dir); - warn ($plural); - $warn_count++; - } - if (defined $tgzname) { - printf (_g("\nCompressing multistrap system in '%s' to a tarball called: '%s'.\n"), $dir, $tgzname); - chdir ("$dir"); - unlink $tgzname if (-f $tgzname); - my $retval = system ("tar -czf " . shellescape("../$tgzname .")); - $retval >>= 8; - if ($retval == 0) { - printf (_g("\nRemoving build directory: '%s'\n"), $dir); - system ("rm -rf " . shellescape($dir) . "/*"); - } - my $final_path=realpath ("$dir/../$tgzname"); - if (not defined $warn_count) { - printf (_g("\nMultistrap system packaged successfully as '%s'.\n"), $final_path); - } else { - warn sprintf(_g("\nMultistrap system packaged as '%s' with warnings.\n"), $final_path); - } + system ("$apt_get update"); } +# run second set of hooks + #&run_hooks($settings, "cleanup"); + unlink $tmp_apt_conf; + printf (g_("\nMultistrap system installed successfully in %s.\n"), $dir); + #my $tgzname = $settings->{general}{tarballname}; + #if (defined $tgzname) { + # printf (g_("\nCompressing multistrap system in '%s' to a tarball called: '%s'.\n"), $dir, $tgzname); + # chdir ("$dir"); + # unlink $tgzname if (-f $tgzname); + # my $retval = system ("tar -czf " . shellescape("../$tgzname .")); + # $retval >>= 8; + # my $final_path=realpath ("$dir/../$tgzname"); + # printf (g_("\nMultistrap system packaged successfully as '%s'.\n"), $final_path); + #} print "\n"; - if (not defined $warn_count) { exit 0; - } else { - exit $warn_count; - } } # avoid dependency on String::ShellQuote by implementing the mechanism @@ -506,755 +750,49 @@ sub shellescape { return "'$string'"; } -sub add_extra_packages { - my $apt_get = shift; - my $cachedir = shift; - my $libdir = shift; - my $dpkgdir = shift; - if (scalar @extrapkgs > 0) { - $str = join (' ', @extrapkgs); - print "$apt_get -y install $str\n"; - system ("$apt_get -y install $str"); - &force_unpack ($cachedir, $libdir, $dpkgdir, @extrapkgs) if ($unpack eq "true"); - system ("touch " . shellescape("${dir}${libdir}lists/lock")); - &native if (not defined ($foreign)); - } -} - -sub mark_manual_install { - my $apt_mark = shift; - my $cachedir = shift; - my @manual = split(/ +/, $_[0]); - printf (_g("Marking automatically installed packages... please wait\n")); - opendir (DEBS, "${dir}${cachedir}archives/") - or die (_g("Cannot read apt archives directory.\n")); - my @archives=grep(/.*\.deb$/, readdir DEBS); - closedir (DEBS); - my @all = map { - my $escaped_path = shellescape("${dir}${cachedir}archives/$_"); - `LC_ALL=C dpkg -f $escaped_path Package`; - } @archives; - chomp (@all); - my @auto = grep {my $pkg = $_; ! grep /$pkg/, @manual} @all; - printf(ngettext ("Found %d package to mark.\n", - "Found %d packages to mark.\n", scalar @auto), scalar @auto); - system ("$apt_mark auto " . join (" ", sort @auto)) if (scalar @auto > 0); - printf (_g("Marking automatically installed packages completed.\n")); -} - -sub force_unpack { - my $cachedir = shift; - my $libdir = shift; - my $dpkgdir = shift; - my (@limits) = @_; - my %unpack=(); - my %filter = (); - opendir (DEBS, "${dir}${cachedir}archives/") - or die (_g("Cannot read apt archives directory.\n")); - my @archives=grep(/.*\.deb$/, readdir DEBS); - closedir (DEBS); - if (@limits) { - foreach my $l (@limits) { - foreach my $file (@archives) { - if ($file =~ m:$l:) { - $filter{$l} = "$file"; - } - } - } - @archives = sort values %filter; - } - print _g("I: Calculating obsolete packages\n"); - foreach my $deb (sort @archives) { - my $escaped_path = shellescape("${dir}${cachedir}archives/$deb"); - my $version = `LC_ALL=C dpkg -f $escaped_path Version`; - my $package = `LC_ALL=C dpkg -f $escaped_path Package`; - chomp ($version); - chomp ($package); - if (exists $unpack{$package}) { - my $test=system("dpkg --compare-versions ". shellescape($unpack{$package}) . " '<<' " . shellescape($version)); - $test >>= 8; - # unlink version in $unpack if 0 - # unlink $deb (current one) if 1 - if ($test == 0) { - my $old = $deb; - $old =~ s/$version/$unpack{$package}/; - printf (_g("I: Removing %s\n"), $old); - unlink "${dir}${cachedir}archives/$old"; - next; - } else { - printf (_g("I: Removing %s\n"), $deb); - unlink "${dir}${cachedir}archives/$deb"; - } - } - $unpack{$package}=$version; - } - if (not @limits) { - open (LOCK, ">${dir}${libdir}lists/lock"); - close (LOCK); - opendir (DEBS, "${dir}${cachedir}archives/") - or die (_g("Cannot read apt archives directory.\n")); - @archives=grep(/.*\.deb$/, readdir DEBS); - closedir (DEBS); - } - my $old = `pwd`; - chomp ($old); - chdir ("${dir}"); - printf (_g("Using directory %s for unpacking operations\n"), $dir); - foreach my $deb (sort @archives) { - printf (_g("I: Extracting %s...\n"), $deb); - my $escaped_path = shellescape("./${cachedir}archives/$deb"); - my $ver=`LC_ALL=C dpkg -f $escaped_path Version`; - my $pkg=`LC_ALL=C dpkg -f $escaped_path Package`; - my $src=`LC_ALL=C dpkg -f $escaped_path Source`; - my $multi=`LC_ALL=C dpkg -f $escaped_path Multi-Arch`; - chomp ($ver); - chomp ($pkg); - chomp ($src); - chomp ($multi); - if (($multi eq "foreign") or ($multi eq "allowed")) { - $multi = ''; - } elsif ($multi eq "same") { - # actually need dpkg multi-arch support implemented before this can be active. - #$multi=":".`LC_ALL=C dpkg -f ./${cachedir}archives/$deb Architecture`; - #chomp ($multi); - $multi = ''; - if ($multi eq ":all") { - # Translators: imagine "Architecture: all" in quotes. - my $msg = sprintf(_g("Warning: invalid value '%s' for Multi-Arch field in Architecture: all package: %s. "), $multi, $deb); - warn ("$msg\n"); - $multi = ''; - } - } elsif ($multi ne '') { - # Translators: Please do not translate 'same', 'foreign' or 'allowed' - my $msg = sprintf(_g("Warning: unrecognised value '%s' for Multi-Arch field in %s. ". - "(Expecting 'same', 'foreign' or 'allowed'.)"), $multi, $deb); - warn ("$msg\n"); - $multi = ''; - } - $src =~ s/ \(.*\)//; - $src = $pkg if ($src eq ""); - push @dsclist, $src; - mkdir_fatal ("./tmp"); - my $tmpdir = `mktemp -p ./tmp -d -t multistrap.XXXXXX`; - chomp ($tmpdir); - my $escaped_path1 = shellescape("./${cachedir}archives/$deb"); - my $escaped_path2 = shellescape($dir); - my $datatar = `LC_ALL=C dpkg -X $escaped_path1 $escaped_path2`; - my $exit = `echo $?`; - chomp ($exit); - if ($exit ne "0") { - printf(_g("dpkg -X failed with error code %s\nSkipping...\n"), $exit); - next; - } - my @lines = split("\n", $datatar); - open (LIST, ">>./${dpkgdir}info/${pkg}${multi}.list"); - foreach my $l (@lines) { - chomp ($l); - $l =~ s:^\.::; - $l =~ s:^/$:/\.:; - $l =~ s:/$::; - print LIST "$l\n"; - } - close (LIST); - system ("dpkg -e ./${cachedir}archives/$deb ${tmpdir}/"); - opendir (MAINT, "./${tmpdir}"); - my @maint=grep(!m:\.\.?:, readdir (MAINT)); - closedir (MAINT); - open (AVAIL, ">>./${dpkgdir}available"); - open (STATUS, ">>./${dpkgdir}status"); - foreach my $mscript (@maint) { - rename "./${tmpdir}/$mscript", "./${dpkgdir}info/$pkg${multi}.$mscript"; - if ( $mscript eq "control" ) { - open (MSCRIPT, "./${dpkgdir}info/$pkg${multi}.$mscript"); - my @scr=; - close (MSCRIPT); - my @avail = grep(!/^$/, @scr); - print AVAIL @avail; - print STATUS @avail; - print AVAIL "\n"; - print STATUS "Status: install ok unpacked\n"; - unlink ("./${dpkgdir}info/$pkg${multi}.$mscript"); - } - } - close (AVAIL); - if ( -f "./${dpkgdir}info/$pkg${multi}.conffiles") { - print STATUS "Conffiles:\n"; - printf (_g(" -> Processing conffiles for %s\n"), $pkg); - open (CONF, "./${dpkgdir}info/$pkg${multi}.conffiles"); - my @lines=; - close (CONF); - foreach my $line (@lines) { - chomp ($line); - my $md5=`LC_ALL=C md5sum ./$line | cut -d" " -f1`; - chomp ($md5); - print STATUS " $line $md5\n"; - } - } - print STATUS "\n"; - close (STATUS); - system ("rm -rf ./${tmpdir}"); - &guard_lib64 ($dir); - } - chdir ("$old"); - # update-alternatives helper / preinst helper - if (not -d "${dir}usr/share/man/man1") { - &mkdir_fatal ("${dir}usr/share/man/man1"); - } - print _g("I: Unpacking complete.\n"); - foreach my $seed (@debconf) { - next if (not -f $seed); - open (SEED, "$seed") or next; - my @s=; - close (SEED); - my $sfile = basename($seed); - printf (_g("I: Copying debconf preseed data to %s.\n"), $sfile); - mkdir_fatal ("${dir}/tmp/preseeds"); - open (SEED, ">${dir}tmp/preseeds/$sfile"); - print SEED @s; - close (SEED); - } -} - -sub run_download_hooks { - my (@hooks) = @_; - return if (scalar @hooks == 0); - # Translators: the plural is followed by a single repeat for each - printf(ngettext("I: Running %d post-download hook\n", - "I: Running %d post-download hooks\n", scalar @hooks), scalar @hooks); - foreach my $hookscript (@hooks) { - # Translators: this is a single instance, naming the hook - printf (_g("I: Running post-download hook: '%s'\n"), basename($hookscript)); - my $hookret = system (shellescape($hookscript) . " " . shellescape($dir)); - $hookret >>= 8; - if ($hookret != 0) { - printf (_g("I: post-download hook '%s' reported an error: %d\n"), basename($hookscript), $hookret); - $warn_count += abs($hookret); - } - } -} - -sub run_native_hooks_start { - my (@hooks) = @_; - return if (scalar @hooks == 0); - # Translators: the plural is followed by a single repeat for each - printf(ngettext("I: Starting %d native hook\n", - "I: Starting %d native hooks\n", scalar @hooks), scalar @hooks); - foreach my $hookscript (@hooks) { - # Translators: this is a single instance, naming the hook - printf (_g("I: Starting native hook: '%s'\n"), basename($hookscript)); - my $hookret = system (shellescape($hookscript) . " " . shellescape($dir) . " start"); - $hookret >>= 8; - if ($hookret != 0) { - printf (_g("I: run-native hook start '%s' reported an error: %d\n"), basename($hookscript), $hookret); - $warn_count += abs($hookret); - } - } -} - -sub run_native_hooks_end { - my (@hooks) = @_; - return if (scalar @hooks == 0); - # Translators: the plural is followed by a single repeat for each - printf(ngettext("I: Stopping %d native hook\n", - "I: Stopping %d native hooks\n", scalar @hooks), scalar @hooks); - foreach my $hookscript (@hooks) { - # Translators: this is a single instance, naming the hook - printf (_g("I: Stopping native hook: '%s'\n"), basename($hookscript)); - my $hookret = system (shellescape($hookscript) . " " . shellescape($dir) . " end"); - $hookret >>= 8; - if ($hookret != 0) { - printf (_g("I: run-native hook end '%s' reported an error: %d\n"), basename($hookscript), $hookret); - $warn_count += abs($hookret); - } - } -} +sub run_hooks { + my $settings = shift; + my $type = shift; + my @args = @_; + + my @hooks = (); + foreach my $p (@{$settings->{general}{hookdir}}) { + if ( -d $p ) { + opendir (HOOKS, $p) or die "cannot Cannot open directory $p: $!"; + foreach my $f (readdir HOOKS) { + my $fl = realpath("$p/$f"); + if (not -f $fl or not -x $fl) { + next; + } + if ($f !~ /^$type/) { + next; + } + push @hooks, $fl; + } + closedir(HOOKS); + } elsif ( -f $p ) { + } + } + if (scalar @hooks == 0) { + return; + } -sub run_completion_hooks { - my (@hooks) = @_; return if (scalar @hooks == 0); # Translators: the plural is followed by a single repeat for each - printf(ngettext("I: Running %d post-configuration hook\n", - "I: Running %d post-configuration hooks\n", scalar @hooks), scalar @hooks); + printf(g_("I: Running %d hooks\n"), scalar @hooks); foreach my $hookscript (@hooks) { # Translators: this is a single instance, naming the hook - printf (_g("I: Running post-configuration hook: '%s'\n"), basename($hookscript)); - my $hookret = system (shellescape($hookscript) . " " . shellescape($dir)); + printf (g_("I: Running %s hook: '%s'\n"), $type, (join " ", ($hookscript, @args))); + my $dir = $settings->{general}{directory}; + my $hookret = system (shellescape($hookscript) . " " . shellescape($dir) . " " . (join " ", @args)); $hookret >>= 8; if ($hookret != 0) { - printf (_g("I: run-completion hook '%s' reported an error: %d\n"), basename($hookscript), $hookret); - $warn_count += abs($hookret); - } - } -} - -# prevent the absolute symlink in libc6 from allowing -# writes outside the multistrap root dir. See: #553599 -sub guard_lib64 { - my $dir = shift; - my $old = `pwd`; - chomp ($old); - unlink "${dir}lib64" if (-f "${dir}lib64"); - if (-l "${dir}lib64" ) { - my $r = readlink "${dir}lib64"; - chomp ($r); - if ($r =~ m:^/lib$:) { - printf (_g("I: Unlinking unsafe %slib64 -> /lib symbolic link.\n"), $dir); - unlink "${dir}lib64"; - } - } elsif (not -d "${dir}lib64") { - chdir ("$dir"); - my $host = `dpkg --print-architecture`; - chomp($host); - if ($host eq 'i386' and $arch eq 'amd64') { - printf (_g("I: Replaced ./lib64 -> /lib symbolic link with new %slib64 directory.\n"), $dir); - mkdir_fatal ("${dir}lib64"); - } else { - printf (_g("I: Setting %slib64 -> %slib symbolic link.\n"), $dir, $dir); - symlink "./lib", "lib64"; - } - } - chdir ("${old}"); -} - -sub check_bin_sh { - $dir = shift; - my $old = `pwd`; - chomp ($old); - my $host = `dpkg --print-architecture`; - chomp($host); - # dash refuses to configure if no existing shell is found. - # (always expects a diversion to already exist). - # (works OK in subsequent upgrades.) #546528 - unlink ("$dir/var/lib/dpkg/info/dash.postinst"); - unlink ("$dir/var/lib/dpkg/info/dash:${host}.postinst"); - # now ensure that a usable shell is available as /bin/sh - if (not -l "$dir/bin/sh") { - print (_g("I: ./bin/sh symbolic link does not exist.\n")); - if (-f "$dir/bin/dash") { - print (_g("I: Setting ./bin/sh -> ./bin/dash\n")); - chdir ("$dir/bin"); - symlink ("dash", "sh"); - chdir ("$old"); - } elsif (-f "$dir/bin/bash") { - print (_g("I: ./bin/dash not found. Setting ./bin/sh -> ./bin/bash\n")); - chdir ("$dir/bin"); - symlink ("bash", "sh"); - chdir ("$old"); - } - } - if (-l "$dir/bin/sh") { - printf (_g("I: Shell found OK in %s:\n"), "${dir}bin/sh"); - system ("(cd " . shellescape($dir) . " ; ls -lh bin/sh)"); - } else { - die ("No shell in $dir."); - } -} - -sub handle_source_packages { - my $apt_get = shift; - my $cachedir = shift; - my $dpkgdir = shift; - return if (not defined $sourcedir); - if ($unpack eq "true") { - opendir (DEBS, "${dir}${cachedir}/archives/") - or die (_g("Cannot read apt archives directory.\n")); - my @files=grep(!m:\.\.?$:, readdir DEBS); - closedir (DEBS); - foreach my $file (@files) { - next if (-d $file); - next unless ($file =~ /\.deb$/); - if (defined $sourcedir) { - my $escaped_path = shellescape("${dir}${cachedir}archives/$file"); - my $srcname = `LC_ALL=C dpkg -f $escaped_path Source`; - chomp ($srcname); - $srcname =~ s/ \(.*\)//; - if ($srcname eq "") { - my $srcname = `LC_ALL=C dpkg -f $escaped_path Package`; - chomp ($srcname); - } - push @dsclist, $srcname; - } - } - } - print "Checking ${dir}${dpkgdir}status\n"; - if (-f "${dir}${dpkgdir}status") { - open (STATUS, "${dir}${dpkgdir}status"); - my @lines=; - close (STATUS); - my $pkg; - my $src; - foreach my $line (@lines) { - if ($line =~ /^Package: (.*)$/) { - $pkg = $1; - } - if ($line =~ /^Source: (.*)$/) { - my $c = $1; - $c =~ s/\(.*\)$//; - $c =~ s/ //g; - push @dsclist, $c; - $src = $c; - } - if ($line =~ /^$/) { - push @dsclist, $pkg if (not defined $src and defined $pkg); - undef $pkg; - undef $src; - } - } - } - @dsclist = uniq_sort (@dsclist); - my $olddir = getcwd(); - chdir ($sourcedir); - if (scalar @dsclist > 0) { - print "$apt_get -d source " . join (" ", @dsclist) . "\n"; - foreach my $srcpkg (@dsclist) { - system ("$apt_get -d source $srcpkg"); + die (g_("E: hook '%s' reported an error: %d\n"), $hookscript, $hookret); } } - chdir ($olddir); } -sub tidy_apt { - my $cachedir = shift; - my $libdir = shift; - print _g("I: Tidying up apt cache and list data.\n"); - if ($unpack eq "true") { - # FIXME: use apt-get clean instead - opendir (DEBS, "${dir}${cachedir}/archives/") - or die (_g("Cannot read apt archives directory.\n")); - my @files=grep(!m:\.\.?$:, readdir DEBS); - closedir (DEBS); - foreach my $file (@files) { - next if (-d $file); - next unless ($file =~ /\.deb$/); - if (defined $sourcedir) { - system ("mv " . shellescape("${dir}${cachedir}archives/$file") . " " . shellescape("$sourcedir/$file")); - } else { - unlink ("${dir}${cachedir}archives/$file"); - } - } - $sourcedir=undef; - } - # FIXME: use apt-get update -o Dir::Etc::SourceList= -o Dir::Etc::SourceParts= - unlink ("${dir}etc/apt/sources.list") - if (-f "${dir}etc/apt/sources.list"); - opendir (DEBS, "${dir}${libdir}lists/") - or die (_g("Cannot read apt lists directory.\n")); - my @lists=grep(!m:\.\.?$:, readdir DEBS); - closedir (DEBS); - foreach my $file (@lists) { - next if (-d $file); - unlink ("${dir}${libdir}lists/$file"); - } - # FIXME: why are there .bin files in /var/cache/apt? - opendir (DEBS, "${dir}${cachedir}/") - or die (_g("Cannot read apt cache directory.\n")); - my @files=grep(!m:\.\.?$:, readdir DEBS); - closedir (DEBS); - foreach my $file (@files) { - next if (-d $file); - next unless ($file =~ /\.bin$/); - unlink ("${dir}${cachedir}$file"); - } -} - -# if native arch, do a few tasks just because we can and probably should. -sub native { - my $env = "DEBIAN_FRONTEND=noninteractive ". - "DEBCONF_NONINTERACTIVE_SEEN=true ". - "LC_ALL=C LANGUAGE=C LANG=C"; - printf (_g("I: dpkg configuration settings:\n\t%s\n"), $env); - if (exists $ENV{FAKEROOTKEY}) { - warn (_g("W: Cannot use 'chroot' when fakeroot is in use. Skipping package configuration.\n")); - return; - } - print _g("I: Native mode - configuring unpacked packages . . .\n"); - my $str = ""; - if ($ENV{USER} eq 'root') { - $str = "sudo" if (-f "/usr/bin/sudo"); - } - # check that we have a workable shell inside the chroot - &check_bin_sh("$dir"); - # pre-populate debconf - if (-d "${dir}/tmp/preseeds/") { - opendir (SEEDS, "${dir}/tmp/preseeds/") or return; - my @seeds=grep(!m:\.\.?$:, readdir SEEDS); - closedir (SEEDS); - foreach my $s (@seeds) { - printf (_g("I: Running debconf for seed file: %s\n"), $s); - system ("$str $env chroot " . shellescape($dir) . " debconf-set-selections /tmp/preseeds/$s"); - } - } - &run_native_hooks_start(sort @{$hooks{'N'}}) if (defined ($hooks{'N'})); - if (not defined $omitpreinst) { - opendir (PRI, "${dir}/var/lib/dpkg/info") or return; - my @preinsts=grep(/\.preinst$/, readdir PRI); - closedir (PRI); - printf (_g("I: Running preinst scripts with 'install' argument.\n")); - my $f = join (" ", @reinstall); - foreach my $script (sort @preinsts) { - my $t = $script; - $t =~ s/\.preinst//; - next if ($t =~ /$f/); - next if ($script =~ /bash/); - system ("$str $env chroot " . shellescape($dir) . " /var/lib/dpkg/info/$script install"); - } - } - my $retval = 0; - $retval = system ("$str $env chroot " . shellescape($dir) . " dpkg --configure -a"); - $retval >>=8; - if ($retval != 0) { - warn (_g("ERR: dpkg configure reported an error.\n")); - } - # reinstall set - foreach my $reinst (sort @reinstall) { - system ("$str $env chroot " . shellescape($dir) . " apt-get --reinstall -y install $reinst"); - } - &run_native_hooks_end(sort @{$hooks{'N'}}) if (defined $hooks{'N'}); - return $retval; -} - -sub get_required_debs { - my $libdir = shift; - # emulate required="$(get_debs Priority: required)" - # from debootstrap/functions - # needs to be run after the first apt-get install so that - # Packages files exist - my %required=(); - my %listfiles=(); - # FIXME: use apt-get indextargets --format '$(FILENAME)' "Created-By: Packages" | xargs --delimiter=\\\\n /usr/lib/apt/apt-helper cat-file - opendir (PKGS, "${dir}${libdir}lists/") - or die sprintf(_g("Cannot open %s directory. %s\n"), - "${dir}${libdir}lists/", $!); - my @lists=grep(/_Packages$/, readdir (PKGS)); - closedir (PKGS); - foreach my $strap (@debootstrap) { - my $s = lc($strap); - foreach my $l (@lists) { - $listfiles{$l}++; - } - } - foreach my $file (keys %listfiles) { - # FIXME: instead of requiring libparse-debian-packages-perl, use - # Dpkg::Index - my $fh = IO::File->new("${dir}${libdir}lists/$file"); - my $parser = Parse::Debian::Packages->new( $fh ); - while (my %package = $parser->next) { - if (not defined $package{'Priority'} and (defined $package{'Essential'})) { - $required{$package{'Package'}}++; - next; - } - next if (not defined $package{'Priority'}); - if ($package{'Priority'} eq "required") { - $required{$package{'Package'}}++; - } elsif ($package{'Priority'} eq "important") { - $important{$package{'Package'}}++; - } - } - } - return %required; -} - -# inherited from apt-cross -sub prepare_sources_list { - my @source_list=(); - # collate all available/configured sources into one list - if (-e "/etc/apt/sources.list") { - open (SOURCES, "/etc/apt/sources.list") - or die _g("cannot open apt sources list. %s",$!); - @source_list = ; - close (SOURCES); - } - if (-d "/etc/apt/sources.list.d/") { - opendir (FILES, "/etc/apt/sources.list.d/") - or die _g("cannot open apt sources.list directory %s\n",$!); - my @files = grep(!/^\.\.?$/, readdir FILES); - foreach my $f (@files) { - next if ($f =~ /\.ucf-old$/); - open (SOURCES, "/etc/apt/sources.list.d/$f") or - die _g("cannot open /etc/apt/sources.list.d/%s %s",$f, $!); - while() { - push @source_list, $_; - } - close (SOURCES); - } - closedir (FILES); - } - return \@source_list; -} - -sub usageversion { - my $progname = basename($0); - printf STDERR (_g(" -Usage: - %s [-a ARCH] [-d DIR] -f CONFIG_FILE - %s -?|-h|--help|--version - -Command: - -f|--file CONFIG_FILE: path to the multistrap configuration file. - -Options: - -a|--arch ARCHITECTURE: override the configuration file architecture. - -d|--dir PATH: override the configuration file directory. - --no-auth: do not use Secure Apt for any repositories - --tidy-up: remove apt cache data and downloaded archives. - --dry-run: output the configuration and exit - --simulate: output the configuration and exit - -?|-h|--help: print this usage message and exit - --version: print this usage message and exit - -%s replaces debootstrap to provide support for multiple -repositories, using a configuration file to specify the relevant suites, -architecture, extra packages and the mirror to use for each repository. - -Example configuration: -[General] -arch=armel -directory=/opt/multistrap/ -# same as --tidy-up option if set to true -cleanup=true -# same as --no-auth option if set to true -# keyring packages listed in each bootstrap will -# still be installed. -noauth=false -# extract all downloaded archives (default is true) -unpack=true -# enable MultiArch for the specified architectures -# default is empty -multiarch= -# aptsources is a list of sections to be used for downloading packages -# and lists and placed in the /etc/apt/sources.list.d/multistrap.sources.list -# of the target. Order is not important -aptsources=Debian -# the order of sections is not important. -# the bootstrap option determines which repository -# is used to calculate the list of Priority: required packages. -bootstrap=Debian - -[Debian] -packages= -source=http://http.debian.net/debian -keyring=debian-archive-keyring -suite=stable - -This will result in a completely normal bootstrap of Debian stable from -the specified mirror, for armel in /opt/multistrap/. - -'Architecture' and 'directory' can be overridden on the command line. - -Specify a package to extend the bootstap to include that package and -all dependencies. Dependencies will be calculated by apt so as to use -only the most recent suitable version from all configured repositories. - -General settings: - -'directory' specifies the top level directory where the bootstrap -will be created - it is not packed into a .tgz once complete. - -"), $progname, $progname, $progname) - or die ("$progname: ". _g("failed to write usage:") . "$!\n"); -} - -my $general_spec = { - arch => { - type => 'string', - help => 'Native architecture'}, - directory => { - type => 'string', - help => 'Output directory' - }, - cleanup => { - type => 'bool', - default => 1, - help => 'remove apt cache data' - }, - noauth => { - type => 'bool', - default => 0, - help => 'Allow the use of unauthenticated repositories' - }, - unpack => { - type => 'bool', - default => 1, - help => 'Extract all downloaded archives' - }, - explicitsuite => { - type => 'bool', - default => 0, - help => 'Suite explicitly selected instead of using latest versions.' - }, - aptsources => { - type => 'section', - list => 1, - help => '' - }, - bootstrap => { - type => 'section', - list => 1, - help => '' - }, - omitrequired => { - type => 'bool', - default => 0, - help => '' - }, - addimportant => { - type => 'bool', - default => 0, - help => '' - }, - debootstrap => { - type => 'section', - list => 1, - default => [], - help => '' - }, - bootstrap => { - type => 'section', - list => 1, - default => [], - help => '' - }, - ignorenative => { - type => 'bool', - default => 0, - help => '' - }, - retainsources => { - type => 'string', - help => '' - }, -}; - -my $section_spec = { - packages => { - type => 'stringlist', - list => 1, - default => [], - help => '' - }, - source => { - type => 'string', - list => 1, - help => '' - }, - keyring => { - type => 'string', - list => 1, - default => [], - help => '' - }, - suite => { - type => 'string', - help => '' - }, - omitdebsrc => { - type => 'bool', - default => 0, - help => '' - }, -}; - -sub get_inclduegraph_from_tree { +sub get_includegraph_from_config_tree { my $config_tree = shift; if (!exists $config_tree->{general}{include}) { @@ -1291,7 +829,7 @@ sub get_inclduegraph_from_tree { return $includegraph; } -sub get_config_from_tree { +sub settings_from_config_tree { my $config_tree = shift; my $config = {}; @@ -1313,7 +851,7 @@ sub get_config_from_tree { # Then overwrite the default values with what was read from the config. while (my ($k, $v) = each %{$config_tree->{$section}}) { if (! exists $spec->{$k}) { - printf("unknown property: $k\n"); + die("unknown property in section $section: $k\n"); next; } # The "include" parameter of the "general" section is the only one @@ -1334,14 +872,14 @@ sub get_config_from_tree { my @valid_bool = ('true', 'false', 'yes', 'no', '1', '0'); # Check if the given value can be interpreted as a boolean. foreach my $b (@{$v->[0]}) { - if (none {lc($b) eq $_} @valid_bool) { + if (none { $_ eq lc($b) } @valid_bool) { die "property $k is not a valid boolean"; } } # Check if the given value evaluates to true. sub is_true { my $s = shift; - return any { $s eq $_ } ('true', 'yes', '1'); + return (any { $_ eq $s } ('true', 'yes', '1')); } @value = map { is_true(lc($_)) } @{$v->[0]}; } elsif ($spec->{$k}{type} eq 'section') { @@ -1413,7 +951,7 @@ sub dump_settings { # Get a representation of the configuration sections such that the # "general" section comes first and is followed by the others in sorted # order. - my @sections = sort grep !/^general$/ (keys %{$settings}) + my @sections = sort (grep !/^general$/, (keys %{$settings})); if (exists $settings->{general}) { unshift @sections, "general"; } @@ -1443,19 +981,28 @@ sub dump_settings { } else { $spec = $section_spec; } - print("[$section]\n"); + print("\n[$section]\n"); foreach my $k (sort keys %{$settings->{$section}}) { my $v = $settings->{$section}{$k}; my $type = $spec->{$k}{type}; my $t = $spec->{$k}{help}; - if (exists $spec->{$k}{default}) { - $t .= " (default: "; - $t .= value_formatter($spec->{$k}{default}, $type); - $t .= ")"; + if (exists $spec->{$k}{default} && $type eq "bool") { + $t .= "\n"; + if ($v == $spec->{$k}{default}) { + print(wrap('# ', '# ', $t)); + print "#"; + } else { + $t .= " (default: "; + $t .= value_formatter($spec->{$k}{default}, $type); + $t .= ")"; + $t .= "\n"; + print(wrap('# ', '# ', $t)); + } + } else { + $t .= "\n"; + print(wrap('# ', '# ', $t)); } - $t .= "\n"; - print(wrap('# ', '# ', $t)); - if (ref $v eq 'ARRAY') { + if (ref $v eq 'ARRAY' && $type ne 'stringlist') { foreach my $e (@{$v}) { print("$k=" . value_formatter($e, $type) . "\n"); } @@ -1552,13 +1099,13 @@ sub parse_ini { @seen_includes = ($file); } my $progname = basename($0); - printf STDERR (_g("%s using %s\n"), $progname, $file); + printf STDERR (g_("%s using %s\n"), $progname, $file); tie (my %ini, 'Config::IniFiles', ( -file => $file, -nocase => 1, -allowedcommentchars => '#', -handle_trailing_comment => 1)) - || die sprintf(_g("Failed to parse '%s'!\n"), $file); + || die sprintf(g_("Failed to parse '%s'!\n"), $file); # Go through all included configs, parse them and put the values from # the results into the SECOND tuple element (the children of this # config) @@ -1646,7 +1193,7 @@ sub system_fatal { my $err = $!; $retval >>= 8; return if ($retval == 0); - my $msg = sprintf(_g("ERR: system call failed: '%s' %s"), $cmd, $err); + my $msg = sprintf(g_("ERR: system call failed: '%s' %s"), $cmd, $err); die ("$msg\n"); } @@ -1656,7 +1203,7 @@ sub mkdir_fatal { if (not -d "$d") { my $ret = system ("mkdir -p " . shellescape($d)); $ret >>= 8 if (defined $ret); - my $msg = sprintf (_g("Unable to create directory '%s'"),$d); + my $msg = sprintf (g_("Unable to create directory '%s'"),$d); die "$progname: $msg\n" if ($ret != 0); } } @@ -1675,9 +1222,10 @@ multistrap - multiple repository bootstraps =head1 SYNOPSIS - multistrap [-a ARCH] [-d DIR] -f CONFIG_FILE - multistrap [--simulate] -f CONFIG_FILE - multistrap -?|-h|--help|--version + multistrap SUITE TARGET [MIRROR] + multistrap [-a ARCH] [-d DIR] [-f /path/to/multistrap.conf|-s shortcut] + multistrap [--simulate] [-f /path/to/multistrap.conf|-s shortcut] + multistrap -?|-h|--help =head1 OPTIONS @@ -1685,7 +1233,7 @@ multistrap - multiple repository bootstraps =over 8 -=item B<-?|-h|--help|--version> +=item B<-?|-h|--help> output the help text and exit successfully. @@ -1729,14 +1277,6 @@ Same as cleanup=true. allow the use of unauthenticated repositories. Same as noauth=true -=item B<--source-dir> DIR - -move the contents of var/cache/apt/archives/ from inside the chroot to the -specified external directory, then add the Debian source packages for each -used binary. Same as retainsources=DIR If the specified directory does not -exist, nothing is done. Requires --tidy-up in order to calculate the full list -of source packages, including dependencies. - =back =head1 DESCRIPTION