-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
macOS app: alternate approach using PyInstaller
- Loading branch information
1 parent
334fad7
commit c0574ee
Showing
6 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import os | ||
import IPython | ||
from snappy.app import main | ||
|
||
# make sure we have a home directory where IPython can scribble | ||
if os.name == 'nt' and 'HOME' not in os.environ: | ||
os.environ['HOME'] = os.environ['USERPROFILE'] | ||
|
||
def get_home_dir(): | ||
return os.environ['HOME'] | ||
|
||
def get_ipython_dir(): | ||
return os.path.join(os.environ['HOME'], '.ipython') | ||
|
||
IPython.utils.path.get_home_dir = get_home_dir | ||
IPython.utils.path.get_ipython_dir = get_ipython_dir() | ||
|
||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
# -*- mode: python -*- | ||
from PyInstaller.utils.hooks import collect_submodules, collect_data_files | ||
import sys | ||
|
||
block_cipher = None | ||
|
||
options = [('v', None, 'OPTION')] | ||
|
||
imports = collect_submodules('snappy') | ||
imports += collect_submodules('snappy_15_knots') | ||
imports += collect_submodules('cypari') | ||
imports += collect_submodules('jedi') | ||
imports += collect_submodules('pyx') | ||
imports += collect_submodules('low_index') | ||
imports += collect_submodules('tkinter_gl') | ||
|
||
|
||
datafiles = collect_data_files('jedi') | ||
datafiles += collect_data_files('pyx') | ||
datafiles += collect_data_files('parso') | ||
datafiles += collect_data_files('snappy_manifolds') | ||
datafiles += collect_data_files('snappy_15_knots') | ||
datafiles += collect_data_files('snappy') | ||
datafiles += collect_data_files('plink') | ||
datafiles += collect_data_files('spherogram') | ||
datafiles += collect_data_files('tkinter_gl') | ||
|
||
|
||
# SnapPyHP.pyd and twister_core.pyd are compiled with the MS Visual | ||
# C++ compiler from Visual Studio 2015, which dynamically links them | ||
# against the C++ runtime library msvcp140.dll. As of PyInstaller | ||
# 3.3, vcruntime140.dll (and msvcp100.dll) are listed in | ||
# PyInstaller.depends.dylib._includes but msvcp140.dll is not. To | ||
# work around this we add msvcp140.dll as a binary, specifying | ||
# that it should be at the top level of the bundle, adjacent to the | ||
# two .pyd files which depend on it. | ||
|
||
a = Analysis(['SnapPy.py'], | ||
hiddenimports=imports + ['linecache', 'pkg_resources.py2_warn', 'pkg_resources.extern'], | ||
datas=datafiles, | ||
hookspath=[], | ||
runtime_hooks=[], | ||
excludes=['gi', 'pytz', 'td', 'sphinx', 'alabaster', 'babel', | ||
'idlelib', 'bsddb'], | ||
win_no_prefer_redirects=False, | ||
win_private_assemblies=False, | ||
cipher=block_cipher) | ||
|
||
# As of PyInstaller 3.3, the system dlls api-ms-win-core*.dll and | ||
# api-ms-win-crt*.dll are explicitly included in the app bundle (see | ||
# pyinstaller.depend.dylib._includes). But these DLLs are not | ||
# compatible across different versions of Windows, so an app built on | ||
# Windows 10 will crash on Windows 7. Moreover, they do not need to | ||
# be included in the app bundle because they are always available from | ||
# the OS. Until this is fixed, the following hack is a workaround. | ||
|
||
# a.binaries = [b for b in a.binaries if b[1].find('api-ms-win') < 0] | ||
|
||
pyz = PYZ(a.pure, | ||
a.zipped_data, | ||
cipher=block_cipher) | ||
|
||
exe = EXE(pyz, | ||
a.scripts, | ||
exclude_binaries=True, | ||
name='SnapPy', | ||
debug=False, | ||
bootloader_ignore_signals=False, | ||
strip=False, | ||
upx=True, | ||
console=False, | ||
disable_windowed_traceback=False, | ||
target_arch=None, | ||
codesign_identity=None, | ||
entitlements_file=None, | ||
icon='../icons/SnapPy.icns') | ||
|
||
coll = COLLECT(exe, | ||
a.binaries, | ||
a.zipfiles, | ||
a.datas, | ||
strip=False, | ||
upx=True, | ||
upx_exclude=[], | ||
name='SnapPy') | ||
|
||
app = BUNDLE(coll, | ||
name='SnapPy.app', | ||
icon='../icons/SnapPy.icns', | ||
bundle_identifier=None) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
#! /usr/bin/env python | ||
""" | ||
Creates a nice disk image, with background and /Applications symlink | ||
for the app. | ||
One issue here is that Snow Leopard uses a different (undocumented, of | ||
course) format for the .DS_Store files than earlier versions, which makes | ||
disk images created on it not work correctly on those systems. Thus this "solution" uses a .DS_Store file created on Leopard as follows: | ||
(1) Use Disk Utility to create a r/w DMG large enough to store everything and open it. | ||
(2) Copy over the application and add a symlink to /Applications. | ||
(3) Create a subdirectory ".background" containing the file "background.png". | ||
(4) Open the disk image in the Finder and do View->Hide Tool Bar and then View->Show View Options. To add the background picture inside the hidden directory, use cmd-shift-g in the file dialog. Adjust everything to suit, close window and open it. Then copy the .DS_Store file to dotDS_store. | ||
""" | ||
import os | ||
import re | ||
from math import ceil | ||
|
||
name = "SnapPy3" | ||
dist_dir = "../dist" | ||
print('dmg name is %s' % name) | ||
|
||
|
||
def main(): | ||
# Make sure the dmg isn't currently mounted, or this won't work. | ||
mount_name = "/Volumes/" + name | ||
while os.path.exists(mount_name): | ||
print("Trying to eject " + mount_name) | ||
os.system("hdiutil detach " + mount_name) | ||
# Remove old dmg if there is one | ||
while os.path.exists(name + ".dmg"): | ||
os.remove(name + ".dmg") | ||
while os.path.exists(name + "-tmp.dmg"): | ||
os.remove(name + "-tmp.dmg") | ||
# Add symlink to /Applications if not there: | ||
if not os.path.exists(dist_dir + "/Applications"): | ||
os.symlink("/Applications/", dist_dir + "/Applications") | ||
|
||
# copy over the background and .DS_Store file | ||
os.system("rm -Rf " + dist_dir + "/.background") | ||
os.system("mkdir " + dist_dir + "/.background") | ||
os.system("cp background.png " + dist_dir + "/.background") | ||
os.system("cp dotDS_Store " + dist_dir + "/.DS_Store") | ||
|
||
# figure out the needed size: | ||
raw_size = os.popen("du -sh " + dist_dir).read() | ||
size, units = re.search("([0-9.]+)([KMG])", raw_size).groups() | ||
new_size = "%d" % ceil(1.2 * float(size)) + units | ||
# Run the main script: | ||
os.system("hdiutil makehybrid -hfs -hfs-volume-name SnapPy -hfs-openfolder %s %s -o SnapPy-tmp.dmg" % (dist_dir, dist_dir)) | ||
os.system("hdiutil convert -format UDZO SnapPy-tmp.dmg -o %s.dmg"%name) | ||
os.remove("SnapPy-tmp.dmg") | ||
# Delete symlink to /Applications or egg_info will be glacial on newer setuptools. | ||
os.remove(dist_dir + "/Applications") | ||
|
||
|
||
|
||
if __name__ == "__main__": | ||
main() | ||
|
||
|
||
|
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
#! /usr/bin/env python | ||
import os | ||
import sys | ||
import re | ||
import shutil | ||
from glob import glob | ||
import subprocess | ||
import configparser | ||
from subprocess import check_call, call, Popen, PIPE | ||
from math import ceil | ||
|
||
PYTHON_VERSION = '3.13' | ||
PYTHON_VERSION_SHORT = PYTHON_VERSION.replace('.', '') | ||
APP_PYTHON = 'python' + PYTHON_VERSION | ||
PYTHON_ZIP = 'python' + PYTHON_VERSION_SHORT + '.zip' | ||
FRAMEWORKS_TARBALL = 'Frameworks-' + PYTHON_VERSION + '.tgz' | ||
|
||
# Make sure that we have our frameworks. | ||
# if not os.path.exists(FRAMEWORKS_TARBALL): | ||
# print("Please build the frameworks for SnapPy.app first.") | ||
# sys.exit(1) | ||
|
||
os.environ['_PYTHON_HOST_PLATFORM'] = 'macosx-10.13-universal2' | ||
os.environ['ARCHFLAGS'] = '-arch arm64 -arch x86_64' | ||
|
||
try: | ||
import pyx | ||
except ImportError: | ||
print("ERROR: Need to install PyX!") | ||
sys.exit(1) | ||
|
||
def get_tk_ver(python): | ||
""" | ||
Figure out which version of Tk is used by this python. | ||
""" | ||
out, errors = Popen([python, "-c", "import _tkinter; print(_tkinter.TK_VERSION)"], | ||
stdout=PIPE, text=True).communicate() | ||
return out.strip() | ||
|
||
def freshen_SnapPy(python): | ||
""" | ||
Build SnapPy and install it twice to make sure the documentation is | ||
up to date. | ||
""" | ||
os.chdir("../") | ||
check_call(["git", "pull"]) | ||
check_call([python, "setup.py", "pip_install"]) | ||
os.chdir("macOS_app") | ||
|
||
def build_app(python): | ||
""" | ||
Build the standalone app bundle. | ||
""" | ||
check_call([python, '-m', 'PyInstaller', '--noconfirm', 'SnapPy_macOS.spec']) | ||
# Replace the frameworks that py2app installs with our own signed frameworks. | ||
#shutil.rmtree(os.path.join('dist', 'SnapPy.app', 'Contents', 'Frameworks')) | ||
#check_call(['tar', 'xfz', FRAMEWORKS_TARBALL]) | ||
#contents = os.path.join('dist', 'SnapPy.app', 'Contents') | ||
#resources = os.path.join(contents, 'Resources') | ||
#frameworks = os.path.join(contents, 'Frameworks') | ||
#os.rename('Frameworks', frameworks) | ||
## shutil.copy('Info.plist', contents) | ||
|
||
def cleanup_app(python): | ||
""" | ||
Tidy things up. | ||
""" | ||
extra_dynload = glob('dist/SnapPy.app/Contents/Resources/lib/python*/lib-dynload')[0] | ||
shutil.rmtree(extra_dynload) | ||
dev_directory = os.path.join('dist', 'SnapPy.app', 'Contents', 'Resources', 'lib', | ||
python, 'snappy', 'dev') | ||
shutil.rmtree(dev_directory, ignore_errors=True) | ||
resources = os.path.join('dist', 'SnapPy.app', 'Contents', 'Resources') | ||
# Remove Python.org junk that may or may not have been added by py2app. | ||
shutil.rmtree(os.path.join(resources, 'lib', 'tcl8.6'), ignore_errors=True) | ||
shutil.rmtree(os.path.join(resources, 'lib', 'tcl8'), ignore_errors=True) | ||
shutil.rmtree(os.path.join(resources, 'lib', 'tk8.6'), ignore_errors=True) | ||
|
||
def package_app(dmg_name): | ||
""" | ||
Create a disk image containing the app, with a nice background and | ||
a symlink to the Applications folder. | ||
""" | ||
image_dir = "disk_images" | ||
if not os.path.exists(image_dir): | ||
os.mkdir(image_dir) | ||
mount_name = "/Volumes/SnapPy" | ||
dmg_path = os.path.join(image_dir, dmg_name + ".dmg") | ||
temp_path = os.path.join(image_dir, dmg_name + "-tmp.dmg") | ||
# Make sure the dmg isn't currently mounted, or this won't work. | ||
while os.path.exists(mount_name): | ||
print("Trying to eject " + mount_name) | ||
os.system("hdiutil detach " + mount_name) | ||
# Remove old dmgs if they exist. | ||
if os.path.exists(dmg_path): | ||
os.remove(dmg_path) | ||
if os.path.exists(temp_path): | ||
os.remove(temp_path) | ||
# Add symlink to /Applications if not there. | ||
if not os.path.exists("dist/Applications"): | ||
os.symlink("/Applications/", "dist/Applications") | ||
|
||
# Copy over the background and .DS_Store file. | ||
check_call(["rm", "-rf", "dist/.background"]) | ||
os.mkdir("dist/.background") | ||
check_call(["cp", "dmg-maker/background.png", "dist/.background"]) | ||
check_call(["cp", "dmg-maker/dotDS_Store", "dist/.DS_Store"]) | ||
|
||
# Figure out the needed size. | ||
raw_size, errors = Popen(["du", "-sh", "dist"], stdout=PIPE).communicate() | ||
size, units = re.search("([0-9.]+)([KMG])", str(raw_size)).groups() | ||
new_size = "%d" % ceil(1.2 * float(size)) + units | ||
# Run hdiutil to build the dmg file.: | ||
check_call(["hdiutil", "makehybrid", "-hfs", "-hfs-volume-name", "SnapPy", | ||
"-hfs-openfolder", "dist", "dist", "-o", temp_path]) | ||
check_call(["hdiutil", "convert", "-format", "UDZO", temp_path, "-o", dmg_path]) | ||
os.remove(temp_path) | ||
# Delete the symlink to /Applications or egg_info will be glacial on newer setuptools. | ||
os.remove("dist/Applications") | ||
|
||
def sign_app(): | ||
if not os.path.exists('notabot.cfg'): | ||
print('The notabot.cfg file does not exist. The app cannot be signed.') | ||
return | ||
config = configparser.ConfigParser() | ||
config.read('notabot.cfg') | ||
identity = config['developer']['identity'] | ||
codesign = ['codesign', '-v', '-s', identity, '--timestamp', '--entitlements', 'entitlement.plist', | ||
'--options', 'runtime', '--force'] | ||
app = os.path.join('dist', 'SnapPy.app') | ||
contents = os.path.join(app, 'Contents') | ||
resources = os.path.join(contents, 'Resources') | ||
python_exe = os.path.join(contents, 'MacOS', 'python') | ||
|
||
def sign(path): | ||
subprocess.run(codesign + [path]) | ||
|
||
sign(python_exe) | ||
for dirpath, dirnames, filenames in os.walk(resources): | ||
for name in filenames: | ||
base, ext = os.path.splitext(name) | ||
if ext in ('.so', '.dylib'): | ||
print('Signing', os.path.join(dirpath, name)) | ||
sign(os.path.join(dirpath, name)) | ||
sign(app) | ||
|
||
def do_release(python, dmg_name, freshen=True): | ||
#if freshen: | ||
# freshen_SnapPy(python) | ||
build_app(python) | ||
#cleanup_app(python) | ||
#sign_app() | ||
package_app(dmg_name) | ||
|
||
|
||
if __name__ == '__main__': | ||
freshen = '--no-freshen' not in sys.argv | ||
do_release(APP_PYTHON, "SnapPy", freshen) |