test_build_ext.py 19 KB

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