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

First running version with a workaround for python scripts (python

overrides plist parameters) and successful icon creation
Other command line arguments are untested yet
parent 784d2070
No related branches found
No related tags found
No related merge requests found
......@@ -26,38 +26,87 @@ logging.basicConfig(level=logging.WARNING)
INFO_PLIST_TEMPLATE = '''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
{% if environment -%}
<key>LSEnvironment</key>
<dict>
{% if environ %}
<key>LSEnvironment</key>
<dict>
{% for key, value in environ.iteritems() %}
<key>{{ key }}</key>
<string>{{ value }}</string>
{% endfor %}
</dict>
{% endif %}
<key>CFBundleDevelopmentRegion</key>
<string>English</value>
<key>CFBundleExecutable</key>
<string>{{ executable }}</string>
<key>CFBundleIconFile</key>
<string>{{ icon_file }}</string>
<key>CFBundleIdentifier</key>
<string>de.fz-juelich.{{ name }}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{{ name }}</string>
<key>CFBundleShortVersionString</key>
<string>{{ short_version }}</string>
<key>CFBundleVersion</key>
<string>{{ version }}</string>
{% for key, value in environment.iteritems() -%}
<key>{{ key }}</key>
<string>{{ value }}</string>
{% endfor -%}
</dict>
</plist>
'''
{% endif -%}
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>{{ executable }}</string>
{% if icon_file -%}
<key>CFBundleIconFile</key>
<string>{{ icon_file }}</string>
{% endif -%}
<key>CFBundleIdentifier</key>
<string>de.fz-juelich.{{ name }}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{{ name }}</string>
<key>CFBundleDisplayName</key>
<string>{{ name }}</string>
<key>CFBundleShortVersionString</key>
<string>{{ short_version }}</string>
<key>CFBundleVersion</key>
<string>{{ version }}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
</dict>
</plist>
'''.strip()
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):
......@@ -74,15 +123,21 @@ class TemporaryDirectory(object):
class Arguments(object):
def __init__(self, **kwargs):
super(Arguments, self).__setattr__('_members', {})
for key, value in kwargs.iteritems():
setattr(self, key, property(value))
self._members[key] = value
def __getattr__(self, attr):
return self._members[attr]
def __setattr__(self, key, value):
raise NotImplementedError
def __getitem__(self, item):
return getattr(self, item)
def keys(self):
keys = [key for key in self.__dict__ if isinstance(key, property)]
return keys
return self._members.keys()
def parse_args():
......@@ -90,7 +145,7 @@ def parse_args():
parser = argparse.ArgumentParser(description='''
Creates a runnable application for Mac OS X with references to
system libraries. Therefore, the built app will NOT be self-contained.''')
parser.add_argument('-d', '--source-directory', dest='source_directory_path', action='store', type=os.path.abspath,
parser.add_argument('-d', '--source-directory', dest='source_root_path', action='store', type=os.path.abspath,
help='Defines the source 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.')
......@@ -100,26 +155,26 @@ def parse_args():
help='Sets the path the app will be saved to.')
parser.add_argument('-v', '--version', dest='version_string', action='store',
help='Specifies the version string of the program.')
parser.add_argument('executable_path', action='store', type=os.path.abspath, required=True,
parser.add_argument('executable_path', action='store', type=os.path.abspath,
help='Sets the executable that is started when the app is opened.')
args = parser.parse_args()
return args
args = parse_commandline()
source_directory_path = args.source_directory_path
source_root_path = args.source_root_path
icon_path = args.icon_path
environment_keys = args.environment_keys
if args.app_path is not None:
app_path = args.app_path
else:
app_path = '{path_without_ext}.app'.format(os.path.splitext(os.path.abspath(args.executable_path)))
app_path = '{path_without_ext}.app'.format(path_without_ext=os.path.splitext(os.path.abspath(args.executable_path))[0])
if args.version_string is not None:
version_string = args.version_string
else:
version_string = '0.0.0'
executable_path = os.path.abspath(args.executable_path)
return Arguments(source_directory_path=source_directory_path,
return Arguments(source_root_path=source_root_path,
icon_path=icon_path,
environment_keys=environment_keys,
app_path=app_path,
......@@ -137,26 +192,36 @@ def create_info_plist_content(app_name, version, executable_path, source_root_pa
short_version = '0.0.0'
return short_version
if source_root_path is None:
source_root_path = os.path.dirname(executable_path)
vars = {'executable': os.path.relpath(executable_path, source_root_path),
'icon_file': os.path.basename(icon_path),
'icon_file': os.path.basename(icon_path) if icon_path is not None else None,
'name': app_name,
'short_version': get_short_version(version),
'version': version}
if environment_keys is not None:
environment_variables = dict(((key, os.environ[key]) for key in environment_keys))
vars.update(environment_variables)
vars['environment'] = environment_variables
template = Template(INFO_PLIST_TEMPLATE)
info_plist = template.render(**vars)
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):
with TemporaryDirectory as tmp_dir:
tmp_icns_dir = '{tmp_dir}/icon.icns'.format(tmp_dir=tmp_dir)
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, suffix), factor*size)
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'))):
resized_icon = original_icon.resize((size, size), Image.ANTIALIAS)
......@@ -167,24 +232,53 @@ def create_app(app_path, version_string, executable_path, source_root_path=None,
def abs_path(relative_bundle_path, base=None):
return '{app_path}/{dir}'.format(app_path=app_path if base is None else base, dir=relative_bundle_path)
def setup_python_startup():
main_module = os.path.splitext(app_executable_path)[0].replace('/', '.')
python_startup_script = create_python_startup_script(main_module)
new_executable_path = abs_path('___startup___.py', macos_path)
with open(new_executable_path, 'w') as f:
f.writelines(python_startup_script.encode('utf-8'))
return new_executable_path
def write_info_plist():
info_plist_content = create_info_plist_content(app_name, version_string, app_executable_path, source_root_path,
bundle_icon_path, environment_keys)
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:
f.write(PKG_INFO_CONTENT)
def copy_source():
if source_root_path is None:
shutil.copy(executable_path, macos_path)
else:
os.rmdir(macos_path)
shutil.copytree(source_root_path, macos_path)
def set_file_permissions():
os.chmod(app_executable_path, 0555)
directory_structure = ('Contents', 'Contents/MacOS', 'Contents/Resources')
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
app_name = os.path.splitext(os.path.basename(app_path))[0]
if source_root_path is not None:
app_executable_path = os.path.relpath(executable_path, source_root_path)
else:
app_executable_path = os.path.basename(executable_path)
for current_path in (abs_path(dir) for dir in directory_structure):
os.makedirs(current_path)
copy_source()
if icon_path is not None:
create_icon_set(icon_path, bundle_icon_path)
info_plist_content = create_info_plist_content(app_name, version_string, executable_path, source_root_path,
bundle_icon_path, environment_keys)
with open(abs_path('Icon.icns', contents_path) , 'w') as f:
f.writelines(info_plist_content)
if source_root_path is None:
shutil.copy(executable_path, macos_path)
else:
os.rmdir(macos_path)
shutil.copytree(source_root_path, macos_path)
if app_executable_path.endswith('.py'):
app_executable_path = setup_python_startup()
write_info_plist()
write_pkg_info()
set_file_permissions()
def main():
args = parse_args()
......
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