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