425 lines
16 KiB
Python
Executable file
425 lines
16 KiB
Python
Executable file
#!/usr/bin/env python2
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
finish the job started by macdeployqtfix
|
|
from: https://github.com/arl/macdeployqtfix
|
|
|
|
The MIT License (MIT)
|
|
|
|
Copyright (c) 2015 Aurelien Rainone
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
|
|
"""
|
|
|
|
from subprocess import Popen, PIPE
|
|
from string import Template
|
|
import os
|
|
import sys
|
|
import logging
|
|
import argparse
|
|
import re
|
|
from collections import namedtuple
|
|
|
|
|
|
QTLIB_NAME_REGEX = r'^(?:@executable_path)?/.*/(Qt[a-zA-Z]*).framework/(?:Versions/\d/)?\1$'
|
|
QTLIB_NORMALIZED = r'$prefix/Frameworks/$qtlib.framework/Versions/$qtversion/$qtlib'
|
|
|
|
QTPLUGIN_NAME_REGEX = r'^(?:@executable_path)?/.*/[pP]lug[iI]ns/(.*)/(.*).dylib$'
|
|
QTPLUGIN_NORMALIZED = r'$prefix/PlugIns/$plugintype/$pluginname.dylib'
|
|
|
|
LOADERPATH_REGEX = r'^@[a-z_]+path/(.*)'
|
|
LOADERPATH_NORMALIZED = r'$prefix/Frameworks/$loaderpathlib'
|
|
|
|
|
|
class GlobalConfig(object):
|
|
logger = None
|
|
qtpath = None
|
|
exepath = None
|
|
|
|
|
|
def run_and_get_output(popen_args):
|
|
"""Run process and get all output"""
|
|
process_output = namedtuple('ProcessOutput', ['stdout', 'stderr', 'retcode'])
|
|
try:
|
|
GlobalConfig.logger.debug('run_and_get_output({0})'.format(repr(popen_args)))
|
|
|
|
proc = Popen(popen_args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
|
stdout, stderr = proc.communicate(b'')
|
|
proc_out = process_output(stdout, stderr, proc.returncode)
|
|
|
|
GlobalConfig.logger.debug('\tprocess_output: {0}'.format(proc_out))
|
|
return proc_out
|
|
except Exception as exc:
|
|
GlobalConfig.logger.error('\texception: {0}'.format(exc))
|
|
return process_output('', exc.message, -1)
|
|
|
|
|
|
def get_dependencies(filename):
|
|
"""
|
|
input: filename must be an absolute path
|
|
Should call `otool` and returns the list of dependencies, unsorted,
|
|
unmodified, just the raw list so then we could eventually re-use in other
|
|
more specialized functions
|
|
"""
|
|
GlobalConfig.logger.debug('get_dependencies({0})'.format(filename))
|
|
popen_args = ['otool', '-L', filename]
|
|
proc_out = run_and_get_output(popen_args)
|
|
deps = []
|
|
if proc_out.retcode == 0:
|
|
# some string splitting
|
|
deps = [s.strip().split(b' ')[0].decode('utf-8') for s in proc_out.stdout.splitlines()[1:] if s]
|
|
# prevent infinite recursion when a binary depends on itself (seen with QtWidgets)...
|
|
deps = [s for s in deps if os.path.basename(filename) not in s]
|
|
return deps
|
|
|
|
|
|
def is_qt_plugin(filename):
|
|
"""
|
|
Checks if a given file is a qt plugin.
|
|
Accepts absolute path as well as path containing @executable_path
|
|
"""
|
|
qtlib_name_rgx = re.compile(QTPLUGIN_NAME_REGEX)
|
|
return qtlib_name_rgx.match(filename) is not None
|
|
|
|
|
|
def is_qt_lib(filename):
|
|
"""
|
|
Checks if a given file is a qt library.
|
|
Accepts absolute path as well as path containing @executable_path
|
|
"""
|
|
qtlib_name_rgx = re.compile(QTLIB_NAME_REGEX)
|
|
return qtlib_name_rgx.match(filename) is not None
|
|
|
|
|
|
def is_loader_path_lib(filename):
|
|
"""
|
|
Checks if a given file is loaded via @loader_path or @rpath
|
|
"""
|
|
qtlib_name_rgx = re.compile(LOADERPATH_REGEX)
|
|
return qtlib_name_rgx.match(filename) is not None
|
|
|
|
|
|
def normalize_qtplugin_name(filename):
|
|
"""
|
|
input: a path to a qt plugin, as returned by otool, that can have this form :
|
|
- an absolute path /../plugins/PLUGINTYPE/PLUGINNAME.dylib
|
|
- @executable_path/../plugins/PLUGINTYPE/PLUGINNAME.dylib
|
|
output:
|
|
a tuple (qtlib, abspath, rpath) where:
|
|
- qtname is the name of the plugin (libqcocoa.dylib, etc.)
|
|
- abspath is the absolute path of the qt lib inside the app bundle of exepath
|
|
- relpath is the correct rpath to a qt lib inside the app bundle
|
|
"""
|
|
|
|
GlobalConfig.logger.debug('normalize_plugin_name({0})'.format(filename))
|
|
|
|
qtplugin_name_rgx = re.compile(QTPLUGIN_NAME_REGEX)
|
|
rgxret = qtplugin_name_rgx.match(filename)
|
|
if not rgxret:
|
|
msg = 'couldn\'t normalize a non-qt plugin filename: {0}'.format(filename)
|
|
GlobalConfig.logger.critical(msg)
|
|
raise Exception(msg)
|
|
|
|
# qtplugin normalization settings
|
|
qtplugintype = rgxret.groups()[0]
|
|
qtpluginname = rgxret.groups()[1]
|
|
|
|
templ = Template(QTPLUGIN_NORMALIZED)
|
|
|
|
# from qtlib, forge 2 path :
|
|
# - absolute path of qt lib in bundle,
|
|
abspath = os.path.normpath(templ.safe_substitute(
|
|
prefix=os.path.dirname(GlobalConfig.exepath) + '/..',
|
|
plugintype=qtplugintype,
|
|
pluginname=qtpluginname))
|
|
|
|
# - and rpath containing @executable_path, relative to exepath
|
|
rpath = templ.safe_substitute(
|
|
prefix='@executable_path/..',
|
|
plugintype=qtplugintype,
|
|
pluginname=qtpluginname)
|
|
|
|
GlobalConfig.logger.debug('\treturns({0})'.format((qtpluginname, abspath, rpath)))
|
|
return qtpluginname, abspath, rpath
|
|
|
|
|
|
def normalize_qtlib_name(filename):
|
|
"""
|
|
input: a path to a qt library, as returned by otool, that can have this form :
|
|
- an absolute path /lib/xxx/yyy
|
|
- @executable_path/../Frameworks/QtSerialPort.framework/Versions/5/QtSerialPort
|
|
output:
|
|
a tuple (qtlib, abspath, rpath) where:
|
|
- qtlib is the name of the qtlib (QtCore, QtWidgets, etc.)
|
|
- abspath is the absolute path of the qt lib inside the app bundle of exepath
|
|
- relpath is the correct rpath to a qt lib inside the app bundle
|
|
"""
|
|
GlobalConfig.logger.debug('normalize_qtlib_name({0})'.format(filename))
|
|
|
|
qtlib_name_rgx = re.compile(QTLIB_NAME_REGEX)
|
|
rgxret = qtlib_name_rgx.match(filename)
|
|
if not rgxret:
|
|
msg = 'couldn\'t normalize a non-qt lib filename: {0}'.format(filename)
|
|
GlobalConfig.logger.critical(msg)
|
|
raise Exception(msg)
|
|
|
|
# qtlib normalization settings
|
|
qtlib = rgxret.groups()[0]
|
|
qtversion = 5
|
|
|
|
templ = Template(QTLIB_NORMALIZED)
|
|
|
|
# from qtlib, forge 2 path :
|
|
# - absolute path of qt lib in bundle,
|
|
abspath = os.path.normpath(templ.safe_substitute(
|
|
prefix=os.path.dirname(GlobalConfig.exepath) + '/..',
|
|
qtlib=qtlib,
|
|
qtversion=qtversion))
|
|
|
|
# - and rpath containing @executable_path, relative to exepath
|
|
rpath = templ.safe_substitute(
|
|
prefix='@executable_path/..',
|
|
qtlib=qtlib,
|
|
qtversion=qtversion)
|
|
|
|
GlobalConfig.logger.debug('\treturns({0})'.format((qtlib, abspath, rpath)))
|
|
return qtlib, abspath, rpath
|
|
|
|
|
|
def normalize_loaderpath_name(filename):
|
|
"""
|
|
input: a path to a loaderpath library, as returned by otool, that can have this form :
|
|
- an relative path @loaderpath/yyy
|
|
output:
|
|
a tuple (loaderpathlib, abspath, rpath) where:
|
|
- loaderpathlib is the name of the loaderpath lib
|
|
- abspath is the absolute path of the qt lib inside the app bundle of exepath
|
|
- relpath is the correct rpath to a qt lib inside the app bundle
|
|
"""
|
|
GlobalConfig.logger.debug('normalize_loaderpath_name({0})'.format(filename))
|
|
|
|
loaderpath_name_rgx = re.compile(LOADERPATH_REGEX)
|
|
rgxret = loaderpath_name_rgx.match(filename)
|
|
if not rgxret:
|
|
msg = 'couldn\'t normalize a loaderpath lib filename: {0}'.format(filename)
|
|
GlobalConfig.logger.critical(msg)
|
|
raise Exception(msg)
|
|
|
|
# loaderpath normalization settings
|
|
loaderpathlib = rgxret.groups()[0]
|
|
templ = Template(LOADERPATH_NORMALIZED)
|
|
|
|
# from loaderpath, forge 2 path :
|
|
# - absolute path of qt lib in bundle,
|
|
abspath = os.path.normpath(templ.safe_substitute(
|
|
prefix=os.path.dirname(GlobalConfig.exepath) + '/..',
|
|
loaderpathlib=loaderpathlib))
|
|
|
|
# - and rpath containing @executable_path, relative to exepath
|
|
rpath = templ.safe_substitute(
|
|
prefix='@executable_path/..',
|
|
loaderpathlib=loaderpathlib)
|
|
|
|
GlobalConfig.logger.debug('\treturns({0})'.format((loaderpathlib, abspath, rpath)))
|
|
return loaderpathlib, abspath, rpath
|
|
|
|
|
|
def fix_dependency(binary, dep):
|
|
"""
|
|
fix 'dep' dependency of 'binary'. 'dep' is a qt library
|
|
"""
|
|
if is_qt_lib(dep):
|
|
qtname, dep_abspath, dep_rpath = normalize_qtlib_name(dep)
|
|
qtnamesrc = os.path.join(GlobalConfig.qtpath, 'lib', '{0}.framework'.
|
|
format(qtname), qtname)
|
|
elif is_qt_plugin(dep):
|
|
qtname, dep_abspath, dep_rpath = normalize_qtplugin_name(dep)
|
|
qtnamesrc = os.path.join(GlobalConfig.qtpath, 'lib', '{0}.framework'.
|
|
format(qtname), qtname)
|
|
elif is_loader_path_lib(dep):
|
|
qtname, dep_abspath, dep_rpath = normalize_loaderpath_name(dep)
|
|
qtnamesrc = os.path.join(GlobalConfig.qtpath + '/lib', qtname)
|
|
else:
|
|
return True
|
|
|
|
# if the source path doesn't exist it's probably not a dependency
|
|
# originating with vcpkg and we should leave it alone
|
|
if not os.path.exists(qtnamesrc):
|
|
return True
|
|
|
|
dep_ok = True
|
|
# check that rpath of 'dep' inside binary has been correctly set
|
|
# (ie: relative to exepath using '@executable_path' syntax)
|
|
if dep != dep_rpath:
|
|
# dep rpath is not ok
|
|
GlobalConfig.logger.info('changing rpath \'{0}\' in binary {1}'.format(dep, binary))
|
|
|
|
# call install_name_tool -change on binary
|
|
popen_args = ['install_name_tool', '-change', dep, dep_rpath, binary]
|
|
proc_out = run_and_get_output(popen_args)
|
|
if proc_out.retcode != 0:
|
|
GlobalConfig.logger.error(proc_out.stderr)
|
|
dep_ok = False
|
|
else:
|
|
# call install_name_tool -id on binary
|
|
popen_args = ['install_name_tool', '-id', dep_rpath, binary]
|
|
proc_out = run_and_get_output(popen_args)
|
|
if proc_out.retcode != 0:
|
|
GlobalConfig.logger.error(proc_out.stderr)
|
|
dep_ok = False
|
|
|
|
# now ensure that 'dep' exists at the specified path, relative to bundle
|
|
if dep_ok and not os.path.exists(dep_abspath):
|
|
|
|
# ensure destination directory exists
|
|
GlobalConfig.logger.info('ensuring directory \'{0}\' exists: {0}'.
|
|
format(os.path.dirname(dep_abspath)))
|
|
popen_args = ['mkdir', '-p', os.path.dirname(dep_abspath)]
|
|
proc_out = run_and_get_output(popen_args)
|
|
if proc_out.retcode != 0:
|
|
GlobalConfig.logger.info(proc_out.stderr)
|
|
dep_ok = False
|
|
else:
|
|
# copy missing dependency into bundle
|
|
GlobalConfig.logger.info('copying missing dependency in bundle: {0}'.
|
|
format(qtname))
|
|
popen_args = ['cp', qtnamesrc, dep_abspath]
|
|
proc_out = run_and_get_output(popen_args)
|
|
if proc_out.retcode != 0:
|
|
GlobalConfig.logger.info(proc_out.stderr)
|
|
dep_ok = False
|
|
else:
|
|
# ensure permissions are correct if we ever have to change its rpath
|
|
GlobalConfig.logger.info('ensuring 755 perm to {0}'.format(dep_abspath))
|
|
popen_args = ['chmod', '755', dep_abspath]
|
|
proc_out = run_and_get_output(popen_args)
|
|
if proc_out.retcode != 0:
|
|
GlobalConfig.logger.info(proc_out.stderr)
|
|
dep_ok = False
|
|
else:
|
|
GlobalConfig.logger.debug('{0} is at correct location in bundle'.format(qtname))
|
|
|
|
if dep_ok:
|
|
return fix_binary(dep_abspath)
|
|
return False
|
|
|
|
|
|
def fix_binary(binary):
|
|
"""
|
|
input:
|
|
binary: relative or absolute path (no @executable_path syntax)
|
|
process:
|
|
- first fix the rpath for the qt libs on which 'binary' depend
|
|
- copy into the bundle of exepath the eventual libraries that are missing
|
|
- (create the soft links) needed ?
|
|
- do the same for all qt dependencies of binary (recursive)
|
|
"""
|
|
GlobalConfig.logger.debug('fix_binary({0})'.format(binary))
|
|
|
|
# loop on 'binary' dependencies
|
|
for dep in get_dependencies(binary):
|
|
if not fix_dependency(binary, dep):
|
|
GlobalConfig.logger.error('quitting early: couldn\'t fix dependency {0} of {1}'.format(dep, binary))
|
|
return False
|
|
return True
|
|
|
|
|
|
def fix_main_binaries():
|
|
"""
|
|
list the main binaries of the app bundle and fix them
|
|
"""
|
|
# deduce bundle path
|
|
bundlepath = os.path.sep.join(GlobalConfig.exepath.split(os.path.sep)[0:-3])
|
|
|
|
# fix main binary
|
|
GlobalConfig.logger.info('fixing executable \'{0}\''.format(GlobalConfig.exepath))
|
|
if fix_binary(GlobalConfig.exepath):
|
|
GlobalConfig.logger.info('fixing plugins')
|
|
for root, dummy, files in os.walk(bundlepath):
|
|
for name in [f for f in files if os.path.splitext(f)[1] == '.dylib']:
|
|
GlobalConfig.logger.info('fixing plugin {0}'.format(name))
|
|
if not fix_binary(os.path.join(root, name)):
|
|
return False
|
|
return True
|
|
|
|
|
|
def main():
|
|
descr = """finish the job started by macdeployqt!
|
|
- find dependencies/rpaths with otool
|
|
- copy missed dependencies with cp and mkdir
|
|
- fix missed rpaths with install_name_tool
|
|
|
|
exit codes:
|
|
- 0 : success
|
|
- 1 : error
|
|
"""
|
|
|
|
parser = argparse.ArgumentParser(description=descr,
|
|
formatter_class=argparse.RawTextHelpFormatter)
|
|
parser.add_argument('exepath',
|
|
help='path to the binary depending on Qt')
|
|
parser.add_argument('qtpath',
|
|
help='path of Qt libraries used to build the Qt application')
|
|
parser.add_argument('-q', '--quiet', action='store_true', default=False,
|
|
help='do not create log on standard output')
|
|
parser.add_argument('-nl', '--no-log-file', action='store_true', default=False,
|
|
help='do not create log file \'./macdeployqtfix.log\'')
|
|
parser.add_argument('-v', '--verbose', action='store_true', default=False,
|
|
help='produce more log messages(debug log)')
|
|
args = parser.parse_args()
|
|
|
|
# globals
|
|
GlobalConfig.qtpath = os.path.normpath(args.qtpath)
|
|
GlobalConfig.exepath = args.exepath
|
|
GlobalConfig.logger = logging.getLogger()
|
|
|
|
# configure logging
|
|
###################
|
|
|
|
# create formatter
|
|
formatter = logging.Formatter('%(levelname)s | %(message)s')
|
|
# create console GlobalConfig.logger
|
|
if not args.quiet:
|
|
chdlr = logging.StreamHandler(sys.stdout)
|
|
chdlr.setFormatter(formatter)
|
|
GlobalConfig.logger.addHandler(chdlr)
|
|
|
|
# create file GlobalConfig.logger
|
|
if not args.no_log_file:
|
|
fhdlr = logging.FileHandler('./macdeployqtfix.log', mode='w')
|
|
fhdlr.setFormatter(formatter)
|
|
GlobalConfig.logger.addHandler(fhdlr)
|
|
|
|
if args.no_log_file and args.quiet:
|
|
GlobalConfig.logger.addHandler(logging.NullHandler())
|
|
else:
|
|
GlobalConfig.logger.setLevel(logging.DEBUG if args.verbose else logging.INFO)
|
|
|
|
if fix_main_binaries():
|
|
GlobalConfig.logger.info('macdeployqtfix terminated with success')
|
|
ret = 0
|
|
else:
|
|
GlobalConfig.logger.error('macdeployqtfix terminated with error')
|
|
ret = 1
|
|
sys.exit(ret)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|