Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support a --venv mode similar to --unzip mode. #1153

Merged
merged 6 commits into from
Dec 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 78 additions & 10 deletions pex/bin/pex.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
import os
import sys
import tempfile
import zipfile
from argparse import Action, ArgumentDefaultsHelpFormatter, ArgumentParser, ArgumentTypeError
from shlex import shlex
from textwrap import TextWrapper

from pex import pex_warnings
from pex.common import die, safe_delete, safe_mkdtemp
from pex.common import atomic_directory, die, open_zip, safe_mkdtemp
from pex.inherit_path import InheritPath
from pex.interpreter import PythonInterpreter
from pex.interpreter_constraints import (
Expand All @@ -27,18 +27,19 @@
from pex.network_configuration import NetworkConfiguration
from pex.orderedset import OrderedSet
from pex.pex import PEX
from pex.pex_bootstrapper import iter_compatible_interpreters
from pex.pex_bootstrapper import ensure_venv, iter_compatible_interpreters
from pex.pex_builder import PEXBuilder
from pex.pip import ResolverVersion
from pex.platforms import Platform
from pex.resolver import Unsatisfiable, parsed_platform, resolve_multi
from pex.tracer import TRACER
from pex.typing import TYPE_CHECKING
from pex.variables import ENV, Variables
from pex.venv_bin_path import BinPath
from pex.version import __version__

if TYPE_CHECKING:
from typing import List
from typing import List, Iterable
from argparse import Namespace


Expand Down Expand Up @@ -88,6 +89,17 @@ def __call__(self, parser, namespace, value, option_str=None):
setattr(namespace, self.dest, option_str == "--transitive")


class HandleVenvAction(Action):
def __init__(self, *args, **kwargs):
kwargs["nargs"] = "?"
kwargs["choices"] = (BinPath.PREPEND.value, BinPath.APPEND.value)
super(HandleVenvAction, self).__init__(*args, **kwargs)

def __call__(self, parser, namespace, value, option_str=None):
bin_path = BinPath.FALSE if value is None else BinPath.for_value(value)
setattr(namespace, self.dest, bin_path)


class PrintVariableHelpAction(Action):
def __call__(self, parser, namespace, values, option_str=None):
for variable_name, variable_type, variable_help in Variables.iter_help():
Expand Down Expand Up @@ -326,7 +338,8 @@ def configure_clp_pex_options(parser):
"complete pex file, including dependencies, to be unzipped.",
)

