test_build_ext.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. import contextlib
  2. import glob
  3. import importlib
  4. import os.path
  5. import platform
  6. import re
  7. import shutil
  8. import site
  9. import subprocess
  10. import sys
  11. import tempfile
  12. import textwrap
  13. import time
  14. from distutils import sysconfig
  15. from distutils.command.build_ext import build_ext
  16. from distutils.core import Distribution
  17. from distutils.errors import (
  18. CompileError,
  19. DistutilsPlatformError,
  20. DistutilsSetupError,
  21. UnknownFileError,
  22. )
  23. from distutils.extension import Extension
  24. from distutils.tests import missing_compiler_executable
  25. from distutils.tests.support import TempdirManager, copy_xxmodule_c, fixup_build_ext
  26. from io import StringIO
  27. import jaraco.path
  28. import path
  29. import pytest
  30. from test import support
  31. from .compat import py39 as import_helper
  32. @pytest.fixture()
  33. def user_site_dir(request):
  34. self = request.instance
  35. self.tmp_dir = self.mkdtemp()
  36. self.tmp_path = path.Path(self.tmp_dir)
  37. from distutils.command import build_ext
  38. orig_user_base = site.USER_BASE
  39. site.USER_BASE = self.mkdtemp()
  40. build_ext.USER_BASE = site.USER_BASE
  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. with self.tmp_path:
  45. yield
  46. site.USER_BASE = orig_user_base
  47. build_ext.USER_BASE = orig_user_base
  48. if sys.platform == 'cygwin':
  49. time.sleep(1)
  50. @contextlib.contextmanager
  51. def safe_extension_import(name, path):
  52. with import_helper.CleanImport(name):
  53. with extension_redirect(name, path) as new_path:
  54. with import_helper.DirsOnSysPath(new_path):
  55. yield
  56. @contextlib.contextmanager
  57. def extension_redirect(mod, path):
  58. """
  59. Tests will fail to tear down an extension module if it's been imported.
  60. Before importing, copy the file to a temporary directory that won't
  61. be cleaned up. Yield the new path.
  62. """
  63. if platform.system() != "Windows" and sys.platform != "cygwin":
  64. yield path
  65. return
  66. with import_helper.DirsOnSysPath(path):
  67. spec = importlib.util.find_spec(mod)
  68. filename = os.path.basename(spec.origin)
  69. trash_dir = tempfile.mkdtemp(prefix='deleteme')
  70. dest = os.path.join(trash_dir, os.path.basename(filename))
  71. shutil.copy(spec.origin, dest)
  72. yield trash_dir
  73. # TODO: can the file be scheduled for deletion?
  74. @pytest.mark.usefixtures('user_site_dir')
  75. class TestBuildExt(TempdirManager):
  76. def build_ext(self, *args, **kwargs):
  77. return build_ext(*args, **kwargs)
  78. @pytest.mark.parametrize("copy_so", [False])
  79. def test_build_ext(self, copy_so):
  80. missing_compiler_executable()
  81. copy_xxmodule_c(self.tmp_dir)
  82. xx_c = os.path.join(self.tmp_dir, 'xxmodule.c')
  83. xx_ext = Extension('xx', [xx_c])
  84. if sys.platform != "win32":
  85. if not copy_so:
  86. xx_ext = Extension(
  87. 'xx',
  88. [xx_c],
  89. library_dirs=['/usr/lib'],
  90. libraries=['z'],
  91. runtime_library_dirs=['/usr/lib'],
  92. )
  93. elif sys.platform == 'linux':
  94. libz_so = {
  95. os.path.realpath(name) for name in glob.iglob('/usr/lib*/libz.so*')
  96. }
  97. libz_so = sorted(libz_so, key=lambda lib_path: len(lib_path))
  98. shutil.copyfile(libz_so[-1], '/tmp/libxx_z.so')
  99. xx_ext = Extension(
  100. 'xx',
  101. [xx_c],
  102. library_dirs=['/tmp'],
  103. libraries=['xx_z'],
  104. runtime_library_dirs=['/tmp'],
  105. )
  106. dist = Distribution({'name': 'xx', 'ext_modules': [xx_ext]})
  107. dist.package_dir = self.tmp_dir
  108. cmd = self.build_ext(dist)
  109. fixup_build_ext(cmd)
  110. cmd.build_lib = self.tmp_dir
  111. cmd.build_temp = self.tmp_dir
  112. old_stdout = sys.stdout
  113. if not support.verbose:
  114. # silence compiler output
  115. sys.stdout = StringIO()
  116. try:
  117. cmd.ensure_finalized()
  118. cmd.run()
  119. finally:
  120. sys.stdout = old_stdout
  121. with safe_extension_import('xx', self.tmp_dir):
  122. self._test_xx(copy_so)
  123. if sys.platform == 'linux' and copy_so:
  124. os.unlink('/tmp/libxx_z.so')
  125. @staticmethod
  126. def _test_xx(copy_so):
  127. import xx # type: ignore[import-not-found] # Module generated for tests
  128. for attr in ('error', 'foo', 'new', 'roj'):
  129. assert hasattr(xx, attr)
  130. assert xx.foo(2, 5) == 7
  131. assert xx.foo(13, 15) == 28
  132. assert xx.new().demo() is None
  133. if support.HAVE_DOCSTRINGS:
  134. doc = 'This is a template module just for instruction.'
  135. assert xx.__doc__ == doc
  136. assert isinstance(xx.Null(), xx.Null)
  137. assert isinstance(xx.Str(), xx.Str)
  138. if sys.platform == 'linux':
  139. so_headers = subprocess.check_output(
  140. ["readelf", "-d", xx.__file__], universal_newlines=True
  141. )
  142. import pprint
  143. pprint.pprint(so_headers)
  144. rpaths = [
  145. rpath
  146. for line in so_headers.split("\n")
  147. if "RPATH" in line or "RUNPATH" in line
  148. for rpath in line.split()[2][1:-1].split(":")
  149. ]
  150. if not copy_so:
  151. pprint.pprint(rpaths)
  152. # Linked against a library in /usr/lib{,64}
  153. assert "/usr/lib" not in rpaths and "/usr/lib64" not in rpaths
  154. else:
  155. # Linked against a library in /tmp
  156. assert "/tmp" in rpaths
  157. # The import is the real test here
  158. def test_solaris_enable_shared(self):
  159. dist = Distribution({'name': 'xx'})
  160. cmd = self.build_ext(dist)
  161. old = sys.platform
  162. sys.platform = 'sunos' # fooling finalize_options
  163. from distutils.sysconfig import _config_vars
  164. old_var = _config_vars.get('Py_ENABLE_SHARED')
  165. _config_vars['Py_ENABLE_SHARED'] = True
  166. try:
  167. cmd.ensure_finalized()
  168. finally:
  169. sys.platform = old
  170. if old_var is None:
  171. del _config_vars['Py_ENABLE_SHARED']
  172. else:
  173. _config_vars['Py_ENABLE_SHARED'] = old_var
  174. # make sure we get some library dirs under solaris
  175. assert len(cmd.library_dirs) > 0
  176. def test_user_site(self):
  177. import site
  178. dist = Distribution({'name': 'xx'})
  179. cmd = self.build_ext(dist)
  180. # making sure the user option is there
  181. options = [name for name, short, label in cmd.user_options]
  182. assert 'user' in options
  183. # setting a value
  184. cmd.user = True
  185. # setting user based lib and include
  186. lib = os.path.join(site.USER_BASE, 'lib')
  187. incl = os.path.join(site.USER_BASE, 'include')
  188. os.mkdir(lib)
  189. os.mkdir(incl)
  190. # let's run finalize
  191. cmd.ensure_finalized()
  192. # see if include_dirs and library_dirs
  193. # were set
  194. assert lib in cmd.library_dirs
  195. assert lib in cmd.rpath
  196. assert incl in cmd.include_dirs
  197. def test_optional_extension(self):
  198. # this extension will fail, but let's ignore this failure
  199. # with the optional argument.
  200. modules = [Extension('foo', ['xxx'], optional=False)]
  201. dist = Distribution({'name': 'xx', 'ext_modules': modules})
  202. cmd = self.build_ext(dist)
  203. cmd.ensure_finalized()
  204. with pytest.raises((UnknownFileError, CompileError)):
  205. cmd.run() # should raise an error
  206. modules = [Extension('foo', ['xxx'], optional=True)]
  207. dist = Distribution({'name': 'xx', 'ext_modules': modules})
  208. cmd = self.build_ext(dist)
  209. cmd.ensure_finalized()
  210. cmd.run() # should pass
  211. def test_finalize_options(self):
  212. # Make sure Python's include directories (for Python.h, pyconfig.h,
  213. # etc.) are in the include search path.
  214. modules = [Extension('foo', ['xxx'], optional=False)]
  215. dist = Distribution({'name': 'xx', 'ext_modules': modules})
  216. cmd = self.build_ext(dist)
  217. cmd.finalize_options()
  218. py_include = sysconfig.get_python_inc()
  219. for p in py_include.split(os.path.pathsep):
  220. assert p in cmd.include_dirs
  221. plat_py_include = sysconfig.get_python_inc(plat_specific=True)
  222. for p in plat_py_include.split(os.path.pathsep):
  223. assert p in cmd.include_dirs
  224. # make sure cmd.libraries is turned into a list
  225. # if it's a string
  226. cmd = self.build_ext(dist)
  227. cmd.libraries = 'my_lib, other_lib lastlib'
  228. cmd.finalize_options()
  229. assert cmd.libraries == ['my_lib', 'other_lib', 'lastlib']
  230. # make sure cmd.library_dirs is turned into a list
  231. # if it's a string
  232. cmd = self.build_ext(dist)
  233. cmd.library_dirs = f'my_lib_dir{os.pathsep}other_lib_dir'
  234. cmd.finalize_options()
  235. assert 'my_lib_dir' in cmd.library_dirs
  236. assert 'other_lib_dir' in cmd.library_dirs
  237. # make sure rpath is turned into a list
  238. # if it's a string
  239. cmd = self.build_ext(dist)
  240. cmd.rpath = f'one{os.pathsep}two'
  241. cmd.finalize_options()
  242. assert cmd.rpath == ['one', 'two']
  243. # make sure cmd.link_objects is turned into a list
  244. # if it's a string
  245. cmd = build_ext(dist)
  246. cmd.link_objects = 'one two,three'
  247. cmd.finalize_options()
  248. assert cmd.link_objects == ['one', 'two', 'three']
  249. # XXX more tests to perform for win32
  250. # make sure define is turned into 2-tuples
  251. # strings if they are ','-separated strings
  252. cmd = self.build_ext(dist)
  253. cmd.define = 'one,two'
  254. cmd.finalize_options()
  255. assert cmd.define == [('one', '1'), ('two', '1')]
  256. # make sure undef is turned into a list of
  257. # strings if they are ','-separated strings
  258. cmd = self.build_ext(dist)
  259. cmd.undef = 'one,two'
  260. cmd.finalize_options()
  261. assert cmd.undef == ['one', 'two']
  262. # make sure swig_opts is turned into a list
  263. cmd = self.build_ext(dist)
  264. cmd.swig_opts = None
  265. cmd.finalize_options()
  266. assert cmd.swig_opts == []
  267. cmd = self.build_ext(dist)
  268. cmd.swig_opts = '1 2'
  269. cmd.finalize_options()
  270. assert cmd.swig_opts == ['1', '2']
  271. def test_check_extensions_list(self):
  272. dist = Distribution()
  273. cmd = self.build_ext(dist)
  274. cmd.finalize_options()
  275. # 'extensions' option must be a list of Extension instances
  276. with pytest.raises(DistutilsSetupError):
  277. cmd.check_extensions_list('foo')
  278. # each element of 'ext_modules' option must be an
  279. # Extension instance or 2-tuple
  280. exts = [('bar', 'foo', 'bar'), 'foo']
  281. with pytest.raises(DistutilsSetupError):
  282. cmd.check_extensions_list(exts)
  283. # first element of each tuple in 'ext_modules'
  284. # must be the extension name (a string) and match
  285. # a python dotted-separated name
  286. exts = [('foo-bar', '')]
  287. with pytest.raises(DistutilsSetupError):
  288. cmd.check_extensions_list(exts)
  289. # second element of each tuple in 'ext_modules'
  290. # must be a dictionary (build info)
  291. exts = [('foo.bar', '')]
  292. with pytest.raises(DistutilsSetupError):
  293. cmd.check_extensions_list(exts)
  294. # ok this one should pass
  295. exts = [('foo.bar', {'sources': [''], 'libraries': 'foo', 'some': 'bar'})]
  296. cmd.check_extensions_list(exts)
  297. ext = exts[0]
  298. assert isinstance(ext, Extension)
  299. # check_extensions_list adds in ext the values passed
  300. # when they are in ('include_dirs', 'library_dirs', 'libraries'
  301. # 'extra_objects', 'extra_compile_args', 'extra_link_args')
  302. assert ext.libraries == 'foo'
  303. assert not hasattr(ext, 'some')
  304. # 'macros' element of build info dict must be 1- or 2-tuple
  305. exts = [
  306. (
  307. 'foo.bar',
  308. {
  309. 'sources': [''],
  310. 'libraries': 'foo',
  311. 'some': 'bar',
  312. 'macros': [('1', '2', '3'), 'foo'],
  313. },
  314. )
  315. ]
  316. with pytest.raises(DistutilsSetupError):
  317. cmd.check_extensions_list(exts)
  318. exts[0][1]['macros'] = [('1', '2'), ('3',)]
  319. cmd.check_extensions_list(exts)
  320. assert exts[0].undef_macros == ['3']
  321. assert exts[0].define_macros == [('1', '2')]
  322. def test_get_source_files(self):
  323. modules = [Extension('foo', ['xxx'], optional=False)]
  324. dist = Distribution({'name': 'xx', 'ext_modules': modules})
  325. cmd = self.build_ext(dist)
  326. cmd.ensure_finalized()
  327. assert cmd.get_source_files() == ['xxx']
  328. def test_unicode_module_names(self):
  329. modules = [
  330. Extension('foo', ['aaa'], optional=False),
  331. Extension('föö', ['uuu'], optional=False),
  332. ]
  333. dist = Distribution({'name': 'xx', 'ext_modules': modules})
  334. cmd = self.build_ext(dist)
  335. cmd.ensure_finalized()
  336. assert re.search(r'foo(_d)?\..*', cmd.get_ext_filename(modules[0].name))
  337. assert re.search(r'föö(_d)?\..*', cmd.get_ext_filename(modules[1].name))
  338. assert cmd.get_export_symbols(modules[0]) == ['PyInit_foo']
  339. assert cmd.get_export_symbols(modules[1]) == ['PyInitU_f_1gaa']
  340. def test_export_symbols__init__(self):
  341. # https://github.com/python/cpython/issues/80074
  342. # https://github.com/pypa/setuptools/issues/4826
  343. modules = [
  344. Extension('foo.__init__', ['aaa']),
  345. Extension('föö.__init__', ['uuu']),
  346. ]
  347. dist = Distribution({'name': 'xx', 'ext_modules': modules})
  348. cmd = self.build_ext(dist)
  349. cmd.ensure_finalized()
  350. assert cmd.get_export_symbols(modules[0]) == ['PyInit_foo']
  351. assert cmd.get_export_symbols(modules[1]) == ['PyInitU_f_1gaa']
  352. def test_compiler_option(self):
  353. # cmd.compiler is an option and
  354. # should not be overridden by a compiler instance
  355. # when the command is run
  356. dist = Distribution()
  357. cmd = self.build_ext(dist)
  358. cmd.compiler = 'unix'
  359. cmd.ensure_finalized()
  360. cmd.run()
  361. assert cmd.compiler == 'unix'
  362. def test_get_outputs(self):
  363. missing_compiler_executable()
  364. tmp_dir = self.mkdtemp()
  365. c_file = os.path.join(tmp_dir, 'foo.c')
  366. self.write_file(c_file, 'void PyInit_foo(void) {}\n')
  367. ext = Extension('foo', [c_file], optional=False)
  368. dist = Distribution({'name': 'xx', 'ext_modules': [ext]})
  369. cmd = self.build_ext(dist)
  370. fixup_build_ext(cmd)
  371. cmd.ensure_finalized()
  372. assert len(cmd.get_outputs()) == 1
  373. cmd.build_lib = os.path.join(self.tmp_dir, 'build')
  374. cmd.build_temp = os.path.join(self.tmp_dir, 'tempt')
  375. # issue #5977 : distutils build_ext.get_outputs
  376. # returns wrong result with --inplace
  377. other_tmp_dir = os.path.realpath(self.mkdtemp())
  378. old_wd = os.getcwd()
  379. os.chdir(other_tmp_dir)
  380. try:
  381. cmd.inplace = True
  382. cmd.run()
  383. so_file = cmd.get_outputs()[0]
  384. finally:
  385. os.chdir(old_wd)
  386. assert os.path.exists(so_file)
  387. ext_suffix = sysconfig.get_config_var('EXT_SUFFIX')
  388. assert so_file.endswith(ext_suffix)
  389. so_dir = os.path.dirname(so_file)
  390. assert so_dir == other_tmp_dir
  391. cmd.inplace = False
  392. cmd.compiler = None
  393. cmd.run()
  394. so_file = cmd.get_outputs()[0]
  395. assert os.path.exists(so_file)
  396. assert so_file.endswith(ext_suffix)
  397. so_dir = os.path.dirname(so_file)
  398. assert so_dir == cmd.build_lib
  399. # inplace = False, cmd.package = 'bar'
  400. build_py = cmd.get_finalized_command('build_py')
  401. build_py.package_dir = {'': 'bar'}
  402. path = cmd.get_ext_fullpath('foo')
  403. # checking that the last directory is the build_dir
  404. path = os.path.split(path)[0]
  405. assert path == cmd.build_lib
  406. # inplace = True, cmd.package = 'bar'
  407. cmd.inplace = True
  408. other_tmp_dir = os.path.realpath(self.mkdtemp())
  409. old_wd = os.getcwd()
  410. os.chdir(other_tmp_dir)
  411. try:
  412. path = cmd.get_ext_fullpath('foo')
  413. finally:
  414. os.chdir(old_wd)
  415. # checking that the last directory is bar
  416. path = os.path.split(path)[0]
  417. lastdir = os.path.split(path)[-1]
  418. assert lastdir == 'bar'
  419. def test_ext_fullpath(self):
  420. ext = sysconfig.get_config_var('EXT_SUFFIX')
  421. # building lxml.etree inplace
  422. # etree_c = os.path.join(self.tmp_dir, 'lxml.etree.c')
  423. # etree_ext = Extension('lxml.etree', [etree_c])
  424. # dist = Distribution({'name': 'lxml', 'ext_modules': [etree_ext]})
  425. dist = Distribution()
  426. cmd = self.build_ext(dist)
  427. cmd.inplace = True
  428. cmd.distribution.package_dir = {'': 'src'}
  429. cmd.distribution.packages = ['lxml', 'lxml.html']
  430. curdir = os.getcwd()
  431. wanted = os.path.join(curdir, 'src', 'lxml', 'etree' + ext)
  432. path = cmd.get_ext_fullpath('lxml.etree')
  433. assert wanted == path
  434. # building lxml.etree not inplace
  435. cmd.inplace = False
  436. cmd.build_lib = os.path.join(curdir, 'tmpdir')
  437. wanted = os.path.join(curdir, 'tmpdir', 'lxml', 'etree' + ext)
  438. path = cmd.get_ext_fullpath('lxml.etree')
  439. assert wanted == path
  440. # building twisted.runner.portmap not inplace
  441. build_py = cmd.get_finalized_command('build_py')
  442. build_py.package_dir = {}
  443. cmd.distribution.packages = ['twisted', 'twisted.runner.portmap']
  444. path = cmd.get_ext_fullpath('twisted.runner.portmap')
  445. wanted = os.path.join(curdir, 'tmpdir', 'twisted', 'runner', 'portmap' + ext)
  446. assert wanted == path
  447. # building twisted.runner.portmap inplace
  448. cmd.inplace = True
  449. path = cmd.get_ext_fullpath('twisted.runner.portmap')
  450. wanted = os.path.join(curdir, 'twisted', 'runner', 'portmap' + ext)
  451. assert wanted == path
  452. @pytest.mark.skipif('platform.system() != "Darwin"')
  453. @pytest.mark.usefixtures('save_env')
  454. def test_deployment_target_default(self):
  455. # Issue 9516: Test that, in the absence of the environment variable,
  456. # an extension module is compiled with the same deployment target as
  457. # the interpreter.
  458. self._try_compile_deployment_target('==', None)
  459. @pytest.mark.skipif('platform.system() != "Darwin"')
  460. @pytest.mark.usefixtures('save_env')
  461. def test_deployment_target_too_low(self):
  462. # Issue 9516: Test that an extension module is not allowed to be
  463. # compiled with a deployment target less than that of the interpreter.
  464. with pytest.raises(DistutilsPlatformError):
  465. self._try_compile_deployment_target('>', '10.1')
  466. @pytest.mark.skipif('platform.system() != "Darwin"')
  467. @pytest.mark.usefixtures('save_env')
  468. def test_deployment_target_higher_ok(self): # pragma: no cover
  469. # Issue 9516: Test that an extension module can be compiled with a
  470. # deployment target higher than that of the interpreter: the ext
  471. # module may depend on some newer OS feature.
  472. deptarget = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET')
  473. if deptarget:
  474. # increment the minor version number (i.e. 10.6 -> 10.7)
  475. deptarget = [int(x) for x in deptarget.split('.')]
  476. deptarget[-1] += 1
  477. deptarget = '.'.join(str(i) for i in deptarget)
  478. self._try_compile_deployment_target('<', deptarget)
  479. def _try_compile_deployment_target(self, operator, target): # pragma: no cover
  480. if target is None:
  481. if os.environ.get('MACOSX_DEPLOYMENT_TARGET'):
  482. del os.environ['MACOSX_DEPLOYMENT_TARGET']
  483. else:
  484. os.environ['MACOSX_DEPLOYMENT_TARGET'] = target
  485. jaraco.path.build(
  486. {
  487. 'deptargetmodule.c': textwrap.dedent(f"""\
  488. #include <AvailabilityMacros.h>
  489. int dummy;
  490. #if TARGET {operator} MAC_OS_X_VERSION_MIN_REQUIRED
  491. #else
  492. #error "Unexpected target"
  493. #endif
  494. """),
  495. },
  496. self.tmp_path,
  497. )
  498. # get the deployment target that the interpreter was built with
  499. target = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET')
  500. target = tuple(map(int, target.split('.')[0:2]))
  501. # format the target value as defined in the Apple
  502. # Availability Macros. We can't use the macro names since
  503. # at least one value we test with will not exist yet.
  504. if target[:2] < (10, 10):
  505. # for 10.1 through 10.9.x -> "10n0"
  506. tmpl = '{:02}{:01}0'
  507. else:
  508. # for 10.10 and beyond -> "10nn00"
  509. if len(target) >= 2:
  510. tmpl = '{:02}{:02}00'
  511. else:
  512. # 11 and later can have no minor version (11 instead of 11.0)
  513. tmpl = '{:02}0000'
  514. target = tmpl.format(*target)
  515. deptarget_ext = Extension(
  516. 'deptarget',
  517. [self.tmp_path / 'deptargetmodule.c'],
  518. extra_compile_args=[f'-DTARGET={target}'],
  519. )
  520. dist = Distribution({'name': 'deptarget', 'ext_modules': [deptarget_ext]})
  521. dist.package_dir = self.tmp_dir
  522. cmd = self.build_ext(dist)
  523. cmd.build_lib = self.tmp_dir
  524. cmd.build_temp = self.tmp_dir
  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. except CompileError:
  536. self.fail("Wrong deployment target during compilation")
  537. class TestParallelBuildExt(TestBuildExt):
  538. def build_ext(self, *args, **kwargs):
  539. build_ext = super().build_ext(*args, **kwargs)
  540. build_ext.parallel = True
  541. return build_ext