1 import sys 2 import os 3 from StringIO import StringIO 4 import textwrap 5 6 from distutils.core import Extension, Distribution 7 from distutils.command.build_ext import build_ext 8 from distutils import sysconfig 9 from distutils.tests import support 10 from distutils.errors import (DistutilsSetupError, CompileError, 11 DistutilsPlatformError) 12 13 import unittest 14 from test import test_support 15 16 # http://bugs.python.org/issue4373 17 # Don't load the xx module more than once. 18 ALREADY_TESTED = False 19 20 21 class 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 #Broken after issue 7712(r78136) : add a temp_cwd context manager to test_support ... 64 #Without current working dir: "...cannot find -lpython27" 65 #NOTE: [py3k svn r85559] First (uncontroversial) part of issue 9807, barry.warsaw, 2010-10-16 : 66 # new _fixup_command is bogus, so we will use own work-around 67 cmd.library_dirs.insert(0, test_support.SAVEDCWD) 68 cmd.run() 69 finally: 70 sys.stdout = old_stdout 71 72 if ALREADY_TESTED: 73 return 74 else: 75 ALREADY_TESTED = True 76 77 import xx 78 79 for attr in ('error', 'foo', 'new', 'roj'): 80 self.assertTrue(hasattr(xx, attr)) 81 82 self.assertEqual(xx.foo(2, 5), 7) 83 self.assertEqual(xx.foo(13,15), 28) 84 self.assertEqual(xx.new().demo(), None) 85 if test_support.HAVE_DOCSTRINGS: 86 doc = 'This is a template module just for instruction.' 87 self.assertEqual(xx.__doc__, doc) 88 self.assertTrue(isinstance(xx.Null(), xx.Null)) 89 self.assertTrue(isinstance(xx.Str(), xx.Str)) 90 91 def test_solaris_enable_shared(self): 92 dist = Distribution({'name': 'xx'}) 93 cmd = build_ext(dist) 94 old = sys.platform 95 96 sys.platform = 'sunos' # fooling finalize_options 97 from distutils.sysconfig import _config_vars 98 old_var = _config_vars.get('Py_ENABLE_SHARED') 99 _config_vars['Py_ENABLE_SHARED'] = 1 100 try: 101 cmd.ensure_finalized() 102 finally: 103 sys.platform = old 104 if old_var is None: 105 del _config_vars['Py_ENABLE_SHARED'] 106 else: 107 _config_vars['Py_ENABLE_SHARED'] = old_var 108 109 # make sure we get some library dirs under solaris 110 self.assertTrue(len(cmd.library_dirs) > 0) 111 112 def test_user_site(self): 113 # site.USER_SITE was introduced in 2.6 114 if sys.version < '2.6': 115 return 116 117 import site 118 dist = Distribution({'name': 'xx'}) 119 cmd = build_ext(dist) 120 121 # making sure the user option is there 122 options = [name for name, short, label in 123 cmd.user_options] 124 self.assertIn('user', options) 125 126 # setting a value 127 cmd.user = 1 128 129 # setting user based lib and include 130 lib = os.path.join(site.USER_BASE, 'lib') 131 incl = os.path.join(site.USER_BASE, 'include') 132 os.mkdir(lib) 133 os.mkdir(incl) 134 135 cmd.ensure_finalized() 136 137 # see if include_dirs and library_dirs were set 138 self.assertIn(lib, cmd.library_dirs) 139 self.assertIn(lib, cmd.rpath) 140 self.assertIn(incl, cmd.include_dirs) 141 142 def test_finalize_options(self): 143 # Make sure Python's include directories (for Python.h, pyconfig.h, 144 # etc.) are in the include search path. 145 modules = [Extension('foo', ['xxx'])] 146 dist = Distribution({'name': 'xx', 'ext_modules': modules}) 147 cmd = build_ext(dist) 148 cmd.finalize_options() 149 150 py_include = sysconfig.get_python_inc() 151 self.assertTrue(py_include in cmd.include_dirs) 152 153 plat_py_include = sysconfig.get_python_inc(plat_specific=1) 154 self.assertTrue(plat_py_include in cmd.include_dirs) 155 156 # make sure cmd.libraries is turned into a list 157 # if it's a string 158 cmd = build_ext(dist) 159 cmd.libraries = 'my_lib, other_lib lastlib' 160 cmd.finalize_options() 161 self.assertEqual(cmd.libraries, ['my_lib', 'other_lib', 'lastlib']) 162 163 # make sure cmd.library_dirs is turned into a list 164 # if it's a string 165 cmd = build_ext(dist) 166 cmd.library_dirs = 'my_lib_dir%sother_lib_dir' % os.pathsep 167 cmd.finalize_options() 168 self.assertIn('my_lib_dir', cmd.library_dirs) 169 self.assertIn('other_lib_dir', cmd.library_dirs) 170 171 # make sure rpath is turned into a list 172 # if it's a string 173 cmd = build_ext(dist) 174 cmd.rpath = 'one%stwo' % os.pathsep 175 cmd.finalize_options() 176 self.assertEqual(cmd.rpath, ['one', 'two']) 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 ary (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.assertTrue(isinstance(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.assertTrue(not 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 overriden 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 #Without current working dir: "...cannot find -lpython27" 290 #NOTE: After issue #7712(r78136) test cannot use old_wd ! 291 #cmd.library_dirs.insert(0, old_wd) 292 #NOTE: [py3k svn r85559] First (uncontroversial) part of issue 9807, barry.warsaw, 2010-10-16 : 293 # new _fixup_command is bogus, so we will use own work-around 294 cmd.library_dirs.insert(0, test_support.SAVEDCWD) 295 os.chdir(other_tmp_dir) 296 try: 297 cmd.inplace = 1 298 cmd.run() 299 so_file = cmd.get_outputs()[0] 300 finally: 301 os.chdir(old_wd) 302 self.assertTrue(os.path.exists(so_file)) 303 self.assertEqual(os.path.splitext(so_file)[-1], 304 sysconfig.get_config_var('SO')) 305 so_dir = os.path.dirname(so_file) 306 self.assertEqual(so_dir, other_tmp_dir) 307 cmd.compiler = None 308 cmd.inplace = 0 309 cmd.run() 310 so_file = cmd.get_outputs()[0] 311 self.assertTrue(os.path.exists(so_file)) 312 self.assertEqual(os.path.splitext(so_file)[-1], 313 sysconfig.get_config_var('SO')) 314 so_dir = os.path.dirname(so_file) 315 self.assertEqual(so_dir, cmd.build_lib) 316 317 # inplace = 0, cmd.package = 'bar' 318 build_py = cmd.get_finalized_command('build_py') 319 build_py.package_dir = {'': 'bar'} 320 path = cmd.get_ext_fullpath('foo') 321 # checking that the last directory is the build_dir 322 path = os.path.split(path)[0] 323 self.assertEqual(path, cmd.build_lib) 324 325 # inplace = 1, cmd.package = 'bar' 326 cmd.inplace = 1 327 other_tmp_dir = os.path.realpath(self.mkdtemp()) 328 old_wd = os.getcwd() 329 os.chdir(other_tmp_dir) 330 try: 331 path = cmd.get_ext_fullpath('foo') 332 finally: 333 os.chdir(old_wd) 334 # checking that the last directory is bar 335 path = os.path.split(path)[0] 336 lastdir = os.path.split(path)[-1] 337 self.assertEqual(lastdir, 'bar') 338 339 def test_ext_fullpath(self): 340 ext = sysconfig.get_config_vars()['SO'] 341 dist = Distribution() 342 cmd = build_ext(dist) 343 cmd.inplace = 1 344 cmd.distribution.package_dir = {'': 'src'} 345 cmd.distribution.packages = ['lxml', 'lxml.html'] 346 curdir = os.getcwd() 347 wanted = os.path.join(curdir, 'src', 'lxml', 'etree' + ext) 348 path = cmd.get_ext_fullpath('lxml.etree') 349 self.assertEqual(wanted, path) 350 351 # building lxml.etree not inplace 352 cmd.inplace = 0 353 cmd.build_lib = os.path.join(curdir, 'tmpdir') 354 wanted = os.path.join(curdir, 'tmpdir', 'lxml', 'etree' + ext) 355 path = cmd.get_ext_fullpath('lxml.etree') 356 self.assertEqual(wanted, path) 357 358 # building twisted.runner.portmap not inplace 359 build_py = cmd.get_finalized_command('build_py') 360 build_py.package_dir = {} 361 cmd.distribution.packages = ['twisted', 'twisted.runner.portmap'] 362 path = cmd.get_ext_fullpath('twisted.runner.portmap') 363 wanted = os.path.join(curdir, 'tmpdir', 'twisted', 'runner', 364 'portmap' + ext) 365 self.assertEqual(wanted, path) 366 367 # building twisted.runner.portmap inplace 368 cmd.inplace = 1 369 path = cmd.get_ext_fullpath('twisted.runner.portmap') 370 wanted = os.path.join(curdir, 'twisted', 'runner', 'portmap' + ext) 371 self.assertEqual(wanted, path) 372 373 def test_build_ext_inplace(self): 374 etree_c = os.path.join(self.tmp_dir, 'lxml.etree.c') 375 etree_ext = Extension('lxml.etree', [etree_c]) 376 dist = Distribution({'name': 'lxml', 'ext_modules': [etree_ext]}) 377 cmd = build_ext(dist) 378 cmd.ensure_finalized() 379 cmd.inplace = 1 380 cmd.distribution.package_dir = {'': 'src'} 381 cmd.distribution.packages = ['lxml', 'lxml.html'] 382 curdir = os.getcwd() 383 ext = sysconfig.get_config_var("SO") 384 wanted = os.path.join(curdir, 'src', 'lxml', 'etree' + ext) 385 path = cmd.get_ext_fullpath('lxml.etree') 386 self.assertEqual(wanted, path) 387 388 def test_setuptools_compat(self): 389 import distutils.core, distutils.extension, distutils.command.build_ext 390 saved_ext = distutils.extension.Extension 391 try: 392 # on some platforms, it loads the deprecated "dl" module 393 test_support.import_module('setuptools_build_ext', deprecated=True) 394 395 # theses import patch Distutils' Extension class 396 from setuptools_build_ext import build_ext as setuptools_build_ext 397 from setuptools_extension import Extension 398 399 etree_c = os.path.join(self.tmp_dir, 'lxml.etree.c') 400 etree_ext = Extension('lxml.etree', [etree_c]) 401 dist = Distribution({'name': 'lxml', 'ext_modules': [etree_ext]}) 402 cmd = setuptools_build_ext(dist) 403 cmd.ensure_finalized() 404 cmd.inplace = 1 405 cmd.distribution.package_dir = {'': 'src'} 406 cmd.distribution.packages = ['lxml', 'lxml.html'] 407 curdir = os.getcwd() 408 ext = sysconfig.get_config_var("SO") 409 wanted = os.path.join(curdir, 'src', 'lxml', 'etree' + ext) 410 path = cmd.get_ext_fullpath('lxml.etree') 411 self.assertEqual(wanted, path) 412 finally: 413 # restoring Distutils' Extension class otherwise its broken 414 distutils.extension.Extension = saved_ext 415 distutils.core.Extension = saved_ext 416 distutils.command.build_ext.Extension = saved_ext 417 418 def test_build_ext_path_with_os_sep(self): 419 dist = Distribution({'name': 'UpdateManager'}) 420 cmd = build_ext(dist) 421 cmd.ensure_finalized() 422 ext = sysconfig.get_config_var("SO") 423 ext_name = os.path.join('UpdateManager', 'fdsend') 424 ext_path = cmd.get_ext_fullpath(ext_name) 425 wanted = os.path.join(cmd.build_lib, 'UpdateManager', 'fdsend' + ext) 426 self.assertEqual(ext_path, wanted) 427 428 def test_build_ext_path_cross_platform(self): 429 if sys.platform != 'win32': 430 return 431 dist = Distribution({'name': 'UpdateManager'}) 432 cmd = build_ext(dist) 433 cmd.ensure_finalized() 434 ext = sysconfig.get_config_var("SO") 435 # this needs to work even under win32 436 ext_name = 'UpdateManager/fdsend' 437 ext_path = cmd.get_ext_fullpath(ext_name) 438 wanted = os.path.join(cmd.build_lib, 'UpdateManager', 'fdsend' + ext) 439 self.assertEqual(ext_path, wanted) 440 441 @unittest.skipUnless(sys.platform == 'darwin', 'test only relevant for MacOSX') 442 def test_deployment_target_default(self): 443 # Issue 9516: Test that, in the absence of the environment variable, 444 # an extension module is compiled with the same deployment target as 445 # the interpreter. 446 self._try_compile_deployment_target('==', None) 447 448 @unittest.skipUnless(sys.platform == 'darwin', 'test only relevant for MacOSX') 449 def test_deployment_target_too_low(self): 450 # Issue 9516: Test that an extension module is not allowed to be 451 # compiled with a deployment target less than that of the interpreter. 452 self.assertRaises(DistutilsPlatformError, 453 self._try_compile_deployment_target, '>', '10.1') 454 455 @unittest.skipUnless(sys.platform == 'darwin', 'test only relevant for MacOSX') 456 def test_deployment_target_higher_ok(self): 457 # Issue 9516: Test that an extension module can be compiled with a 458 # deployment target higher than that of the interpreter: the ext 459 # module may depend on some newer OS feature. 460 deptarget = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET') 461 if deptarget: 462 # increment the minor version number (i.e. 10.6 -> 10.7) 463 deptarget = [int(x) for x in deptarget.split('.')] 464 deptarget[-1] += 1 465 deptarget = '.'.join(str(i) for i in deptarget) 466 self._try_compile_deployment_target('<', deptarget) 467 468 def _try_compile_deployment_target(self, operator, target): 469 orig_environ = os.environ 470 os.environ = orig_environ.copy() 471 self.addCleanup(setattr, os, 'environ', orig_environ) 472 473 if target is None: 474 if os.environ.get('MACOSX_DEPLOYMENT_TARGET'): 475 del os.environ['MACOSX_DEPLOYMENT_TARGET'] 476 else: 477 os.environ['MACOSX_DEPLOYMENT_TARGET'] = target 478 479 deptarget_c = os.path.join(self.tmp_dir, 'deptargetmodule.c') 480 481 with open(deptarget_c, 'w') as fp: 482 fp.write(textwrap.dedent('''\ 483 #include <AvailabilityMacros.h> 484 485 int dummy; 486 487 #if TARGET %s MAC_OS_X_VERSION_MIN_REQUIRED 488 #else 489 #error "Unexpected target" 490 #endif 491 492 ''' % operator)) 493 494 # get the deployment target that the interpreter was built with 495 target = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET') 496 target = tuple(map(int, target.split('.'))) 497 target = '%02d%01d0' % target 498 deptarget_ext = Extension( 499 'deptarget', 500 [deptarget_c], 501 extra_compile_args=['-DTARGET=%s'%(target,)], 502 ) 503 dist = Distribution({ 504 'name': 'deptarget', 505 'ext_modules': [deptarget_ext] 506 }) 507 dist.package_dir = self.tmp_dir 508 cmd = build_ext(dist) 509 cmd.build_lib = self.tmp_dir 510 cmd.build_temp = self.tmp_dir 511 512 try: 513 cmd.ensure_finalized() 514 cmd.run() 515 except CompileError: 516 self.fail("Wrong deployment target during compilation") 517 518 def test_suite(): 519 return unittest.makeSuite(BuildExtTestCase) 520 521 if __name__ == '__main__': 522 test_support.run_unittest(test_suite()) 523