blob: e144fd0738b9ca7b4f72c3abd6a111544c1d3b79 [file] [log] [blame]
Tarek Ziade1231a4e2011-05-19 13:07:25 +02001import os
2import sys
3import site
4import shutil
5import sysconfig
Ned Deily5c727cb2011-06-28 20:03:17 -07006import textwrap
Tarek Ziade1231a4e2011-05-19 13:07:25 +02007from io import StringIO
8from packaging.dist import Distribution
Ned Deily5c727cb2011-06-28 20:03:17 -07009from packaging.errors import (UnknownFileError, CompileError,
10 PackagingPlatformError)
Tarek Ziade1231a4e2011-05-19 13:07:25 +020011from packaging.command.build_ext import build_ext
12from packaging.compiler.extension import Extension
Victor Stinner72399392011-06-17 13:52:56 +020013from test.script_helper import assert_python_ok
Tarek Ziade1231a4e2011-05-19 13:07:25 +020014
15from packaging.tests import support, unittest, verbose, unload
16
Tarek Ziade1231a4e2011-05-19 13:07:25 +020017
18def _get_source_filename():
Ned Deily4a1ec122011-06-28 00:53:54 -070019 # use installed copy if available
20 tests_f = os.path.join(os.path.dirname(__file__), 'xxmodule.c')
21 if os.path.exists(tests_f):
22 return tests_f
23 # otherwise try using copy from build directory
Tarek Ziade1231a4e2011-05-19 13:07:25 +020024 srcdir = sysconfig.get_config_var('srcdir')
25 return os.path.join(srcdir, 'Modules', 'xxmodule.c')
26
27
28class BuildExtTestCase(support.TempdirManager,
29 support.LoggingCatcher,
30 unittest.TestCase):
31 def setUp(self):
32 # Create a simple test environment
33 # Note that we're making changes to sys.path
34 super(BuildExtTestCase, self).setUp()
35 self.tmp_dir = self.mkdtemp()
Tarek Ziade2bc55e42011-05-22 21:21:44 +020036 filename = _get_source_filename()
Tarek Ziade35173692011-05-22 22:09:55 +020037 if os.path.exists(filename):
38 shutil.copy(filename, self.tmp_dir)
Tarek Ziade1231a4e2011-05-19 13:07:25 +020039 self.old_user_base = site.USER_BASE
40 site.USER_BASE = self.mkdtemp()
41 build_ext.USER_BASE = site.USER_BASE
42
Éric Araujoed5d2f12011-06-17 15:47:41 +020043 def tearDown(self):
44 # Get everything back to normal
Éric Araujoed5d2f12011-06-17 15:47:41 +020045 if sys.version > "2.6":
46 site.USER_BASE = self.old_user_base
47 build_ext.USER_BASE = self.old_user_base
48
49 super(BuildExtTestCase, self).tearDown()
50
Tarek Ziade2bc55e42011-05-22 21:21:44 +020051 def _fixup_command(self, cmd):
52 # When Python was build with --enable-shared, -L. is not good enough
53 # to find the libpython<blah>.so. This is because regrtest runs it
54 # under a tempdir, not in the top level where the .so lives. By the
55 # time we've gotten here, Python's already been chdir'd to the
56 # tempdir.
57 #
58 # To further add to the fun, we can't just add library_dirs to the
59 # Extension() instance because that doesn't get plumbed through to the
60 # final compiler command.
61 if (sysconfig.get_config_var('Py_ENABLE_SHARED') and
62 not sys.platform.startswith('win')):
63 runshared = sysconfig.get_config_var('RUNSHARED')
64 if runshared is None:
65 cmd.library_dirs = ['.']
66 else:
67 name, equals, value = runshared.partition('=')
68 cmd.library_dirs = value.split(os.pathsep)
69
Tarek Ziade1231a4e2011-05-19 13:07:25 +020070 def test_build_ext(self):
Tarek Ziade1231a4e2011-05-19 13:07:25 +020071 xx_c = os.path.join(self.tmp_dir, 'xxmodule.c')
Tarek Ziade35173692011-05-22 22:09:55 +020072 if not os.path.exists(xx_c):
73 # skipping if we cannot find it
74 return
Tarek Ziade1231a4e2011-05-19 13:07:25 +020075 xx_ext = Extension('xx', [xx_c])
76 dist = Distribution({'name': 'xx', 'ext_modules': [xx_ext]})
77 dist.package_dir = self.tmp_dir
78 cmd = build_ext(dist)
Tarek Ziade2bc55e42011-05-22 21:21:44 +020079 self._fixup_command(cmd)
80
Tarek Ziade1231a4e2011-05-19 13:07:25 +020081 if os.name == "nt":
82 # On Windows, we must build a debug version iff running
83 # a debug build of Python
84 cmd.debug = sys.executable.endswith("_d.exe")
85 cmd.build_lib = self.tmp_dir
86 cmd.build_temp = self.tmp_dir
87
88 old_stdout = sys.stdout
89 if not verbose:
90 # silence compiler output
91 sys.stdout = StringIO()
92 try:
93 cmd.ensure_finalized()
94 cmd.run()
95 finally:
96 sys.stdout = old_stdout
97
Victor Stinner72399392011-06-17 13:52:56 +020098 code = """if 1:
99 import sys
100 sys.path.insert(0, %r)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200101
Victor Stinner72399392011-06-17 13:52:56 +0200102 import xx
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200103
Victor Stinner72399392011-06-17 13:52:56 +0200104 for attr in ('error', 'foo', 'new', 'roj'):
105 assert hasattr(xx, attr)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200106
Victor Stinner72399392011-06-17 13:52:56 +0200107 assert xx.foo(2, 5) == 7
108 assert xx.foo(13, 15) == 28
109 assert xx.new().demo() is None
110 doc = 'This is a template module just for instruction.'
111 assert xx.__doc__ == doc
112 assert isinstance(xx.Null(), xx.Null)
113 assert isinstance(xx.Str(), xx.Str)"""
114 code = code % self.tmp_dir
115 assert_python_ok('-c', code)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200116
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200117 def test_solaris_enable_shared(self):
118 dist = Distribution({'name': 'xx'})
119 cmd = build_ext(dist)
120 old = sys.platform
121
122 sys.platform = 'sunos' # fooling finalize_options
123 from sysconfig import _CONFIG_VARS
124
125 old_var = _CONFIG_VARS.get('Py_ENABLE_SHARED')
126 _CONFIG_VARS['Py_ENABLE_SHARED'] = 1
127 try:
128 cmd.ensure_finalized()
129 finally:
130 sys.platform = old
131 if old_var is None:
132 del _CONFIG_VARS['Py_ENABLE_SHARED']
133 else:
134 _CONFIG_VARS['Py_ENABLE_SHARED'] = old_var
135
136 # make sure we get some library dirs under solaris
137 self.assertGreater(len(cmd.library_dirs), 0)
138
139 @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher')
140 def test_user_site(self):
141 dist = Distribution({'name': 'xx'})
142 cmd = build_ext(dist)
143
144 # making sure the user option is there
145 options = [name for name, short, label in
146 cmd.user_options]
147 self.assertIn('user', options)
148
149 # setting a value
150 cmd.user = True
151
152 # setting user based lib and include
153 lib = os.path.join(site.USER_BASE, 'lib')
154 incl = os.path.join(site.USER_BASE, 'include')
155 os.mkdir(lib)
156 os.mkdir(incl)
157
158 # let's run finalize
159 cmd.ensure_finalized()
160
161 # see if include_dirs and library_dirs
162 # were set
163 self.assertIn(lib, cmd.library_dirs)
164 self.assertIn(lib, cmd.rpath)
165 self.assertIn(incl, cmd.include_dirs)
166
167 def test_optional_extension(self):
168
169 # this extension will fail, but let's ignore this failure
170 # with the optional argument.
171 modules = [Extension('foo', ['xxx'], optional=False)]
172 dist = Distribution({'name': 'xx', 'ext_modules': modules})
173 cmd = build_ext(dist)
174 cmd.ensure_finalized()
175 self.assertRaises((UnknownFileError, CompileError),
176 cmd.run) # should raise an error
177
178 modules = [Extension('foo', ['xxx'], optional=True)]
179 dist = Distribution({'name': 'xx', 'ext_modules': modules})
180 cmd = build_ext(dist)
181 cmd.ensure_finalized()
182 cmd.run() # should pass
183
184 def test_finalize_options(self):
185 # Make sure Python's include directories (for Python.h, pyconfig.h,
186 # etc.) are in the include search path.
187 modules = [Extension('foo', ['xxx'], optional=False)]
188 dist = Distribution({'name': 'xx', 'ext_modules': modules})
189 cmd = build_ext(dist)
190 cmd.finalize_options()
191
192 py_include = sysconfig.get_path('include')
193 self.assertIn(py_include, cmd.include_dirs)
194
195 plat_py_include = sysconfig.get_path('platinclude')
196 self.assertIn(plat_py_include, cmd.include_dirs)
197
198 # make sure cmd.libraries is turned into a list
199 # if it's a string
200 cmd = build_ext(dist)
201 cmd.libraries = 'my_lib'
202 cmd.finalize_options()
203 self.assertEqual(cmd.libraries, ['my_lib'])
204
205 # make sure cmd.library_dirs is turned into a list
206 # if it's a string
207 cmd = build_ext(dist)
208 cmd.library_dirs = 'my_lib_dir'
209 cmd.finalize_options()
210 self.assertIn('my_lib_dir', cmd.library_dirs)
211
212 # make sure rpath is turned into a list
213 # if it's a list of os.pathsep's paths
214 cmd = build_ext(dist)
215 cmd.rpath = os.pathsep.join(['one', 'two'])
216 cmd.finalize_options()
217 self.assertEqual(cmd.rpath, ['one', 'two'])
218
219 # XXX more tests to perform for win32
220
221 # make sure define is turned into 2-tuples
222 # strings if they are ','-separated strings
223 cmd = build_ext(dist)
224 cmd.define = 'one,two'
225 cmd.finalize_options()
226 self.assertEqual(cmd.define, [('one', '1'), ('two', '1')])
227
228 # make sure undef is turned into a list of
229 # strings if they are ','-separated strings
230 cmd = build_ext(dist)
231 cmd.undef = 'one,two'
232 cmd.finalize_options()
233 self.assertEqual(cmd.undef, ['one', 'two'])
234
235 # make sure swig_opts is turned into a list
236 cmd = build_ext(dist)
237 cmd.swig_opts = None
238 cmd.finalize_options()
239 self.assertEqual(cmd.swig_opts, [])
240
241 cmd = build_ext(dist)
242 cmd.swig_opts = '1 2'
243 cmd.finalize_options()
244 self.assertEqual(cmd.swig_opts, ['1', '2'])
245
246 def test_get_source_files(self):
247 modules = [Extension('foo', ['xxx'], optional=False)]
248 dist = Distribution({'name': 'xx', 'ext_modules': modules})
249 cmd = build_ext(dist)
250 cmd.ensure_finalized()
251 self.assertEqual(cmd.get_source_files(), ['xxx'])
252
253 def test_compiler_option(self):
254 # cmd.compiler is an option and
255 # should not be overriden by a compiler instance
256 # when the command is run
257 dist = Distribution()
258 cmd = build_ext(dist)
259 cmd.compiler = 'unix'
260 cmd.ensure_finalized()
261 cmd.run()
262 self.assertEqual(cmd.compiler, 'unix')
263
264 def test_get_outputs(self):
265 tmp_dir = self.mkdtemp()
266 c_file = os.path.join(tmp_dir, 'foo.c')
Éric Araujo7f9b37b2011-05-29 02:59:52 +0200267 self.write_file(c_file, 'void PyInit_foo(void) {}\n')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200268 ext = Extension('foo', [c_file], optional=False)
269 dist = Distribution({'name': 'xx',
270 'ext_modules': [ext]})
271 cmd = build_ext(dist)
Tarek Ziade2bc55e42011-05-22 21:21:44 +0200272 self._fixup_command(cmd)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200273 cmd.ensure_finalized()
274 self.assertEqual(len(cmd.get_outputs()), 1)
275
276 if os.name == "nt":
277 cmd.debug = sys.executable.endswith("_d.exe")
278
279 cmd.build_lib = os.path.join(self.tmp_dir, 'build')
280 cmd.build_temp = os.path.join(self.tmp_dir, 'tempt')
281
282 # issue #5977 : distutils build_ext.get_outputs
283 # returns wrong result with --inplace
284 other_tmp_dir = os.path.realpath(self.mkdtemp())
285 old_wd = os.getcwd()
286 os.chdir(other_tmp_dir)
287 try:
288 cmd.inplace = True
289 cmd.run()
290 so_file = cmd.get_outputs()[0]
291 finally:
292 os.chdir(old_wd)
293 self.assertTrue(os.path.exists(so_file))
294 so_ext = sysconfig.get_config_var('SO')
295 self.assertTrue(so_file.endswith(so_ext))
296 so_dir = os.path.dirname(so_file)
297 self.assertEqual(so_dir, other_tmp_dir)
298
299 cmd.inplace = False
300 cmd.run()
301 so_file = cmd.get_outputs()[0]
302 self.assertTrue(os.path.exists(so_file))
303 self.assertTrue(so_file.endswith(so_ext))
304 so_dir = os.path.dirname(so_file)
305 self.assertEqual(so_dir, cmd.build_lib)
306
307 # inplace = False, cmd.package = 'bar'
308 build_py = cmd.get_finalized_command('build_py')
309 build_py.package_dir = 'bar'
310 path = cmd.get_ext_fullpath('foo')
311 # checking that the last directory is the build_dir
312 path = os.path.split(path)[0]
313 self.assertEqual(path, cmd.build_lib)
314
315 # inplace = True, cmd.package = 'bar'
316 cmd.inplace = True
317 other_tmp_dir = os.path.realpath(self.mkdtemp())
318 old_wd = os.getcwd()
319 os.chdir(other_tmp_dir)
320 try:
321 path = cmd.get_ext_fullpath('foo')
322 finally:
323 os.chdir(old_wd)
324 # checking that the last directory is bar
325 path = os.path.split(path)[0]
326 lastdir = os.path.split(path)[-1]
327 self.assertEqual(lastdir, 'bar')
328
329 def test_ext_fullpath(self):
330 ext = sysconfig.get_config_vars()['SO']
331 # building lxml.etree inplace
332 #etree_c = os.path.join(self.tmp_dir, 'lxml.etree.c')
333 #etree_ext = Extension('lxml.etree', [etree_c])
334 #dist = Distribution({'name': 'lxml', 'ext_modules': [etree_ext]})
335 dist = Distribution()
336 cmd = build_ext(dist)
337 cmd.inplace = True
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 = False
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 = None
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 = True
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
Ned Deily5c727cb2011-06-28 20:03:17 -0700367 @unittest.skipUnless(sys.platform == 'darwin',
368 'test only relevant for Mac OS X')
369 def test_deployment_target_default(self):
370 # Issue 9516: Test that, in the absence of the environment variable,
371 # an extension module is compiled with the same deployment target as
372 # the interpreter.
373 self._try_compile_deployment_target('==', None)
374
375 @unittest.skipUnless(sys.platform == 'darwin',
376 'test only relevant for Mac OS X')
377 def test_deployment_target_too_low(self):
378 # Issue 9516: Test that an extension module is not allowed to be
379 # compiled with a deployment target less than that of the interpreter.
380 self.assertRaises(PackagingPlatformError,
381 self._try_compile_deployment_target, '>', '10.1')
382
383 @unittest.skipUnless(sys.platform == 'darwin',
384 'test only relevant for Mac OS X')
385 def test_deployment_target_higher_ok(self):
386 # Issue 9516: Test that an extension module can be compiled with a
387 # deployment target higher than that of the interpreter: the ext
388 # module may depend on some newer OS feature.
389 deptarget = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET')
390 if deptarget:
391 # increment the minor version number (i.e. 10.6 -> 10.7)
392 deptarget = [int(x) for x in deptarget.split('.')]
393 deptarget[-1] += 1
394 deptarget = '.'.join(str(i) for i in deptarget)
395 self._try_compile_deployment_target('<', deptarget)
396
397 def _try_compile_deployment_target(self, operator, target):
398 orig_environ = os.environ
399 os.environ = orig_environ.copy()
400 self.addCleanup(setattr, os, 'environ', orig_environ)
401
402 if target is None:
403 if os.environ.get('MACOSX_DEPLOYMENT_TARGET'):
404 del os.environ['MACOSX_DEPLOYMENT_TARGET']
405 else:
406 os.environ['MACOSX_DEPLOYMENT_TARGET'] = target
407
408 deptarget_c = os.path.join(self.tmp_dir, 'deptargetmodule.c')
409
410 with open(deptarget_c, 'w') as fp:
411 fp.write(textwrap.dedent('''\
412 #include <AvailabilityMacros.h>
413
414 int dummy;
415
416 #if TARGET %s MAC_OS_X_VERSION_MIN_REQUIRED
417 #else
418 #error "Unexpected target"
419 #endif
420
421 ''' % operator))
422
423 # get the deployment target that the interpreter was built with
424 target = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET')
425 target = tuple(map(int, target.split('.')))
426 target = '%02d%01d0' % target
427
428 deptarget_ext = Extension(
429 'deptarget',
430 [deptarget_c],
431 extra_compile_args=['-DTARGET=%s' % (target,)],
432 )
433 dist = Distribution({
434 'name': 'deptarget',
435 'ext_modules': [deptarget_ext],
436 })
437 dist.package_dir = self.tmp_dir
438 cmd = build_ext(dist)
439 cmd.build_lib = self.tmp_dir
440 cmd.build_temp = self.tmp_dir
441
442 try:
443 old_stdout = sys.stdout
444 if not verbose:
445 # silence compiler output
446 sys.stdout = StringIO()
447 try:
448 cmd.ensure_finalized()
449 cmd.run()
450 finally:
451 sys.stdout = old_stdout
452
453 except CompileError:
454 self.fail("Wrong deployment target during compilation")
455
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200456
457def test_suite():
458 src = _get_source_filename()
459 if not os.path.exists(src):
460 if verbose:
Éric Araujob4fefc82011-05-31 14:33:32 +0200461 print('test_command_build_ext: Cannot find source code (test'
462 ' must run in python build dir)')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200463 return unittest.TestSuite()
464 else:
465 return unittest.makeSuite(BuildExtTestCase)
466
467if __name__ == '__main__':
468 unittest.main(defaultTest='test_suite')