3 # Debian GNU/Linux chdist. Copyright (C) 2007 Lucas Nussbaum and Luk Claes.
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 chdist - script to easily play with several distributions
24 B<chdist> [options] [command] [command parameters]
28 B<chdist> is a rewrite of what used to be known as 'MultiDistroTools'
29 (or mdt). Its use is to create 'APT trees' for several distributions,
30 making it easy to query the status of packages in other distribution
31 without using chroots, for instance.
39 Provide a usage message.
41 =item -d, --data-dir DIR
43 Choose data directory (default: $HOME/.chdist/).
47 Choose architecture (default: `dpkg --print-architecture`)
51 Display version information.
59 =item create DIST : prepare a new tree named DIST
61 =item apt-get DIST (update|source|...) : run apt-get inside DIST
63 =item apt-cache DIST (show|showsrc|...) : run apt-cache inside DIST
65 =item apt-rdepends DIST [...] : run apt-rdepends inside DIST
67 =item src2bin DIST PKG : get binary packages for a source package in DIST
69 =item bin2src DIST PKG : get source package for a binary package in DIST
71 =item compare-packages DIST1 DIST2 [DIST3, ...] : list versions of packages in several DISTributions
73 =item compare-bin-packages DIST1 DIST2 [DIST3, ...]
75 =item compare-versions DIST1 DIST2 : same as compare-packages, but also run dpkg --compare-versions and display where the package is newer.
77 =item compare-bin-versions DIST1 DIST2
79 =item compare-src-bin-packages DIST : compare sources and binaries for DIST
81 =item compare-src-bin-versions DIST : same as compare-src-bin-versions, but also run dpkg --compare-versions and display where the package is newer
83 =item grep-dctrl-packages DIST [...] : run grep-dctrl on *_Packages inside DIST
85 =item grep-dctrl-sources DIST [...] : run grep-dctrl on *_Sources inside DIST
87 =item list : list available DISTs
93 This program is copyright 2007 by Lucas Nussbaum and Luk Claes. This
94 program comes with ABSOLUTELY NO WARRANTY.
96 It is licensed under the terms of the GPL, either version 2 of the
97 License, or (at your option) any later version.
104 use Getopt::Long qw(:config require_order);
105 use Cwd qw(abs_path cwd);
108 my $progname = basename($0);
112 Usage: chdist [options] [command] [command parameters]
115 -h, --help Show this help
116 -d, --data-dir DIR Choose data directory (default: \$HOME/.chdist/)
117 -a, --arch ARCH Choose architecture (default: `dpkg --print-architecture`)
118 -v, --version Display version and copyright information
121 create DIST : prepare a new tree named DIST
122 apt-get DIST (update|source|...) : run apt-get inside DIST
123 apt-cache DIST (show|showsrc|...) : run apt-cache inside DIST
124 apt-rdepends DIST [...] : run apt-rdepends inside DIST
125 src2bin DIST PKG : get binary packages for a source package in DIST
126 bin2src DIST PKG : get source package for a binary package in DIST
127 compare-packages DIST1 DIST2 [DIST3, ...] : list versions of packages in
128 several DISTributions
129 compare-bin-packages DIST1 DIST2 [DIST3, ...]
130 compare-versions DIST1 DIST2 : same as compare-packages, but also run
131 dpkg --compare-versions and display where the package is newer
132 compare-bin-versions DIST1 DIST2
133 compare-src-bin-packages DIST : compare sources and binaries for DIST
134 compare-src-bin-versions DIST : same as compare-src-bin-versions, but also
135 run dpkg --compare-versions and display where the package is newer
136 grep-dctrl-packages DIST [...] : run grep-dctrl on *_Packages inside DIST
137 grep-dctrl-sources DIST [...] : run grep-dctrl on *_Sources inside DIST
138 list : list available DISTs
142 # specify the options we accept and initialize
147 my $versioninfo = <<"EOF";
148 This is $progname, from the Debian devscripts package, version
149 ###VERSION### This code is copyright 2007 by Lucas Nussbaum and Luk
150 Claes. This program comes with ABSOLUTELY NO WARRANTY. You are free
151 to redistribute this code under the terms of the GNU General Public
152 License, version 2 or (at your option) any later version.
156 my $datadir = $ENV{'HOME'} . '/.chdist';
160 "data-dir=s" => \$datadir,
162 "version" => \$version,
165 # Fix-up relative paths
166 $datadir = cwd() . "/$datadir" unless $datadir =~ m!^/!;
167 $datadir = abs_path($datadir);
180 ########################################################
182 ########################################################
186 map { $hash{$_}++ == 0 ? $_ : () } @_;
190 # Check that dist exists in $datadir
193 my $dir = $datadir . '/' . $dist;
194 return 0 if (-d $dir);
195 die "E: Could not find $dist in $datadir. Run `$0 create $dist` first. Exiting.\n";
197 die "E: No dist provided. Exiting. \n";
203 if ( ($type ne 'Sources') && ($type ne 'Packages') ) {
204 die "E: Unknown type $type. Exiting.\n";
213 print "W: Forcing arch $arch for this command only.\n";
214 $opts .= " -o Apt::Architecture=$arch";
220 # Build APT_CONFIG override
222 return "APT_CONFIG=$datadir/$dist/etc/apt/apt.conf";
229 my ($dist, @args) = @_;
231 my $args = aptopts($dist) . " @args";
232 my $aptconfig = aptconfig($dist);
233 system("$aptconfig /usr/bin/apt-cache $args");
238 my ($dist, @args) = @_;
240 my $args = aptopts($dist) . " @args";
241 my $aptconfig = aptconfig($dist);
242 system("$aptconfig /usr/bin/apt-get $args");
246 # Run apt-rdepends cmd
247 my ($dist, @args) = @_;
249 my $args = aptopts($dist) . " @args";
250 my $aptconfig = aptconfig($dist);
251 system("$aptconfig /usr/bin/apt-rdepends $args");
255 my ($dist, $pkg) = @_;
258 die "E: no package name provided. Exiting.\n";
260 my $args = aptopts($dist) . " show $pkg";
261 my $aptconfig = aptconfig($dist);
262 my $source = `$aptconfig /usr/bin/apt-cache $args|grep '^Source:'`;
263 exit($?) if($? != 0);
264 $source =~ s/Source: (.*)/$1/;
265 print $pkg if($source eq '');
266 print $source if($source ne '');
270 my ($dist, $pkg) = @_;
273 die "E: no package name provided. Exiting.\n";
275 my $args = aptopts($dist) . " showsrc $pkg";
276 my $bins = `/usr/bin/apt-cache $args|sed -n '/^Package: $pkg/{N;p}' | sed -n 's/^Binary: \\(.*\\)/\\1/p'`;
277 exit($?) if ($? != 0);
278 my @bins = split /, /, $bins;
279 print join "\n", @bins;
285 my @temp = split /\//, $dir;
287 foreach my $piece (@temp) {
288 $createdir .= "/$piece";
289 if (! -d $createdir) {
296 my ($dist, $method, $version, @sections) = @_;
297 my $dir = $datadir . '/' . $dist;
299 die "E: you must provide a dist name.\n";
302 die "E: $dir already exists, exiting.\n";
308 foreach my $d (('/etc/apt', '/var/lib/apt/lists/partial', '/var/lib/dpkg', '/var/cache/apt/archives/partial')) {
309 recurs_mkdir("$dir/$d");
312 # Create sources.list
313 open(FH, ">$dir/etc/apt/sources.list");
315 # Use provided method, version and sections
316 my $sections_str = join(' ', @sections);
318 deb $method $version $sections_str
319 deb-src $method $version $sections_str
323 warn "W: method provided without a section. Using default content for sources.list\n";
325 # Fill in sources.list with example contents
327 #deb http://ftp.debian.org/debian/ unstable main contrib non-free
328 #deb-src http://ftp.debian.org/debian/ unstable main contrib non-free
330 #deb http://archive.ubuntu.com/ubuntu dapper main restricted
331 #deb http://archive.ubuntu.com/ubuntu dapper universe multiverse
332 #deb-src http://archive.ubuntu.com/ubuntu dapper main restricted
333 #deb-src http://archive.ubuntu.com/ubuntu dapper universe multiverse
338 open(FH, ">$dir/var/lib/dpkg/status");
339 close FH; #empty file
341 $arch ||= `dpkg --print-architecture`;
343 open(FH, ">$dir/etc/apt/apt.conf");
346 Architecture "$arch";
350 Dir::State::status "$dir/var/lib/dpkg/status";
353 print "Now edit $dir/etc/apt/sources.list\n";
354 print "Then run chdist apt-get $dist update\n";
355 print "And enjoy.\n";
361 # Retrieve files to be read
362 # Takes a dist and a type
363 my ($dist, $type) = @_;
365 # Let the above function check the type
370 foreach my $file ( glob($datadir . '/' . $dist . "/var/lib/apt/lists/*_$type") ) {
380 sub dist_compare(\@;$;$) {
381 # Takes a list of dists, a type of comparison and a do_compare flag
382 my ($dists, $do_compare, $type) = @_;
383 # Type is 'Sources' by default
387 $do_compare = 0 if $do_compare eq 'false';
389 # Get the list of dists from the referrence
391 map { dist_check($_) } @dists;
396 foreach my $dist (@dists) {
397 my $files = get_distfiles($dist,$type);
399 foreach my $file ( @files ) {
400 my $parsed_file = parseFile($file);
401 foreach my $package ( keys(%{$parsed_file}) ) {
402 if ( $packages{$dist}{$package} ) {
403 warn "W: Package $package is already listed for $dist. Not overriding.\n";
405 $packages{$dist}{$package} = $parsed_file->{$package};
411 # Get entire list of packages
412 my @all_packages = uniq sort ( map { keys(%{$packages{$_}}) } @dists );
414 foreach my $package (@all_packages) {
415 my $line = "$package ";
419 foreach my $dist (@dists) {
420 if ( $packages{$dist}{$package} ) {
421 $line .= "$packages{$dist}{$package}{'Version'} ";
424 $status = "not_in_$dist";
428 my @versions = map { $packages{$_}{$package}{'Version'} } @dists;
430 my @esc_vers = @versions;
431 foreach my $vers (@esc_vers) {
438 die "E: Can only compare versions if there are two distros.\n";
441 my $cmp = version_compare($versions[0], $versions[1]);
443 $status = "same_version";
445 $status = "newer_in_$dists[1]";
446 if ( $versions[1] =~ m|^$esc_vers[0]| ) {
447 $details = " local_changes_in_$dists[1]";
450 $status = "newer_in_$dists[0]";
451 if ( $versions[0] =~ m|^$esc_vers[1]| ) {
452 $details = " local_changes_in_$dists[0]";
456 $line .= " $status $details";
464 sub compare_src_bin {
465 my ($dist, $do_compare) = @_;
467 $do_compare = 0 if $do_compare eq 'false';
474 my @parse_types = ('Sources', 'Packages');
475 my @comp_types = ('Sources_Bin', 'Packages');
477 foreach my $type (@parse_types) {
478 my $files = get_distfiles($dist, $type);
480 foreach my $file ( @files ) {
481 my $parsed_file = parseFile($file);
482 foreach my $package ( keys(%{$parsed_file}) ) {
483 if ( $packages{$dist}{$package} ) {
484 warn "W: Package $package is already listed for $dist. Not overriding.\n";
486 $packages{$type}{$package} = $parsed_file->{$package};
492 # Build 'Sources_Bin' hash
493 foreach my $package ( keys( %{$packages{Sources}} ) ) {
494 my $package_h = \%{$packages{Sources}{$package}};
495 if ( $package_h->{'Binary'} ) {
496 my @binaries = split(", ", $package_h->{'Binary'});
497 my $version = $package_h->{'Version'};
498 foreach my $binary (@binaries) {
499 if ( $packages{Sources_Bin}{$binary} ) {
500 # TODO: replace if new version is newer (use dpkg --compare-version?)
501 warn "There is already a version for binary $binary. Not replacing.\n";
503 $packages{Sources_Bin}{$binary}{Version} = $version;
507 warn "Source $package has no binaries!\n";
511 # Get entire list of packages
512 my @all_packages = uniq sort ( map { keys(%{$packages{$_}}) } @comp_types );
514 foreach my $package (@all_packages) {
515 my $line = "$package ";
519 foreach my $type (@comp_types) {
520 if ( $packages{$type}{$package} ) {
521 $line .= "$packages{$type}{$package}{'Version'} ";
524 $status = "not_in_$type";
528 my @versions = map { $packages{$_}{$package}{'Version'} } @comp_types;
530 my @esc_vers = @versions;
531 foreach my $vers (@esc_vers) {
537 if ($#comp_types != 1) {
538 die "E: Can only compare versions if there are two types.\n";
541 my $cmp = version_compare($versions[0], $versions[1]);
543 $status = "same_version";
545 $status = "newer_in_$comp_types[1]";
546 if ( $versions[1] =~ m|^$esc_vers[0]| ) {
547 $details = " local_changes_in_$comp_types[1]";
550 $status = "newer_in_$comp_types[0]";
551 if ( $versions[0] =~ m|^$esc_vers[1]| ) {
552 $details = " local_changes_in_$comp_types[0]";
556 $line .= " $status $details";
564 my (@argv, $file) = @_;
565 my $dist = shift @argv;
567 my $f = glob($datadir . '/' . $dist . "/var/lib/apt/lists/*_$file");
568 # FIXME avoid shell invoc, potential quoting problems here
569 system("cat $f | grep-dctrl @argv");
573 opendir(DIR, $datadir) or die "can't open dir $datadir: $!";
574 while (my $file = readdir(DIR)) {
575 if ( (-d "$datadir/$file") && ($file =~ m|^\w+|) ) {
587 # Parse a source file and returns results as a hash
589 open(FILE, "$file") || die("Could not open $file : $!\n");
591 # Use %tmp hash to store tmp data
595 while (my $line = <FILE>) {
596 if ( $line =~ m|^$| ) {
597 # Commit data if empty line
598 if ( $tmp{'Package'} ) {
599 #print "Committing data for $tmp{'Package'}\n";
600 while ( my ($field, $data) = each(%tmp) ) {
601 if ( $field ne "Package" ) {
602 $result{$tmp{'Package'}}{$field} = $data;
608 warn "W: No Package field found. Not committing data.\n";
610 } elsif ( $line =~ m|^[a-zA-Z]| ) {
612 my ($field, $data) = $line =~ m|([a-zA-z-]+): (.*)$|;
614 $tmp{$field} = $data;
626 ########################################################
628 ########################################################
630 my $command = shift @ARGV;
631 if ($command eq 'create') {
634 elsif ($command eq 'apt-get') {
637 elsif ($command eq 'apt-cache') {
640 elsif ($command eq 'apt-rdepends') {
643 elsif ($command eq 'bin2src') {
646 elsif ($command eq 'src2bin') {
649 elsif ($command eq 'compare-packages') {
650 dist_compare(@ARGV, 0, 'Sources');
652 elsif ($command eq 'compare-bin-packages') {
653 dist_compare(@ARGV, 0, 'Packages');
655 elsif ($command eq 'compare-versions') {
656 dist_compare(@ARGV, 1, 'Sources');
658 elsif ($command eq 'compare-bin-versions') {
659 dist_compare(@ARGV, 1, 'Packages');
661 elsif ($command eq 'grep-dctrl-packages') {
662 grep_file(@ARGV, 'Packages');
664 elsif ($command eq 'grep-dctrl-sources') {
665 grep_file(@ARGV, 'Sources');
667 elsif ($command eq 'compare-src-bin-packages') {
668 compare_src_bin(@ARGV, 0);
670 elsif ($command eq 'compare-src-bin-versions') {
671 compare_src_bin(@ARGV, 1);
673 elsif ($command eq 'list') {
677 die "Command unknown. Try $0 -h\n";