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