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