From bea13c130e484111af0f2f49f3e4fe9bb825d03d Mon Sep 17 00:00:00 2001 From: ValdikSS Date: Mon, 8 May 2017 23:01:28 +0300 Subject: [PATCH] macOS pyinstaller spec support --- README.md | 4 +- blockcheck.spec | 26 ++- osx_hooks/hook-_tkinter.py | 247 +++++++++++++++++++++++++++ osx_hooks/loader/pyi_rth__tkinter.py | 34 ++++ 4 files changed, 305 insertions(+), 6 deletions(-) create mode 100644 osx_hooks/hook-_tkinter.py create mode 100644 osx_hooks/loader/pyi_rth__tkinter.py diff --git a/README.md b/README.md index 1e1c1b4..189f0d7 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ BlockCheck [http://habrahabr.ru/post/229377/](http://habrahabr.ru/post/229377/) — статистика по провайдерам (устарела) ### Запуск -Проще всего воспользоваться бинарными сборками под **Windows** и **Linux** из раздела [Releases](https://github.com/ValdikSS/blockcheck/releases). Они самодостаточны и не требуют установки. +Проще всего воспользоваться бинарными сборками под **Windows**, **Linux** и **macOS** из раздела [Releases](https://github.com/ValdikSS/blockcheck/releases). Они самодостаточны и не требуют установки. -Если у вас OS X, или вы хотите изменить исходный код, вам потребуется Python >= 3.4 с модулем dnspython. Установить его можно из репозитория вашего дистрибутива, или используя pip: +Если вы хотите изменить исходный код, вам потребуется Python >= 3.4 с модулем dnspython. Установить его можно из репозитория вашего дистрибутива, или используя pip: `python3 -m pip install -r requirements.txt` diff --git a/blockcheck.spec b/blockcheck.spec index b4576b3..c79c60b 100644 --- a/blockcheck.spec +++ b/blockcheck.spec @@ -4,9 +4,21 @@ from PyInstaller.utils.hooks import exec_statement sys_os = exec_statement(""" import sys print(sys.platform)""").strip() -ca_bundle = [] + +add_datas = [] +hooks_p = [] +hooks_r = [] + if sys_os == 'linux': ca_bundle = [('/etc/ssl/certs/ca-certificates.crt', 'lib')] + add_datas = ca_bundle + +if sys_os == 'darwin': + add_datas = [('/System/Library/Frameworks/Tk.framework/Tk', '.'), + ('/System/Library/Frameworks/Tcl.framework/Versions/8.5/Tcl', '.'), + ] + hooks_p = ['osx_hooks'] + hooks_r = ['osx_hooks/loader/pyi_rth__tkinter.py'] block_cipher = None @@ -14,10 +26,10 @@ block_cipher = None a = Analysis(['blockcheck.py'], pathex=[], binaries=[], - datas=ca_bundle, + datas=add_datas, hiddenimports=[], - hookspath=[], - runtime_hooks=[], + hookspath=hooks_p, + runtime_hooks=hooks_r, excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, @@ -52,3 +64,9 @@ exe = EXE(pyz, strip=False, upx=True, console=False ) + +if sys_os == 'darwin': + app = BUNDLE(exe, + name='blockcheck.app', + icon=None, + bundle_identifier=None) diff --git a/osx_hooks/hook-_tkinter.py b/osx_hooks/hook-_tkinter.py new file mode 100644 index 0000000..658393b --- /dev/null +++ b/osx_hooks/hook-_tkinter.py @@ -0,0 +1,247 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2013-2016, PyInstaller Development Team. +# +# Distributed under the terms of the GNU General Public License with exception +# for distributing bootloader. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- + + +import os +import sys + +from PyInstaller.compat import is_win, is_darwin, is_unix, is_venv, base_prefix +from PyInstaller.compat import modname_tkinter +from PyInstaller.depend.bindepend import selectImports, getImports +from PyInstaller.building.datastruct import Tree +from PyInstaller.utils.hooks import exec_statement, logger + + +def _handle_broken_tcl_tk(): + """ + When freezing from a Windows venv, overwrite the values of the standard + `${TCL_LIBRARY}`, `${TK_LIBRARY}`, and `${TIX_LIBRARY}` environment + variables. + + This is a workaround for broken Tcl/Tk detection in Windows virtual + environments. Older versions of `virtualenv` set such variables erroneously, + preventing PyInstaller from properly detecting Tcl/Tk. This issue has been + noted for `virtualenv` under Python 2.4 and Windows 7. + + See Also + ------- + https://github.com/pypa/virtualenv/issues/93 + """ + if is_win and is_venv: + basedir = os.path.join(base_prefix, 'tcl') + files = os.listdir(basedir) + + # Detect Tcl/Tk paths. + for f in files: + abs_path = os.path.join(basedir, f) + if f.startswith('tcl') and os.path.isdir(abs_path): + os.environ['TCL_LIBRARY'] = abs_path + elif f.startswith('tk') and os.path.isdir(abs_path): + os.environ['TK_LIBRARY'] = abs_path + elif f.startswith('tix') and os.path.isdir(abs_path): + os.environ['TIX_LIBRARY'] = abs_path + + +def _warn_if_activetcl_or_teapot_installed(tcl_root, tcltree): + """ + If the current Tcl installation is a Teapot-distributed version of ActiveTcl + *and* the current platform is OS X, log a non-fatal warning that the + resulting executable will (probably) fail to run on non-host systems. + + PyInstaller does *not* freeze all ActiveTcl dependencies -- including + Teapot, which is typically ignorable. Since Teapot is *not* ignorable in + this case, this function warns of impending failure. + + See Also + ------- + https://github.com/pyinstaller/pyinstaller/issues/621 + """ + from PyInstaller.lib.macholib import util + + # System libraries do not experience this problem. + if util.in_system_path(tcl_root): + return + + # Absolute path of the "init.tcl" script. + try: + init_resource = [r[1] for r in tcltree if r[1].endswith('init.tcl')][0] + # If such script could not be found, silently return. + except IndexError: + return + + mentions_activetcl = False + mentions_teapot = False + with open(init_resource, 'r') as init_file: + for line in init_file.readlines(): + line = line.strip().lower() + if line.startswith('#'): + continue + if 'activetcl' in line: + mentions_activetcl = True + if 'teapot' in line: + mentions_teapot = True + if mentions_activetcl and mentions_teapot: + break + + if mentions_activetcl and mentions_teapot: + logger.warning( + """ +You appear to be using an ActiveTcl build of Tcl/Tk, which PyInstaller has +difficulty freezing. To fix this, comment out all references to "teapot" in: + + %s + +See https://github.com/pyinstaller/pyinstaller/issues/621 for more information. + """ % init_resource) + + +def _find_tcl_tk_darwin_frameworks(binaries): + """ + Get an OS X-specific 2-tuple of the absolute paths of the top-level + external data directories for both Tcl and Tk, respectively. + + Under OS X, Tcl and Tk are installed as Frameworks requiring special care. + + Returns + ------- + list + 2-tuple whose first element is the value of `${TCL_LIBRARY}` and whose + second element is the value of `${TK_LIBRARY}`. + """ + tcl_root = tk_root = None + for nm, fnm in binaries: + if nm == 'Tcl': + tcl_root = os.path.join(os.path.dirname(fnm), 'Resources/Scripts') + elif nm == 'Tk': + tk_root = os.path.join(os.path.dirname(fnm), 'Resources/Scripts') + return tcl_root, tk_root + + +def _find_tcl_tk_dir(): + """ + Get a platform-agnostic 2-tuple of the absolute paths of the top-level + external data directories for both Tcl and Tk, respectively. + + Returns + ------- + list + 2-tuple whose first element is the value of `${TCL_LIBRARY}` and whose + second element is the value of `${TK_LIBRARY}`. + """ + # Python code to get path to TCL_LIBRARY. + tcl_root = exec_statement( + 'from %s import Tcl; print(Tcl().eval("info library"))' % modname_tkinter) + tk_version = exec_statement( + 'from _tkinter import TK_VERSION; print(TK_VERSION)') + + # TK_LIBRARY is in the same prefix as Tcl. + tk_root = os.path.join(os.path.dirname(tcl_root), 'tk%s' % tk_version) + return tcl_root, tk_root + + +def _find_tcl_tk(hook_api): + """ + Get a platform-specific 2-tuple of the absolute paths of the top-level + external data directories for both Tcl and Tk, respectively. + + Returns + ------- + list + 2-tuple whose first element is the value of `${TCL_LIBRARY}` and whose + second element is the value of `${TK_LIBRARY}`. + """ + bins = selectImports(hook_api.__file__) + + if is_darwin: + # _tkinter depends on system Tcl/Tk frameworks. + # For example this is the case of Python from homebrew. + if not bins: + # 'hook_api.binaries' can't be used because on Mac OS X _tkinter.so + # might depend on system Tcl/Tk frameworks and these are not + # included in 'hook_api.binaries'. + bins = getImports(hook_api.__file__) + # Reformat data structure from + # set(['lib1', 'lib2', 'lib3']) + # to + # [('Tcl', '/path/to/Tcl'), ('Tk', '/path/to/Tk')] + mapping = {} + for l in bins: + mapping[os.path.basename(l)] = l + bins = [ + ('Tcl', mapping['Tcl']), + ('Tk', mapping['Tk']), + ] + + # _tkinter depends on Tcl/Tk compiled as frameworks. + path_to_tcl = bins[0][1] + if 'Library/Frameworks' in path_to_tcl: + tcl_tk = _find_tcl_tk_darwin_frameworks(bins) + # Tcl/Tk compiled as on Linux other Unixes. + # For example this is the case of Tcl/Tk from macports. + else: + tcl_tk = _find_tcl_tk_dir() + + else: + tcl_tk = _find_tcl_tk_dir() + + return tcl_tk + + +def _collect_tcl_tk_files(hook_api): + """ + Get a list of TOC-style 3-tuples describing all external Tcl/Tk data files. + + Returns + ------- + Tree + Such list. + """ + # Workaround for broken Tcl/Tk detection in virtualenv on Windows. + _handle_broken_tcl_tk() + + tcl_root, tk_root = _find_tcl_tk(hook_api) + + # TODO Shouldn't these be fatal exceptions? + if not tcl_root: + logger.error('Tcl/Tk improperly installed on this system.') + return [] + if not os.path.isdir(tcl_root): + logger.error('Tcl data directory "%s" not found.', tcl_root) + return [] + if not os.path.isdir(tk_root): + logger.error('Tk data directory "%s" not found.', tk_root) + return [] + + tcltree = Tree( + tcl_root, prefix='tclResources', excludes=['demos', '*.lib', 'tclConfig.sh']) + tktree = Tree( + tk_root, prefix='tkResources', excludes=['demos', '*.lib', 'tkConfig.sh']) + + # If the current Tcl installation is a Teapot-distributed version of + # ActiveTcl and the current platform is OS X, warn that this is bad. + if is_darwin: + _warn_if_activetcl_or_teapot_installed(tcl_root, tcltree) + + return (tcltree + tktree) + + +def hook(hook_api): + # Use a hook-function to get the module's attr:`__file__` easily. + """ + Freeze all external Tcl/Tk data files if this is a supported platform *or* + log a non-fatal error otherwise. + """ + if is_win or is_darwin or is_unix: + # _collect_tcl_tk_files(hook_api) returns a Tree (which is okay), + # so we need to store it into `hook_api.datas` to prevent + # `building.imphook.format_binaries_and_datas` from crashing + # with "too many values to unpack". + hook_api.add_datas(_collect_tcl_tk_files(hook_api)) + else: + logger.error("... skipping Tcl/Tk handling on unsupported platform %s", sys.platform) diff --git a/osx_hooks/loader/pyi_rth__tkinter.py b/osx_hooks/loader/pyi_rth__tkinter.py new file mode 100644 index 0000000..40cfbf3 --- /dev/null +++ b/osx_hooks/loader/pyi_rth__tkinter.py @@ -0,0 +1,34 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2013-2016, PyInstaller Development Team. +# +# Distributed under the terms of the GNU General Public License with exception +# for distributing bootloader. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- + + +import os +import sys + + +try: + FileNotFoundError +except NameError: + # FileNotFoundError is new in Python 3.0 + # NB: Aliasing IOError is not a full emulation of FileNotFoundError, + # but far enough for this usecase, where the whole frozen program + # terminates when this exception occurs. + FileNotFoundError = IOError + +tcldir = os.path.join(sys._MEIPASS, 'tclResources') +tkdir = os.path.join(sys._MEIPASS, 'tkResources') + +if not os.path.isdir(tcldir): + raise FileNotFoundError('Tcl data directory "%s" not found.' % (tcldir)) +if not os.path.isdir(tkdir): + raise FileNotFoundError('Tk data directory "%s" not found.' % (tkdir)) + +# Notify "tkinter" of such directories. +os.environ["TCL_LIBRARY"] = tcldir +os.environ["TK_LIBRARY"] = tkdir