Skip to content

Commit

Permalink
Generate manpages directly from source code.
Browse files Browse the repository at this point in the history
We generate manpages for autoconf’s installed programs (autoconf,
autoheader, etc.) using help2man, which runs each program in order to
learn its --help output.  Each manpage therefore has a dependency on
the existence of the corresponding program, but this dependency is
intentionally left out of the Makefile so that one can build from a
tarball release (which will include prebuilt manpages) without having
help2man installed.

But when building from a git checkout with high levels of
parallelism (-j20 or so), the missing dependency can lead to build
failures, because help2man will try to run the program before it
exists.  In an earlier patch I tried to work around this with a
recursive make invocation in the ‘.x.1’ rule, to ensure the existence
of the program.  That only traded one concurrency bug for another, now
we could have two jobs trying to build the same program simultaneously
and they would clobber each other’s work and the build would still
fail.

Instead, this patch introduces a utility script ‘help-extract.pl’ that
reads --help and --version information directly from the source code
for each program.  This utility, wrapped appropriately for each
program, is what help2man now runs.  Usage is a little weird because
help2man doesn’t let you specify any arguments to the “executable”
that it runs, but it works, and lets us write all of the true
dependencies of each manpage into the Makefile without naming any file
that would be created during a build from a tarball.  help-extract.pl
is a Perl script, so it introduces no new build-time requirements.

A downside is that we have to make sure each of the script sources in
bin/, and also part of lib/Autom4te/ChannelDefs.pm, are parseable by
help-extract.  The most important constraints are that the text output
by --help must be defined in a global variable named ‘help’, and its
definition has to be formatted just the way these definitions are
currently formatted.  Similarly for --version.  Furthermore, only some
non-literal substitutions are possible in these texts; each has to be
explicitly supported in help-extract.pl.  The current list of supported
substitutions is $0, @PACKAGE_NAME@, @Version@, @RELEASE_YEAR@, and
Autom4te::ChannelDefs::usage.

The generated manpages themselves are character-for-character
identical before and after this patch.

 * build-aux/help-extract.pl: New build script that extracts --help
  and --version output from manpages.

 * man/autoconf.w, man/autoheader.w, man/autom4te.w, man/autoreconf.w
 * man/autoscan.w, man/autoupdate.w, man/ifnames.w: New shell scripts
   which wrap build-aux/help-extract.pl.

 * man/local.mk: Generate each manpage by running help2man on the
   corresponding .w script, not on the built utility itself.
   Revise all dependencies to match.

 * bin/autoconf.as: Rename ‘usage’ variable to ‘help’ and
   ‘help’ variable to ‘usage_err’.
 * bin/autoheader.in: Call Autom4te::ChannelDefs::usage with no
   function-call parentheses, matching all the other scripts.
 * bin/autom4te.in: Initialize $version with a regular double-quoted
   string, not a heredoc, matching all the other scripts.
 * bin/autoscan.in: Remove global variable $configure_scan.
  • Loading branch information
zackw committed Aug 21, 2020
1 parent acf6fb0 commit 9b5c0f1
Show file tree
Hide file tree
Showing 15 changed files with 386 additions and 53 deletions.
1 change: 1 addition & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ EXTRA_DIST = ChangeLog.0 ChangeLog.1 ChangeLog.2 ChangeLog.3 \
build-aux/git-version-gen \
build-aux/announce-gen build-aux/gnupload \
build-aux/gitlog-to-changelog \
build-aux/help-extract.pl \
.prev-version .version

edit = sed \
Expand Down
12 changes: 6 additions & 6 deletions bin/autoconf.as
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ m4_divert_push([HEADER-COPYRIGHT])dnl
m4_divert_pop([HEADER-COPYRIGHT])dnl back to BODY
AS_ME_PREPARE[]dnl

