#! /usr/bin/perl

# Build some of the Autoconf test files.

# Copyright (C) 2000-2017, 2020-2021 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/>.

use 5.006;
use strict;
use warnings FATAL => 'all';

# ac_exclude_list
# ---------------
# Check all AC_DEFUN'ed macros with AT_CHECK_MACRO, except these.
# Not every macro can be run without arguments, and some are already
# tested elsewhere.
my @ac_exclude_list = (
  # Internal macros should not be invoked directly from a configure.ac.
  qr/^_?_AC_/,

  # Used in many places.
  qr/^AC_.*_IFELSE$/,
  qr/^AC_LANG/,
  qr/^AC_RUN_LOG$/,
  qr/^AC_TRY/,

  # Need an argument.
  qr/^AC_(CANONICALIZE|PREFIX_PROGRAM|PREREQ)$/,
  qr/^AC_(SEARCH_LIBS|REPLACE_FUNCS)$/,
  qr/^AC_(CACHE_CHECK|COMPUTE)_INT$/,
  qr/^AC_ARG_VAR$/,
  qr/^AC_REQUIRE_SHELL_FN$/,

  # Performed in the semantics tests.
  qr/^AC_CHECK_(
        ALIGNOF|DECL|FILE|FUNC|HEADER|LIB|MEMBER
       |PROG|SIZEOF|(TARGET_)?TOOL|TYPE
     )S?$/x,
  qr/^AC_PATH_PROGS_FEATURE_CHECK$/,

  # Fail when the source does not exist.
  qr/^AC_CONFIG/,

  # AU defined to nothing.
  qr/^AC_(CYGWIN|CYGWIN32|EMXOS2|MING32|EXEEXT|OBJEXT)$/,

  # Produce "= val" because $1, the variable used to store the result,
  # is empty.
  qr/^AC_(F77|FC)_FUNC$/,
  qr/^AC_FC_(PP_)?SRCEXT$/,
  qr/^AC_PATH_((TARGET_)?TOOL|PROG)S?$/,

  # Is a number.
  qr/^AC_FD_CC$/,

  # Obsolete, but needs to be AC_DEFUNed and cannot safely be called
        # without arguments.  Tested in tools.at.
  qr/^AC_(DIAGNOSE|FATAL|FOREACH|OBSOLETE|WARNING)$/,

  # Require a file that is not shipped with Autoconf.  But it should.
  qr/^AC_FUNC_(GETLOADAVG|FNMATCH_GNU)$/,
  qr/^AC_REPLACE_FNMATCH$/,

  # Obsolete, checked in semantics.
  qr/^AC_FUNC_WAIT3$/,
  qr/^AC_FUNC_SETVBUF_REVERSED$/,
  qr/^AC_SYS_RESTARTABLE_SYSCALLS$/,

  # Not intended to be invoked at the top level.
  qr/^AC_INCLUDES_DEFAULT$/,

  # AC_INIT includes all the AC_INIT macros.
  # There is an infinite m4 recursion if AC_INIT is used twice.
  qr/^AC_INIT/,

  # Checked in semantics.
  qr/^AC_(PROG_CC|C_CONST|C_VOLATILE)$/,
  qr/^AC_PATH_XTRA$/,

  # Use without an argument is obsolete.
  # Checked in semantics.
  qr/^AC_PROG_LEX$/,

  # Requires a working C++ compiler, which is not a given.
  qr/^AC_PROG_CXX_C_O$/,

  # Already tested by AT_CHECK_MACRO.
  qr/^AC_OUTPUT$/,

  # Tested alongside m4_divert_text.
  qr/^AC_PRESERVE_HELP_ORDER$/,

  # Tested in erlang.at.
  qr/^AC_ERLANG_SUBST_(INSTALL_LIB_SUBDIR|ROOT_DIR)$/,
  qr/^AC_ERLANG_CHECK_LIB$/,
);

# au_exclude_list
# ---------------
# Check all AU_DEFUN'ed macros with AT_CHECK_AU_MACRO, except these.
my @au_exclude_list = (
  # Empty.
  qr/^AC_(C_CROSS|PROG_CC_(C[89]9|STDC))$/,

  # Use AC_REQUIRE.
  qr/^AC_(CYGWIN|MINGW32|EMXOS2)$/,

  # Already in configure.ac.
  qr/^AC_(INIT|OUTPUT)$/,

  # AC_LANG_SAVE needs user interaction to be removed.
  # AC_LANG_RESTORE cannot be used alone.
  qr/^AC_LANG_(SAVE|RESTORE)$/,

  # Need arguments.  Tested in tools.at.
  qr/^AC_(DIAGNOSE|FATAL|OBSOLETE|WARNING)$/,
  qr/^AC_(FOREACH|LINK_FILES|PREREQ)$/,

  # Need arguments.  Tested in semantics.at.
  qr/^AC_HAVE_LIBRARY$/,
  qr/^AC_COMPILE_CHECK$/,
  qr/^AC_TRY_(COMPILE|CPP|LINK|RUN)$/,

  # Not macros, just mapping from old variable name to a new one.
  qr/^ac_cv_prog_(gcc|gxx|g77)$/,
);


