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