From 737784418ce7628077094dd852ea7007bbe5e470 Mon Sep 17 00:00:00 2001 From: Stefan Seefeld Date: Thu, 27 Apr 2017 15:21:30 -0400 Subject: [PATCH] Add basic configuration API. --- examples/config/fabscript | 60 +++++++++++++++++++++ examples/config/main.cpp | 35 +++++++++++++ src/faber/config/__init__.py | 32 ++++++++++++ src/faber/config/c_checks.py | 23 +++++++++ src/faber/config/check.py | 92 +++++++++++++++++++++++++++++++++ src/faber/config/cxx_checks.py | 43 +++++++++++++++ src/faber/config/try_compile.py | 32 ++++++++++++ src/faber/config/try_link.py | 32 ++++++++++++ src/faber/config/try_run.py | 48 +++++++++++++++++ src/faber/project.py | 6 +++ tests/proto/test_try_compile.py | 44 ++++++++++++++++ 11 files changed, 447 insertions(+) create mode 100644 examples/config/fabscript create mode 100644 examples/config/main.cpp create mode 100644 src/faber/config/__init__.py create mode 100644 src/faber/config/c_checks.py create mode 100644 src/faber/config/check.py create mode 100644 src/faber/config/cxx_checks.py create mode 100644 src/faber/config/try_compile.py create mode 100644 src/faber/config/try_link.py create mode 100644 src/faber/config/try_run.py create mode 100644 tests/proto/test_try_compile.py diff --git a/examples/config/fabscript b/examples/config/fabscript new file mode 100644 index 000000000..63d3aa0f3 --- /dev/null +++ b/examples/config/fabscript @@ -0,0 +1,60 @@ +# -*- python -*- +# +# Copyright (c) 2016 Stefan Seefeld +# All rights reserved. +# +# This file is part of Faber. It is made available under the +# Boost Software License, Version 1.0. +# (Consult LICENSE or http://www.boost.org/LICENSE_1_0.txt) + +# +# This example demonstrates how to configure a project. +# +# Run with: +# +# * `faber cxxflags=-I/usr/include/python2.7 libs=python2.7` +# * `faber cxxflags=-std=c++14` +# * `faber --with-python-inc=/usr/include/python2.7 --with-python-lib=python2.7` +# +# To remove config artefacts run with: +# +# * `faber .config-clean` + +from faber.artefacts.binary import binary +from faber.tools.compiler import define, include, linkpath, libs +from faber.types import cxx, c +from faber.config.try_link import * +from faber.config import report, c_checks, cxx_checks, try_run +from faber import scheduler + +python_inc = options.get_with('python-inc') +python_linkpath = options.get_with('python-linkpath') +python_lib = options.get_with('python-lib') +if python_inc: + features |= include(python_inc) +if python_linkpath: + features |= linkpath(python_linkpath) +if python_lib: + features |= libs(python_lib) + +pysrc=""" +#include +int main() +{ + Py_Initialize(); +} +""" +checks = [c_checks.sizeof('char', cxx, features=features), + c_checks.sizeof('long', cxx, features=features), + try_link('pytest', pysrc, cxx, features, + define('HAS_PYTHON=1'), + define('HAS_PYTHON=0')), + cxx_checks.has_cxx11(features, define('HAS_CXX11')), + cxx_checks.has_cxx14(features, define('HAS_CXX14')), + cxx_checks.has_cxx17(features, define('HAS_CXX17'))] + +config = report('config', checks) +bin = binary('check', 'main.cpp', dependencies=config, features=config.use) +report = rule(action('run', '$(>)'), 'report', bin, attrs=notfile|always) + +default = report diff --git a/examples/config/main.cpp b/examples/config/main.cpp new file mode 100644 index 000000000..f11f4e3b0 --- /dev/null +++ b/examples/config/main.cpp @@ -0,0 +1,35 @@ +// +// Copyright (c) 2016 Stefan Seefeld +// All rights reserved. +// +// This file is part of Faber. It is made available under the +// Boost Software License, Version 1.0. +// (Consult LICENSE or http://www.boost.org/LICENSE_1_0.txt) + +#include + +#ifdef HAS_CXX11 +# define cxx11 "defined" +#else +# define cxx11 "undefined" +#endif +#ifdef HAS_CXX14 +# define cxx14 "defined" +#else +# define cxx14 "undefined" +#endif +#ifdef HAS_CXX17 +# define cxx17 "defined" +#else +# define cxx17 "undefined" +#endif + + +int main() +{ + std::cout << "HAS_PYTHON=" << HAS_PYTHON << '\n' + << "HAS_CXX11 " << cxx11 << '\n' + << "HAS_CXX14 " << cxx14 << '\n' + << "HAS_CXX17 " << cxx17 << '\n' + << std::endl; +} diff --git a/src/faber/config/__init__.py b/src/faber/config/__init__.py new file mode 100644 index 000000000..720e097fd --- /dev/null +++ b/src/faber/config/__init__.py @@ -0,0 +1,32 @@ +# +# Copyright (c) 2016 Stefan Seefeld +# All rights reserved. +# +# This file is part of Faber. It is made available under the +# Boost Software License, Version 1.0. +# (Consult LICENSE or http://www.boost.org/LICENSE_1_0.txt) + +from ..artefact import artefact, notfile +from ..rule import depend +from .. import output + + +class report(artefact): + + def __init__(self, name, checks): + use = [c.use for c in checks] + artefact.__init__(self, name, attrs=notfile, use=use) + depend(self, checks) + self.checks = checks + + def _report(self): + + max_name_length = max(len(c.qname) for c in self.checks) + print(output.coloured('configuration check results:', attrs=['bold'])) + for c in self.checks: + print(' {:{}} : {} {}' + .format(c.qname, max_name_length, c.result, '(cached)' if c.cached else '')) + + def __status__(self, status): + artefact.__status__(self, status) + self._report() diff --git a/src/faber/config/c_checks.py b/src/faber/config/c_checks.py new file mode 100644 index 000000000..bb4125dea --- /dev/null +++ b/src/faber/config/c_checks.py @@ -0,0 +1,23 @@ +# +# Copyright (c) 2016 Stefan Seefeld +# All rights reserved. +# +# This file is part of Faber. It is made available under the +# Boost Software License, Version 1.0. +# (Consult LICENSE or http://www.boost.org/LICENSE_1_0.txt) + +from .try_run import check_output +from .. import types + + +class sizeof(check_output): + + src = """#include +int main(){{ printf("%i", sizeof({}));}}""" + + def __init__(self, c_type, lang_type=types.c, features=()): + check_output.__init__(self, 'sizeof_' + c_type, sizeof.src.format(c_type), lang_type, + features) + + def post_process(self, output): + self.result = int(output) diff --git a/src/faber/config/check.py b/src/faber/config/check.py new file mode 100644 index 000000000..933a970c1 --- /dev/null +++ b/src/faber/config/check.py @@ -0,0 +1,92 @@ +# +# Copyright (c) 2016 Stefan Seefeld +# All rights reserved. +# +# This file is part of Faber. It is made available under the +# Boost Software License, Version 1.0. +# (Consult LICENSE or http://www.boost.org/LICENSE_1_0.txt) + +from ..feature import conditional +from ..artefact import artefact, notfile, nocare +from ..module import module +import sqlite3 +import hashlib +import os +import os.path +import logging + +logger = logging.getLogger('config') + + +class cache(object): + + def __init__(self): + if not os.path.exists(module.current.builddir): + os.makedirs(module.current.builddir) + self.filename = os.path.join(module.current.builddir, '.configcache') + self.conn = sqlite3.connect(self.filename) + # Create table if it doesn't exist yet. + if not next(self.conn.execute('SELECT name FROM sqlite_master ' + 'WHERE type="table" AND name="checks"'), None): + self.conn.execute('CREATE TABLE checks ' + '(key TEXT, status INTEGER, type TEXT, value TEXT)') + + def __del__(self): + self.conn.commit() + + def clean(self): + self.conn.close() + os.remove(self.filename) + del self + + def __contains__(self, key): + with self.conn: + a = self.conn.execute('SELECT key FROM checks WHERE key=?', (key,)) + value = next(a, None) + return bool(value) + + def __setitem__(self, key, value): + status, result = value[0], value[1] + with self.conn: + self.conn.execute('INSERT INTO checks VALUES(?,?,?,?)', + (key, status, type(result).__name__, str(result))) + + def __getitem__(self, key): + with self.conn: + a = self.conn.execute('SELECT status, type, value FROM checks WHERE key=?', + (key,)) + status, type, value = next(a, (None, None, None)) + value = {'str': lambda x: x, + 'unicode': lambda x: x, + 'bool': lambda x: eval(x), + 'int': lambda x: int(x)}[type](value) + return status, value + + +class check(artefact): + """A check is an artefact that performs some tests (typically involving compilation), + then stores the result in a cache, so it doesn't need to be performed again, + until the cache is explicitly cleared.""" + + cache = cache() if module.current else None # to support sphinx' autoclass + + def __init__(self, name, features=(), if_=(), ifnot=()): + + self.result = None + artefact.__init__(self, name, attrs=notfile|nocare, features=features) + # The 'condition' here is simply the value of the check's status member. + self.use = conditional(lambda ctx: self.status, self, if_, ifnot) + key = str((self.name, str(self.features))).encode('utf-8') + self._cache_key = hashlib.md5(key).hexdigest() + self.cached = self._cache_key in check.cache + if self.cached: + self.status, self.result = check.cache[self._cache_key] + + def __status__(self, status): + logger.debug('check.__status__({})'.format(status)) + if self.cached: + return # the cached value takes precedence + artefact.__status__(self, status) + if not self.status or self.result is None: + self.result = self.status + check.cache[self._cache_key] = (self.status, self.result) diff --git a/src/faber/config/cxx_checks.py b/src/faber/config/cxx_checks.py new file mode 100644 index 000000000..8d392466e --- /dev/null +++ b/src/faber/config/cxx_checks.py @@ -0,0 +1,43 @@ +# +# Copyright (c) 2016 Stefan Seefeld +# All rights reserved. +# +# This file is part of Faber. It is made available under the +# Boost Software License, Version 1.0. +# (Consult LICENSE or http://www.boost.org/LICENSE_1_0.txt) + +from .try_compile import try_compile +from .. import types + + +class has_cxx11(try_compile): + + src = r"""#if __cplusplus < 201103L +#error no C++11 +#endif""" + + def __init__(self, features=(), if_=(), ifnot=()): + try_compile.__init__(self, 'has_cxx11', has_cxx11.src, types.cxx, features, + if_, ifnot) + + +class has_cxx14(try_compile): + + src = r"""#if __cplusplus < 201402L +#error no C++14 +#endif""" + + def __init__(self, features=(), if_=(), ifnot=()): + try_compile.__init__(self, 'has_cxx14', has_cxx14.src, types.cxx, features, + if_, ifnot) + + +class has_cxx17(try_compile): + + src = r"""#if __cplusplus < 201500L +#error no C++17 +#endif""" + + def __init__(self, features=(), if_=(), ifnot=()): + try_compile.__init__(self, 'has_cxx17', has_cxx17.src, types.cxx, features, + if_, ifnot) diff --git a/src/faber/config/try_compile.py b/src/faber/config/try_compile.py new file mode 100644 index 000000000..8c4486f83 --- /dev/null +++ b/src/faber/config/try_compile.py @@ -0,0 +1,32 @@ +# +# Copyright (c) 2016 Stefan Seefeld +# All rights reserved. +# +# This file is part of Faber. It is made available under the +# Boost Software License, Version 1.0. +# (Consult LICENSE or http://www.boost.org/LICENSE_1_0.txt) + +from .check import check +from ..artefact import intermediate, always +from ..tools.compiler import compiler +from ..rule import rule, alias +from ..artefacts.object import object + + +class try_compile(check): + """Try to compile a chunk of source code.""" + + def __init__(self, name, source, type, features=(), if_=(), ifnot=()): + + check.__init__(self, name, features, if_, ifnot) + compiler.check_instance_for_type(type, features) + if not self.cached: + # create source file + src = type.synthesize_name(self.name) + + def generate(targets, _): + with open(targets[0]._filename, 'w') as os: + os.write(source) + src = rule(generate, src, attrs=intermediate|always) + obj = object(self.name, src, attrs=intermediate, features=self.features) + alias(self, obj) diff --git a/src/faber/config/try_link.py b/src/faber/config/try_link.py new file mode 100644 index 000000000..1e34b5f20 --- /dev/null +++ b/src/faber/config/try_link.py @@ -0,0 +1,32 @@ +# +# Copyright (c) 2016 Stefan Seefeld +# All rights reserved. +# +# This file is part of Faber. It is made available under the +# Boost Software License, Version 1.0. +# (Consult LICENSE or http://www.boost.org/LICENSE_1_0.txt) + +from .check import check +from ..artefact import intermediate, always +from ..tools.compiler import compiler +from ..rule import rule, alias +from ..artefacts.binary import binary + + +class try_link(check): + """Try to compile and link a chunk of source code.""" + + def __init__(self, name, source, type, features=(), if_=(), ifnot=()): + + check.__init__(self, name, features, if_, ifnot) + compiler.check_instance_for_type(type, features) + if not self.cached: + # create source file + src = type.synthesize_name(self.name) + + def generate(targets, _): + with open(targets[0]._filename, 'w') as os: + os.write(source) + src = rule(generate, src, attrs=intermediate|always) + bin = binary(self.name, src, attrs=intermediate, features=self.features) + alias(self, bin) diff --git a/src/faber/config/try_run.py b/src/faber/config/try_run.py new file mode 100644 index 000000000..2aec721e3 --- /dev/null +++ b/src/faber/config/try_run.py @@ -0,0 +1,48 @@ +# +# Copyright (c) 2016 Stefan Seefeld +# All rights reserved. +# +# This file is part of Faber. It is made available under the +# Boost Software License, Version 1.0. +# (Consult LICENSE or http://www.boost.org/LICENSE_1_0.txt) + +from .check import check +from ..artefact import intermediate, always +from ..tools.compiler import compiler +from ..rule import rule +from ..action import action +from ..artefacts.binary import binary +import subprocess + + +class try_run(check): + """Try to compile and run a chunk of source code.""" + + run = action('run', '$(>)') + + def __init__(self, name, source, type, features=(), if_=(), ifnot=()): + + check.__init__(self, name, features, if_, ifnot) + compiler.check_instance_for_type(type, self.features) + if not self.cached: + # create source file + src = type.synthesize_name(self.name) + + def generate(targets, _): + with open(targets[0]._filename, 'w') as os: + os.write(source) + src = rule(generate, src, attrs=intermediate|always) + bin = binary(self.name, src, features=self.features, attrs=intermediate) + rule(self.run, self, bin) + + +class check_output(try_run): + """Compile and run a chunk of source code and check the generated output.""" + + def post_process(self, output): + self.result = output + + def run(self, _, source): + """run the binary in a subprocess, then post-process the output.""" + output = subprocess.check_output([source[0].filename.eval()]).decode().strip() + self.post_process(output) diff --git a/src/faber/project.py b/src/faber/project.py index 9907ba8b6..e898b6c37 100644 --- a/src/faber/project.py +++ b/src/faber/project.py @@ -78,9 +78,15 @@ def clean(level, options, parameters, srcdir, builddir): """Clean up file artefacts.""" options = optioncache(builddir, options) + module.init([], options, parameters) + m = module('', srcdir, builddir) # noqa F841 scheduler.clean(level) if level > 1: + from .config.check import check + if check.cache: + check.cache.clean() options.clean() + module.finish() return True diff --git a/tests/proto/test_try_compile.py b/tests/proto/test_try_compile.py new file mode 100644 index 000000000..f829834d7 --- /dev/null +++ b/tests/proto/test_try_compile.py @@ -0,0 +1,44 @@ +# +# Copyright (c) 2017 Stefan Seefeld +# All rights reserved. +# +# This file is part of Faber. It is made available under the +# Boost Software License, Version 1.0. +# (Consult LICENSE or http://www.boost.org/LICENSE_1_0.txt) + +from faber.artefact import artefact +from faber.artefact import notfile, always, intermediate +from faber.rule import rule, depend, alias +from faber.tools import fileutils +from faber import scheduler +import pytest +try: + from unittest.mock import patch +except ImportError: + from mock import patch + + +@pytest.mark.usefixtures('module') +def test_composite(): + """Test the workflow of faber.config.try_compile""" + + src = rule(fileutils.touch, 'src', attrs=intermediate|always) + obj = artefact('obj', attrs=intermediate) + check = artefact('check', attrs=notfile) + + def assemble(targets, sources): + rule(fileutils.copy, obj, src, + attrs=intermediate, module=targets[0].module) + + # make a dependency graph + ass = rule(assemble, 'ass', attrs=notfile|always) + # make a binary + depend(obj, dependencies=ass) + # and test it + check = alias(check, obj) + + with patch('faber.scheduler._report_recipe') as recipe: + scheduler.update([check]) + (_, _, status, _, _, _), kwds = recipe.call_args_list[-1] + assert recipe.call_count == 3 + assert status == 0