Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

macOS build configuration for pyinstaller with associated helper assets #111

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ source = src
[report]
exclude_lines =
if __name__ == .__main__.:
if sys.platform == "win32":
if sys.platform != "win32":
if sys.platform != "darwin":
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ wheels/
.installed.cfg
*.egg
MANIFEST
*.dmg

# OS litter
.DS_Store
Desktop.ini
._*
Thumbs.db
.Trashes

# PyInstaller
# Usually these files are written by a python script from a template
Expand Down
9 changes: 7 additions & 2 deletions cq_editor/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
NAME = 'CadQuery GUI (PyQT)'

#need to initialize QApp here, otherewise svg icons do not work on windows
app = QApplication(sys.argv,
applicationName=NAME)
if sys.platform == "win32":
app = QApplication(sys.argv, applicationName=NAME)

from .main_window import MainWindow

def main():
# if this is not a windows platform, initialize QApp here.
# This also silences the warning from qt about initializing
# pyqtgraph after QApplication
if sys.platform != "win32":
app = QApplication(sys.argv, applicationName=NAME)

win = MainWindow()

Expand Down
7 changes: 5 additions & 2 deletions cq_editor/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ def __init__(self,parent=None):

self.prepare_statusbar()
self.prepare_actions()

self.components['object_tree'].addLines()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The application (without bundling) did work on Mac (according to some users). So I'd rather not remove this functionality but rather fix the graphics initialization process

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I leave this statement, the resulting app binary is guaranteed to crash (verified in macOS 10.14 and 10.15). We can either defer this pull request until a root cause is found or leave the 'if platform' guard in place until a root cause is found for future releases. Unless there is some urgency to make a new release including macOS, we can always defer the PR and wait until the root cause / remedy to this race condition crash at startup.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually if you are willing to help, I have some ideas regarding debugging this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I did spend a few hours yesterday trying to get more clues. One issue is the timing when “InitDriver” is called relative to MainWindow.show(). The rest of the issue is related to abi trap from AIS interactive viewer. I think it tries to throw an exception and it doesn’t get handled properly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adam-urbanczyk let me know if you want to proceed with some macOS debugging of this issue--I would like to help if I can. BTW, good luck with the FOSDEM talk--I just checked out your slide deck!

# on macOS adding the axis lines this early causes a crash
# since OpenGL does not get initialized in time.
if sys.platform != "darwin":
self.components['object_tree'].addLines()

self.prepare_console()

Expand Down
Binary file added icons/cadquery_logo_dark.icns
Binary file not shown.
Binary file added macos/CQDiskImageIcon.icns
Binary file not shown.
Binary file added macos/CQDiskImageIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added macos/CQInstallerBackground.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 84 additions & 0 deletions macos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
## macOS CQ-Editor Build Files

This directory contains build scripts and resources to build a standalone macOS application bundle of the CQ-Editor.

## Requirements

