You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1747 lines
58 KiB
Perl
1747 lines
58 KiB
Perl
#!/usr/bin/perl
|
|
# vim: tabstop=4:shiftwidth=4:softtabstop=4:expandtab
|
|
|
|
# Copyright (C) 2009-2015 Neil Williams <codehelp@debian.org>
|
|
# Copyright (C) 2015-2017 Johannes Schauer <josch@mister-muffin.de>
|
|
#
|
|
# This package is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
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;
|
|
use Config::IniFiles;
|
|
use Cwd qw (realpath getcwd);
|
|
use File::Basename;
|
|
use Parse::Debian::Packages; # FIXME: use Dpkg::Index instead
|
|
use POSIX qw(locale_h);
|
|
use Dpkg::Gettext;
|
|
use File::Copy;
|
|
use List::Util qw(any all none);
|
|
use Text::Wrap;
|
|
use Getopt::Long;
|
|
use Pod::Usage;
|
|
|
|
sub main {
|
|
setlocale(LC_MESSAGES, "");
|
|
textdomain("multistrap");
|
|
my $progname = basename($0);
|
|
|
|
my $options = {};
|
|
# The long option must come before the short option because the first
|
|
# option will become the key in the $options hash.
|
|
#
|
|
# --man is a hidden option (not documented)
|
|
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!'
|
|
) 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 (exists $options->{shortcut} && exists $options->{file}) {
|
|
die (_g("Options --shortcut and --file are mutually exclusive\n"));
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
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();
|
|
# Overwrite settings from the configuration file with settings from the
|
|
# command line
|
|
if (exists $options->{arch}) {
|
|
$settings->{general}{arch} = $options->{arch};
|
|
}
|
|
if (exists $options->{directory}) {
|
|
$settings->{general}{directory} = $options->{directory};
|
|
}
|
|
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.
|
|
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 ".
|
|
"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;
|
|
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);
|
|
} elsif ($arch eq $host) {
|
|
printf (_g("Defaulting architecture to native: %s\n"),$arch);
|
|
} else {
|
|
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;
|
|
}
|
|
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);
|
|
}
|
|
|
|
# Translators: fields are: programname, architecture, host architecture.
|
|
printf (_g("%s building %s multistrap on '%s'\n"), $progname, $arch, $host);
|
|
if ($dir =~ /^$/) {
|
|
my $msg = _g("No directory specified!");
|
|
die "$progname: $msg\n";
|
|
}
|
|
&mkdir_fatal ($dir);
|
|
$dir = realpath ($dir);
|
|
$dir .= ($dir =~ m:/$:) ? '' : "/";
|
|
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}");
|
|
system_fatal ("mkdir -p " . shellescape("${dir}etc/apt/sources.list.d/"))
|
|
if (not -d "${dir}etc/apt/sources.list.d/");
|
|
system_fatal ("mkdir -p " . shellescape("${dir}etc/apt/trusted.gpg.d/"))
|
|
if (not -d "${dir}etc/apt/trusted.gpg.d/");
|
|
system_fatal ("mkdir -p " . shellescape("${dir}etc/apt/preferences.d/"))
|
|
if (not -d "${dir}etc/apt/preferences.d/");
|
|
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) {
|
|
open (PREF, "$preffile") or die ("$progname: $preffile $!");
|
|
my @prefs=<PREF>;
|
|
close (PREF);
|
|
my $name = basename($preffile);
|
|
open (MPREF, ">${dir}etc/apt/preferences.d/$name") or die ("$progname: $name $!");
|
|
print MPREF @prefs;
|
|
close (MPREF);
|
|
}
|
|
my @dirs = qw/ alternatives info parts updates /;
|
|
my @touch = qw/ arch diversions statoverride status lock/;
|
|
foreach my $dpkgd (@dirs) {
|
|
if (not -d "${dir}${dpkgdir}$dpkgd") {
|
|
mkdir_fatal ("${dir}${dpkgdir}$dpkgd");
|
|
}
|
|
}
|
|
foreach my $file (@touch) {
|
|
utime(time, time, "${dir}${dpkgdir}/$file") or (
|
|
open(F, ">${dir}${dpkgdir}/$file") && close F );
|
|
}
|
|
utime(time, time, "${dir}etc/shells") or
|
|
(open(F, ">${dir}etc/shells") && close F );
|
|
|
|
if (not -d "${dir}etc/network") {
|
|
mkdir_fatal ("${dir}etc/network");
|
|
}
|
|
|
|
if (not -d "${dir}dev") {
|
|
mkdir_fatal ("${dir}dev");
|
|
}
|
|
if (scalar (@foreignarches) > 0) {
|
|
open (VMA, ">${dir}${dpkgdir}arch");
|
|
print VMA "$host\n";
|
|
foreach my $farch (@foreignarches) {
|
|
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}");
|
|
}
|
|
if (not -e "$dir/${libdir}lists") {
|
|
mkdir_fatal ("$dir/${libdir}lists");
|
|
}
|
|
if (not -e "$dir/${libdir}lists/partial") {
|
|
mkdir_fatal ("$dir/${libdir}lists/partial");
|
|
}
|
|
if (not -e "$dir/${cachedir}archives") {
|
|
mkdir_fatal ("$dir/${cachedir}archives");
|
|
}
|
|
if (not -e "$dir/${cachedir}archives/partial") {
|
|
mkdir_fatal ("$dir/${cachedir}archives/partial");
|
|
}
|
|
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/") {
|
|
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) {
|
|
print SOURCES "deb [arch=$arch] $mirror $suite $component\n";
|
|
} else {
|
|
foreach my $farch (@foreignarches) {
|
|
print SOURCES "deb [arch=$farch] $mirror $suite $component\n";
|
|
}
|
|
}
|
|
print SOURCES "deb-src $mirror $suite $component\n" if (not defined $omitdebsrc{$aptsrc});
|
|
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);
|
|
}
|
|
}
|
|
|
|
my $pre_config_str = '';
|
|
$pre_config_str .= "Dir::Etc \"${dir}${etcdir}\";\n";
|
|
$pre_config_str .= "Dir::Etc::Parts \"${dir}${etcdir}apt.conf.d/\";\n";
|
|
$pre_config_str .= "Dir::Etc::PreferencesParts \"${dir}${etcdir}preferences.d/\";\n";
|
|
|
|
my $tmp_apt_conf = `mktemp -t multistrap.XXXXXX`;
|
|
chomp ($tmp_apt_conf);
|
|
|
|
open CONFIG, ">$tmp_apt_conf";
|
|
print CONFIG $pre_config_str;
|
|
close CONFIG;
|
|
|
|
my $config_str = '';
|
|
$config_str .= " -o Apt::Architecture=" . shellescape($arch);
|
|
$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);
|
|
$config_str .= " -o Apt::Get::Download-Only=true";
|
|
$config_str .= " -o Apt::Install-Recommends=false"
|
|
if (not defined $allow_recommends);
|
|
$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);
|
|
# 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);
|
|
|
|
my $retval = system ("$apt_get update");
|
|
$retval >>= 8;
|
|
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*$/));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
chomp($str);
|
|
foreach my $keystr (values %keyrings) {
|
|
next if (not defined $keystr);
|
|
$str .= " " . $keystr . " ";
|
|
}
|
|
chomp($str);
|
|
@s = split (/ /, $str);
|
|
@s = uniq_sort (@s);
|
|
$str = join (' ', @s);
|
|
print "$apt_get -y install $str\n";
|
|
$retval = 0;
|
|
$retval = system ("$apt_get -y install $str");
|
|
$retval >>= 8;
|
|
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);
|
|
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 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++;
|
|
}
|
|
&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"));
|
|
my @sources=grep(m:^multistrap-.*\.list$:, readdir LISTS);
|
|
closedir (LISTS);
|
|
foreach my $filelist (@sources) {
|
|
next if (-d $filelist);
|
|
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";
|
|
if (defined $mirror and defined $suite) {
|
|
if (scalar (@foreignarches) == 0) {
|
|
print SOURCES "deb [arch=$arch] $mirror $suite $component\n";
|
|
} else {
|
|
foreach my $farch (@foreignarches) {
|
|
print SOURCES "deb [arch=$farch] $mirror $suite $component\n";
|
|
}
|
|
}
|
|
print SOURCES "deb-src $mirror $suite $component\n" if (not defined $omitdebsrc{$aptsrc});
|
|
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);
|
|
} 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);
|
|
}
|
|
}
|
|
print "\n";
|
|
if (not defined $warn_count) {
|
|
exit 0;
|
|
} else {
|
|
exit $warn_count;
|
|
}
|
|
}
|
|
|
|
# avoid dependency on String::ShellQuote by implementing the mechanism
|
|
# from python's shlex.quote function
|
|
sub shellescape {
|
|
my $string = shift;
|
|
if (length $string == 0) {
|
|
return "''";
|
|
}
|
|
# search for occurrences of characters that are not safe
|
|
# the 'a' regex modifier makes sure that \w only matches ASCII
|
|
if ($string !~ m/[^\w@\%+=:,.\/-]/a) {
|
|
return $string;
|
|
}
|
|
# wrap the string in single quotes and handle existing single quotes by
|
|
# putting them outside of the single-quoted string
|
|
$string =~ s/'/'"'"'/g;
|
|
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=<MSCRIPT>;
|
|
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=<CONF>;
|
|
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=<SEED>;
|
|
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_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);
|
|
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));
|
|
$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=<STATUS>;
|
|
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");
|
|
}
|
|
}
|
|
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 = <SOURCES>;
|
|
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(<SOURCES>) {
|
|
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 {
|
|
my $config_tree = shift;
|
|
|
|
if (!exists $config_tree->{general}{include}) {
|
|
return [];
|
|
}
|
|
# Traverse the tree in depth-first-search order.
|
|
#
|
|
# If the same file occurs in multiple branches of the tree, then the
|
|
# resulting graph will be a directed acyclic graph and not a tree
|
|
# anymore.
|
|
my $includegraph = [];
|
|
sub visit {
|
|
my $acc = shift;
|
|
my $n = shift;
|
|
# The origin of the includes in this file must exactly be one file
|
|
# and not the result of a merge of two or more files.
|
|
if (scalar @{$n->[2]} != 1) {
|
|
die "Include statements were merged but that is forbidden";
|
|
}
|
|
# Add an edge from the filename of this node to all files that it
|
|
# included.
|
|
my $f = $n->[2]->[0];
|
|
for my $i (@{$n->[0]}) {
|
|
# Make the filename absolute instead of relative to the
|
|
# current file
|
|
push @{$acc}, [$f, dirname($f) . '/' . $i];
|
|
}
|
|
# Recurse.
|
|
for my $c (@{$n->[1]}) {
|
|
visit($acc, $c);
|
|
}
|
|
};
|
|
visit($includegraph, $config_tree->{general}{include});
|
|
return $includegraph;
|
|
}
|
|
|
|
sub get_config_from_tree {
|
|
my $config_tree = shift;
|
|
|
|
my $config = {};
|
|
foreach my $section (keys %{$config_tree}) {
|
|
my $spec;
|
|
if ($section eq "general") {
|
|
$spec = $general_spec;
|
|
} else {
|
|
$spec = $section_spec;
|
|
}
|
|
# First fill the default with the configuration from the spec.
|
|
while (my ($k, $v) = each %{$spec}) {
|
|
# Do not set values from the spec that do not have a default
|
|
if (! exists $v->{default}) {
|
|
next;
|
|
}
|
|
$config->{$section}{$k} = $v->{default};
|
|
}
|
|
# 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");
|
|
next;
|
|
}
|
|
# The "include" parameter of the "general" section is the only one
|
|
# where we are interested in more values than from the root node of
|
|
# the config tree. We do not handle it here.
|
|
if ($section eq 'general' && $k eq 'include') {
|
|
next;
|
|
}
|
|
# Make sure that non-list-type values contain no more than one element
|
|
if (scalar @{$v->[0]} > 1 && $spec->{$k}{list} != 1) {
|
|
die "property $k must not be a list";
|
|
}
|
|
my @value;
|
|
# Convert and validate config settings.
|
|
if ($spec->{$k}{type} eq 'string') {
|
|
@value = @{$v->[0]};
|
|
} elsif ($spec->{$k}{type} eq 'bool') {
|
|
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) {
|
|
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');
|
|
}
|
|
@value = map { is_true(lc($_)) } @{$v->[0]};
|
|
} elsif ($spec->{$k}{type} eq 'section') {
|
|
# Check if the given section name matches an existing section.
|
|
foreach my $s (@{$v->[0]}) {
|
|
foreach my $t (split /\s+/, $s) {
|
|
if (lc($t) eq "general") {
|
|
die "section name $t forbidden";
|
|
}
|
|
if (! exists $config_tree->{lc($t)}) {
|
|
die "case-insensitive section name $t not found";
|
|
}
|
|
}
|
|
}
|
|
@value = map { lc } (map { split /\s+/ } @{$v->[0]});
|
|
} elsif ($spec->{$k}{type} eq 'stringlist') {
|
|
@value = map { split /\s+/ } @{$v->[0]};
|
|
} else {
|
|
die "invalid type: $spec->{$k}{type}";
|
|
}
|
|
if (exists $spec->{$k}{list} && $spec->{$k}{list} == 1) {
|
|
# If this is a list-type value, store it as an array reference
|
|
$config->{$section}{$k} = [@value];
|
|
} else {
|
|
# If this is a non-list value, store its first (and only) value as
|
|
# a simple scalar
|
|
$config->{$section}{$k} = $value[0];
|
|
}
|
|
}
|
|
}
|
|
return $config;
|
|
}
|
|
|
|
# Write a representation of the include graph in dot format to standard output
|
|
sub dump_includegraph {
|
|
my $includegraph = shift;
|
|
|
|
print "digraph g {\n";
|
|
my %mapping = ();
|
|
my $num_verts = 0;
|
|
foreach my $e (@{$includegraph}) {
|
|
my ($v1, $v2) = @{$e};
|
|
if (! exists $mapping{$v1}) {
|
|
$mapping{$v1} = $num_verts;
|
|
$num_verts += 1;
|
|
}
|
|
if (! exists $mapping{$v2}) {
|
|
$mapping{$v2} = $num_verts;
|
|
$num_verts += 1;
|
|
}
|
|
}
|
|
foreach my $v (sort keys %mapping) {
|
|
my $i = $mapping{$v};
|
|
print " $i [label=\"$v\"];\n";
|
|
}
|
|
foreach my $e (@{$includegraph}) {
|
|
my ($v1, $v2) = @{$e};
|
|
my $i1 = $mapping{$v1};
|
|
my $i2 = $mapping{$v2};
|
|
print " $i1 -> $i2;\n";
|
|
}
|
|
print "}\n";
|
|
return $includegraph;
|
|
}
|
|
|
|
sub dump_settings {
|
|
my $settings = shift;
|
|
|
|
# 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})
|
|
if (exists $settings->{general}) {
|
|
unshift @sections, "general";
|
|
}
|
|
|
|
sub value_formatter {
|
|
my $val = shift;
|
|
my $type = shift;
|
|
if ($type eq "bool") {
|
|
if ($val) {
|
|
return "true";
|
|
} else {
|
|
return "false";
|
|
}
|
|
} elsif ($type eq "section") {
|
|
return lc($val);
|
|
} elsif ($type eq "stringlist") {
|
|
return join " ", @{$val};
|
|
} else {
|
|
return $val;
|
|
}
|
|
};
|
|
|
|
foreach my $section (@sections) {
|
|
my $spec;
|
|
if ($section eq "general") {
|
|
$spec = $general_spec;
|
|
} else {
|
|
$spec = $section_spec;
|
|
}
|
|
print("[$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 .= ")";
|
|
}
|
|
$t .= "\n";
|
|
print(wrap('# ', '# ', $t));
|
|
if (ref $v eq 'ARRAY') {
|
|
foreach my $e (@{$v}) {
|
|
print("$k=" . value_formatter($e, $type) . "\n");
|
|
}
|
|
} else {
|
|
print("$k=" . value_formatter($v, $type) . "\n");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Given a config.ini, recursively traverses all included ini files and returns
|
|
# a hash which represents a merge of the included ini file tree.
|
|
#
|
|
# Note, that the resulting data structure will even be a tree if the same ini
|
|
# is included by multiple siblings.
|
|
#
|
|
# The first argument is the ini file to parse.
|
|
#
|
|
# To prevent cycles, the remaining arguments are the set of ini files that
|
|
# make the path of the current config to the root to prevent cycles.
|
|
#
|
|
# The merging is done such that all configuration values that are specified in
|
|
# more than one descendant, are represented as nested array refs representing
|
|
# the transitive reduction of the configuration file tree that they appeared
|
|
# in. We don't use nested hash refs because those would not remain in order.
|
|
#
|
|
# Nodes in the tree are represented as tuples (array refs) where the first
|
|
# element is the list of values stored in the current node and the second
|
|
# element is its list of children.
|
|
#
|
|
# Example:
|
|
#
|
|
# complex.ini:
|
|
# [general]
|
|
# include=blub.ini
|
|
# include=bla.ini
|
|
# property=1
|
|
#
|
|
# branch1.ini:
|
|
# [general]
|
|
# include=shared.ini
|
|
# property=2
|
|
#
|
|
# branch2.ini
|
|
# [general]
|
|
# include=intermediate.ini
|
|
# property=3
|
|
#
|
|
# intermediate.ini
|
|
# [general]
|
|
# include=shared.ini
|
|
# foo=bar
|
|
#
|
|
# shared.ini
|
|
# [general]
|
|
# property=4
|
|
#
|
|
# Result:
|
|
#
|
|
# my %config = {
|
|
# general => {
|
|
# include => [
|
|
# [ 'branch1.ini', 'branch2.ini' ],
|
|
# [
|
|
# [ ['shared.ini'], [] ],
|
|
# [ ['intermediate.ini'], [
|
|
# [ ['shared.ini'], [] ]
|
|
# ] ]
|
|
# ]
|
|
# ],
|
|
# property => [
|
|
# [ '1' ],
|
|
# [
|
|
# [ ['2'], [ [ [ '4' ], [] ] ] ],
|
|
# [ ['3'], [ [ [ '4' ], [] ] ] ],
|
|
# ]
|
|
# ],
|
|
# foo => [ [ 'bar' ], [] ]
|
|
# }
|
|
# }
|
|
#
|
|
# Observations:
|
|
#
|
|
# - The full include tree is seen in $config{'global'}{'include'}.
|
|
# - Part of the tree is seen in $config{'global'}{'property'}. There is no
|
|
# "node" for intermediate.ini because it didn't contain the property.
|
|
# - $config{'global'}{'foo'} is a simple scalar because it only occurred once
|
|
sub parse_ini {
|
|
my $file = shift;
|
|
my @seen_includes = @_;
|
|
# if this is the first call, then seen_includes might be empty. Then
|
|
# add ourselves
|
|
if (scalar @seen_includes == 0) {
|
|
@seen_includes = ($file);
|
|
}
|
|
my $progname = basename($0);
|
|
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);
|
|
# Go through all included configs, parse them and put the values from
|
|
# the results into the SECOND tuple element (the children of this
|
|
# config)
|
|
my $config;
|
|
if (exists $ini{'general'}{'include'}) {
|
|
my @includes;
|
|
if (ref ($ini{'general'}{'include'}) eq 'ARRAY') {
|
|
@includes = @{$ini{'general'}{'include'}};
|
|
} else {
|
|
@includes = ($ini{'general'}{'include'});
|
|
}
|
|
foreach my $include (@includes) {
|
|
if (any { $_ eq $include } @seen_includes) {
|
|
die "$include was included already. Cyclic or duplicate includes detected.";
|
|
}
|
|
my $newini = parse_ini(dirname($file).'/'.$include, uniq_sort(@seen_includes, $include));
|
|
# merge this configuration into the ones that were read so far
|
|
foreach my $section (keys %{$newini}) {
|
|
# FIXME: we would like to use "each" but there is
|
|
# #849298
|
|
foreach my $parameter (keys %{$newini->{$section}}) {
|
|
my $value = $newini->{$section}{$parameter};
|
|
if (exists $config->{$section}{$parameter}) {
|
|
push @{$config->{$section}{$parameter}[1]}, $value;
|
|
push @{$config->{$section}{$parameter}[2]}, $include;
|
|
} else {
|
|
# parameter doesn't exist, so just copy it
|
|
$config->{$section}{$parameter} = [ undef, [$value], [$include] ];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
# Go through this config and put the read values into the FIRST tuple
|
|
# element
|
|
foreach my $section (keys %ini) {
|
|
foreach my $parameter (keys %{$ini{$section}}) {
|
|
my $value = $ini{$section}{$parameter};
|
|
if (ref $value ne 'ARRAY') {
|
|
$value = [$value];
|
|
}
|
|
if (exists $config->{$section}{$parameter}) {
|
|
$config->{$section}{$parameter}[0] = $value;
|
|
$config->{$section}{$parameter}[2] = [$file];
|
|
} else {
|
|
$config->{$section}{$parameter} = [ $value, [], [$file] ];
|
|
}
|
|
}
|
|
}
|
|
# Go through all config parameters at this level (we don't recurse
|
|
# here) and apply a transformation on nodes that were not filled by
|
|
# this config file
|
|
foreach my $section (keys %{$config}) {
|
|
foreach my $parameter (keys %{$config->{$section}}) {
|
|
my $value = $config->{$section}{$parameter};
|
|
# only operate on this node if its value is not set
|
|
if (defined $value->[0]) {
|
|
next;
|
|
}
|
|
if (scalar @{$value->[1]} == 1) {
|
|
# if this node only has a single child, replace this node
|
|
# by the child
|
|
$config->{$section}{$parameter} = $value->[1]->[0];
|
|
} else {
|
|
# concatenate the values of all leave nodes to the value of
|
|
# this node
|
|
my @leaves = grep {scalar @{$_->[1]} == 0} @{$value->[1]};
|
|
my @nonleaves = grep {scalar @{$_->[1]} != 0} @{$value->[1]};
|
|
$config->{$section}{$parameter} = [
|
|
# make sure to dereference the leave values so
|
|
# that we do not get a nested list
|
|
[map({@{$_->[0]}} @leaves)],
|
|
[@nonleaves],
|
|
[map({@{$_->[2]}} @leaves)]
|
|
]
|
|
}
|
|
}
|
|
}
|
|
return $config;
|
|
}
|
|
|
|
sub system_fatal {
|
|
my $cmd = shift;
|
|
my $retval = system ("$cmd");
|
|
my $err = $!;
|
|
$retval >>= 8;
|
|
return if ($retval == 0);
|
|
my $msg = sprintf(_g("ERR: system call failed: '%s' %s"), $cmd, $err);
|
|
die ("$msg\n");
|
|
}
|
|
|
|
sub mkdir_fatal {
|
|
my $progname = basename($0);
|
|
my $d = shift;
|
|
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);
|
|
die "$progname: $msg\n" if ($ret != 0);
|
|
}
|
|
}
|
|
|
|
sub uniq_sort {
|
|
my %uniq;
|
|
@uniq{@_} = ();
|
|
return sort keys %uniq;
|
|
}
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
multistrap - multiple repository bootstraps
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
multistrap [-a ARCH] [-d DIR] -f CONFIG_FILE
|
|
multistrap [--simulate] -f CONFIG_FILE
|
|
multistrap -?|-h|--help|--version
|
|
|
|
=head1 OPTIONS
|
|
|
|
=head2 General Options
|
|
|
|
=over 8
|
|
|
|
=item B<-?|-h|--help|--version>
|
|
|
|
output the help text and exit successfully.
|
|
|
|
=item B<--dry-run> B<--simulate>
|
|
|
|
collate all the configuration settings and output a bare summary.
|
|
|
|
=back
|
|
|
|
=head2 Configuration Options
|
|
|
|
These options overwrite values from the given configuration file which is
|
|
documented in L<multistrap.conf(5)>.
|
|
|
|
=over 8
|
|
|
|
=item B<-a|--arch>
|
|
|
|
architecture of the packages to put into the multistrap.
|
|
|
|
=item B<-d|--dir>
|
|
|
|
directory into which the bootstrap will be installed.
|
|
|
|
=item B<-f|--file>
|
|
|
|
configuration file for multistrap [required]
|
|
|
|
=item B<-s|--shortcut>
|
|
|
|
shortened version of -f for files in known locations without the .conf suffix.
|
|
Searched locations are F</usr/share/multistrap/>, F</etc/multistrap.d/> and
|
|
F<~/.config/multistrap>.
|
|
|
|
=item B<--tidy-up>
|
|
|
|
remove apt cache data, downloaded Packages files and the apt package cache.
|
|
Same as cleanup=true.
|
|
|
|
=item B<--no-auth>
|
|
|
|
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
|
|
|
|
blubber
|
|
|
|
=cut
|