aboutsummaryrefslogtreecommitdiffstats
path: root/pyside_postinstall.py
blob: 3cae89970215970d63f7219dba2a7f8d66cfdef7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# Postinstall script for PySide
#
# Generates the qt.conf file
#
# This file is based on pywin32_postinstall.py file from pywin32 project

import os, sys, traceback, shutil, fnmatch, stat
from os.path import dirname, abspath
from subprocess import Popen, PIPE
import re


try:
    # When this script is run from inside the bdist_wininst installer,
    # file_created() and directory_created() are additional builtin
    # functions which write lines to Python23\pyside-install.log. This is
    # a list of actions for the uninstaller, the format is inspired by what
    # the Wise installer also creates.
    file_created
    is_bdist_wininst = True
except NameError:
    is_bdist_wininst = False # we know what it is not - but not what it is :)
    def file_created(file):
        pass

def install():
    if sys.platform == "win32":
        install_win32()
    else:
        install_posix()

def filter_match(name, patterns):
    for pattern in patterns:
        if pattern is None:
            continue
        if fnmatch.fnmatch(name, pattern):
            return True
    return False

def set_exec(name):
    mode = os.stat(name).st_mode
    new_mode = mode
    if new_mode & stat.S_IRUSR:
        new_mode = new_mode | stat.S_IXUSR
    if new_mode & stat.S_IRGRP:
        new_mode = new_mode | stat.S_IXGRP
    if new_mode & stat.S_IROTH:
        new_mode = new_mode | stat.S_IXOTH
    if (mode != new_mode):
        print("Setting exec for '%s' (mode %o => %o)" % (name, mode, new_mode))
        os.chmod(name, new_mode)


def back_tick(cmd, ret_err=False):
    """ Run command `cmd`, return stdout, or stdout, stderr if `ret_err`

    Roughly equivalent to ``check_output`` in Python 2.7

    Parameters
    ----------
    cmd : str
        command to execute
    ret_err : bool, optional
        If True, return stderr in addition to stdout.  If False, just return
        stdout

    Returns
    -------
    out : str or tuple
        If `ret_err` is False, return stripped string containing stdout from
        `cmd`.  If `ret_err` is True, return tuple of (stdout, stderr) where
        ``stdout`` is the stripped stdout, and ``stderr`` is the stripped
        stderr.

    Raises
    ------
    Raises RuntimeError if command returns non-zero exit code
    """
    proc = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True)
    out, err = proc.communicate()
    retcode = proc.returncode
    if retcode is None:
        proc.terminate()
        raise RuntimeError(cmd + ' process did not terminate')
    if retcode != 0:
        raise RuntimeError(cmd + ' process returned code %d' % retcode)
    out = out.strip()
    if not ret_err:
        return out
    return out, err.strip()


OSX_OUTNAME_RE = re.compile(r'\(compatibility version [\d.]+, current version '
                        '[\d.]+\)')

def osx_get_install_names(libpath):
    """ Get OSX library install names from library `libpath` using ``otool``

    Parameters
    ----------
    libpath : str
        path to library

    Returns
    -------
    install_names : list of str
        install names in library `libpath`
    """
    out = back_tick('otool -L ' + libpath)
    libs = [line for line in out.split('\n')][1:]
    return [OSX_OUTNAME_RE.sub('', lib).strip() for lib in libs]


OSX_RPATH_RE = re.compile(r"path (.+) \(offset \d+\)")

def osx_get_rpaths(libpath):
    """ Get rpaths from library `libpath` using ``otool``

    Parameters
    ----------
    libpath : str
        path to library

    Returns
    -------
    rpaths : list of str
        rpath values stored in ``libpath``

    Notes
    -----
    See ``man dyld`` for more information on rpaths in libraries
    """
    lines = back_tick('otool -l ' + libpath).split('\n')
    ctr = 0
    rpaths = []
    while ctr < len(lines):
        line = lines[ctr].strip()
        if line != 'cmd LC_RPATH':
            ctr += 1
            continue
        assert lines[ctr + 1].strip().startswith('cmdsize')
        rpath_line = lines[ctr + 2].strip()
        match = OSX_RPATH_RE.match(rpath_line)
        if match is None:
            raise RuntimeError('Unexpected path line: ' + rpath_line)
        rpaths.append(match.groups()[0])
        ctr += 3
    return rpaths


def localize_libpaths(libpath, local_libs, enc_path=None):
    """ Set rpaths and install names to load local dynamic libs at run time

    Use ``install_name_tool`` to set relative install names in `libpath` (as
    named in `local_libs` to be relative to `enc_path`.  The default for
    `enc_path` is the directory containing `libpath`.

    Parameters
    ----------
    libpath : str
        path to library for which to set install names and rpaths
    local_libs : sequence of str
        library (install) names that should be considered relative paths
    enc_path : str, optional
        path that does or will contain the `libpath` library, and to which the
        `local_libs` are relative.  Defaults to current directory containing
        `libpath`.
    """
    if enc_path is None:
        enc_path = abspath(dirname(libpath))
    install_names = osx_get_install_names(libpath)
    need_rpath = False
    for install_name in install_names:
        if install_name[0] in '/@':
            continue
        back_tick('install_name_tool -change %s @rpath/%s %s' %
           (install_name, install_name, libpath))
        need_rpath = True
    if need_rpath and enc_path not in osx_get_rpaths(libpath):
        back_tick('install_name_tool -add_rpath %s %s' %
           (enc_path, libpath))


