Adding side stream changes to Maemian. Working to integrate full upstream libraries...
[maemian] / nokia-lintian / reporting / html_reports
1 #!/usr/bin/perl -w
2 #
3 # Lintian HTML reporting tool -- Create Lintian web reports
4 #
5 # Copyright (C) 1998 Christian Schwarz and Richard Braakman
6 # Copyright (C) 2007 Russ Allbery
7 #
8 # This program is free software.  It is distributed under the terms of
9 # the GNU General Public License as published by the Free Software
10 # Foundation; either version 2 of the License, or (at your option) any
11 # later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program.  If not, you can find it on the World Wide
20 # Web at http://www.gnu.org/copyleft/gpl.html, or write to the Free
21 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
22 # MA 02110-1301, USA.
23
24 use strict;
25 use File::Copy qw(copy);
26 use URI::Escape;
27 use Text::Template ();
28
29 # ------------------------------
30 # Global variables and configuration
31
32 # Maximum number of identical tags per package to display.  Any remaining tags
33 # will be compressed into a "... reported %d more times" line.
34 our $MAX_TAGS = 8;
35
36 # These have no default and must be set in the configuration file.
37 # FIXME: $statistics_file should be in all caps as well.
38 our ($LINTIAN_ROOT, $LINTIAN_LAB, $LINTIAN_ARCHIVEDIR, $LINTIAN_DIST,
39      $LINTIAN_SECTION, $LINTIAN_ARCH, $HTML_TMP_DIR, $statistics_file);
40
41 # Read the configuration.
42 require './config';
43
44 # The path to the mirror timestamp.
45 our $LINTIAN_TIMESTAMP
46     = "$LINTIAN_ARCHIVEDIR/project/trace/ftp-master.debian.org";
47
48 # FIXME: At least the lab should be a parameter to Read_pkglists rather
49 # than an environment variable.
50 $ENV{'LINTIAN_LAB'} = $LINTIAN_LAB;
51 $ENV{'LINTIAN_ROOT'} = $LINTIAN_ROOT;
52
53 # Import Lintian Perl libraries.
54 use lib "$ENV{LINTIAN_ROOT}/lib";
55 use Read_pkglists;
56 use Read_taginfo;
57 use Util;
58
59 # Global variables from Read_pkglists.  Ugh.
60 # FIXME: Read_pkglists should return this information instead.
61 our (%binary_info, %source_info, %udeb_info, %bin_src_ref);
62
63 # Get the tag information from the Lintian *.desc files.
64 our %tag_info = %{ read_tag_info('html') };
65
66 # Set the Lintian version, current timestamp, and archive timestamp.
67 our $LINTIAN_VERSION = `$LINTIAN_ROOT/frontend/lintian --print-version`;
68 our $timestamp = `date -u --rfc-822`;
69 our $mirror_timestamp = slurp_entire_file($LINTIAN_TIMESTAMP);
70 chomp ($LINTIAN_VERSION, $timestamp, $mirror_timestamp);
71
72
73 # ------------------------------
74 # Initialize templates
75
76 # The path to our templates.
77 our $TEMPLATES = "$LINTIAN_ROOT/reporting/templates";
78
79 # This only has to be done once, so do it at the start and then reuse the same
80 # templates throughout.
81 our %templates;
82 for my $template (qw/head foot clean index maintainer maintainers packages tag tags/) {
83     my %options = (TYPE => 'FILE', SOURCE => "$TEMPLATES/$template.tmpl");
84     $templates{$template} = Text::Template->new (%options)
85         or die "cannot load template $template: $Text::Template::ERROR\n";
86 }
87
88
89 # ------------------------------
90 # Main routine
91
92 # Read the package lists.
93 #
94 # FIXME: get_bin_src_ref runs read_src_list unconditionally so we can't call
95 # it directly, which is confusing.
96 read_bin_list;
97 read_udeb_list;
98 get_bin_src_ref;
99
100 # Create output directories.
101 mkdir($HTML_TMP_DIR, 0777)
102     or die "cannot create output directory $HTML_TMP_DIR: $!\n";
103 mkdir("$HTML_TMP_DIR/full", 0777)
104     or die "cannot create output directory $HTML_TMP_DIR/full: $!\n";
105 mkdir("$HTML_TMP_DIR/maintainer", 0777)
106     or die "cannot create output directory $HTML_TMP_DIR/maintainer: $!\n";
107 mkdir("$HTML_TMP_DIR/tags", 0777)
108     or die "cannot create output directory $HTML_TMP_DIR/tags: $!\n";
109 symlink(".", "$HTML_TMP_DIR/reports")
110     or die "cannot create symlink $HTML_TMP_DIR/reports: $!\n";
111 symlink("$LINTIAN_ROOT/doc/lintian.html", "$HTML_TMP_DIR/manual")
112     or die "cannot create symlink $HTML_TMP_DIR/manual: $!\n";
113 if ($ARGV[0]) {
114     symlink($ARGV[0], "$HTML_TMP_DIR/lintian.log")
115         or die "cannot create symlink $HTML_TMP_DIR/lintian.log: $!\n";
116 }
117 copy("$LINTIAN_ROOT/reporting/lintian.css", "$HTML_TMP_DIR/lintian.css")
118     or die "cannot copy lintian.css to $HTML_TMP_DIR: $!\n";
119 for my $image (qw/ico.png l.png logo-small.png/) {
120     copy("$LINTIAN_ROOT/reporting/images/$image", "$HTML_TMP_DIR/$image")
121         or die "cannot copy images/$image to $HTML_TMP_DIR: $!\n";
122 }
123
124 # This variable will accumulate statistics.  For tags: errors, warnings,
125 # experimental, overridden, and info are the keys holding the count of tags of
126 # that sort.  For packages: binary, udeb, and source are the number of
127 # packages of each type with Lintian errors or warnings.  For maintainers:
128 # maintainers is the number of maintainers with Lintian errors or warnings.
129 my %statistics;
130
131 # %by_maint holds a hash of maintainer names to packages and tags.  Each
132 # maintainer is a key.  The value is a hash of package names to hashes.  Each
133 # package hash is in turn a hash of versions to an anonymous array of hashes,
134 # with each hash having keys code, package, type, tag, extra, and xref.  xref
135 # gets the partial URL of the maintainer page for that source package.
136 #
137 # In other words, the lintian output line:
138 #
139 #     W: gnubg source: substvar-source-version-is-deprecated gnubg-data
140 #
141 # for gnubg 0.15~20061120-1 maintained by Russ Allbery <rra@debian.org> is
142 # turned into the following structure:
143 #
144 # { 'gnubg' => {
145 #       '0.15~20061120-1' => [
146 #           { code    => 'W',
147 #             package => 'gnubg',
148 #             type    => 'source',
149 #             tag     => 'substvar-source-version-is-deprecated',
150 #             extra   => 'gnubg-data'
151 #             xref    => 'rra@debian.org.html#gnubg' } ] } }
152 #
153 # and then stored under the key 'Russ Allbery <rra@debian.org>'
154 #
155 # %by_uploader holds the same thing except for packages for which the person
156 # is only an uploader.
157 #
158 # %by_tag is a hash of tag names to an anonymous array of tag information
159 # hashes just like the inside-most data structure above.
160 my (%by_maint, %by_uploader, %by_tag);
161
162 # We take a lintian log file on either standard input or as the first
163 # argument.  This log file contains all the tags lintian found, plus N: tags
164 # with informational messages.  Ignore all the N: tags and load everything
165 # else into the hashes we use for all web page generation.
166 #
167 # We keep track of a hash from maintainer page URLs to maintainer values so
168 # that we don't have two maintainers who map to the same page and overwrite
169 # each other's pages.  If we find two maintainers who map to the same URL,
170 # just assume that the second maintainer is the same as the first (but warn
171 # about it).
172 my (%seen, %saw_maintainer);
173 while (<>) {
174     chomp;
175     next unless m/^([EWIXO]): (\S+)(?: (\S+))?: (\S+)(?:\s+(.*))?/;
176     my ($code, $package, $type, $tag, $extra) = ($1, $2, $3, $4, $5);
177     $type = 'binary' unless (defined $type);
178     next unless ($type eq 'source' || $type eq 'binary' || $type eq 'udeb');
179
180     # Update statistics.
181     my $key = {
182         E => 'errors',
183         W => 'warnings',
184         I => 'info',
185         X => 'experimental',
186         O => 'overridden'
187     }->{$code};
188     $statistics{$key}++;
189     unless ($seen{"$package $type"}) {
190         $statistics{"$type-packages"}++;
191         $seen{"$package $type"} = 1;
192     }
193
194     # Determine the source package for this package and warn if there appears
195     # to be no source package in the archive.  Determine the maintainer and
196     # version.  Work around a missing source package by pulling information
197     # from a binary package or udeb of the same name if there is any.
198     my ($source, $version, $maintainer, $uploaders);
199     if ($type eq 'source') {
200         $source = $package;
201         if (exists $source_info{$source}) {
202             $version = $source_info{$source}->{version};
203             $maintainer = $source_info{$source}->{maintainer};
204             $uploaders = $source_info{$source}->{uploaders};
205         } else {
206             warn "source package $package not listed!\n";
207         }
208     } else {
209         $source = $bin_src_ref{$package};
210         if ($source and exists $source_info{$source}) {
211             $maintainer = $source_info{$source}->{maintainer};
212             $uploaders = $source_info{$source}->{uploaders};
213         } else {
214             warn "source for package $package not found!\n";
215             $source = $package;
216             if ($type eq 'binary') {
217                 $maintainer = $binary_info{$package}->{maintainer};
218             } elsif ($type eq 'udeb') {
219                 $maintainer = $udeb_info{$package}->{maintainer};
220             }
221         }
222         if ($type eq 'binary') {
223             $version = $binary_info{$package}->{version};
224         } elsif ($type eq 'udeb') {
225             $version = $udeb_info{$package}->{version};
226         }
227     }
228     $maintainer ||= '(unknown)';
229     $version ||= 'unknown';
230
231     # Check if we've seen the URL for this maintainer before and, if so, map
232     # them to the same person as the previous one.
233     $maintainer = map_maintainer ($maintainer);
234     $saw_maintainer{$maintainer} = 1;
235
236     # Update maintainer statistics.
237     $statistics{maintainers}++ unless defined $by_maint{$maintainer};
238
239     # Sanitize, just out of paranoia.
240     $source =~ s/[^a-zA-Z0-9.+-]/_/g;
241     $version =~ s/[^a-zA-Z0-9.+:~-]/_/g;
242
243     # Add the tag information to our hashes.  Share the data between the
244     # hashes to save space (which means we can't later do destructive tricks
245     # with it).
246     my $info = {
247         code    => html_quote ($code),
248         package => html_quote ($package),
249         type    => html_quote ($type),
250         tag     => html_quote ($tag),
251         extra   => html_quote ($extra),
252         xref    => maintainer_url ($maintainer) . "#$source"
253     };
254     $by_maint{$maintainer}{$source}{$version} ||= [];
255     push(@{ $by_maint{$maintainer}{$source}{$version} }, $info);
256     $by_tag{$tag} ||= [];
257     push(@{ $by_tag{$tag} }, $info);
258
259     # If the package had uploaders listed, also add the information to
260     # %by_uploaders (still sharing the data between hashes).
261     if ($uploaders) {
262         my @uploaders = split (/\s*,\s*/, $uploaders);
263         for (@uploaders) {
264             my $uploader = map_maintainer ($_);
265             next if $uploader eq $maintainer;
266             $saw_maintainer{$uploader} = 1;
267             $by_uploader{$uploader}{$source}{$version} ||= [];
268             push(@{ $by_uploader{$uploader}{$source}{$version} }, $info);
269         }
270     }
271 }
272
273 # Build a hash of all maintainers, not just those with Lintian tags.  We use
274 # this later to generate stub pages for maintainers whose packages are all
275 # Lintian-clean.
276 my %clean;
277 for my $source (keys %source_info) {
278     my $maintainer = $source_info{$source}->{maintainer};
279     my $id = maintainer_url ($maintainer);
280     $clean{$id} = $maintainer;
281 }
282
283 # Now, walk through the tags by source package (sorted by maintainer).  Output
284 # a summary page of errors and warnings for each maintainer, output a full
285 # page that includes info, experimental, and overriden tags, and assemble the
286 # maintainer index and the QA package list as we go.
287 my (%qa, %maintainers, %packages);
288 my @maintainers;
289 {
290     my %seen;
291     @maintainers =
292         sort grep { !$seen{$_}++ } keys (%by_maint), keys (%by_uploader);
293 }
294 for my $maintainer (@maintainers) {
295     my $id = maintainer_url ($maintainer);
296     delete $clean{$id};
297
298     # For each of this maintainer's packages, add statistical information
299     # about warnings and errors to the QA list and build the packages hash
300     # used for the package index.  We only do this for the maintainer
301     # packages, not the uploader packages, to avoid double-counting.
302     for my $source (keys %{ $by_maint{$maintainer} }) {
303         my ($errors, $warnings) = (0, 0);
304         for my $version (keys %{ $by_maint{$maintainer}{$source} }) {
305             my $tags = $by_maint{$maintainer}{$source}{$version};
306             for my $tag (@$tags) {
307                 $errors++ if $tag->{code} eq 'E';
308                 $warnings++ if $tag->{code} eq 'W';
309                 $packages{$tag->{package}} = $tag->{xref};
310             }
311         }
312         $qa{$source} = [ $errors, $warnings ];
313     }
314
315     # Determine if the maintainer's page is clean.  Check all packages for
316     # which they're either maintainer or uploader and set $error_clean if
317     # they have no errors or warnings.
318     my $error_clean = 1;
319     for my $source (keys %{ $by_maint{$maintainer} },
320                     keys %{ $by_uploader{$maintainer} }) {
321         my $versions = $by_maint{$maintainer}{$source}
322             || $by_uploader{$maintainer}{$source};
323         for my $version (keys %$versions) {
324             my $tags = $versions->{$version};
325             for my $tag (@$tags) {
326                 $error_clean = 0 if ($tag->{code} eq 'E');
327                 $error_clean = 0 if ($tag->{code} eq 'W');
328             }
329         }
330     }
331
332     # Determine the parts of the maintainer and the file name for the
333     # maintainer page.
334     my ($name, $email) = ($maintainer =~ /^(.*) <([^>]+)>/);
335     $name = 'Unknown Maintainer' unless $name;
336     $email = 'unknown' unless $email;
337     my $regular = "maintainer/$id";
338     my $full = "full/$id";
339
340     # Create the regular maintainer page (only errors and warnings) and the
341     # full maintainer page (all tags, including overrides and info tags).
342     print "Generating page for $id\n";
343     my %data = (
344         email      => html_quote (uri_escape ($email)),
345         errors     => 1,
346         id         => $id,
347         maintainer => html_quote ($maintainer),
348         name       => html_quote ($name),
349         packages   => $by_maint{$maintainer},
350         uploads    => $by_uploader{$maintainer},
351     );
352     my $template;
353     if ($error_clean) {
354         $template = $templates{clean};
355     } else {
356         $template = $templates{maintainer};
357     }
358     output_template ($regular, $template, \%data);
359     $template = $templates{maintainer};
360     $data{errors} = 0;
361     output_template ($full, $template, \%data);
362
363     # Add this maintainer to the hash of maintainer to URL mappings.
364     $maintainers{$maintainer} = $id;
365 }
366
367 # Write out the maintainer index.
368 my %data = (
369     maintainers => \%maintainers,
370 );
371 output_template ('maintainers.html', $templates{maintainers}, \%data);
372
373 # Write out the QA package list.
374 open (QA, '>', "$HTML_TMP_DIR/qa-list.txt")
375     or die "cannot create qa-list.txt: $!\n";
376 for my $source (sort keys %qa) {
377     print QA "$source $qa{$source}[0] $qa{$source}[1]\n";
378 }
379 close QA or die "cannot write to qa-list: $!\n";
380
381 # Now, generate stub pages for every maintainer who has only clean packages.
382 for my $id (keys %clean) {
383     my $maintainer = $clean{$id};
384     my ($name, $email) = ($maintainer =~ /^(.*) <([^>]+)>/);
385     $email = 'unknown' unless $email;
386     my %data = (
387         email      => html_quote (uri_escape ($email)),
388         maintainer => html_quote ($maintainer),
389         name       => html_quote ($name),
390     );
391     print "Generating clean page for $id\n";
392     output_template ("maintainer/$id", $templates{clean}, \%data);
393     output_template ("full/$id", $templates{clean}, \%data);
394 }
395
396 # Create the pages for each tag.  Each page shows the extended description for
397 # the tag and all the packages for which that tag was issued.
398 for my $tag (sort keys %by_tag) {
399     my $description;
400     if ($tag_info{$tag}) {
401         $description = wrap_paragraphs('HTML', '    ', $tag_info{$tag});
402     } else {
403         $description = "    <p>Can't find description of tag $tag.</p>";
404     }
405     my $code = 'O';
406     foreach (@{$by_tag{$tag}}) {
407         if ($_->{code} ne 'O') {
408             $code = $_->{code};
409             last;
410         }
411     }
412     my %data = (
413         description => $description,
414         tag         => html_quote ($tag),
415         code        => $code,
416         tags        => $by_tag{$tag},
417     );
418     output_template ("tags/$tag.html", $templates{tag}, \%data);
419 }
420
421 # Create the general tag index.
422 %data = (
423     tags      => \%by_tag,
424 );
425 output_template ('tags.html', $templates{tags}, \%data);
426
427 # Generate the package lists.  These are huge, so we break them into four
428 # separate pages.
429 #
430 # FIXME: Does anyone actually use these pages?  They're basically unreadable.
431 my %list;
432 $list{'0-9, A-F'} = [];
433 $list{'G-L'}      = [];
434 $list{'M-R'}      = [];
435 $list{'S-Z'}      = [];
436 for my $package (sort keys %packages) {
437     my $first = uc substr($package, 0, 1);
438     if    ($first le 'F') { push(@{ $list{'0-9, A-F'} }, $package) }
439     elsif ($first le 'L') { push(@{ $list{'G-L'} },      $package) }
440     elsif ($first le 'R') { push(@{ $list{'M-R'} },      $package) }
441     else                  { push(@{ $list{'S-Z'} },      $package) }
442 }
443 %data = (
444     packages  => \%packages,
445 );
446 my $i = 1;
447 for my $section (sort keys %list) {
448     $data{section} = $section;
449     $data{list} = $list{$section};
450     output_template ("packages_$i.html", $templates{packages}, \%data);
451     $i++;
452 }
453
454 # Finally, we can start creating the index page.  First, read in the old
455 # statistics file so that we can calculate deltas for all of our statistics.
456 my $old_statistics;
457 if (-f $statistics_file) {
458     ($old_statistics) = read_dpkg_control($statistics_file);
459 }
460 my %delta;
461 my @attrs = qw(maintainers source-packages binary-packages udeb-packages
462                errors warnings info experimental overridden);
463 for my $attr (@attrs) {
464     my $old = $old_statistics->{$attr} || 0;
465     $statistics{$attr} ||= 0;
466     $delta{$attr} = sprintf("%d (%+d)", $statistics{$attr},
467                             $statistics{$attr} - $old);
468 }
469
470 # Update the statistics file.
471 open (STATS, '>', $statistics_file)
472     or die "cannot open $statistics_file for writing: $!\n";
473 print STATS "last-updated: $timestamp\n";
474 print STATS "mirror-timestamp: $mirror_timestamp\n";
475 for my $attr (@attrs) {
476     print STATS "$attr: $statistics{$attr}\n";
477 }
478 print STATS "lintian-version: $LINTIAN_VERSION\n";
479 close STATS or die "cannot write to $statistics_file: $!\n";
480
481 # Create the main page.
482 %data = (
483     architecture => $LINTIAN_ARCH,
484     delta        => \%delta,
485     dist         => $LINTIAN_DIST,
486     mirror       => $mirror_timestamp,
487     previous     => $old_statistics->{'last-updated'},
488     section      => $LINTIAN_SECTION,
489 );
490 output_template ('index.html', $templates{index}, \%data);
491 exit 0;
492
493 # ------------------------------
494 # Utility functions
495
496 # Determine the file name for the maintainer page given a maintainer.  It
497 # should be <email>.html where <email> is their email address with all
498 # characters other than a-z A-Z 0-9 - _ . @ = + replaced with _.  Don't change
499 # this without coordinating with QA.
500 sub maintainer_url {
501     my ($maintainer) = @_;
502     my ($email) = ($maintainer =~ /<([^>]+)>/);
503     my ($regular, $full);
504     if ($email) {
505         my $id = $email;
506         $id =~ tr/a-zA-Z0-9_.@=+-/_/c;
507         return "$id.html";
508     } else {
509         return 'unsorted.html';
510     }
511 }
512
513 # Deduplicate maintainers.  Maintains a cache of the maintainers we've seen
514 # with a given e-mail address, issues a warning if two maintainers have the
515 # same e-mail address, and returns the maintainer string that we should use
516 # (which is whatever maintainer we saw first with that e-mail).
517 {
518     my (%urlmap, %warned);
519     sub map_maintainer {
520         my ($maintainer) = @_;
521         my $url = maintainer_url ($maintainer);
522         if ($urlmap{$url} && $urlmap{$url} ne $maintainer) {
523             warn "$maintainer has the same page as $urlmap{$url}\n"
524                 unless ($warned{$maintainer}
525                         || lc ($maintainer) eq lc ($urlmap{$url})
526                         || $maintainer =~ /\@lists\.(alioth\.)?debian\.org>/);
527             $warned{$maintainer}++;
528             $maintainer = $urlmap{$url};
529         } else {
530             $urlmap{$url} = $maintainer;
531         }
532         return $maintainer;
533     }
534 }
535
536 # Quote special characters for HTML output.
537 sub html_quote {
538     my ($text) = @_;
539     $text ||= '';
540     $text =~ s/&/\&amp;/g;
541     $text =~ s/</\&lt;/g;
542     $text =~ s/>/\&gt;/g;
543     return $text;
544 }
545
546 # Given a file name, a template, and a data hash, fill out the template with
547 # that data hash and output the results to the file.
548 sub output_template {
549     my ($file, $template, $data) = @_;
550     $data->{version} ||= $LINTIAN_VERSION;
551     $data->{timestamp} ||= $timestamp;
552     $data->{head} ||= sub { $templates{head}->fill_in (HASH => { page_title => $_[0],
553                                                                  path_prefix => '../' x ($_[1]||0),
554                                                                  %$data }) };
555     $data->{foot} ||= sub { $templates{foot}->fill_in (HASH => $data) };
556     open (OUTPUT, '>', "$HTML_TMP_DIR/$file")
557         or die "creating $HTML_TMP_DIR/$file falied: $!\n";
558     $template->fill_in (OUTPUT => \*OUTPUT, HASH => $data)
559         or die "filling out $file failed: $Text::Template::ERROR\n";
560     close OUTPUT;
561 }
562
563 # Local Variables:
564 # indent-tabs-mode: nil
565 # cperl-indent-level: 4
566 # End:
567 # vim: syntax=perl sw=4 sts=4 ts=4 et shiftround