announce-gen 15 KB

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