#!/usr/bin/env python
# Load a virtual environment to run MagellanMapper
"""Cross-platform environment activation and MagellanMapper launcher script.
Launches MagellanMapper within a Conda or Venv environment. Environment
activation is in this order:
* If a Conda or Venv environment is already activate, launch in current
environment
* Attempt to activate a Conda environment
* Attempt to activate a Venv environment
This script is designed to run MagellanMapper without assuming that a Python
environment has been activated. It does assume that an environment has
been generated and can be identified.
Executing this script as a text file (ie not through Python directly)
assumes that the ``python`` command is accessible without a Python
environment. Use ``bin/runaltpy.sh`` instead if only ``python3`` is
available (eg on Linux) or ``python`` is only available through Conda.
Conda activation assumes that the ``conda`` command is accessible, typically
from a previous initialization through ``conda init`` (preferred) or
by adding the ``conda`` binary directory to the ``PATH``. Note that ``conda``
may not be available in environments that do not load the full shell
configuration such as Python launched via Finder in the macOS platform.
"""
import logging
import multiprocessing
import os
import platform
import subprocess
import sys
import tempfile
#: str: Name of Conda or Venv environment
ENV_NAME = "mag"
#: str: Directory with Venvs directories.
VENV_DIR = "../venvs"
#: str: Conda environment variable for the currently activated environment name.
_CONDA_ENV_KEY = "CONDA_DEFAULT_ENV"
#: List[str]: Shell commands to launch the MagellanMapper.
ARGS_MAGMAP = [
"python -u -c \"from magmap.io import cli; cli.main(); "
"from magmap.gui import visualizer; visualizer.main()\" {}"
.format(" ".join(sys.argv[1:])),
]
# Conda hook for Windows Command Prompt
_ARG_CONDA_HOOK_WIN = "conda_hook.bat"
#: List[str]: Shell commands to activate a Conda environment.
# add Conda hook for Bash shells to temporarily initialize Conda if
# `conda init` was not run, allowing commands such as `conda activate`
# TODO: add other shells
ARGS_CONDA = [
"eval \"$(conda shell.bash hook)\"",
"conda activate {}".format(ENV_NAME),
]
[docs]
def is_conda_activated():
"""Check whether the MagellanMapper Conda environment is active.
The environment name does not have to exactly match :const:`ENV_NAME`
but at least start with this name to provide some flexibility for
different versions of this environment, such as ``mag2``.
Returns:
bool: True if a Conda environment starting with the name
:const:`ENV_NAME` is currently activated.
"""
return (_CONDA_ENV_KEY in os.environ
and os.environ[_CONDA_ENV_KEY].startswith(ENV_NAME))
[docs]
def is_venv_activated():
"""Check whether a Venv or virtualenv environment is active.
Returns:
True if the environment is activated.
"""
return (hasattr(sys, "real_prefix") or
(hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix))
[docs]
def launch_subprocess(args, working_dir=None, sys_shell=False):
"""Launch a subprocess with multiple commands strung together by
logical ands.
Args:
args (List): List of shell commands.
working_dir (str): Working directory path; defaults to None.
sys_shell (bool): True to launch the subprocess in the system shell;
otherwise, ``args`` will be run in an interactive Bash shell.
Defaults to False.
Returns:
int: 0 if the return code was 0; otherwise, raises a
:class:`subprocess.CalledProcessError`.
"""
subproc_args = {
"args": "&&".join(args),
"cwd": working_dir,
"shell": sys_shell,
"stderr": subprocess.STDOUT,
}
if not sys_shell:
subproc_args["args"] = ["bash", "-i", "-c", subproc_args["args"]]
return subprocess.check_output(**subproc_args)
[docs]
def launch_magmap():
"""Launch MagellanMapper.
First launch the CLI to process user arguments, which will shut down
the process afterward if a CLI task has been selected. If the process
remains alive, the GUI will be launched.
"""
# set up uncaught exception handler in case this function is the entry point
sys.excepthook = log_uncaught_exception
if sys.path and sys.path[0].endswith(os.path.dirname(__file__)):
# remove this module's sub-package from path as may occur when the
# module is launched directly, eg from a subprocess in Visualization,
# which can mask other packages named the same as this app's modules
sys.path.pop(0)
from magmap.io import cli
cli.main()
from magmap.gui import visualizer
visualizer.main()
[docs]
def log_uncaught_exception(exc_type, exc, trace):
"""Handle uncaught exceptions globally with logging.
Args:
exc_type: Exception class.
exc: Exception instance.
trace: Traceback object.
Returns:
"""
logger = logging.getLogger()
if not (any([isinstance(h, logging.StreamHandler)
for h in logger.handlers])):
# add stream handler to output to terminal if not set up yet
logger.addHandler(logging.StreamHandler())
# log to temp file in case file logging has not been set up yet,
# in additional to any existing log file handler
log_file = tempfile.NamedTemporaryFile(
prefix="magellanmapper_error_", suffix=".log", delete=False)
logger.addHandler(logging.FileHandler(log_file.name))
# log the exception
logger.critical(
"Unhandled exception. Additional log saved to: %s", log_file.name,
exc_info=(exc_type, exc, trace))
[docs]
def main():
"""Launch MagellanMapper with environment activation.
If necessary, attempt to activate a virtual environment created
by MagellanMapper.
"""
# log any unhandled exception
sys.excepthook = log_uncaught_exception
working_dir = os.path.dirname(os.path.abspath(__file__))
if is_conda_activated() or is_venv_activated():
# launch MagellanMapper if environment is already active
launch_magmap()
return
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
# detect frozen env using the PyInstaller-specific attributes
# prioritize JRE in app root dir; non-symlink required for Mac .app
java_home = os.path.realpath(os.path.join(sys._MEIPASS, "jre"))
if java_home and os.path.isdir(java_home):
# adjust JAVA_HOME environment variable for frozen environment
java_home_orig = os.getenv("JAVA_HOME")
os.environ["JAVA_HOME"] = str(java_home)
home_orig = (java_home_orig if java_home_orig is None
else f"\"{java_home_orig}\"")
print(f"Converted JAVA_HOME from {home_orig} to \"{java_home}\"")
# bypass environment activation
print("Launching from from bundled environment")
launch_magmap()
return
use_sys_shell = False
if platform.system() == "Windows":
# replace Conda hook with Command Prompt shell hook
# TODO: check whether this hook command is necessary in Windows
ARGS_CONDA[0] = _ARG_CONDA_HOOK_WIN
use_sys_shell = True
try:
# activate Conda environment, assuming default name in setup script
# and need to initialize shell, and launch MagellanMapper
print("Attempting to activate Conda environment")
launch_subprocess(ARGS_CONDA + ARGS_MAGMAP, working_dir, use_sys_shell)
except subprocess.CalledProcessError as e:
print(e.output)
try:
# non-POSIX shells do not accept eval but may run without
# initializing the Conda shell hook
print("Retrying Conda activation without shell hook")
launch_subprocess(
ARGS_CONDA[1:] + ARGS_MAGMAP, working_dir, use_sys_shell)
except subprocess.CalledProcessError:
print(e.output)
try:
# if unable to activate Conda env, try Venv
print("Conda environment not available, trying Venv")
launch_subprocess(
["source {}/{}/bin/activate".format(VENV_DIR, ENV_NAME)]
+ ARGS_MAGMAP, working_dir, use_sys_shell)
except subprocess.CalledProcessError:
print(e.output)
# as fallback, attempt to launch without activating
# an environment
print("Neither environment is available, attempting to launch "
"without environment")
launch_magmap()
if __name__ == "__main__":
# support multiprocessing in frozen environments, necessary for Windows;
# no effect on other platforms or non-frozen environments
multiprocessing.freeze_support()
# start MagellanMapper
print("Loading MagellanMapper environment...")
main()