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