group.add_argument(
runtime_mode = group.add_mutually_exclusive_group()
runtime_mode.add_argument(
"--unzip",
"--no-unzip",
dest="unzip",
Expand All @@ -336,6 +349,18 @@ def configure_clp_pex_options(parser):
"be run multiple times under a stable runtime PEX_ROOT the unzipping will only be "
"performed once and subsequent runs will enjoy lower startup latency.",
)
runtime_mode.add_argument(
"--venv",
dest="venv",
metavar="{prepend,append}",
default=False,
action=HandleVenvAction,
help="Convert the pex file to a venv before executing it. If 'prepend' or 'append' is "
"specified, then all scripts and console scripts provided by distributions in the pex file "
"will be added to the PATH in the corresponding position. If the the pex file will be run "
"multiple times under a stable runtime PEX_ROOT, the venv creation will only be done once "
"and subsequent runs will enjoy lower startup latency.",
)

group.add_argument(
"--always-write-cache",
Expand Down Expand Up @@ -712,6 +737,16 @@ def configure_clp():
help="Specify the temporary directory Pex and its subprocesses should use.",
)

parser.add_argument(
"--seed",
"--no-seed",
dest="seed",
action=HandleBoolAction,
default=False,
help="Seed local Pex caches for the generated PEX and print out the command line to run "
"directly from the seed with.",
)

parser.add_argument(
"--help-variables",
action=PrintVariableHelpAction,
Expand Down Expand Up @@ -824,7 +859,7 @@ def to_python_interpreter(full_path_or_basename):
path=safe_mkdtemp(),
interpreter=interpreter,
preamble=preamble,
include_tools=options.include_tools,
include_tools=options.include_tools or options.venv,
)

if options.resources_directory:
Expand All @@ -844,6 +879,8 @@ def to_python_interpreter(full_path_or_basename):
pex_info = pex_builder.info
pex_info.zip_safe = options.zip_safe
pex_info.unzip = options.unzip
pex_info.venv = bool(options.venv)
pex_info.venv_bin_path = options.venv
pex_info.pex_path = options.pex_path
pex_info.always_write_cache = options.always_write_cache
pex_info.ignore_errors = options.ignore_errors
Expand Down Expand Up @@ -1005,14 +1042,14 @@ def warn_ignore_pex_root(set_via):

if options.pex_name is not None:
log("Saving PEX file to %s" % options.pex_name, V=options.verbosity)
tmp_name = options.pex_name + "~"
safe_delete(tmp_name)
pex_builder.build(
tmp_name,
options.pex_name,
bytecode_compile=options.compile,
deterministic_timestamp=not options.use_system_time,
)
os.rename(tmp_name, options.pex_name)
if options.seed:
jsirois marked this conversation as resolved.
Show resolved Hide resolved
execute_cached_args = seed_cache(options, pex)
print(" ".join(execute_cached_args))
else:
if not _compatible_with_current_platform(interpreter, options.platforms):
log("WARNING: attempting to run PEX with incompatible platforms!", V=1)
Expand All @@ -1030,5 +1067,36 @@ def warn_ignore_pex_root(set_via):
sys.exit(pex.run(args=list(cmdline), env=patched_env))


def seed_cache(
options, # type: Namespace
pex, # type: PEX
):
# type: (...) -> Iterable[str]
pex_path = pex.path()
with TRACER.timed("Seeding local caches for {}".format(pex_path)):
if options.unzip:
unzip_dir = pex.pex_info().unzip_dir
if unzip_dir is None:
raise AssertionError(
"Expected PEX-INFO for {} to have the components of an unzip directory".format(
pex_path
)
)
with atomic_directory(unzip_dir, exclusive=True) as chroot:
if chroot:
with TRACER.timed("Extracting {}".format(pex_path)):
with open_zip(options.pex_name) as pex_zip:
pex_zip.extractall(chroot)
return [pex.interpreter.binary, unzip_dir]
elif options.venv:
with TRACER.timed("Creating venv from {}".format(pex_path)):
venv_pex = ensure_venv(pex)
return [venv_pex]
else:
with TRACER.timed("Extracting code and distributions for {}".format(pex_path)):
pex.activate()
return [os.path.abspath(options.pex_name)]


if __name__ == "__main__":
main()
18 changes: 17 additions & 1 deletion pex/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from pex.typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any, DefaultDict, Iterable, Iterator, NoReturn, Optional, Set
from typing import Any, DefaultDict, Iterable, Iterator, NoReturn, Optional, Set, Sized

# We use the start of MS-DOS time, which is what zipfiles use (see section 4.4.6 of
# https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT).
Expand Down Expand Up @@ -60,6 +60,22 @@ def die(msg, exit_code=1):
sys.exit(exit_code)


def pluralize(
subject, # type: Sized
noun, # type: str
):
# type: (...) -> str
if noun == "":
return ""
count = len(subject)
if count == 1:
return noun
if noun[-1] in ("s", "x", "z") or noun[-2:] in ("sh", "ch"):
return noun + "es"
else:
return noun + "s"


def safe_copy(source, dest, overwrite=False):
# type: (str, str, bool) -> None
def do_copy():
Expand Down
11 changes: 9 additions & 2 deletions pex/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from pex.util import CacheHelper, DistributionHelper

if TYPE_CHECKING:
from typing import Container, Optional
from typing import Container, Iterator, Optional, Tuple, Iterable


def _import_pkg_resources():
Expand Down Expand Up @@ -114,7 +114,7 @@ def explode_code(
dest_dir, # type: str
exclude=(), # type: Container[str]
):
# type: (...) -> None
# type: (...) -> Iterable[Tuple[str, str]]
with TRACER.timed("Unzipping {}".format(pex_file)):
with open_zip(pex_file) as pex_zip:
pex_files = (
Expand All @@ -125,6 +125,13 @@ def explode_code(
and name not in exclude
)
pex_zip.extractall(dest_dir, pex_files)
return [
(
"{pex_file}:{zip_path}".format(pex_file=pex_file, zip_path=f),
os.path.join(dest_dir, f),
)
for f in pex_files
]

@classmethod
def _force_local(cls, pex_file, pex_info):
Expand Down
2 changes: 1 addition & 1 deletion pex/inherit_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pex.typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Tuple, Union
from typing import Union


class InheritPath(object):
Expand Down
6 changes: 5 additions & 1 deletion pex/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,11 @@ def create_interpreter(stdout, check_binary=False):
# would otherwise be unstable.
#
# See cls._REGEXEN for a related affordance.
path_id = binary.replace(os.sep, ".").lstrip(".")
#
# N.B.: The path for --venv mode interpreters can be quite long; so we just used a fixed
# length hash of the interpreter binary path to ensure uniqueness and not run afoul of file
# name length limits.
path_id = hashlib.sha1(binary.encode("utf-8")).hexdigest()

cache_dir = os.path.join(os_cache_dir, interpreter_hash, path_id)
cache_file = os.path.join(cache_dir, cls.INTERP_INFO_FILE)
Expand Down
46 changes: 42 additions & 4 deletions pex/pex_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import sys

from pex import pex_warnings
from pex.common import die
from pex.common import atomic_directory, die
from pex.inherit_path import InheritPath
from pex.interpreter import PythonInterpreter
from pex.interpreter_constraints import UnsatisfiableInterpreterConstraintsError
Expand All @@ -29,6 +29,8 @@
Callable,
)

from pex.pex import PEX

InterpreterIdentificationError = Tuple[str, str]
InterpreterOrError = Union[PythonInterpreter, InterpreterIdentificationError]
PathFilter = Callable[[str], bool]
Expand Down Expand Up @@ -361,15 +363,51 @@ def _bootstrap(entry_point):
return pex_info


def ensure_venv(pex):
# type: (PEX) -> str
pex_info = pex.pex_info()
venv_dir = pex_info.venv_dir
if venv_dir is None:
raise AssertionError(
"Expected PEX-INFO for {} to have the components of a venv directory".format(pex.path())
)
with atomic_directory(venv_dir, exclusive=True) as venv:
if venv:
from .tools.commands.venv import populate_venv_with_pex
from .tools.commands.virtualenv import Virtualenv

virtualenv = Virtualenv.create(venv_dir=venv, interpreter=pex.interpreter)
populate_venv_with_pex(
virtualenv,
pex,
bin_path=pex_info.venv_bin_path,
python=os.path.join(venv_dir, "bin", os.path.basename(pex.interpreter.binary)),
collisions_ok=True,
)
return os.path.join(venv_dir, "pex")


# NB: This helper is used by the PEX bootstrap __main__.py code.
def bootstrap_pex(entry_point):
# type: (str) -> None
pex_info = _bootstrap(entry_point)
maybe_reexec_pex(pex_info.interpreter_constraints)

from . import pex
if not ENV.PEX_TOOLS and pex_info.venv:
try:
target = find_compatible_interpreter(
interpreter_constraints=pex_info.interpreter_constraints,
)
except UnsatisfiableInterpreterConstraintsError as e:
die(str(e))
from . import pex

venv_pex = ensure_venv(pex.PEX(entry_point, interpreter=target))
os.execv(venv_pex, [venv_pex] + sys.argv[1:])
else:
maybe_reexec_pex(pex_info.interpreter_constraints)
from . import pex

pex.PEX(entry_point).execute()
pex.PEX(entry_point).execute()


# NB: This helper is used by third party libs - namely https://github.com/wickman/lambdex.
Expand Down
Loading