Skip to content
Snippets Groups Projects
Commit 9862d676 authored by Ingo Meyer's avatar Ingo Meyer
Browse files

Merge branch 'feature-miniconda_support' into develop

parents 548f0546 e0491539
No related branches found
No related tags found
No related merge requests found
Pipeline #
......@@ -5,13 +5,14 @@ from __future__ import unicode_literals
from __future__ import division
from __future__ import absolute_import
import importlib
import pkgutil
__author__ = 'Ingo Heimbach'
__email__ = 'i.heimbach@fz-juelich.de'
import importlib
import pkgutil
_modules = None
_ext2module = None
......@@ -23,6 +24,7 @@ def _normalize_ext(f):
return f(file_ext, *args, **kwargs)
return g
def _check_ext_availability(f):
@_normalize_ext
def g(file_ext, *args, **kwargs):
......@@ -35,6 +37,7 @@ def _check_ext_availability(f):
raise NotInitializedError
return g
class NotInitializedError(Exception):
pass
......@@ -50,14 +53,17 @@ def add_plugin_command_line_arguments(parser):
name_or_flags = [name_or_flags]
parser.add_argument(*name_or_flags, **kwargs)
@_check_ext_availability
def parse_command_line_arguments(file_ext, arguments):
return _ext2module[file_ext].parse_command_line_arguments(arguments)
@_check_ext_availability
def pre_create_app(file_ext, **arguments):
return _ext2module[file_ext].pre_create_app(**arguments)
@_normalize_ext
def setup_startup(file_ext, app_path, executable_path, app_executable_path,
executable_root_path, macos_path, resources_path):
......@@ -72,10 +78,12 @@ def setup_startup(file_ext, app_path, executable_path, app_executable_path,
else:
raise NotInitializedError
@_check_ext_availability
def post_create_app(file_ext, **arguments):
return _ext2module[file_ext].post_create_app(**arguments)
def _pkg_init():
global _modules, _ext2module
......
......@@ -5,9 +5,6 @@ from __future__ import unicode_literals
from __future__ import division
from __future__ import absolute_import
__author__ = 'Ingo Heimbach'
__email__ = 'i.heimbach@fz-juelich.de'
import fnmatch
import itertools
import os
......@@ -15,10 +12,13 @@ import re
import shutil
import subprocess
from jinja2 import Template
from .util import libpatch
from .util import command
__author__ = 'Ingo Heimbach'
__email__ = 'i.heimbach@fz-juelich.de'
PY_PRE_STARTUP_CONDA_SETUP = '''
#!/bin/bash
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
......@@ -74,46 +74,45 @@ _CONDA_DEFAULT_PACKAGES = ('pyobjc-framework-cocoa', )
_CONDA_DEFAULT_CHANNELS = ('https://conda.binstar.org/erik', )
_EXT_PYLIB_VARIABLE = 'PYLIBPATH'
_EXT_MAKEFILE_TARGET = 'app_extension_modules'
_GR_LIB_COPY_DICT = {'/opt/X11/lib': 'lib/X11'}
_GR_LIB_DIR_PATHS_TO_PATCH = ('lib/X11', 'lib/python2.7/site-packages/gr', 'lib/python2.7/site-packages/gr3')
_GR_OLD_TO_NEW_DEPENDENCY_DICT = {'/opt/X11/': '@executable_path/../lib/X11/',
'/usr/local/qt-4.8/lib/': '@executable_path/../lib/'}
# TODO: add support for more libraries, for example wxWidgets
_create_conda_env = False
_requirements_file = None
_conda_channels = None
_extension_makefile = None
_conda_gr_included = False
class CondaError(Exception):
pass
class LibPatchingError(Exception):
pass
class ExtensionModuleError(Exception):
pass
def get_command_line_arguments():
arguments = [(('--conda', ), {'dest': 'conda_req_file', 'action': 'store', 'type': os.path.abspath,
'help': 'Creates a miniconda environment from the given conda requirements file and includes it in the app bundle. Can be used to create self-contained python apps.'}),
'help': 'Creates a miniconda environment from the given conda requirements file '
'and includes it in the app bundle. Can be used to create self-contained '
'python apps.'}),
(('--conda-channels', ), {'dest': 'conda_channels', 'action': 'store', 'nargs': '+',
'help': 'A list of custom conda channels to install packages that are not included in the main anaconda distribution.'}),
'help': 'A list of custom conda channels to install packages that are not '
'included in the main anaconda distribution.'}),
(('--extension-makefile', ), {'dest': 'extension_makefile', 'action': 'store', 'type': os.path.abspath,
'help': 'Path to a makefile for building python extension modules. The makefile is called with the target "{target}" and a variable "{libvariable}" that holds the path to the conda python library.'.format(target=_EXT_MAKEFILE_TARGET, libvariable=_EXT_PYLIB_VARIABLE)})]
'help': 'Path to a makefile for building python extension modules. The '
'makefile is called with the target "{target}" and a variable '
'"{libvariable}" that holds the path to the conda python '
'library.'.format(target=_EXT_MAKEFILE_TARGET,
libvariable=_EXT_PYLIB_VARIABLE)})]
return arguments
def parse_command_line_arguments(args):
global _create_conda_env, _requirements_file, _conda_channels, _extension_makefile, _conda_gr_included
def is_gr_in_conda_requirements(requirements_file):
with open(requirements_file, 'r') as f:
found_gr = any((line.startswith('gr=') for line in f))
return found_gr
def parse_command_line_arguments(args):
global _create_conda_env, _requirements_file, _conda_channels, _extension_makefile
checked_args = {}
if args.conda_req_file is not None:
......@@ -124,12 +123,13 @@ def parse_command_line_arguments(args):
_conda_channels = args.conda_channels
if args.extension_makefile is not None:
_extension_makefile = args.extension_makefile
_conda_gr_included = is_gr_in_conda_requirements(_requirements_file)
return checked_args
def pre_create_app(**kwargs):
pass
def setup_startup(app_path, executable_path, app_executable_path, executable_root_path, macos_path, resources_path):
def create_python_startup_script(main_module):
template = Template(PY_STARTUP_SCRIPT)
......@@ -144,7 +144,8 @@ def setup_startup(app_path, executable_path, app_executable_path, executable_roo
python_lib_pathes = tuple(['{lib_dir_path}/{path}'.format(lib_dir_path=lib_dir_path, path=path)
for path in os.listdir(lib_dir_path) if fnmatch.fnmatch(path, lib_pattern)])
for python_lib_path in python_lib_pathes:
rel_python_lib_path = '@executable_path/{rel_path}'.format(rel_path=os.path.relpath(python_lib_path, python_dir_path))
rel_python_lib_path = '@executable_path/{rel_path}'.format(rel_path=os.path.relpath(python_lib_path,
python_dir_path))
with open(os.devnull, 'w') as dummy:
try:
subprocess.check_call(['install_name_tool', '-id', rel_python_lib_path, python_lib_path],
......@@ -159,13 +160,14 @@ def setup_startup(app_path, executable_path, app_executable_path, executable_roo
env_path = '{resources}/{env}'.format(resources=resources_path, env='conda_env')
try:
subprocess.check_call(['conda', 'create', '-p', env_path,
'--file', _requirements_file, '--copy', '--quiet', '--yes']
+ list(itertools.chain(*[('-c', channel) for channel in conda_channels])),
'--file', _requirements_file, '--copy', '--quiet', '--yes'] +
list(itertools.chain(*[('-c', channel) for channel in conda_channels])),
stdout=dummy, stderr=dummy)
subprocess.check_call(' '.join(['source', '{env_path}/bin/activate'.format(env_path=env_path), env_path, ';',
'conda', 'install', '--copy', '--quiet', '--yes']
+ list(_CONDA_DEFAULT_PACKAGES)
+ list(itertools.chain(*[('-c', channel) for channel in _CONDA_DEFAULT_CHANNELS]))),
subprocess.check_call(' '.join(['source', '{env_path}/bin/activate'.format(env_path=env_path),
env_path, ';', 'conda', 'install', '--copy', '--quiet', '--yes'] +
list(_CONDA_DEFAULT_PACKAGES) +
list(itertools.chain(*[('-c', channel)
for channel in _CONDA_DEFAULT_CHANNELS]))),
stdout=dummy, stderr=dummy, shell=True)
except subprocess.CalledProcessError:
raise CondaError('The conda environment could not be installed.')
......@@ -184,8 +186,10 @@ def setup_startup(app_path, executable_path, app_executable_path, executable_roo
for root_path, dirnames, filenames in os.walk(env_path):
dirpaths = [os.path.join(root_path, dirname) for dirname in dirnames]
filepaths = [os.path.join(root_path, filename) for filename in filenames]
link_dirpaths = [dirpath for dirpath in dirpaths if os.path.islink(dirpath) and not os.path.realpath(dirpath).startswith(env_path)]
link_filepaths = [filepath for filepath in filepaths if os.path.islink(filepath) and not os.path.realpath(filepath).startswith(env_path)]
link_dirpaths = [dirpath for dirpath in dirpaths if os.path.islink(dirpath) and
not os.path.realpath(dirpath).startswith(env_path)]
link_filepaths = [filepath for filepath in filepaths if os.path.islink(filepath) and
not os.path.realpath(filepath).startswith(env_path)]
for link_dirpath in link_dirpaths:
real_dirpath = os.path.realpath(link_dirpath)
os.remove(link_dirpath)
......@@ -198,7 +202,8 @@ def setup_startup(app_path, executable_path, app_executable_path, executable_roo
def fix_activate_script():
SEARCHED_LINE_START = '_THIS_DIR='
INSERT_LINE = 'export PATH=${_THIS_DIR}:${PATH}'
full_conda_activate_path = '{env_path}/{conda_activate_path}'.format(env_path=env_path, conda_activate_path=CONDA_ACTIVATE_PATH)
full_conda_activate_path = '{env_path}/{conda_activate_path}'.format(env_path=env_path,
conda_activate_path=CONDA_ACTIVATE_PATH)
found_line = False
new_lines = []
with open(full_conda_activate_path, 'r') as f:
......@@ -250,18 +255,6 @@ def setup_startup(app_path, executable_path, app_executable_path, executable_roo
fix_conda_shebang()
copy_missing_conda_packages()
def fix_conda_gr(env_path):
def copy_missing_dependencies():
for src, dst in _GR_LIB_COPY_DICT.iteritems():
shutil.copytree(src, '{env_path}/{relative_dst}'.format(env_path=env_path, relative_dst=dst))
def patch_lib_dependencies():
lib_dir_paths = tuple(('{env_path}/{relative_lib_path}'.format(env_path=env_path, relative_lib_path=lib_path) for lib_path in _GR_LIB_DIR_PATHS_TO_PATCH))
libpatch.patch_libs(lib_dir_paths, _GR_OLD_TO_NEW_DEPENDENCY_DICT)
copy_missing_dependencies()
patch_lib_dependencies()
def build_extension_modules(env_path):
def get_makefile_path():
if executable_root_path is not None and \
......@@ -292,8 +285,6 @@ def setup_startup(app_path, executable_path, app_executable_path, executable_roo
if _create_conda_env:
env_path = create_conda_env()
make_conda_portable(env_path)
if _conda_gr_included:
fix_conda_gr(env_path)
if _extension_makefile is not None:
build_extension_modules(env_path)
env_startup_script = PY_PRE_STARTUP_CONDA_SETUP
......@@ -305,5 +296,6 @@ def setup_startup(app_path, executable_path, app_executable_path, executable_roo
return new_executable_path
def post_create_app(**kwargs):
pass
......@@ -5,10 +5,12 @@ from __future__ import unicode_literals
from __future__ import division
from __future__ import absolute_import
import subprocess
__author__ = 'Ingo Heimbach'
__email__ = 'i.heimbach@fz-juelich.de'
import subprocess
def exec_cmd(*cmd):
cmd = ' '.join(cmd)
......@@ -16,5 +18,6 @@ def exec_cmd(*cmd):
stdout, stderr = p.communicate()
return stdout
def which(cmd):
return exec_cmd('which', cmd).strip()
......@@ -5,9 +5,6 @@ from __future__ import unicode_literals
from __future__ import division
from __future__ import absolute_import
__author__ = 'Ingo Heimbach'
__email__ = 'i.heimbach@fz-juelich.de'
import logging
import os
import os.path
......@@ -15,6 +12,10 @@ import re
from .command import exec_cmd
__author__ = 'Ingo Heimbach'
__email__ = 'i.heimbach@fz-juelich.de'
def extract_dependencies(lib_path, dependency_path_prefix):
dependency_output = exec_cmd('otool -L', lib_path)
dependencies = []
......@@ -25,6 +26,7 @@ def extract_dependencies(lib_path, dependency_path_prefix):
dependencies.append(current_dependency)
return dependencies
def replace_install_name(lib_path, new_install_name_prefix):
lib_name = os.path.basename(lib_path)
new_install_name = '{new_install_name_prefix}{lib_name}'.format(new_install_name_prefix=new_install_name_prefix,
......@@ -32,6 +34,7 @@ def replace_install_name(lib_path, new_install_name_prefix):
logging.debug('set new install name {name}'.format(name=new_install_name))
exec_cmd('install_name_tool -id', new_install_name, lib_path)
def replace_dependency(lib_path, old_dependency, new_dependency_prefix):
old_dependency_lib_name = os.path.basename(old_dependency)
new_dependency = '{new_dependency_prefix}{lib_name}'.format(new_dependency_prefix=new_dependency_prefix,
......@@ -39,17 +42,20 @@ def replace_dependency(lib_path, old_dependency, new_dependency_prefix):
logging.debug('replace dependency {old_dep} with {new_dep}'.format(old_dep=old_dependency, new_dep=new_dependency))
exec_cmd('install_name_tool -change', old_dependency, new_dependency, lib_path)
def patch_lib(lib_path, old_dependency_prefix, new_dependency_prefix):
logging.debug('patching library {lib}'.format(lib=lib_path))
lib_name = os.path.basename(lib_path)
# lib_name = os.path.basename(lib_path)
lib_dependencies = extract_dependencies(lib_path, old_dependency_prefix)
# replace_install_name(lib_path, new_dependency_prefix) # it is not necessary to change the install name
for dependency in lib_dependencies:
replace_dependency(lib_path, dependency, new_dependency_prefix)
def list_libs_from_directory(dir_path):
return tuple((os.path.join(dir_path, lib) for lib in os.listdir(dir_path) if re.match('.+\.((dylib)|(so))', lib)))
def patch_libs(lib_dir_paths, old_to_new_dependency_prefix_dict):
for lib_dir_path in lib_dir_paths:
logging.debug('current library directory: {lib_dir_path}'.format(lib_dir_path=lib_dir_path))
......
......@@ -6,15 +6,10 @@ from __future__ import unicode_literals
from __future__ import division
from __future__ import absolute_import
__author__ = 'Ingo Heimbach'
__email__ = 'i.heimbach@fz-juelich.de'
__version_info__ = (0, 1, 1)
__version__ = '.'.join(map(str, __version_info__))
import argparse
import os
import os.path
import plugins
import re
import shutil
import subprocess
......@@ -25,7 +20,12 @@ from PIL import Image
import logging
logging.basicConfig(level=logging.WARNING)
import plugins
__author__ = 'Ingo Heimbach'
__email__ = 'i.heimbach@fz-juelich.de'
__version_info__ = (0, 1, 1)
__version__ = '.'.join(map(str, __version_info__))
INFO_PLIST_TEMPLATE = '''
......@@ -107,9 +107,11 @@ class Arguments(object):
class MissingIconError(Exception):
pass
class AppAlreadyExistingError(Exception):
pass
class InvalidAppPath(Exception):
pass
......@@ -119,12 +121,15 @@ def parse_args():
parser = argparse.ArgumentParser(description='''
Creates a runnable application for Mac OS X with references to
system libraries. The result is a NON-self-contained app bundle.''')
parser.add_argument('-d', '--executable-directory', dest='executable_root_path', action='store', type=os.path.abspath,
parser.add_argument('-d', '--executable-directory', dest='executable_root_path', action='store',
type=os.path.abspath,
help='Defines the executable root directory that will be included in the app.')
parser.add_argument('-i', '--icon', dest='icon_path', action='store', type=os.path.abspath,
help='Image file that is used for app icon creation. It must be quadratic with a resolution of 1024x1024 pixels or more.')
help='Image file that is used for app icon creation. It must be quadratic with a '
'resolution of 1024x1024 pixels or more.')
parser.add_argument('-e', '--environment', dest='environment_vars', action='store', nargs='+',
help='Specifies which environment variables -- set on the current interpreter startup -- shall be included in the app bundle.')
help='Specifies which environment variables -- set on the current interpreter startup -- '
' shall be included in the app bundle.')
parser.add_argument('-o', '--output', dest='app_path', action='store', type=os.path.abspath,
help='Sets the path the app will be saved to.')
parser.add_argument('-v', '--version', dest='version_string', action='store',
......@@ -157,7 +162,8 @@ def parse_args():
if args.app_path is not None:
checked_args['app_path'] = args.app_path
else:
checked_args['app_path'] = '{basename_without_ext}.app'.format(basename_without_ext=os.path.splitext(os.path.basename(os.path.abspath(args.executable_path)))[0])
basename_without_ext = os.path.splitext(os.path.basename(os.path.abspath(args.executable_path)))[0]
checked_args['app_path'] = '{basename_without_ext}.app'.format(basename_without_ext=basename_without_ext)
if args.version_string is not None:
checked_args['version_string'] = args.version_string
else:
......@@ -170,7 +176,9 @@ def parse_args():
args.update(plugin_args)
return Arguments(**args)
def create_info_plist_content(app_name, version, executable_path, executable_root_path=None, icon_path=None, environment_vars=None):
def create_info_plist_content(app_name, version, executable_path, executable_root_path=None, icon_path=None,
environment_vars=None):
def get_short_version(version):
match_obj = re.search('\d+(\.\d+){0,2}', version)
if match_obj is not None:
......@@ -204,19 +212,22 @@ def create_info_plist_content(app_name, version, executable_path, executable_roo
return info_plist
def create_icon_set(icon_path, iconset_out_path):
with TemporaryDirectory() as tmp_dir:
tmp_icns_dir = '{tmp_dir}/icon.iconset'.format(tmp_dir=tmp_dir)
os.mkdir(tmp_icns_dir)
original_icon = Image.open(icon_path)
for name, size in (('icon_{size}x{size}{suffix}.png'.format(size=size, suffix=suffix), factor*size)
for size in (16, 32, 128, 256, 512)
for factor, suffix in ((1, ''), (2, '@2x'))):
for size in (16, 32, 128, 256, 512)
for factor, suffix in ((1, ''), (2, '@2x'))):
resized_icon = original_icon.resize((size, size), Image.ANTIALIAS)
resized_icon.save('{icns_dir}/{icon_name}'.format(icns_dir=tmp_icns_dir, icon_name=name))
subprocess.call(('iconutil', '--convert', 'icns', tmp_icns_dir, '--output', iconset_out_path))
def create_app(app_path, version_string, executable_path, executable_root_path=None, icon_path=None, environment_vars=None, **kwargs):
def create_app(app_path, version_string, executable_path, executable_root_path=None, icon_path=None,
environment_vars=None, **kwargs):
def abs_path(relative_bundle_path, base=None):
return os.path.abspath('{app_path}/{dir}'.format(app_path=base or app_path, dir=relative_bundle_path))
......@@ -227,13 +238,13 @@ def create_app(app_path, version_string, executable_path, executable_root_path=N
raise InvalidAppPath('The specified app path is a subpath of the source root directory.')
def write_info_plist():
info_plist_content = create_info_plist_content(app_name, version_string, app_executable_path, executable_root_path,
bundle_icon_path, environment_vars)
with open(abs_path('Info.plist', contents_path) , 'w') as f:
info_plist_content = create_info_plist_content(app_name, version_string, app_executable_path,
executable_root_path, bundle_icon_path, environment_vars)
with open(abs_path('Info.plist', contents_path), 'w') as f:
f.writelines(info_plist_content.encode('utf-8'))
def write_pkg_info():
with open(abs_path('PkgInfo', contents_path) , 'w') as f:
with open(abs_path('PkgInfo', contents_path), 'w') as f:
f.write(PKG_INFO_CONTENT)
def copy_source():
......@@ -273,6 +284,7 @@ def create_app(app_path, version_string, executable_path, executable_root_path=N
write_pkg_info()
set_file_permissions()
def main():
args = parse_args()
try:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment