diff --git a/.gitignore b/.gitignore index 3fb3af0d13..bd2d0ea705 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ *.sif *.dll *.pyc +*.whl a.out __pycache__ diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 3f409c3820..0b270cc68f 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -16,6 +16,7 @@ endif() project(lammps CXX) set(SOVERSION 0) +get_property(BUILD_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) get_filename_component(LAMMPS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/.. ABSOLUTE) get_filename_component(LAMMPS_LIB_BINARY_DIR ${CMAKE_BINARY_DIR}/lib ABSOLUTE) @@ -748,13 +749,15 @@ if(BUILD_SHARED_LIBS) else() find_package(Python COMPONENTS Interpreter) endif() + if(BUILD_IS_MULTI_CONFIG) + set(LIBLAMMPS_SHARED_BINARY ${CMAKE_BINARY_DIR}/$/liblammps${LAMMPS_MACHINE}${CMAKE_SHARED_LIBRARY_SUFFIX}) + else() + set(LIBLAMMPS_SHARED_BINARY ${CMAKE_BINARY_DIR}/liblammps${LAMMPS_MACHINE}${CMAKE_SHARED_LIBRARY_SUFFIX}) + endif() if(Python_EXECUTABLE) add_custom_target( install-python ${CMAKE_COMMAND} -E remove_directory build - COMMAND ${Python_EXECUTABLE} install.py -v ${LAMMPS_SOURCE_DIR}/version.h - -p ${LAMMPS_PYTHON_DIR}/lammps - -l ${CMAKE_BINARY_DIR}/liblammps${LAMMPS_MACHINE}${CMAKE_SHARED_LIBRARY_SUFFIX} - WORKING_DIRECTORY ${LAMMPS_PYTHON_DIR} + COMMAND ${Python_EXECUTABLE} ${LAMMPS_PYTHON_DIR}/install.py -p ${LAMMPS_PYTHON_DIR}/lammps -l ${LIBLAMMPS_SHARED_BINARY} COMMENT "Installing LAMMPS Python module") else() add_custom_target( @@ -799,7 +802,6 @@ if(ClangFormat_FOUND) endif() get_target_property(DEFINES lammps COMPILE_DEFINITIONS) -get_property(BUILD_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(BUILD_IS_MULTI_CONFIG) set(LAMMPS_BUILD_TYPE "Multi-Config") else() diff --git a/doc/src/Python_install.rst b/doc/src/Python_install.rst index abf96accbf..aaeeec18f1 100644 --- a/doc/src/Python_install.rst +++ b/doc/src/Python_install.rst @@ -25,11 +25,10 @@ Installing the LAMMPS Python Module and Shared Library ====================================================== Making LAMMPS usable within Python and vice versa requires putting the -LAMMPS Python package (``lammps``) into a location where the -Python interpreter can find it and installing the LAMMPS shared library -into a folder that the dynamic loader searches or inside of the installed -``lammps`` package folder. There are multiple ways to achieve -this. +LAMMPS Python package (``lammps``) into a location where the Python +interpreter can find it and installing the LAMMPS shared library into a +folder that the dynamic loader searches or inside of the installed +``lammps`` package folder. There are multiple ways to achieve this. #. Do a full LAMMPS installation of libraries, executables, selected headers, documentation (if enabled), and supporting files (only @@ -159,38 +158,52 @@ this. make install-python - This will try to install (only) the shared library and the Python - package into a system folder and if that fails (due to missing - write permissions) will instead do the installation to a user - folder under ``$HOME/.local``. For a system-wide installation you + This will try to build a so-called (binary) 'wheel', a compressed + binary python package and then install it with the python package + manager 'pip'. Installation will be attempted into a system-wide + ``site-packages`` folder and if that fails into the corresponding + folder in the user's home directory. For a system-wide installation you would have to gain superuser privilege, e.g. though ``sudo`` - +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ - | File | Location | Notes | - +========================+=================================================================+=============================================================+ - | LAMMPS Python package | * ``$HOME/.local/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``$HOME/.local/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | - +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS shared library | * ``$HOME/.local/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``$HOME/.local/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | - +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + +------------------------+----------------------------------------------------------+-------------------------------------------------------------+ + | File | Location | Notes | + +========================+==========================================================+=============================================================+ + | LAMMPS Python package | * ``$HOME/.local/lib/pythonX.Y/site-packages/lammps`` | ``X.Y`` depends on the installed Python version | + +------------------------+----------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS shared library | * ``$HOME/.local/lib/pythonX.Y/site-packages/lammps`` | ``X.Y`` depends on the installed Python version | + +------------------------+----------------------------------------------------------+-------------------------------------------------------------+ For a system-wide installation those folders would then become. - +------------------------+---------------------------------------------------------+-------------------------------------------------------------+ - | File | Location | Notes | - +========================+=========================================================+=============================================================+ - | LAMMPS Python package | * ``/usr/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``/usr/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | - +------------------------+---------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS shared library | * ``/usr/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``/usr/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | - +------------------------+---------------------------------------------------------+-------------------------------------------------------------+ + +------------------------+-------------------------------------------------+-------------------------------------------------------------+ + | File | Location | Notes | + +========================+=================================================+=============================================================+ + | LAMMPS Python package | * ``/usr/lib/pythonX.Y/site-packages/lammps`` | ``X.Y`` depends on the installed Python version | + +------------------------+-------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS shared library | * ``/usr/lib/pythonX.Y/site-packages/lammps`` | ``X.Y`` depends on the installed Python version | + +------------------------+-------------------------------------------------+-------------------------------------------------------------+ No environment variables need to be set for those, as those folders are searched by default by Python or the LAMMPS Python package. + .. versionchanged:: TBD + + .. note:: + + If there is an existing installation of the LAMMPS python + module, ``make install-python`` will try to update it. + However, that will fail if the older version of the module + was installed by LAMMPS versions until 17Feb2022. Those + were using the distutils package, which does not create a + "manifest" that allows a clean uninstall. The ``make + install-python`` command will always produce a + lammps-----.whl file (the + 'wheel'). And this file can be later installed directly with + ``python -m pip install .whl`` without having to + type ``make install-python`` again and repeating the build + step, too. + For the traditional make process you can override the python version to version x.y when calling ``make`` with ``PYTHON=pythonX.Y``. For a CMake based compilation this choice @@ -201,16 +214,12 @@ this. .. code-block:: bash - $ python install.py -p -l -v [-d ] + $ python install.py -p -l [-n] * The ``-p`` flag points to the ``lammps`` Python package folder to be installed, * the ``-l`` flag points to the LAMMPS shared library file to be installed, - * the ``-v`` flag points to the ``version.h`` file in the LAMMPS source - * and the optional ``-d`` flag to a custom (legacy) installation folder - - If you use a legacy installation folder, you will need to set your - ``PYTHONPATH`` and ``LD_LIBRARY_PATH`` (and/or ``DYLD_LIBRARY_PATH``) environment - variables accordingly as explained in the description for "In place use". + * and the optional ``-n`` instructs the script to only build a wheel file + but not attempt to install it. .. tab:: Virtual environment @@ -257,32 +266,29 @@ this. package and the shared library file are installed into the following locations: - +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ - | File | Location | Notes | - +========================+=================================================================+=============================================================+ - | LAMMPS Python Module | * ``$VIRTUAL_ENV/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``$VIRTUAL_ENV/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | - +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS shared library | * ``$VIRTUAL_ENV/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``$VIRTUAL_ENV/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | - +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + +------------------------+--------------------------------------------------------+-------------------------------------------------------------+ + | File | Location | Notes | + +========================+========================================================+=============================================================+ + | LAMMPS Python Module | * ``$VIRTUAL_ENV/lib/pythonX.Y/site-packages/lammps`` | ``X.Y`` depends on the installed Python version | + +------------------------+--------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS shared library | * ``$VIRTUAL_ENV/lib/pythonX.Y/site-packages/lammps`` | ``X.Y`` depends on the installed Python version | + +------------------------+--------------------------------------------------------+-------------------------------------------------------------+ If you do a full installation (CMake only) with "install", this leads to the following installation locations: - +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ - | File | Location | Notes | - +========================+=================================================================+=============================================================+ - | LAMMPS Python Module | * ``$VIRTUAL_ENV/lib/pythonX.Y/site-packages/lammps`` (32bit) | ``X.Y`` depends on the installed Python version | - | | * ``$VIRTUAL_ENV/lib64/pythonX.Y/site-packages/lammps`` (64bit) | | - +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS shared library | * ``$VIRTUAL_ENV/lib/`` (32bit) | Set shared loader environment variable to this path | - | | * ``$VIRTUAL_ENV/lib64/`` (64bit) | (see below for more info on this) | - +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS executable | * ``$VIRTUAL_ENV/bin/`` | | - +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ - | LAMMPS potential files | * ``$VIRTUAL_ENV/share/lammps/potentials/`` | Set ``LAMMPS_POTENTIALS`` environment variable to this path | - +------------------------+-----------------------------------------------------------------+-------------------------------------------------------------+ + +------------------------+--------------------------------------------------------+-------------------------------------------------------------+ + | File | Location | Notes | + +========================+========================================================+=============================================================+ + | LAMMPS Python Module | * ``$VIRTUAL_ENV/lib/pythonX.Y/site-packages/lammps`` | ``X.Y`` depends on the installed Python version | + +------------------------+--------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS shared library | * ``$VIRTUAL_ENV/lib/`` (32bit) | Set shared loader environment variable to this path | + | | * ``$VIRTUAL_ENV/lib64/`` (64bit) | (see below for more info on this) | + +------------------------+--------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS executable | * ``$VIRTUAL_ENV/bin/`` | | + +------------------------+--------------------------------------------------------+-------------------------------------------------------------+ + | LAMMPS potential files | * ``$VIRTUAL_ENV/share/lammps/potentials/`` | Set ``LAMMPS_POTENTIALS`` environment variable to this path | + +------------------------+--------------------------------------------------------+-------------------------------------------------------------+ In that case you need to modify the ``$HOME/myenv/bin/activate`` script in a similar fashion you need to update your diff --git a/doc/utils/sphinx-config/false_positives.txt b/doc/utils/sphinx-config/false_positives.txt index 18ae834078..31e03140f1 100644 --- a/doc/utils/sphinx-config/false_positives.txt +++ b/doc/utils/sphinx-config/false_positives.txt @@ -3,6 +3,7 @@ aat abc abf ABI +abi abo Abramyan absTol @@ -2450,6 +2451,7 @@ ortho orthonormal orthorhombic Ortner +os oso Otype Ouadfel @@ -3616,6 +3618,7 @@ wget Whelan whitesmoke whitespace +whl Wi Wicaksono widom diff --git a/python/install.py b/python/install.py index a3668754d9..a71a601c42 100644 --- a/python/install.py +++ b/python/install.py @@ -1,16 +1,17 @@ #!/usr/bin/env python """ -Installer script to install the LAMMPS python package and the corresponding -shared library into either the system-wide site-packages tree, or - failing -that - into the corresponding user tree. Called from the 'install-python' -build target in the conventional and CMake based build systems +Script to build a "binary wheel" for the 'pip' Python package manager for +the LAMMPS python module which includes the shared library file. After a +successful build the script attempts to install the wheel into a system +specific site-packages folder or - failing that - into the corresponding +user site-packages folder. Called from the 'install-python' build target +in the GNU make and CMake based build systems. Can also be called +independently and used to build the wheel without installing it. """ -# copy LAMMPS shared library and lammps package to system dirs - from __future__ import print_function -import sys,os,shutil,time +import sys,os,shutil,time,glob,subprocess from argparse import ArgumentParser parser = ArgumentParser(prog='install.py', @@ -20,11 +21,8 @@ parser.add_argument("-p", "--package", required=True, help="path to the LAMMPS Python package") parser.add_argument("-l", "--lib", required=True, help="path to the compiled LAMMPS shared library") -parser.add_argument("-v", "--version", required=True, - help="path to the LAMMPS version.h header file") - -parser.add_argument("-d","--dir", - help="Legacy custom installation folder selection for package and library") +parser.add_argument("-n", "--noinstall", action="store_true", default=False, + help="only build a binary wheel. Don't attempt to install it") args = parser.parse_args() @@ -46,91 +44,70 @@ if args.lib: else: args.lib = os.path.abspath(args.lib) -if args.version: - if not os.path.exists(args.version): - print( "ERROR: LAMMPS version header file %s does not exist" % args.version) - parser.print_help() - sys.exit(1) - else: - args.version = os.path.abspath(args.version) - -if args.dir: - if not os.path.isdir(args.dir): - print( "ERROR: Installation folder %s does not exist" % args.dir) - parser.print_help() - sys.exit(1) - else: - args.dir = os.path.abspath(args.dir) - -# if a custom directory is given, we copy the files directly -# without any special processing or additional steps to that folder - -if args.dir: - print("Copying LAMMPS Python package to custom folder %s" % args.dir) - try: - shutil.copytree(args.package, os.path.join(args.dir,'lammps')) - except shutil.Error: - pass # fail silently - - print("Copying LAMMPS shared library to custom folder %s" % args.dir) - try: - shutil.copyfile(args.lib, os.path.join(args.dir,os.path.basename(args.lib))) - except shutil.Error: - pass # fail silently - - sys.exit() - -# extract LAMMPS version string from header -# and convert to python packaging compatible version -def get_lammps_version(header): - with open(header, 'r') as f: - line = f.readline() - start_pos = line.find('"')+1 - end_pos = line.find('"', start_pos) - t = time.strptime("".join(line[start_pos:end_pos].split()), "%d%b%Y") - return "{}.{}.{}".format(t.tm_year,t.tm_mon,t.tm_mday) - -verstr = get_lammps_version(args.version) - -print("Installing LAMMPS Python package version %s into site-packages folder" % verstr) - # we need to switch to the folder of the python package +olddir = os.path.abspath('.') os.chdir(os.path.dirname(args.package)) -from distutils.core import setup -from distutils.sysconfig import get_python_lib -import site -from sys import version_info +# remove any wheel files left over from previous calls +print("Purging existing wheels...") +for wheel in glob.glob('lammps-*.whl'): + print("deleting " + wheel) + os.remove(wheel) -if version_info.major >= 3: - pkgs = ['lammps', 'lammps.mliap'] -else: - pkgs = ['lammps'] +# copy shared object to the current folder so that +# it will show up in the installation at the expected location +os.putenv('LAMMPS_SHARED_LIB',os.path.basename(args.lib)) +shutil.copy(args.lib,'lammps') -#Arguments common to global or user install -- everything but data_files -setup_kwargs= dict(name="lammps", - version=verstr, - author="Steve Plimpton", - author_email="sjplimp@sandia.gov", - url="https://www.lammps.org", - description="LAMMPS Molecular Dynamics Python package", - license="GPL", - packages=pkgs, - ) - -tryuser=False +# create a virtual environment for building the wheel +shutil.rmtree('buildwheel',True) try: - sys.argv = ["setup.py","install"] # as if had run "python setup.py install" - setup_kwargs['data_files']=[(os.path.join(get_python_lib(), 'lammps'), [args.lib])] - setup(**setup_kwargs) -except: # lgtm [py/catch-base-exception] - tryuser=True - print ("Installation into global site-packages folder failed.\nTrying user folder %s now." % site.USER_SITE) + txt = subprocess.check_output([sys.executable, '-m', 'virtualenv', 'buildwheel', '-p', sys.executable], stderr=subprocess.STDOUT, shell=False) + print(txt.decode('UTF-8')) +except subprocess.CalledProcessError as err: + sys.exit("Failed to create a virtualenv: {0}".format(err.output.decode('UTF-8'))) -if tryuser: +# now run the commands to build the wheel. those must be in a separate script +# and run in subprocess, since this will use the virtual environment and +# there is no simple way to return from that in python. +os.system(sys.executable + ' makewheel.py') + +# remove temporary folders and files +shutil.rmtree('buildwheel',True) +shutil.rmtree('build',True) +shutil.rmtree('lammps.egg-info',True) +os.remove(os.path.join('lammps',os.path.basename(args.lib))) + +# stop here if we were asked not to install the wheel we created +if args.noinstall: + exit(0) + +# install the wheel with pip. first try to install in the default environment. +# that will be a virtual environment, if active, or the system folder. +# recent versions of pip will automatically drop to use the user folder +# in case the system folder is not writable. + +# we use a subprocess so we can catch an exception on failure. +# we need to check whether pip refused to install because of a +# version of the module previously installed with distutils. those +# must be uninstalled manually. We must not ignore this and drop +# back to install into a (forced) user folder. + +print("Installing wheel") +for wheel in glob.glob('lammps-*.whl'): try: - sys.argv = ["setup.py","install","--user"] # as if had run "python setup.py install --user" - setup_kwargs['data_files']=[(os.path.join(site.USER_SITE, 'lammps'), [args.lib])] - setup(**setup_kwargs) - except: # lgtm [py/catch-base-exception] - print("Installation into user site package folder failed.") + txt = subprocess.check_output([sys.executable, '-m', 'pip', 'install', '--force-reinstall', wheel], stderr=subprocess.STDOUT, shell=False) + print(txt.decode('UTF-8')) + continue + except subprocess.CalledProcessError as err: + errmsg = err.output.decode('UTF-8') + if errmsg.find("distutils installed"): + sys.exit(errmsg + "You need to uninstall the LAMMPS python module manually first.\n") + try: + print('Installing wheel into standard site-packages folder failed. Trying user folder now') + txt = subprocess.check_output([sys.executable, '-m', 'pip', 'install', '--user', '--force-reinstall', wheel], stderr=subprocess.STDOUT, shell=False) + print(txt.decode('UTF-8')) + except: + sys.exit('Failed to install wheel ' + wheel) + shutil.copy(wheel, olddir) + os.remove(wheel) diff --git a/python/makewheel.py b/python/makewheel.py new file mode 100644 index 0000000000..64ecbe2464 --- /dev/null +++ b/python/makewheel.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +import sys,os,shutil + +# find python script to activate the virtual environment and source it +if sys.platform == 'win32': + virtenv=os.path.join('buildwheel','Scripts','activate_this.py') +else: + virtenv=os.path.join('buildwheel','bin','activate_this.py') + +exec(open(virtenv).read(), {'__file__': virtenv}) + +# update pip and install all requirements to build the wheel +os.system('python -m pip install --upgrade pip') +os.system('python -m pip install --upgrade -r wheel_requirements.txt') + +print("Building new binary wheel") +os.system('python -m build -n --wheel -o .') diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000000..b5c9a51ece --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = [ "setuptools>=42", "wheel" ] +build-backend = "setuptools.build_meta" diff --git a/python/setup.py b/python/setup.py index 5a6a54258a..0097e3d596 100644 --- a/python/setup.py +++ b/python/setup.py @@ -1,8 +1,9 @@ # this only installs the LAMMPS python package # it assumes the LAMMPS shared library is already installed from setuptools import setup +from setuptools.dist import Distribution from sys import version_info -import os,time +import os,time,shutil LAMMPS_PYTHON_DIR = os.path.dirname(os.path.realpath(__file__)) LAMMPS_DIR = os.path.dirname(LAMMPS_PYTHON_DIR) LAMMPS_SOURCE_DIR = os.path.join(LAMMPS_DIR, 'src') @@ -21,18 +22,48 @@ def get_lammps_version(): t = time.strptime("".join(line[start_pos:end_pos].split()), "%d%b%Y") return "{}.{}.{}".format(t.tm_year,t.tm_mon,t.tm_mday) +class BinaryDistribution(Distribution): + """Wrapper to enforce creating a binary package""" + def has_ext_modules(foo): + return True + if version_info.major >= 3: pkgs = ['lammps', 'lammps.mliap'] else: pkgs = ['lammps'] +with open("README", "r") as fh: + long_description = fh.read() + +libname = os.environ.get("LAMMPS_SHARED_LIB") +if libname: + pkgdata = {'lammps': [ libname ]} + bdist = BinaryDistribution +else: + pkgdata = {} + bdist = Distribution + setup( name = "lammps", version = get_lammps_version(), author = "Steve Plimpton", author_email = "sjplimp@sandia.gov", url = "https://www.lammps.org", + project_urls = { + "Bug Tracker": "https://github.com/lammps/lammps/issues", + }, description = "LAMMPS Molecular Dynamics Python package", + long_description = long_description, + long_description_content_type = "text/plain", + classifiers = [ + "Programming Language :: Python :: 3", + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Operating System :: OS Independent", + ], license = "GPL", - packages=pkgs, + packages = pkgs, + package_data = pkgdata, + distclass = bdist, ) diff --git a/python/wheel_requirements.txt b/python/wheel_requirements.txt new file mode 100644 index 0000000000..dafedeee23 --- /dev/null +++ b/python/wheel_requirements.txt @@ -0,0 +1,4 @@ +pip +build +wheel +setuptools diff --git a/src/Makefile b/src/Makefile index 44cebcb20c..6dfa0357ce 100644 --- a/src/Makefile +++ b/src/Makefile @@ -458,8 +458,7 @@ mpi-stubs: sinclude ../lib/python/Makefile.lammps install-python: @rm -rf ../python/build - @$(PYTHON) ../python/install.py -v ../src/version.h \ - -p ../python/lammps -l ../src/liblammps.so + @$(PYTHON) ../python/install.py -p ../python/lammps -l ../src/liblammps.so # Create a tarball of src dir and packages diff --git a/tools/singularity/rocky8.def b/tools/singularity/rocky8.def index ea74a50339..098fd2b868 100644 --- a/tools/singularity/rocky8.def +++ b/tools/singularity/rocky8.def @@ -74,7 +74,7 @@ EOF CUSTOM_PROMPT_ENV=/.singularity.d/env/99-zz_custom_prompt.sh cat >$CUSTOM_PROMPT_ENV <