announce-gen 16 KB

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