lintian/lib/Lintian/Pool.pm

414 lines
10 KiB
Perl

# Copyright (C) 2011 Niels Thykier <niels@thykier.net>
# Copyright (C) 2020-2021 Felix Lechner
#
# 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, you can find it on the World Wide
# Web at https://www.gnu.org/copyleft/gpl.html, or write to the Free
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
# MA 02110-1301, USA.
## Represents a pool of processables (Lintian::Processable)
package Lintian::Pool;
use v5.20;
use warnings;
use utf8;
use Const::Fast;
use Cwd qw(getcwd);
use List::SomeUtils qw(any);
use Time::HiRes qw(gettimeofday tv_interval);
use Path::Tiny;
use POSIX qw(:sys_wait_h);
use Proc::ProcessTable;
use Unicode::UTF8 qw(encode_utf8);
use Lintian::Group;
const my $SPACE => q{ };
const my $COMMA => q{,};
const my $SEMICOLON => q{;};
const my $LEFT_PARENS => q{(};
const my $RIGHT_PARENS => q{)};
const my $PLURAL_S => q{s};
const my $ANY_CHILD => -1;
const my $WORLD_WRITABLE_FOLDER => oct(777);
use Moo;
use namespace::clean;
=head1 NAME
Lintian::Pool -- Pool of processables
=head1 SYNOPSIS
use Lintian::Pool;
my $pool = Lintian::Pool->new;
$pool->add_file('foo.changes');
$pool->add_file('bar.dsc');
$pool->add_file('baz.deb');
$pool->add_file('qux.buildinfo');
foreach my $gname ($pool->get_group_names){
my $group = $pool->get_group($gname);
process($gname, $group);
}
=head1 METHODS
=over 4
=item $pool->groups
Returns a hash reference to the list of processable groups that are currently
in the pool. The key is a unique identifier based on name and version.
=item C<savedir>
=cut
has groups => (is => 'rw', default => sub{ {} });
has savedir => (is => 'rw', default => sub{ getcwd; });
# must be absolute; frontend/lintian depends on it
has basedir => (
is => 'rw',
default => sub {
my $absolute
= Path::Tiny->tempdir(TEMPLATE => 'lintian-pool-XXXXXXXXXX');
$absolute->mkpath({mode => $WORLD_WRITABLE_FOLDER});
return $absolute;
}
);
=item $pool->basedir
Returns the base directory for the pool. Most likely it's a temporary directory.
=item $pool->add_group($group)
Adds a group to the pool.
=cut
sub add_group {
my ($self, $group) = @_;
my $name = $group->name;
unless (exists $self->groups->{$name}){
# group does not exist; just add whole
$self->groups->{$name} = $group;
return 1;
}
# group exists; merge & accept all new
my $added = 0;
my $old = $self->groups->{$name};
for my $type (qw/source buildinfo changes/) {
if (!defined $old->$type && defined $group->$type) {
$old->add_processable($group->$type);
$added = 1;
}
}
for my $installable ($group->get_installables){
# New binary package ?
my $was_new = $old->add_processable($installable);
$added ||= $was_new;
}
return $added;
}
=item $pool->process
Process the pool.
=cut
sub process{
my ($self, $PROFILE, $exit_code_ref, $option)= @_;
if ($self->empty) {
say {*STDERR} encode_utf8('No packages selected.');
return;
}
my %reported_count;
my %override_count;
my %ignored_overrides;
my $unused_overrides = 0;
for my $group (values %{$self->groups}) {
my $total_start = [gettimeofday];
$group->profile($PROFILE);
$group->jobs($option->{'jobs'});
my $success= $group->process(\%ignored_overrides, $option);
for my $processable ($group->get_processables){
my @keep;
for my $hint (@{$processable->hints}) {
my $tag = $PROFILE->get_tag($hint->tag_name);
# discard experimental tags
next
if $tag->experimental
&& !$option->{'display-experimental'};
# discard overridden tags
next
if defined $hint->override
&& !$option->{'show-overrides'};
# discard outside the selected display level
next
unless $PROFILE->display_level_for_tag($hint->tag_name);
if (!defined $hint->override
|| $option->{'show-overrides'}) {
++$reported_count{$tag->visibility}
if !$tag->experimental;
++$reported_count{experimental}
if $tag->experimental;
}
++$reported_count{override}
if defined $hint->override;
++$unused_overrides
if $hint->tag_name eq 'unused-override'
|| $hint->tag_name eq 'mismatched-override';
push(@keep, $hint);
}
$processable->hints(\@keep);
}
${$exit_code_ref} = 2
if $success && any { $reported_count{$_} } @{$option->{'fail-on'}};
# interruptions can leave processes behind (manpages); wait and reap
if (${$exit_code_ref} == 1) {
1 while waitpid($ANY_CHILD, WNOHANG) > 0;
}
if ($option->{debug}) {
my $process_table = Proc::ProcessTable->new;
my @leftover= grep { $_->ppid == $$ } @{$process_table->table};
# announce left over processes, see commit 3bbcc3b
if (@leftover) {
warn encode_utf8(
"\nSome processes were left over (maybe unreaped):\n");
my $FORMAT = ' %-12s %-12s %-8s %-24s %s';
say encode_utf8(
sprintf(
$FORMAT,'PID', 'TTY', 'STATUS', 'START', 'COMMAND'
)
);
say encode_utf8(
sprintf($FORMAT,
$_->pid,$_->ttydev,
$_->state,scalar(localtime($_->start)),
$_->cmndline)
)for @leftover;
${$exit_code_ref} = 1;
die encode_utf8("Aborting.\n");
}
}
my $total_raw_res = tv_interval($total_start);
my $total_tres = sprintf('%.3fs', $total_raw_res);
my $status = $success ? 'complete' : 'error';
say {*STDERR}
encode_utf8($status . $SPACE . $group->name . " ($total_tres)")
if $option->{'status-log'};
say {*STDERR} encode_utf8('Finished processing group ' . $group->name)
if $option->{debug};
${$exit_code_ref} = 1
unless $success;
}
my $OUTPUT;
if ($option->{'output-format'} eq 'html') {
require Lintian::Output::HTML;
$OUTPUT = Lintian::Output::HTML->new;
} elsif ($option->{'output-format'} eq 'json') {
require Lintian::Output::JSON;
$OUTPUT = Lintian::Output::JSON->new;
} elsif ($option->{'output-format'} eq 'universal') {
require Lintian::Output::Universal;
$OUTPUT = Lintian::Output::Universal->new;
} else {
require Lintian::Output::EWI;
$OUTPUT = Lintian::Output::EWI->new;
}
# pass everything, in case some groups or processables have no hints
$OUTPUT->issue_hints($PROFILE, [values %{$self->groups}], $option);
my $errors = $override_count{error} // 0;
my $warnings = $override_count{warning} // 0;
my $info = $override_count{info} // 0;
my $total = $errors + $warnings + $info;
if ( $option->{'output-format'} eq 'ewi'
&& !$option->{'no-override'}
&& !$option->{'show-overrides'}
&& ($total > 0 || $unused_overrides > 0)) {
my @details;
push(@details, quantity($errors, 'error'))
if $errors;
push(@details, quantity($warnings, 'warning'))
if $warnings;
push(@details, "$info info")
if $info;
my $text = quantity($total, 'hint') . ' overridden';
$text
.= $SPACE
. $LEFT_PARENS
. join($COMMA . $SPACE, @details)
. $RIGHT_PARENS
if @details;
$text
.= $SEMICOLON
. $SPACE
. quantity($unused_overrides, 'unused override');
say encode_utf8("N: $text");
}
if ($option->{'output-format'} eq 'ewi' && %ignored_overrides) {
say encode_utf8('N: Some overrides were ignored.');
if ($option->{verbose}) {
say encode_utf8(
'N: The following tags had at least one override but are mandatory:'
);
say encode_utf8("N: - $_") for sort keys %ignored_overrides;
} else {
say encode_utf8('N: Use --verbose for more information.');
}
}
path($self->basedir)->remove_tree
if length $self->basedir && -d $self->basedir;
return;
}
=item quantity
=cut
sub quantity {
my ($count, $unit) = @_;
my $text = $count . $SPACE . $unit;
$text .= $PLURAL_S
unless $count == 1;
return $text;
}
=item $pool->get_group_names
Returns the name of all the groups in this pool.
Do not modify the list nor its contents.
=cut
sub get_group_names{
my ($self) = @_;
return keys %{ $self->groups };
}
=item $pool->get_group($name)
Returns the group called $name or C<undef>
if there is no group called $name.
=cut
sub get_group{
my ($self, $group) = @_;
return $self->groups->{$group};
}
=item $pool->empty
Returns true if the pool is empty.
=cut
sub empty{
my ($self) = @_;
return scalar keys %{$self->groups} == 0;
}
=back
=head1 AUTHOR
Originally written by Niels Thykier <niels@thykier.net> for Lintian.
=head1 SEE ALSO
lintian(1)
L<Lintian::Processable>
L<Lintian::Group>
=cut
1;
# Local Variables:
# indent-tabs-mode: nil
# cperl-indent-level: 4
# End:
# vim: syntax=perl sw=4 sts=4 sr et