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             #Broken after issue 7712(r78136) : add a temp_cwd context manager to test_support ...
     64             #Without current working dir: "...cannot find -lpython27"
     65             #NOTE: [py3k svn r85559] First (uncontroversial) part of issue 9807, barry.warsaw, 2010-10-16 :
     66             #  new _fixup_command is bogus, so we will use own work-around
     67             cmd.library_dirs.insert(0, test_support.SAVEDCWD)
     68             cmd.run()
     69         finally:
     70             sys.stdout = old_stdout
     71 
     72         if ALREADY_TESTED:
     73             return
     74         else:
     75             ALREADY_TESTED = True
     76 
     77         import xx
     78 
     79         for attr in ('error', 'foo', 'new', 'roj'):
     80             self.assertTrue(hasattr(xx, attr))
     81 
     82         self.assertEqual(xx.foo(2, 5), 7)
     83         self.assertEqual(xx.foo(13,15), 28)
     84         self.assertEqual(xx.new().demo(), None)
     85         if test_support.HAVE_DOCSTRINGS:
     86             doc = 'This is a template module just for instruction.'
     87             self.assertEqual(xx.__doc__, doc)
     88         self.assertTrue(isinstance(xx.Null(), xx.Null))
     89         self.assertTrue(isinstance(xx.Str(), xx.Str))
     90 
     91     def test_solaris_enable_shared(self):
     92         dist = Distribution({'name': 'xx'})
     93         cmd = build_ext(dist)
     94         old = sys.platform
     95 
     96         sys.platform = 'sunos' # fooling finalize_options
     97         from distutils.sysconfig import  _config_vars
     98         old_var = _config_vars.get('Py_ENABLE_SHARED')
     99         _config_vars['Py_ENABLE_SHARED'] = 1
    100         try:
    101             cmd.ensure_finalized()
    102         finally:
    103             sys.platform = old
    104             if old_var is None:
    105                 del _config_vars['Py_ENABLE_SHARED']
    106             else:
    107                 _config_vars['Py_ENABLE_SHARED'] = old_var
    108 
    109         # make sure we get some library dirs under solaris
    110         self.assertTrue(len(cmd.library_dirs) > 0)
    111 
    112     def test_user_site(self):
    113         # site.USER_SITE was introduced in 2.6
    114         if sys.version < '2.6':
    115             return
    116 
    117         import site
    118         dist = Distribution({'name': 'xx'})
    119         cmd = build_ext(dist)
    120 
    121         # making sure the user option is there
    122         options = [name for name, short, label in
    123                    cmd.user_options]
    124         self.assertIn('user', options)
    125 
    126         # setting a value
    127         cmd.user = 1
    128 
    129         # setting user based lib and include
    130         lib = os.path.join(site.USER_BASE, 'lib')
    131         incl = os.path.join(site.USER_BASE, 'include')
    132         os.mkdir(lib)
    133         os.mkdir(incl)
    134 
    135         cmd.ensure_finalized()
    136 
    137         # see if include_dirs and library_dirs were set
    138         self.assertIn(lib, cmd.library_dirs)
    139         self.assertIn(lib, cmd.rpath)
    140         self.assertIn(incl, cmd.include_dirs)
    141 
    142     def test_finalize_options(self):
    143         # Make sure Python's include directories (for Python.h, pyconfig.h,
    144         # etc.) are in the include search path.
    145         modules = [Extension('foo', ['xxx'])]
    146         dist = Distribution({'name': 'xx', 'ext_modules': modules})
    147         cmd = build_ext(dist)
    148         cmd.finalize_options()
    149 
    150         py_include = sysconfig.get_python_inc()
    151         self.assertTrue(py_include in cmd.include_dirs)
    152 
    153         plat_py_include = sysconfig.get_python_inc(plat_specific=1)
    154         self.assertTrue(plat_py_include in cmd.include_dirs)
    155 
    156         # make sure cmd.libraries is turned into a list
    157         # if it's a string
    158         cmd = build_ext(dist)
    159         cmd.libraries = 'my_lib, other_lib lastlib'
    160         cmd.finalize_options()
    161         self.assertEqual(cmd.libraries, ['my_lib', 'other_lib', 'lastlib'])
    162 
    163         # make sure cmd.library_dirs is turned into a list
    164         # if it's a string
    165         cmd = build_ext(dist)
    166         cmd.library_dirs = 'my_lib_dir%sother_lib_dir' % os.pathsep
    167         cmd.finalize_options()
    168         self.assertIn('my_lib_dir', cmd.library_dirs)
    169         self.assertIn('other_lib_dir', cmd.library_dirs)
    170 
    171         # make sure rpath is turned into a list
    172         # if it's a string
    173         cmd = build_ext(dist)
    174         cmd.rpath = 'one%stwo' % os.pathsep
    175         cmd.finalize_options()
    176         self.assertEqual(cmd.rpath, ['one', 'two'])
    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 ary (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.assertTrue(isinstance(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.assertTrue(not 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 overriden 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         #Without current working dir: "...cannot find -lpython27"
    290         #NOTE: After issue #7712(r78136) test cannot use old_wd !
    291         #cmd.library_dirs.insert(0, old_wd)
    292         #NOTE: [py3k svn r85559] First (uncontroversial) part of issue 9807, barry.warsaw, 2010-10-16 :
    293         #  new _fixup_command is bogus, so we will use own work-around
    294         cmd.library_dirs.insert(0, test_support.SAVEDCWD)
    295         os.chdir(other_tmp_dir)
    296         try:
    297             cmd.inplace = 1
    298             cmd.run()
    299             so_file = cmd.get_outputs()[0]
    300         finally:
    301             os.chdir(old_wd)
    302         self.assertTrue(os.path.exists(so_file))
    303         self.assertEqual(os.path.splitext(so_file)[-1],
    304                          sysconfig.get_config_var('SO'))
    305         so_dir = os.path.dirname(so_file)
    306         self.assertEqual(so_dir, other_tmp_dir)
    307         cmd.compiler = None
    308         cmd.inplace = 0
    309         cmd.run()
    310         so_file = cmd.get_outputs()[0]
    311         self.assertTrue(os.path.exists(so_file))
    312         self.assertEqual(os.path.splitext(so_file)[-1],
    313                          sysconfig.get_config_var('SO'))
    314         so_dir = os.path.dirname(so_file)
    315         self.assertEqual(so_dir, cmd.build_lib)
    316 
    317         # inplace = 0, cmd.package = 'bar'
    318         build_py = cmd.get_finalized_command('build_py')
    319         build_py.package_dir = {'': 'bar'}
    320         path = cmd.get_ext_fullpath('foo')
    321         # checking that the last directory is the build_dir
    322         path = os.path.split(path)[0]
    323         self.assertEqual(path, cmd.build_lib)
    324 
    325         # inplace = 1, cmd.package = 'bar'
    326         cmd.inplace = 1
    327         other_tmp_dir = os.path.realpath(self.mkdtemp())
    328         old_wd = os.getcwd()
    329         os.chdir(other_tmp_dir)
    330         try:
    331             path = cmd.get_ext_fullpath('foo')
    332         finally:
    333             os.chdir(old_wd)
    334         # checking that the last directory is bar
    335         path = os.path.split(path)[0]
    336         lastdir = os.path.split(path)[-1]
    337         self.assertEqual(lastdir, 'bar')
    338 
    339     def test_ext_fullpath(self):
    340         ext = sysconfig.get_config_vars()['SO']
    341         dist = Distribution()
    342         cmd = build_ext(dist)
    343         cmd.inplace = 1
    344         cmd.distribution.package_dir = {'': 'src'}
    345         cmd.distribution.packages = ['lxml', 'lxml.html']
    346         curdir = os.getcwd()
    347         wanted = os.path.join(curdir, 'src', 'lxml', 'etree' + ext)
    348         path = cmd.get_ext_fullpath('lxml.etree')
    349         self.assertEqual(wanted, path)
    350 
    351         # building lxml.etree not inplace
    352         cmd.inplace = 0
    353         cmd.build_lib = os.path.join(curdir, 'tmpdir')
    354         wanted = os.path.join(curdir, 'tmpdir', 'lxml', 'etree' + ext)
    355         path = cmd.get_ext_fullpath('lxml.etree')
    356         self.assertEqual(wanted, path)
    357 
    358         # building twisted.runner.portmap not inplace
    359         build_py = cmd.get_finalized_command('build_py')
    360         build_py.package_dir = {}
    361         cmd.distribution.packages = ['twisted', 'twisted.runner.portmap']
    362         path = cmd.get_ext_fullpath('twisted.runner.portmap')
    363         wanted = os.path.join(curdir, 'tmpdir', 'twisted', 'runner',
    364                               'portmap' + ext)
    365         self.assertEqual(wanted, path)
    366 
    367         # building twisted.runner.portmap inplace
    368         cmd.inplace = 1
    369         path = cmd.get_ext_fullpath('twisted.runner.portmap')
    370         wanted = os.path.join(curdir, 'twisted', 'runner', 'portmap' + ext)
    371         self.assertEqual(wanted, path)
    372 
    373     def test_build_ext_inplace(self):
    374         etree_c = os.path.join(self.tmp_dir, 'lxml.etree.c')
    375         etree_ext = Extension('lxml.etree', [etree_c])
    376         dist = Distribution({'name': 'lxml', 'ext_modules': [etree_ext]})
    377         cmd = build_ext(dist)
    378         cmd.ensure_finalized()
    379         cmd.inplace = 1
    380         cmd.distribution.package_dir = {'': 'src'}
    381         cmd.distribution.packages = ['lxml', 'lxml.html']
    382         curdir = os.getcwd()
    383         ext = sysconfig.get_config_var("SO")
    384         wanted = os.path.join(curdir, 'src', 'lxml', 'etree' + ext)
    385         path = cmd.get_ext_fullpath('lxml.etree')
    386         self.assertEqual(wanted, path)
    387 
    388     def test_setuptools_compat(self):
    389         import distutils.core, distutils.extension, distutils.command.build_ext
    390         saved_ext = distutils.extension.Extension
    391         try:
    392             # on some platforms, it loads the deprecated "dl" module
    393             test_support.import_module('setuptools_build_ext', deprecated=True)
    394 
    395             # theses import patch Distutils' Extension class
    396             from setuptools_build_ext import build_ext as setuptools_build_ext
    397             from setuptools_extension import Extension
    398 
    399             etree_c = os.path.join(self.tmp_dir, 'lxml.etree.c')
    400             etree_ext = Extension('lxml.etree', [etree_c])
    401             dist = Distribution({'name': 'lxml', 'ext_modules': [etree_ext]})
    402             cmd = setuptools_build_ext(dist)
    403             cmd.ensure_finalized()
    404             cmd.inplace = 1
    405             cmd.distribution.package_dir = {'': 'src'}
    406             cmd.distribution.packages = ['lxml', 'lxml.html']
    407             curdir = os.getcwd()
    408             ext = sysconfig.get_config_var("SO")
    409             wanted = os.path.join(curdir, 'src', 'lxml', 'etree' + ext)
    410             path = cmd.get_ext_fullpath('lxml.etree')
    411             self.assertEqual(wanted, path)
    412         finally:
    413             # restoring Distutils' Extension class otherwise its broken
    414             distutils.extension.Extension = saved_ext
    415             distutils.core.Extension = saved_ext
    416             distutils.command.build_ext.Extension = saved_ext
    417 
    418     def test_build_ext_path_with_os_sep(self):
    419         dist = Distribution({'name': 'UpdateManager'})
    420         cmd = build_ext(dist)
    421         cmd.ensure_finalized()
    422         ext = sysconfig.get_config_var("SO")
    423         ext_name = os.path.join('UpdateManager', 'fdsend')
    424         ext_path = cmd.get_ext_fullpath(ext_name)
    425         wanted = os.path.join(cmd.build_lib, 'UpdateManager', 'fdsend' + ext)
    426         self.assertEqual(ext_path, wanted)
    427 
    428     def test_build_ext_path_cross_platform(self):
    429         if sys.platform != 'win32':
    430             return
    431         dist = Distribution({'name': 'UpdateManager'})
    432         cmd = build_ext(dist)
    433         cmd.ensure_finalized()
    434         ext = sysconfig.get_config_var("SO")
    435         # this needs to work even under win32
    436         ext_name = 'UpdateManager/fdsend'
    437         ext_path = cmd.get_ext_fullpath(ext_name)
    438         wanted = os.path.join(cmd.build_lib, 'UpdateManager', 'fdsend' + ext)
    439         self.assertEqual(ext_path, wanted)
    440 
    441     @unittest.skipUnless(sys.platform == 'darwin', 'test only relevant for MacOSX')
    442     def test_deployment_target_default(self):
    443         # Issue 9516: Test that, in the absence of the environment variable,
    444         # an extension module is compiled with the same deployment target as
    445         #  the interpreter.
    446         self._try_compile_deployment_target('==', None)
    447 
    448     @unittest.skipUnless(sys.platform == 'darwin', 'test only relevant for MacOSX')
    449     def test_deployment_target_too_low(self):
    450         # Issue 9516: Test that an extension module is not allowed to be
    451         # compiled with a deployment target less than that of the interpreter.
    452         self.assertRaises(DistutilsPlatformError,
    453             self._try_compile_deployment_target, '>', '10.1')
    454 
    455     @unittest.skipUnless(sys.platform == 'darwin', 'test only relevant for MacOSX')
    456     def test_deployment_target_higher_ok(self):
    457         # Issue 9516: Test that an extension module can be compiled with a
    458         # deployment target higher than that of the interpreter: the ext
    459         # module may depend on some newer OS feature.
    460         deptarget = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET')
    461         if deptarget:
    462             # increment the minor version number (i.e. 10.6 -> 10.7)
    463             deptarget = [int(x) for x in deptarget.split('.')]
    464             deptarget[-1] += 1
    465             deptarget = '.'.join(str(i) for i in deptarget)
    466             self._try_compile_deployment_target('<', deptarget)
    467 
    468     def _try_compile_deployment_target(self, operator, target):
    469         orig_environ = os.environ
    470         os.environ = orig_environ.copy()
    471         self.addCleanup(setattr, os, 'environ', orig_environ)
    472 
    473         if target is None:
    474             if os.environ.get('MACOSX_DEPLOYMENT_TARGET'):
    475                 del os.environ['MACOSX_DEPLOYMENT_TARGET']
    476         else:
    477             os.environ['MACOSX_DEPLOYMENT_TARGET'] = target
    478 
    479         deptarget_c = os.path.join(self.tmp_dir, 'deptargetmodule.c')
    480 
    481         with open(deptarget_c, 'w') as fp:
    482             fp.write(textwrap.dedent('''\
    483                 #include <AvailabilityMacros.h>
    484 
    485                 int dummy;
    486 
    487                 #if TARGET %s MAC_OS_X_VERSION_MIN_REQUIRED
    488                 #else
    489                 #error "Unexpected target"
    490                 #endif
    491 
    492             ''' % operator))
    493 
    494         # get the deployment target that the interpreter was built with
    495         target = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET')
    496         target = tuple(map(int, target.split('.')))
    497         target = '%02d%01d0' % target
    498         deptarget_ext = Extension(
    499             'deptarget',
    500             [deptarget_c],
    501             extra_compile_args=['-DTARGET=%s'%(target,)],
    502         )
    503         dist = Distribution({
    504             'name': 'deptarget',
    505             'ext_modules': [deptarget_ext]
    506         })
    507         dist.package_dir = self.tmp_dir
    508         cmd = build_ext(dist)
    509         cmd.build_lib = self.tmp_dir
    510         cmd.build_temp = self.tmp_dir
    511 
    512         try:
    513             cmd.ensure_finalized()
    514             cmd.run()
    515         except CompileError:
    516             self.fail("Wrong deployment target during compilation")
    517 
    518 def test_suite():
    519     return unittest.makeSuite(BuildExtTestCase)
    520 
    521 if __name__ == '__main__':
    522     test_support.run_unittest(test_suite())
    523