Initial module implementation

At this stage it handles Autoconf (#undef), CMake (#cmakedefine), and Meson
(#mesondefine) special lines but does not have any built-in tests.
This commit is contained in:
Boris Kolpackov 2021-11-01 13:43:47 +02:00
parent 19fcde9ddc
commit ef54e197e9
30 changed files with 929 additions and 3 deletions

View File

@ -8,8 +8,8 @@ CMake and Meson variants.
Similar to Autoconf, the module provides built-in support for a number of
common `HAVE_*` configuration options. However, the values of these options
are not discovered by probing, such as trying to compile a test program to
check if the header is present. Instead, they are set to the expected values
based on the platform/compiler macros.
are not discovered by dynamic probing, such as trying to compile a test
program to check if the header is present. Instead, they are set to static
expected values based on the platform/compiler macros.
[module-in]: https://build2.org/build2/doc/build2-build-system-manual.xhtml#module-in

24
libbuild2-autoconf-tests/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Compiler/linker output.
#
*.d
*.t
*.i
*.i.*
*.ii
*.ii.*
*.o
*.obj
*.gcm
*.pcm
*.ifc
*.so
*.dll
*.a
*.lib
*.exp
*.pdb
*.ilk
*.exe
*.exe.dlls/
*.exe.manifest
*.pc

View File

@ -0,0 +1 @@
../AUTHORS

View File

@ -0,0 +1 @@
../LICENSE

View File

@ -0,0 +1,3 @@
# libbuild2-autoconf-tests
Tests package for the Autoconf emulation build system module for `build2`.

View File

@ -0,0 +1 @@
./: testscript

View File

@ -0,0 +1,208 @@
# Note: --silent rather than --quiet to suppress module update diagnostics.
#
test.options += --no-default-options --serial-stop --silent --buildfile -
# We disable bdep auto-synchronization since we will potentially be updating
# the module from multiple parallel tests. Note that we've made sure it is
# up-to-date by pre-loading it in the tests project (see root.build for
# details).
#
+export BDEP_SYNC=0
# Note that we are using the libbuild2-autoconf-tests project as our
# amalgamation in order to get the module import location and config.c.
#
+cat <<EOI >=bootstrap.build
project = basics
#amalgamation =
subprojects =
version = 1.2.3
EOI
+cat <<EOI >=root.build
using c
using autoconf
EOI
: basics-autoconf
:
mkdir build;
ln -s ../../bootstrap.build ../../root.build build/;
cat <<EOI >=config.h.in;
#define VERSION "@version@"
#undef TRUE
#undef FALSE
#undef ONE
#undef ZERO
#undef VALUE
# undef TRUE
#undef CUSTOM_LINE
#undef CUSTOM_BLOCK
EOI
$* <<EOI &config.h &config.h.d;
./: h{config}: in{config}
{
TRUE = true
FALSE = [bool] false
ONE = 1
ZERO = [uint64] 000
VALUE = [uint64] 0123
CUSTOM_LINE = '#define CUSTOM 123'
CUSTOM_BLOCK = \
'
/* Make sure we do not redefine CUSTOM. */
#ifndef CUSTOM
# define CUSTOM 123
#endif
'
}
EOI
cat config.h >>EOO
#define VERSION "1.2.3"
#define TRUE 1
#undef FALSE
#define ONE 1
#undef ZERO
#define VALUE 123
#define TRUE 1
#define CUSTOM 123
/* Make sure we do not redefine CUSTOM. */
#ifndef CUSTOM
# define CUSTOM 123
#endif
EOO
: basics-cmake
:
mkdir build;
ln -s ../../bootstrap.build ../../root.build build/;
cat <<EOI >=config.h.in;
#define VERSION "@version@"
#cmakedefine TRUE
#cmakedefine FALSE
#cmakedefine ONE
#cmakedefine ZERO
#cmakedefine VALUE
# cmakedefine TRUE
#cmakedefine CUSTOM_LINE
#cmakedefine CUSTOM_BLOCK
#cmakedefine CUSTOM_BLOCK @version@
#cmakedefine TRUE true
#cmakedefine FALSE false
#cmakedefine VALUE @VALUE@ /* @version@ */
EOI
$* <<EOI &config.h &config.h.d;
./: h{config}: in{config}
{
autoconf.flavor = cmake
TRUE = true
FALSE = [bool] false
ONE = 1
ZERO = [uint64] 000
VALUE = [uint64] 0123
CUSTOM_LINE = '#define CUSTOM 123'
CUSTOM_BLOCK = \
'
/* Make sure we do not redefine CUSTOM. */
#ifndef CUSTOM
# define CUSTOM 123
#endif
'
}
EOI
cat config.h >>EOO
#define VERSION "1.2.3"
#define TRUE 1
#undef FALSE
#define ONE 1
#undef ZERO
#define VALUE 123
#define TRUE 1
#define CUSTOM 123
/* Make sure we do not redefine CUSTOM. */
#ifndef CUSTOM
# define CUSTOM 123
#endif
/* Make sure we do not redefine CUSTOM. */
#ifndef CUSTOM
# define CUSTOM 123
#endif
#define TRUE true
#undef FALSE
#define VALUE 123 /* 1.2.3 */
EOO
: basics-meson
:
mkdir build;
ln -s ../../bootstrap.build ../../root.build build/;
cat <<EOI >=config.h.in;
#define VERSION "@version@"
#mesondefine TRUE
#mesondefine FALSE
#mesondefine ONE
#mesondefine ZERO
#mesondefine VALUE
# mesondefine TRUE
#mesondefine CUSTOM_LINE
#mesondefine CUSTOM_BLOCK
EOI
$* <<EOI &config.h &config.h.d;
./: h{config}: in{config}
{
autoconf.flavor = meson
TRUE = true
FALSE = [bool] false
ONE = 1
ZERO = [uint64] 000
VALUE = [uint64] 0123
CUSTOM_LINE = '#define CUSTOM 123'
CUSTOM_BLOCK = \
'
/* Make sure we do not redefine CUSTOM. */
#ifndef CUSTOM
# define CUSTOM 123
#endif
'
}
EOI
cat config.h >>EOO
#define VERSION "1.2.3"
#define TRUE 1
#undef FALSE
#define ONE 1
#undef ZERO
#define VALUE 123
#define TRUE 1
#define CUSTOM 123
/* Make sure we do not redefine CUSTOM. */
#ifndef CUSTOM
# define CUSTOM 123
#endif
EOO

View File

@ -0,0 +1,4 @@
/config.build
/root/
/bootstrap/
build/

View File

@ -0,0 +1,6 @@
project = libbuild2-autoconf-tests
using version
using config
using test
using dist

View File

@ -0,0 +1,12 @@
# Load the module to make sure it is up-to-date before we start running the
# tests. Failed that, we may try to update it from multiple tests in parallel.
# Also, our tests use this project as their amalgamation in order to get the
# module import location (as well as the C module configuration).
#
using autoconf
using c
# @@ Should we be able to do just test = $build.path?
#
testscript{*}: test = $recall($build.path)

View File

@ -0,0 +1 @@
./: {*/ -build/} doc{README.md} legal{LICENSE AUTHORS} manifest

View File

@ -0,0 +1,11 @@
: 1
name: libbuild2-autoconf-tests
version: 0.1.0-a.0.z
project: build2
summary: Tests for the Autoconf emulation build system module for build2
license: MIT ; MIT License.
description-file: README.md
url: https://github.com/build2/libbuild2-autoconf
email: users@build2.org
depends: * build2 >= 0.15.0-
depends: * bpkg >= 0.15.0-

24
libbuild2-autoconf/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Compiler/linker output.
#
*.d
*.t
*.i
*.i.*
*.ii
*.ii.*
*.o
*.obj
*.gcm
*.pcm
*.ifc
*.so
*.dll
*.a
*.lib
*.exp
*.pdb
*.ilk
*.exe
*.exe.dlls/
*.exe.manifest
*.pc

1
libbuild2-autoconf/AUTHORS Symbolic link
View File

@ -0,0 +1 @@
../AUTHORS

1
libbuild2-autoconf/LICENSE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE

View File

@ -0,0 +1 @@
../README.md

4
libbuild2-autoconf/build/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/config.build
/root/
/bootstrap/
build/

View File

@ -0,0 +1,7 @@
project = libbuild2-autoconf
using version
using config
using test
using install
using dist

View File

@ -0,0 +1,6 @@
$out_root/
{
include libbuild2/autoconf/
}
export $out_root/libbuild2/autoconf/$import.target

View File

@ -0,0 +1,16 @@
cxx.std = latest
using cxx
hxx{*}: extension = hxx
ixx{*}: extension = ixx
txx{*}: extension = txx
cxx{*}: extension = cxx
if ($cxx.target.system == 'win32-msvc')
cc.poptions += -D_CRT_SECURE_NO_WARNINGS -D_SCL_SECURE_NO_WARNINGS
if ($cxx.class == 'msvc')
cc.coptions += /wd4251 /wd4275 /wd4800
elif ($cxx.id == 'gcc')
cxx.coptions += -Wno-maybe-uninitialized -Wno-free-nonheap-object # libbutl

View File

@ -0,0 +1 @@
./: {*/ -build/} doc{README.md} legal{LICENSE AUTHORS} manifest

View File

@ -0,0 +1,3 @@
# Generated version header.
#
version.hxx

View File

@ -0,0 +1,47 @@
intf_libs = # Interface dependencies.
impl_libs = # Implementation dependencies.
import impl_libs += build2%lib{build2} # Implied interface dependency.
import impl_libs += build2%lib{build2-in}
lib{build2-autoconf}: {hxx ixx txx cxx}{**} $impl_libs $intf_libs
# Build options.
#
cxx.poptions =+ "-I$out_root" "-I$src_root"
obja{*}: cxx.poptions += -DLIBBUILD2_AUTOCONF_STATIC_BUILD
objs{*}: cxx.poptions += -DLIBBUILD2_AUTOCONF_SHARED_BUILD
# Export options.
#
lib{build2-autoconf}:
{
cxx.export.poptions = "-I$out_root" "-I$src_root"
cxx.export.libs = $intf_libs
}
liba{build2-autoconf}: cxx.export.poptions += -DLIBBUILD2_AUTOCONF_STATIC
libs{build2-autoconf}: cxx.export.poptions += -DLIBBUILD2_AUTOCONF_SHARED
# For pre-releases use the complete version to make sure they cannot be used
# in place of another pre-release or the final version. See the version module
# for details on the version.* variable values.
#
if $version.pre_release
lib{build2-autoconf}: bin.lib.version = "-$version.project_id"
else
lib{build2-autoconf}: bin.lib.version = "-$version.major.$version.minor"
# Embed the build system core version as our load suffix.
#
libs{build2-autoconf}: bin.lib.load_suffix = "-$build.version.interface"
# Install into the libbuild2/autoconf/ subdirectory of, say, /usr/include/
# recreating subdirectories.
#
{hxx ixx txx}{*}:
{
install = include/libbuild2/autoconf/
install.subdirs = true
}

View File

@ -0,0 +1,38 @@
#pragma once
// Normally we don't export class templates (but do complete specializations),
// inline functions, and classes with only inline member functions. Exporting
// classes that inherit from non-exported/imported bases (e.g., std::string)
// will end up badly. The only known workarounds are to not inherit or to not
// export. Also, MinGW GCC doesn't like seeing non-exported functions being
// used before their inline definition. The workaround is to reorder code. In
// the end it's all trial and error.
#if defined(LIBBUILD2_AUTOCONF_STATIC) // Using static.
# define LIBBUILD2_AUTOCONF_SYMEXPORT
#elif defined(LIBBUILD2_AUTOCONF_STATIC_BUILD) // Building static.
# define LIBBUILD2_AUTOCONF_SYMEXPORT
#elif defined(LIBBUILD2_AUTOCONF_SHARED) // Using shared.
# ifdef _WIN32
# define LIBBUILD2_AUTOCONF_SYMEXPORT __declspec(dllimport)
# else
# define LIBBUILD2_AUTOCONF_SYMEXPORT
# endif
#elif defined(LIBBUILD2_AUTOCONF_SHARED_BUILD) // Building shared.
# ifdef _WIN32
# define LIBBUILD2_AUTOCONF_SYMEXPORT __declspec(dllexport)
# else
# define LIBBUILD2_AUTOCONF_SYMEXPORT
# endif
#else
// If none of the above macros are defined, then we assume we are being used
// by some third-party build system that cannot/doesn't signal the library
// type. Note that this fallback works for both static and shared libraries
// provided the library only exports functions (in other words, no global
// exported data) and for the shared case the result will be sub-optimal
// compared to having dllimport. If, however, your library does export data,
// then you will probably want to replace the fallback with the (commented
// out) error since it won't work for the shared case.
//
# define LIBBUILD2_AUTOCONF_SYMEXPORT // Using static or shared.
#endif

View File

@ -0,0 +1,75 @@
#include <libbuild2/autoconf/init.hxx>
#include <libbuild2/scope.hxx>
#include <libbuild2/variable.hxx>
#include <libbuild2/diagnostics.hxx>
#include <libbuild2/autoconf/rule.hxx>
namespace build2
{
namespace autoconf
{
static const rule rule_;
//-
// The `autoconf` module.
//-
bool
init (scope& rs,
scope& bs,
const location& l,
bool,
bool,
module_init_extra&)
{
tracer trace ("autoconf::init");
l5 ([&]{trace << "for " << bs;});
// Load in.base (in.* variables, in{} target type).
//
load_module (rs, rs, "in.base", l);
// Enter variables.
//
{
auto& vp (rs.var_pool ());
// Configuration file flavor. Valid values are `autoconf` (default),
// `cmake`, and `meson`.
//
vp.insert<string> ("autoconf.flavor");
}
// Register the rule.
//
// @@ TODO: this will be ambiguous, for example, version.in rule. Note
// also that if we register it for cc{}, then it will always take
// precedence over version.in, which is probably something we don't
// want. In fact, we would have liked it to be of lower precedence
// since version.in will only match if there is dependency on
// manifest.
//
rs.insert_rule<file> (perform_update_id, "autoconf.in", rule_);
rs.insert_rule<file> (perform_clean_id, "autoconf.in", rule_);
rs.insert_rule<file> (configure_update_id, "autoconf.in", rule_);
return true;
}
static const module_functions mod_functions[] =
{
// NOTE: don't forget to also update the documentation in init.hxx if
// changing anything here.
{"autoconf", nullptr, init},
{nullptr, nullptr, nullptr}
};
const module_functions*
build2_autoconf_load ()
{
return mod_functions;
}
}
}

View File

@ -0,0 +1,20 @@
#pragma once
#include <libbuild2/types.hxx>
#include <libbuild2/utility.hxx>
#include <libbuild2/module.hxx>
#include <libbuild2/autoconf/export.hxx>
namespace build2
{
namespace autoconf
{
//-
// Module `autoconf` does not require bootstrapping.
//-
extern "C" LIBBUILD2_AUTOCONF_SYMEXPORT const module_functions*
build2_autoconf_load ();
}
}

View File

@ -0,0 +1,355 @@
#include <libbuild2/autoconf/rule.hxx>
#include <libbuild2/target.hxx>
#include <libbuild2/algorithm.hxx>
#include <libbuild2/diagnostics.hxx>
namespace build2
{
namespace autoconf
{
enum class flavor {autoconf, cmake, meson};
struct match_data
{
autoconf::flavor flavor;
};
static_assert (sizeof (match_data) <= target::data_size,
"insufficient space");
rule::
rule ()
: in::rule ("autoconf.in 1",
"autoconf.in",
'@' /* symbol */,
false /* strict */)
{
}
recipe rule::
apply (action a, target& t) const
{
// Determine and cache the configuration file flavor.
//
flavor f (flavor::autoconf);
if (const string* s = cast_null<string> (t["autoconf.flavor"]))
{
if (*s == "cmake")
f = flavor::cmake;
else if (*s == "meson")
f = flavor::meson;
else if (*s != "autoconf")
fail << "invalid configuration file flavor '" << *s << "'";
}
t.data (match_data {f});
return in::rule::apply (a, t);
}
void rule::
process (const location& l,
action a, const target& t,
depdb& dd, size_t dd_skip,
string& s, size_t b,
const char* nl,
char sym,
bool strict,
const optional<string>& null) const
{
auto ws = [] (char c)
{
return c == ' ' || c == '\t';
};
auto skip_ws = [&ws] (const string& s, size_t& b)
{
for (; ws (s[b]); ++b) ;
};
// Scan a C identifier returning its size. Return 0 if it's not a valid
// identifier or is followed by a non-whitespace.
//
auto read_id = [&ws] (const string& s, size_t b) -> size_t
{
size_t i (b);
char c;
for (; (c = s[i]) == '_' || (i == b ? alpha (c) : alnum (c)); ++i) ;
return i != b && (c == '\0' || ws (c)) ? i - b : 0;
};
size_t i (b);
flavor f (t.data<match_data> ().flavor);
// Substitute special #undef/#cmakedfine/#mesondefine line. If value is
// false, then do not append the value to #define.
//
// Return true if it was substituted with #define, false if with #undef,
// and nullopt if with a custom block of preprocessor directives.
//
auto substitute_special = [&skip_ws, this,
&l,
a, &t,
&dd, &dd_skip,
nl, strict, &null,
&s] (const string& name,
bool value = true) -> optional<bool>
{
// Note that we must be careful here with going too ad hoc since there
// is a parallel debdb validation logic in in::rule which calls
// substitute().
//
optional<string> ov (
substitute (l, a, t, dd, dd_skip, name, strict, null));
assert (ov); // C identifier is a valid variable name.
string& v (*ov);
// As an extension, we allow replacing the entire line with a
// potentially multi-line block of preprocessor directives. To detect
// this, we look for a line that starts with `#` after whitespaces.
//
size_t p (0);
for (;; ++p)
{
skip_ws (v, p);
if (v[p] == '#')
break;
p = v.find ('\n', p);
if (p == string::npos)
break;
}
optional<bool> r;
if (p != string::npos)
{
s.clear ();
// Skip leading and trailing newlines.
//
for (; v.back () == '\n'; v.pop_back ()) ;
for (p = 0; v[p] == '\n'; ++p) ;
for (;; ++p)
{
size_t b (p);
p = v.find ('\n', p);
s.append (v, b, p - b);
if (p == string::npos)
break;
s.append (nl); // Last line is added by our caller.
}
}
// Otherwise translate false/0 to #undef and true to 1.
//
else if (v == "false" || v == "0")
{
s = "#undef ";
s += name;
r = false;
}
else
{
if (v == "true")
v = "1";
s = "#define ";
s += name;
if (value)
{
s += ' ';
s += v;
}
r = true;
}
return r;
};
// Deal with special lines of each flavor. Return if the line has has
// been handled and fall through to use the normal substitution logic.
//
switch (f)
{
case flavor::autoconf:
{
// Autoconf recognizes two forms of special lines:
//
// #undef NAME
// #define NAME 0
//
// However, the latter form as well as comments after NAME in the
// former form are "strongly discouraged". So for now we only
// recognize #undef, especially seeing that it's the dominant form
// in the wild.
//
skip_ws (s, i);
if (s[i] == '#')
{
++i;
skip_ws (s, i);
if (s.compare (i, 5, "undef") == 0 && ws (s[i + 5]))
{
i += 5;
skip_ws (s, i);
size_t n (read_id (s, i));
if (n == 0)
fail (l) << "expected identifier after #undef";
string name (s, i, n);
i += n;
skip_ws (s, i);
// Let's not bother with detecting comments seeing that it's
// strongly discouraged.
//
if (s[i] != '\0')
fail (l) << "junk after identifier " << name;
substitute_special (name);
return;
}
}
break;
}
case flavor::cmake:
{
// CMake recognizes two forms of special lines:
//
// #cmakedefine NAME [VALUE]
// #cmakedefine01 NAME
//
// The first variant is replaced with #define if NAME is "set to any
// value not considered false" and to (commented out) #undef
// otherwise. In the former case, if VALUE is present, then it is
// processed normally with @-substitutions. So in a sense there are
// two entities at play: variable NAME controls #define/#undef while
// VALUE -- the value in case of #define. The following patterns are
// fairly common:
//
// #cmakedefine HAVE_MEMORY_H
// #cmakedefine HAVE_MEMORY_H 1
// #cmakedefine SIZEOF_LONG @SIZEOF_LONG@
//
// But also:
//
// #cmakedefine const
// #cmakedefine const @HAVE_CONST@
// #cmakedefine size_t @SIZE_T@
//
// The #cmakedefine01 variant is always replaced with #define,
// either with value 1 if NAME is true and 0 otherwise. It's doesn't
// appear to be used much in practice so we are not going to bother
// with it.
//
skip_ws (s, i);
if (s[i] == '#')
{
++i;
skip_ws (s, i);
if (s.compare (i, 11, "cmakedefine") == 0 && ws (s[i + 11]))
{
i += 11;
skip_ws (s, i);
size_t n (read_id (s, i));
if (n == 0)
fail (l) << "expected identifier after #cmakedefine";
string name (s, i, n);
i += n;
skip_ws (s, i);
// If there is value, then save it for later (see below).
//
optional<string> value;
if (s[i] != '\0')
value = string (s, i);
optional<bool> r (substitute_special (name, !value));
if (!value || !r || !*r)
return;
// Append the value and fall through to the normal substitution
// logic.
//
s += ' ';
b = s.size (); // Start substituting from here.
s += *value;
}
else if (s.compare (i, 13, "cmakedefine01") == 0 && ws (s[i + 13]))
fail(l) << "#cmakedefine01 is not yet supported";
}
break;
}
case flavor::meson:
{
// Meson recognizes only one special form:
//
// #mesondefine NAME
//
// It is replaced with commented out #undef if NAME is not set, with
// #undef if NAME is set to boolean false, with #define without a
// value if it is set to boolean true, and to #define with the value
// of NAME if it is set to integer or string.
//
// Since we don't do commented out #undef, we will treat the first
// two cases the same. We will also map the third case to value 1,
// similar to Autoconf.
//
skip_ws (s, i);
if (s[i] == '#')
{
++i;
skip_ws (s, i);
if (s.compare (i, 11, "mesondefine") == 0 && ws (s[i + 11]))
{
i += 11;
skip_ws (s, i);
size_t n (read_id (s, i));
if (n == 0)
fail (l) << "expected identifier after #mesondefine";
string name (s, i, n);
i += n;
skip_ws (s, i);
if (s[i] != '\0')
fail (l) << "junk after identifier " << name;
substitute_special (name);
return;
}
}
break;
}
}
in::rule::process (l, a, t, dd, dd_skip, s, b, nl, sym, strict, null);
}
}
}

View File

@ -0,0 +1,38 @@
#pragma once
#include <libbuild2/types.hxx>
#include <libbuild2/utility.hxx>
#include <libbuild2/in/rule.hxx>
namespace build2
{
namespace autoconf
{
// Process a config.h.in file.
//
// Note that to be usable as a drop-in replacement we make the default
// substitution symbol '@' and the mode -- lax. The user, however, is
// still able to override both of these choices with the corresponding
// in.* variables.
//
class rule: public in::rule
{
public:
rule ();
virtual recipe
apply (action, target&) const override;
virtual void
process (const location&,
action, const target&,
depdb&, size_t,
string&, size_t,
const char*,
char,
bool,
const optional<string>&) const override;
};
}
}

View File

@ -0,0 +1,13 @@
: 1
name: libbuild2-autoconf
version: 0.1.0-a.0.z
project: build2
summary: Autoconf emulation build system module for build2
license: MIT ; MIT License.
description-file: README.md
url: https://github.com/build2/libbuild2-autoconf
email: users@build2.org
build-warning-email: builds@build2.org
depends: * build2 >= 0.15.0-
depends: * bpkg >= 0.15.0-
tests: * libbuild2-autoconf-tests == $

4
packages.manifest Normal file
View File

@ -0,0 +1,4 @@
: 1
location: libbuild2-autoconf/
:
location: libbuild2-autoconf-tests/