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