426 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			426 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()
 |