Home | History | Annotate | Download | only in tests
      1 import sys
      2 import os
      3 from StringIO import StringIO
      4 import textwrap
      5 
      6 from distutils.core import Extension, Distribution
      7 from distutils.command.build_ext import build_ext
      8 from distutils import sysconfig
      9 from distutils.tests import support
     10 from distutils.errors import (DistutilsSetupError, CompileError,
     11                               DistutilsPlatformError)
     12 
     13 import unittest
     14 from test import test_support
     15 
     16 # http://bugs.python.org/issue4373
     17 # Don't load the xx module more than once.
     18 ALREADY_TESTED = False
     19 
     20 
     21 class 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 
    519 def test_suite():
    520     return unittest.makeSuite(BuildExtTestCase)
    521 
    522 if __name__ == '__main__':
    523     test_support.run_unittest(test_suite())
    524