test_build_ext.py 20 KB

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