Skip to content

Commit

Permalink
macOS app: alternate approach using PyInstaller
Browse files Browse the repository at this point in the history
  • Loading branch information
NathanDunfield committed Nov 30, 2024
1 parent 334fad7 commit c0574ee
Showing 6 changed files with 332 additions and 0 deletions.
18 changes: 18 additions & 0 deletions macOS_app/alt_approach/SnapPy.py
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()
90 changes: 90 additions & 0 deletions macOS_app/alt_approach/SnapPy_macOS.spec
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)
Binary file added macOS_app/alt_approach/dmg-maker/background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions macOS_app/alt_approach/dmg-maker/dmg-maker.py
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 added macOS_app/alt_approach/dmg-maker/dotDS_Store
Binary file not shown.
158 changes: 158 additions & 0 deletions macOS_app/alt_approach/release_alt.py
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)

0 comments on commit c0574ee

Please sign in to comment.