Home | History | Annotate | Download | only in build-aux
      1 eval '(exit $?0)' && eval 'exec perl -wS "$0" ${1+"$@"}'
      2   & eval 'exec perl -wS "$0" $argv:q'
      3     if 0;
      4 # Generate a release announcement message.
      5 
      6 my $VERSION = '2012-06-08 06:53'; # UTC
      7 # The definition above must lie within the first 8 lines in order
      8 # for the Emacs time-stamp write hook (at end) to update it.
      9 # If you change this file with Emacs, please let the write hook
     10 # do its job.  Otherwise, update this string manually.
     11 
     12 # Copyright (C) 2002-2012 Free Software Foundation, Inc.
     13 
     14 # This program is free software: you can redistribute it and/or modify
     15 # it under the terms of the GNU General Public License as published by
     16 # the Free Software Foundation, either version 3 of the License, or
     17 # (at your option) any later version.
     18 
     19 # This program is distributed in the hope that it will be useful,
     20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     22 # GNU General Public License for more details.
     23 
     24 # You should have received a copy of the GNU General Public License
     25 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
     26 
     27 # Written by Jim Meyering
     28 
     29 use strict;
     30 
     31 use Getopt::Long;
     32 use Digest::MD5;
     33 eval { require Digest::SHA; }
     34   or eval 'use Digest::SHA1';
     35 use POSIX qw(strftime);
     36 
     37 (my $ME = $0) =~ s|.*/||;
     38 
     39 my %valid_release_types = map {$_ => 1} qw (alpha beta stable);
     40 my @archive_suffixes = ('tar.gz', 'tar.bz2', 'tar.lzma', 'tar.xz');
     41 my $srcdir = '.';
     42 
     43 sub usage ($)
     44 {
     45   my ($exit_code) = @_;
     46   my $STREAM = ($exit_code == 0 ? *STDOUT : *STDERR);
     47   if ($exit_code != 0)
     48     {
     49       print $STREAM "Try '$ME --help' for more information.\n";
     50     }
     51   else
     52     {
     53       my @types = sort keys %valid_release_types;
     54       print $STREAM <<EOF;
     55 Usage: $ME [OPTIONS]
     56 Generate an announcement message.  Run this from builddir.
     57 
     58 OPTIONS:
     59 
     60 These options must be specified:
     61 
     62    --release-type=TYPE          TYPE must be one of @types
     63    --package-name=PACKAGE_NAME
     64    --previous-version=VER
     65    --current-version=VER
     66    --gpg-key-id=ID         The GnuPG ID of the key used to sign the tarballs
     67    --url-directory=URL_DIR
     68 
     69 The following are optional:
     70 
     71    --news=NEWS_FILE             include the NEWS section about this release
     72                                 from this NEWS_FILE; accumulates.
     73    --srcdir=DIR                 where to find the NEWS_FILEs (default: $srcdir)
     74    --bootstrap-tools=TOOL_LIST  a comma-separated list of tools, e.g.,
     75                                 autoconf,automake,bison,gnulib
     76    --gnulib-version=VERSION     report VERSION as the gnulib version, where
     77                                 VERSION is the result of running git describe
     78                                 in the gnulib source directory.
     79                                 required if gnulib is in TOOL_LIST.
     80    --no-print-checksums         do not emit MD5 or SHA1 checksums
     81    --archive-suffix=SUF         add SUF to the list of archive suffixes
     82    --mail-headers=HEADERS       a space-separated list of mail headers, e.g.,
     83                                 To: x\@example.com Cc: y-announce\@example.com,...
     84 
     85    --help             display this help and exit
     86    --version          output version information and exit
     87 
     88 EOF
     89     }
     90   exit $exit_code;
     91 }
     92 
     93 
     94 =item C<%size> = C<sizes (@file)>
     95 
     96 Compute the sizes of the C<@file> and return them as a hash.  Return
     97 C<undef> if one of the computation failed.
     98 
     99 =cut
    100 
    101 sub sizes (@)
    102 {
    103   my (@file) = @_;
    104 
    105   my $fail = 0;
    106   my %res;
    107   foreach my $f (@file)
    108     {
    109       my $cmd = "du -h $f";
    110       my $t = `$cmd`;
    111       # FIXME-someday: give a better diagnostic, a la $PROCESS_STATUS
    112       $@
    113         and (warn "command failed: '$cmd'\n"), $fail = 1;
    114       chomp $t;
    115       $t =~ s/^\s*([\d.]+[MkK]).*/${1}B/;
    116       $res{$f} = $t;
    117     }
    118   return $fail ? undef : %res;
    119 }
    120 
    121 =item C<print_locations ($title, \@url, \%size, @file)
    122 
    123 Print a section C<$title> dedicated to the list of <@file>, which
    124 sizes are stored in C<%size>, and which are available from the C<@url>.
    125 
    126 =cut
    127 
    128 sub print_locations ($\@\%@)
    129 {
    130   my ($title, $url, $size, @file) = @_;
    131   print "Here are the $title:\n";
    132   foreach my $url (@{$url})
    133     {
    134       for my $file (@file)
    135         {
    136           print "  $url/$file";
    137           print "   (", $$size{$file}, ")"
    138             if exists $$size{$file};
    139           print "\n";
    140         }
    141     }
    142   print "\n";
    143 }
    144 
    145 =item C<print_checksums (@file)
    146 
    147 Print the MD5 and SHA1 signature section for each C<@file>.
    148 
    149 =cut
    150 
    151 sub print_checksums (@)
    152 {
    153   my (@file) = @_;
    154 
    155   print "Here are the MD5 and SHA1 checksums:\n";
    156   print "\n";
    157 
    158   foreach my $meth (qw (md5 sha1))
    159     {
    160       foreach my $f (@file)
    161         {
    162           open IN, '<', $f
    163             or die "$ME: $f: cannot open for reading: $!\n";
    164           binmode IN;
    165           my $dig =
    166             ($meth eq 'md5'
    167              ? Digest::MD5->new->addfile(*IN)->hexdigest
    168              : Digest::SHA1->new->addfile(*IN)->hexdigest);
    169           close IN;
    170           print "$dig  $f\n";
    171         }
    172     }
    173   print "\n";
    174 }
    175 
    176 =item C<print_news_deltas ($news_file, $prev_version, $curr_version)
    177 
    178 Print the section of the NEWS file C<$news_file> addressing changes
    179 between versions C<$prev_version> and C<$curr_version>.
    180 
    181 =cut
    182 
    183 sub print_news_deltas ($$$)
    184 {
    185   my ($news_file, $prev_version, $curr_version) = @_;
    186 
    187   my $news_name = $news_file;
    188   $news_name =~ s|^\Q$srcdir\E/||;
    189 
    190   print "\n$news_name\n\n";
    191 
    192   # Print all lines from $news_file, starting with the first one
    193   # that mentions $curr_version up to but not including
    194   # the first occurrence of $prev_version.
    195   my $in_items;
    196 
    197   my $re_prefix = qr/(?:\* )?(?:Noteworthy c|Major c|C)(?i:hanges)/;
    198 
    199   my $found_news;
    200   open NEWS, '<', $news_file
    201     or die "$ME: $news_file: cannot open for reading: $!\n";
    202   while (defined (my $line = <NEWS>))
    203     {
    204       if ( ! $in_items)
    205         {
    206           # Match lines like these:
    207           # * Major changes in release 5.0.1:
    208           # * Noteworthy changes in release 6.6 (2006-11-22) [stable]
    209           $line =~ /^$re_prefix.*(?:[^\d.]|$)\Q$curr_version\E(?:[^\d.]|$)/o
    210             or next;
    211           $in_items = 1;
    212           print $line;
    213         }
    214       else
    215         {
    216           # This regexp must not match version numbers in NEWS items.
    217           # For example, they might well say "introduced in 4.5.5",
    218           # and we don't want that to match.
    219           $line =~ /^$re_prefix.*(?:[^\d.]|$)\Q$prev_version\E(?:[^\d.]|$)/o
    220             and last;
    221           print $line;
    222           $line =~ /\S/
    223             and $found_news = 1;
    224         }
    225     }
    226   close NEWS;
    227 
    228   $in_items
    229     or die "$ME: $news_file: no matching lines for '$curr_version'\n";
    230   $found_news
    231     or die "$ME: $news_file: no news item found for '$curr_version'\n";
    232 }
    233 
    234 sub print_changelog_deltas ($$)
    235 {
    236   my ($package_name, $prev_version) = @_;
    237 
    238   # Print new ChangeLog entries.
    239 
    240   # First find all CVS-controlled ChangeLog files.
    241   use File::Find;
    242   my @changelog;
    243   find ({wanted => sub {$_ eq 'ChangeLog' && -d 'CVS'
    244                           and push @changelog, $File::Find::name}},
    245         '.');
    246 
    247   # If there are no ChangeLog files, we're done.
    248   @changelog
    249     or return;
    250   my %changelog = map {$_ => 1} @changelog;
    251 
    252   # Reorder the list of files so that if there are ChangeLog
    253   # files in the specified directories, they're listed first,
    254   # in this order:
    255   my @dir = qw ( . src lib m4 config doc );
    256 
    257   # A typical @changelog array might look like this:
    258   # ./ChangeLog
    259   # ./po/ChangeLog
    260   # ./m4/ChangeLog
    261   # ./lib/ChangeLog
    262   # ./doc/ChangeLog
    263   # ./config/ChangeLog
    264   my @reordered;
    265   foreach my $d (@dir)
    266     {
    267       my $dot_slash = $d eq '.' ? $d : "./$d";
    268       my $target = "$dot_slash/ChangeLog";
    269       delete $changelog{$target}
    270         and push @reordered, $target;
    271     }
    272 
    273   # Append any remaining ChangeLog files.
    274   push @reordered, sort keys %changelog;
    275 
    276   # Remove leading './'.
    277   @reordered = map { s!^\./!!; $_ } @reordered;
    278 
    279   print "\nChangeLog entries:\n\n";
    280   # print join ("\n", @reordered), "\n";
    281 
    282   $prev_version =~ s/\./_/g;
    283   my $prev_cvs_tag = "\U$package_name\E-$prev_version";
    284 
    285   my $cmd = "cvs -n diff -u -r$prev_cvs_tag -rHEAD @reordered";
    286   open DIFF, '-|', $cmd
    287     or die "$ME: cannot run '$cmd': $!\n";
    288   # Print two types of lines, making minor changes:
    289   # Lines starting with '+++ ', e.g.,
    290   # +++ ChangeLog   22 Feb 2003 16:52:51 -0000      1.247
    291   # and those starting with '+'.
    292   # Don't print the others.
    293   my $prev_printed_line_empty = 1;
    294   while (defined (my $line = <DIFF>))
    295     {
    296       if ($line =~ /^\+\+\+ /)
    297         {
    298           my $separator = "*"x70 ."\n";
    299           $line =~ s///;
    300           $line =~ s/\s.*//;
    301           $prev_printed_line_empty
    302             or print "\n";
    303           print $separator, $line, $separator;
    304         }
    305       elsif ($line =~ /^\+/)
    306         {
    307           $line =~ s///;
    308           print $line;
    309           $prev_printed_line_empty = ($line =~ /^$/);
    310         }
    311     }
    312   close DIFF;
    313 
    314   # The exit code should be 1.
    315   # Allow in case there are no modified ChangeLog entries.
    316   $? == 256 || $? == 128
    317     or warn "warning: '$cmd' had unexpected exit code or signal ($?)\n";
    318 }
    319 
    320 sub get_tool_versions ($$)
    321 {
    322   my ($tool_list, $gnulib_version) = @_;
    323   @$tool_list
    324     or return ();
    325 
    326   my $fail;
    327   my @tool_version_pair;
    328   foreach my $t (@$tool_list)
    329     {
    330       if ($t eq 'gnulib')
    331         {
    332           push @tool_version_pair, ucfirst $t . ' ' . $gnulib_version;
    333           next;
    334         }
    335       # Assume that the last "word" on the first line of
    336       # 'tool --version' output is the version string.
    337       my ($first_line, undef) = split ("\n", `$t --version`);
    338       if ($first_line =~ /.* (\d[\w.-]+)$/)
    339         {
    340           $t = ucfirst $t;
    341           push @tool_version_pair, "$t $1";
    342         }
    343       else
    344         {
    345           defined $first_line
    346             and $first_line = '';
    347           warn "$t: unexpected --version output\n:$first_line";
    348           $fail = 1;
    349         }
    350     }
    351 
    352   $fail
    353     and exit 1;
    354 
    355   return @tool_version_pair;
    356 }
    357 
    358 {
    359   # Neutralize the locale, so that, for instance, "du" does not
    360   # issue "1,2" instead of "1.2", what confuses our regexps.
    361   $ENV{LC_ALL} = "C";
    362 
    363   my $mail_headers;
    364   my $release_type;
    365   my $package_name;
    366   my $prev_version;
    367   my $curr_version;
    368   my $gpg_key_id;
    369   my @url_dir_list;
    370   my @news_file;
    371   my $bootstrap_tools;
    372   my $gnulib_version;
    373   my $print_checksums_p = 1;
    374 
    375   # Reformat the warnings before displaying them.
    376   local $SIG{__WARN__} = sub
    377     {
    378       my ($msg) = @_;
    379       # Warnings from GetOptions.
    380       $msg =~ s/Option (\w)/option --$1/;
    381       warn "$ME: $msg";
    382     };
    383 
    384   GetOptions
    385     (
    386      'mail-headers=s'     => \$mail_headers,
    387      'release-type=s'     => \$release_type,
    388      'package-name=s'     => \$package_name,
    389      'previous-version=s' => \$prev_version,
    390      'current-version=s'  => \$curr_version,
    391      'gpg-key-id=s'       => \$gpg_key_id,
    392      'url-directory=s'    => \@url_dir_list,
    393      'news=s'             => \@news_file,
    394      'srcdir=s'           => \$srcdir,
    395      'bootstrap-tools=s'  => \$bootstrap_tools,
    396      'gnulib-version=s'   => \$gnulib_version,
    397      'print-checksums!'   => \$print_checksums_p,
    398      'archive-suffix=s'   => \@archive_suffixes,
    399 
    400      help => sub { usage 0 },
    401      version => sub { print "$ME version $VERSION\n"; exit },
    402     ) or usage 1;
    403 
    404   my $fail = 0;
    405   # Ensure that each required option is specified.
    406   $release_type
    407     or (warn "release type not specified\n"), $fail = 1;
    408   $package_name
    409     or (warn "package name not specified\n"), $fail = 1;
    410   $prev_version
    411     or (warn "previous version string not specified\n"), $fail = 1;
    412   $curr_version
    413     or (warn "current version string not specified\n"), $fail = 1;
    414   $gpg_key_id
    415     or (warn "GnuPG key ID not specified\n"), $fail = 1;
    416   @url_dir_list
    417     or (warn "URL directory name(s) not specified\n"), $fail = 1;
    418 
    419   my @tool_list = split ',', $bootstrap_tools;
    420 
    421   grep (/^gnulib$/, @tool_list) ^ defined $gnulib_version
    422     and (warn "when specifying gnulib as a tool, you must also specify\n"
    423         . "--gnulib-version=V, where V is the result of running git describe\n"
    424         . "in the gnulib source directory.\n"), $fail = 1;
    425 
    426   exists $valid_release_types{$release_type}
    427     or (warn "'$release_type': invalid release type\n"), $fail = 1;
    428 
    429   @ARGV
    430     and (warn "too many arguments:\n", join ("\n", @ARGV), "\n"),
    431       $fail = 1;
    432   $fail
    433     and usage 1;
    434 
    435   my $my_distdir = "$package_name-$curr_version";
    436 
    437   my $xd = "$package_name-$prev_version-$curr_version.xdelta";
    438 
    439   my @candidates = map { "$my_distdir.$_" } @archive_suffixes;
    440   my @tarballs = grep {-f $_} @candidates;
    441 
    442   @tarballs
    443     or die "$ME: none of " . join(', ', @candidates) . " were found\n";
    444   my @sizable = @tarballs;
    445   -f $xd
    446     and push @sizable, $xd;
    447   my %size = sizes (@sizable);
    448   %size
    449     or exit 1;
    450 
    451   my $headers = '';
    452   if (defined $mail_headers)
    453     {
    454       ($headers = $mail_headers) =~ s/\s+(\S+:)/\n$1/g;
    455       $headers .= "\n";
    456     }
    457 
    458   # The markup is escaped as <\# so that when this script is sent by
    459   # mail (or part of a diff), Gnus is not triggered.
    460   print <<EOF;
    461 
    462 ${headers}Subject: $my_distdir released [$release_type]
    463 
    464 <\#secure method=pgpmime mode=sign>
    465 
    466 FIXME: put comments here
    467 
    468 EOF
    469 
    470   if (@url_dir_list == 1 && @tarballs == 1)
    471     {
    472       # When there's only one tarball and one URL, use a more concise form.
    473       my $m = "$url_dir_list[0]/$tarballs[0]";
    474       print "Here are the compressed sources and a GPG detached signature[*]:\n"
    475         . "  $m\n"
    476         . "  $m.sig\n\n";
    477     }
    478   else
    479     {
    480       print_locations ("compressed sources", @url_dir_list, %size, @tarballs);
    481       -f $xd
    482         and print_locations ("xdelta diffs (useful? if so, "
    483                              . "please tell bug-gnulib\@gnu.org)",
    484                              @url_dir_list, %size, $xd);
    485       my @sig_files = map { "$_.sig" } @tarballs;
    486       print_locations ("GPG detached signatures[*]", @url_dir_list, %size,
    487                        @sig_files);
    488     }
    489 
    490   if ($url_dir_list[0] =~ "gnu\.org")
    491     {
    492       print "Use a mirror for higher download bandwidth:\n";
    493       if (@tarballs == 1 && $url_dir_list[0] =~ m!http://ftp\.gnu\.org/gnu/!)
    494         {
    495           (my $m = "$url_dir_list[0]/$tarballs[0]")
    496             =~ s!http://ftp\.gnu\.org/gnu/!http://ftpmirror\.gnu\.org/!;
    497           print "  $m\n"
    498               . "  $m.sig\n\n";
    499 
    500         }
    501       else
    502         {
    503           print "  http://www.gnu.org/order/ftp.html\n\n";
    504         }
    505     }
    506 
    507   $print_checksums_p
    508     and print_checksums (@sizable);
    509 
    510   print <<EOF;
    511 [*] Use a .sig file to verify that the corresponding file (without the
    512 .sig suffix) is intact.  First, be sure to download both the .sig file
    513 and the corresponding tarball.  Then, run a command like this:
    514 
    515   gpg --verify $tarballs[0].sig
    516 
    517 If that command fails because you don't have the required public key,
    518 then run this command to import it:
    519 
    520   gpg --keyserver keys.gnupg.net --recv-keys $gpg_key_id
    521 
    522 and rerun the 'gpg --verify' command.
    523 EOF
    524 
    525   my @tool_versions = get_tool_versions (\@tool_list, $gnulib_version);
    526   @tool_versions
    527     and print "\nThis release was bootstrapped with the following tools:",
    528       join ('', map {"\n  $_"} @tool_versions), "\n";
    529 
    530   print_news_deltas ($_, $prev_version, $curr_version)
    531     foreach @news_file;
    532 
    533   $release_type eq 'stable'
    534     or print_changelog_deltas ($package_name, $prev_version);
    535 
    536   exit 0;
    537 }
    538 
    539 ### Setup "GNU" style for perl-mode and cperl-mode.
    540 ## Local Variables:
    541 ## mode: perl
    542 ## perl-indent-level: 2
    543 ## perl-continued-statement-offset: 2
    544 ## perl-continued-brace-offset: 0
    545 ## perl-brace-offset: 0
    546 ## perl-brace-imaginary-offset: 0
    547 ## perl-label-offset: -2
    548 ## perl-extra-newline-before-brace: t
    549 ## perl-merge-trailing-else: nil
    550 ## eval: (add-hook 'write-file-hooks 'time-stamp)
    551 ## time-stamp-start: "my $VERSION = '"
    552 ## time-stamp-format: "%:y-%02m-%02d %02H:%02M"
    553 ## time-stamp-time-zone: "UTC"
    554 ## time-stamp-end: "'; # UTC"
    555 ## End:
    556