summaryrefslogtreecommitdiffstats
path: root/scripts/debsnap.pl
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/debsnap.pl')
-rwxr-xr-xscripts/debsnap.pl423
1 files changed, 423 insertions, 0 deletions
diff --git a/scripts/debsnap.pl b/scripts/debsnap.pl
new file mode 100755
index 0000000..479e80c
--- /dev/null
+++ b/scripts/debsnap.pl
@@ -0,0 +1,423 @@
+#!/usr/bin/perl
+# vim: set ai shiftwidth=4 tabstop=4 expandtab:
+
+# Copyright © 2010, David Paleino <d.paleino@gmail.com>,
+#
+# 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 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 <https://www.gnu.org/licenses/>.
+
+use strict;
+use warnings;
+
+use Getopt::Long qw(:config bundling permute no_getopt_compat);
+use File::Basename;
+use Cwd qw/cwd abs_path/;
+use File::Path qw/make_path/;
+use Dpkg::Version;
+use JSON::PP;
+
+my $progname = basename($0);
+
+eval {
+ require LWP::Simple;
+ require LWP::UserAgent;
+ no warnings;
+ $LWP::Simple::ua = LWP::UserAgent->new(
+ agent => 'LWP::UserAgent/Devscripts/###VERSION###');
+ $LWP::Simple::ua->env_proxy();
+};
+if ($@) {
+ if ($@ =~ m/Can\'t locate LWP/) {
+ die
+ "$progname: Unable to run: the libwww-perl package is not installed";
+ } else {
+ die "$progname: Unable to run: Couldn't load LWP::Simple: $@";
+ }
+}
+
+my $modified_conf_msg = '';
+my %config_vars = ();
+
+my %opt = (architecture => []);
+my $package = '';
+my $pkgversion;
+my $firstversion;
+my $lastversion;
+my $warnings = 0;
+
+sub fatal($);
+sub verbose($);
+
+sub version {
+ print <<"EOF";
+This is $progname, from the Debian devscripts package, version ###VERSION###
+This code is copyright 2010 by David Paleino <dapal\@debian.org>.
+This program comes with ABSOLUTELY NO WARRANTY.
+You are free to redistribute this code under the terms of the GNU
+General Public License v3 or, at your option, any later version.
+EOF
+ exit 0;
+}
+
+sub usage {
+ my $rc = shift;
+ print <<"EOF";
+$progname [options] <package name> [package version]
+
+Automatically downloads packages from snapshot.debian.org
+
+The following options are supported:
+ -h, --help Shows this help message
+ --version Shows information about version
+ -v, --verbose Be verbose
+ -d <destination directory>,
+ --destdir=<destination directory> Directory for retrieved packages
+ Default is ./source-<package name>
+ -f, --force Force overwriting an existing
+ destdir
+ -l, --list Don't download but just list versions
+ --binary Download binary packages instead of
+ source packages
+ -a <architecture>,
+ --architecture <architecture> Specify architecture of binary packages,
+ implies --binary. May be given multiple
+ times
+
+Default settings modified by devscripts configuration files or command-line
+options:
+$modified_conf_msg
+EOF
+ exit $rc;
+}
+
+sub fetch_json_page {
+ my ($json_url) = @_;
+
+ # download the json page:
+ verbose "Getting json $json_url\n";
+ my $content = LWP::Simple::get($json_url);
+ return unless defined $content;
+ my $json = JSON::PP->new();
+
+ # these are some nice json options to relax restrictions a bit:
+ my $json_text = $json->allow_nonref->utf8->relaxed->decode($content);
+
+ return $json_text;
+}
+
+sub read_conf {
+ my @config_files = ('/etc/devscripts.conf', '~/.devscripts');
+ %config_vars = (
+ 'DEBSNAP_VERBOSE' => 'no',
+ 'DEBSNAP_DESTDIR' => '',
+ 'DEBSNAP_BASE_URL' => 'https://snapshot.debian.org',
+ );
+
+ my %config_default = %config_vars;
+
+ my $shell_cmd;
+ # Set defaults
+ $shell_cmd .= qq[unset `set | grep "^DEBSNAP_" | cut -d= -f1`;\n];
+ foreach my $var (keys %config_vars) {
+ $shell_cmd .= qq[$var="$config_vars{$var}";\n];
+ }
+ $shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n";
+ $shell_cmd .= '[ -f $file ] && . $file; done;' . "\n";
+ # Read back values
+ foreach my $var (keys %config_vars) { $shell_cmd .= "echo \$$var;\n" }
+ my $shell_out = `/bin/bash -c '$shell_cmd'`;
+ @config_vars{ keys %config_vars } = split /\n/, $shell_out, -1;
+
+ # Check validity
+ $config_vars{'DEBSNAP_VERBOSE'} =~ /^(yes|no)$/
+ or $config_vars{'DEBSNAP_VERBOSE'} = 'no';
+
+ foreach my $var (sort keys %config_vars) {
+ if ($config_vars{$var} ne $config_default{$var}) {
+ $modified_conf_msg .= " $var=$config_vars{$var}\n";
+ }
+ }
+
+ $modified_conf_msg ||= " (none)\n";
+ chomp $modified_conf_msg;
+
+ $opt{verbose} = $config_vars{DEBSNAP_VERBOSE} eq 'yes';
+ $opt{destdir} = $config_vars{DEBSNAP_DESTDIR};
+ $opt{baseurl} = $config_vars{DEBSNAP_BASE_URL};
+}
+
+sub have_file($$) {
+ my ($path, $hash) = @_;
+
+ if (-e $path) {
+ open(HASH, '-|', 'sha1sum', $path) || fatal "Can't run sha1sum: $!";
+ while (<HASH>) {
+ if (m/^([a-fA-F\d]{40}) /) {
+ close(HASH) || fatal "sha1sum problems: $! $?";
+ return $1 eq $hash;
+ }
+ }
+ }
+ return 0;
+}
+
+sub fatal($) {
+ my ($pack, $file, $line);
+ ($pack, $file, $line) = caller();
+ (my $msg = "$progname: fatal error at line $line:\n@_\n") =~ tr/\0//d;
+ $msg =~ s/\n\n$/\n/;
+ $! = 1;
+ die $msg;
+}
+
+sub verbose($) {
+ (my $msg = "@_\n") =~ tr/\0//d;
+ $msg =~ s/\n\n$/\n/;
+ print "$msg" if $opt{verbose};
+}
+
+sub keep_version($) {
+ my $version = shift;
+ if (defined $pkgversion) {
+ return version_compare_relation($pkgversion, REL_EQ, $version);
+ }
+ if (defined $firstversion) {
+ if ($firstversion > $version) {
+ verbose "skip version $version: older than first";
+ return 0;
+ }
+ }
+ if (defined $lastversion) {
+ if ($lastversion < $version) {
+ verbose "skip version $version: newer than last";
+ return 0;
+ }
+ }
+ return 1;
+}
+
+###
+# Main program
+###
+read_conf(@ARGV);
+Getopt::Long::Configure('gnu_compat');
+Getopt::Long::Configure('no_ignore_case');
+GetOptions(
+ \%opt, 'verbose|v', 'destdir|d=s', 'force|f',
+ 'help|h', 'version', 'first=s', 'last=s',
+ 'list|l', 'binary', 'architecture|a=s@'
+) || usage(1);
+
+usage(0) if $opt{help};
+version() if $opt{version};
+usage(1) unless @ARGV;
+$package = shift;
+if (@ARGV) {
+ my $version = shift;
+ $pkgversion = Dpkg::Version->new($version);
+ fatal "Invalid version '$version'" unless $pkgversion->is_valid();
+}
+
+if (defined $opt{first}) {
+ $firstversion = Dpkg::Version->new($opt{first});
+ fatal "Invalid version '$opt{first}'" unless $firstversion->is_valid();
+}
+
+if (defined $opt{last}) {
+ $lastversion = Dpkg::Version->new($opt{last});
+ fatal "Invalid version '$opt{last}'" unless $lastversion->is_valid();
+}
+
+$package eq '' && usage(1);
+
+$opt{binary} ||= @{ $opt{architecture} };
+
+my $baseurl;
+if ($opt{binary}) {
+ $opt{destdir} ||= "binary-$package";
+ $baseurl = "$opt{baseurl}/mr/binary/$package/";
+} else {
+ $opt{destdir} ||= "source-$package";
+ $baseurl = "$opt{baseurl}/mr/package/$package/";
+}
+
+my $mkdir_done = 0;
+my $mkDestDir = sub {
+ unless ($mkdir_done) {
+ if (-d $opt{destdir}) {
+ unless ($opt{force} || cwd() eq abs_path($opt{destdir})) {
+ fatal
+"Destination dir $opt{destdir} already exists.\nPlease (re)move it first, or use --force to overwrite.";
+ }
+ }
+
+ make_path($opt{destdir});
+ $mkdir_done = 1;
+ }
+};
+
+my $json_text = fetch_json_page($baseurl);
+unless ($json_text && @{ $json_text->{result} }) {
+ fatal "Unable to retrieve information for $package from $baseurl.";
+}
+
+my @versions = @{ $json_text->{result} };
+@versions
+ = $opt{binary}
+ ? grep { keep_version($_->{binary_version}) } @versions
+ : grep { keep_version($_->{version}) } @versions;
+unless (@versions) {
+ warn "$progname: No matching versions found for $package\n";
+ $warnings++;
+}
+if ($opt{list}) {
+ foreach my $version (@versions) {
+ if ($opt{binary}) {
+ print "$version->{binary_version}\n";
+ } else {
+ print "$version->{version}\n";
+ }
+ }
+} elsif ($opt{binary}) {
+ foreach my $version (@versions) {
+ my $src_json
+ = fetch_json_page(
+"$opt{baseurl}/mr/package/$version->{source}/$version->{version}/binfiles/$version->{name}/$version->{binary_version}?fileinfo=1"
+ );
+
+ unless ($src_json) {
+ warn
+"$progname: No binary packages found for $package version $version->{binary_version}\n";
+ $warnings++;
+ next;
+ }
+
+ my @results = @{ $src_json->{result} };
+ if (@{ $opt{architecture} }) {
+ my %archs = map { ($_ => 0) } @{ $opt{architecture} };
+ @results = grep {
+ exists $archs{ $_->{architecture} }
+ && ++$archs{ $_->{architecture} }
+ } @results;
+ my @missing = grep { $archs{$_} == 0 } sort keys %archs;
+ if (@missing) {
+ warn
+"$progname: No binary packages found for $package version $version->{binary_version} on "
+ . join(', ', @missing) . "\n";
+ $warnings++;
+ }
+ }
+ foreach my $result (@results) {
+ my $hash = $result->{hash};
+ my $fileinfo = @{ $src_json->{fileinfo}{$hash} }[0];
+ my $file_url = "$opt{baseurl}/file/$hash";
+ my $file_name = basename($fileinfo->{name});
+ if (!have_file("$opt{destdir}/$file_name", $hash)) {
+ verbose "Getting file $file_name: $file_url";
+ $mkDestDir->();
+ LWP::Simple::mirror($file_url, "$opt{destdir}/$file_name");
+ }
+ }
+ }
+} else {
+ foreach my $version (@versions) {
+ my $src_json
+ = fetch_json_page("$baseurl$version->{version}/srcfiles?fileinfo=1");
+ unless ($src_json) {
+ warn
+"$progname: No source files found for $package version $version->{version}\n";
+ $warnings++;
+ next;
+ }
+
+ # Get the dsc file and parse it to get the list of files to be
+ # restored (this should fix most issues with multi-tarball
+ # source packages):
+ my $dsc_name;
+ my $dsc_hash;
+ foreach my $hash (keys %{ $src_json->{fileinfo} }) {
+ my $fileinfo = $src_json->{fileinfo}{$hash};
+ foreach my $info (@$fileinfo) {
+ if ($info->{name} =~ m/^\Q${package}\E_.*\.dsc/) {
+ $dsc_name = $info->{name};
+ $dsc_hash = $hash;
+ last;
+ }
+ }
+ last if $dsc_name;
+ }
+ unless ($dsc_name) {
+ warn
+"$progname: No dsc file detected for $package version $version->{version}\n";
+ $warnings++;
+ next;
+ }
+
+ # Retrieve the dsc file:
+ my $file_url = "$opt{baseurl}/file/$dsc_hash";
+ if (!have_file("$opt{destdir}/$dsc_name", $dsc_hash)) {
+ verbose "Getting dsc file $dsc_name: $file_url";
+ $mkDestDir->();
+ LWP::Simple::mirror($file_url, "$opt{destdir}/$dsc_name");
+ }
+
+ # Get the list of files from the dsc:
+ my @files;
+ open my $fh, '<', "$opt{destdir}/$dsc_name"
+ or die "unable to open the dsc file $opt{destdir}/$dsc_name";
+ while (<$fh> !~ /^Files:/) { }
+ while (<$fh> =~ /^ (\S+) (\d+) (\S+)$/) {
+ my ($checksum, $size, $file) = ($1, $2, $3);
+ push @files, $file;
+ }
+ close $fh
+ or die "unable to close the dsc file";
+
+ # Iterate over files and find the right contents:
+ foreach my $file_name (@files) {
+ my $file_hash;
+ foreach my $hash (keys %{ $src_json->{fileinfo} }) {
+ my $fileinfo = $src_json->{fileinfo}{$hash};
+
+ foreach my $info (@{$fileinfo}) {
+ if ($info->{name} eq $file_name) {
+ $file_hash = $hash;
+ last;
+ }
+ }
+ last if $file_hash;
+ }
+ unless ($file_hash) {
+ # Warning: this next statement will only move to the
+ # next files, not the next package
+ print
+"$progname: No hash found for file $file_name needed by $package version $version->{version}\n";
+ $warnings++;
+ next;
+ }
+
+ my $file_url = "$opt{baseurl}/file/$file_hash";
+ $file_name = basename($file_name);
+ if (!have_file("$opt{destdir}/$file_name", $file_hash)) {
+ verbose "Getting file $file_name: $file_url";
+ $mkDestDir->();
+ LWP::Simple::mirror($file_url, "$opt{destdir}/$file_name");
+ }
+ }
+ }
+}
+
+if ($warnings) {
+ exit 2;
+}
+exit 0;