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