#!/usr/bin/perl
# Debian GNU/Linux chdist. Copyright (C) 2007 Lucas Nussbaum and Luk Claes.
#
# This program 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 2 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 .
=head1 NAME
chdist - script to easily play with several distributions
=head1 SYNOPSIS
B [I] [I] [I]
=head1 DESCRIPTION
B is a rewrite of what used to be known as 'MultiDistroTools'
(or mdt). Its use is to create 'APT trees' for several distributions,
making it easy to query the status of packages in other distribution
without using chroots, for instance.
=head1 OPTIONS
=over 4
=item B<-h>, B<--help>
Provide a usage message.
=item B<-d>, B<--data-dir> I
Choose data directory (default: F<~/.chdist/>).
=item B<-a>, B<--arch> I
Choose architecture (default: `B`).
=item B<--version>
Display version information.
=back
=head1 COMMANDS
=over 4
=item B I [I I I]
Prepare a new tree named I
=item B I |B|B|B|...>
Run B inside I
=item B I |B|...>
Run B inside I
=item B I |B|...>
Run B inside I
=item B I |B|...>
Run B inside I
=item B I [...]
Run B inside I
=item B I [...]
Run B inside I
=item B I
List binary packages for I in I
=item B I
List source package for I in I
=item B I [I, ...]
=item B I [I, ...]
List versions of packages in several Iributions
=item B I
=item B I
Same as B/B, but also runs
B and display where the package is newer.
=item B I
Compare sources and binaries for I
=item B I
Same as B, but also run B
and display where the package is newer
=item B I [...]
Run B on F<*_Packages> inside I
=item B I [...]
Run B on F<*_Sources> inside I
=item B
List available Is
=back
=head1 COPYRIGHT
This program is copyright 2007 by Lucas Nussbaum and Luk Claes. This
program comes with ABSOLUTELY NO WARRANTY.
It is licensed under the terms of the GPL, either version 2 of the
License, or (at your option) any later version.
=cut
use strict;
use warnings;
no if $] >= 5.018, 'warnings', 'experimental::smartmatch';
use feature 'switch';
use File::Copy qw(cp);
use File::HomeDir;
use File::Path qw(make_path);
use File::Basename;
use Getopt::Long qw(:config gnu_compat bundling require_order);
use Cwd qw(abs_path cwd);
use Dpkg::Version qw(version_compare);
use Pod::Usage;
# Redefine Pod::Text's cmd_i so pod2usage converts I<...> to <...> instead of
# *...*
{
package Pod::Text;
no warnings qw(redefine);
sub cmd_i { '<' . $_[2] . '>' }
}
my $progname = basename($0);
sub usage {
pod2usage(
-verbose => 99,
-exitval => $_[0],
-sections => 'SYNOPSIS|OPTIONS|ARGUMENTS|COMMANDS'
);
}
# specify the options we accept and initialize
# the option parser
my $help = '';
my $version = '';
my $versioninfo = <<"EOF";
This is $progname, from the Debian devscripts package, version
###VERSION### This code is copyright 2007 by Lucas Nussbaum and Luk
Claes. This program comes with ABSOLUTELY NO WARRANTY. You are free
to redistribute this code under the terms of the GNU General Public
License, version 2 or (at your option) any later version.
EOF
my $arch;
my $datadir = File::HomeDir->my_home . '/.chdist';
GetOptions(
"h|help" => \$help,
"d|data-dir=s" => \$datadir,
"a|arch=s" => \$arch,
"version" => \$version,
) or usage(1);
# Fix-up relative paths
$datadir = cwd() . "/$datadir" if $datadir !~ m!^/!;
$datadir = abs_path($datadir);
if ($help) {
usage(0);
}
if ($version) {
print $versioninfo;
exit 0;
}
########################################################
### Functions
########################################################
sub fatal {
my ($msg) = @_;
$msg =~ s/\n?$/\n/;
print STDERR "$progname: $msg";
exit 1;
}
sub uniq (@) {
my %hash;
map { $hash{$_}++ == 0 ? $_ : () } @_;
}
sub dist_check {
# Check that dist exists in $datadir
my ($dist) = @_;
if ($dist) {
my $dir = "$datadir/$dist";
return 0 if (-d $dir);
fatal(
"Could not find $dist in $datadir. Run `$progname create $dist` first."
);
} else {
fatal('No dist provided.');
}
}
sub type_check {
my ($type) = @_;
if (($type ne 'Sources') && ($type ne 'Packages')) {
fatal("Unknown type $type.");
}
}
sub aptopts {
# Build apt options
my ($dist) = @_;
my @opts = ();
if ($arch) {
print "W: Forcing arch $arch for this command only.\n";
push(@opts, '-o', "Apt::Architecture=$arch");
push(@opts, '-o', "Apt::Architectures=$arch");
}
return @opts;
}
sub aptconfig {
# Build APT_CONFIG override
my ($dist) = @_;
my $aptconf = "$datadir/$dist/etc/apt/apt.conf";
if (!-r $aptconf) {
fatal("Unable to read $aptconf");
}
$ENV{'APT_CONFIG'} = $aptconf;
}
###
sub aptcmd {
my ($cmd, $dist, @args) = @_;
dist_check($dist);
unshift(@args, aptopts($dist));
aptconfig($dist);
exec($cmd, @args);
}
sub apt_file {
my ($dist, @args) = @_;
dist_check($dist);
aptconfig($dist);
my @query = ('dpkg-query', '-W', '-f');
open(my $fd, '-|', @query, '${Version}', 'apt-file')
or fatal('Unable to run dpkg-query.');
my $aptfile_version = <$fd>;
close($fd);
if (version_compare('3.0~', $aptfile_version) < 0) {
open($fd, '-|', @query, '${Conffiles}\n', 'apt-file')
or fatal('Unable to run dpkg-query.');
my @aptfile_confs = map { (split)[0] }
grep { /apt\.conf\.d/ } <$fd>;
close($fd);
# New-style apt-file
for my $conffile (@aptfile_confs) {
if (!-f "$datadir/$dist/$conffile") {
cp($conffile, "$datadir/$dist/$conffile");
}
}
} else {
my $cache_directory
= $datadir . '/' . $dist . "/var/cache/apt/apt-file";
unshift(@args, '--cache', $cache_directory);
}
exec('apt-file', @args);
}
sub bin2src {
my ($dist, $pkg) = @_;
dist_check($dist);
if (!defined($pkg)) {
fatal("No package name provided. Exiting.");
}
my @args = (aptopts($dist), 'show', $pkg);
aptconfig($dist);
my $src = $pkg;
my $pid = open(CACHE, '-|', 'apt-cache', @args);
if (!defined($pid)) {
fatal("Couldn't run apt-cache: $!");
}
if ($pid) {
while () {
if (m/^Source: (.*)/) {
$src = $1;
# Slurp remaining output to avoid SIGPIPE
local $/ = undef;
my $junk = ;
last;
}
}
close CACHE || fatal("bad apt-cache $!: $?");
print "$src\n";
}
}
sub src2bin {
my ($dist, $pkg) = @_;
dist_check($dist);
if (!defined($pkg)) {
fatal("no package name provided. Exiting.");
}
my @args = (aptopts($dist), 'showsrc', $pkg);
aptconfig($dist);
my $pid = open(CACHE, '-|', 'apt-cache', @args);
if (!defined($pid)) {
fatal("Couldn't run apt-cache: $!");
}
if ($pid) {
while () {
if (m/^Binary: (.*)/) {
print join("\n", split(/, /, $1)) . "\n";
# Slurp remaining output to avoid SIGPIPE
local $/ = undef;
my $junk = ;
last;
}
}
close CACHE || fatal("bad apt-cache $!: $?");
}
}
sub dist_create {
my ($dist, $method, $version, @sections) = @_;
if (!defined($dist)) {
fatal("you must provide a dist name.");
}
my $dir = "$datadir/$dist";
if (-d $dir) {
fatal("$dir already exists, exiting.");
}
make_path($datadir);
foreach my $d ((
'/etc/apt', '/etc/apt/apt.conf.d',
'/etc/apt/preferences.d', '/etc/apt/trusted.gpg.d',
'/etc/apt/sources.list.d', '/var/lib/apt/lists/partial',
'/var/cache/apt/archives/partial', '/var/lib/dpkg'
)
) {
make_path("$dir/$d");
}
# Create sources.list
open(FH, '>', "$dir/etc/apt/sources.list");
if ($version) {
# Use provided method, version and sections
my $sections_str = join(' ', @sections);
print FH <', "$dir/var/lib/dpkg/status");
close FH; #empty file
# Create apt.conf
$arch ||= `dpkg --print-architecture`;
chomp $arch;
open(FH, ">$dir/etc/apt/apt.conf");
print FH <{$package}{Version};
my $delta
= $version
&& $alt_ver
&& version_compare($version, $alt_ver);
if (defined($delta) && $delta < 0) {
$packages{$dist}{$package} = $parsed_file->{$package};
} else {
warn
"W: Package $package is already listed for $dist. Not overriding.\n";
}
} else {
$packages{$dist}{$package} = $parsed_file->{$package};
}
}
}
}
# Get entire list of packages
my @all_packages = uniq sort (map { keys(%{ $packages{$_} }) } @dists);
foreach my $package (@all_packages) {
my $line = "$package ";
my $status = "";
my $details;
foreach my $dist (@dists) {
if ($packages{$dist}{$package}) {
$line .= "$packages{$dist}{$package}{'Version'} ";
} else {
$line .= "UNAVAIL ";
$status = "not_in_$dist";
}
}
my @versions = map { $packages{$_}{$package}{'Version'} } @dists;
# Escaped versions
my @esc_vers = @versions;
foreach my $vers (@esc_vers) {
$vers =~ s|\+|\\\+| if defined $vers;
}
# Do compare
if ($do_compare) {
if (!@dists) {
fatal('Can only compare versions if there are two distros.');
}
if (!$status) {
my $cmp = version_compare($versions[0], $versions[1]);
if (!$cmp) {
$status = "same_version";
} elsif ($cmp < 0) {
$status = "newer_in_$dists[1]";
if ($versions[1] =~ m|^$esc_vers[0]|) {
$details = " local_changes_in_$dists[1]";
}
} else {
$status = "newer_in_$dists[0]";
if ($versions[0] =~ m|^$esc_vers[1]|) {
$details = " local_changes_in_$dists[0]";
}
}
}
$line .= " $status $details";
}
print "$line\n";
}
}
sub compare_src_bin {
my ($dist, $do_compare) = @_;
dist_check($dist);
# Get all packages
my %packages;
my @parse_types = ('Sources', 'Packages');
my @comp_types = ('Sources_Bin', 'Packages');
foreach my $type (@parse_types) {
my $files = get_distfiles($dist, $type);
my @files = @$files;
foreach my $file (@files) {
my $parsed_file = parseFile($file);
foreach my $package (keys(%{$parsed_file})) {
if ($packages{$dist}{$package}) {
warn
"W: Package $package is already listed for $dist. Not overriding.\n";
} else {
$packages{$type}{$package} = $parsed_file->{$package};
}
}
}
}
# Build 'Sources_Bin' hash
foreach my $package (keys(%{ $packages{Sources} })) {
my $package_h = \%{ $packages{Sources}{$package} };
if ($package_h->{'Binary'}) {
my @binaries = split(", ", $package_h->{'Binary'});
my $version = $package_h->{'Version'};
foreach my $binary (@binaries) {
if (defined $packages{Sources_Bin}{$binary}) {
my $alt_ver = $packages{Sources_Bin}{$binary}{Version};
# Skip this entry if it's an older version than we already
# have
if (version_compare($version, $alt_ver) < 0) {
next;
}
}
$packages{Sources_Bin}{$binary}{Version} = $version;
}
} else {
warn "Source $package has no binaries!\n";
}
}
# Get entire list of packages
my @all_packages
= uniq sort (map { keys(%{ $packages{$_} }) } @comp_types);
foreach my $package (@all_packages) {
my $line = "$package ";
my $status = "";
my $details = '';
foreach my $type (@comp_types) {
if ($packages{$type}{$package}) {
$line .= "$packages{$type}{$package}{'Version'} ";
} else {
$line .= "UNAVAIL ";
$status = "not_in_$type";
}
}
my @versions = map { $packages{$_}{$package}{'Version'} } @comp_types;
# Do compare
if ($do_compare) {
if (!@comp_types) {
fatal('Can only compare versions if there are two types.');
}
if (!$status) {
my $cmp = version_compare($versions[0], $versions[1]);
if (!$cmp) {
$status = "same_version";
} elsif ($cmp < 0) {
$status = "newer_in_$comp_types[1]";
if ($versions[1] =~ m|^\Q$versions[0]\E|) {
$details = " local_changes_in_$comp_types[1]";
}
} else {
$status = "newer_in_$comp_types[0]";
if ($versions[0] =~ m|^\Q$versions[1]\E|) {
$details = " local_changes_in_$comp_types[0]";
}
}
}
$line .= " $status $details";
}
print "$line\n";
}
}
sub grep_file(\@$) {
my ($argv, $file) = @_;
my $dist = shift @{$argv};
dist_check($dist);
my @f = glob($datadir . '/' . $dist . "/var/lib/apt/lists/*_$file");
if (@f) {
exec('grep-dctrl', @{$argv}, @f);
} else {
fatal("Couldn't find a $file for $dist.");
}
}
sub list {
opendir(DIR, $datadir) or fatal("can't open dir $datadir: $!");
while (my $file = readdir(DIR)) {
if ((-d "$datadir/$file") && ($file =~ m|^\w+|)) {
print "$file\n";
}
}
closedir(DIR);
}
sub parseFile {
my ($file) = @_;
# Parse a source file and returns results as a hash
open(FILE, '<', $file) || fatal("Could not open $file : $!");
# Use %tmp hash to store tmp data
my %tmp;
my %result;
while (my $line = ) {
if ($line =~ m|^$|) {
# Commit data if empty line
if ($tmp{'Package'}) {
#print "Committing data for $tmp{'Package'}\n";
while (my ($field, $data) = each(%tmp)) {
if ($field ne "Package") {
$result{ $tmp{'Package'} }{$field} = $data;
}
}
# Reset %tmp
%tmp = ();
} else {
warn "W: No Package field found. Not committing data.\n";
}
} elsif ($line =~ m|^[a-zA-Z]|) {
# Gather data
my ($field, $data) = $line =~ m|([a-zA-Z-]+): (.*)$|;
if ($data) {
$tmp{$field} = $data;
}
}
}
close(FILE);
return \%result;
}
########################################################
### Command parsing
########################################################
my $recursed = 0;
MAIN:
my $command = shift @ARGV;
given ($command) {
when ('create') {
dist_create(@ARGV);
}
when ('apt') {
aptcmd('apt', @ARGV);
}
when ('apt-get') {
aptcmd('apt-get', @ARGV);
}
when ('apt-cache') {
aptcmd('apt-cache', @ARGV);
}
when ('apt-file') {
apt_file(@ARGV);
}
when ('apt-rdepends') {
aptcmd('apt-rdepends', @ARGV);
}
when ('aptitude') {
aptcmd('aptitude', @ARGV);
}
when ('bin2src') {
bin2src(@ARGV);
}
when ('src2bin') {
src2bin(@ARGV);
}
when ('compare-packages') {
dist_compare(@ARGV, 0, 'Sources');
}
when ('compare-bin-packages') {
dist_compare(@ARGV, 0, 'Packages');
}
when ('compare-versions') {
dist_compare(@ARGV, 1, 'Sources');
}
when ('compare-bin-versions') {
dist_compare(@ARGV, 1, 'Packages');
}
when ('grep-dctrl-packages') {
grep_file(@ARGV, 'Packages');
}
when ('grep-dctrl-sources') {
grep_file(@ARGV, 'Sources');
}
when ('compare-src-bin-packages') {
compare_src_bin(@ARGV, 0);
}
when ('compare-src-bin-versions') {
compare_src_bin(@ARGV, 1);
}
when ('list') {
list;
}
default {
my $dist = $command;
my $dir = "$datadir/$dist";
if (-d $dir && !$recursed) {
splice @ARGV, 1, 0, $dist;
$recursed = 1;
goto MAIN;
} elsif ($dist && !$recursed) {
dist_check($dist);
} else {
usage(1);
}
}
}