+
+# option --all and packages specified at the same time?
+if (($check_everything or $packages_file) and $#ARGV+1 > 0) {
+ print STDERR "warning: options -a or -p can't be mixed with package parameters!\n";
+ print STDERR "(will ignore -a or -p option)\n";
+ undef $check_everything;
+ undef $packages_file;
+}
+
+# check permitted values for --color
+if ($color and $color !~ /^(never|always|auto|html)$/) {
+ die "invalid argument to --color: $color\n";
+}
+
+# check specified action
+$action = 'check' unless $action;
+
+# check for arguments
+if ($action =~ /^(check|unpack|remove)$/ and $#ARGV == -1 and not $check_everything and not $packages_file) {
+ syntax();
+}
+
+# }}}
+
+# {{{ Setup Configuration
+#
+# root permissions?
+# check if effective UID is 0
+if ($> == 0 and not $allow_root) {
+ print STDERR "warning: maemian's authors do not recommend running it with root privileges!\n";
+}
+
+# search for configuration file if it was not set with --cfg
+# do not search the default locations if it was set.
+if ($MAEMIAN_CFG) {
+} elsif (exists $ENV{'MAEMIAN_CFG'} &&
+ -f ($MAEMIAN_CFG = $ENV{'MAEMIAN_CFG'})) {
+} elsif (-f ($MAEMIAN_CFG = $MAEMIAN_ROOT . '/maemianrc')) {
+} elsif (exists $ENV{'HOME'} &&
+ -f ($MAEMIAN_CFG = $ENV{'HOME'} . '/.maemianrc')) {
+} elsif (-f ($MAEMIAN_CFG = '/etc/maemianrc')) {
+} else {
+ undef $MAEMIAN_CFG;
+}
+
+use constant VARS => qw(LAB ARCHIVEDIR DIST UNPACK_LEVEL SECTION AREA ARCH);
+# read configuration file
+if ($MAEMIAN_CFG) {
+ open(CFG, '<', $MAEMIAN_CFG)
+ or die("cannot open configuration file $MAEMIAN_CFG for reading: $!");
+ while (<CFG>) {
+ chop;
+ s/\#.*$//go;
+ s/\"//go;
+ next if m/^\s*$/o;
+
+ # substitute some special variables
+ s,\$HOME/,$ENV{'HOME'}/,go;
+ s,\~/,$ENV{'HOME'}/,go;
+
+ my $found = 0;
+ foreach my $var (VARS) {
+ no strict 'refs';
+ $var = "MAEMIAN_$var";
+ if (m/^\s*$var\s*=\s*(.*\S)\s*$/i) {
+ $$var = $1;
+ $found = 1;
+ last;
+ }
+ }
+ unless ($found) {
+ die "syntax error in configuration file: $_\n";
+ }
+ }
+ close(CFG);
+}
+
+# environment variables overwrite settings in conf file:
+foreach (VARS) {
+ no strict 'refs';
+ my $var = "MAEMIAN_$_";
+ my $opt_var = "OPT_$var";
+ $$var = $ENV{$var} if $ENV{$var};
+ $$var = $$opt_var if $$opt_var;
+}
+
+# MAEMIAN_SECTION is deprecated in favour of MAEMIAN_AREA
+if (defined $MAEMIAN_SECTION) {
+ print STDERR "warning: MAEMIAN_SECTION has been deprecated in favour of MAEMIAN_AREA.\n";
+ if (defined $MAEMIAN_AREA) {
+ print STDERR "Using MAEMIAN_AREA as both were defined.\n";
+ } else {
+ print STDERR "Both are currently accepted, but MAEMIAN_SECTION may be removed\n";
+ print STDERR "in a future Maemian release.\n";
+ $MAEMIAN_AREA = $MAEMIAN_SECTION;
+ }
+}
+
+# determine requested unpack level
+if (defined($unpack_level)) {
+ # specified through command line
+} elsif (defined($MAEMIAN_UNPACK_LEVEL)) {
+ # specified via configuration file or env variable
+ $unpack_level = $MAEMIAN_UNPACK_LEVEL;
+} else {
+ # determine by action
+ if (($action eq 'unpack') or ($action eq 'check')) {
+ $unpack_level = 1;
+ } else {
+ $unpack_level = 0;
+ }
+}
+unless (($unpack_level == 0) or ($unpack_level == 1) or ($unpack_level == 2)) {
+ die("bad unpack level $unpack_level specified");
+}
+
+$MAEMIAN_UNPACK_LEVEL = $unpack_level;
+
+# export current settings for our helper scripts
+foreach (('ROOT', 'CFG', VARS)) {
+ no strict 'refs';
+ my $var = "MAEMIAN_$_";
+ if ($$var) {
+ $ENV{$var} = $$var;
+ } else {
+ $ENV{$var} = "";
+ $$var = "";
+ }
+}
+
+my $debug = $#debug + 1;
+$verbose = 1 if $debug;
+$ENV{'MAEMIAN_DEBUG'} = $debug;
+
+# Loading maeian's own libraries (now that MAEMIAN_ROOT is known)
+unshift @INC, "$MAEMIAN_ROOT/lib";
+
+require Lab;
+
+require Util;
+require Read_pkglists;
+
+import Util;
+
+require Tags;
+import Tags;
+
+require Maemian::Data;
+require Maemian::Schedule;
+require Maemian::Output;
+import Maemian::Output qw(:messages);
+require Maemian::Command;
+import Maemian::Command qw(spawn reap);
+require Maemian::Check;
+import Maemian::Check qw(check_maintainer);
+
+no warnings 'once';
+if (defined $experimental_output_opts) {
+ my %opts = map { split(/=/) } split( /,/, $experimental_output_opts );
+ foreach (keys %opts) {
+ if ($_ eq 'format') {
+ if ($opts{$_} eq 'colons') {
+ require Maemian::Output::ColonSeparated;
+ $Maemian::Output::GLOBAL = new Maemian::Output::ColonSeparated;
+ } elsif ($opts{$_} eq 'letterqualifier') {
+ require Maemian::Output::LetterQualifier;
+ $Maemian::Output::GLOBAL = new Maemian::Output::LetterQualifier;
+ } elsif ($opts{$_} eq 'xml') {
+ require Maemian::Output::XML;
+ $Maemian::Output::GLOBAL = new Maemian::Output::XML;
+ }
+ }
+ no strict 'refs';
+ ${"Tags::$_"} = $opts{$_};
+ }
+}
+
+$Maemian::Output::GLOBAL->verbose($verbose);
+$Maemian::Output::GLOBAL->debug($debug);
+$Maemian::Output::GLOBAL->quiet($quiet);
+$Maemian::Output::GLOBAL->color($color);
+$Maemian::Output::GLOBAL->showdescription($maemian_info);
+
+# Print Debug banner, now that we're finished determining
+# the values and have Maemian::Output available
+debug_msg(1,
+ $BANNER,
+ "Maemian root directory: $MAEMIAN_ROOT",
+ "Configuration file: $MAEMIAN_CFG",
+ "Laboratory: $MAEMIAN_LAB",
+ "Archive directory: $MAEMIAN_ARCHIVEDIR",
+ "Distribution: $MAEMIAN_DIST",
+ "Default unpack level: $MAEMIAN_UNPACK_LEVEL",
+ "Architecture: $MAEMIAN_ARCH",
+ delimiter(),
+ );
+
+my @l_secs = read_dpkg_control("$MAEMIAN_ROOT/checks/maemian.desc");
+shift(@l_secs);
+map { $_->{'script'} = 'maemian'; Tags::add_tag($_) } @l_secs;
+
+$Tags::show_experimental = $display_experimentaltags;
+$Tags::show_pedantic = $display_pedantictags;
+$Tags::show_overrides = $show_overrides;
+%Tags::display_level = %display_level;
+%Tags::display_source = %display_source;
+%Tags::only_issue_tags = map { $_ => 1 } (split(/,/, $check_tags))
+
+ if defined $check_tags;
+use warnings;
+use vars qw(%source_info %binary_info %udeb_info); # from the above
+
+# Set up clean-up handlers.
+undef $cleanup_filename;
+$SIG{'INT'} = \&interrupted;
+$SIG{'QUIT'} = \&interrupted;
+
+# }}}
+
+# {{{ Create/Maintain Lab and add any specified Debian Archives (*.debs)
+
+$LAB = new Lab( $MAEMIAN_LAB, $MAEMIAN_DIST );
+
+#######################################
+# Process -S option
+if ($action eq 'setup-lab') {
+ if ($#ARGV+1 > 0) { # Cannot define Lab on the command line.
+ warning("ignoring additional command line arguments");
+ }
+
+ $LAB->setup_static()
+ or fail("There was an error while setting up the static lab.");
+
+ exit 0;
+
+#######################################
+# Process -R option
+} elsif ($action eq 'remove-lab') {
+ if ($#ARGV+1 > 0) {
+ warning("ignoring additional command line arguments");
+ }
+
+ $LAB->delete_static()
+ or fail("There was an error while removing the static lab.");
+
+ exit 0;
+
+#######################################
+# Check for non deb specific actions
+} elsif (not (($action eq 'unpack') or ($action eq 'check')
+ or ($action eq 'remove'))) {
+ fail("bad action $action specified");
+}
+
+# sanity check:
+fail("maemian lab has not been set up correctly (perhaps you forgot to run maemian --setup-lab?)")
+ unless $LAB->is_lab();
+
+#XXX: There has to be a cleaner way to do this
+$MAEMIAN_LAB = $LAB->{dir};
+
+# }}}
+
+# {{{ Compile list of files to process
+
+$schedule = new Maemian::Schedule(verbose => $verbose);
+# process package/file arguments
+while (my $arg = shift) {
+ # file?
+ if (-f $arg) {
+ # $arg contains absolute dir spec?
+ unless ($arg =~ m,^/,) {
+ $arg = "$cwd/$arg";
+ }
+
+ # .deb file?
+ if ($arg =~ /\.deb$/) {
+ $schedule->add_deb('b', $arg)
+ or warning("$arg is a zero-byte file, skipping");
+ }
+ # .udeb file?
+ elsif ($arg =~ /\.udeb$/) {
+ $schedule->add_deb('u', $arg)
+ or warning("$arg is a zero-byte file, skipping");
+ }
+ # .dsc file?
+ elsif ($arg =~ /\.dsc$/) {
+ $schedule->add_dsc($arg)
+ or warning("$arg is a zero-byte file, skipping");
+ }
+ # .changes file?
+ elsif ($arg =~ /\.changes$/) {
+ # get directory and filename part of $arg
+ my ($arg_dir, $arg_name) = $arg =~ m,(.*)/([^/]+)$,;
+
+ v_msg("Processing changes file $arg_name ...");
+
+ my ($data) = read_dpkg_control($arg);
+ if (not defined $data) {
+ warning("$arg is a zero-byte file, skipping");
+ next;
+ }
+ Tags::set_pkg( $arg, $arg_name, "", "", 'binary' );
+
+ # If we don't have a Format key, something went seriously wrong.
+ # Tag the file and skip remaining processing.
+ if (!$data->{'format'}) {
+ tag('malformed-changes-file');
+ next;
+ }
+
+ # Description is mandated by dak, but only makes sense if binary
+ # packages are included. Don't tag pure source uploads.
+ if (!$data->{'description'} && $data->{'architecture'} ne 'source') {
+ tag("no-description-in-changes-file");
+ }
+
+ # check distribution field
+ if (defined $data->{distribution}) {
+ my $ubuntu_dists = Maemian::Data->new ('changelog-file/ubuntu-dists');
+ my $ubuntu_regex = join('|', $ubuntu_dists->all);
+ my @distributions = split /\s+/o, $data->{distribution};
+ for my $distribution (@distributions) {
+ if ($distribution eq 'UNRELEASED') {
+ # ignore
+ } elsif ($data->{version} =~ /ubuntu|$ubuntu_regex/
+ or $distribution =~ /$ubuntu_regex/) {
+ if ($distribution !~ /^($ubuntu_regex)(-(proposed|updates|backports|security))?$/ ) {
+ tag("bad-ubuntu-distribution-in-changes-file",
+ $distribution);
+ }
+ } elsif (! (($distribution eq 'oldstable')
+ or ($distribution eq 'stable')
+ or ($distribution eq 'testing')
+ or ($distribution eq 'unstable')
+ or ($distribution eq 'experimental')
+ or ($distribution =~ /^\w+-backports$/)
+ or ($distribution =~ /^\w+-proposed-updates$/)
+ or ($distribution =~ /^\w+-security$/))
+ ) {
+ # bad distribution entry
+ tag("bad-distribution-in-changes-file",
+ $distribution);
+ }
+ }
+
+ if ($#distributions > 0) {
+ # Currently disabled until dak stops accepting the syntax
+ # tag("multiple-distributions-in-changes-file",
+ # $data->{'distribution'});
+ }
+ }
+
+ # Urgency is only recommended by Policy.
+ if (!$data->{'urgency'}) {
+ tag("no-urgency-in-changes-file");
+ } else {
+ my $urgency = lc $data->{'urgency'};
+ $urgency =~ s/ .*//;
+ unless ($urgency =~ /^(low|medium|high|critical|emergency)$/i) {
+ tag("bad-urgency-in-changes-file", $data->{'urgency'});
+ }
+ }
+
+ # Changed-By is optional in Policy, but if set, must be
+ # syntactically correct. It's also used by dak.
+ if ($data->{'changed-by'}) {
+ check_maintainer($data->{'changed-by'}, 'changed-by');
+ }
+
+ # process all listed `files:'
+ my %files;
+
+ my $file_list = $data->{files} || '';
+ for ( split /\n/, $file_list ) {
+ chomp;
+ s/^\s+//o;
+ next if $_ eq '';
+
+ my ($md5sum,$size,$section,$priority,$file) = split(/\s+/o, $_);
+ $files{$file}{md5} = $md5sum;
+ $files{$file}{size} = $size;
+
+ # check section
+ if (($section eq 'non-free') or ($section eq 'contrib')) {
+ tag( "bad-section-in-changes-file", $file, $section );
+ }
+
+ }
+
+ foreach my $alg (qw(sha1 sha256)) {
+ my $list = $data->{"checksums-$alg"} || '';
+ for ( split /\n/, $list ) {
+ chomp;
+ s/^\s+//o;
+ next if $_ eq '';
+
+ my ($checksum,$size,$file) = split(/\s+/o, $_);
+ $files{$file}{$alg} = $checksum;
+ if ($files{$file}{size} != $size) {
+ tag( "file-size-mismatch-in-changes-file", $file,
+ "$files{$file}{size} != $size" );
+ }
+ }
+ }
+
+
+ foreach my $file (keys %files) {
+ my $filename = $arg_dir . '/' . $file;
+
+ # check size
+ if (not -f $filename) {
+ warning("$file does not exist, exiting");
+ exit 2;
+ }
+ my $size = -s _;
+ if ($size ne $files{$file}{size}) {
+ tag( "file-size-mismatch-in-changes-file", $file,
+ "$files{$file}{size} != $size");
+ }
+
+ # check checksums
+ if ($check_checksums or $file =~ /\.dsc$/) {
+ foreach my $alg (qw(md5 sha1 sha256)) {
+ next unless exists $files{$file}{$alg};
+
+ my $real_checksum = get_file_checksum($alg, $filename);
+
+ if ($real_checksum ne $files{$file}{$alg}) {
+ tag( "checksum-mismatch-in-changes-file", $alg, $file );
+ }
+ }
+ }
+
+ # process file?
+ if ($file =~ /\.dsc$/) {
+ $schedule->add_dsc($filename);
+ } elsif ($file =~ /\.deb$/) {
+ $schedule->add_deb('b', $filename);
+ } elsif ($file =~ /\.udeb$/) {
+ $schedule->add_deb('u', $filename);
+ }
+ }
+
+ unless ($exit_code) {
+ my $stats = Tags::get_stats( $arg );
+ if ($stats->{types}{E}) {
+ $exit_code = 1;
+ } elsif ($fail_on_warnings && $stats->{types}{W}) {
+ $exit_code = 1;
+ }
+ }
+
+ } else {
+ fail("bad package file name $arg (neither .deb, .udeb or .dsc file)");
+ }
+ } else {
+ # parameter is a package name--so look it up
+ # search the distribution first, then the lab
+ # special case: search only in lab if action is `remove'
+
+ my $search;
+ if ($action eq 'remove') {
+ # search only in lab--see below
+ $search = 'lab';
+ } else {
+ # search in dist, then in lab
+ $search = 'dist or lab';
+
+ my $found = 0;
+
+ # read package info
+ read_src_list("$MAEMIAN_LAB/info/source-packages", 0);
+ read_bin_list("$MAEMIAN_LAB/info/binary-packages", 0);
+ read_udeb_list("$MAEMIAN_LAB/info/udeb-packages", 0);
+
+ if (($pkg_mode eq 'b') or ($pkg_mode eq 'a')) {
+ if ($binary_info{$arg}) {
+ $schedule->add_file('b', "$MAEMIAN_ARCHIVEDIR/$binary_info{$arg}->{'file'}",
+ %{$binary_info{$arg}});
+ $found = 1;
+ }
+ }
+ if (($pkg_mode eq 'u') or ($pkg_mode eq 'a')) {
+ if ($udeb_info{$arg}) {
+ $schedule->add_file('u', "$MAEMIAN_ARCHIVEDIR/$udeb_info{$arg}->{'file'}",
+ %{$udeb_info{$arg}});
+ $found = 1;
+ }
+ }
+ if (($pkg_mode eq 's') or ($pkg_mode eq 'a')) {
+ if ($source_info{$arg}) {
+ $schedule->add_file('s', "$MAEMIAN_ARCHIVEDIR/$source_info{$arg}->{'file'}",
+ %{$source_info{$arg}});
+ $found = 1;
+ }
+ }
+
+ next if $found;
+ }
+
+ # nothing found so far, so search the lab
+
+ my $b = "$MAEMIAN_LAB/binary/$arg";
+ my $s = "$MAEMIAN_LAB/source/$arg";
+ my $u = "$MAEMIAN_LAB/udeb/$arg";
+
+ if ($pkg_mode eq 'b') {
+ unless (-d $b) {
+ warn "error: cannot find binary package $arg in $search (skipping)\n";
+ $exit_code = 2;
+ next;
+ }
+ } elsif ($pkg_mode eq 's') {
+ unless (-d $s) {
+ warning("cannot find source package $arg in $search (skipping)");
+ $exit_code = 2;
+ next;
+ }
+ } elsif ($pkg_mode eq 'u') {
+ unless (-d $u) {
+ warning("cannot find udeb package $arg in $search (skipping)");
+ $exit_code = 2;
+ next;
+ }
+ } else {
+ # $pkg_mode eq 'a'
+ unless (-d $b or -d $s or -d $u) {
+ warning("cannot find binary, udeb or source package $arg in $search (skipping)");
+ $exit_code = 2;
+ next;
+ }
+ }
+
+ if (($pkg_mode eq 'b') or (($pkg_mode eq 'a') and (-d $b))) {
+ $schedule->add_file('b', get_bin_info_from_lab($b));
+ }
+ if (($pkg_mode eq 's') or (($pkg_mode eq 'a') and (-d $s))) {
+ $schedule->add_file('s', get_src_info_from_lab($s));
+ }
+ if (($pkg_mode eq 'u') or (($pkg_mode eq 'a') and (-d $u))) {
+ $schedule->add_file('u', get_bin_info_from_lab($u));
+ }
+ }
+}
+
+if (not $check_everything and not $packages_file and not $schedule->count) {
+ v_msg("No packages selected.");
+ exit $exit_code;
+}
+
+# Check to make sure there are packages to check.
+sub set_value {
+ my ($f,$target,$field,$source,$required) = @_;
+ use YAML;
+# print map { Dump($_) } @_;
+ if ($required and not $source->{$field}) {
+ print Dump($f)."\n";
+ fail("description file $f does not define required tag $field");
+ }
+ $target->{$field} = $source->{$field};
+ delete $source->{$field};
+}
+
+opendir(COLLDIR, "$MAEMIAN_ROOT/collection")
+ or fail("cannot read directory $MAEMIAN_ROOT/collection");
+
+for my $f (readdir COLLDIR) {
+ next if $f =~ /^\./;
+ next unless $f =~ /\.desc$/;
+
+ debug_msg(2, "Reading collector description file $f ...");
+ my @secs = read_dpkg_control("$MAEMIAN_ROOT/collection/$f");
+ my $script;
+ ($#secs+1 == 1)
+ or fail("syntax error in description file $f: too many sections");
+
+ ($script = $secs[0]->{'collector-script'})
+ or fail("error in description file $f: `Collector-Script:' not defined");
+
+ delete $secs[0]->{'collector-script'};
+ $collection_info{$script}->{'script'} = $script;
+ my $p = $collection_info{$script};
+
+ set_value($f, $p,'type',$secs[0],1);
+ # convert Type:
+ my ($b,$s,$u) = ( "", "", "" );;
+ for (split(/\s*,\s*/o,$p->{'type'})) {
+ if ($_ eq 'binary') {
+ $b = 'b';
+ } elsif ($_ eq 'source') {
+ $s = 's';
+ } elsif ($_ eq 'udeb') {
+ $u = 'u';
+ } else {
+ fail("unknown type $_ specified in description file $f");
+ }
+ }
+ $p->{'type'} = "$s$b$u";
+
+ set_value($f,$p,'unpack-level',$secs[0],1);
+ set_value($f,$p,'order',$secs[0],1);
+ set_value($f,$p,'version',$secs[0],1);
+
+ if (exists $secs[0]->{'needs-info'} && defined $secs[0]->{'needs-info'}) {
+ for (split(/\s*,\s*/o,$secs[0]->{'needs-info'})) {
+ $p->{$_} = 1;
+ }
+ delete $secs[0]->{'needs-info'};
+ }
+
+ # ignore Info: and other fields for now
+ delete $secs[0]->{'info'};
+ delete $secs[0]->{'author'};
+
+ for (keys %{$secs[0]}) {
+ warning("unused tag $_ in description file $f");
+ }
+
+ debug_msg(2, map( { "$_: $p->{$_}" } sort keys %$p ));
+}
+
+closedir(COLLDIR);
+# }}}
+
+# {{{ Now we're ready to load info about checks & tags
+
+# load information about checker scripts
+opendir(CHECKDIR, "$MAEMIAN_ROOT/checks")
+ or fail("cannot read directory $MAEMIAN_ROOT/checks");
+
+for my $f (readdir CHECKDIR) {
+ next if $f =~ /^\./;
+ next unless $f =~ /\.desc$/;
+ debug_msg(2, "Reading checker description file $f ...");
+
+ my @secs = read_dpkg_control("$MAEMIAN_ROOT/checks/$f");
+ my $script;
+ ($script = $secs[0]->{'check-script'})
+ or fail("error in description file $f: `Check-Script:' not defined");
+
+ # ignore check `maemian' (this check is a special case and contains the
+ # tag info for the maemian frontend--this script here)
+ next if $script eq 'maemian';
+
+ delete $secs[0]->{'check-script'};
+ $check_info{$script}->{'script'} = $script;
+ my $p = $check_info{$script};
+
+ set_value($f,$p,'type',$secs[0],1);
+ # convert Type:
+ my ($b,$s,$u) = ( "", "", "" );
+ for (split(/\s*,\s*/o,$p->{'type'})) {
+ if ($_ eq 'binary') {
+ $b = 'b';
+ } elsif ($_ eq 'source') {
+ $s = 's';
+ } elsif ($_ eq 'udeb') {
+ $u = 'u';
+ } else {
+ fail("unknown type $_ specified in description file $f");
+ }
+ }
+ $p->{'type'} = "$s$b$u";
+
+ set_value($f,$p,'unpack-level',$secs[0],1);
+ set_value($f,$p,'abbrev',$secs[0],1);
+
+ if (exists $secs[0]->{'needs-info'} && defined $secs[0]->{'needs-info'}) {
+ for (split(/\s*,\s*/o,$secs[0]->{'needs-info'})) {
+ $p->{$_} = 1;
+ }
+ delete $secs[0]->{'needs-info'};
+ }
+
+ # ignore Info: and other fields for now...
+ delete $secs[0]->{'info'};
+ delete $secs[0]->{'standards-version'};
+ delete $secs[0]->{'author'};
+
+ for (keys %{$secs[0]}) {
+ warning("unused tag $_ in description file $f");
+ }
+
+ debug_msg(2, map( { "$_: $p->{$_}" } sort keys %$p ));
+
+ shift(@secs);
+ $p->{'requested-tags'} = 0;
+ foreach my $tag (@secs) {
+ $tag->{'script'} = $script;
+ Tags::add_tag($tag);
+ $p->{'requested-tags'}++ if Tags::display_tag($tag);
+ }
+}
+
+closedir(CHECKDIR);
+
+# }}}
+
+# {{{ Again some lone code the author just dumped where his cursor just happened to be
+if ($unpack_info) {
+ # determine which info has been requested
+ for my $i (split(/,/,$unpack_info)) {
+ unless ($collection_info{$i}) {
+ fail("unknown info specified: $i");
+ }
+ $unpack_infos{$i} = 1;
+ }
+}
+
+# create check_abbrev hash
+for my $c (keys %check_info) {
+ $check_abbrev{$check_info{$c}->{'abbrev'}} = $c;
+}
+
+# }}}
+
+# {{{ determine which checks have been requested
+if ($action eq 'check') {
+ if ($check_tags) {
+ foreach my $t (split(/,/, $check_tags)) {
+ my $info = Tags::get_tag_info($t);
+
+ fail("unknown tag specified: $t") unless defined($info);
+ my $script = $info->{'script'};
+ next if $script eq 'maemian';
+ if ($check_info{$script}) {
+ $checks{$script} = 1;
+ } else {
+ # should never happen
+ fail("no info for script $script");
+ }
+ }
+ } else {
+ my %dont_check = map { $_ => 1 } (split m/,/, ($dont_check || ""));
+ $checks or ($checks = join(',',keys %check_info));
+ for my $c (split(/,/,$checks)) {
+ if ($check_info{$c}) {
+ if ($dont_check{$c}
+ || ($check_info{$c}->{'abbrev'}
+ && $dont_check{$check_info{$c}->{'abbrev'}})) {
+ #user requested not to run this check
+ } elsif ($check_info{$c}->{'requested-tags'} == 0) {
+ #no need to run this check, no tags will be issued
+ } else {
+ $checks{$c} = 1;
+ }
+ } elsif (exists $check_abbrev{$c}) {
+ #abbrevs only used when -C is given, so we don't need %dont_check
+ $checks{$check_abbrev{$c}} = 1;
+ } else {
+ fail("unknown check specified: $c");
+ }
+ }
+ }
+
+ # determine which info is needed by the checks
+ for my $c (keys %checks) {
+ for my $i (keys %collection_info) {
+ # required by $c ?
+ if ($check_info{$c}->{$i}) {
+ $unpack_infos{$i} = 1;
+ }
+ }
+ }
+}
+
+# }}}
+
+# {{{ determine which info is needed by the collection scripts
+for my $c (keys %unpack_infos) {
+ for my $i (keys %collection_info) {
+ # required by $c ?
+ if ($collection_info{$c}->{$i}) {
+ $unpack_infos{$i} = 1;
+ }
+ }
+}
+# }}}
+
+# {{{ process all packages in the archive?
+if ($check_everything) {
+ # make sure package info is available
+ read_src_list("$MAEMIAN_LAB/info/source-packages", 0);
+ read_bin_list("$MAEMIAN_LAB/info/binary-packages", 0);
+ read_udeb_list("$MAEMIAN_LAB/info/udeb-packages", 0);
+
+ debug_msg(2, "pkg_mode = $pkg_mode");
+
+ if (($pkg_mode eq 'a') or ($pkg_mode eq 's')) {
+ for my $arg (sort keys %source_info) {
+ debug_msg(1, "doing stuff with $MAEMIAN_ARCHIVEDIR/$source_info{$arg}->{'file'}");
+ $schedule->add_file('s', "$MAEMIAN_ARCHIVEDIR/$source_info{$arg}->{'file'}",
+ %{$source_info{$arg}});
+ }
+ }
+ if (($pkg_mode eq 'a') or ($pkg_mode eq 'b')) {
+ for my $arg (sort keys %binary_info) {
+ debug_msg(1, "doing stuff with $MAEMIAN_ARCHIVEDIR/$binary_info{$arg}->{'file'}");
+ $schedule->add_file('b', "$MAEMIAN_ARCHIVEDIR/$binary_info{$arg}->{'file'}",
+ %{$binary_info{$arg}});
+ }
+ }
+ if (($pkg_mode eq 'a') or ($pkg_mode eq 'u')) {
+ for my $arg (sort keys %udeb_info) {
+ debug_msg(1, "doing stuff with $MAEMIAN_ARCHIVEDIR/$udeb_info{$arg}->{'file'}");
+ $schedule->add_file('u', "$MAEMIAN_ARCHIVEDIR/$udeb_info{$arg}->{'file'}",
+ %{$udeb_info{$arg}});
+ }
+ }
+
+ # package list still empty?
+ unless ($schedule->count) {
+ warning("no packages found in distribution directory");
+ }
+} elsif ($packages_file) { # process all packages listed in packages file?
+ $schedule->add_pkg_list($packages_file);
+}
+# }}}
+
+# {{{ Some silent exit
+unless ($schedule->count) {
+ v_msg("No packages selected.");
+ exit 0;
+}
+# }}}
+
+# {{{ Okay, now really processing the packages in one huge loop
+$unpack_infos{ "override-file" } = 1 unless $no_override;
+v_msg(sprintf("Processing %d packages...", $schedule->count));
+debug_msg(1,
+ "Selected action: $action",
+ "Requested unpack level: $unpack_level",
+ sprintf("Requested data to collect: %s", join(',',keys %unpack_infos)),
+ sprintf("Selected checks: %s", join(',',keys %checks)),
+ );
+
+require Checker;
+require Maemian::Collect;
+
+my %overrides;
+my @pending_jobs;
+PACKAGE:
+foreach my $pkg_info ($schedule->get_all) {
+ my ($type, $pkg, $ver, $arch, $file) =
+ @$pkg_info{qw(type package version architecture file)};
+ my $long_type = ($type eq 'b' ? 'binary' :
+ ($type eq 's' ? 'source' : 'udeb' ));
+
+ Tags::set_pkg( $file, $pkg, $ver, $arch, $long_type );
+
+ # Kill pending jobs, if any
+ Maemian::Command::kill(@pending_jobs);
+ undef @pending_jobs;
+
+ # determine base directory
+ my $base = "$MAEMIAN_LAB/$long_type/$pkg";
+ unless ($base =~ m,^/,) {
+ $base = "$cwd/$base";
+ }
+ debug_msg(1, "Base directory in lab: $base");
+
+ my $act_unpack_level = 0;
+
+ # unpacked package up-to-date?
+ if (-d $base) {
+ my $remove_basedir = 0;
+
+ # there's a base dir, so we assume that at least
+ # one level of unpacking has been done
+ $act_unpack_level = 1;
+
+ # maemian status file exists?
+ unless (-f "$base/.maemian-status") {
+ v_msg("No maemian status file found (removing old directory in lab)");
+ $remove_basedir = 1;
+ goto REMOVE_BASEDIR;
+ }
+
+ # read unpack status -- catch any possible errors
+ my $data;
+ eval { ($data) = read_dpkg_control("$base/.maemian-status"); };
+ if ($@) { # error!
+ v_msg($@);
+ $remove_basedir = 1;
+ goto REMOVE_BASEDIR;
+ }
+
+ # compatible maemian version?
+ if (not exists $data->{'lab-format'} or ($data->{'lab-format'} < $LAB_FORMAT)) {
+ v_msg("Lab directory was created by incompatible maemian version");
+ $remove_basedir = 1;
+ goto REMOVE_BASEDIR;
+ }
+
+ # version up to date?
+ if (not exists $data->{'version'} or ($data->{'version'} ne $ver)) {
+ debug_msg(1, "Removing package in lab (newer version exists) ...");
+ $remove_basedir = 1;
+ goto REMOVE_BASEDIR;
+ }
+
+ # unpack level defined?
+ unless (exists $data->{'unpack-level'}) {
+ warning("cannot determine unpack-level of package");
+ $remove_basedir = 1;
+ goto REMOVE_BASEDIR;
+ } else {
+ $act_unpack_level = $data->{'unpack-level'};
+ }
+
+ # file modified?
+ my $timestamp;
+ my @stat;
+ unless (@stat = stat $file) {
+ warning("cannot stat file $file: $!");
+ } else {
+ $timestamp = $stat[9];
+ }
+ if ((not defined $timestamp) or (not exists $data->{'timestamp'}) or ($data->{'timestamp'} != $timestamp)) {
+ debug_msg(1, "Removing package in lab (package has been changed) ...");
+ $remove_basedir = 1;
+ goto REMOVE_BASEDIR;
+ }
+
+ REMOVE_BASEDIR:
+ if ($remove_basedir) {
+ v_msg("Removing $pkg");
+ unless (remove_pkg($base)) {
+ warning("skipping $action of $long_type package $pkg");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+ $act_unpack_level = 0;
+ }
+ }
+
+ # unpack to requested unpack level
+ $act_unpack_level = unpack_pkg($type,$base,$file,$act_unpack_level,
+ $unpack_level);
+ if ($act_unpack_level == -1) {
+ warning("could not unpack package to desired level",
+ "skipping $action of $long_type package $pkg");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+
+ if (($action eq 'unpack') or ($action eq 'check')) { # collect info
+ my $current_order = -1;
+ for my $coll (sort by_collection_order keys %unpack_infos) {
+ my $ci = $collection_info{$coll};
+ my %run_opts = ('description' => $coll);
+
+ # current type?
+ next unless ($ci->{'type'} =~ m/$type/);
+
+ # If a file named .SCRIPT-VERSION already exists, we've already
+ # collected this information and we can skip it. Otherwise,
+ # remove any .SCRIPT-* files (which are old version information).
+ next if (-f "$base/.${coll}-$ci->{'version'}");
+ opendir(BASE, $base)
+ or fail("cannot read directory $base: $!");
+ for my $file (readdir BASE) {
+ if ($file =~ /^\.\Q$coll-/) {
+ unlink("$base/$file");
+ }
+ }
+ closedir(BASE);
+
+ # unpack to desired unpack level (if necessary)
+ $act_unpack_level = unpack_pkg($type,$base,$file,$act_unpack_level,$ci->{'unpack-level'});
+ if ($act_unpack_level == -1) {
+ warning("could not unpack package to desired level",
+ "skipping $action of $long_type package $pkg");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+
+ # chdir to base directory
+ unless (chdir($base)) {
+ warning("could not chdir into directory $base: $!",
+ "skipping $action of $long_type package $pkg");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+
+ $current_order = $ci->{'order'} if ($current_order == -1);
+ if ($current_order != $ci->{'order'}) {
+ debug_msg(1, "Waiting for jobs from order $current_order ...");
+ unless (reap_collect_jobs($pkg, $base, @pending_jobs)) {
+ warning("skipping $action of $long_type package $pkg");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+ undef @pending_jobs;
+ $current_order = $ci->{'order'};
+ }
+
+ # collect info
+ remove_status_file($base);
+ debug_msg(1, "Collecting info: $coll ...");
+ my $script = "$MAEMIAN_ROOT/collection/$ci->{'script'}";
+ unless (spawn(\%run_opts, [ $script, $pkg, $long_type, '&' ])) {
+ warning("collect info $coll about package $pkg failed",
+ "skipping $action of $long_type package $pkg");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+ push @pending_jobs, \%run_opts;
+ }
+
+ # wait until all the jobs finish and skip this package if any of them
+ # failed.
+ debug_msg(1, "Waiting for jobs from order $current_order ...");
+ unless (reap_collect_jobs($pkg, $base, @pending_jobs)) {
+ warning("skipping $action of $long_type package $pkg");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+ undef @pending_jobs;
+ }
+
+ if ($action eq 'check') { # read override file
+
+ unless ($no_override) {
+ Tags::add_overrides("$base/override", $pkg, $long_type)
+ if (-f "$base/override")
+ }
+
+ # perform checks
+ my $info = Maemian::Collect->new($pkg, $long_type);
+ for my $check (keys %checks) {
+ my $ci = $check_info{$check};
+
+ # current type?
+ next unless ($ci->{'type'} =~ m/$type/);
+
+ # unpack to desired unpack level (if necessary)
+ $act_unpack_level = unpack_pkg($type,$base,$file,$act_unpack_level,$ci->{'unpack-level'});
+ if ($act_unpack_level == -1) {
+ warning("could not unpack package to desired level",
+ "skipping $action of $long_type package $pkg");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+
+ # chdir to base directory
+ unless (chdir($base)) {
+ warning("could not chdir into directory $base: $!",
+ "skipping $action of $long_type package $pkg");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+
+ my $returnvalue = Checker::runcheck($pkg, $long_type, $info, $check);
+ # Set exit_code correctly if there was not yet an exit code
+ $exit_code = $returnvalue unless $exit_code;
+
+ if ($returnvalue == 2) {
+ warning("skipping $action of $long_type package $pkg");
+ next PACKAGE;
+ }
+
+ }
+ unless ($exit_code) {
+ my $stats = Tags::get_stats( $file );
+ if ($stats->{types}{E}) {
+ $exit_code = 1;
+ } elsif ($fail_on_warnings && $stats->{types}{W}) {
+ $exit_code = 1;
+ }
+ }
+
+ # report unused overrides
+ if (not $no_override) {
+ my $overrides = Tags::get_overrides( $file );
+
+ for my $tag (sort keys %$overrides) {
+ my $taginfo = Tags::get_tag_info{$tag};
+ if (defined $taginfo) {
+ # Did we run the check script containing the tag?
+ next unless $checks{$taginfo->{'script'}};
+
+ # If only checking specific tags, is this one of them?
+ next unless (scalar keys %Tags::only_issue_tags == 0)
+ or exists $Tags::only_issue_tags{$tag};
+ }
+
+ for my $extra (sort keys %{$overrides->{$tag}}) {
+ next if $overrides->{$tag}{$extra};
+
+ tag( "unused-override", $tag, $extra );
+ }
+ }
+ }
+
+ # Report override statistics.
+ if (not $no_override and not $show_overrides) {
+ my $stats = Tags::get_stats($file);
+ my $short = $file;
+ $short =~ s%.*/%%;
+ my $errors = $stats->{overrides}{types}{E} || 0;
+ my $warnings = $stats->{overrides}{types}{W} || 0;
+ my $info = $stats->{overrides}{types}{I} || 0;
+ $overrides{errors} += $errors;
+ $overrides{warnings} += $warnings;
+ $overrides{info} += $info;
+ }
+ }
+
+ # chdir to maemian root directory (to unlock $base so it can be removed below)
+ unless (chdir($MAEMIAN_ROOT)) {
+ warning("could not chdir into directory $MAEMIAN_ROOT: $!",
+ "skipping $action of $long_type package $pkg");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+
+ # clean up
+ if ($act_unpack_level > $unpack_level) {
+ $act_unpack_level = clean_pkg($type,$base,$file,$act_unpack_level,$unpack_level);
+ if ($act_unpack_level == -1) {
+ warning("could not clean up laboratory for package $pkg: $!",
+ "skipping clean up");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+ }
+
+ # create Maemian status file
+ if (($act_unpack_level > 0) and (not -f "$base/.maemian-status")) {
+ my @stat;
+ unless (@stat = stat $file) {
+ warning("cannot stat file $file: $!",
+ "skipping creation of status file");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+ my $timestamp = $stat[9];
+
+ unless (open(STATUS, '>', "$base/.maemian-status")) {
+ warning("could not create status file $base/.maemian-status for package $pkg: $!");
+ $exit_code = 2;
+ next PACKAGE;
+ }
+
+ print STATUS "Maemian-Version: $MAEMIAN_VERSION\n";
+ print STATUS "Lab-Format: $LAB_FORMAT\n";
+ print STATUS "Package: $pkg\n";
+ print STATUS "Version: $ver\n";
+ print STATUS "Type: $type\n";
+ print STATUS "Unpack-Level: $act_unpack_level\n";
+ print STATUS "Timestamp: $timestamp\n";
+ close(STATUS);
+ }
+}
+Tags::reset_pkg();
+if ($action eq 'check' and not $no_override and not $show_overrides) {
+ my $errors = $overrides{errors} || 0;
+ my $warnings = $overrides{warnings} || 0;
+ my $info = $overrides{info} || 0;
+ my $total = $errors + $warnings + $info;
+ if ($total > 0) {
+ my $total = ($total == 1)
+ ? "$total tag overridden"
+ : "$total tags overridden";
+ my @output;
+ if ($errors) {
+ push (@output, ($errors == 1) ? "$errors error" : "$errors errors");
+ }
+ if ($warnings) {
+ push (@output, ($warnings == 1) ? "$warnings warning" : "$warnings warnings");
+ }
+ if ($info) {
+ push (@output, "$info info");
+ }
+ msg("$total (". join (', ', @output). ")");
+ }
+}
+
+# }}}
+
+exit $exit_code;
+
+# {{{ Some subroutines
+
+sub unpack_pkg {
+ my ($type,$base,$file,$cur_level,$new_level) = @_;
+
+ debug_msg(1, sprintf("Current unpack level is %d",$cur_level));
+
+ return $cur_level if $cur_level == $new_level;
+
+ # remove .maemian-status file
+ remove_status_file($base);
+
+ if ( ($cur_level == 0) and (-d $base) ) {
+ # We were lied to, there's something already there - clean it up first
+ remove_pkg($base) or return -1;
+ }
+
+ if ( ($new_level >= 1) and
+ (not defined ($cur_level) or ($cur_level < 1)) ) {
+ # create new directory
+ debug_msg(1, "Unpacking package to level 1 ...");
+ if (($type eq 'b') || ($type eq 'u')) {
+ spawn({}, ["$MAEMIAN_ROOT/unpack/unpack-binpkg-l1", $base, $file])
+ or return -1;
+ } else {
+ spawn({}, ["$MAEMIAN_ROOT/unpack/unpack-srcpkg-l1", $base, $file])
+ or return -1;
+ }
+ $cur_level = 1;
+ }
+
+ if ( ($new_level >= 2) and
+ (not defined ($cur_level) or ($cur_level < 2)) ) {
+ # unpack package contents
+ debug_msg(1, "Unpacking package to level 2 ...");
+ if (($type eq 'b') || ($type eq 'u')) {
+ spawn({}, ["$MAEMIAN_ROOT/unpack/unpack-binpkg-l2", $base])
+ or return -1;
+ } else {
+ debug_msg(1, "$MAEMIAN_ROOT/unpack/unpack-srcpkg-l2 $base");
+ spawn({}, ["$MAEMIAN_ROOT/unpack/unpack-srcpkg-l2", $base])
+ or return -1;
+ }
+ $cur_level = 2;
+ }
+
+ return $cur_level;
+}
+
+# Given a list of jobs corresponding to collect scripts, reap each of the
+# jobs. For each successful job, record that it was successful by creating
+# the corresponding version marker file in the lab. For each unsuccessful
+# job, warn that it was unsuccessful.
+#
+# Takes the current package, base directory, and the list of pending jobs.
+# Return true if all jobs were successful, false otherwise.
+sub reap_collect_jobs {
+ my ($pkg, $base, @pending_jobs) = @_;
+ my $status = reap(@pending_jobs);
+ for my $job (@pending_jobs) {
+ my $coll = $job->{'description'};
+ if ($job->{success}) {
+ my $ci = $collection_info{$coll};
+ open(VERSION, '>', "$base/.${coll}-$ci->{'version'}")
+ or fail("cannot create $base/.${coll}-$ci->{'version'}: $!");
+ print VERSION "Maemian-Version: $MAEMIAN_VERSION\n"
+ . "Timestamp: " . time . "\n";
+ close(VERSION);
+ } else {
+ warning("collect info $coll about package $pkg failed");
+ }
+ }
+ return $status;
+}
+
+# TODO: is this the best way to clean dirs in perl?
+# no, look at File::Path module
+sub clean_pkg {
+ my ($type,$base,$file,$cur_level,$new_level) = @_;
+
+ return $cur_level if $cur_level == $new_level;
+
+ if ($new_level < 1) {
+ # remove base directory
+ remove_pkg($base) or return -1;
+ return 0;
+ }
+
+ if ( ($new_level < 2) and defined ($cur_level) and ($cur_level >= 2) ) {
+ # remove .maemian-status file
+ remove_status_file($base);
+
+ # remove unpacked/ directory
+ debug_msg(1, "Decreasing unpack level to 1 (removing files) ...");
+ if ( -l "$base/unpacked" ) {
+ delete_dir("$base/".readlink("$base/unpacked"))
+ or return -1;
+ delete_dir("$base/unpacked") or return -1;
+ } else {
+ delete_dir("$base/unpacked") or return -1;
+ }
+
+ $cur_level = 1;
+ }
+
+ return $cur_level;
+}
+
+# this function removes a package's base directory in the lab completely
+sub remove_pkg {
+ my ($base) = @_;
+
+ debug_msg(1, "Removing package in lab ...");
+ unless (delete_dir($base)) {
+ warning("cannot remove directory $base: $!");
+ return 0;
+ }
+
+ return 1;
+}
+
+sub remove_status_file {
+ my ($base) = @_;
+
+ # status file exists?
+ if (not -e "$base/.maemian-status") {
+ return 1;
+ }
+
+ if (not unlink("$base/.maemian-status")) {
+ warning("cannot remove status file $base/.maemian-status: $!");
+ return 0;
+ }
+
+ return 1;
+}
+
+# get package name, version, and file name from the lab
+sub get_bin_info_from_lab {
+ my ($base_dir) = @_;
+ my ($pkg,$ver,$arch,$file);
+
+ ($pkg = read_file("$base_dir/fields/package"))
+ or fail("cannot read file $base_dir/fields/package: $!");
+
+ ($ver = read_file("$base_dir/fields/version"))
+ or fail("cannot read file $base_dir/fields/version: $!");
+
+ ($arch = read_file("$base_dir/fields/architecture"))
+ or fail("cannot read file $base_dir/fields/architecture: $!");
+
+ ($file = readlink("$base_dir/deb"))
+ or fail("cannot read link $base_dir/deb: $!");
+
+ return ($file, package => $pkg, version => $ver, architecture => $arch);
+}
+
+# get package name, version, and file name from the lab
+sub get_src_info_from_lab {
+ my ($base_dir) = @_;
+ my ($pkg,$ver,$file);
+
+ ($pkg = read_file("$base_dir/fields/source"))
+ or fail("cannot read file $base_dir/fields/source: $!");
+
+ ($ver = read_file("$base_dir/fields/version"))
+ or fail("cannot read file $base_dir/fields/version: $!");
+
+ ($file = readlink("$base_dir/dsc"))
+ or fail("cannot read link $base_dir/dsc: $!");
+
+ return ($file, source => $pkg, version => $ver);
+}
+
+# read first line of a file
+sub read_file {
+ my $first_line;
+
+ open(T, '<', $_[0]) or return;
+ chop($first_line = <T>);
+ close(T) or return;
+
+ return $first_line;
+}
+
+# sort collection list by `order'
+sub by_collection_order {
+ $collection_info{$a}->{'order'} <=> $collection_info{$b}->{'order'};
+}
+
+sub END {
+ # Prevent Lab::delete from affecting the exit code.
+ local $?;
+
+ $SIG{'INT'} = 'DEFAULT';
+ $SIG{'QUIT'} = 'DEFAULT';
+
+ $LAB->delete() if $LAB and not $keep_lab;
+}
+
+sub interrupted {
+ $SIG{$_[0]} = 'DEFAULT';
+ die "N: Interrupted.\n";
+}
+
+1;
+__END__