Commits

Ronald Oussoren  committed 3674877 Merge

merge 0.7.x branch, fixes issue #65

  • Participants
  • Parent commits d345278, d37c2ad

Comments (0)

Files changed (8)

File doc/changelog.rst

 - Issue #87: Ignore '.git' and '.hg' directories while copying package data
   ('.svn' and 'CVS' were already ignored).
 
+- Issue #65: the fix in 0.7 to avoid copying a symlinked library twice caused
+  problems for some users because only one of the file names ended up in the
+  application bundle. This release ensures that both names exist (one as a
+  symbolic name to the other).
+
 py2app 0.7.2
 ------------
 

File py2app/build_app.py

         dest = os.path.join(self.dest, os.path.basename(src))
         if os.path.islink(src):
             dest = os.path.join(self.dest, os.path.basename(os.path.realpath(src)))
+
+            # Ensure that the orginal name also exists, avoids problems when
+            # the filename is used from Python (see issue #65)
+            link_dest = os.path.join(self.dest, os.path.basename(src))
+            os.symlink(os.path.basename(dest), link_dest)
+
         else:
             dest = os.path.join(self.dest, os.path.basename(src))
         return self.appbuilder.copy_dylib(src, dest)

File py2app_tests/app_with_sharedlib/main.py

+import sys
+from square import square
+from double import double
+
+
+def function():
+    import decimal
+
+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()

File py2app_tests/app_with_sharedlib/mod.c

+#include "Python.h"
+#include "sharedlib.h"
+
+#define _STR(x) #x
+#define STR(x) _STR(x)
+
+static PyObject*
+mod_function(PyObject* mod __attribute__((__unused__)), PyObject* arg)
+{
+	int value = PyLong_AsLong(arg);
+	if (PyErr_Occurred()) {
+		return NULL;
+	}
+	value = FUNC_NAME(value);
+	return PyLong_FromLong(value);
+}
+
+static PyMethodDef mod_methods[] = {
+	{
+		STR(NAME),
+		(PyCFunction)mod_function,
+		METH_O,
+		0
+	},
+	{ 0, 0, 0, 0 }
+};
+
+#if PY_VERSION_HEX >= 0x03000000
+
+static struct PyModuleDef mod_module = {
+	PyModuleDef_HEAD_INIT,
+	STR(NAME),
+	NULL,
+	0,
+	mod_methods,
+	NULL,
+	NULL,
+	NULL,
+	NULL
+};
+
+#define INITERROR() return NULL
+#define INITDONE() return m
+
+#define INITFUNC PyInt_ ## NAME
+
+PyObject* INITFUNC(void);
+
+PyObject*
+INITFUNC(void)
+
+#else
+
+#define INITERROR() return
+#define INITDONE() return
+
+
+void INITFUNC(void);
+
+void
+INITFUNC(void)
+#endif
+
+{
+	PyObject* m;
+
+
+#if PY_VERSION_HEX >= 0x03000000
+	m = PyModule_Create(&mod_module);
+#else
+	m = Py_InitModule4(STR(NAME), mod_methods,
+		NULL, NULL, PYTHON_API_VERSION);
+#endif
+	if (!m) {
+		INITERROR();
+	}
+
+	INITDONE();
+}

File py2app_tests/app_with_sharedlib/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
+
+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)
+
+
+        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)
+        if os.path.exists('lib/libshared.dylib'):
+            os.unlink('lib/libshared.dylib')
+        os.symlink('libshared.1.dylib', 'lib/libshared.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)
+
+class my_build_ext (mod_build_ext.build_ext):
+    def run(self):
+        cmd = self.get_finalized_command('sharedlib')
+        cmd.run()
+
+
+        mod_build_ext.build_ext.run(self)
+        
+        subprocess.check_call([
+            'install_name_tool', '-change', 
+               os.path.abspath('lib/libshared.1.dylib'),
+               os.path.abspath('lib/libshared.dylib'),
+               'square.so'
+            ])
+
+setup(
+    name='BasicApp',
+    app=['main.py'],
+    cmdclass=dict(
+        sharedlib=sharedlib,
+        cleanup=cleanup,
+        build_ext=my_build_ext
+    ),
+    ext_modules=[
+        Extension("double", [ "mod.c" ], 
+            extra_compile_args=["-Isrc", '-DNAME=double', '-DFUNC_NAME=doubled', '-DINITFUNC=initdouble'],
+            extra_link_args=["-Llib", "-lshared"]),
+        Extension("square", [ "mod.c" ], 
+            extra_compile_args=["-Isrc", '-DNAME=square', '-DFUNC_NAME=squared', '-DINITFUNC=initsquare'],
+            extra_link_args=["-Llib", "-lshared.1"]),
+    ],
+    options=dict(
+        build_ext=dict(
+            inplace=True
+        )
+    ),
+)

File py2app_tests/app_with_sharedlib/src/sharedlib.c

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

File py2app_tests/app_with_sharedlib/src/sharedlib.h

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

File py2app_tests/test_app_with_sharedlib.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 TestBasicAppWithExtension (unittest.TestCase):
+    py2app_args = []
+    python_args = []
+    app_dir = os.path.join(DIR_NAME, 'app_with_sharedlib')
+
+    # 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', '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_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")
+
+    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")
+
+        # Dependency of the main module:
+        p.stdin.write('import_module("decimal")\n'.encode('latin1'))
+        p.stdin.flush()
+        ln = p.stdout.readline()
+        self.assertEqual(ln.strip(), b"decimal")
+
+        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'):
+            # 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().decode('utf-8')
+            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.assertFalse(os.path.exists(os.path.join(path, 'Contents', 'Frameworks', 'libshared.1.dylib')))
+            self.assertFalse(os.path.exists(os.path.join(path, 'Contents', 'Frameworks', 'libshared.dylib')))
+
+        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 TestBasicAliasAppWithExtension (TestBasicAppWithExtension):
+    py2app_args = [ '--alias', ]
+
+class TestBasicSemiStandaloneAppWithExtension (TestBasicAppWithExtension):
+    py2app_args = [ '--semi-standalone', ]
+
+
+if __name__ == "__main__":
+    unittest.main()