usage=["\
help=["\
Usage: $0 [OPTION]... [TEMPLATE-FILE]
Generate a configuration script from a TEMPLATE-FILE if given, or
Expand Down Expand Up @@ -75,11 +75,11 @@ There is NO WARRANTY, to the extent permitted by law.
Written by David J. MacKenzie and Akim Demaille."]

help="\
usage_err="\
Try '$as_me --help' for more information."

exit_missing_arg='
m4_bpatsubst([AS_ERROR([option '$[1]' requires an argument$as_nl$help])],
m4_bpatsubst([AS_ERROR([option '$[1]' requires an argument$as_nl$usage_err])],
['], ['\\''])'
# restore font-lock: '

Expand All @@ -100,7 +100,7 @@ while test $# -gt 0 ; do
--version | -V )
echo "$version" ; exit ;;
--help | -h )
AS_ECHO(["$usage"]); exit ;;
AS_ECHO(["$usage_err"]); exit ;;

--verbose | -v )
verbose=:
Expand Down Expand Up @@ -154,7 +154,7 @@ while test $# -gt 0 ; do
break ;;
-* )
exec >&2
AS_ERROR([invalid option '$[1]'$as_nl$help]) ;;
AS_ERROR([invalid option '$[1]'$as_nl$usage_err]) ;;
* )
break ;;
esac
Expand All @@ -178,7 +178,7 @@ case $# in
1)
infile=$1 ;;
*) exec >&2
AS_ERROR([invalid number of arguments$as_nl$help]) ;;
AS_ERROR([invalid number of arguments$as_nl$usage_err]) ;;
esac

# Unless specified, the output is stdout.
Expand Down
2 changes: 1 addition & 1 deletion bin/autoheader.in
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ or else 'configure.in'.
-f, --force consider all files obsolete
-W, --warnings=CATEGORY report the warnings falling in CATEGORY
" . Autom4te::ChannelDefs::usage () . "
" . Autom4te::ChannelDefs::usage . "
Library directories:
-B, --prepend-include=DIR prepend directory DIR to search path
Expand Down
5 changes: 2 additions & 3 deletions bin/autom4te.in
Original file line number Diff line number Diff line change
Expand Up @@ -204,16 +204,15 @@ General help using GNU software: <https://www.gnu.org/gethelp/>.

# $VERSION
# --------
$version = <<"EOF";
autom4te (@PACKAGE_NAME@) @VERSION@
$version = "autom4te (@PACKAGE_NAME@) @VERSION@
Copyright (C) @RELEASE_YEAR@ Free Software Foundation, Inc.
License GPLv3+/Autoconf: GNU GPL version 3 or later
<https://gnu.org/licenses/gpl.html>, <https://gnu.org/licenses/exceptions.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Written by Akim Demaille.
EOF
";


## ---------- ##
Expand Down
5 changes: 2 additions & 3 deletions bin/autoscan.in
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ my %needed_macros =
'AC_PREREQ' => [$me],
);

my $configure_scan = 'configure.scan';
my $log;

