1import sys
2import os
3from io import StringIO
4import textwrap
5
6from distutils.core import Distribution
7from distutils.command.build_ext import build_ext
8from distutils import sysconfig
9from distutils.tests.support import (TempdirManager, LoggingSilencer,
10                                     copy_xxmodule_c, fixup_build_ext)
11from distutils.extension import Extension
12from distutils.errors import (
13    CompileError, DistutilsPlatformError, DistutilsSetupError,
14    UnknownFileError)
15
16import unittest
17from test import support
18
19# http://bugs.python.org/issue4373
20# Don't load the xx module more than once.
21ALREADY_TESTED = False
22
23
24class BuildExtTestCase(TempdirManager,
25                       LoggingSilencer,
26                       unittest.TestCase):
27    def setUp(self):
28        # Create a simple test environment
29        # Note that we're making changes to sys.path
30        super(BuildExtTestCase, self).setUp()
31        self.tmp_dir = self.mkdtemp()
32        self.sys_path = sys.path, sys.path[:]
33        sys.path.append(self.tmp_dir)
34        import site
35        self.old_user_base = site.USER_BASE
36        site.USER_BASE = self.mkdtemp()
37        from distutils.command import build_ext
38        build_ext.USER_BASE = site.USER_BASE
39
40    def build_ext(self, *args, **kwargs):
41        return build_ext(*args, **kwargs)
42
43    def test_build_ext(self):
44        cmd = support.missing_compiler_executable()
45        if cmd is not None:
46            self.skipTest('The %r command is not found' % cmd)
47        global ALREADY_TESTED
48        copy_xxmodule_c(self.tmp_dir)
49        xx_c = os.path.join(self.tmp_dir, 'xxmodule.c')
50        xx_ext = Extension('xx', [xx_c])
51        dist = Distribution({'name': 'xx', 'ext_modules': [xx_ext]})
52        dist.package_dir = self.tmp_dir
53        cmd = self.build_ext(dist)
54        fixup_build_ext(cmd)
55        cmd.build_lib = self.tmp_dir
56        cmd.build_temp = self.tmp_dir
57
58        old_stdout = sys.stdout
59        if not support.verbose:
60            # silence compiler output
61            sys.stdout = StringIO()
62        try:
63            cmd.ensure_finalized()
64            cmd.run()
65        finally:
66            sys.stdout = old_stdout
67
68        if ALREADY_TESTED:
69            self.skipTest('Already tested in %s' % ALREADY_TESTED)
70        else:
71            ALREADY_TESTED = type(self).__name__
72
73        import xx
74
75        for attr in ('error', 'foo', 'new', 'roj'):
76            self.assertTrue(hasattr(xx, attr))
77
78        self.assertEqual(xx.foo(2, 5), 7)
79        self.assertEqual(xx.foo(13,15), 28)
80        self.assertEqual(xx.new().demo(), None)
81        if support.HAVE_DOCSTRINGS:
82            doc = 'This is a template module just for instruction.'
83            self.assertEqual(xx.__doc__, doc)
84        self.assertIsInstance(xx.Null(), xx.Null)
85        self.assertIsInstance(xx.Str(), xx.Str)
86
87    def tearDown(self):
88        # Get everything back to normal
89        support.unload('xx')
90        sys.path = self.sys_path[0]
91        sys.path[:] = self.sys_path[1]
92        import site
93        site.USER_BASE = self.old_user_base
94        from distutils.command import build_ext
95        build_ext.USER_BASE = self.old_user_base
96        super(BuildExtTestCase, self).tearDown()
97
98    def test_solaris_enable_shared(self):
99        dist = Distribution({'name': 'xx'})
100        cmd = self.build_ext(dist)
101        old = sys.platform
102
103        sys.platform = 'sunos' # fooling finalize_options
104        from distutils.sysconfig import  _config_vars
105        old_var = _config_vars.get('Py_ENABLE_SHARED')
106        _config_vars['Py_ENABLE_SHARED'] = 1
107        try:
108            cmd.ensure_finalized()
109        finally:
110            sys.platform = old
111            if old_var is None:
112                del _config_vars['Py_ENABLE_SHARED']
113            else:
114                _config_vars['Py_ENABLE_SHARED'] = old_var
115
116        # make sure we get some library dirs under solaris
117        self.assertGreater(len(cmd.library_dirs), 0)
118
119    def test_user_site(self):
120        import site
121        dist = Distribution({'name': 'xx'})
122        cmd = self.build_ext(dist)
123
124        # making sure the user option is there
125        options = [name for name, short, lable in
126                   cmd.user_options]
127        self.assertIn('user', options)
128
129        # setting a value
130        cmd.user = 1
131
132        # setting user based lib and include
133        lib = os.path.join(site.USER_BASE, 'lib')
134        incl = os.path.join(site.USER_BASE, 'include')
135        os.mkdir(lib)
136        os.mkdir(incl)
137
138        # let's run finalize
139        cmd.ensure_finalized()
140
141        # see if include_dirs and library_dirs
142        # were set
143        self.assertIn(lib, cmd.library_dirs)
144        self.assertIn(lib, cmd.rpath)
145        self.assertIn(incl, cmd.include_dirs)
146
147    def test_optional_extension(self):
148
149        # this extension will fail, but let's ignore this failure
150        # with the optional argument.
151        modules = [Extension('foo', ['xxx'], optional=False)]
152        dist = Distribution({'name': 'xx', 'ext_modules': modules})
153        cmd = self.build_ext(dist)
154        cmd.ensure_finalized()
155        self.assertRaises((UnknownFileError, CompileError),
156                          cmd.run)  # should raise an error
157
158        modules = [Extension('foo', ['xxx'], optional=True)]
159        dist = Distribution({'name': 'xx', 'ext_modules': modules})
160        cmd = self.build_ext(dist)
161        cmd.ensure_finalized()
162        cmd.run()  # should pass
163
164    def test_finalize_options(self):
165        # Make sure Python's include directories (for Python.h, pyconfig.h,
166        # etc.) are in the include search path.
167        modules = [Extension('foo', ['xxx'], optional=False)]
168        dist = Distribution({'name': 'xx', 'ext_modules': modules})
169        cmd = self.build_ext(dist)
170        cmd.finalize_options()
171
172        py_include = sysconfig.get_python_inc()
173        self.assertIn(py_include, cmd.include_dirs)
174
175        plat_py_include = sysconfig.get_python_inc(plat_specific=1)
176        self.assertIn(plat_py_include, cmd.include_dirs)
177
178        # make sure cmd.libraries is turned into a list
179        # if it's a string
180        cmd = self.build_ext(dist)
181        cmd.libraries = 'my_lib, other_lib lastlib'
182        cmd.finalize_options()
183        self.assertEqual(cmd.libraries, ['my_lib', 'other_lib', 'lastlib'])
184
185        # make sure cmd.library_dirs is turned into a list
186        # if it's a string
187        cmd = self.build_ext(dist)
188        cmd.library_dirs = 'my_lib_dir%sother_lib_dir' % os.pathsep
189        cmd.finalize_options()
190        self.assertIn('my_lib_dir', cmd.library_dirs)
191        self.assertIn('other_lib_dir', cmd.library_dirs)
192
193        # make sure rpath is turned into a list
194        # if it's a string
195        cmd = self.build_ext(dist)
196        cmd.rpath = 'one%stwo' % os.pathsep
197        cmd.finalize_options()
198        self.assertEqual(cmd.rpath, ['one', 'two'])
199
200        # make sure cmd.link_objects is turned into a list
201        # if it's a string
202        cmd = build_ext(dist)
203        cmd.link_objects = 'one two,three'
204        cmd.finalize_options()
205        self.assertEqual(cmd.link_objects, ['one', 'two', 'three'])
206
207        # XXX more tests to perform for win32
208
209        # make sure define is turned into 2-tuples
210        # strings if they are ','-separated strings
211        cmd = self.build_ext(dist)
212        cmd.define = 'one,two'
213        cmd.finalize_options()
214        self.assertEqual(cmd.define, [('one', '1'), ('two', '1')])
215
216        # make sure undef is turned into a list of
217        # strings if they are ','-separated strings
218        cmd = self.build_ext(dist)
219        cmd.undef = 'one,two'
220        cmd.finalize_options()
221        self.assertEqual(cmd.undef, ['one', 'two'])
222
223        # make sure swig_opts is turned into a list
224        cmd = self.build_ext(dist)
225        cmd.swig_opts = None
226        cmd.finalize_options()
227        self.assertEqual(cmd.swig_opts, [])
228
229        cmd = self.build_ext(dist)
230        cmd.swig_opts = '1 2'
231        cmd.finalize_options()
232        self.assertEqual(cmd.swig_opts, ['1', '2'])
233
234    def test_check_extensions_list(self):
235        dist = Distribution()
236        cmd = self.build_ext(dist)
237        cmd.finalize_options()
238
239        #'extensions' option must be a list of Extension instances
240        self.assertRaises(DistutilsSetupError,
241                          cmd.check_extensions_list, 'foo')
242
243        # each element of 'ext_modules' option must be an
244        # Extension instance or 2-tuple
245        exts = [('bar', 'foo', 'bar'), 'foo']
246        self.assertRaises(DistutilsSetupError, cmd.check_extensions_list, exts)
247
248        # first element of each tuple in 'ext_modules'
249        # must be the extension name (a string) and match
250        # a python dotted-separated name
251        exts = [('foo-bar', '')]
252        self.assertRaises(DistutilsSetupError, cmd.check_extensions_list, exts)
253
254        # second element of each tuple in 'ext_modules'
255        # must be a dictionary (build info)
256        exts = [('foo.bar', '')]
257        self.assertRaises(DistutilsSetupError, cmd.check_extensions_list, exts)
258
259        # ok this one should pass
260        exts = [('foo.bar', {'sources': [''], 'libraries': 'foo',
261                             'some': 'bar'})]
262        cmd.check_extensions_list(exts)
263        ext = exts[0]
264        self.assertIsInstance(ext, Extension)
265
266        # check_extensions_list adds in ext the values passed
267        # when they are in ('include_dirs', 'library_dirs', 'libraries'
268        # 'extra_objects', 'extra_compile_args', 'extra_link_args')
269        self.assertEqual(ext.libraries, 'foo')
270        self.assertFalse(hasattr(ext, 'some'))
271
272        # 'macros' element of build info dict must be 1- or 2-tuple
273        exts = [('foo.bar', {'sources': [''], 'libraries': 'foo',
274                'some': 'bar', 'macros': [('1', '2', '3'), 'foo']})]
275        self.assertRaises(DistutilsSetupError, cmd.check_extensions_list, exts)
276
277        exts[0][1]['macros'] = [('1', '2'), ('3',)]
278        cmd.check_extensions_list(exts)
279        self.assertEqual(exts[0].undef_macros, ['3'])
280        self.assertEqual(exts[0].define_macros, [('1', '2')])
281
282    def test_get_source_files(self):
283        modules = [Extension('foo', ['xxx'], optional=False)]
284        dist = Distribution({'name': 'xx', 'ext_modules': modules})
285        cmd = self.build_ext(dist)
286        cmd.ensure_finalized()
287        self.assertEqual(cmd.get_source_files(), ['xxx'])
288
289    def test_compiler_option(self):
290        # cmd.compiler is an option and
291        # should not be overridden by a compiler instance
292        # when the command is run
293        dist = Distribution()
294        cmd = self.build_ext(dist)
295        cmd.compiler = 'unix'
296        cmd.ensure_finalized()
297        cmd.run()
298        self.assertEqual(cmd.compiler, 'unix')
299
300    def test_get_outputs(self):
301        cmd = support.missing_compiler_executable()
302        if cmd is not None:
303            self.skipTest('The %r command is not found' % cmd)
304        tmp_dir = self.mkdtemp()
305        c_file = os.path.join(tmp_dir, 'foo.c')
306        self.write_file(c_file, 'void PyInit_foo(void) {}\n')
307        ext = Extension('foo', [c_file], optional=False)
308        dist = Distribution({'name': 'xx',
309                             'ext_modules': [ext]})
310        cmd = self.build_ext(dist)
311        fixup_build_ext(cmd)
312        cmd.ensure_finalized()
313        self.assertEqual(len(cmd.get_outputs()), 1)
314
315        cmd.build_lib = os.path.join(self.tmp_dir, 'build')
316        cmd.build_temp = os.path.join(self.tmp_dir, 'tempt')
317
318        # issue #5977 : distutils build_ext.get_outputs
319        # returns wrong result with --inplace
320        other_tmp_dir = os.path.realpath(self.mkdtemp())
321        old_wd = os.getcwd()
322        os.chdir(other_tmp_dir)
323        try:
324            cmd.inplace = 1
325            cmd.run()
326            so_file = cmd.get_outputs()[0]
327        finally:
328            os.chdir(old_wd)
329        self.assertTrue(os.path.exists(so_file))
330        ext_suffix = sysconfig.get_config_var('EXT_SUFFIX')
331        self.assertTrue(so_file.endswith(ext_suffix))
332        so_dir = os.path.dirname(so_file)
333        self.assertEqual(so_dir, other_tmp_dir)
334
335        cmd.inplace = 0
336        cmd.compiler = None
337        cmd.run()
338        so_file = cmd.get_outputs()[0]
339        self.assertTrue(os.path.exists(so_file))
340        self.assertTrue(so_file.endswith(ext_suffix))
341        so_dir = os.path.dirname(so_file)
342        self.assertEqual(so_dir, cmd.build_lib)
343
344        # inplace = 0, cmd.package = 'bar'
345        build_py = cmd.get_finalized_command('build_py')
346        build_py.package_dir = {'': 'bar'}
347        path = cmd.get_ext_fullpath('foo')
348        # checking that the last directory is the build_dir
349        path = os.path.split(path)[0]
350        self.assertEqual(path, cmd.build_lib)
351
352        # inplace = 1, cmd.package = 'bar'
353        cmd.inplace = 1
354        other_tmp_dir = os.path.realpath(self.mkdtemp())
355        old_wd = os.getcwd()
356        os.chdir(other_tmp_dir)
357        try:
358            path = cmd.get_ext_fullpath('foo')
359        finally:
360            os.chdir(old_wd)
361        # checking that the last directory is bar
362        path = os.path.split(path)[0]
363        lastdir = os.path.split(path)[-1]
364        self.assertEqual(lastdir, 'bar')
365
366    def test_ext_fullpath(self):
367        ext = sysconfig.get_config_var('EXT_SUFFIX')
368        # building lxml.etree inplace
369        #etree_c = os.path.join(self.tmp_dir, 'lxml.etree.c')
370        #etree_ext = Extension('lxml.etree', [etree_c])
371        #dist = Distribution({'name': 'lxml', 'ext_modules': [etree_ext]})
372        dist = Distribution()
373        cmd = self.build_ext(dist)
374        cmd.inplace = 1
375        cmd.distribution.package_dir = {'': 'src'}
376        cmd.distribution.packages = ['lxml', 'lxml.html']
377        curdir = os.getcwd()
378        wanted = os.path.join(curdir, 'src', 'lxml', 'etree' + ext)
379        path = cmd.get_ext_fullpath('lxml.etree')
380        self.assertEqual(wanted, path)
381
382        # building lxml.etree not inplace
383        cmd.inplace = 0
384        cmd.build_lib = os.path.join(curdir, 'tmpdir')
385        wanted = os.path.join(curdir, 'tmpdir', 'lxml', 'etree' + ext)
386        path = cmd.get_ext_fullpath('lxml.etree')
387        self.assertEqual(wanted, path)
388
389        # building twisted.runner.portmap not inplace
390        build_py = cmd.get_finalized_command('build_py')
391        build_py.package_dir = {}
392        cmd.distribution.packages = ['twisted', 'twisted.runner.portmap']
393        path = cmd.get_ext_fullpath('twisted.runner.portmap')
394        wanted = os.path.join(curdir, 'tmpdir', 'twisted', 'runner',
395                              'portmap' + ext)
396        self.assertEqual(wanted, path)
397
398        # building twisted.runner.portmap inplace
399        cmd.inplace = 1
400        path = cmd.get_ext_fullpath('twisted.runner.portmap')
401        wanted = os.path.join(curdir, 'twisted', 'runner', 'portmap' + ext)
402        self.assertEqual(wanted, path)
403
404
405    @unittest.skipUnless(sys.platform == 'darwin', 'test only relevant for MacOSX')
406    def test_deployment_target_default(self):
407        # Issue 9516: Test that, in the absence of the environment variable,
408        # an extension module is compiled with the same deployment target as
409        #  the interpreter.
410        self._try_compile_deployment_target('==', None)
411
412    @unittest.skipUnless(sys.platform == 'darwin', 'test only relevant for MacOSX')
413    def test_deployment_target_too_low(self):
414        # Issue 9516: Test that an extension module is not allowed to be
415        # compiled with a deployment target less than that of the interpreter.
416        self.assertRaises(DistutilsPlatformError,
417            self._try_compile_deployment_target, '>', '10.1')
418
419    @unittest.skipUnless(sys.platform == 'darwin', 'test only relevant for MacOSX')
420    def test_deployment_target_higher_ok(self):
421        # Issue 9516: Test that an extension module can be compiled with a
422        # deployment target higher than that of the interpreter: the ext
423        # module may depend on some newer OS feature.
424        deptarget = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET')
425        if deptarget:
426            # increment the minor version number (i.e. 10.6 -> 10.7)
427            deptarget = [int(x) for x in deptarget.split('.')]
428            deptarget[-1] += 1
429            deptarget = '.'.join(str(i) for i in deptarget)
430            self._try_compile_deployment_target('<', deptarget)
431
432    def _try_compile_deployment_target(self, operator, target):
433        orig_environ = os.environ
434        os.environ = orig_environ.copy()
435        self.addCleanup(setattr, os, 'environ', orig_environ)
436
437        if target is None:
438            if os.environ.get('MACOSX_DEPLOYMENT_TARGET'):
439                del os.environ['MACOSX_DEPLOYMENT_TARGET']
440        else:
441            os.environ['MACOSX_DEPLOYMENT_TARGET'] = target
442
443        deptarget_c = os.path.join(self.tmp_dir, 'deptargetmodule.c')
444
445        with open(deptarget_c, 'w') as fp:
446            fp.write(textwrap.dedent('''\
447                #include <AvailabilityMacros.h>
448
449                int dummy;
450
451                #if TARGET %s MAC_OS_X_VERSION_MIN_REQUIRED
452                #else
453                #error "Unexpected target"
454                #endif
455
456            ''' % operator))
457
458        # get the deployment target that the interpreter was built with
459        target = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET')
460        target = tuple(map(int, target.split('.')[0:2]))
461        # format the target value as defined in the Apple
462        # Availability Macros.  We can't use the macro names since
463        # at least one value we test with will not exist yet.
464        if target[1] < 10:
465            # for 10.1 through 10.9.x -> "10n0"
466            target = '%02d%01d0' % target
467        else:
468            # for 10.10 and beyond -> "10nn00"
469            target = '%02d%02d00' % target
470        deptarget_ext = Extension(
471            'deptarget',
472            [deptarget_c],
473            extra_compile_args=['-DTARGET=%s'%(target,)],
474        )
475        dist = Distribution({
476            'name': 'deptarget',
477            'ext_modules': [deptarget_ext]
478        })
479        dist.package_dir = self.tmp_dir
480        cmd = self.build_ext(dist)
481        cmd.build_lib = self.tmp_dir
482        cmd.build_temp = self.tmp_dir
483
484        try:
485            old_stdout = sys.stdout
486            if not support.verbose:
487                # silence compiler output
488                sys.stdout = StringIO()
489            try:
490                cmd.ensure_finalized()
491                cmd.run()
492            finally:
493                sys.stdout = old_stdout
494
495        except CompileError:
496            self.fail("Wrong deployment target during compilation")
497
498
499class ParallelBuildExtTestCase(BuildExtTestCase):
500
501    def build_ext(self, *args, **kwargs):
502        build_ext = super().build_ext(*args, **kwargs)
503        build_ext.parallel = True
504        return build_ext
505
506
507def test_suite():
508    suite = unittest.TestSuite()
509    suite.addTest(unittest.makeSuite(BuildExtTestCase))
510    suite.addTest(unittest.makeSuite(ParallelBuildExtTestCase))
511    return suite
512
513if __name__ == '__main__':
514    support.run_unittest(__name__)
515