Commits

Ronald Oussoren committed 96d125a

Ensure that the Frameworks directory in the application is on the search path for ctypes.util.find_library

This should fix issue #133

Comments (0)

Files changed (10)

doc/changelog.rst

 - issue #134: Remove target location before copying files into
   the bundle.
 
+- issue #133: Ensure that the application's "Framework" folder
+  is on the search path for ``ctypes.util.find_library``.
+
 py2app 0.8.1
 ------------
 

py2app/bootstrap/ctypes_setup.py

+def _setup_ctypes():
+    from ctypes.macholib import dyld
+    import os
+    frameworks = os.path.join(os.environ['RESOURCEPATH'], '..', 'Frameworks')
+    dyld.DEFAULT_FRAMEWORK_FALLBACK.insert(0, frameworks)
+    dyld.DEFAULT_LIBRARY_FALLBACK.insert(0, frameworks)
+
+_setup_ctypes()

py2app/build_app.py

             prescripts.append('disable_linecache')
             prescripts.append('boot_' + self.style)
         else:
+
+            # Add ctypes prescript because it is needed to
+            # find libraries in the bundle, but we don't run
+            # recipes and hence the ctypes recipe is not used
+            # for alias builds.
+            prescripts.append('ctypes_setup')
+
             if self.additional_paths:
                 prescripts.append('path_inject')
                 prescripts.append(
 
             inc_dir = os.path.join(resdir, 'include', includedir)
             self.mkpath(inc_dir)
-            self.copy_file(get_config_h_filename(), 
+            self.copy_file(get_config_h_filename(),
                            os.path.join(inc_dir, 'pyconfig.h'))
 
 

py2app/recipes/__init__.py

 from . import matplotlib
 from . import pyenchant
 from . import lxml
-
+from . import ctypes
 from . import virtualenv
 from . import pyside
 from . import wx

py2app/recipes/ctypes.py

+import os
+def check(cmd, mf):
+    m = mf.findNode('ctypes')
+    if m is None or m.filename is None:
+        return None
+
+    return dict(
+        prescripts=['py2app.bootstrap.ctypes_setup'],
+    )

py2app_tests/app_with_shared_ctypes/main.py

+import sys
+import ctypes
+import ctypes.util
+
+
+def find_library(name):
+    print(ctypes.util.find_library(name))
+
+mod = None
+def _load():
+    global mod
+    if mod is None:
+        mod = ctypes.CDLL(ctypes.util.find_library("libshared.dylib"))
+    return mod
+
+def half(v):
+    return _load().half(v)
+
+def double(v):
+    return _load().doubled(v)
+
+def square(v):
+    return _load().squared(v)
+
+def import_module(name):
+    try:
+        exec ("import %s"%(name,))
+        m = eval(name)
+    except ImportError:
+        print ("* import failed")
+
+    else:
+        #for k in name.split('.')[1:]:
+        #    m = getattr(m, k)
+        print (m.__name__)
+
+def print_path():
+    print(sys.path)
+
+while True:
+    line = sys.stdin.readline()
+    if not line:
+        break
+
+    try:
+        exec (line)
+    except SystemExit:
+        raise
+
+    except Exception:
+        print ("* Exception " + str(sys.exc_info()[1]))
+
+    sys.stdout.flush()
+    sys.stderr.flush()

py2app_tests/app_with_shared_ctypes/setup.py

+from setuptools import setup, Command, Extension
+from distutils.command import build_ext as mod_build_ext
+
+from distutils.sysconfig import get_config_var
+import subprocess
+import os
+import shutil
+import platform
+import shlex
+import re
+import sys
+
+class sharedlib (Command):
+    description = "build a shared library"
+    user_options = []
+
+    def initialize_options(self): pass
+    def finalize_options(self): pass
+
+    def run(self):
+        if platform.mac_ver()[0] < '10.7.':
+            cc = [get_config_var('CC')]
+            env = dict(os.environ)
+            env['MACOSX_DEPLOYMENT_TARGET'] = get_config_var('MACOSX_DEPLOYMENT_TARGET')
+        else:
+            cc = ['xcrun', 'clang']
+            env = dict(os.environ)
+            env['MACOSX_DEPLOYMENT_TARGET'] = get_config_var('MACOSX_DEPLOYMENT_TARGET')
+
+
+        if not os.path.exists('lib'):
+            os.mkdir('lib')
+        cflags = get_config_var('CFLAGS')
+        arch_flags = sum([shlex.split(x) for x in re.findall('-arch\s+\S+', cflags)], [])
+        root_flags = sum([shlex.split(x) for x in re.findall('-isysroot\s+\S+', cflags)], [])
+
+        cmd = cc + arch_flags + root_flags + ['-dynamiclib', '-o', os.path.abspath('lib/libshared.1.dylib'), 'src/sharedlib.c']
+        subprocess.check_call(cmd, env=env)
+        if os.path.exists('lib/libshared.dylib'):
+            os.unlink('lib/libshared.dylib')
+        os.symlink('libshared.1.dylib', 'lib/libshared.dylib')
+
+        if not os.path.exists('lib/stash'):
+            os.makedirs('lib/stash')
+
+        if os.path.exists('lib/libhalf.dylib'):
+            os.unlink('lib/libhalf.dylib')
+
+        cmd = cc + arch_flags + root_flags + ['-dynamiclib', '-o', os.path.abspath('lib/libhalf.dylib'), 'src/sharedlib.c']
+        subprocess.check_call(cmd, env=env)
+
+        os.rename('lib/libhalf.dylib', 'lib/stash/libhalf.dylib')
+        os.symlink('stash/libhalf.dylib', 'lib/libhalf.dylib')
+
+
+class cleanup (Command):
+    description = "cleanup build stuff"
+    user_options = []
+
+    def initialize_options(self): pass
+    def finalize_options(self): pass
+
+    def run(self):
+        for dn in ('lib', 'build', 'dist'):
+            if os.path.exists(dn):
+                shutil.rmtree(dn)
+
+        for fn in os.listdir('.'):
+            if fn.endswith('.so'):
+                os.unlink(fn)
+
+setup(
+    name='BasicApp',
+    app=['main.py'],
+    cmdclass=dict(
+        sharedlib=sharedlib,
+        cleanup=cleanup,
+    ),
+    options=dict(
+        py2app=dict(
+            frameworks=['lib/libshared.dylib'],
+        ),
+    ),
+)

py2app_tests/app_with_shared_ctypes/src/sharedlib.c

+#include "sharedlib.h"
+
+int squared(int x)
+{
+	return x*x;
+}
+
+int doubled(int x)
+{
+	return x+x;
+}
+
+int half(int x)
+{
+	return x/2;
+}

py2app_tests/app_with_shared_ctypes/src/sharedlib.h

+#ifndef SHARED_LIB_H
+#define SHARED_LIB_H
+
+extern int squared(int);
+extern int doubled(int);
+extern int half(int);
+
+#endif /* SHARED_LIB_H */

py2app_tests/test_app_with_ctypes.py

+import sys
+if (sys.version_info[0] == 2 and sys.version_info[:2] >= (2,7)) or \
+        (sys.version_info[0] == 3 and sys.version_info[:2] >= (3,2)):
+    import unittest
+else:
+    import unittest2 as unittest
+
+import subprocess
+import shutil
+import time
+import os
+import signal
+import py2app
+import hashlib
+
+DIR_NAME=os.path.dirname(os.path.abspath(__file__))
+
+class TestBasicAppWithCTypes (unittest.TestCase):
+    py2app_args = []
+    python_args = []
+    app_dir = os.path.join(DIR_NAME, 'app_with_shared_ctypes')
+
+    # Basic setup code
+    #
+    # The code in this block needs to be moved to
+    # a base-class.
+    @classmethod
+    def setUpClass(cls):
+        env=os.environ.copy()
+        pp = os.path.dirname(os.path.dirname(py2app.__file__))
+        if 'PYTHONPATH' in env:
+            env['PYTHONPATH'] = pp + ':' + env['PYTHONPATH']
+        else:
+            env['PYTHONPATH'] = pp
+
+        if 'LANG' not in env:
+            # Ensure that testing though SSH works
+            env['LANG'] = 'en_US.UTF-8'
+
+        p = subprocess.Popen([
+                sys.executable] + cls.python_args + [
+                    'setup.py', 'sharedlib'],
+                cwd = cls.app_dir,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            close_fds=True,
+            env=env
+            )
+        lines = p.communicate()[0]
+        if p.wait() != 0:
+            print (lines)
+            raise AssertionError("Running sharedlib failed")
+
+        p = subprocess.Popen([
+                sys.executable ] + cls.python_args + [
+                    'setup.py', 'py2app'] + cls.py2app_args,
+            cwd = cls.app_dir,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            close_fds=True,
+            env=env
+            )
+        lines = p.communicate()[0]
+        if p.wait() != 0:
+            print (lines)
+            raise AssertionError("Creating basic_app bundle failed")
+
+    @classmethod
+    def tearDownClass(cls):
+        if os.path.exists(os.path.join(cls.app_dir, 'build')):
+            shutil.rmtree(os.path.join(cls.app_dir, 'build'))
+
+        if os.path.exists(os.path.join(cls.app_dir, 'dist')):
+            shutil.rmtree(os.path.join(cls.app_dir, 'dist'))
+
+        if os.path.exists(os.path.join(cls.app_dir, 'lib')):
+            shutil.rmtree(os.path.join(cls.app_dir, 'lib'))
+
+        for fn in os.listdir(cls.app_dir):
+            if fn.endswith('.so'):
+                os.unlink(os.path.join(cls.app_dir, fn))
+
+    def start_app(self):
+        # Start the test app, return a subprocess object where
+        # stdin and stdout are connected to pipes.
+        path = os.path.join(
+                self.app_dir,
+            'dist/BasicApp.app/Contents/MacOS/BasicApp')
+
+        p = subprocess.Popen([path],
+                stdin=subprocess.PIPE,
+                stdout=subprocess.PIPE,
+                close_fds=True,
+                )
+                #stderr=subprocess.STDOUT)
+        return p
+
+    def wait_with_timeout(self, proc, timeout=10):
+        for i in range(timeout):
+            x = proc.poll()
+            if x is None:
+                time.sleep(1)
+            else:
+                return x
+
+        os.kill(proc.pid, signal.SIGKILL)
+        return proc.wait()
+
+    #
+    # End of setup code
+    #
+
+    def test_basic_start(self):
+        p = self.start_app()
+
+        p.stdin.close()
+
+        exit = self.wait_with_timeout(p)
+        self.assertEqual(exit, 0)
+
+        p.stdout.close()
+
+    def test_find_library(self):
+        p = self.start_app()
+        p.stdin.write('find_library("shared")\n'.encode('latin1'))
+        ln = p.stdout.readline().strip()
+        self.assertEqual(os.path.realpath(ln), os.path.realpath(
+            os.path.join(self.app_dir, 'dist/BasicApp.app',
+            'Contents', 'Frameworks', 'libshared.dylib')))
+
+        p.stdin.write('find_library("System")\n'.encode('latin1'))
+        ln = p.stdout.readline().strip()
+        self.assertEqual(os.path.realpath(ln), os.path.realpath('/usr/lib/libSystem.dylib'))
+
+
+
+
+    def test_extension_use(self):
+        p = self.start_app()
+
+        p.stdin.write('print(double(9))\n'.encode('latin1'))
+        p.stdin.flush()
+        ln = p.stdout.readline()
+        self.assertEqual(ln.strip(), b"18")
+
+        p.stdin.write('print(square(9))\n'.encode('latin1'))
+        p.stdin.flush()
+        ln = p.stdout.readline()
+        self.assertEqual(ln.strip(), b"81")
+
+        p.stdin.write('print(half(16))\n'.encode('latin1'))
+        p.stdin.flush()
+        ln = p.stdout.readline()
+        self.assertEqual(ln.strip(), b"8")
+
+    def test_simple_imports(self):
+        p = self.start_app()
+
+        # Basic module that is always present:
+        p.stdin.write('import_module("os")\n'.encode('latin1'))
+        p.stdin.flush()
+        ln = p.stdout.readline()
+        self.assertEqual(ln.strip(), b"os")
+
+        can_import_stdlib = False
+        if '--alias' in self.py2app_args:
+            can_import_stdlib = True
+
+        if '--semi-standalone' in self.py2app_args:
+            can_import_stdlib = True
+
+        if sys.prefix.startswith('/System/'):
+            can_import_stdlib = True
+
+        if not can_import_stdlib:
+            # Not a dependency of the module (stdlib):
+            p.stdin.write('import_module("xdrlib")\n'.encode('latin1'))
+            p.stdin.flush()
+            ln = p.stdout.readline().decode('utf-8')
+            self.assertTrue(ln.strip().startswith("* import failed"), ln)
+
+        else:
+            p.stdin.write('import_module("xdrlib")\n'.encode('latin1'))
+            p.stdin.flush()
+            ln = p.stdout.readline()
+            self.assertEqual(ln.strip(), b"xdrlib")
+
+        if sys.prefix.startswith('/System') or '--alias' in self.py2app_args:
+            # py2app is included as part of the system install
+            p.stdin.write('import_module("py2app")\n'.encode('latin1'))
+            p.stdin.flush()
+            ln = p.stdout.readline()
+            self.assertEqual(ln.strip(), b"py2app")
+
+
+        else:
+            # Not a dependency of the module (external):
+            p.stdin.write('import_module("py2app")\n'.encode('latin1'))
+            p.stdin.flush()
+            ln = p.stdout.readline().decode('utf-8')
+            self.assertTrue(ln.strip().startswith("* import failed"), ln)
+
+        p.stdin.close()
+        p.stdout.close()
+
+    def test_app_structure(self):
+        path = os.path.join(self.app_dir, 'dist/BasicApp.app')
+
+        if '--alias' in self.py2app_args:
+            #self.assertTrue(os.path.exists(os.path.join(path, 'Contents', 'Frameworks', 'libshared.1.dylib')))
+            #self.assertTrue(os.path.exists(os.path.join(path, 'Contents', 'Frameworks', 'libshared.dylib')))
+            pass
+
+        else:
+            self.assertTrue(os.path.isfile(os.path.join(path, 'Contents', 'Frameworks', 'libshared.1.dylib')))
+            self.assertTrue(os.path.islink(os.path.join(path, 'Contents', 'Frameworks', 'libshared.dylib')))
+            self.assertEqual(os.readlink(os.path.join(path, 'Contents', 'Frameworks', 'libshared.dylib')), 'libshared.1.dylib')
+
+
+
+class TestBasicAliasAppWithCTypes (TestBasicAppWithCTypes):
+    py2app_args = [ '--alias', ]
+
+class TestBasicSemiStandaloneAppWithCTypes (TestBasicAppWithCTypes):
+    py2app_args = [ '--semi-standalone', ]
+
+
+if __name__ == "__main__":
+    unittest.main()