- XCode - Apple Developer IDE, compilers, and build utilities. Available from the macOS App Store.
- [brew](https://brew.sh) - a popular macOS package manager
- conda - virtual python environments with packaging. The [miniconda](https://docs.conda.io/en/latest/miniconda.html) variant of conda is recommended for a minimal installation without unnecessary libraries and packages.

## conda environment

The `macos_env.yml` file specifies a conda environment which can be used to build and/or run the CQ-Editor. This command will create a new conda environment named `cqgui` containing the necessary tools and libraries:

```shell
$ conda env create -f macos_env.yml -n cqgui
$ conda activate cqgui
```

## pyinstaller

The `pyinstaller.spec` file contains the build specification to build all platform variants of the CQ-Editor. The macOS specific section of this file is as follows:

```python
app = BUNDLE(
coll,
name="CQ-Editor.app",
icon="icons/cadquery_logo_dark.icns",
bundle_identifier="org.cadquery.cqeditor",
info_plist={
"CFBundleName": "CQ-Editor",
"CFBundleShortVersionString": "0.1.0",
"NSHighResolutionCapable": True,
"NSPrincipalClass": "NSApplication",
"NSAppleScriptEnabled": False,
"LSBackgroundOnly": False,
},
)
```

The `CFBundleShortVersionString` key in the `info_plist` dictionary can be changed to the desired version number of the build.

## Building the Application Bundle

To build the application bundle using pyinstaller, a convenient build script is contained in this folder and can be executed as follows:

```shell
$ ./makeapp.sh
```

Alternatively, pyinstaller can be run directly from the repository root directory as follows:

```shell
$ pyinstaller --onedir --windowed --clean -y pyinstaller.spec
```

The resulting application bundle `CQ-Editor.app` will be found in the `dist` directory. Verify that it works by either launching the `CQ-Editor.app` file in the Finder or double-clicking the standalone executable `dist/CQ-Editor/CQ-Editor`.

## Building a DMG Installer

To distribute an application bundle, a disk image file (.DMG) is typically used as a convenient single file container format. A DMG file also allows the application bundle to be compressed for efficiency. A DMG file can be created from the application bundle using the build script in this folder:

```shell
$ ./makedmg.sh
```

The `makedmg.sh` script file has a variable called `version` which can be changed to match the `CFBundleShortVersionString` key in the `pyinstaller.spec` file.

This script requires the following helper components:

- [dmgbuild](https://github.com/al45tair/dmgbuild/blob/master/doc/index.rst) : python utility which creates `dmg` files with a great deal of customization (install using pip)
- [fileicon](https://github.com/mklement0/fileicon) : a small utility which can assign custom icons to macOS files and/or folders (install using brew)

## Resource Files

| Resource | File | Description |
| --- | --- | --- |
| ![alt text](../icons/cadquery_logo_dark.svg) | `icons/cadquery_logo_dark.icns` | `CQ-Editor.app` application icon |
| ![alt text](./CQDiskImageIcon.png) | `CQDiskImageIcon.png` | DMG file icon |
| ![alt text](./CQInstallerBackground.png) | `./CQInstallerBackground.png` | DMG folder background image |




6 changes: 6 additions & 0 deletions macos/brew.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Make sure we’re using the latest Homebrew.
brew update
# Upgrade any already-installed formulae.
brew upgrade

brew install fileicon
83 changes: 83 additions & 0 deletions macos/cq_dmg_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import biplist
import os.path

#
# Example settings file for dmgbuild
#
# Use like this: dmgbuild -s settings.py "Test Volume" test.dmg

# You can actually use this file for your own application (not just TextEdit)
# by doing e.g.
#
# dmgbuild -s settings.py -D app=/path/to/My.app "My Application" MyApp.dmg

# .. Useful stuff ..............................................................

application = defines.get("app", "../dist/CQ-Editor.app")
appname = os.path.basename(application)

# Volume format (see hdiutil create -help)
format = defines.get("format", "UDBZ")

# Volume size
size = defines.get("size", "1.0g")

# Files to include
files = [application]

# Symlinks to create
symlinks = {"Applications": "/Applications"}

# Volume icon
#
# You can either define icon, in which case that icon file will be copied to the
# image, *or* you can define badge_icon, in which case the icon file you specify
# will be used to badge the system's Removable Disk icon
#
badge_icon = "../icons/cadquery_logo_dark.icns"

# Where to put the icons
icon_locations = {appname: (130, 190), "Applications": (470, 185)}

# .. Window configuration ......................................................

background = "CQInstallerBackground.png"
show_status_bar = False
show_tab_view = False
show_toolbar = False
show_pathbar = False
show_sidebar = False
sidebar_width = 180

# Window position in ((x, y), (w, h)) format
window_rect = ((200, 120), (600, 400))

# Select the default view; must be one of
#
# 'icon-view'
# 'list-view'
# 'column-view'
# 'coverflow'
#
default_view = "icon-view"

# General view configuration
show_icon_preview = False

# Set these to True to force inclusion of icon/list view settings (otherwise
# we only include settings for the default view)
include_icon_view_settings = "auto"
include_list_view_settings = "auto"

# .. Icon view configuration ...................................................

arrange_by = None
grid_offset = (0, 0)
grid_spacing = 100
scroll_position = (0, 0)
label_pos = "bottom" # or 'right'
text_size = 16
icon_size = 88
24 changes: 24 additions & 0 deletions macos/macos_env.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: cqgui
channels:
- CadQuery
- defaults
- conda-forge
dependencies:
- pyqt=5.9.2
- pyparsing
- pyqtgraph=0.10.0
- python=3.7
- spyder=3.3.4
- pythonocc-core=0.18.2
- path.py
- logbook
- qtconsole=4.4.4
- requests
- pyinstaller
- pip
- pip:
- "git+https://github.com/CadQuery/cadquery"
- PyQt5
- spyder==3.3.4
- pyobjc-framework-Quartz
- dmgbuild
4 changes: 4 additions & 0 deletions macos/makeapp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
cd ..
pyinstaller --onedir --windowed --clean -y pyinstaller.spec
cd macos
9 changes: 9 additions & 0 deletions macos/makedmg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/sh
version="0.1"
appname="CQ-Editor"
appbundle="../dist/CQ-Editor.app"
dmgfile="${appname} v${version}.dmg"
test -f "$dmgfile" && rm "$dmgfile"
# xattr -cr $appbundle
dmgbuild -s cq_dmg_settings.py "${appname} App v.${version}" "$dmgfile"
fileicon set "$dmgfile" CQDiskImageIcon.png
102 changes: 57 additions & 45 deletions pyinstaller.spec
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,64 @@ import sys, site, os
from path import Path

block_cipher = None
spyder_data = Path(site.getsitepackages()[-1]) / "spyder"
parso_grammar = Path(site.getsitepackages()[-1]) / "parso/python/grammar36.txt"

spyder_data = Path(site.getsitepackages()[-1]) / 'spyder'
parso_grammar = Path(site.getsitepackages()[-1]) / 'parso/python/grammar36.txt'

if sys.platform == 'linux':
oce_dir = Path(sys.prefix) / 'share' / 'oce-0.18'
if sys.platform == "linux" or sys.platform == "darwin":
oce_dir = Path(sys.prefix) / "share" / "oce-0.18"
else:
oce_dir = Path(sys.prefix) / 'Library' / 'share' / 'oce'

a = Analysis(['run.py'],
pathex=['/home/adam/cq/CQ-editor'],
binaries=[],
datas=[(spyder_data ,'spyder'),
(parso_grammar, 'parso/python'),
(oce_dir , 'oce')],
hiddenimports=['ipykernel.datapub'],
hookspath=[],
runtime_hooks=['pyinstaller/pyi_rth_occ.py',
'pyinstaller/pyi_rth_fontconfig.py'],
excludes=['_tkinter',],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)

pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='CQ-editor',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
icon='icons/cadquery_logo_dark.ico')

exclude = ('libGL','libEGL','libbsd')
oce_dir = Path(sys.prefix) / "Library" / "share" / "oce"


a = Analysis(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's with the formatting change? I think only one line is actually edited.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entire pyinstaller.spec file was accidentally reformatted with black. Therefore, spacing and single-quote substitution was applied globally. Since it had no semantic impact, I left it. In the line above, I simply added the check for sys.platform=="darwin"

["run.py"],
pathex=[],
binaries=[],
datas=[(spyder_data, "spyder"), (parso_grammar, "parso/python"), (oce_dir, "oce")],
hiddenimports=["ipykernel.datapub"],
hookspath=[],
runtime_hooks=["pyinstaller/pyi_rth_occ.py", "pyinstaller/pyi_rth_fontconfig.py"],
excludes=["_tkinter",],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name="CQ-Editor",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
icon="icons/cadquery_logo_dark.ico",
)

exclude = ("libGL", "libEGL", "libbsd")
a.binaries = TOC([x for x in a.binaries if not x[0].startswith(exclude)])

coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='CQ-editor')
coll = COLLECT(
exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, name="CQ-Editor"
)

app = BUNDLE(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did build on Win and Linux without this section. Shouldn't it be behind an if?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I understand, the "BUNDLE" component will not be processed on linux/windows anyway. Guarding with an "if" is likely not necessary, but wouldn't hurt--everything still builds the same in macOS with or without the if guard.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, but still it will be more readable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, I'll put a platform 'if' guard for macOS to enhance readability.

coll,
name="CQ-Editor.app",
icon="icons/cadquery_logo_dark.icns",
bundle_identifier="org.cadquery.cqeditor",
info_plist={
"CFBundleName": "CQ-Editor",
"CFBundleShortVersionString": "0.1.0",
"NSHighResolutionCapable": True,
"NSPrincipalClass": "NSApplication",
"NSAppleScriptEnabled": False,
"LSBackgroundOnly": False,
},
)

Loading