Bug 1269517 - Implement try_compile for Python configure. draft
authorChris Manchester <cmanchester@mozilla.com>
Fri, 22 Jul 2016 10:39:36 -0700
changeset 391396 6d03954a4b14b0ffdde29886bdb774dfc06db0bf
parent 391395 0a429bd1a48b4d4eb9954a76a64f8fc3c7d1ba45
child 391397 dc211d0b750db3e9cd5a692b875c9a86a49d06e7
push id23897
push usercmanchester@mozilla.com
push dateFri, 22 Jul 2016 17:39:54 +0000
bugs1269517
milestone50.0a1
Bug 1269517 - Implement try_compile for Python configure. MozReview-Commit-ID: AE7uRVneGXJ
build/moz.configure/compilechecks.configure
build/moz.configure/toolchain.configure
python/moz.build
python/mozbuild/mozbuild/test/configure/test_header_checks.py
new file mode 100644
--- /dev/null
+++ b/build/moz.configure/compilechecks.configure
@@ -0,0 +1,55 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+# Generates a test program and attempts to compile it. In case of failure, the
+# resulting check will return None. If the test program succeeds, it will return
+# the output of the test program.
+# - `includes` are the includes (as file names) that will appear at the top of
+#   the generated test program.
+# - `body` is the code that will appear in the main function of the generated
+#   test program. `return 0;` is appended to the function body automatically.
+# - `language` is the language selection, so that the appropriate compiler is
+#   used.
+# - `flags` are the flags to be passed to the compiler, in addition to `-c`.
+# - `check_msg` is the message to be printed to accompany compiling the test
+#   program.
+@template
+@imports('textwrap')
+def try_compile(includes=None, body='', language='C++', flags=None, check_msg=None):
+    includes = includes or []
+    source_lines = ['#include <%s>' % f for f in includes]
+    source = '\n'.join(source_lines) + '\n'
+    source += textwrap.dedent('''\
+        int
+        main(void)
+        {
+        %s
+          ;
+          return 0;
+        }
+    ''' % body)
+    flags = flags or []
+    flags.append('-c')
+
+    if check_msg:
+        def checking_fn(fn):
+            return checking(check_msg, callback=lambda r: r is not None)(fn)
+    else:
+        def checking_fn(fn):
+            return fn
+
+    @depends(cxx_compiler, c_compiler)
+    @checking_fn
+    def check(cxx_info, c_info):
+        info = {
+            'C': c_info,
+            'C++': cxx_info,
+        }[language]
+        return try_invoke_compiler(info.wrapper + [info.compiler] + info.flags,
+                                   language, source, flags,
+                                   onerror=lambda: None)
+    return check
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure
@@ -572,16 +572,18 @@ def compiler(language, host_or_target, c
 
 c_compiler = compiler('C', target)
 cxx_compiler = compiler('C++', target, c_compiler=c_compiler)
 host_c_compiler = compiler('C', host, other_compiler=c_compiler)
 host_cxx_compiler = compiler('C++', host, c_compiler=host_c_compiler,
                              other_compiler=cxx_compiler,
                              other_c_compiler=c_compiler)
 
+include('compilechecks.configure')
+
 @depends(c_compiler)
 def default_debug_flags(compiler_info):
     # Debug info is ON by default.
     if compiler_info.type == 'msvc':
         return '-Zi'
     return '-g'
 
 option(env='MOZ_DEBUG_FLAGS',
--- a/python/moz.build
+++ b/python/moz.build
@@ -34,16 +34,17 @@ PYTHON_UNIT_TESTS += [
     'mozbuild/mozbuild/test/backend/test_android_eclipse.py',
     'mozbuild/mozbuild/test/backend/test_build.py',
     'mozbuild/mozbuild/test/backend/test_configenvironment.py',
     'mozbuild/mozbuild/test/backend/test_recursivemake.py',
     'mozbuild/mozbuild/test/backend/test_visualstudio.py',
     'mozbuild/mozbuild/test/compilation/test_warnings.py',
     'mozbuild/mozbuild/test/configure/test_checks_configure.py',
     'mozbuild/mozbuild/test/configure/test_configure.py',
+    'mozbuild/mozbuild/test/configure/test_header_checks.py',
     'mozbuild/mozbuild/test/configure/test_moz_configure.py',
     'mozbuild/mozbuild/test/configure/test_options.py',
     'mozbuild/mozbuild/test/configure/test_toolchain_configure.py',
     'mozbuild/mozbuild/test/configure/test_toolchain_helpers.py',
     'mozbuild/mozbuild/test/configure/test_util.py',
     'mozbuild/mozbuild/test/controller/test_ccachestats.py',
     'mozbuild/mozbuild/test/controller/test_clobber.py',
     'mozbuild/mozbuild/test/frontend/test_context.py',
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/configure/test_header_checks.py
@@ -0,0 +1,154 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import os
+import textwrap
+import unittest
+
+from StringIO import StringIO
+
+from buildconfig import topsrcdir
+from common import ConfigureTestSandbox
+from mozbuild.util import exec_
+from mozunit import main
+from test_toolchain_helpers import FakeCompiler
+
+
+class TestHeaderChecks(unittest.TestCase):
+
+    def get_mock_compiler(self, expected_test_content=None, expected_flags=None):
+        expected_flags = expected_flags or []
+        def mock_compiler(stdin, args):
+            args, test_file = args[:-1], args[-1]
+            self.assertIn('-c', args)
+            for flag in expected_flags:
+                self.assertIn(flag, args)
+
+            if expected_test_content:
+                with open(test_file) as fh:
+                    test_content = fh.read()
+                self.assertEqual(test_content, expected_test_content)
+
+            return FakeCompiler()(None, args)
+        return mock_compiler
+
+    def do_compile_test(self, command, expected_test_content=None,
+                        expected_flags=None):
+
+        paths = {
+            os.path.abspath('/usr/bin/mockcc'): self.get_mock_compiler(
+                expected_test_content=expected_test_content,
+                expected_flags=expected_flags),
+        }
+
+        mock_compiler_defs = textwrap.dedent('''\
+            @depends('--help')
+            def c_compiler(_):
+                return namespace(
+                    flags=[],
+                    compiler=os.path.abspath('/usr/bin/mockcc'),
+                    wrapper=[],
+                )
+
+            @depends('--help')
+            def cxx_compiler(_):
+                return namespace(
+                    flags=[],
+                    compiler=os.path.abspath('/usr/bin/mockcc'),
+                    wrapper=[],
+                )
+        ''')
+
+        config = {}
+        out = StringIO()
+        sandbox = ConfigureTestSandbox(paths, config, {}, ['/bin/configure'],
+                                       out, out)
+        base_dir = os.path.join(topsrcdir, 'build', 'moz.configure')
+        sandbox.include_file(os.path.join(base_dir, 'util.configure'))
+        sandbox.include_file(os.path.join(base_dir, 'checks.configure'))
+        exec_(mock_compiler_defs, sandbox)
+        sandbox.include_file(os.path.join(base_dir, 'compilechecks.configure'))
+
+        status = 0
+        try:
+            exec_(command, sandbox)
+            sandbox.run()
+        except SystemExit as e:
+            status = e.code
+
+        return config, out.getvalue(), status
+
+    def test_try_compile_include(self):
+        expected_test_content = textwrap.dedent('''\
+          #include <foo.h>
+          #include <bar.h>
+          int
+          main(void)
+          {
+
+            ;
+            return 0;
+          }
+        ''')
+
+        cmd = textwrap.dedent('''\
+            try_compile(['foo.h', 'bar.h'], language='C')
+        ''')
+
+        config, out, status = self.do_compile_test(cmd, expected_test_content)
+        self.assertEqual(status, 0)
+        self.assertEqual(config, {})
+
+    def test_try_compile_flags(self):
+        expected_flags = ['--extra', '--flags']
+
+        cmd = textwrap.dedent('''\
+            try_compile(language='C++', flags=['--flags', '--extra'])
+        ''')
+
+        config, out, status = self.do_compile_test(cmd, expected_flags=expected_flags)
+        self.assertEqual(status, 0)
+        self.assertEqual(config, {})
+
+    def test_try_compile_failure(self):
+        cmd = textwrap.dedent('''\
+            @depends(try_compile(body='somefn();', flags=['-funknown-flag']))
+            def have_fn(value):
+                if value is not None:
+                    return True
+            set_config('HAVE_SOMEFN', have_fn)
+
+            @depends(try_compile(body='anotherfn();', language='C'))
+            def have_another(value):
+                if value is not None:
+                    return True
+            set_config('HAVE_ANOTHERFN', have_another)
+        ''')
+
+        config, out, status = self.do_compile_test(cmd)
+        self.assertEqual(status, 0)
+        self.assertEqual(config, {
+            'HAVE_ANOTHERFN': True,
+        })
+
+    def test_try_compile_msg(self):
+        cmd = textwrap.dedent('''\
+            @depends(try_compile(language='C++', flags=['-fknown-flag'],
+                     check_msg='whether -fknown-flag works'))
+            def known_flag(result):
+                if result is not None:
+                    return True
+            set_config('HAVE_KNOWN_FLAG', known_flag)
+        ''')
+        config, out, status = self.do_compile_test(cmd)
+        self.assertEqual(status, 0)
+        self.assertEqual(config, {'HAVE_KNOWN_FLAG': True})
+        self.assertEqual(out, textwrap.dedent('''\
+            checking whether -fknown-flag works... yes
+        '''))
+
+if __name__ == '__main__':
+    main()