# test_parameters
# ---------------
# Extra arguments to pass to the test macro for particular macros.
# Keys are macro names, values are records containing one or more
# of the possible optional arguments to AT_CHECK_(AU_)MACRO:
# macro_use, additional_commands, autoconf_flags, test_parameters => '...'.
# Entries in this hash are grouped by common situations, and sorted
# alphabetically within each group.
# Note that you must provide M4 quotation; emit_test will not quote
# the arguments for you.  (This is so you can _not_ quote the arguments
# when that's useful.)

my %test_parameters = (
  # Uses AC_RUN_IFELSE, cross-compilation test fails.
  AC_FC_CHECK_BOUNDS      => { test_parameters => '[no-cross]' },
  AC_FUNC_CHOWN           => { test_parameters => '[no-cross]' },
  AC_FUNC_FNMATCH         => { test_parameters => '[no-cross]' },
  AC_FUNC_FORK            => { test_parameters => '[no-cross]' },
  AC_FUNC_GETGROUPS       => { test_parameters => '[no-cross]' },
  AC_FUNC_LSTAT           => { test_parameters => '[no-cross]' },
  AC_FUNC_MALLOC          => { test_parameters => '[no-cross]' },
  AC_FUNC_MEMCMP          => { test_parameters => '[no-cross]' },
  AC_FUNC_MKTIME          => { test_parameters => '[no-cross]' },
  AC_FUNC_MMAP            => { test_parameters => '[no-cross]' },
  AC_FUNC_REALLOC         => { test_parameters => '[no-cross]' },
  AC_FUNC_STAT            => { test_parameters => '[no-cross]' },
  AC_FUNC_STRCOLL         => { test_parameters => '[no-cross]' },
  AC_FUNC_STRNLEN         => { test_parameters => '[no-cross]' },
  AC_FUNC_STRTOD          => { test_parameters => '[no-cross]' },

  # Different result with a C++ compiler than a C compiler:
  # C++ compilers may or may not support these features from C1999 and later.
  AC_C_RESTRICT => {
    test_parameters => ('[cxx_define_varies:restrict' .
                        ' cxx_cv_varies:cxx_restrict]')
  },
  AC_C_TYPEOF => {
    test_parameters => ('[cxx_define_varies:typeof' .
                        ' cxx_define_varies:HAVE_TYPEOF' .
                        ' cxx_cv_varies:cxx_typeof]')
  },
  AC_C__GENERIC => {
    test_parameters => ('[cxx_define_varies:HAVE_C__GENERIC' .
                        ' cxx_cv_varies:cxx__Generic]')
  },
  AC_C_VARARRAYS => {
    test_parameters => ('[cxx_define_varies:HAVE_C_VARARRAYS' .
                        ' cxx_define_varies:__STDC_NO_VLA__' .
                        ' cxx_cv_varies:cxx_vararrays]')
  },

  # stdbool.h is supposed to be includeable from C++, per C++2011
  # [support.runtime], but the type _Bool was not added to the C++
  # standard, so it may or may not be present depending on how much
  # the C++ compiler cares about C source compatibility.
  AC_CHECK_HEADER_STDBOOL => {
    test_parameters => ('[cxx_define_varies:HAVE__BOOL' .
                        ' cxx_cv_varies:type__Bool]')
  },
  AC_HEADER_STDBOOL => {
    test_parameters => ('[cxx_define_varies:HAVE__BOOL' .
                        ' cxx_cv_varies:type__Bool]')
  },

  # G++ forces -D_GNU_SOURCE which, with some versions of GNU libc,
  # changes the declaration of strerror_r.  Blech.
  AC_FUNC_STRERROR_R => {
    test_parameters => ('[cxx_define_varies:STRERROR_R_CHAR_P' .
                        ' cxx_cv_varies:func_strerror_r_char_p]')
  },
);

# skip_macro MACRO, EXCLUDE-LIST
# ------------------------------
# Returns truthy if any of the regexes in EXCLUDE-LIST match MACRO.
sub skip_macro
{
  my $macro = shift;
  for my $pat (@_)
    {
      return 1 if $macro =~ $pat;
    }
  return 0;
}