def install_posix():
    # Try to find PySide package
    try:
        import PySide
    except ImportError:
        print("The PySide package not found: %s" % traceback.print_exception(*sys.exc_info()))
        return
    pyside_path = os.path.abspath(os.path.dirname(PySide.__file__))
    print("PySide package found in %s..." % pyside_path)

    executables = ['shiboken']
    if sys.platform.startswith('linux'):
        executables.append('patchelf')
        patchelf_path = os.path.join(pyside_path, "patchelf")
        from distutils.spawn import spawn

        def rpath_cmd(pyside_path, srcpath):
            cmd = [patchelf_path, '--set-rpath', pyside_path, srcpath]
            spawn(cmd, search_path=False, verbose=1)

        pyside_libs = [lib for lib in os.listdir(pyside_path) if filter_match(
                       lib, ["Qt*.so", "phonon.so", "shiboken"])]
    elif sys.platform == 'darwin':
        pyside_libs = [lib for lib in os.listdir(pyside_path) if filter_match(
                       lib, ["*.so", "*.dylib", "shiboken"])]

        def rpath_cmd(pyside_path, srcpath):
            localize_libpaths(srcpath, pyside_libs, pyside_path)

    else:
        raise RuntimeError('Not configured for platform ' +
                           sys.platform)

    # Set exec mode on executables
    for executable in executables:
        execpath = os.path.join(pyside_path, executable)
        set_exec(execpath)

    # Update rpath in PySide libs
    for srcname in pyside_libs:
        if os.path.isdir(srcname):
            continue
        srcpath = os.path.join(pyside_path, srcname)
        rpath_cmd(pyside_path, srcpath)
        print("Patched rpath in %s to %s." % (srcpath, pyside_path))

    # Check PySide installation status
    try:
        from PySide import QtCore
        print("PySide package successfully installed in %s..." % \
            os.path.abspath(os.path.dirname(QtCore.__file__)))
    except ImportError:
        print("The PySide package not installed: %s" % traceback.print_exception(*sys.exc_info()))


def install_win32():
    # Try to find PySide package
    try:
        from PySide import QtCore
    except ImportError:
        print("The PySide package not found: %s" % traceback.print_exception(*sys.exc_info()))
        return
    pyside_path = os.path.dirname(QtCore.__file__)
    pyside_path = pyside_path.replace("\\", "/")
    pyside_path = pyside_path.replace("lib/site-packages", "Lib/site-packages")
    print("PySide package found in %s..." % pyside_path)

    # Since version 1.2.0 there is no need to run post install procedure on win32
    from PySide import __version_info__ as pyside_version_info
    if pyside_version_info >= (1,2,0):
        return

    if is_bdist_wininst:
        # Run from inside the bdist_wininst installer
        import distutils.sysconfig
        exec_prefix = distutils.sysconfig.get_config_var("exec_prefix")
    else:
        # Run manually
        exec_prefix = os.path.dirname(sys.executable)

    # Generate qt.conf
    qtconf_path = os.path.join(exec_prefix, "qt.conf")
    print("Generating file %s..." % qtconf_path)
    f = open(qtconf_path, 'wt')
    file_created(qtconf_path)
    f.write("""[Paths]
Prefix = %(pyside_prefix)s
Binaries = %(pyside_prefix)s
Plugins = %(pyside_prefix)s/plugins
Translations = %(pyside_prefix)s/translations
""" % { "pyside_prefix": pyside_path })
    print("The PySide extensions were successfully installed.")

    # Install OpenSSL libs
    for dll in ["libeay32.dll", "ssleay32.dll"]:
        dest_path = os.path.join(exec_prefix, dll)
        src_path = os.path.join(os.path.join(pyside_path, "openssl"), dll)
        if not os.path.exists(dest_path) and os.path.exists(src_path):
            shutil.copy(src_path, dest_path)
            file_created(dest_path)
            print("Installed %s to %s." % (dll, exec_prefix))

def uninstall():
    print("The PySide extensions were successfully uninstalled.")

def usage():
    msg = \
"""%s: A post-install script for the PySide extensions.
    
This should be run automatically after installation, but if it fails you
can run it again with a '-install' parameter, to ensure the environment
is setup correctly.
"""
    print(msg.strip() % os.path.basename(sys.argv[0]))

# NOTE: If this script is run from inside the bdist_wininst created
# binary installer or uninstaller, the command line args are either
# '-install' or '-remove'.

# Important: From inside the binary installer this script MUST NOT
# call sys.exit() or raise SystemExit, otherwise not only this script
# but also the installer will terminate! (Is there a way to prevent
# this from the bdist_wininst C code?)

if __name__ == '__main__':
    if len(sys.argv)==1:
        usage()
        sys.exit(1)

    arg_index = 1
    while arg_index < len(sys.argv):
        arg = sys.argv[arg_index]
        if arg == "-install":
            install()
        elif arg == "-remove":
            # bdist_msi calls us before uninstall, so we can undo what we
            # previously did.  Sadly, bdist_wininst calls us *after*, so
            # we can't do much at all.
            if not is_bdist_wininst:
                uninstall()
        else:
            print("Unknown option: %s" % arg)
            usage()
            sys.exit(0)
        arg_index += 1