1"""
2Generic setup.py file for PyObjC framework wrappers.
3
4This file should only be changed in pyobjc-core and then copied
5to all framework wrappers.
6"""
7
8__all__ = ('setup', 'Extension', 'Command')
9
10import sys
11from pkg_resources import Distribution
12
13try:
14    import setuptools
15
16except ImportError:
17    import distribute_setup
18    distribute_setup.use_setuptools()
19
20from setuptools.command import test
21from setuptools.command import build_py
22from distutils.sysconfig import get_config_var, get_config_vars
23
24
25from distutils import log
26
27class oc_build_py (build_py.build_py):
28    def build_packages(self):
29        log.info("overriding build_packages to copy PyObjCTest")
30        p = self.packages
31        self.packages = list(self.packages) + ['PyObjCTest']
32        try:
33            build_py.build_py.build_packages(self)
34        finally:
35            self.packages = p
36
37
38from pkg_resources import working_set, normalize_path, add_activation_listener, require
39
40class oc_test (test.test):
41    description = "run test suite"
42    user_options = [
43        ('verbosity=', None, "print what tests are run"),
44    ]
45
46    def initialize_options(self):
47        test.test.initialize_options(self)
48        self.verbosity='1'
49
50    def finalize_options(self):
51        test.test.finalize_options(self)
52        if isinstance(self.verbosity, str):
53            self.verbosity = int(self.verbosity)
54
55
56    def cleanup_environment(self):
57        ei_cmd = self.get_finalized_command('egg_info')
58        egg_name = ei_cmd.egg_name.replace('-', '_')
59
60        to_remove =  []
61        for dirname in sys.path:
62            bn = os.path.basename(dirname)
63            if bn.startswith(egg_name + "-"):
64                to_remove.append(dirname)
65
66        for dirname in to_remove:
67            log.info("removing installed %r from sys.path before testing"%(
68                dirname,))
69            sys.path.remove(dirname)
70
71        from pkg_resources import add_activation_listener
72        add_activation_listener(lambda dist: dist.activate())
73        working_set.__init__()
74
75    def add_project_to_sys_path(self):
76        from pkg_resources import normalize_path, add_activation_listener
77        from pkg_resources import working_set, require
78
79        self.reinitialize_command('egg_info')
80        self.run_command('egg_info')
81        self.reinitialize_command('build_ext', inplace=1)
82        self.run_command('build_ext')
83
84        self.__old_path = sys.path[:]
85        self.__old_modules = sys.modules.copy()
86
87        if 'PyObjCTools' in sys.modules:
88            del sys.modules['PyObjCTools']
89
90
91        ei_cmd = self.get_finalized_command('egg_info')
92        sys.path.insert(0, normalize_path(ei_cmd.egg_base))
93        sys.path.insert(1, os.path.dirname(__file__))
94
95        add_activation_listener(lambda dist: dist.activate())
96        working_set.__init__()
97        require('%s==%s'%(ei_cmd.egg_name, ei_cmd.egg_version))
98
99    def remove_from_sys_path(self):
100        from pkg_resources import working_set
101        sys.path[:] = self.__old_path
102        sys.modules.clear()
103        sys.modules.update(self.__old_modules)
104        working_set.__init__()
105
106
107    def run(self):
108        import unittest
109
110        # Ensure that build directory is on sys.path (py3k)
111        import sys
112
113        self.cleanup_environment()
114        self.add_project_to_sys_path()
115
116        import PyObjCTools.TestSupport as modo
117
118        from pkg_resources import EntryPoint
119        loader_ep = EntryPoint.parse("x="+self.test_loader)
120        loader_class = loader_ep.load(require=False)
121
122        try:
123            meta = self.distribution.metadata
124            name = meta.get_name()
125            test_pkg = name + "_tests"
126            suite = loader_class().loadTestsFromName(self.distribution.test_suite)
127
128            runner = unittest.TextTestRunner(verbosity=self.verbosity)
129            result = runner.run(suite)
130
131            # Print out summary. This is a structured format that
132            # should make it easy to use this information in scripts.
133            summary = dict(
134                count=result.testsRun,
135                fails=len(result.failures),
136                errors=len(result.errors),
137                xfails=len(getattr(result, 'expectedFailures', [])),
138                xpass=len(getattr(result, 'unexpectedSuccesses', [])),
139                skip=len(getattr(result, 'skipped', [])),
140            )
141            print("SUMMARY: %s"%(summary,))
142
143        finally:
144            self.remove_from_sys_path()
145
146
147from setuptools import setup as _setup, Extension as _Extension, Command
148from distutils.errors import DistutilsPlatformError
149from distutils.command import build, install
150from setuptools.command import develop, test, build_ext, install_lib
151import pkg_resources
152import shutil
153import os
154import plistlib
155import sys
156import __main__
157
158CLASSIFIERS = filter(None,
159"""
160Development Status :: 5 - Production/Stable
161Environment :: Console
162Environment :: MacOS X :: Cocoa
163Intended Audience :: Developers
164License :: OSI Approved :: MIT License
165Natural Language :: English
166Operating System :: MacOS :: MacOS X
167Programming Language :: Python
168Programming Language :: Python :: 2
169Programming Language :: Python :: 2.6
170Programming Language :: Python :: 2.7
171Programming Language :: Python :: 3
172Programming Language :: Python :: 3.1
173Programming Language :: Python :: 3.2
174Programming Language :: Python :: 3.3
175Programming Language :: Objective C
176Topic :: Software Development :: Libraries :: Python Modules
177Topic :: Software Development :: User Interfaces
178""".splitlines())
179
180
181def get_os_level():
182    pl = plistlib.readPlist('/System/Library/CoreServices/SystemVersion.plist')
183    v = pl['ProductVersion']
184    return tuple(map(int, v.split('.')[:2]))
185
186class pyobjc_install_lib (install_lib.install_lib):
187    def get_exclusions(self):
188        result = install_lib.install_lib.get_exclusions(self)
189        for fn in install_lib._install_lib.get_outputs(self):
190            if 'PyObjCTest' in fn:
191                result[fn] = 1
192
193        result['PyObjCTest'] = 1
194        result[os.path.join(self.install_dir, 'PyObjCTest')] = 1
195        for fn in os.listdir('PyObjCTest'):
196            result[os.path.join('PyObjCTest', fn)] = 1
197            result[os.path.join(self.install_dir, 'PyObjCTest', fn)] = 1
198
199        return result
200
201def _find_executable(executable):
202    if os.path.isfile(executable):
203        return executable
204
205    else:
206        for p in os.environ['PATH'].split(os.pathsep):
207            f = os.path.join(p, executable)
208            if os.path.isfile(f):
209                return f
210    return None
211
212def _working_compiler(executable):
213    import tempfile, subprocess, shlex
214    with tempfile.NamedTemporaryFile(mode='w', suffix='.c') as fp:
215        fp.write('#include <stdarg.h>\nint main(void) { return 0; }\n')
216        fp.flush()
217
218        cflags = get_config_var('CFLAGS')
219        cflags = shlex.split(cflags)
220
221        p = subprocess.Popen([
222            executable, '-c', fp.name] + cflags,
223            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
224        exit = p.wait()
225        if exit != 0:
226            return False
227
228        binfile = fp.name[:-1] + 'o'
229        if os.path.exists(binfile):
230            os.unlink(binfile)
231
232
233        binfile = os.path.basename(binfile)
234        if os.path.exists(binfile):
235            os.unlink(binfile)
236
237    return True
238
239def _fixup_compiler():
240    if 'CC' in os.environ:
241        # CC is in the environment, always use explicit
242        # overrides.
243        return
244
245    cc = oldcc = get_config_var('CC').split()[0]
246    cc = _find_executable(cc)
247    if cc is not None and os.path.basename(cc).startswith('gcc'):
248        # Check if compiler is LLVM-GCC, that's known to
249        # generate bad code.
250        data = os.popen("'%s' --version 2>/dev/null"%(
251            cc.replace("'", "'\"'\"'"),)).read()
252        if 'llvm-gcc' in data:
253            cc = None
254
255    if cc is not None and not _working_compiler(cc):
256        cc = None
257
258    if cc is None:
259        # Default compiler is not useable, try finding 'clang'
260        cc = _find_executable('clang')
261        if cc is None:
262            cc = os.popen("/usr/bin/xcrun -find clang").read()
263
264    if not cc:
265        raise SystemExit("Cannot locate compiler candidate")
266
267    if not _working_compiler(cc):
268        raise SystemExit("Cannot locate a working compiler")
269
270    if cc != oldcc:
271        print("Use '%s' instead of '%s' as the compiler"%(
272            cc, oldcc))
273
274        vars = get_config_vars()
275        for env in ('BLDSHARED', 'LDSHARED', 'CC', 'CXX'):
276            if env in vars and env not in os.environ:
277                split = vars[env].split()
278                split[0] = cc if env != 'CXX' else cc + '++'
279                vars[env] = ' '.join(split)
280
281class pyobjc_build_ext (build_ext.build_ext):
282    def run(self):
283        _fixup_compiler()
284
285        # Ensure that the PyObjC header files are available
286        # in 2.3 and later the headers are in the egg,
287        # before that we ship a copy.
288        dist, = pkg_resources.require('pyobjc-core')
289
290        include_root = os.path.join(self.build_temp, 'pyobjc-include')
291        if os.path.exists(include_root):
292            shutil.rmtree(include_root)
293
294        os.makedirs(include_root)
295        if dist.has_metadata('include'):
296            for fn in dist.metadata_listdir('include'):
297                data = dist.get_metadata('include/%s'%(fn,))
298                fp = open(os.path.join(include_root, fn), 'w')
299                try:
300                    fp.write(data)
301                finally:
302                    fp.close()
303
304        else:
305            raise SystemExit("pyobjc-core egg-info does not include header files")
306
307        for e in self.extensions:
308            if include_root not in e.include_dirs:
309                e.include_dirs.append(include_root)
310
311        # Run the actual build
312        build_ext.build_ext.run(self)
313
314        # Then tweak the copy_extensions bit to ensure PyObjCTest gets
315        # copied to the right place.
316        extensions = self.extensions
317        self.extensions = [
318            e for e in extensions if e.name.startswith('PyObjCTest') ]
319        self.copy_extensions_to_source()
320        self.extensions = extensions
321
322
323
324def Extension(*args, **kwds):
325    """
326    Simple wrapper about distutils.core.Extension that adds additional PyObjC
327    specific flags.
328    """
329    os_level = get_os_level()
330    cflags =  ["-DPyObjC_BUILD_RELEASE=%02d%02d"%os_level]
331    ldflags = []
332    if os_level != (10, 4):
333        pass
334        pass
335    else:
336        cflags.append('-DNO_OBJC2_RUNTIME')
337
338    if 'extra_compile_args' in kwds:
339        kwds['extra_compile_args'] = kwds['extra_compile_args'] + cflags
340    else:
341        kwds['extra_compile_args'] = cflags
342
343    if 'extra_link_args' in kwds:
344        kwds['extra_link_args'] = kwds['extra_link_args'] + ldflags
345    else:
346        kwds['extra_link_args'] = ldflags
347
348    return _Extension(*args, **kwds)
349
350
351def setup(
352        min_os_level=None,
353        max_os_level=None,
354        cmdclass=None,
355        **kwds):
356
357
358    k = kwds.copy()
359
360    os_level = get_os_level()
361    os_compatible = True
362    if sys.platform != 'darwin':
363        os_compatible = False
364
365    else:
366        if min_os_level is not None:
367            if os_level < tuple(map(int, min_os_level.split('.'))):
368                os_compatible = False
369        if max_os_level is not None:
370            if os_level > tuple(map(int, max_os_level.split('.'))):
371                os_compatible = False
372
373    if cmdclass is None:
374        cmdclass = {}
375    else:
376        cmdclass = cmdclass.copy()
377
378    if not os_compatible:
379        if min_os_level != None:
380            if max_os_level != None:
381                msg = "This distribution is only supported on MacOSX versions %s upto and including %s"%(
382                        min_os_level, max_os_level)
383            else:
384                msg = "This distribution is only supported on MacOSX >= %s"%(min_os_level,)
385        elif max_os_level != None:
386            msg = "This distribution is only supported on MacOSX <= %s"%(max_os_level,)
387        else:
388            msg = "This distribution is only supported on MacOSX"
389
390        def create_command_subclass(base_class):
391
392            class subcommand (base_class):
393                def run(self):
394                    raise DistutilsPlatformError(msg)
395
396            return subcommand
397
398        class no_test (oc_test):
399            def run(self):
400                print("WARNING: %s\n"%(msg,))
401                print("SUMMARY: {'count': 0, 'fails': 0, 'errors': 0, 'xfails': 0, 'skip': 65, 'xpass': 0, 'message': msg }\n")
402
403        cmdclass['build'] = create_command_subclass(build.build)
404        cmdclass['test'] = no_test
405        cmdclass['install'] = create_command_subclass(install.install)
406        cmdclass['install_lib'] = create_command_subclass(pyobjc_install_lib)
407        cmdclass['develop'] = create_command_subclass(develop.develop)
408        cmdclass['build_py'] = create_command_subclass(oc_build_py)
409    else:
410        cmdclass['build_ext'] = pyobjc_build_ext
411        cmdclass['install_lib'] = pyobjc_install_lib
412        cmdclass['test'] = oc_test
413        cmdclass['build_py'] = oc_build_py
414
415    plat_name = "MacOS X"
416    plat_versions = []
417    if min_os_level is not None and min_os_level == max_os_level:
418        plat_versions.append("==%s"%(min_os_level,))
419    else:
420        if min_os_level is not None:
421            plat_versions.append(">=%s"%(min_os_level,))
422        if max_os_level is not None:
423            plat_versions.append("<=%s"%(max_os_level,))
424    if plat_versions:
425        plat_name += " (%s)"%(", ".join(plat_versions),)
426
427    _setup(
428        cmdclass=cmdclass,
429        long_description=__main__.__doc__,
430        author='Ronald Oussoren',
431        author_email='pyobjc-dev@lists.sourceforge.net',
432        url='http://pyobjc.sourceforge.net',
433        platforms = [ plat_name ],
434        package_dir = { '': 'Lib', 'PyObjCTest': 'PyObjCTest' },
435        dependency_links = [],
436        package_data = { '': ['*.bridgesupport'] },
437        test_suite='PyObjCTest',
438        zip_safe = False,
439        license = 'MIT License',
440        classifiers = CLASSIFIERS,
441        **k
442    )
443