# -*- mode: python; coding: utf-8 -*-
# Copyright 2015, 2017 Peter Williams <peter@newton.cx> and collaborators.
# Licensed under the MIT License.
"""pwkit.environments - working with external software environments
Classes:
Environment - base class for launching programs in an external environment.
Submodules:
heasoft - HEAsoft
sas - SAS
Functions:
prepend_environ_path - Prepend into a $PATH in an environment dict.
prepend_path - Prepend text into a $PATH-like environment variable.
user_data_path - Generate paths for storing miscellaneous user data.
Standard usage is to create an `Environment` instance, then use its
`launch(argv, ...)` method to run programs in the specified environment.
`launch()` returns a `subprocess.Popen` instance that can be used in the
standard ways.
"""
from __future__ import absolute_import, division, print_function
"""For reference: the Python docs state that Python automatically decodes
Unicode environment variables as UTF-8 on non-Windows, so we don't need to be
crazy about bytes-ifying values.
"""
__all__ = "Environment prepend_environ_path prepend_path user_data_path".split()
import os, subprocess, sys
from .. import cli
from ..cli import multitool
class Environment(object):
def modify_environment(self, environ):
"""Modify the passed-in dictionary of environment variables to be suitable for
executing programs in this software environment. Make sure to copy
os.environ() if you don't want to modify it for the current process.
"""
raise NotImplementedError()
def _preexec(self, env, **kwargs):
pass
def launch(
self,
argv,
stdin=None,
stdout=None,
stderr=None,
close_fds=False,
env=None,
shell=False,
cwd=None,
preexec_fn=None,
executable=None,
**kwargs
):
if env is None:
env = os.environ
env = self.modify_environment(env.copy())
self._preexec(env, **kwargs)
return subprocess.Popen(
argv,
stdin=stdin,
stdout=stdout,
stderr=stderr,
close_fds=close_fds,
env=env,
shell=shell,
cwd=cwd,
preexec_fn=preexec_fn,
executable=executable,
)
def execvpe(self, argv, env=None, **kwargs):
if env is None:
env = os.environ
env = self.modify_environment(env.copy())
self._preexec(env, **kwargs)
# unlike subprocess.Popen, execvpe doesn't use the new path to find
# the program. But we're about to replace ourselves with the new
# program, so no worries about mutating os.environ. If the exec fails
# the mutation will remain, though.
os.environ["PATH"] = env["PATH"]
os.execvpe(argv[0], argv, env)
def slurp(self, **kwargs):
from ..slurp import Slurper
return Slurper(subproc_factory=self.launch, **kwargs)
def get_wrapout_wrapper(self, **kwargs):
from ..cli.wrapout import Wrapper
w = Wrapper(**kwargs)
w.slurp_factory = self.slurp
return w
[docs]
def prepend_path(orig, text, pathsep=os.pathsep):
"""Returns a $PATH-like environment variable with `text` prepended. `orig` is
the original variable value, or None. `pathsep` is the character
separating path elements, defaulting to `os.pathsep`.
Example:
newpath = cli.prepend_path(oldpath, '/mypackage/bin')
See also `prepend_environ_path`.
"""
if orig is None:
orig = ""
if not len(orig):
return text
return "".join([text, pathsep, orig])
[docs]
def prepend_environ_path(env, name, text, pathsep=os.pathsep):
"""Prepend `text` into a $PATH-like environment variable. `env` is a
dictionary of environment variables and `name` is the variable name.
`pathsep` is the character separating path elements, defaulting to
`os.pathsep`. The variable will be created if it is not already in `env`.
Returns `env`.
Example::
prepend_environ_path(env, 'PATH', '/mypackage/bin')
The `name` and `text` arguments should be `str` objects; that is, bytes in
Python 2 and Unicode in Python 3. Literal strings will be OK unless you
use the ``from __future__ import unicode_literals`` feature.
"""
env[name] = prepend_path(env.get(name), text, pathsep=pathsep)
return env
def _make_user_data_pather():
datadir = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
def pathfunc(*args):
return os.path.join(datadir, *args)
return pathfunc
user_data_path = _make_user_data_pather()
# Command-line access
class DefaultExecCommand(multitool.Command):
name = "exec"
argspec = "<command> [args...]"
summary = "Run a program in the environment."
def invoke(self, args, envclass=None, **kwargs):
if len(args) < 1:
raise multitool.UsageError("exec requires at least 1 argument")
envclass().execvpe(args)
class DefaultShellCommand(multitool.Command):
# XXX we hardcode bash!
name = "shell"
argspec = ""
summary = "Start an interactive shell in the environment."
help_if_no_args = False
def invoke(self, args, envname=None, envclass=None, **kwargs):
if len(args):
raise multitool.UsageError("shell expects no arguments")
from tempfile import NamedTemporaryFile
with NamedTemporaryFile(mode="wt", delete=False) as f:
print(
"""[ -e ~/.bashrc ] && source ~/.bashrc
PS1="%s $PS1"
rm %s"""
% (envname, f.name),
file=f,
)
envclass().execvpe(["bash", "--rcfile", f.name, "-i"])
class DefaultTool(multitool.Multitool):
def __init__(self, envname, envclass, module):
super(DefaultTool, self).__init__()
self.envname = envname
self.envclass = envclass
self.module = module
self.cli_name = "pkenvtool " + envname
self.summary = "Run tools in the %s environment." % envname
def invoke_command(self, cmd, args, **kwargs):
return super(DefaultTool, self).invoke_command(
cmd,
args,
envname=self.envname,
envclass=self.envclass,
module=self.module,
**kwargs
)
def _default_env_commandline(envname, module, argv):
for name in dir(module):
v = getattr(module, name)
if v is not Environment and issubclass(v, Environment):
envclass = v
break
else:
cli.die("internal error: cannot identify environment class for %s", envname)
tool = DefaultTool(envname, envclass, module)
tool.populate(globals().values())
tool.commandline(argv)
def commandline(argv=sys.argv):
cli.propagate_sigint()
cli.backtrace_on_usr1()
cli.unicode_stdio()
if len(argv) < 2 or argv[1] in ("-h", "--help"):
print(
"""usage: pkenvtool <environment> [args...]
Where acceptable "args" depend on the environment in question."""
)
return
envname = argv[1]
if not len(envname) or envname[0] == ".":
cli.die("illegal environment name %r", envname)
from importlib import import_module
try:
envmod = import_module("." + envname, package=__package__)
except Exception:
cli.die("unable to load module for environment %r", envname)
modargv = ["pkenvtool " + argv[1]] + argv[2:]
if hasattr(envmod, "commandline"):
envmod.commandline(modargv)
else:
_default_env_commandline(envname, envmod, modargv)