Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
S
shallow-appify
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Model registry
Operate
Environments
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Terms and privacy
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Ingo Meyer
shallow-appify
Commits
a535014e
Commit
a535014e
authored
9 years ago
by
Ingo Meyer
Browse files
Options
Downloads
Plain Diff
Merge branch 'feature-appify' into develop
parents
a2ca4d28
7b43334c
No related branches found
Branches containing commit
No related tags found
Tags containing commit
No related merge requests found
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
src/shallow-appify.py
+346
-0
346 additions, 0 deletions
src/shallow-appify.py
with
346 additions
and
0 deletions
src/shallow-appify.py
0 → 100755
+
346
−
0
View file @
a535014e
#!/usr/bin/env python
# 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
'
__version_info__
=
(
0
,
0
,
0
)
__version__
=
'
.
'
.
join
(
map
(
str
,
__version_info__
))
import
argparse
import
os
import
os.path
import
re
import
shutil
import
subprocess
import
sys
import
tempfile
from
jinja2
import
Template
from
PIL
import
Image
import
logging
logging
.
basicConfig
(
level
=
logging
.
WARNING
)
INFO_PLIST_TEMPLATE
=
'''
<?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>
{% for key, value in environment.iteritems() -%}
<key>{{ key }}</key>
<string>{{ value }}</string>
{% endfor -%}
</dict>
{% 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>undefined.{{ 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
):
def
__init__
(
self
):
self
.
tmp_dir
=
tempfile
.
mkdtemp
()
def
__enter__
(
self
):
return
self
.
tmp_dir
def
__exit__
(
self
,
type
,
value
,
traceback
):
shutil
.
rmtree
(
self
.
tmp_dir
)
self
.
tmp_dir
=
None
class
Arguments
(
object
):
def
__init__
(
self
,
**
kwargs
):
super
(
Arguments
,
self
).
__setattr__
(
'
_members
'
,
{})
for
key
,
value
in
kwargs
.
iteritems
():
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
):
return
self
.
_members
.
keys
()
class
MissingIconError
(
Exception
):
pass
class
AppAlreadyExistingError
(
Exception
):
pass
def
parse_args
():
def
parse_commandline
():
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
'
,
'
--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.
'
)
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.
'
)
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
'
,
help
=
'
Specifies the version string of the program.
'
)
parser
.
add_argument
(
'
executable_path
'
,
action
=
'
store
'
,
type
=
os
.
path
.
abspath
,
help
=
'
Sets the executable that is started when the app is opened.
'
)
if
len
(
sys
.
argv
)
<
2
:
parser
.
print_help
()
sys
.
exit
(
1
)
args
=
parser
.
parse_args
()
return
args
def
map_environment_arguments_to_dict
(
enviroment_argument_list
):
if
enviroment_argument_list
is
not
None
:
keys_and_values
=
[
item
.
split
(
'
=
'
)
for
item
in
enviroment_argument_list
]
for
item
in
keys_and_values
:
if
len
(
item
)
<
2
:
item
.
append
(
os
.
environ
[
item
[
0
]])
result
=
dict
(
keys_and_values
)
else
:
result
=
None
return
result
args
=
parse_commandline
()
executable_root_path
=
args
.
executable_root_path
icon_path
=
args
.
icon_path
environment_vars
=
map_environment_arguments_to_dict
(
args
.
environment_vars
)
if
args
.
app_path
is
not
None
:
app_path
=
args
.
app_path
else
:
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
:
version_string
=
args
.
version_string
else
:
version_string
=
'
0.0.0
'
executable_path
=
os
.
path
.
abspath
(
args
.
executable_path
)
return
Arguments
(
executable_root_path
=
executable_root_path
,
icon_path
=
icon_path
,
environment_vars
=
environment_vars
,
app_path
=
app_path
,
version_string
=
version_string
,
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
get_short_version
(
version
):
match_obj
=
re
.
search
(
'
\d+(\.\d+){0,2}
'
,
version
)
if
match_obj
is
not
None
:
short_version
=
match_obj
.
group
()
while
not
re
.
match
(
'
\d+\.\d+\.\d+
'
,
short_version
):
short_version
+=
'
.0
'
else
:
short_version
=
'
0.0.0
'
return
short_version
if
executable_root_path
is
None
:
executable_root_path
=
os
.
path
.
dirname
(
executable_path
)
vars
=
{
'
executable
'
:
os
.
path
.
relpath
(
executable_path
,
executable_root_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_vars
is
not
None
:
environment_variables
=
dict
(((
key
,
os
.
environ
[
key
])
for
key
in
environment_vars
))
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.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
'
))):
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
):
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
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
:
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
executable_root_path
is
None
:
shutil
.
copy
(
executable_path
,
macos_path
)
else
:
os
.
rmdir
(
macos_path
)
shutil
.
copytree
(
executable_root_path
,
macos_path
)
def
set_file_permissions
():
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
'
)
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
executable_root_path
is
not
None
:
app_executable_path
=
os
.
path
.
relpath
(
executable_path
,
executable_root_path
)
else
:
app_executable_path
=
os
.
path
.
basename
(
executable_path
)
if
os
.
path
.
exists
(
abs_path
(
'
.
'
)):
raise
AppAlreadyExistingError
(
'
The app path {app_path} already exists.
'
.
format
(
app_path
=
app_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
:
try
:
create_icon_set
(
icon_path
,
bundle_icon_path
)
except
IOError
as
e
:
raise
MissingIconError
(
e
)
setup_result
=
StartupSetup
.
setup_startup
(
os
.
path
.
splitext
(
app_executable_path
)[
1
])
if
setup_result
is
not
NotImplemented
:
app_executable_path
=
setup_result
write_info_plist
()
write_pkg_info
()
set_file_permissions
()
def
main
():
args
=
parse_args
()
try
:
create_app
(
**
args
)
except
Exception
as
e
:
sys
.
stderr
.
write
(
'
Error: {message}
\n
'
.
format
(
message
=
e
))
if
__name__
==
'
__main__
'
:
main
()
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment