diff --git a/.coveragerc b/.coveragerc index 9aa183ac..59d4eb86 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,3 +6,6 @@ source = src [report] exclude_lines = if __name__ == .__main__.: + if sys.platform == "win32": + if sys.platform != "win32": + if sys.platform != "darwin": diff --git a/.gitignore b/.gitignore index 894a44cc..e4f2e03b 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/cq_editor/__main__.py b/cq_editor/__main__.py index cf0aa7ba..c0b200ef 100644 --- a/cq_editor/__main__.py +++ b/cq_editor/__main__.py @@ -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() diff --git a/cq_editor/main_window.py b/cq_editor/main_window.py index 8048c8a4..6a68abee 100644 --- a/cq_editor/main_window.py +++ b/cq_editor/main_window.py @@ -46,8 +46,11 @@ def __init__(self,parent=None): self.prepare_statusbar() self.prepare_actions() - - self.components['object_tree'].addLines() + + # 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() diff --git a/icons/cadquery_logo_dark.icns b/icons/cadquery_logo_dark.icns new file mode 100644 index 00000000..f3f3b799 Binary files /dev/null and b/icons/cadquery_logo_dark.icns differ diff --git a/macos/CQDiskImageIcon.icns b/macos/CQDiskImageIcon.icns new file mode 100644 index 00000000..b2c1f2c9 Binary files /dev/null and b/macos/CQDiskImageIcon.icns differ diff --git a/macos/CQDiskImageIcon.png b/macos/CQDiskImageIcon.png new file mode 100644 index 00000000..3565c1af Binary files /dev/null and b/macos/CQDiskImageIcon.png differ diff --git a/macos/CQInstallerBackground.png b/macos/CQInstallerBackground.png new file mode 100644 index 00000000..27563d0c Binary files /dev/null and b/macos/CQInstallerBackground.png differ diff --git a/macos/README.md b/macos/README.md new file mode 100644 index 00000000..4d29d39a --- /dev/null +++ b/macos/README.md @@ -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 | + + + + diff --git a/macos/brew.sh b/macos/brew.sh new file mode 100755 index 00000000..a1ee5c76 --- /dev/null +++ b/macos/brew.sh @@ -0,0 +1,6 @@ +# Make sure we’re using the latest Homebrew. +brew update +# Upgrade any already-installed formulae. +brew upgrade + +brew install fileicon diff --git a/macos/cq_dmg_settings.py b/macos/cq_dmg_settings.py new file mode 100644 index 00000000..695b6bcb --- /dev/null +++ b/macos/cq_dmg_settings.py @@ -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 diff --git a/macos/macos_env.yml b/macos/macos_env.yml new file mode 100644 index 00000000..5e0b9ca0 --- /dev/null +++ b/macos/macos_env.yml @@ -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 diff --git a/macos/makeapp.sh b/macos/makeapp.sh new file mode 100755 index 00000000..980cc8df --- /dev/null +++ b/macos/makeapp.sh @@ -0,0 +1,4 @@ +#!/bin/sh +cd .. +pyinstaller --onedir --windowed --clean -y pyinstaller.spec +cd macos diff --git a/macos/makedmg.sh b/macos/makedmg.sh new file mode 100755 index 00000000..8c49d7a8 --- /dev/null +++ b/macos/makedmg.sh @@ -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 diff --git a/pyinstaller.spec b/pyinstaller.spec index 077f8fb3..50c467d0 100644 --- a/pyinstaller.spec +++ b/pyinstaller.spec @@ -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( + ["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( + 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, + }, +) + diff --git a/run.py b/run.py index 74c18ff4..24c8c606 100644 --- a/run.py +++ b/run.py @@ -1,10 +1,28 @@ -import os +import os, sys import faulthandler faulthandler.enable() -if 'CASROOT' in os.environ: - del os.environ['CASROOT'] +# macOS implements a security sandbox policy to executable +# programs when they are launched from the launchd context, e.g. +# when double clicked from the Finder or launched from the shell +# with the 'open' OS command. Therefore, the macOS build requires +# the path to the environment variable 'CSF_ShadersDirectory' +# to be specified as an absolute path rather than a relative one. +# +# The following code performs a runtime substitution of the +# 'CSF_ShadersDirectory' environment variable if it is discovered. +# It introspects this program's absolute path and appends the +# relative path discovered from the environment. + +if sys.platform == 'darwin': + basedir = os.path.dirname(os.path.abspath(sys.argv[0])) + if 'CSF_ShadersDirectory' in os.environ: + new_path = basedir + os.sep + os.environ['CSF_ShadersDirectory'] + os.environ['CSF_ShadersDirectory'] = new_path +else: + if 'CASROOT' in os.environ: + del os.environ['CASROOT'] from cq_editor.__main__ import main