From 3d98797d8c230b42ab998cb65b321c024677dc4a Mon Sep 17 00:00:00 2001 From: Charlotte Weaver Date: Fri, 12 Jul 2019 12:29:22 -0700 Subject: [PATCH] Installer (#840) --- .bumpversion.cfg | 4 + cellxgene-osx.spec | 41 ++++++ cellxgene-windows.spec | 36 +++++ makefile | 24 +++- server/__init__.py | 1 + server/app/rest_api/rest.py | 10 +- server/gui/cellxgene.spec | 29 ++++ server/gui/hook-cefpython3.py | 237 +++++++++++++++++++++++++++++++ server/gui/images/cxg_icons.icns | Bin 0 -> 44068 bytes server/gui/images/icon.ico | Bin 0 -> 14782 bytes server/gui/main.py | 7 +- server/requirements-gui.txt | 4 + 12 files changed, 383 insertions(+), 10 deletions(-) create mode 100644 cellxgene-osx.spec create mode 100644 cellxgene-windows.spec create mode 100644 server/gui/cellxgene.spec create mode 100644 server/gui/hook-cefpython3.py create mode 100644 server/gui/images/cxg_icons.icns create mode 100644 server/gui/images/icon.ico create mode 100644 server/requirements-gui.txt diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d2058a44..6a3692ae 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -13,3 +13,7 @@ replace = version="{new_version}" search = "version": "{current_version}" replace = "version": "{new_version}" +[bumpversion:file:server/__init__.py] +search = "__version__ = "{current_version}" +replace = "__version__ = "{new_version}" + diff --git a/cellxgene-osx.spec b/cellxgene-osx.spec new file mode 100644 index 00000000..7bac23c4 --- /dev/null +++ b/cellxgene-osx.spec @@ -0,0 +1,41 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + + +a = Analysis(['server/gui/main.py'], + pathex=['/Users/charlotteweaver/Documents/Git/cellxgene'], + binaries=[('/System/Library/Frameworks/Tk.framework/Tk', 'tk'), ('/System/Library/Frameworks/Tcl.framework/Tcl', 'tcl')], + datas=[('server/app/web/templates/', 'server/app/web/templates/'), ('server/app/web/static/', 'server/app/web/static/')], + hiddenimports=['sklearn', 'sklearn.utils._cython_blas', 'sklearn.neighbors.typedefs', 'sklearn.neighbors.quad_tree', 'sklearn.tree', 'sklearn.tree._utils'], + hookspath=['server/gui/'], + runtime_hooks=[], + excludes=[], + 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='cellxgene', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False , icon='server/gui/images/cxg_icons.icns') +coll = COLLECT(exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='cellxgene') +app = BUNDLE(coll, + name='cellxgene.app', + icon='server/gui/images/cxg_icons.icns', + bundle_identifier=None) diff --git a/cellxgene-windows.spec b/cellxgene-windows.spec new file mode 100644 index 00000000..2bd7054a --- /dev/null +++ b/cellxgene-windows.spec @@ -0,0 +1,36 @@ +# -*- mode: python -*- + +block_cipher = None + + +a = Analysis(['server\\gui\\main.py'], + pathex=['C:\\Users\\Charlotte\\Documents\\git\\cellxgene'], + binaries=[], + datas=[('server/app/web/templates/', 'server/app/web/templates'), ('server/app/web/static/', 'server/app/web/static')], + hiddenimports=[], + hookspath=['server/gui/'], + runtime_hooks=[], + excludes=[], + 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='cellxgene', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False , icon='server\\gui\\images\\icon.ico') +coll = COLLECT(exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + name='cellxgene') diff --git a/makefile b/makefile index 388d308e..5e61f72f 100644 --- a/makefile +++ b/makefile @@ -1,7 +1,7 @@ BUILDDIR := build CLIENTBUILD := $(BUILDDIR)/client SERVERBUILD := $(BUILDDIR)/server -CLEANFILES := $(BUILDDIR)/ client/build dist cellxgene.egg-info +CLEANFILES := $(BUILDDIR)/ client/build build dist cellxgene.egg-info PART ?= patch @@ -84,6 +84,9 @@ release-directly-to-prod : dev-env pydist twine-prod dev-env : pip install -r server/requirements-dev.txt +gui-env : dev-env + pip install -r server/requirements-gui.txt + # give PART=[major, minor, part] as param to make bump bump : bumpversion --config-file .bumpversion.cfg $(PART) @@ -136,4 +139,21 @@ uninstall : build-assets : pyside2-rcc server/gui/cellxgene.qrc -o server/gui/cellxgene_rc.py -.PHONY : build-assets +gui-spec-osx : clean-lite gui-env + pip install -e .[gui] + pyi-makespec -D -w --additional-hooks-dir server/gui/ -n cellxgene --add-binary='/System/Library/Frameworks/Tk.framework/Tk':'tk' --add-binary='/System/Library/Frameworks/Tcl.framework/Tcl':'tcl' --add-data server/app/web/templates/:server/app/web/templates/ --add-data server/app/web/static/:server/app/web/static/ --icon server/gui/images/cxg_icons.icns server/gui/main.py + mv cellxgene.spec cellxgene-osx.spec + +gui-spec-windows : clean-lite dev-env + pip install -e .[gui] + pyi-makespec -D -w --additional-hooks-dir server/gui/ -n cellxgene --add-data server/app/web/templates;server/app/web/templates --add-data server/app/web/static;server/app/web/static --icon server/gui/images/icon.ico server/gui/main.py + mv cellxgene.spec cellxgene-windows.spec + +gui-build-osx : clean-lite + pyinstaller --clean cellxgene-osx.spec + +gui-build-windows : clean-lite + pyinstaller --clean cellxgene-windows.spec + +.PHONY : build-assets gui-build-osx gui-build-windows gui-build-osx gui-build-windows + diff --git a/server/__init__.py b/server/__init__.py index e69de29b..1f4c4d43 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -0,0 +1 @@ +__version__ = "0.10.1" diff --git a/server/app/rest_api/rest.py b/server/app/rest_api/rest.py index aafe175a..b94c5439 100644 --- a/server/app/rest_api/rest.py +++ b/server/app/rest_api/rest.py @@ -1,9 +1,10 @@ from http import HTTPStatus -import pkg_resources import warnings from flask import Blueprint, current_app, jsonify, make_response, request from flask_restful import Api, Resource +from server import __version__ as cellxgene_version +from anndata import __version__ as anndata_version from server.app.util.constants import ( Axis, @@ -59,16 +60,15 @@ class ConfigAPI(Resource): }, ], "displayNames": { - "engine": f"cellxgene Scanpy engine version {pkg_resources.get_distribution('cellxgene').version}", + "engine": f"cellxgene Scanpy engine version ", "dataset": current_app.config["DATASET_TITLE"], }, "parameters": { "max-category-items": current_app.data.config["max_category_items"] }, "library_versions": { - "scanpy": pkg_resources.get_distribution("scanpy").version, - "cellxgene": pkg_resources.get_distribution("cellxgene").version, - "anndata": pkg_resources.get_distribution("cellxgene").version + "cellxgene": cellxgene_version, + "anndata": anndata_version } } } diff --git a/server/gui/cellxgene.spec b/server/gui/cellxgene.spec new file mode 100644 index 00000000..0dc65d3b --- /dev/null +++ b/server/gui/cellxgene.spec @@ -0,0 +1,29 @@ +# -*- mode: python -*- + +block_cipher = None + + +a = Analysis(['main.py'], + pathex=['/Users/charlotteweaver/Documents/Git/cellxgene/server/gui'], + hookspath=["/Users/charlotteweaver/Documents/Git/cellxgene/server/gui/"], + 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, + a.binaries, + a.zipfiles, + a.datas, + [], + name='cellxgene', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + runtime_tmpdir=None, + console=True ) diff --git a/server/gui/hook-cefpython3.py b/server/gui/hook-cefpython3.py new file mode 100644 index 00000000..4c6e0a9d --- /dev/null +++ b/server/gui/hook-cefpython3.py @@ -0,0 +1,237 @@ +""" +This is PyInstaller hook file for CEF Python. This file +helps PyInstaller find CEF Python dependencies that are +required to run final executable. + +See PyInstaller docs for hooks: +https://pyinstaller.readthedocs.io/en/stable/hooks.html +""" + +import glob +import os +import platform +import re +import sys +import PyInstaller +from PyInstaller.utils.hooks import is_module_satisfies, get_package_paths +from PyInstaller.compat import is_win, is_darwin, is_linux, is_py2 +from PyInstaller import log as logging + +# Constants +CEFPYTHON_MIN_VERSION = "57.0" +PYINSTALLER_MIN_VERSION = "3.2.1" + +# Makes assumption that using "python.exe" and not "pyinstaller.exe" +# TODO: use this code to work cross-platform: +# from PyInstaller.utils.hooks import get_package_paths +# get_package_paths("cefpython3") + +CEFPYTHON3_DIR = get_package_paths("cefpython3")[1] + +CYTHON_MODULE_EXT = ".pyd" if is_win else ".so" + +# Globals +logger = logging.getLogger(__name__) + + +# Functions +def check_platforms(): + if not is_win and not is_darwin and not is_linux: + raise SystemExit("Error: Currently only Windows, Linux and Darwin " + "platforms are supported, see Issue #135.") + + +def check_pyinstaller_version(): + """Using is_module_satisfies() for pyinstaller fails when + installed using 'pip install develop.zip' command + (PyInstaller Issue #2802).""" + # Example version string for dev version of pyinstaller: + # > 3.3.dev0+g5dc9557c + version = PyInstaller.__version__ + match = re.search(r"^\d+\.\d+(\.\d+)?", version) + if not (match.group(0) >= PYINSTALLER_MIN_VERSION): + raise SystemExit("Error: pyinstaller %s or higher is required" + % PYINSTALLER_MIN_VERSION) + + +def check_cefpython3_version(): + if not is_module_satisfies("cefpython3 >= %s" % CEFPYTHON_MIN_VERSION): + raise SystemExit("Error: cefpython3 %s or higher is required" + % CEFPYTHON_MIN_VERSION) + + +def get_cefpython_modules(): + """Get all cefpython Cython modules in the cefpython3 package. + It returns a list of names without file extension. Eg. + 'cefpython_py27'. """ + pyds = glob.glob(os.path.join(CEFPYTHON3_DIR, + "cefpython_py*" + CYTHON_MODULE_EXT)) + assert len(pyds) > 1, "Missing cefpython3 Cython modules" + modules = [] + for path in pyds: + filename = os.path.basename(path) + mod = filename.replace(CYTHON_MODULE_EXT, "") + modules.append(mod) + return modules + + +def get_excluded_cefpython_modules(): + """CEF Python package includes Cython modules for various Python + versions. When using Python 2.7 pyinstaller should not + bundle modules for eg. Python 3.6, otherwise it will + cause to include Python 3 dll dependencies. Returns a list + of fully qualified names eg. 'cefpython3.cefpython_py27'.""" + pyver = "".join(map(str, sys.version_info[:2])) + pyver_string = "py%s" % pyver + modules = get_cefpython_modules() + excluded = [] + for mod in modules: + if pyver_string in mod: + continue + excluded.append("cefpython3.%s" % mod) + logger.info("Exclude cefpython3 module: %s" % excluded[-1]) + return excluded + + +def get_cefpython3_datas(): + """Returning almost all of cefpython binaries as DATAS (see exception + below), because pyinstaller does strange things and fails if these are + returned as BINARIES. It first updates manifest in .dll files: + >> Updating manifest in chrome_elf.dll + + And then because of that it fails to load the library: + >> hsrc = win32api.LoadLibraryEx(filename, 0, LOAD_LIBRARY_AS_DATAFILE) + >> pywintypes.error: (5, 'LoadLibraryEx', 'Access is denied.') + + It is not required for pyinstaller to modify in any way + CEF binaries or to look for its dependencies. CEF binaries + does not have any external dependencies like MSVCR or similar. + + The .pak .dat and .bin files cannot be marked as BINARIES + as pyinstaller would fail to find binary depdendencies on + these files. + + One exception is subprocess (subprocess.exe on Windows) executable + file, which is passed to pyinstaller as BINARIES in order to collect + its dependecies. + + DATAS are in format: tuple(full_path, dest_subdir). + """ + ret = list() + + if is_win: + cefdatadir = "." + elif is_darwin or is_linux: + cefdatadir = "." + else: + assert False, "Unsupported system {}".format(platform.system()) + + # Binaries, licenses and readmes in the cefpython3/ directory + for filename in os.listdir(CEFPYTHON3_DIR): + # Ignore Cython modules which are already handled by + # pyinstaller automatically. + if filename[:-len(CYTHON_MODULE_EXT)] in get_cefpython_modules(): + continue + + # CEF binaries and datas + extension = os.path.splitext(filename)[1] + if extension in \ + [".exe", ".dll", ".pak", ".dat", ".bin", ".txt", ".so", ".plist"] \ + or filename.lower().startswith("license"): + logger.info("Include cefpython3 data: {}".format(filename)) + ret.append((os.path.join(CEFPYTHON3_DIR, filename), cefdatadir)) + + if is_darwin: + # "Chromium Embedded Framework.framework/Resources" with subdirectories + # is required. Contain .pak files and locales (each locale in separate + # subdirectory). + resources_subdir = \ + os.path.join("Chromium Embedded Framework.framework", "Resources") + base_path = os.path.join(CEFPYTHON3_DIR, resources_subdir) + assert os.path.exists(base_path), \ + "{} dir not found in cefpython3".format(resources_subdir) + for path, dirs, files in os.walk(base_path): + for file in files: + absolute_file_path = os.path.join(path, file) + dest_path = os.path.relpath(path, CEFPYTHON3_DIR) + ret.append((absolute_file_path, dest_path)) + logger.info("Include cefpython3 data: {}/{}".format(dest_path, file)) + elif is_win or is_linux: + # The .pak files in cefpython3/locales/ directory + locales_dir = os.path.join(CEFPYTHON3_DIR, "locales") + assert os.path.exists(locales_dir), \ + "locales/ dir not found in cefpython3" + for filename in os.listdir(locales_dir): + logger.info("Include cefpython3 data: {}/{}".format( + os.path.basename(locales_dir), filename)) + ret.append((os.path.join(locales_dir, filename), + os.path.join(cefdatadir, "locales"))) + + # Optional .so/.dll files in cefpython3/swiftshader/ directory + swiftshader_dir = os.path.join(CEFPYTHON3_DIR, "swiftshader") + if os.path.isdir(swiftshader_dir): + for filename in os.listdir(swiftshader_dir): + logger.info("Include cefpython3 data: {}/{}".format( + os.path.basename(swiftshader_dir), filename)) + ret.append((os.path.join(swiftshader_dir, filename), + os.path.join(cefdatadir, "swiftshader"))) + return ret + + +# ---------------------------------------------------------------------------- +# Main +# ---------------------------------------------------------------------------- + +# Checks +check_platforms() +check_pyinstaller_version() +check_cefpython3_version() + +# Info +logger.info("CEF Python package directory: %s" % CEFPYTHON3_DIR) + +# Hidden imports. +# PyInstaller has no way on detecting imports made by Cython +# modules, so all pure Python imports made in cefpython .pyx +# files need to be manually entered here. +# TODO: Write a tool script that would find such imports in +# .pyx files automatically. +hiddenimports = [ + "codecs", + "copy", + "datetime", + "inspect", + "json", + "os", + "platform", + "random", + "re", + "sys", + "time", + "traceback", + "types", + "urllib", + "weakref", +] +if is_py2: + hiddenimports += [ + "urlparse", + ] + +# Excluded modules +excludedimports = get_excluded_cefpython_modules() + +# Include binaries requiring to collect its dependencies +if is_darwin or is_linux: + binaries = [(os.path.join(CEFPYTHON3_DIR, "subprocess"), ".")] +elif is_win: + binaries = [(os.path.join(CEFPYTHON3_DIR, "subprocess.exe"), ".")] +else: + binaries = [] + +# Include datas +datas = get_cefpython3_datas() + +# Notify pyinstaller.spec code that this hook was executed +# and that it succeeded. +os.environ["PYINSTALLER_CEFPYTHON3_HOOK_SUCCEEDED"] = "1" diff --git a/server/gui/images/cxg_icons.icns b/server/gui/images/cxg_icons.icns new file mode 100644 index 0000000000000000000000000000000000000000..d57d20cf9e4eaddd591028277ae4a1dfe48a7b85 GIT binary patch literal 44068 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 VQ*vH;+wfKqsstt<%Abl0{{#mj$qN7g literal 0 HcmV?d00001 diff --git a/server/gui/images/icon.ico b/server/gui/images/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cea1f139d1b22557dd0191315811f523d67211f8 GIT binary patch literal 14782 zcmeI2O^g*q5P(~Q2V-JZyl{N1l6W#Cq8DStKg5Fv)@Y*fpe82bk*j*gn@XatxEr!d zkf@P^9v~{12%2c{=w(siVB8TF}!PRO*Lf@pOZ&qs8?MiLmp3skOQ|jJ3 z()3>+Q0g9$AtFU$!Y7rI?&{9l5-{)b_~_9Y-GoP+HhA8B;Pz>>KYn6HkL{k-)X1~% zV^7ZN6gWIqFTAJUUDCgOcUhl)XGxDgHEZf~Cs(u&;IVqJ54}_0u4u~CH!J$k)ABAg zWo6--{->S*Z3;AOvSXud`KP{a^cQ*XAKN>x4?jC+fHt^uLW}8-UiywcJY#@1xNh~2 zwtp@DYs|Hszwc#k#$^7enLFmi1#r-s^OqAp!~pZ?186pOQgtI%O*~osGS|$l1@x!l zr=h6-pma;Xd8pi$N$A32}CT-OtO=Je!90<^(FTdRC*M&78izG#DkwjT1aJC$EVKDa@V z-%0;;dVV_{pC&*G9(nQ4n*MU%yf;5sd{J(I4zDjiu%5yD@*8;$S`65nn9UEl?;@K# zM=pv1n-g>8Wxg@&3CN}7yBIk0Uh5tuq$`{E3Z~c0AALeHU0P}RIr-U@>9(2E^@<-? zuG>z7xW*@x8xb_#G#G)2yeb|1!B5D-7ABhSyNQ*SRSiBWZG z|DyWjg^LM)SDquo+tkRgDROpI>aP2C2$-@X{vA%~C?)CN?e}cpK^{E-I))mGY5O9 z0{)P!B3g^C%$}{qXR=3RAD4GG1m4^$HO}MgK|}n$yQNrMi>@Nu#gEv+8SdEY^ZLYF zmkb<#GpTt84;?d>kB_#HgNujq~;wwcck!rD0iND z{PbxTtX@|awzb4cW4f+ez7HOnF68&gh{d_OHj@3sEo1i;@Wyyy>t^BV|4!60w+xe&Rf93zy)tmOojl6yb{rffO`-ef_zxmcqS)BW=ER+AM^H*6< z`}y&|J*&q0pY!+^d+YkA@xQ(2cks~N^Sh59-R*)szq`6(?B|Z+pxld0>`BI8?=d)E z**3O9hmZS{JbwDL3qD;j_a|nbF85K`gnxbaOyHq6e>*MU_sNLGwdl&+m*k!+#IMX( z9%OyNJa8Ap_jo-4naIwyGj=>7zJ9tt4B6?UiEsMp{;=#k5nsfp@4m6!+(SQ`f6Cq; zwqrkQG4#FjXZAQOV=0sWH>&-<`?oj`hq>^7ne*>P_5UEpKl!I`_CLNHAHqrg@4Nj^ z$X*|bLE*3rGY{U1=66 +requests +PyInstaller>=3.4 +PySide2>=5.12.3