1import sys 2import os 3from io import StringIO 4import textwrap 5 6from distutils.core import Distribution 7from distutils.command.build_ext import build_ext 8from distutils import sysconfig 9from distutils.tests.support import (TempdirManager, LoggingSilencer, 10 copy_xxmodule_c, fixup_build_ext) 11from distutils.extension import Extension 12from distutils.errors import ( 13 CompileError, DistutilsPlatformError, DistutilsSetupError, 14 UnknownFileError) 15 16import unittest 17from test import support 18from test.support import os_helper 19from test.support.script_helper import assert_python_ok 20from test.support import threading_helper 21 22# http://bugs.python.org/issue4373 23# Don't load the xx module more than once. 24ALREADY_TESTED = False 25 26 27class BuildExtTestCase(TempdirManager, 28 LoggingSilencer, 29 unittest.TestCase): 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 540class ParallelBuildExtTestCase(BuildExtTestCase): 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 548def test_suite(): 549 suite = unittest.TestSuite() 550 suite.addTest(unittest.TestLoader().loadTestsFromTestCase(BuildExtTestCase)) 551 suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ParallelBuildExtTestCase)) 552 return suite 553 554if __name__ == '__main__': 555 support.run_unittest(__name__) 556