1#!/usr/bin/env python
2
3import sys
4import subprocess
5import shutil
6import re
7import os
8
9# We need at least Python 2.5
10MIN_PYTHON = (2, 5)
11
12# FIXME: autodetect default values for USE_* variables:
13#  both should be false by default, unless
14#  1) python is /usr/bin/python: both should be true
15#  2) python build with --with-system-libffi: USE_SYSTEM_FFI
16#     should be true.
17
18# Set USE_SYSTEM_FFI to True to link to the system version
19# of libffi
20USE_SYSTEM_FFI = True
21
22# Set USE_SYSTEM_LIBXML to True to link to the system version
23# of libxml2 (defaults to False to avoid problems when building
24# on 10.6 and running on an earlier release)
25USE_SYSTEM_LIBXML = True
26
27SDKROOT = os.environ.get('SDKROOT')
28if SDKROOT is None or SDKROOT is '':
29    SDKROOT = '/'
30
31if sys.version_info < MIN_PYTHON:
32    vstr = '.'.join(map(str, MIN_PYTHON))
33    raise SystemExit('PyObjC: Need at least Python ' + vstr)
34
35
36try:
37    import setuptools
38
39except ImportError:
40    import distribute_setup
41    distribute_setup.use_setuptools()
42
43
44extra_args=dict(
45    use_2to3 = True,
46)
47
48from setuptools.command import build_py
49from setuptools.command import test
50from distutils import log
51class oc_build_py (build_py.build_py):
52    def run_2to3(self, files, doctests=True):
53        files = [ fn for fn in files if not os.path.basename(fn).startswith('test3_') ]
54        build_py.build_py.run_2to3(self, files, doctests)
55
56    def build_packages(self):
57        log.info("Overriding build_packages to copy PyObjCTest")
58        p = self.packages
59        self.packages = list(self.packages) + ['PyObjCTest']
60        try:
61            build_py.build_py.build_packages(self)
62        finally:
63            self.packages = p
64
65from pkg_resources import working_set, normalize_path, add_activation_listener, require
66
67class oc_test (test.test):
68    def run_tests(self):
69        import sys, os
70
71        if sys.version_info[0] == 3:
72            rootdir =  os.path.dirname(os.path.abspath(__file__))
73            if rootdir in sys.path:
74                sys.path.remove(rootdir)
75
76        ei_cmd = self.get_finalized_command('egg_info')
77        egg_name = ei_cmd.egg_name.replace('-', '_')
78
79        to_remove = []
80        for dirname in sys.path:
81            bn = os.path.basename(dirname)
82            if bn.startswith(egg_name + "-"):
83                to_remove.append(dirname)
84
85        for dirname in to_remove:
86            log.info("removing installed %r from sys.path before testing"%(dirname,))
87            sys.path.remove(dirname)
88
89        from PyObjCTest.loader import makeTestSuite
90        import unittest
91
92        #import pprint; pprint.pprint(sys.path)
93
94        unittest.main(None, None, [unittest.__file__]+self.test_args)
95
96from setuptools.command import egg_info as orig_egg_info
97
98def write_header(cmd, basename, filename):
99    data = open(os.path.join('Modules/objc/', os.path.basename(basename)), 'rU').read()
100    if not cmd.dry_run:
101        if not os.path.exists(os.path.dirname(filename)):
102            os.makedirs(os.path.dirname(filename))
103
104    cmd.write_file(basename, filename, data)
105
106
107# This is a workaround for a bug in setuptools: I'd like
108# to use the 'egg_info.writers' entry points in the setup()
109# call, but those don't work when also using a package_base
110# argument as we do.
111# (issue 123 in the distribute tracker)
112class egg_info (orig_egg_info.egg_info):
113    def run(self):
114        orig_egg_info.egg_info.run(self)
115
116        for hdr in ("pyobjc-compat.h", "pyobjc-api.h"):
117            fn = os.path.join("include", hdr)
118
119            write_header(self, fn, os.path.join(self.egg_info, fn))
120
121if sys.version_info[0] == 3:
122    # FIXME: add custom test command that does the work.
123    # - Patch sys.path
124    # - Ensure PyObjCTest gets translated by 2to3
125    from distutils.util import get_platform
126    build_dir = 'build/lib.%s-%d.%d'%(
127        get_platform(), sys.version_info[0], sys.version_info[1])
128    if hasattr(sys, 'gettotalrefcount'):
129        build_dir += '-pydebug'
130    sys.path.insert(0,  build_dir)
131
132
133import os
134import glob
135import site
136import platform
137
138if 'MallocStackLogging' in os.environ:
139    del os.environ['MallocStackLogging']
140if 'MallocStackLoggingNoCompact' in os.environ:
141    del os.environ['MallocStackLoggingNoCompact']
142
143# See the news file:
144#os.environ['MACOSX_DEPLOYMENT_TARGET']='10.5'
145
146
147
148#if int(os.uname()[2].split('.')[0]) >= 10:
149#        USE_SYSTEM_FFI = True
150
151
152# Some PiPy stuff
153LONG_DESCRIPTION="""
154PyObjC is a bridge between Python and Objective-C.  It allows full
155featured Cocoa applications to be written in pure Python.  It is also
156easy to use other frameworks containing Objective-C class libraries
157from Python and to mix in Objective-C, C and C++ source.
158
159Python is a highly dynamic programming language with a shallow learning
160curve.  It combines remarkable power with very clear syntax.
161
162The installer package installs a number of Xcode templates for
163easily creating new Cocoa-Python projects.
164
165PyObjC also supports full introspection of Objective-C classes and
166direct invocation of Objective-C APIs from the interactive interpreter.
167
168PyObjC requires MacOS X 10.4 or later.  This beta release requires
169MacOS X 10.5.
170"""
171
172from setuptools import setup, Extension, find_packages
173from setuptools.command import build_ext, install_lib
174import os
175
176class pyobjc_install_lib (install_lib.install_lib):
177    def get_exclusions(self):
178        result = install_lib.install_lib.get_exclusions(self)
179        for fn in install_lib._install_lib.get_outputs(self):
180            if 'PyObjCTest' in fn:
181                result[fn] = 1
182
183        for fn in os.listdir('PyObjCTest'):
184            result[os.path.join('PyObjCTest', fn)] = 1
185            result[os.path.join(self.install_dir, 'PyObjCTest', fn)] = 1
186
187
188        return result
189
190class pyobjc_build_ext (build_ext.build_ext):
191    def run(self):
192        if not USE_SYSTEM_LIBXML:
193            build = self.get_finalized_command('build')
194            xmldir=os.path.join(build.build_base, 'libxml')
195            if not os.path.exists(xmldir):
196                builddir=os.path.join(build.build_base, 'libxml-build')
197                if os.path.exists(builddir):
198                    shutil.rmtree(builddir)
199                os.makedirs(builddir)
200
201                cflags = get_config_var('CFLAGS')
202                if '-isysroot' in cflags:
203                    cflags = re.sub(r'-isysroot\s*\S*', '-isysroot /', cflags)
204
205                p = subprocess.Popen(
206                        ['../../libxml2-src/configure',
207                            '--disable-dependency-tracking',
208                            '--enable-static',
209                            '--disable-shared',
210                            '--prefix=%s'%(os.path.abspath(xmldir),),
211                            '--with-minimum',
212                            'CFLAGS=%s'%(cflags,),
213                            'CC=%s'%(get_config_var('CC')),
214                            ],
215                        cwd=builddir)
216                xit=p.wait()
217                if xit != 0:
218                    shutil.rmtree(builddir)
219                    raise RuntimeError("libxml configure failed")
220                p = subprocess.Popen(
221                        ['make', 'install'],
222                        cwd=builddir)
223                xit=p.wait()
224                if xit != 0:
225                    raise RuntimeError("libxml install failed")
226
227            os.environ['PATH'] = xmldir + "/bin:" + os.environ['PATH']
228
229        for ext in self.extensions:
230            if ext.name == "objc._objc":
231                if not USE_SYSTEM_LIBXML:
232                    ext.extra_link_args.append('-Wl,-search_paths_first')
233                ext.extra_compile_args.extend(xml2config('--cflags'))
234                ext.extra_link_args.extend(xml2config('--libs'))
235
236        build_ext.build_ext.run(self)
237        extensions = self.extensions
238        self.extensions = [
239                e for e in extensions if e.name.startswith('PyObjCTest') ]
240        self.copy_extensions_to_source()
241        self.extensions = extensions
242
243def frameworks(*args):
244    lst = []
245    for arg in args:
246        lst.extend(['-framework', arg])
247    return lst
248
249def IfFrameWork(name, packages, extensions, headername=None):
250    """
251    Return the packages and extensions if the framework exists, or
252    two empty lists if not.
253    """
254    import os
255    for pth in ('/System/Library/Frameworks', '/Library/Frameworks'):
256        basedir = os.path.join(pth, name)
257        if os.path.exists(basedir):
258            if (headername is None) or os.path.exists(os.path.join(basedir, "Headers", headername)):
259                return packages, extensions
260    return [], []
261
262# Double-check
263if sys.platform != 'darwin':
264    print("You're not running on MacOS X, and don't use GNUstep")
265    print("I don't know how to build PyObjC on such a platform.")
266    print("Please read the ReadMe.")
267    print("")
268    raise SystemExit("ObjC runtime not found")
269
270from distutils.sysconfig import get_config_var
271
272CFLAGS=[ ]
273
274# Enable 'PyObjC_STRICT_DEBUGGING' to enable some costly internal
275# assertions.
276CFLAGS.extend([
277
278    # Use this to analyze with clang
279    #"--analyze",
280
281# The following flags are an attempt at getting rid of /usr/local
282# in the compiler search path.
283    "-DPyObjC_STRICT_DEBUGGING",
284    "-DMACOSX", # For libffi
285    "-DPyObjC_BUILD_RELEASE=%02d%02d"%(tuple(map(int, platform.mac_ver()[0].split('.')[:2]))),
286    "-no-cpp-precomp",
287    "-DMACOSX",
288    "-g",
289    "-fexceptions",
290
291
292    # Loads of warning flags
293    "-Wall", "-Wstrict-prototypes", "-Wmissing-prototypes",
294    "-Wformat=2", "-W",
295    #"-Wshadow", # disabled due to warnings from Python headers
296    "-Wpointer-arith", #"-Wwrite-strings",
297    "-Wmissing-declarations",
298    "-Wnested-externs",
299    "-Wno-long-long",
300
301    "-Wno-import",
302    ])
303
304## Arghh, a stupid compiler flag can cause problems. Don't
305## enable -O0 if you value your sanity. With -O0 PyObjC will crash
306## on i386 systems when a method returns a struct that isn't returned
307## in registers.
308if '-O0' in get_config_var('CFLAGS'):
309    print ("Change -O0 to -O1")
310    CFLAGS.append('-O1')
311
312OBJC_LDFLAGS = frameworks('CoreFoundation', 'Foundation', 'Carbon')
313
314if not os.path.exists(os.path.join(SDKROOT, 'usr/include/objc/runtime.h')):
315    CFLAGS.append('-DNO_OBJC2_RUNTIME')
316
317# Force compilation with the local SDK, compilation of PyObC will result in
318# a binary that runs on other releases of the OS without using a particular SDK.
319pass
320pass
321
322
323# We're using xml2, check for the flags to use:
324def xml2config(arg):
325    import os, shlex
326    ln = os.popen('xml2-config %s'%(arg,), 'r').readline()
327    ln = ln.strip()
328
329    return shlex.split(ln)
330
331#CFLAGS.extend(xml2config('--cflags'))
332#OBJC_LDFLAGS.extend(xml2config('--libs'))
333
334
335
336CFLAGS.append('-Ibuild/codegen/')
337
338# Patch distutils: it needs to compile .S files as well.
339from distutils.unixccompiler import UnixCCompiler
340UnixCCompiler.src_extensions.append('.S')
341del UnixCCompiler
342
343
344#
345# Support for an embedded copy of libffi
346#
347FFI_CFLAGS=['-Ilibffi-src/include', '-Ilibffi-src/powerpc']
348
349# The list below includes the source files for all CPU types that we run on
350# this makes it easier to build fat binaries on Mac OS X.
351FFI_SOURCE=[
352    "libffi-src/ffi.c",
353    "libffi-src/types.c",
354    "libffi-src/powerpc/ppc-darwin.S",
355    "libffi-src/powerpc/ppc-darwin_closure.S",
356    "libffi-src/powerpc/ppc-ffi_darwin.c",
357    "libffi-src/powerpc/ppc64-darwin_closure.S",
358    "libffi-src/x86/darwin64.S",
359    "libffi-src/x86/x86-darwin.S",
360    "libffi-src/x86/x86-ffi64.c",
361    "libffi-src/x86/x86-ffi_darwin.c",
362]
363
364
365
366#
367# Calculate the list of extensions: objc._objc + extensions for the unittests
368#
369
370if USE_SYSTEM_FFI:
371    ExtensionList =  [
372        Extension("objc._objc",
373            list(glob.glob(os.path.join('Modules', 'objc', '*.m'))),
374            extra_compile_args=CFLAGS + ['-I' + os.path.join(SDKROOT, "usr/include/ffi")],
375            extra_link_args=OBJC_LDFLAGS + ["-lffi"],
376            depends=list(glob.glob(os.path.join('Modules', 'objc', '*.h'))),
377        ),
378    ]
379
380else:
381    ExtensionList =  [
382        Extension("objc._objc",
383            FFI_SOURCE + list(glob.glob(os.path.join('Modules', 'objc', '*.m'))),
384            extra_compile_args=CFLAGS + FFI_CFLAGS,
385            extra_link_args=OBJC_LDFLAGS,
386            depends=list(glob.glob(os.path.join('Modules', 'objc', '*.h'))),
387        ),
388    ]
389
390for test_source in glob.glob(os.path.join('Modules', 'objc', 'test', '*.m')):
391    name, ext = os.path.splitext(os.path.basename(test_source))
392
393    ExtensionList.append(Extension('PyObjCTest.' + name,
394        [test_source],
395        extra_compile_args=['-IModules/objc'] + CFLAGS,
396        extra_link_args=OBJC_LDFLAGS))
397
398def package_version():
399    fp = open('Modules/objc/pyobjc.h', 'r')
400    for ln in fp.readlines():
401        if ln.startswith('#define OBJC_VERSION'):
402            fp.close()
403            return ln.split()[-1][1:-1]
404
405    raise ValueError("Version not found")
406
407CLASSIFIERS = filter(None,
408"""
409Development Status :: 5 - Production/Stable
410Environment :: Console
411Environment :: MacOS X :: Cocoa
412Intended Audience :: Developers
413License :: OSI Approved :: MIT License
414Natural Language :: English
415Operating System :: MacOS :: MacOS X
416Programming Language :: Python
417Programming Language :: Python :: 2
418Programming Language :: Python :: 2.6
419Programming Language :: Python :: 2.7
420Programming Language :: Python :: 3
421Programming Language :: Python :: 3.1
422Programming Language :: Python :: 3.2
423Programming Language :: Objective C
424Topic :: Software Development :: Libraries :: Python Modules
425Topic :: Software Development :: User Interfaces
426""".splitlines())
427
428
429dist = setup(
430    name = "pyobjc-core",
431    version = package_version(),
432    description = "Python<->ObjC Interoperability Module",
433    long_description = LONG_DESCRIPTION,
434    author = "Ronald Oussoren, bbum, SteveM, LeleG, many others stretching back through the reaches of time...",
435    author_email = "pyobjc-dev@lists.sourceforge.net",
436    url = "http://pyobjc.sourceforge.net/",
437    platforms = [ 'MacOS X' ],
438    ext_modules = ExtensionList,
439    packages = [ 'objc', 'PyObjCTools', ],
440    #namespace_packages = ['PyObjCTools'],
441    package_dir = { '': 'Lib', 'PyObjCTest': 'PyObjCTest' },
442    extra_path = "PyObjC",
443    cmdclass = {'build_ext': pyobjc_build_ext, 'install_lib': pyobjc_install_lib, 'build_py': oc_build_py, 'test': oc_test, 'egg_info':egg_info },
444    options = {'egg_info': {'egg_base': 'Lib'}},
445    classifiers = CLASSIFIERS,
446    license = 'MIT License',
447    download_url = 'http://pyobjc.sourceforge.net/software/index.php',
448    test_suite='PyObjCTest.loader.makeTestSuite',
449    zip_safe = False,
450#    entry_points = {
451#        "egg_info.writers": [
452#            "include/pyobjc-api.h = __main__:write_header",
453#            "include/pyobjc-compat.h = __main__:write_header",
454#        ],
455#    },
456    **extra_args
457)
458