# emit_test FH, TEST-MACRO, MACRO
# --------------------------------
# Emit code to FH to test MACRO using TEST-MACRO.
# TEST-MACRO is expected to be either AT_CHECK_MACRO or AT_CHECK_AU_MACRO;
# see local.at.
sub emit_test
{
  my ($fh, $test_macro, $macro) = @_;
  my $params = $test_parameters{$macro} || {};
  my $macro_use           = ${$params}{macro_use}           || '';
  my $additional_commands = ${$params}{additional_commands} || '';
  my $autoconf_flags      = ${$params}{autoconf_flags}      || '';
  my $test_parameters     = ${$params}{test_parameters}     || '';

  $autoconf_flags = '[]'
    if $autoconf_flags eq '' && $test_parameters ne '';
  $additional_commands = '[]'
    if $additional_commands eq '' && $autoconf_flags ne '';
  $macro_use = '[]'
    if $macro_use eq '' && $additional_commands ne '';

  print $fh "$test_macro([$macro]";
  print $fh ", $autoconf_flags" if $autoconf_flags ne '';
  print $fh ", $additional_commands" if $additional_commands ne '';
  print $fh ", $autoconf_flags" if $autoconf_flags ne '';
  print $fh ", $test_parameters" if $test_parameters ne '';
  print $fh ")\n";
}

# scan_m4_files
# -------------
# Scan all of the Autoconf source files and produce lists of macros
# to be tested
sub scan_m4_files
{
  my @macros_to_test;
  my %required_macros;

  for my $file (@_)
    {
      open my $fh, "<", $file
        or die "$file: $!\n";

      my (@ac_macros, @au_macros);
      while (<$fh>)
        {
          chomp;
          s/\bdnl\b.*$//;
          if (/\bAC_REQUIRE\(\[*([a-zA-Z0-9_]+)/)
            {
              $required_macros{$1} = 1;
            }
          elsif (/^AC_DEFUN(?:_ONCE)?\(\[*([a-zA-Z0-9_]+)/)
            {
              push @ac_macros, $1
                unless skip_macro $1, @ac_exclude_list;
            }
          elsif (/^AU_DEFUN?\(\[*([a-zA-Z0-9_]+)/)
            {
              push @au_macros, $1
                unless skip_macro $1, @au_exclude_list;
            }
        }
      push @macros_to_test, [ $file, \@ac_macros, \@au_macros ];
    }

  # Filter out macros that are AC_REQUIREd by some other macro;
  # it's not necessary to test them directly.
  my @pruned_macros_to_test;
  for my $elt (@macros_to_test)
    {
      my ($file, $ac_macros, $au_macros) = @$elt;
      my (@pruned_ac_macros, @pruned_au_macros);

      for my $macro (@$ac_macros)
        {
          push @pruned_ac_macros, $macro
            unless defined $required_macros{$macro};
        }
      for my $macro (@$au_macros)
        {
          push @pruned_au_macros, $macro
            unless defined $required_macros{$macro};
        }

      push @pruned_macros_to_test, [
        $file, \@pruned_ac_macros, \@pruned_au_macros
      ];
    }

  return @pruned_macros_to_test;
}


# make_read_only FILE
# -------------------
# Make FILE read-only on disk.  Also clears the execute and special bits.
sub make_read_only
{
  my $f = shift;
  my $mode = (stat $f)[2];
  die "stat($f): $!\n" unless defined $mode;
  # clear all the bits in $mode except r--r--r--
  $mode &= 00444;
  chmod $mode, $f or die "making $f read-only: $!\n";
}


# create_test_files OUTDIR, M4-FILES...
# -----------------
# Main loop: for each file listed in M4-FILES, generate an .at file
# named "ac${file}.at" that does cursory tests on each of the macros
# defined in $file.
sub create_test_files
{
  my $outdir = shift;
  if (@_ == 0)
    {
      print STDERR "usage: $0 outdir m4-files...\n";
      exit 1;
    }

  for my $elt (scan_m4_files @_)
    {
      my ($file, $ac_macros, $au_macros) = @$elt;

      my $base = $file;
      $base =~ s|^.*/([^/.]+)(?:.[^/]*)?$|$1|;

      my $tmpout = "${outdir}/ac${base}.tat";
      my $out = "${outdir}/ac${base}.at";

      open my $fh, ">", $tmpout
        or die "$tmpout: $!\n";

      print $fh <<"EOF";
# -*- autotest -*-
# Generated by $0 from $file.
# Do not edit this file by hand.

EOF

      if (@$ac_macros || @$au_macros)
        {
          print $fh "AT_BANNER([Testing autoconf/$base macros.])\n";

          if (@$ac_macros)
            {
              print $fh "\n# Modern macros.\n";
              emit_test ($fh, 'AT_CHECK_MACRO', $_)
                for sort @$ac_macros;
            }
          if (@$au_macros)
            {
              print $fh "\n# Obsolete macros.\n";
              emit_test ($fh, 'AT_CHECK_AU_MACRO', $_)
                for sort @$au_macros;
            }
        }
      else
        {
          print $fh "# Nothing to test.\n";
        }

      close $fh or die "writing to $tmpout: $!\n";
      make_read_only $tmpout;
      rename $tmpout, $out or die "updating $out: $!\n";
    }
}

create_test_files @ARGV;
exit 0;

### 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:
