#!/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); } } }