# Autoconf and lib files.
Expand All @@ -103,7 +102,7 @@ $help = "Usage: $0 [OPTION]... [SRCDIR]
Examine source files in the directory tree rooted at SRCDIR, or the
current directory if none is given. Search the source files for
common portability problems, check for incompleteness of
'configure.ac', and create a file '$configure_scan' which is a
'configure.ac', and create a file 'configure.scan' which is a
preliminary 'configure.ac' for that package.
-h, --help print this help, then exit
Expand Down Expand Up @@ -462,7 +461,7 @@ sub output_kind ($$)
if exists $kind_comment{$kind};
foreach my $word (sort keys %{$used{$kind}})
{
# Output the needed macro invocations in $configure_scan if not
# Output the needed macro invocations in configure.scan if not
# already printed, and remember these macros are needed.
foreach my $macro (@{$macro{$kind}{$word}})
{
Expand Down
275 changes: 275 additions & 0 deletions build-aux/help-extract.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
# help-extract -- extract --help and --version output from a script.
# Copyright (C) 2020 Free Software Foundation, Inc.

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

# Written by Zack Weinberg.

use strict;
use warnings;

# File::Spec itself was added in 5.005.
# File::Spec::Functions was added in 5.6.1 which is just barely too new.
use File::Spec;

# This script is not intended to be used directly. It's run by
# help2man via wrappers in man/, e.g. man/autoconf.w, as if it were
# one of autoconf's executable scripts. It extracts the --help and
# --version output of that script from its source form, without
# actually running it. The script to work from is set by the wrapper,
# and several other parameters are passed down from the Makefile as
# environment variables; see parse_args below.

# The point of this script is, the preprocessed forms of the
# executable scripts, and their wrappers for uninstalled use
# (e.g. <build-dir>/{bin,tests}/autoconf) do not need to exist to
# generate the corresponding manpages. This is desirable because we
# can't put those dependencies in the makefiles without breaking
# people's ability to build autoconf from a release tarball without
# help2man installed. It also ensures that we will generate manpages
# from the current source code and not from an older version of the
# script that has already been installed.

## ----------------------------- ##
## Extraction from Perl scripts. ##
## ----------------------------- ##

sub eval_qq_no_interpolation ($)
{
# The argument is expected to be a "double quoted string" including the
# leading and trailing delimiters. Returns the text of this string after
# processing backslash escapes but NOT interpolation.
# / (?<!\\) (?>\\\\)* blah /x means match blah preceded by an
# *even* number of backslashes. It would be nice if we could use \K
# to exclude the backslashes from the matched text, but that was only
# added in Perl 5.10 and we still support back to 5.006.
return eval $_[0] =~ s/ (?<!\\) (?>\\\\)* [\$\@] /\\$&/xrg;
}

sub extract_channeldefs_usage ($)
{
my ($channeldefs_pm) = @_;
my $usage = "";
my $parse_state = 0;
local $_;

open (my $fh, "<", $channeldefs_pm) or die "$channeldefs_pm: $!\n";
while (<$fh>)
{
if ($parse_state == 0)
{
$parse_state = 1 if /^sub usage\b/;
}
elsif ($parse_state == 1)
{
if (s/^ return "//)
{
$parse_state = 2;
$usage .= $_;
}
}
elsif ($parse_state == 2)
{
if (s/(?<!\\) ((?>\\\\)*) "; $/$1/x)
{
$usage .= $_;
return $usage;
}
else
{
$usage .= $_;
}
}
}

die "$channeldefs_pm: unexpected EOF in state $parse_state\n";
}

sub extract_perl_assignment (*$$$)
{
my ($fh, $source, $channeldefs_pm, $what) = @_;
my $value = "";
my $parse_state = 0;
local $_;

while (<$fh>)
{
if ($parse_state == 0)
{
if (s/^\$\Q${what}\E = (?=")//o)
{
$value .= $_;
$parse_state = 1;
}
}
elsif ($parse_state == 1)
{
if (/^"\s*\.\s*Autom4te::ChannelDefs::usage\s*(?:\(\))?\s*\.\s*"$/)
{
$value .= extract_channeldefs_usage ($channeldefs_pm);
}
elsif (/^";$/)
{
$value .= '"';
return eval_qq_no_interpolation ($value);
}
else
{
$value .= $_;
}
}
}

die "$source: unexpected EOF in state $parse_state\n";
}


## ------------------------------ ##
## Extraction from shell scripts. ##
## ------------------------------ ##

sub extract_shell_assignment (*$$)
{
my ($fh, $source, $what) = @_;
my $value = "";
my $parse_state = 0;
local $_;

while (<$fh>)
{
if ($parse_state == 0)
{
if (/^\Q${what}\E=\[\"\\$/)
{
$parse_state = 1;
}
}
elsif ($parse_state == 1)
{
my $done = s/"\]$//;
$value .= $_;
if ($done)
{
# This is not strictly correct but it works acceptably
# for the escapes that actually occur in the strings
# we're extracting.
return eval_qq_no_interpolation ('"'.$value.'"');
}
}
}

die "$source: unexpected EOF in state $parse_state\n";
}


## -------------- ##
## Main program. ##
## -------------- ##


sub extract_assignment ($$$)
{
my ($source, $channeldefs_pm, $what) = @_;

open (my $fh, "<", $source) or die "$source: $!\n";

my $firstline = <$fh>;
if ($firstline =~ /\@PERL\@/ || $firstline =~ /-\*-\s*perl\s*-\*-/i)
{
return extract_perl_assignment ($fh, $source, $channeldefs_pm, $what);
}
elsif ($firstline =~ /\bAS_INIT\b/
|| $firstline =~ /bin\/[a-z0-9]*sh\b/
|| $firstline =~ /-\*-\s*shell-script\s*-\*-/i)
{
return extract_shell_assignment ($fh, $source, $what);
}
else
{
die "$source: language not recognized\n";
}
}

sub main ()
{
# Most of our arguments come from environment variables, because
# help2man doesn't allow for passing additional command line
# arguments to the wrappers, and it's easier to write the wrappers
# to not mess with the command line.
my $usage = "Usage: $0 script-source (--help | --version)
Extract help and version information from a perl or shell script.
Required environment variables:
top_srcdir relative path from cwd to the top of the source tree
channeldefs_pm relative path from top_srcdir to ChannelDefs.pm
PACKAGE_NAME the autoconf PACKAGE_NAME substitution variable
VERSION the autoconf VERSION substitution variable
RELEASE_YEAR the autoconf RELEASE_YEAR substitution variable
The script-source argument should also be relative to top_srcdir.
";

my $source = shift(@ARGV) || die $usage;
my $what = shift(@ARGV) || die $usage;
my $top_srcdir = $ENV{top_srcdir} || die $usage;
my $channeldefs_pm = $ENV{channeldefs_pm} || die $usage;
my $package_name = $ENV{PACKAGE_NAME} || die $usage;
my $version = $ENV{VERSION} || die $usage;
my $release_year = $ENV{RELEASE_YEAR} || die $usage;

if ($what eq "-h" || $what eq "--help")
{
$what = "help";
}
elsif ($what eq "-V" || $what eq "--version")
{
$what = "version";
}
else
{
die $usage;
}

my $cmd_name = $source =~ s{^.*/([^./]+)\.(?:as|in)$}{$1}r;
$source = File::Spec->catfile($top_srcdir, $source);
$channeldefs_pm = File::Spec->catfile($top_srcdir, $channeldefs_pm);

my $text = extract_assignment ($source, $channeldefs_pm, $what);
$text =~ s/\$0\b/$cmd_name/g;
$text =~ s/[@]PACKAGE_NAME@/$package_name/g;
$text =~ s/[@]VERSION@/$version/g;
$text =~ s/[@]RELEASE_YEAR@/$release_year/g;
print $text;
}

main;


### Setup "GNU" style for perl-mode and cperl-mode.
## Local Variables:
## perl-indent-level: 2
## perl-continued-statement-offset: 2
## perl-continued-brace-offset: 0
## perl-brace-offset: 0
## perl-brace-imaginary-offset: 0
## perl-label-offset: -2
## cperl-indent-level: 2
## cperl-brace-offset: 0
## cperl-continued-brace-offset: 0
## cperl-label-offset: -2
## cperl-extra-newline-before-brace: t
## cperl-merge-trailing-else: nil
## cperl-continued-statement-offset: 2
## End:
4 changes: 4 additions & 0 deletions man/autoconf.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#! /bin/sh
# help-extract wrapper, see build-aux/help-extract.pl and man/local.mk
exec "${PERL-perl}" "${top_srcdir}/build-aux/help-extract.pl" \
bin/autoconf.as "$@"
4 changes: 4 additions & 0 deletions man/autoheader.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#! /bin/sh
# help-extract wrapper, see build-aux/help-extract.pl and man/local.mk
exec "${PERL-perl}" "${top_srcdir}/build-aux/help-extract.pl" \
bin/autoheader.in "$@"
Loading

0 comments on commit 9b5c0f1

Please sign in to comment.