1#!/usr/bin/env python
2
3import sys
4import subprocess
5import shutil
6import re
7import os
8import plistlib
9
10# We need at least Python 2.5
11MIN_PYTHON = (2, 6)
12
13# FIXME: autodetect default values for USE_* variables:
14#  both should be false by default, unless
15#  1) python is /usr/bin/python: both should be true
16#  2) python build with --with-system-libffi: USE_SYSTEM_FFI
17#     should be true.
18
19# Set USE_SYSTEM_FFI to True to link to the system version
20# of libffi
21USE_SYSTEM_FFI = True
22
23SDKROOT = os.environ.get('SDKROOT')
24if SDKROOT is None or SDKROOT is '':
25    SDKROOT = '/'
26
27if sys.version_info < MIN_PYTHON:
28    vstr = '.'.join(map(str, MIN_PYTHON))
29    raise SystemExit('PyObjC: Need at least Python ' + vstr)
30
31
32try:
33    import setuptools
34
35except ImportError:
36    import distribute_setup
37    distribute_setup.use_setuptools()
38
39
40#extra_args=dict(
41    #use_2to3 = True,
42#)
43
44def get_os_level():
45    pl = plistlib.readPlist('/System/Library/CoreServices/SystemVersion.plist')
46    v = pl['ProductVersion']
47    return '.'.join(v.split('.')[:2])
48
49
50
51
52
53from setuptools.command import build_py
54from setuptools.command import test
55from distutils import log
56from distutils.core import Command
57
58
59class oc_build_py (build_py.build_py):
60    def run_2to3(self, files, doctests=True):
61        files = [ fn for fn in files if not os.path.basename(fn).startswith('test3_') ]
62        build_py.build_py.run_2to3(self, files, doctests)
63
64    def build_packages(self):
65        log.info("Overriding build_packages to copy PyObjCTest")
66        p = self.packages
67        self.packages = list(self.packages) + ['PyObjCTest']
68        try:
69            build_py.build_py.build_packages(self)
70        finally:
71            self.packages = p
72
73from pkg_resources import working_set, normalize_path, add_activation_listener, require
74
75class oc_test (test.test):
76    description = "run test suite"
77    user_options = [
78        ('verbosity=', None, "print what tests are run"),
79    ]
80
81    def initialize_options(self):
82        self.verbosity='1'
83
84    def finalize_options(self):
85        if isinstance(self.verbosity, str):
86            self.verbosity = int(self.verbosity)
87
88
89    def cleanup_environment(self):
90        ei_cmd = self.get_finalized_command('egg_info')
91        egg_name = ei_cmd.egg_name.replace('-', '_')
92
93        to_remove =  []
94        for dirname in sys.path:
95            bn = os.path.basename(dirname)
96            if bn.startswith(egg_name + "-"):
97                to_remove.append(dirname)
98
99        for dirname in to_remove:
100            log.info("removing installed %r from sys.path before testing"%(
101                dirname,))
102            sys.path.remove(dirname)
103
104        from pkg_resources import add_activation_listener
105        add_activation_listener(lambda dist: dist.activate())
106        working_set.__init__()
107
108    def add_project_to_sys_path(self):
109        from pkg_resources import normalize_path, add_activation_listener
110        from pkg_resources import working_set, require
111
112        if getattr(self.distribution, 'use_2to3', False):
113
114            # Using 2to3, cannot do this inplace:
115            self.reinitialize_command('build_py', inplace=0)
116            self.run_command('build_py')
117            bpy_cmd = self.get_finalized_command("build_py")
118            build_path = normalize_path(bpy_cmd.build_lib)
119
120            self.reinitialize_command('egg_info', egg_base=build_path)
121            self.run_command('egg_info')
122
123            self.reinitialize_command('build_ext', inplace=0)
124            self.run_command('build_ext')
125
126        else:
127            self.reinitialize_command('egg_info')
128            self.run_command('egg_info')
129            self.reinitialize_command('build_ext', inplace=1)
130            self.run_command('build_ext')
131
132        self.__old_path = sys.path[:]
133        self.__old_modules = sys.modules.copy()
134
135        if 'PyObjCTools' in sys.modules:
136            del sys.modules['PyObjCTools']
137
138
139        ei_cmd = self.get_finalized_command('egg_info')
140        sys.path.insert(0, normalize_path(ei_cmd.egg_base))
141        sys.path.insert(1, os.path.dirname(__file__))
142
143        add_activation_listener(lambda dist: dist.activate())
144        working_set.__init__()
145        require('%s==%s'%(ei_cmd.egg_name, ei_cmd.egg_version))
146
147    def remove_from_sys_path(self):
148        from pkg_resources import working_set
149        sys.path[:] = self.__old_path
150        sys.modules.clear()
151        sys.modules.update(self.__old_modules)
152        working_set.__init__()
153
154
155    def run(self):
156        import unittest
157
158        # Ensure that build directory is on sys.path (py3k)
159        import sys
160
161        self.cleanup_environment()
162        self.add_project_to_sys_path()
163
164        from PyObjCTest.loader import makeTestSuite
165        import PyObjCTools.TestSupport as mod
166
167        try:
168            meta = self.distribution.metadata
169            name = meta.get_name()
170            test_pkg = name + "_tests"
171            suite = makeTestSuite()
172
173            runner = unittest.TextTestRunner(verbosity=self.verbosity)
174            result = runner.run(suite)
175
176            # Print out summary. This is a structured format that
177            # should make it easy to use this information in scripts.
178            summary = dict(
179                count=result.testsRun,
180                fails=len(result.failures),
181                errors=len(result.errors),
182                xfails=len(getattr(result, 'expectedFailures', [])),
183                xpass=len(getattr(result, 'expectedSuccesses', [])),
184                skip=len(getattr(result, 'skipped', [])),
185            )
186            print("SUMMARY: %s"%(summary,))
187
188        finally:
189            self.remove_from_sys_path()
190
191from setuptools.command import egg_info
192
193def write_header(cmd, basename, filename):
194    with open(os.path.join('Modules/objc/', os.path.basename(basename)), 'rU') as fp:
195        data = fp.read()
196    if not cmd.dry_run:
197        if not os.path.exists(os.path.dirname(filename)):
198            os.makedirs(os.path.dirname(filename))
199
200    cmd.write_file(basename, filename, data)
201
202
203# This is a workaround for a bug in setuptools: I'd like
204# to use the 'egg_info.writers' entry points in the setup()
205# call, but those don't work when also using a package_base
206# argument as we do.
207# (issue 123 in the distribute tracker)
208class my_egg_info (egg_info.egg_info):
209    def run(self):
210        self.mkpath(self.egg_info)
211
212        for hdr in ("pyobjc-compat.h", "pyobjc-api.h"):
213            fn = os.path.join("include", hdr)
214
215            write_header(self, fn, os.path.join(self.egg_info, fn))
216
217        egg_info.egg_info.run(self)
218
219
220if sys.version_info[0] == 3:
221    # FIXME: add custom test command that does the work.
222    # - Patch sys.path
223    # - Ensure PyObjCTest gets translated by 2to3
224    from distutils.util import get_platform
225    build_dir = 'build/lib.%s-%d.%d'%(
226        get_platform(), sys.version_info[0], sys.version_info[1])
227    if hasattr(sys, 'gettotalrefcount'):
228        build_dir += '-pydebug'
229    sys.path.insert(0,  build_dir)
230
231
232import os
233import glob
234import site
235import platform
236
237if 'MallocStackLogging' in os.environ:
238    del os.environ['MallocStackLogging']
239if 'MallocStackLoggingNoCompact' in os.environ:
240    del os.environ['MallocStackLoggingNoCompact']
241
242# See the news file:
243#os.environ['MACOSX_DEPLOYMENT_TARGET']='10.5'
244
245
246
247#if int(os.uname()[2].split('.')[0]) >= 10:
248#        USE_SYSTEM_FFI = True
249
250
251# Some PiPy stuff
252LONG_DESCRIPTION="""
253PyObjC is a bridge between Python and Objective-C.  It allows full
254featured Cocoa applications to be written in pure Python.  It is also
255easy to use other frameworks containing Objective-C class libraries
256from Python and to mix in Objective-C, C and C++ source.
257
258Python is a highly dynamic programming language with a shallow learning
259curve.  It combines remarkable power with very clear syntax.
260
261The installer package installs a number of Xcode templates for
262easily creating new Cocoa-Python projects.
263
264PyObjC also supports full introspection of Objective-C classes and
265direct invocation of Objective-C APIs from the interactive interpreter.
266"""
267
268from setuptools import setup, Extension, find_packages
269from setuptools.command import build_ext, install_lib
270import os
271
272class pyobjc_install_lib (install_lib.install_lib):
273    def get_exclusions(self):
274        result = install_lib.install_lib.get_exclusions(self)
275        for fn in install_lib._install_lib.get_outputs(self):
276            if 'PyObjCTest' in fn:
277                result[fn] = 1
278
279        for fn in os.listdir('PyObjCTest'):
280            result[os.path.join('PyObjCTest', fn)] = 1
281            result[os.path.join(self.install_dir, 'PyObjCTest', fn)] = 1
282
283
284        return result
285
286
287def _find_executable(executable):
288    if os.path.isfile(executable):
289        return executable
290
291    else:
292        for p in os.environ['PATH'].split(os.pathsep):
293            f = os.path.join(p, executable)
294            if os.path.isfile(f):
295                return f
296    return None
297
298def _working_compiler(executable):
299    import tempfile, subprocess, shlex
300    with tempfile.NamedTemporaryFile(mode='w', suffix='.c') as fp:
301        fp.write('#include <stdarg.h>\nint main(void) { return 0; }\n')
302        fp.flush()
303
304        cflags = get_config_var('CFLAGS')
305        cflags = shlex.split(cflags)
306        cflags += CFLAGS
307
308        p = subprocess.Popen([
309            executable, '-c', fp.name] + cflags,
310            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
311        exit = p.wait()
312        if exit != 0:
313            return False
314
315        binfile = fp.name[:-1] + 'o'
316        if os.path.exists(binfile):
317            os.unlink(binfile)
318
319        binfile = os.path.basename(binfile)
320        if os.path.exists(binfile):
321            os.unlink(binfile)
322
323    return True
324
325def _fixup_compiler():
326    if 'CC' in os.environ:
327        # CC is in the environment, always use explicit
328        # overrides.
329        return
330
331    cc = oldcc = get_config_var('CC').split()[0]
332    cc = _find_executable(cc)
333    if cc is not None and os.path.basename(cc).startswith('gcc'):
334        # Check if compiler is LLVM-GCC, that's known to
335        # generate bad code.
336        data = os.popen("'%s' --version 2>/dev/null"%(
337            cc.replace("'", "'\"'\"'"),)).read()
338        if 'llvm-gcc' in data:
339            cc = None
340
341    if cc is not None and not _working_compiler(cc):
342        cc = None
343
344    if cc is None:
345        # Default compiler is not useable, try finding 'clang'
346        cc = _find_executable('clang')
347        if cc is None:
348            cc = os.popen("/usr/bin/xcrun -find clang").read()
349
350    if not cc:
351        raise SystemExit("Cannot locate compiler candidate")
352
353    if not _working_compiler(cc):
354        raise SystemExit("Cannot locate a working compiler")
355
356    if cc != oldcc:
357        print("Use '%s' instead of '%s' as the compiler"%(
358            cc, oldcc))
359
360        vars = get_config_vars()
361        for env in ('BLDSHARED', 'LDSHARED', 'CC', 'CXX'):
362            if env in vars and env not in os.environ:
363                split = vars[env].split()
364                split[0] = cc if env != 'CXX' else cc + '++'
365                vars[env] = ' '.join(split)
366
367
368class pyobjc_build_ext (build_ext.build_ext):
369    def run(self):
370        _fixup_compiler()
371        build_ext.build_ext.run(self)
372        extensions = self.extensions
373        self.extensions = [
374                e for e in extensions if e.name.startswith('PyObjCTest') ]
375        self.copy_extensions_to_source()
376        self.extensions = extensions
377
378def frameworks(*args):
379    lst = []
380    for arg in args:
381        lst.extend(['-framework', arg])
382    return lst
383
384def IfFrameWork(name, packages, extensions, headername=None):
385    """
386    Return the packages and extensions if the framework exists, or
387    two empty lists if not.
388    """
389    import os
390    for pth in ('/System/Library/Frameworks', '/Library/Frameworks'):
391        basedir = os.path.join(pth, name)
392        if os.path.exists(basedir):
393            if (headername is None) or os.path.exists(os.path.join(basedir, "Headers", headername)):
394                return packages, extensions
395    return [], []
396
397# Double-check
398if sys.platform != 'darwin':
399    print("You're not running on MacOS X, and don't use GNUstep")
400    print("I don't know how to build PyObjC on such a platform.")
401    print("Please read the ReadMe.")
402    print("")
403    raise SystemExit("ObjC runtime not found")
404
405from distutils.sysconfig import get_config_var, get_config_vars
406
407CFLAGS=[ ]
408
409# Enable 'PyObjC_STRICT_DEBUGGING' to enable some costly internal
410# assertions.
411CFLAGS.extend([
412    #"-fdiagnostics-show-option",
413
414    # Use this to analyze with clang
415    #"--analyze",
416
417# The following flags are an attempt at getting rid of /usr/local
418# in the compiler search path.
419    "-DPyObjC_STRICT_DEBUGGING",
420    "-DMACOSX", # For libffi
421    "-DPyObjC_BUILD_RELEASE=%02d%02d"%(tuple(map(int, platform.mac_ver()[0].split('.')[:2]))),
422#    "-no-cpp-precomp",
423    "-DMACOSX",
424    "-g",
425    "-fexceptions",
426
427
428    # Loads of warning flags
429    "-Wall", "-Wstrict-prototypes", "-Wmissing-prototypes",
430    "-Wformat=2", "-W",
431    #"-Wshadow", # disabled due to warnings from Python headers
432    "-Wpointer-arith", #"-Wwrite-strings",
433    "-Wmissing-declarations",
434    "-Wnested-externs",
435#    "-Wno-long-long",
436    "-W",
437
438    #"-fcatch-undefined-behavior",
439    #"-Wno-missing-method-return-type", # XXX
440    "-Wno-import",
441    "-DPyObjC_BUILD_RELEASE=%02d%02d"%(tuple(map(int, get_os_level().split('.')))),
442    #"-Warray-bounds", # XXX: Needed to avoid False positives for PyTuple access macros
443    ])
444
445## Arghh, a stupid compiler flag can cause problems. Don't
446## enable -O0 if you value your sanity. With -O0 PyObjC will crash
447## on i386 systems when a method returns a struct that isn't returned
448## in registers.
449if '-O0' in get_config_var('CFLAGS'):
450    print ("Change -O0 to -O1")
451    vars = get_config_vars()
452    for k in vars:
453        if isinstance(vars[k], str) and '-O0' in vars[k]:
454            vars[k] = vars[k].replace('-O0', '-O1')
455
456OBJC_LDFLAGS = frameworks('CoreFoundation', 'Foundation', 'Carbon')
457
458if not os.path.exists(os.path.join(SDKROOT, 'usr/include/objc/runtime.h')):
459    CFLAGS.append('-DNO_OBJC2_RUNTIME')
460
461# Force compilation with the local SDK, compilation of PyObC will result in
462# a binary that runs on other releases of the OS without using a particular SDK.
463pass
464pass
465CFLAGS.append('-Ibuild/codegen/')
466
467# Patch distutils: it needs to compile .S files as well.
468from distutils.unixccompiler import UnixCCompiler
469UnixCCompiler.src_extensions.append('.S')
470del UnixCCompiler
471
472
473#
474# Support for an embedded copy of libffi
475#
476FFI_CFLAGS=['-Ilibffi-src/include', '-Ilibffi-src/powerpc']
477
478# The list below includes the source files for all CPU types that we run on
479# this makes it easier to build fat binaries on Mac OS X.
480FFI_SOURCE=[
481    "libffi-src/ffi.c",
482    "libffi-src/types.c",
483    "libffi-src/powerpc/ppc-darwin.S",
484    "libffi-src/powerpc/ppc-darwin_closure.S",
485    "libffi-src/powerpc/ppc-ffi_darwin.c",
486    "libffi-src/powerpc/ppc64-darwin_closure.S",
487    "libffi-src/x86/darwin64.S",
488    "libffi-src/x86/x86-darwin.S",
489    "libffi-src/x86/x86-ffi64.c",
490    "libffi-src/x86/x86-ffi_darwin.c",
491]
492
493
494
495#
496# Calculate the list of extensions: objc._objc + extensions for the unittests
497#
498
499if USE_SYSTEM_FFI:
500    ExtensionList =  [
501        Extension("objc._objc",
502            list(glob.glob(os.path.join('Modules', 'objc', '*.m'))),
503            extra_compile_args=CFLAGS + ['-I' + os.path.join(SDKROOT, "usr/include/ffi")],
504            extra_link_args=OBJC_LDFLAGS + ["-lffi"],
505            depends=list(glob.glob(os.path.join('Modules', 'objc', '*.h'))),
506        ),
507    ]
508
509else:
510    ExtensionList =  [
511        Extension("objc._objc",
512            FFI_SOURCE + list(glob.glob(os.path.join('Modules', 'objc', '*.m'))),
513            extra_compile_args=CFLAGS + FFI_CFLAGS,
514            extra_link_args=OBJC_LDFLAGS,
515            depends=list(glob.glob(os.path.join('Modules', 'objc', '*.h'))),
516        ),
517    ]
518
519for test_source in glob.glob(os.path.join('Modules', 'objc', 'test', '*.m')):
520    name, ext = os.path.splitext(os.path.basename(test_source))
521
522    ExtensionList.append(Extension('PyObjCTest.' + name,
523        [test_source],
524        extra_compile_args=['-IModules/objc'] + CFLAGS,
525        extra_link_args=OBJC_LDFLAGS))
526
527def package_version():
528    fp = open('Modules/objc/pyobjc.h', 'r')
529    for ln in fp.readlines():
530        if ln.startswith('#define OBJC_VERSION'):
531            fp.close()
532            return ln.split()[-1][1:-1]
533
534    raise ValueError("Version not found")
535
536CLASSIFIERS = filter(None,
537"""
538Development Status :: 5 - Production/Stable
539Environment :: Console
540Environment :: MacOS X :: Cocoa
541Intended Audience :: Developers
542License :: OSI Approved :: MIT License
543Natural Language :: English
544Operating System :: MacOS :: MacOS X
545Programming Language :: Python
546Programming Language :: Python :: 2
547Programming Language :: Python :: 2.6
548Programming Language :: Python :: 2.7
549Programming Language :: Python :: 3
550Programming Language :: Python :: 3.1
551Programming Language :: Python :: 3.2
552Programming Language :: Python :: 3.3
553Programming Language :: Objective C
554Topic :: Software Development :: Libraries :: Python Modules
555Topic :: Software Development :: User Interfaces
556""".splitlines())
557
558
559dist = setup(
560    name = "pyobjc-core",
561    version = package_version(),
562    description = "Python<->ObjC Interoperability Module",
563    long_description = LONG_DESCRIPTION,
564    author = "Ronald Oussoren, bbum, SteveM, LeleG, many others stretching back through the reaches of time...",
565    author_email = "pyobjc-dev@lists.sourceforge.net",
566    url = "http://pyobjc.sourceforge.net/",
567    platforms = [ 'MacOS X' ],
568    ext_modules = ExtensionList,
569    packages = [ 'objc', 'PyObjCTools', ],
570    #namespace_packages = ['PyObjCTools'],
571    package_dir = { '': 'Lib', 'PyObjCTest': 'PyObjCTest' },
572    extra_path = "PyObjC",
573    cmdclass = {'build_ext': pyobjc_build_ext, 'install_lib': pyobjc_install_lib, 'build_py': oc_build_py, 'test': oc_test, 'egg_info':my_egg_info },
574    options = {'egg_info': {'egg_base': 'Lib'}},
575    classifiers = CLASSIFIERS,
576    license = 'MIT License',
577    download_url = 'http://pyobjc.sourceforge.net/software/index.php',
578    zip_safe = False,
579)
580