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

Added support for miniconda environments

Support for c extensions (linking against conda libpython*.dylib) is
still missing
parent a7dbda7c
No related branches found
No related tags found
No related merge requests found
# coding: utf-8
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division
from __future__ import absolute_import
__author__ = 'Ingo Heimbach'
__email__ = 'i.heimbach@fz-juelich.de'
import pkgutil
import re
_modules = None
_ext2module = None
def _normalize_ext(f):
def g(file_ext, *args, **kwargs):
if file_ext.startswith('.'):
file_ext = file_ext[1:]
return f(file_ext, *args, **kwargs)
return g
def _check_ext_availability(f):
@_normalize_ext
def g(file_ext, *args, **kwargs):
if _ext2module is not None:
if file_ext in _ext2module:
return f(file_ext, *args, **kwargs)
else:
return NotImplemented
else:
raise NotInitializedError
return g
class NotInitializedError(Exception):
pass
def add_plugin_command_line_arguments(parser):
for module in _modules.values():
arguments = module.get_command_line_arguments()
for name_or_flags, kwargs in arguments:
if 'help' in kwargs:
kwargs['help'] = '({plugin_name} only) {help}'.format(plugin_name=module._plugin_name_,
help=kwargs['help'])
if not isinstance(name_or_flags, (tuple, list)):
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):
global _ext2startup_func
if _ext2module is not None:
if file_ext in _ext2module:
return _ext2module[file_ext].setup_startup(app_path, executable_path, app_executable_path,
executable_root_path, macos_path, resources_path)
else:
return NotImplemented
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
_modules = {}
_ext2module = {}
for importer, module_name, is_package in pkgutil.iter_modules(__path__):
current_module = importer.find_module(module_name).load_module(module_name)
_modules[module_name] = current_module
_ext2module[current_module._file_ext_] = current_module
_pkg_init()
# coding: utf-8
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division
from __future__ import absolute_import
__author__ = 'Ingo Heimbach'
__email__ = 'i.heimbach@fz-juelich.de'
import itertools
import os
import subprocess
from jinja2 import Template
PY_PRE_STARTUP_CONDA_SETUP = '''
#!/bin/bash
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
cd ${SCRIPT_DIR}
source ../Resources/conda_env/bin/activate ../Resources/conda_env
python __startup__.py
'''.strip()
PY_STARTUP_SCRIPT = '''
#!/usr/bin/env python
# coding: utf-8
from __future__ import unicode_literals
import os
import os.path
from xml.etree import ElementTree as ET
from Foundation import NSBundle
def fix_current_working_directory():
os.chdir(os.path.dirname(os.path.abspath(__file__)))
def set_cf_keys():
bundle = NSBundle.mainBundle()
bundle_info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
info_plist = ET.parse('../Info.plist')
root = info_plist.getroot()
plist_dict = root.find('dict')
current_key = None
for child in plist_dict:
if child.tag == 'key' and child.text.startswith('CF'): # CoreFoundation key
current_key = child.text
elif current_key is not None:
bundle_info[current_key] = child.text
current_key = None
def main():
fix_current_working_directory()
set_cf_keys()
import {{ main_module }}
{{ main_module }}.main() # a main function is required
if __name__ == '__main__':
main()
'''.strip()
_plugin_name_ = 'Python'
_file_ext_ = 'py'
_PY_STARTUP_SCRIPT_NAME = '__startup__.py'
_ENV_STARTUP_SCRIPT_NAME = '__startup__.sh'
_CONDA_DEFAULT_PACKAGES = ('pyobjc-framework-cocoa', )
_CONDA_DEFAULT_CHANNELS = ('https://conda.binstar.org/erik', )
_create_conda_env = False
_requirements_file = None
_conda_channels = None
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.'}),
(('--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.'})]
return arguments
def parse_command_line_arguments(args):
global _create_conda_env, _requirements_file, _conda_channels
checked_args = {}
if args.conda_req_file is not None:
checked_args['python_conda'] = args.conda_req_file
_requirements_file = args.conda_req_file
_create_conda_env = True
if args.conda_channels is not None:
_conda_channels = args.conda_channels
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)
startup_script = template.render(main_module=main_module)
return startup_script
def create_conda_env():
conda_channels = _conda_channels or []
with open(os.devnull, 'w') as dummy:
env_path = '{resources}/{env}'.format(resources=resources_path, env='conda_env')
subprocess.call(['conda', 'create', '-p', env_path,
'--file', _requirements_file, '--copy', '--quiet', '--yes']
+ list(itertools.chain(*[('-c', channel) for channel in conda_channels])),
stdout=dummy, stderr=dummy)
subprocess.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)
main_module = os.path.splitext(app_executable_path)[0].replace('/', '.')
python_startup_script = create_python_startup_script(main_module)
with open('{macos}/{startup}'.format(macos=macos_path, startup=_PY_STARTUP_SCRIPT_NAME), 'w') as f:
f.writelines(python_startup_script.encode('utf-8'))
if _create_conda_env:
create_conda_env()
env_startup_script = PY_PRE_STARTUP_CONDA_SETUP
with open('{macos}/{startup}'.format(macos=macos_path, startup=_ENV_STARTUP_SCRIPT_NAME), 'w') as f:
f.writelines(env_startup_script.encode('utf-8'))
new_executable_path = _ENV_STARTUP_SCRIPT_NAME
else:
new_executable_path = _PY_STARTUP_SCRIPT_NAME
return new_executable_path
def post_create_app(**kwargs):
pass
...@@ -25,6 +25,8 @@ from PIL import Image ...@@ -25,6 +25,8 @@ from PIL import Image
import logging import logging
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
import plugins
INFO_PLIST_TEMPLATE = ''' INFO_PLIST_TEMPLATE = '''
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
...@@ -70,45 +72,6 @@ INFO_PLIST_TEMPLATE = ''' ...@@ -70,45 +72,6 @@ INFO_PLIST_TEMPLATE = '''
PKG_INFO_CONTENT = 'APPL????' PKG_INFO_CONTENT = 'APPL????'
STARTUP_SKRIPT = '''
#!/usr/bin/env python
# coding: utf-8
from __future__ import unicode_literals
import os
import os.path
from xml.etree import ElementTree as ET
from Foundation import NSBundle
def fix_current_working_directory():
os.chdir(os.path.dirname(os.path.abspath(__file__)))
def set_cf_keys():
bundle = NSBundle.mainBundle()
bundle_info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
info_plist = ET.parse('../Info.plist')
root = info_plist.getroot()
plist_dict = root.find('dict')
current_key = None
for child in plist_dict:
if child.tag == 'key' and child.text.startswith('CF'): # CoreFoundation key
current_key = child.text
elif current_key is not None:
bundle_info[current_key] = child.text
current_key = None
def main():
fix_current_working_directory()
set_cf_keys()
import {{ main_module }}
{{ main_module }}.main() # a main function is required
if __name__ == '__main__':
main()
'''.strip()
class TemporaryDirectory(object): class TemporaryDirectory(object):
def __init__(self): def __init__(self):
...@@ -168,6 +131,7 @@ def parse_args(): ...@@ -168,6 +131,7 @@ def parse_args():
help='Specifies the version string of the program.') help='Specifies the version string of the program.')
parser.add_argument('executable_path', action='store', type=os.path.abspath, parser.add_argument('executable_path', action='store', type=os.path.abspath,
help='Sets the executable that is started when the app is opened.') help='Sets the executable that is started when the app is opened.')
plugins.add_plugin_command_line_arguments(parser)
if len(sys.argv) < 2: if len(sys.argv) < 2:
parser.print_help() parser.print_help()
sys.exit(1) sys.exit(1)
...@@ -186,25 +150,25 @@ def parse_args(): ...@@ -186,25 +150,25 @@ def parse_args():
return result return result
args = parse_commandline() args = parse_commandline()
executable_root_path = args.executable_root_path checked_args = {}
icon_path = args.icon_path checked_args['executable_root_path'] = args.executable_root_path
environment_vars = map_environment_arguments_to_dict(args.environment_vars) checked_args['icon_path'] = args.icon_path
checked_args['environment_vars'] = map_environment_arguments_to_dict(args.environment_vars)
if args.app_path is not None: if args.app_path is not None:
app_path = args.app_path checked_args['app_path'] = args.app_path
else: else:
app_path = '{basename_without_ext}.app'.format(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=os.path.splitext(os.path.basename(os.path.abspath(args.executable_path)))[0])
if args.version_string is not None: if args.version_string is not None:
version_string = args.version_string checked_args['version_string'] = args.version_string
else: else:
version_string = '0.0.0' checked_args['version_string'] = '0.0.0'
executable_path = args.executable_path checked_args['executable_path'] = args.executable_path
return Arguments(executable_root_path=executable_root_path, plugin_args = plugins.parse_command_line_arguments(os.path.splitext(checked_args['executable_path'])[1], args)
icon_path=icon_path,
environment_vars=environment_vars, args = checked_args.copy()
app_path=app_path, args.update(plugin_args)
version_string=version_string, return Arguments(**args)
executable_path=executable_path)
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): def get_short_version(version):
...@@ -240,12 +204,6 @@ def create_info_plist_content(app_name, version, executable_path, executable_roo ...@@ -240,12 +204,6 @@ def create_info_plist_content(app_name, version, executable_path, executable_roo
return info_plist return info_plist
def create_python_startup_script(main_module_name):
template = Template(STARTUP_SKRIPT)
startup_script = template.render(main_module=main_module_name)
return startup_script
def create_icon_set(icon_path, iconset_out_path): def create_icon_set(icon_path, iconset_out_path):
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
tmp_icns_dir = '{tmp_dir}/icon.iconset'.format(tmp_dir=tmp_dir) tmp_icns_dir = '{tmp_dir}/icon.iconset'.format(tmp_dir=tmp_dir)
...@@ -258,14 +216,14 @@ def create_icon_set(icon_path, iconset_out_path): ...@@ -258,14 +216,14 @@ def create_icon_set(icon_path, iconset_out_path):
resized_icon.save('{icns_dir}/{icon_name}'.format(icns_dir=tmp_icns_dir, icon_name=name)) 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)) 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): 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): def abs_path(relative_bundle_path, base=None):
return os.path.abspath('{app_path}/{dir}'.format(app_path=app_path if base is None else base, dir=relative_bundle_path)) return os.path.abspath('{app_path}/{dir}'.format(app_path=base or app_path, dir=relative_bundle_path))
def error_checks(): def error_checks():
if os.path.exists(abs_path('.')): if os.path.exists(abs_path('.')):
raise AppAlreadyExistingError('The app path {app_path} already exists.'.format(app_path=app_path)) raise AppAlreadyExistingError('The app path {app_path} already exists.'.format(app_path=app_path))
if abs_path('.').startswith(os.path.abspath(executable_root_path)): if executable_root_path is not None and abs_path('.').startswith(os.path.abspath(executable_root_path)+'/'):
raise InvalidAppPath('The specified app path is a subpath of the source root directory.') raise InvalidAppPath('The specified app path is a subpath of the source root directory.')
def write_info_plist(): def write_info_plist():
...@@ -288,40 +246,6 @@ def create_app(app_path, version_string, executable_path, executable_root_path=N ...@@ -288,40 +246,6 @@ def create_app(app_path, version_string, executable_path, executable_root_path=N
def set_file_permissions(): def set_file_permissions():
os.chmod(abs_path(app_executable_path, macos_path), 0555) os.chmod(abs_path(app_executable_path, macos_path), 0555)
class StartupSetup(object):
'''
Class that contains functions to handle the startup of specific program types, e.g. python scripts.
Extend with more methods to support further languages if necessary. To do so, add a static method
named '_<program-file-extension>_startup' and create an own startup script. The variable
'app_executable_path' contains the relative path in the app bundle to the original executable file.
The method must return the path to newly created startup script.
See '_py_startup' as an example.
'''
@staticmethod
def _py_startup():
main_module = os.path.splitext(app_executable_path)[0].replace('/', '.')
python_startup_script = create_python_startup_script(main_module)
new_executable_path = '___startup___.py'
with open(abs_path(new_executable_path, macos_path), 'w') as f:
f.writelines(python_startup_script.encode('utf-8'))
return new_executable_path
@classmethod
def setup_startup(cls, file_ext):
if file_ext.startswith('.'):
file_ext = file_ext[1:]
if not hasattr(cls, '_ext2func'):
cls._ext2func = {}
for key, value in cls.__dict__.iteritems():
match = re.search('_[a-z]+_startup', key)
if match:
cls._ext2func[match.group()[1:-len('_startup')]] = value
if file_ext in cls._ext2func:
return cls._ext2func[file_ext].__func__()
else:
return NotImplemented
directory_structure = ('Contents', 'Contents/MacOS', 'Contents/Resources') directory_structure = ('Contents', 'Contents/MacOS', 'Contents/Resources')
contents_path, macos_path, resources_path = (abs_path(dir) for dir in directory_structure) contents_path, macos_path, resources_path = (abs_path(dir) for dir in directory_structure)
bundle_icon_path = abs_path('Icon.icns', resources_path) if icon_path is not None else None bundle_icon_path = abs_path('Icon.icns', resources_path) if icon_path is not None else None
...@@ -341,7 +265,8 @@ def create_app(app_path, version_string, executable_path, executable_root_path=N ...@@ -341,7 +265,8 @@ def create_app(app_path, version_string, executable_path, executable_root_path=N
create_icon_set(icon_path, bundle_icon_path) create_icon_set(icon_path, bundle_icon_path)
except IOError as e: except IOError as e:
raise MissingIconError(e) raise MissingIconError(e)
setup_result = StartupSetup.setup_startup(os.path.splitext(app_executable_path)[1]) setup_result = plugins.setup_startup(os.path.splitext(executable_path)[1], app_path, executable_path,
app_executable_path, executable_root_path, macos_path, resources_path)
if setup_result is not NotImplemented: if setup_result is not NotImplemented:
app_executable_path = setup_result app_executable_path = setup_result
write_info_plist() write_info_plist()
...@@ -351,7 +276,9 @@ def create_app(app_path, version_string, executable_path, executable_root_path=N ...@@ -351,7 +276,9 @@ def create_app(app_path, version_string, executable_path, executable_root_path=N
def main(): def main():
args = parse_args() args = parse_args()
try: try:
plugins.pre_create_app(os.path.splitext(args.executable_path)[1], **args)
create_app(**args) create_app(**args)
plugins.post_create_app(os.path.splitext(args.executable_path)[1], **args)
except Exception as e: except Exception as e:
sys.stderr.write('Error: {message}\n'.format(message=e)) sys.stderr.write('Error: {message}\n'.format(message=e))
......
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