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