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