juc500 정상동작
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,295 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
from abc import ABC, abstractmethod
|
||||
from argparse import ArgumentTypeError
|
||||
from ast import literal_eval
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from argparse import ArgumentParser
|
||||
from typing import Any, NoReturn
|
||||
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
from virtualenv.app_data.base import AppData
|
||||
from virtualenv.config.cli.parser import VirtualEnvOptions
|
||||
|
||||
from os.path import commonpath
|
||||
|
||||
from virtualenv.util.path import safe_delete
|
||||
from virtualenv.util.subprocess import LogCmd, run_cmd
|
||||
from virtualenv.version import __version__
|
||||
|
||||
from .pyenv_cfg import PyEnvCfg
|
||||
|
||||
HERE = Path(os.path.abspath(__file__)).parent
|
||||
DEBUG_SCRIPT = HERE / "debug.py"
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CreatorMeta:
|
||||
def __init__(self) -> None:
|
||||
self.error = None
|
||||
|
||||
|
||||
class Creator(ABC):
|
||||
"""A class that given a python Interpreter creates a virtual environment."""
|
||||
|
||||
def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None:
|
||||
"""Construct a new virtual environment creator.
|
||||
|
||||
:param options: the CLI option as parsed from :meth:`add_parser_arguments`
|
||||
:param interpreter: the interpreter to create virtual environment from
|
||||
|
||||
"""
|
||||
self.interpreter = interpreter
|
||||
self._debug = None
|
||||
self.dest = Path(options.dest)
|
||||
self.clear = options.clear
|
||||
self.no_vcs_ignore = options.no_vcs_ignore
|
||||
self.pyenv_cfg = PyEnvCfg.from_folder(self.dest)
|
||||
self.app_data = options.app_data
|
||||
self.env = options.env
|
||||
self.prompt = getattr(options, "prompt", None)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@property
|
||||
def exe(self) -> Path: ...
|
||||
|
||||
@property
|
||||
def env_name(self) -> str: ...
|
||||
|
||||
@property
|
||||
def bin_dir(self) -> Path: ...
|
||||
|
||||
@property
|
||||
def script_dir(self) -> Path: ...
|
||||
|
||||
@property
|
||||
def libs(self) -> list[Path]: ...
|
||||
|
||||
@property
|
||||
def purelib(self) -> Path: ...
|
||||
|
||||
@property
|
||||
def platlib(self) -> Path: ...
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in self._args())})"
|
||||
|
||||
def _args(self) -> list[tuple[str, Any]]:
|
||||
return [
|
||||
("dest", str(self.dest)),
|
||||
("clear", self.clear),
|
||||
("no_vcs_ignore", self.no_vcs_ignore),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def can_create(cls, interpreter: PythonInfo) -> CreatorMeta | bool | None: # noqa: ARG003
|
||||
"""Determine if we can create a virtual environment.
|
||||
|
||||
:param interpreter: the interpreter in question
|
||||
|
||||
:returns: ``None`` if we can't create, any other object otherwise that will be forwarded to
|
||||
:meth:`add_parser_arguments`
|
||||
|
||||
"""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(
|
||||
cls,
|
||||
parser: ArgumentParser,
|
||||
interpreter: PythonInfo, # noqa: ARG003
|
||||
meta: CreatorMeta, # noqa: ARG003
|
||||
app_data: AppData, # noqa: ARG003
|
||||
) -> None:
|
||||
"""Add CLI arguments for the creator.
|
||||
|
||||
:param parser: the CLI parser
|
||||
:param app_data: the application data folder
|
||||
:param interpreter: the interpreter we're asked to create virtual environment for
|
||||
:param meta: value as returned by :meth:`can_create`
|
||||
|
||||
"""
|
||||
parser.add_argument(
|
||||
"dest",
|
||||
help="directory to create virtualenv at",
|
||||
type=cls.validate_dest,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--clear",
|
||||
dest="clear",
|
||||
action="store_true",
|
||||
help="remove the destination directory if exist before starting (will overwrite files otherwise)",
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-vcs-ignore",
|
||||
dest="no_vcs_ignore",
|
||||
action="store_true",
|
||||
help="don't create VCS ignore directive in the destination directory",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def create(self) -> None:
|
||||
"""Perform the virtual environment creation."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def validate_dest(cls, raw_value: str) -> str: # noqa: C901
|
||||
"""No path separator in the path, valid chars and must be write-able."""
|
||||
|
||||
def non_write_able(dest: Path, value: Path) -> NoReturn:
|
||||
common = Path(commonpath([str(value), str(dest)]))
|
||||
msg = f"the destination {dest.relative_to(common)} is not write-able at {common}"
|
||||
raise ArgumentTypeError(msg)
|
||||
|
||||
# the file system must be able to encode
|
||||
# note in newer CPython this is always utf-8 https://www.python.org/dev/peps/pep-0529/
|
||||
encoding = sys.getfilesystemencoding()
|
||||
refused = OrderedDict()
|
||||
kwargs = {"errors": "ignore"} if encoding != "mbcs" else {}
|
||||
for char in str(raw_value):
|
||||
try:
|
||||
trip = char.encode(encoding, **kwargs).decode(encoding)
|
||||
if trip == char:
|
||||
continue
|
||||
raise ValueError(trip) # noqa: TRY301
|
||||
except ValueError:
|
||||
refused[char] = None
|
||||
if refused:
|
||||
bad = "".join(refused.keys())
|
||||
msg = f"the file system codec ({encoding}) cannot handle characters {bad!r} within {raw_value!r}"
|
||||
raise ArgumentTypeError(msg)
|
||||
if os.pathsep in raw_value:
|
||||
msg = (
|
||||
f"destination {raw_value!r} must not contain the path separator ({os.pathsep})"
|
||||
f" as this would break the activation scripts"
|
||||
)
|
||||
raise ArgumentTypeError(msg)
|
||||
|
||||
value = Path(raw_value)
|
||||
if value.exists() and value.is_file():
|
||||
msg = f"the destination {value} already exists and is a file"
|
||||
raise ArgumentTypeError(msg)
|
||||
dest = Path(os.path.abspath(str(value))).resolve() # on Windows absolute does not imply resolve so use both
|
||||
value = dest
|
||||
while dest:
|
||||
if dest.exists():
|
||||
if os.access(str(dest), os.W_OK):
|
||||
break
|
||||
non_write_able(dest, value)
|
||||
base, _ = dest.parent, dest.name
|
||||
if base == dest:
|
||||
non_write_able(dest, value) # pragma: no cover
|
||||
dest = base
|
||||
return str(value)
|
||||
|
||||
def run(self) -> None:
|
||||
if self.dest.exists() and self.clear:
|
||||
LOGGER.debug("delete %s", self.dest)
|
||||
safe_delete(self.dest)
|
||||
self.create()
|
||||
self.add_cachedir_tag()
|
||||
self.set_pyenv_cfg()
|
||||
if not self.no_vcs_ignore:
|
||||
self.setup_ignore_vcs()
|
||||
|
||||
def add_cachedir_tag(self) -> None:
|
||||
"""Generate a file indicating that this is not meant to be backed up."""
|
||||
cachedir_tag_file = self.dest / "CACHEDIR.TAG"
|
||||
if not cachedir_tag_file.exists():
|
||||
cachedir_tag_text = textwrap.dedent("""
|
||||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by Python virtualenv.
|
||||
# For information about cache directory tags, see:
|
||||
# https://bford.info/cachedir/
|
||||
""").strip()
|
||||
cachedir_tag_file.write_text(cachedir_tag_text, encoding="utf-8")
|
||||
|
||||
def set_pyenv_cfg(self) -> None:
|
||||
self.pyenv_cfg.content = OrderedDict()
|
||||
system_executable = self.interpreter.system_executable or self.interpreter.executable
|
||||
assert system_executable is not None # noqa: S101
|
||||
self.pyenv_cfg["home"] = os.path.dirname(os.path.abspath(system_executable))
|
||||
self.pyenv_cfg["implementation"] = self.interpreter.implementation
|
||||
self.pyenv_cfg["version_info"] = ".".join(str(i) for i in self.interpreter.version_info)
|
||||
self.pyenv_cfg["version"] = ".".join(str(i) for i in self.interpreter.version_info[:3])
|
||||
self.pyenv_cfg["executable"] = os.path.realpath(system_executable)
|
||||
self.pyenv_cfg["command"] = f"{sys.executable} -m virtualenv {self.dest}"
|
||||
self.pyenv_cfg["virtualenv"] = __version__
|
||||
if self.prompt is not None:
|
||||
prompt_value = os.path.basename(os.getcwd()) if self.prompt == "." else self.prompt
|
||||
self.pyenv_cfg["prompt"] = prompt_value
|
||||
|
||||
def setup_ignore_vcs(self) -> None:
|
||||
"""Generate ignore instructions for version control systems."""
|
||||
# mark this folder to be ignored by VCS, handle https://www.python.org/dev/peps/pep-0610/#registered-vcs
|
||||
git_ignore = self.dest / ".gitignore"
|
||||
if not git_ignore.exists():
|
||||
git_ignore.write_text("# created by virtualenv automatically\n*\n", encoding="utf-8")
|
||||
# Mercurial - does not support the .hgignore file inside a subdirectory directly, but only if included via the
|
||||
# subinclude directive from root, at which point on might as well ignore the directory itself, see
|
||||
# https://www.selenic.com/mercurial/hgignore.5.html for more details
|
||||
# Bazaar - does not support ignore files in sub-directories, only at root level via .bzrignore
|
||||
# Subversion - does not support ignore files, requires direct manipulation with the svn tool
|
||||
|
||||
@property
|
||||
def debug(self) -> dict[str, Any] | None:
|
||||
"""Debug information about the virtual environment (only valid after :meth:`create` has run)."""
|
||||
if self._debug is None and self.exe is not None:
|
||||
self._debug = get_env_debug_info(self.exe, self.debug_script(), self.app_data, self.env)
|
||||
return self._debug
|
||||
|
||||
@staticmethod
|
||||
def debug_script() -> Path:
|
||||
return DEBUG_SCRIPT
|
||||
|
||||
|
||||
def get_env_debug_info(env_exe: Path, debug_script: Path, app_data: AppData, env: dict[str, str]) -> dict[str, Any]:
|
||||
env = env.copy()
|
||||
env.pop("PYTHONPATH", None)
|
||||
|
||||
with app_data.ensure_extracted(debug_script) as debug_script_extracted:
|
||||
cmd = [str(env_exe), str(debug_script_extracted)]
|
||||
LOGGER.debug("debug via %r", LogCmd(cmd))
|
||||
code, out, err = run_cmd(cmd)
|
||||
|
||||
try:
|
||||
result = _parse_debug_output(code, out, err)
|
||||
except Exception as exception: # noqa: BLE001
|
||||
return {"out": out, "err": err, "returncode": code, "exception": repr(exception)}
|
||||
if "sys" in result and "path" in result["sys"]:
|
||||
del result["sys"]["path"][0]
|
||||
return result
|
||||
|
||||
|
||||
def _parse_debug_output(code: int, out: str, err: str) -> dict[str, Any]:
|
||||
if code != 0:
|
||||
if out:
|
||||
result = literal_eval(out)
|
||||
elif code == 2 and "file" in err: # noqa: PLR2004
|
||||
raise OSError(err)
|
||||
else:
|
||||
raise Exception(err) # noqa: TRY002
|
||||
else:
|
||||
result = json.loads(out)
|
||||
if err:
|
||||
result["err"] = err
|
||||
return result
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Creator",
|
||||
"CreatorMeta",
|
||||
]
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Inspect a target Python interpreter virtual environment wise."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys # built-in
|
||||
|
||||
|
||||
def encode_path(value: object) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, (str, bytes)):
|
||||
value = repr(value) if isinstance(value, type) else repr(type(value))
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode(sys.getfilesystemencoding())
|
||||
return value
|
||||
|
||||
|
||||
def encode_list_path(value: list[object]) -> list[str | None]:
|
||||
return [encode_path(i) for i in value]
|
||||
|
||||
|
||||
def run() -> None:
|
||||
"""Print debug data about the virtual environment."""
|
||||
sys_info: dict[str, str | list[str | None] | None] = {}
|
||||
result: dict[str, str | dict[str, str | list[str | None] | None] | None] = {"sys": sys_info}
|
||||
path_keys = (
|
||||
"executable",
|
||||
"_base_executable",
|
||||
"prefix",
|
||||
"base_prefix",
|
||||
"exec_prefix",
|
||||
"base_exec_prefix",
|
||||
"path",
|
||||
"meta_path",
|
||||
)
|
||||
for key in path_keys:
|
||||
value = getattr(sys, key, None)
|
||||
value = encode_list_path(value) if isinstance(value, list) else encode_path(value)
|
||||
sys_info[key] = value
|
||||
sys_info["fs_encoding"] = sys.getfilesystemencoding()
|
||||
sys_info["io_encoding"] = getattr(sys.stdout, "encoding", None)
|
||||
result["version"] = sys.version
|
||||
|
||||
try:
|
||||
import sysconfig # noqa: PLC0415
|
||||
|
||||
result["makefile_filename"] = encode_path(sysconfig.get_makefile_filename())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import os # landmark # noqa: PLC0415
|
||||
|
||||
result["os"] = repr(os)
|
||||
|
||||
try:
|
||||
import site # site # noqa: PLC0415
|
||||
|
||||
result["site"] = repr(site)
|
||||
except ImportError as exception: # pragma: no cover
|
||||
result["site"] = repr(exception) # pragma: no cover
|
||||
|
||||
try:
|
||||
import datetime # site # noqa: PLC0415
|
||||
|
||||
result["datetime"] = repr(datetime)
|
||||
except ImportError as exception: # pragma: no cover
|
||||
result["datetime"] = repr(exception) # pragma: no cover
|
||||
|
||||
try:
|
||||
import math # site # noqa: PLC0415
|
||||
|
||||
result["math"] = repr(math)
|
||||
except ImportError as exception: # pragma: no cover
|
||||
result["math"] = repr(exception) # pragma: no cover
|
||||
|
||||
# try to print out, this will validate if other core modules are available (json in this case)
|
||||
try:
|
||||
import json # noqa: PLC0415
|
||||
|
||||
result["json"] = repr(json)
|
||||
except ImportError as exception:
|
||||
result["json"] = repr(exception)
|
||||
else:
|
||||
try:
|
||||
content = json.dumps(result, indent=2)
|
||||
sys.stdout.write(content)
|
||||
except (ValueError, TypeError) as exception: # pragma: no cover
|
||||
sys.stderr.write(repr(exception))
|
||||
sys.stdout.write(repr(result)) # pragma: no cover
|
||||
raise SystemExit(1) # noqa: B904 # pragma: no cover
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.info import IS_WIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
|
||||
class Describe:
|
||||
"""Given a host interpreter tell us information about what the created interpreter might look like."""
|
||||
|
||||
suffix = ".exe" if IS_WIN else ""
|
||||
|
||||
def __init__(self, dest: Path, interpreter: PythonInfo) -> None:
|
||||
self.interpreter = interpreter
|
||||
self.dest = dest
|
||||
self._stdlib = None
|
||||
self._stdlib_platform = None
|
||||
self._system_stdlib = None
|
||||
self._conf_vars = None
|
||||
|
||||
@property
|
||||
def bin_dir(self) -> Path:
|
||||
return self.script_dir
|
||||
|
||||
@property
|
||||
def script_dir(self) -> Path:
|
||||
return self.dest / self.interpreter.install_path("scripts")
|
||||
|
||||
@property
|
||||
def purelib(self) -> Path:
|
||||
return self.dest / self.interpreter.install_path("purelib")
|
||||
|
||||
@property
|
||||
def platlib(self) -> Path:
|
||||
return self.dest / self.interpreter.install_path("platlib")
|
||||
|
||||
@property
|
||||
def libs(self) -> list[Path]:
|
||||
return list(OrderedDict(((self.platlib, None), (self.purelib, None))).keys())
|
||||
|
||||
@property
|
||||
def stdlib(self) -> Path:
|
||||
if self._stdlib is None:
|
||||
self._stdlib = Path(self.interpreter.sysconfig_path("stdlib", config_var=self._config_vars))
|
||||
return self._stdlib
|
||||
|
||||
@property
|
||||
def stdlib_platform(self) -> Path:
|
||||
if self._stdlib_platform is None:
|
||||
self._stdlib_platform = Path(self.interpreter.sysconfig_path("platstdlib", config_var=self._config_vars))
|
||||
return self._stdlib_platform
|
||||
|
||||
@property
|
||||
def _config_vars(self) -> dict[str, Any]:
|
||||
if self._conf_vars is None:
|
||||
self._conf_vars = self._calc_config_vars(self.dest)
|
||||
return self._conf_vars
|
||||
|
||||
def _calc_config_vars(self, to: Path) -> dict[str, Any]:
|
||||
sys_vars = self.interpreter.sysconfig_vars
|
||||
return {
|
||||
k: (to if isinstance(v, str) and v.startswith(self.interpreter.prefix) else v) for k, v in sys_vars.items()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter: PythonInfo) -> bool: # noqa: ARG003
|
||||
"""Knows means it knows how the output will look."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def env_name(self) -> str:
|
||||
return self.dest.parts[-1]
|
||||
|
||||
@property
|
||||
def exe(self) -> Path:
|
||||
return self.bin_dir / f"{self.exe_stem()}{self.suffix}"
|
||||
|
||||
@classmethod
|
||||
def exe_stem(cls) -> str:
|
||||
"""Executable name without suffix - there seems to be no standard way to get this without creating it."""
|
||||
raise NotImplementedError
|
||||
|
||||
def script(self, name: str) -> Path:
|
||||
return self.script_dir / f"{name}{self.suffix}"
|
||||
|
||||
|
||||
class Python3Supports(Describe, ABC):
|
||||
pass
|
||||
|
||||
|
||||
class PosixSupports(Describe, ABC):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter: PythonInfo) -> bool:
|
||||
return interpreter.os == "posix" and super().can_describe(interpreter)
|
||||
|
||||
|
||||
class WindowsSupports(Describe, ABC):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter: PythonInfo) -> bool:
|
||||
return interpreter.os == "nt" and super().can_describe(interpreter)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Describe",
|
||||
"PosixSupports",
|
||||
"Python3Supports",
|
||||
"WindowsSupports",
|
||||
]
|
||||
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PyEnvCfg:
|
||||
def __init__(self, content: OrderedDict[str, str], path: Path) -> None:
|
||||
self.content = content
|
||||
self.path = path
|
||||
|
||||
@classmethod
|
||||
def from_folder(cls, folder: Path) -> PyEnvCfg:
|
||||
return cls.from_file(folder / "pyvenv.cfg")
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> PyEnvCfg:
|
||||
content = cls._read_values(path) if path.exists() else OrderedDict()
|
||||
return PyEnvCfg(content, path)
|
||||
|
||||
@staticmethod
|
||||
def _read_values(path: Path) -> OrderedDict[str, str]:
|
||||
content = OrderedDict()
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
equals_at = line.index("=")
|
||||
key = line[:equals_at].strip()
|
||||
value = line[equals_at + 1 :].strip()
|
||||
if len(value) > 1 and value[0] in {"'", '"'} and value[0] == value[-1]:
|
||||
value = value[1:-1]
|
||||
content[key] = value
|
||||
return content
|
||||
|
||||
def write(self) -> None:
|
||||
LOGGER.debug("write %s", self.path)
|
||||
text = ""
|
||||
for key, value in self.content.items():
|
||||
# Use abspath to normalize relative paths but preserve symlinks (match venv behavior)
|
||||
# See issue #2770 - realpath resolves symlinks which breaks prefix symlinks
|
||||
if key == "prompt" and value:
|
||||
normalized_value = f'"{value}"'
|
||||
else:
|
||||
normalized_value = os.path.abspath(value) if value and os.path.exists(value) else value
|
||||
line = f"{key} = {normalized_value}"
|
||||
LOGGER.debug("\t%s", line)
|
||||
text += line
|
||||
text += "\n"
|
||||
self.path.write_text(text, encoding="utf-8")
|
||||
|
||||
def refresh(self) -> OrderedDict[str, str]:
|
||||
self.content = self._read_values(self.path)
|
||||
return self.content
|
||||
|
||||
def __setitem__(self, key: str, value: str) -> None:
|
||||
self.content[key] = value
|
||||
|
||||
def __getitem__(self, key: str) -> str:
|
||||
return self.content[key]
|
||||
|
||||
def __contains__(self, item: str) -> bool:
|
||||
return item in self.content
|
||||
|
||||
def update(self, other: dict[str, str]) -> PyEnvCfg:
|
||||
self.content.update(other)
|
||||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(path={self.path})"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PyEnvCfg",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+114
@@ -0,0 +1,114 @@
|
||||
"""Patches that are applied at runtime to the virtual environment."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import types
|
||||
from collections.abc import Callable
|
||||
from importlib.machinery import ModuleSpec
|
||||
|
||||
VIRTUALENV_PATCH_FILE = os.path.abspath(__file__)
|
||||
|
||||
|
||||
def patch_dist(dist: types.ModuleType) -> None:
|
||||
"""Distutils allows user to configure some arguments via a configuration file: https://docs.python.org/3/install/index.html#distutils-configuration-files.
|
||||
|
||||
Some of this arguments though don't make sense in context of the virtual environment files, let's fix them up.
|
||||
|
||||
"""
|
||||
# we cannot allow some install config as that would get packages installed outside of the virtual environment
|
||||
old_parse_config_files = dist.Distribution.parse_config_files
|
||||
|
||||
def parse_config_files(self, *args: object, **kwargs: object) -> object: # noqa: ANN001
|
||||
result = old_parse_config_files(self, *args, **kwargs)
|
||||
install = self.get_option_dict("install")
|
||||
|
||||
if "prefix" in install: # the prefix governs where to install the libraries
|
||||
install["prefix"] = VIRTUALENV_PATCH_FILE, os.path.abspath(sys.prefix)
|
||||
for base in ("purelib", "platlib", "headers", "scripts", "data"):
|
||||
key = f"install_{base}"
|
||||
if key in install: # do not allow global configs to hijack venv paths
|
||||
install.pop(key, None)
|
||||
return result
|
||||
|
||||
dist.Distribution.parse_config_files = parse_config_files
|
||||
|
||||
|
||||
# Import hook that patches some modules to ignore configuration values that break package installation in case
|
||||
# of virtual environments.
|
||||
_DISTUTILS_PATCH = "distutils.dist", "setuptools.dist"
|
||||
# https://docs.python.org/3/library/importlib.html#setting-up-an-importer
|
||||
|
||||
|
||||
class _Finder:
|
||||
"""A meta path finder that allows patching the imported distutils modules."""
|
||||
|
||||
fullname = None
|
||||
|
||||
# lock[0] is threading.Lock(), but initialized lazily to avoid importing threading very early at startup,
|
||||
# because there are gevent-based applications that need to be first to import threading by themselves.
|
||||
# See https://github.com/pypa/virtualenv/issues/1895 for details.
|
||||
lock = [] # noqa: RUF012
|
||||
|
||||
def find_spec(self, fullname: str, path: object, target: object = None) -> ModuleSpec | None: # noqa: ARG002
|
||||
# Guard against race conditions during file rewrite by checking if _DISTUTILS_PATCH is defined.
|
||||
# This can happen when the file is being overwritten while it's being imported by another process.
|
||||
# See https://github.com/pypa/virtualenv/issues/2969 for details.
|
||||
try:
|
||||
distutils_patch = _DISTUTILS_PATCH
|
||||
except NameError:
|
||||
return None
|
||||
if fullname in distutils_patch and self.fullname is None:
|
||||
# initialize lock[0] lazily
|
||||
if len(self.lock) == 0:
|
||||
import threading # noqa: PLC0415
|
||||
|
||||
lock = threading.Lock()
|
||||
# there is possibility that two threads T1 and T2 are simultaneously running into find_spec,
|
||||
# observing .lock as empty, and further going into hereby initialization. However due to the GIL,
|
||||
# list.append() operation is atomic and this way only one of the threads will "win" to put the lock
|
||||
# - that every thread will use - into .lock[0].
|
||||
# https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe
|
||||
self.lock.append(lock)
|
||||
|
||||
from functools import partial # noqa: PLC0415
|
||||
from importlib.util import find_spec # noqa: PLC0415
|
||||
|
||||
with self.lock[0]:
|
||||
self.fullname = fullname
|
||||
spec = None
|
||||
try:
|
||||
spec = find_spec(fullname, path) # ty: ignore[invalid-argument-type]
|
||||
finally:
|
||||
self.fullname = None
|
||||
if spec is not None:
|
||||
return self._patch_spec(spec, partial)
|
||||
return None
|
||||
|
||||
def _patch_spec(self, spec: ModuleSpec, partial: Callable[..., object]) -> ModuleSpec:
|
||||
old = getattr(spec.loader, "exec_module", None)
|
||||
if old is not None and old is not self.exec_module:
|
||||
try: # noqa: SIM105
|
||||
spec.loader.exec_module = partial(self.exec_module, old) # ty: ignore[invalid-assignment]
|
||||
except AttributeError:
|
||||
pass
|
||||
return spec
|
||||
|
||||
@staticmethod
|
||||
def exec_module(old: Callable[..., object], module: types.ModuleType) -> None:
|
||||
old(module)
|
||||
try:
|
||||
distutils_patch = _DISTUTILS_PATCH
|
||||
except NameError:
|
||||
return
|
||||
if module.__name__ in distutils_patch:
|
||||
with contextlib.suppress(NameError):
|
||||
patch_dist(module)
|
||||
|
||||
|
||||
sys.meta_path.insert(0, _Finder())
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from abc import ABC
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.create.creator import Creator, CreatorMeta
|
||||
from virtualenv.info import fs_supports_symlink
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from argparse import ArgumentParser
|
||||
from typing import Any
|
||||
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
from virtualenv.app_data.base import AppData
|
||||
from virtualenv.config.cli.parser import VirtualEnvOptions
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ViaGlobalRefMeta(CreatorMeta):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.copy_error = None
|
||||
self.symlink_error = None
|
||||
if not fs_supports_symlink():
|
||||
self.symlink_error = "the filesystem does not supports symlink"
|
||||
|
||||
@property
|
||||
def can_copy(self) -> bool:
|
||||
return not self.copy_error
|
||||
|
||||
@property
|
||||
def can_symlink(self) -> bool:
|
||||
return not self.symlink_error
|
||||
|
||||
|
||||
class ViaGlobalRefApi(Creator, ABC):
|
||||
def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None:
|
||||
super().__init__(options, interpreter)
|
||||
self.symlinks = self._should_symlink(options)
|
||||
self.enable_system_site_package = options.system_site
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@property
|
||||
def purelib(self) -> Path: ...
|
||||
|
||||
@property
|
||||
def script_dir(self) -> Path: ...
|
||||
|
||||
@staticmethod
|
||||
def _should_symlink(options: VirtualEnvOptions) -> bool:
|
||||
# Priority of where the option is set to follow the order: CLI, env var, file, hardcoded.
|
||||
# If both set at same level prefers copy over symlink.
|
||||
copies, symlinks = getattr(options, "copies", False), getattr(options, "symlinks", False)
|
||||
copy_src, sym_src = options.get_source("copies"), options.get_source("symlinks")
|
||||
for level in ["cli", "env var", "file", "default"]:
|
||||
s_opt = symlinks if sym_src == level else None
|
||||
c_opt = copies if copy_src == level else None
|
||||
if s_opt is True and c_opt is True:
|
||||
return False
|
||||
if s_opt is True:
|
||||
return True
|
||||
if c_opt is True:
|
||||
return False
|
||||
return False # fallback to copy
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(
|
||||
cls, parser: ArgumentParser, interpreter: PythonInfo, meta: ViaGlobalRefMeta, app_data: AppData
|
||||
) -> None: # ty: ignore[invalid-method-override]
|
||||
super().add_parser_arguments(parser, interpreter, meta, app_data)
|
||||
parser.add_argument(
|
||||
"--system-site-packages",
|
||||
default=False,
|
||||
action="store_true",
|
||||
dest="system_site",
|
||||
help="give the virtual environment access to the system site-packages dir",
|
||||
)
|
||||
if not meta.can_symlink and not meta.can_copy:
|
||||
errors = []
|
||||
if meta.symlink_error:
|
||||
errors.append(f"symlink: {meta.symlink_error}")
|
||||
if meta.copy_error:
|
||||
errors.append(f"copy: {meta.copy_error}")
|
||||
msg = f"neither symlink or copy method supported: {', '.join(errors)}"
|
||||
raise RuntimeError(msg)
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
if meta.can_symlink:
|
||||
group.add_argument(
|
||||
"--symlinks",
|
||||
default=True,
|
||||
action="store_true",
|
||||
dest="symlinks",
|
||||
help="try to use symlinks rather than copies, when symlinks are not the default for the platform",
|
||||
)
|
||||
if meta.can_copy:
|
||||
group.add_argument(
|
||||
"--copies",
|
||||
"--always-copy",
|
||||
default=not meta.can_symlink,
|
||||
action="store_true",
|
||||
dest="copies",
|
||||
help="try to use copies rather than symlinks, even when symlinks are the default for the platform",
|
||||
)
|
||||
|
||||
def create(self) -> None:
|
||||
self.install_patch()
|
||||
|
||||
def install_patch(self) -> None:
|
||||
# Python 3.10+ ignores the distutils install config keys this patch guards against (pip does so by default,
|
||||
# setuptools and CPython's vendored distutils drop them too), so the runtime import hook only earns its
|
||||
# startup cost on 3.9. See https://github.com/pypa/virtualenv/issues/3181.
|
||||
if self.interpreter.version_info >= (3, 10):
|
||||
return
|
||||
text = self.env_patch_text()
|
||||
if text:
|
||||
pth = self.purelib / "_virtualenv.pth"
|
||||
LOGGER.debug("create virtualenv import hook file %s", pth)
|
||||
pth.write_text("import _virtualenv", encoding="utf-8")
|
||||
dest_path = self.purelib / "_virtualenv.py"
|
||||
LOGGER.debug("create %s", dest_path)
|
||||
dest_path.write_text(text, encoding="utf-8")
|
||||
|
||||
def env_patch_text(self) -> str:
|
||||
"""Patch the distutils package to not be derailed by its configuration files."""
|
||||
with self.app_data.ensure_extracted(Path(__file__).parent / "_virtualenv.py") as resolved_path:
|
||||
return resolved_path.read_text(encoding="utf-8")
|
||||
|
||||
def _args(self) -> list[tuple[str, Any]]:
|
||||
return [*super()._args(), ("global", self.enable_system_site_package)]
|
||||
|
||||
def set_pyenv_cfg(self) -> None:
|
||||
super().set_pyenv_cfg()
|
||||
self.pyenv_cfg["include-system-site-packages"] = "true" if self.enable_system_site_package else "false"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ViaGlobalRefApi",
|
||||
"ViaGlobalRefMeta",
|
||||
]
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.create.creator import Creator
|
||||
from virtualenv.create.describe import Describe
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
from virtualenv.config.cli.parser import VirtualEnvOptions
|
||||
|
||||
|
||||
class VirtualenvBuiltin(Creator, Describe, ABC):
|
||||
"""A creator that does operations itself without delegation, if we can create it we can also describe it."""
|
||||
|
||||
def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None:
|
||||
Creator.__init__(self, options, interpreter)
|
||||
Describe.__init__(self, self.dest, interpreter)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"VirtualenvBuiltin",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+99
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from abc import ABC
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.create.describe import PosixSupports, WindowsSupports
|
||||
from virtualenv.create.via_global_ref.builtin.ref import RefMust, RefWhen
|
||||
from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
|
||||
class CPython(ViaGlobalRefVirtualenvBuiltin, ABC):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter: PythonInfo) -> bool:
|
||||
return interpreter.implementation == "CPython" and super().can_describe(interpreter)
|
||||
|
||||
@classmethod
|
||||
def exe_stem(cls) -> str:
|
||||
return "python"
|
||||
|
||||
|
||||
class CPythonPosix(CPython, PosixSupports, ABC):
|
||||
"""Create a CPython virtual environment on POSIX platforms."""
|
||||
|
||||
@classmethod
|
||||
def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], str, str]]:
|
||||
host_exe = Path(interpreter.system_executable) # ty: ignore[invalid-argument-type]
|
||||
minor = interpreter.version_info.minor
|
||||
names = [
|
||||
"python",
|
||||
"python3",
|
||||
f"python3.{minor}",
|
||||
*((f"python3.{minor}t",) if interpreter.free_threaded else ()),
|
||||
host_exe.name,
|
||||
]
|
||||
targets = OrderedDict((i, None) for i in names)
|
||||
yield host_exe, list(targets.keys()), RefMust.NA, RefWhen.ANY
|
||||
|
||||
|
||||
class CPythonWindows(CPython, WindowsSupports, ABC):
|
||||
@classmethod
|
||||
def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], str, str]]:
|
||||
# symlink of the python executables does not work reliably, copy always instead
|
||||
# - https://bugs.python.org/issue42013
|
||||
# - venv
|
||||
host = cls.host_python(interpreter)
|
||||
minor = interpreter.version_info.minor
|
||||
names = {
|
||||
"python.exe",
|
||||
"python3.exe",
|
||||
"python3",
|
||||
host.name,
|
||||
*((f"python3.{minor}t.exe",) if interpreter.free_threaded else ()),
|
||||
}
|
||||
for path in (host.parent / n for n in names):
|
||||
yield host, [path.name], RefMust.COPY, RefWhen.ANY
|
||||
# for more info on pythonw.exe see https://stackoverflow.com/a/30313091
|
||||
python_w = host.parent / "pythonw.exe"
|
||||
yield (
|
||||
python_w,
|
||||
[python_w.name, "pythonw3.exe", *((f"pythonw3.{minor}t.exe",) if interpreter.free_threaded else ())],
|
||||
RefMust.COPY,
|
||||
RefWhen.ANY,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def host_python(cls, interpreter: PythonInfo) -> Path:
|
||||
return Path(interpreter.system_executable) # ty: ignore[invalid-argument-type]
|
||||
|
||||
|
||||
def is_mac_os_framework(interpreter: PythonInfo) -> bool:
|
||||
if interpreter.platform == "darwin":
|
||||
return interpreter.sysconfig_vars.get("PYTHONFRAMEWORK") == "Python3"
|
||||
return False
|
||||
|
||||
|
||||
def is_macos_brew(interpreter: PythonInfo) -> bool:
|
||||
return interpreter.platform == "darwin" and _BREW.fullmatch(interpreter.system_prefix) is not None
|
||||
|
||||
|
||||
_BREW = re.compile(
|
||||
r"/(usr/local|opt/homebrew)/(opt/python@3\.\d{1,2}|Cellar/python@3\.\d{1,2}/3\.\d{1,2}\.\d{1,2})/Frameworks/"
|
||||
r"Python\.framework/Versions/3\.\d{1,2}",
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CPython",
|
||||
"CPythonPosix",
|
||||
"CPythonWindows",
|
||||
"is_mac_os_framework",
|
||||
"is_macos_brew",
|
||||
]
|
||||
vendor/python-windows/Lib/site-packages/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py
Vendored
+202
@@ -0,0 +1,202 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import fnmatch
|
||||
from operator import methodcaller as method
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.create.describe import Python3Supports
|
||||
from virtualenv.create.via_global_ref.builtin.ref import ExePathRefToDest, PathRefToDest, RefWhen
|
||||
from virtualenv.create.via_global_ref.store import is_store_python
|
||||
from virtualenv.util.path import copy as copy_path
|
||||
from virtualenv.util.path import ensure_dir
|
||||
|
||||
from .common import CPython, CPythonPosix, CPythonWindows, is_mac_os_framework, is_macos_brew
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
from virtualenv.create.via_global_ref.builtin.ref import PathRef
|
||||
from virtualenv.create.via_global_ref.builtin.via_global_self_do import BuiltinViaGlobalRefMeta
|
||||
from virtualenv.create.via_global_ref.venv import Venv
|
||||
|
||||
|
||||
class CPython3(CPython, Python3Supports, abc.ABC):
|
||||
"""CPython 3 or later."""
|
||||
|
||||
|
||||
class CPython3Posix(CPythonPosix, CPython3):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter: PythonInfo) -> bool:
|
||||
return (
|
||||
is_mac_os_framework(interpreter) is False
|
||||
and is_macos_brew(interpreter) is False
|
||||
and super().can_describe(interpreter)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def sources(cls, interpreter: PythonInfo) -> Generator[PathRef]: # ty: ignore[invalid-method-override]
|
||||
yield from super().sources(interpreter)
|
||||
if shared_lib := cls._shared_libpython(interpreter):
|
||||
yield PathRefToDest(shared_lib, dest=cls._to_lib, when=RefWhen.COPY)
|
||||
|
||||
@classmethod
|
||||
def _to_lib(cls, creator: CPython3Posix, src: Path) -> Path:
|
||||
return creator.dest / "lib" / src.name
|
||||
|
||||
@classmethod
|
||||
def _shared_libpython(cls, interpreter: PythonInfo) -> Path | None:
|
||||
if not interpreter.sysconfig_vars.get("Py_ENABLE_SHARED"):
|
||||
return None
|
||||
if not (instsoname := interpreter.sysconfig_vars.get("INSTSONAME")):
|
||||
return None
|
||||
if not (libdir := interpreter.sysconfig_vars.get("LIBDIR")):
|
||||
return None
|
||||
if not (lib_path := Path(libdir) / instsoname).exists():
|
||||
return None
|
||||
return lib_path
|
||||
|
||||
def install_venv_shared_libs(self, venv_creator: Venv) -> None:
|
||||
if venv_creator.symlinks:
|
||||
return
|
||||
if not (shared_lib := self._shared_libpython(venv_creator.interpreter)):
|
||||
return
|
||||
dest = venv_creator.dest / "lib" / shared_lib.name
|
||||
ensure_dir(dest.parent)
|
||||
copy_path(shared_lib, dest)
|
||||
|
||||
|
||||
class CPython3Windows(CPythonWindows, CPython3):
|
||||
"""CPython 3 on Windows."""
|
||||
|
||||
@classmethod
|
||||
def setup_meta(cls, interpreter: PythonInfo) -> BuiltinViaGlobalRefMeta | None: # ty: ignore[invalid-method-override]
|
||||
if is_store_python(interpreter): # store python is not supported here
|
||||
return None
|
||||
return super().setup_meta(interpreter)
|
||||
|
||||
@classmethod
|
||||
def sources(cls, interpreter: PythonInfo) -> Generator[PathRef]: # ty: ignore[invalid-method-override]
|
||||
yield from cls.executables(interpreter)
|
||||
if not cls.has_shim(interpreter):
|
||||
yield from cls.dll_and_pyd(interpreter)
|
||||
yield from cls.python_zip(interpreter)
|
||||
|
||||
@classmethod
|
||||
def _debug_suffix(cls, interpreter: PythonInfo) -> str:
|
||||
return "_d" if getattr(interpreter, "debug_build", False) else ""
|
||||
|
||||
@classmethod
|
||||
def executables(cls, interpreter: PythonInfo) -> list[PathRef] | Generator[PathRef]:
|
||||
sources = super().sources(interpreter)
|
||||
if interpreter.version_info >= (3, 13):
|
||||
host = cls.host_python(interpreter)
|
||||
t_suffix = "t" if interpreter.free_threaded else ""
|
||||
d_suffix = cls._debug_suffix(interpreter)
|
||||
updated_sources: list[PathRef] = []
|
||||
for ref in sources:
|
||||
if ref.base == "python.exe":
|
||||
launcher_path = host.with_name(f"venvlauncher{t_suffix}{d_suffix}.exe")
|
||||
if launcher_path.exists():
|
||||
new_ref = ExePathRefToDest(
|
||||
launcher_path, dest=ref.dest, targets=[ref.base, *ref.aliases], must=ref.must, when=ref.when
|
||||
)
|
||||
updated_sources.append(new_ref)
|
||||
continue
|
||||
elif ref.src.name == "pythonw.exe":
|
||||
w_launcher_path = ref.src.with_name(f"venvwlauncher{t_suffix}{d_suffix}.exe")
|
||||
if w_launcher_path.exists():
|
||||
new_ref = ExePathRefToDest(
|
||||
w_launcher_path,
|
||||
dest=ref.dest,
|
||||
targets=[ref.base, *ref.aliases],
|
||||
must=ref.must,
|
||||
when=ref.when,
|
||||
)
|
||||
updated_sources.append(new_ref)
|
||||
continue
|
||||
updated_sources.append(ref)
|
||||
return updated_sources
|
||||
return sources
|
||||
|
||||
@classmethod
|
||||
def has_shim(cls, interpreter: PythonInfo) -> bool:
|
||||
return cls.shim(interpreter) is not None
|
||||
|
||||
@classmethod
|
||||
def shim(cls, interpreter: PythonInfo) -> Path | None:
|
||||
root = Path(interpreter.system_stdlib) / "venv" / "scripts" / "nt"
|
||||
d_suffix = cls._debug_suffix(interpreter)
|
||||
if interpreter.version_info >= (3, 13):
|
||||
# https://github.com/python/cpython/issues/112984
|
||||
t_suffix = "t" if interpreter.free_threaded else ""
|
||||
exe_name = f"venvlauncher{t_suffix}{d_suffix}.exe"
|
||||
else:
|
||||
exe_name = f"python{d_suffix}.exe"
|
||||
if (shim := root / exe_name).exists():
|
||||
return shim
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def host_python(cls, interpreter: PythonInfo) -> Path:
|
||||
if cls.has_shim(interpreter):
|
||||
# starting with CPython 3.7 Windows ships with a venvlauncher.exe that avoids the need for dll/pyd copies
|
||||
# it also means the wrapper must be copied to avoid bugs such as https://bugs.python.org/issue42013
|
||||
return cls.shim(interpreter) # ty: ignore[invalid-return-type]
|
||||
return super().host_python(interpreter)
|
||||
|
||||
@classmethod
|
||||
def dll_and_pyd(cls, interpreter: PythonInfo) -> Generator[PathRefToDest]:
|
||||
folders = [Path(interpreter.system_executable).parent] # ty: ignore[invalid-argument-type]
|
||||
|
||||
# May be missing on some Python hosts.
|
||||
# See https://github.com/pypa/virtualenv/issues/2368
|
||||
dll_folder = Path(interpreter.system_prefix) / "DLLs"
|
||||
if dll_folder.is_dir():
|
||||
folders.append(dll_folder)
|
||||
|
||||
for folder in folders:
|
||||
for file in folder.iterdir():
|
||||
if file.suffix in {".pyd", ".dll"}:
|
||||
# Skip pywin32 DLLs to avoid conflicts with pywin32 installation
|
||||
# pywin32 has its own post-install that places DLLs in site-packages/pywin32_system32
|
||||
# See https://github.com/pypa/virtualenv/issues/2662
|
||||
if cls._is_pywin32_dll(file.name):
|
||||
continue
|
||||
yield PathRefToDest(file, cls.to_bin)
|
||||
|
||||
@classmethod
|
||||
def _is_pywin32_dll(cls, filename: str) -> bool:
|
||||
"""Check if a DLL file belongs to pywin32."""
|
||||
# pywin32 DLLs follow patterns like: pywintypes39.dll, pythoncom39.dll
|
||||
name_lower = filename.lower()
|
||||
return name_lower.startswith(("pywintypes", "pythoncom"))
|
||||
|
||||
@classmethod
|
||||
def python_zip(cls, interpreter: PythonInfo) -> Generator[PathRefToDest]:
|
||||
"""``python{VERSION}.zip`` contains compiled ``*.pyc`` std lib packages, where ``VERSION`` is ``py_version_nodot`` var from the ``sysconfig`` module.
|
||||
|
||||
See https://docs.python.org/3/using/windows.html#the-embeddable-package, ``discovery.py_info.PythonInfo`` class
|
||||
(interpreter), and ``python -m sysconfig`` output.
|
||||
|
||||
The embeddable Python distribution for Windows includes ``python{VERSION}.zip`` and ``python{VERSION}._pth``
|
||||
files. User can move/rename the zip file and edit ``sys.path`` by editing the ``_pth`` file. Here the
|
||||
``pattern`` is used only for the default zip file name.
|
||||
|
||||
"""
|
||||
pattern = f"*python{interpreter.version_nodot}.zip"
|
||||
matches = fnmatch.filter(interpreter.path, pattern)
|
||||
matched_paths = map(Path, matches)
|
||||
existing_paths = filter(method("exists"), matched_paths)
|
||||
if (path := next(existing_paths, None)) is not None:
|
||||
yield PathRefToDest(path, cls.to_bin)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CPython3",
|
||||
"CPython3Posix",
|
||||
"CPython3Windows",
|
||||
]
|
||||
Vendored
+290
@@ -0,0 +1,290 @@
|
||||
"""The Apple Framework builds require their own customization."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.create.via_global_ref.builtin.ref import (
|
||||
ExePathRefToDest,
|
||||
PathRefToDest,
|
||||
RefMust,
|
||||
)
|
||||
from virtualenv.create.via_global_ref.builtin.via_global_self_do import BuiltinViaGlobalRefMeta
|
||||
|
||||
from .common import CPython, CPythonPosix, is_mac_os_framework, is_macos_brew
|
||||
from .cpython3 import CPython3
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Generator
|
||||
from io import BufferedRandom
|
||||
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
from virtualenv.create.via_global_ref.builtin.ref import PathRef
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CPythonmacOsFramework(CPython, ABC):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter: PythonInfo) -> bool:
|
||||
return is_mac_os_framework(interpreter) and super().can_describe(interpreter)
|
||||
|
||||
def create(self) -> None:
|
||||
super().create()
|
||||
|
||||
target = self.desired_mach_o_image_path()
|
||||
current = self.current_mach_o_image_path()
|
||||
for src in self._sources:
|
||||
if isinstance(src, ExePathRefToDest) and (src.must == RefMust.COPY or not self.symlinks):
|
||||
exes = [self.bin_dir / src.base]
|
||||
if not self.symlinks:
|
||||
exes.extend(self.bin_dir / a for a in src.aliases)
|
||||
for exe in exes:
|
||||
fix_mach_o(str(exe), current, target, self.interpreter.max_size) # ty: ignore[invalid-argument-type]
|
||||
try:
|
||||
subprocess.check_call(["codesign", "--force", "--sign", "-", str(exe)]) # noqa: S607
|
||||
except (OSError, subprocess.CalledProcessError) as e:
|
||||
LOGGER.warning("Could not ad-hoc re-sign %s: %s", exe, e)
|
||||
|
||||
@classmethod
|
||||
def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], str, str]]:
|
||||
for _, targets, must, when in super()._executables(interpreter):
|
||||
# Make sure we use the embedded interpreter inside the framework, even if sys.executable points to the
|
||||
# stub executable in ${sys.prefix}/bin.
|
||||
# See http://groups.google.com/group/python-virtualenv/browse_thread/thread/17cab2f85da75951
|
||||
fixed_host_exe = Path(interpreter.prefix) / "Resources" / "Python.app" / "Contents" / "MacOS" / "Python" # ty: ignore[invalid-argument-type]
|
||||
yield fixed_host_exe, targets, must, when
|
||||
|
||||
@abstractmethod
|
||||
def current_mach_o_image_path(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def desired_mach_o_image_path(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CPython3macOsFramework(CPythonmacOsFramework, CPython3, CPythonPosix):
|
||||
def current_mach_o_image_path(self) -> str:
|
||||
return "@executable_path/../../../../Python3"
|
||||
|
||||
def desired_mach_o_image_path(self) -> str:
|
||||
return "@executable_path/../.Python"
|
||||
|
||||
@classmethod
|
||||
def sources(cls, interpreter: PythonInfo) -> Generator[PathRef]: # ty: ignore[invalid-method-override]
|
||||
yield from super().sources(interpreter)
|
||||
|
||||
# add a symlink to the host python image
|
||||
exe = Path(interpreter.prefix) / "Python3" # ty: ignore[invalid-argument-type]
|
||||
yield PathRefToDest(exe, dest=lambda self, _: self.dest / ".Python", must=RefMust.SYMLINK)
|
||||
|
||||
@property
|
||||
def reload_code(self) -> str:
|
||||
result = super().reload_code # ty: ignore[unresolved-attribute]
|
||||
return dedent(
|
||||
f"""
|
||||
# the bundled site.py always adds the global site package if we're on python framework build, escape this
|
||||
import sys
|
||||
before = sys._framework
|
||||
try:
|
||||
sys._framework = None
|
||||
{result}
|
||||
finally:
|
||||
sys._framework = before
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def fix_mach_o(exe: str, current: str, new: str, max_size: int) -> None:
|
||||
"""https://en.wikipedia.org/wiki/Mach-O.
|
||||
|
||||
Mach-O, short for Mach object file format, is a file format for executables, object code, shared libraries,
|
||||
dynamically-loaded code, and core dumps. A replacement for the a.out format, Mach-O offers more extensibility and
|
||||
faster access to information in the symbol table.
|
||||
|
||||
Each Mach-O file is made up of one Mach-O header, followed by a series of load commands, followed by one or more
|
||||
segments, each of which contains between 0 and 255 sections. Mach-O uses the REL relocation format to handle
|
||||
references to symbols. When looking up symbols Mach-O uses a two-level namespace that encodes each symbol into an
|
||||
'object/symbol name' pair that is then linearly searched for by first the object and then the symbol name.
|
||||
|
||||
The basic structure—a list of variable-length "load commands" that reference pages of data elsewhere in the file—was
|
||||
also used in the executable file format for Accent. The Accent file format was in turn, based on an idea from Spice
|
||||
Lisp.
|
||||
|
||||
With the introduction of Mac OS X 10.6 platform the Mach-O file underwent a significant modification that causes
|
||||
binaries compiled on a computer running 10.6 or later to be (by default) executable only on computers running Mac OS
|
||||
X 10.6 or later. The difference stems from load commands that the dynamic linker, in previous Mac OS X versions,
|
||||
does not understand. Another significant change to the Mach-O format is the change in how the Link Edit tables
|
||||
(found in the __LINKEDIT section) function. In 10.6 these new Link Edit tables are compressed by removing unused and
|
||||
unneeded bits of information, however Mac OS X 10.5 and earlier cannot read this new Link Edit table format.
|
||||
|
||||
"""
|
||||
try:
|
||||
LOGGER.debug("change Mach-O for %s from %s to %s", exe, current, new)
|
||||
_builtin_change_mach_o(max_size)(exe, current, new)
|
||||
except Exception as e: # noqa: BLE001
|
||||
LOGGER.warning("Could not call _builtin_change_mac_o: %s. Trying to call install_name_tool instead.", e)
|
||||
try:
|
||||
cmd = ["install_name_tool", "-change", current, new, exe]
|
||||
subprocess.check_call(cmd)
|
||||
except Exception:
|
||||
logging.fatal("Could not call install_name_tool -- you must have Apple's development tools installed")
|
||||
raise
|
||||
|
||||
|
||||
def _builtin_change_mach_o(maxint: int) -> Callable[[str, str, str], None]: # noqa: C901
|
||||
MH_MAGIC = 0xFEEDFACE # noqa: N806
|
||||
MH_CIGAM = 0xCEFAEDFE # noqa: N806
|
||||
MH_MAGIC_64 = 0xFEEDFACF # noqa: N806
|
||||
MH_CIGAM_64 = 0xCFFAEDFE # noqa: N806
|
||||
FAT_MAGIC = 0xCAFEBABE # noqa: N806
|
||||
BIG_ENDIAN = ">" # noqa: N806
|
||||
LITTLE_ENDIAN = "<" # noqa: N806
|
||||
LC_LOAD_DYLIB = 0xC # noqa: N806
|
||||
|
||||
class FileView:
|
||||
"""A proxy for file-like objects that exposes a given view of a file. Modified from macholib."""
|
||||
|
||||
def __init__(self, file_obj: FileView | BufferedRandom, start: int = 0, size: int = maxint) -> None:
|
||||
if isinstance(file_obj, FileView):
|
||||
self._file_obj = file_obj._file_obj # noqa: SLF001
|
||||
else:
|
||||
self._file_obj = file_obj
|
||||
self._start = start
|
||||
self._end = start + size
|
||||
self._pos = 0
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<fileview [{self._start:d}, {self._end:d}] {self._file_obj!r}>"
|
||||
|
||||
def tell(self) -> int:
|
||||
return self._pos
|
||||
|
||||
def _checkwindow(self, seek_to: int, op: str) -> None:
|
||||
if not (self._start <= seek_to <= self._end):
|
||||
msg = f"{op} to offset {seek_to:d} is outside window [{self._start:d}, {self._end:d}]"
|
||||
raise OSError(msg)
|
||||
|
||||
def seek(self, offset: int, whence: int = 0) -> None:
|
||||
seek_to = offset
|
||||
if whence == os.SEEK_SET:
|
||||
seek_to += self._start
|
||||
elif whence == os.SEEK_CUR:
|
||||
seek_to += self._start + self._pos
|
||||
elif whence == os.SEEK_END:
|
||||
seek_to += self._end
|
||||
else:
|
||||
msg = f"Invalid whence argument to seek: {whence!r}"
|
||||
raise OSError(msg)
|
||||
self._checkwindow(seek_to, "seek")
|
||||
self._file_obj.seek(seek_to)
|
||||
self._pos = seek_to - self._start
|
||||
|
||||
def write(self, content: bytes) -> None:
|
||||
here = self._start + self._pos
|
||||
self._checkwindow(here, "write")
|
||||
self._checkwindow(here + len(content), "write")
|
||||
self._file_obj.seek(here, os.SEEK_SET)
|
||||
self._file_obj.write(content)
|
||||
self._pos += len(content)
|
||||
|
||||
def read(self, size: int = maxint) -> bytes:
|
||||
assert size >= 0 # noqa: S101
|
||||
here = self._start + self._pos
|
||||
self._checkwindow(here, "read")
|
||||
size = min(size, self._end - here)
|
||||
self._file_obj.seek(here, os.SEEK_SET)
|
||||
read_bytes = self._file_obj.read(size)
|
||||
self._pos += len(read_bytes)
|
||||
return read_bytes
|
||||
|
||||
def read_data(file: FileView, endian: str, num: int = 1) -> int | tuple[int, ...]:
|
||||
"""Read a given number of 32-bits unsigned integers from the given file with the given endianness."""
|
||||
res = struct.unpack(endian + "L" * num, file.read(num * 4))
|
||||
if len(res) == 1:
|
||||
return res[0]
|
||||
return res
|
||||
|
||||
def mach_o_change(at_path: str, what: str, value: str) -> None: # noqa: C901
|
||||
"""Replace a given name (what) in any LC_LOAD_DYLIB command found in the given binary with a new name (value), provided it's shorter."""
|
||||
|
||||
def do_macho(file: FileView, bits: int, endian: str) -> None:
|
||||
# Read Mach-O header (the magic number is assumed read by the caller)
|
||||
_cpu_type, _cpu_sub_type, _file_type, n_commands, _size_of_commands, _flags = read_data(file, endian, 6) # ty: ignore[not-iterable]
|
||||
# 64-bits header has one more field.
|
||||
if bits == 64: # noqa: PLR2004
|
||||
read_data(file, endian)
|
||||
# The header is followed by n commands
|
||||
for _ in range(n_commands):
|
||||
where = file.tell()
|
||||
# Read command header
|
||||
cmd, cmd_size = read_data(file, endian, 2) # ty: ignore[not-iterable]
|
||||
if cmd == LC_LOAD_DYLIB:
|
||||
# The first data field in LC_LOAD_DYLIB commands is the offset of the name, starting from the
|
||||
# beginning of the command.
|
||||
name_offset = read_data(file, endian)
|
||||
file.seek(where + name_offset, os.SEEK_SET) # ty: ignore[unsupported-operator]
|
||||
# Read the NUL terminated string
|
||||
load = file.read(cmd_size - name_offset).decode() # ty: ignore[unsupported-operator]
|
||||
load = load[: load.index("\0")]
|
||||
# If the string is what is being replaced, overwrite it.
|
||||
if load == what:
|
||||
file.seek(where + name_offset, os.SEEK_SET) # ty: ignore[unsupported-operator]
|
||||
file.write(value.encode() + b"\0")
|
||||
# Seek to the next command
|
||||
file.seek(where + cmd_size, os.SEEK_SET)
|
||||
|
||||
def do_file(file: FileView | BufferedRandom, offset: int = 0, size: int = maxint) -> None:
|
||||
file = FileView(file, offset, size)
|
||||
# Read magic number
|
||||
magic = read_data(file, BIG_ENDIAN)
|
||||
if magic == FAT_MAGIC:
|
||||
# Fat binaries contain nfat_arch Mach-O binaries
|
||||
n_fat_arch = read_data(file, BIG_ENDIAN)
|
||||
for _ in range(n_fat_arch): # ty: ignore[invalid-argument-type]
|
||||
# Read arch header
|
||||
_cpu_type, _cpu_sub_type, offset, size, _align = read_data(file, BIG_ENDIAN, 5) # ty: ignore[not-iterable]
|
||||
do_file(file, offset, size)
|
||||
elif magic == MH_MAGIC:
|
||||
do_macho(file, 32, BIG_ENDIAN)
|
||||
elif magic == MH_CIGAM:
|
||||
do_macho(file, 32, LITTLE_ENDIAN)
|
||||
elif magic == MH_MAGIC_64:
|
||||
do_macho(file, 64, BIG_ENDIAN)
|
||||
elif magic == MH_CIGAM_64:
|
||||
do_macho(file, 64, LITTLE_ENDIAN)
|
||||
|
||||
assert len(what) >= len(value) # noqa: S101
|
||||
|
||||
with open(at_path, "r+b") as f:
|
||||
do_file(f)
|
||||
|
||||
return mach_o_change
|
||||
|
||||
|
||||
class CPython3macOsBrew(CPython3, CPythonPosix):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter: PythonInfo) -> bool:
|
||||
return is_macos_brew(interpreter) and super().can_describe(interpreter)
|
||||
|
||||
@classmethod
|
||||
def setup_meta(cls, interpreter: PythonInfo) -> BuiltinViaGlobalRefMeta: # noqa: ARG003
|
||||
meta = BuiltinViaGlobalRefMeta()
|
||||
meta.copy_error = "Brew disables copy creation: https://github.com/Homebrew/homebrew-core/issues/138159"
|
||||
return meta
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CPython3macOsBrew",
|
||||
"CPython3macOsFramework",
|
||||
"CPythonmacOsFramework",
|
||||
]
|
||||
vendor/python-windows/Lib/site-packages/virtualenv/create/via_global_ref/builtin/graalpy/__init__.py
Vendored
+99
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.create.describe import PosixSupports, WindowsSupports
|
||||
from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest, RefMust, RefWhen
|
||||
from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator, Iterator
|
||||
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
|
||||
class GraalPy(ViaGlobalRefVirtualenvBuiltin, ABC):
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def _native_lib(cls, lib_dir: Path, platform: str) -> Path:
|
||||
"""Return the path to the native library for this platform."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter: PythonInfo) -> bool:
|
||||
return interpreter.implementation == "GraalPy" and super().can_describe(interpreter)
|
||||
|
||||
@classmethod
|
||||
def exe_stem(cls) -> str:
|
||||
return "graalpy"
|
||||
|
||||
@classmethod
|
||||
def exe_names(cls, interpreter: PythonInfo) -> set[str]:
|
||||
return {
|
||||
cls.exe_stem(),
|
||||
"python",
|
||||
f"python{interpreter.version_info.major}",
|
||||
f"python{interpreter.version_info.major}.{interpreter.version_info.minor}",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], RefMust, RefWhen], None, None]: # ty: ignore[invalid-method-override]
|
||||
host = Path(interpreter.system_executable) # ty: ignore[invalid-argument-type]
|
||||
targets = sorted(f"{name}{cls.suffix}" for name in cls.exe_names(interpreter))
|
||||
yield host, targets, RefMust.NA, RefWhen.ANY
|
||||
|
||||
@classmethod
|
||||
def sources(cls, interpreter: PythonInfo) -> Generator[PathRefToDest]: # ty: ignore[invalid-method-override]
|
||||
yield from super().sources(interpreter)
|
||||
python_dir = Path(interpreter.system_executable).resolve().parent # ty: ignore[invalid-argument-type]
|
||||
if python_dir.name in {"bin", "Scripts"}:
|
||||
python_dir = python_dir.parent
|
||||
|
||||
native_lib = cls._native_lib(python_dir / "lib", interpreter.platform)
|
||||
if native_lib.exists():
|
||||
yield PathRefToDest(native_lib, dest=lambda self, s: self.bin_dir.parent / "lib" / s.name)
|
||||
|
||||
for jvm_dir_name in ("jvm", "jvmlibs", "modules"):
|
||||
jvm_dir = python_dir / jvm_dir_name
|
||||
if jvm_dir.exists():
|
||||
yield PathRefToDest(jvm_dir, dest=lambda self, s: self.bin_dir.parent / s.name)
|
||||
|
||||
@classmethod
|
||||
def _shared_libs(cls, python_dir: Path) -> Iterator[Path]:
|
||||
raise NotImplementedError
|
||||
|
||||
def set_pyenv_cfg(self) -> None:
|
||||
super().set_pyenv_cfg()
|
||||
# GraalPy 24.0 and older had home without the bin
|
||||
version = self.interpreter.version_info
|
||||
if version.minor <= 10: # noqa: PLR2004
|
||||
home = Path(self.pyenv_cfg["home"])
|
||||
if home.name == "bin":
|
||||
self.pyenv_cfg["home"] = str(home.parent)
|
||||
|
||||
|
||||
class GraalPyPosix(GraalPy, PosixSupports):
|
||||
@classmethod
|
||||
def _native_lib(cls, lib_dir: Path, platform: str) -> Path:
|
||||
if platform == "darwin":
|
||||
return lib_dir / "libpythonvm.dylib"
|
||||
return lib_dir / "libpythonvm.so"
|
||||
|
||||
|
||||
class GraalPyWindows(GraalPy, WindowsSupports):
|
||||
@classmethod
|
||||
def _native_lib(cls, lib_dir: Path, platform: str) -> Path: # noqa: ARG003
|
||||
return lib_dir / "pythonvm.dll"
|
||||
|
||||
def set_pyenv_cfg(self) -> None:
|
||||
# GraalPy needs an additional entry in pyvenv.cfg on Windows
|
||||
super().set_pyenv_cfg()
|
||||
self.pyenv_cfg["venvlauncher_command"] = self.interpreter.system_executable # ty: ignore[invalid-assignment]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"GraalPyPosix",
|
||||
"GraalPyWindows",
|
||||
]
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest, RefMust, RefWhen
|
||||
from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator, Iterator
|
||||
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
from virtualenv.create.via_global_ref.builtin.ref import PathRef
|
||||
|
||||
|
||||
class PyPy(ViaGlobalRefVirtualenvBuiltin, abc.ABC):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter: PythonInfo) -> bool:
|
||||
return interpreter.implementation == "PyPy" and super().can_describe(interpreter)
|
||||
|
||||
@classmethod
|
||||
def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], str, str]]:
|
||||
host = Path(interpreter.system_executable) # ty: ignore[invalid-argument-type]
|
||||
targets = sorted(f"{name}{PyPy.suffix}" for name in cls.exe_names(interpreter))
|
||||
yield host, targets, RefMust.NA, RefWhen.ANY
|
||||
|
||||
@classmethod
|
||||
def executables(cls, interpreter: PythonInfo) -> Generator[PathRef]:
|
||||
yield from super().sources(interpreter)
|
||||
|
||||
@classmethod
|
||||
def exe_names(cls, interpreter: PythonInfo) -> set[str]:
|
||||
return {
|
||||
cls.exe_stem(),
|
||||
"python",
|
||||
f"python{interpreter.version_info.major}",
|
||||
f"python{interpreter.version_info.major}.{interpreter.version_info.minor}",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def sources(cls, interpreter: PythonInfo) -> Generator[PathRef]: # ty: ignore[invalid-method-override]
|
||||
yield from cls.executables(interpreter)
|
||||
for host in cls._add_shared_libs(interpreter):
|
||||
yield PathRefToDest(host, dest=lambda self, s: self.bin_dir / s.name)
|
||||
|
||||
@classmethod
|
||||
def _add_shared_libs(cls, interpreter: PythonInfo) -> Generator[Path]:
|
||||
# https://bitbucket.org/pypy/pypy/issue/1922/future-proofing-virtualenv
|
||||
python_dir = Path(interpreter.system_executable).resolve().parent # ty: ignore[invalid-argument-type]
|
||||
yield from cls._shared_libs(python_dir)
|
||||
|
||||
@classmethod
|
||||
def _shared_libs(cls, python_dir: Path) -> Iterator[Path]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PyPy",
|
||||
]
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.create.describe import PosixSupports, Python3Supports, WindowsSupports
|
||||
from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest
|
||||
|
||||
from .common import PyPy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator, Iterator
|
||||
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
from virtualenv.create.via_global_ref.builtin.ref import PathRef
|
||||
|
||||
|
||||
class PyPy3(PyPy, Python3Supports, abc.ABC):
|
||||
@classmethod
|
||||
def exe_stem(cls) -> str:
|
||||
return "pypy3"
|
||||
|
||||
@classmethod
|
||||
def exe_names(cls, interpreter: PythonInfo) -> set[str]:
|
||||
return super().exe_names(interpreter) | {"pypy"}
|
||||
|
||||
|
||||
class PyPy3Posix(PyPy3, PosixSupports):
|
||||
"""PyPy 3 on POSIX."""
|
||||
|
||||
@classmethod
|
||||
def _shared_libs(cls, python_dir: Path) -> Iterator[Path]:
|
||||
# glob for libpypy3-c.so, libpypy3-c.dylib, libpypy3.9-c.so ...
|
||||
return python_dir.glob("libpypy3*.*")
|
||||
|
||||
def to_lib(self, src: Path) -> Path:
|
||||
return self.dest / "lib" / src.name
|
||||
|
||||
@classmethod
|
||||
def sources(cls, interpreter: PythonInfo) -> Generator[PathRef]:
|
||||
yield from super().sources(interpreter)
|
||||
# PyPy >= 3.8 supports a standard prefix installation, where older versions always used a portable/development
|
||||
# style installation. If this is a standard prefix installation, skip the below:
|
||||
if interpreter.system_prefix == "/usr":
|
||||
return
|
||||
# Also copy/symlink anything under prefix/lib, which, for "portable" PyPy builds, includes the tk,tcl runtime
|
||||
# and a number of shared objects. In distro-specific builds or on conda this should be empty (on PyPy3.8+ it
|
||||
# will, like on CPython, hold the stdlib).
|
||||
host_lib = Path(interpreter.system_prefix) / "lib"
|
||||
stdlib = Path(interpreter.system_stdlib)
|
||||
if host_lib.exists() and host_lib.is_dir():
|
||||
if (deps_file := host_lib / "PYPY_PORTABLE_DEPS.txt").exists():
|
||||
for line in deps_file.read_text(encoding="utf-8").splitlines():
|
||||
dep = line.strip()
|
||||
if dep and (path := host_lib / dep).exists():
|
||||
yield PathRefToDest(path, dest=cls.to_lib)
|
||||
else:
|
||||
for path in host_lib.iterdir():
|
||||
if stdlib == path:
|
||||
continue
|
||||
yield PathRefToDest(path, dest=cls.to_lib)
|
||||
|
||||
|
||||
class Pypy3Windows(PyPy3, WindowsSupports):
|
||||
"""PyPy 3 on Windows."""
|
||||
|
||||
@classmethod
|
||||
def _shared_libs(cls, python_dir: Path) -> Iterator[Path]:
|
||||
# PyPy does not use a PEP 397 launcher, so all DLLs from the interpreter directory are needed for the venv
|
||||
yield from python_dir.glob("*.dll")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PyPy3",
|
||||
"PyPy3Posix",
|
||||
"Pypy3Windows",
|
||||
]
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
"""Virtual environments in the traditional sense are built as reference to the host python. This file allows declarative references to elements on the file system, allowing our system to automatically detect what modes it can support given the constraints: e.g. can the file system symlink, can the files be read, executed, etc."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import OrderedDict
|
||||
from stat import S_IXGRP, S_IXOTH, S_IXUSR
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.info import fs_is_case_sensitive, fs_supports_symlink
|
||||
from virtualenv.util.path import copy, make_exe, symlink
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
if sys.version_info >= (3, 11): # pragma: no cover (py311+)
|
||||
from enum import StrEnum
|
||||
else: # pragma: no cover (py311+)
|
||||
from enum import Enum
|
||||
|
||||
class StrEnum(str, Enum):
|
||||
pass
|
||||
|
||||
|
||||
class RefMust(StrEnum):
|
||||
NA = "NA"
|
||||
COPY = "copy"
|
||||
SYMLINK = "symlink"
|
||||
|
||||
|
||||
class RefWhen(StrEnum):
|
||||
ANY = "ANY"
|
||||
COPY = "copy"
|
||||
SYMLINK = "symlink"
|
||||
|
||||
|
||||
class PathRef(ABC):
|
||||
"""Base class that checks if a file reference can be symlink/copied."""
|
||||
|
||||
FS_SUPPORTS_SYMLINK = fs_supports_symlink()
|
||||
FS_CASE_SENSITIVE = fs_is_case_sensitive()
|
||||
|
||||
def __init__(self, src: Path, must: str = RefMust.NA, when: str = RefWhen.ANY) -> None:
|
||||
self.must = must
|
||||
self.when = when
|
||||
self.src = src
|
||||
try:
|
||||
self.exists = src.exists()
|
||||
except OSError:
|
||||
self.exists = False
|
||||
self._can_read: bool | None = None if self.exists else False
|
||||
self._can_copy: bool | None = None if self.exists else False
|
||||
self._can_symlink: bool | None = None if self.exists else False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(src={self.src})"
|
||||
|
||||
@property
|
||||
def can_read(self) -> bool:
|
||||
if self._can_read is None:
|
||||
if self.src.is_file():
|
||||
try:
|
||||
with self.src.open("rb"):
|
||||
self._can_read = True
|
||||
except OSError:
|
||||
self._can_read = False
|
||||
else:
|
||||
self._can_read = os.access(str(self.src), os.R_OK)
|
||||
return self._can_read
|
||||
|
||||
@property
|
||||
def can_copy(self) -> bool:
|
||||
if self._can_copy is None:
|
||||
if self.must == RefMust.SYMLINK:
|
||||
self._can_copy = self.can_symlink
|
||||
else:
|
||||
self._can_copy = self.can_read
|
||||
return self._can_copy
|
||||
|
||||
@property
|
||||
def can_symlink(self) -> bool:
|
||||
if self._can_symlink is None:
|
||||
if self.must == RefMust.COPY:
|
||||
self._can_symlink = self.can_copy
|
||||
else:
|
||||
self._can_symlink = self.FS_SUPPORTS_SYMLINK and self.can_read
|
||||
return self._can_symlink
|
||||
|
||||
@abstractmethod
|
||||
def run(self, creator: object, symlinks: bool) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def method(self, symlinks: bool) -> Callable[..., None]:
|
||||
if self.must == RefMust.SYMLINK:
|
||||
return symlink
|
||||
if self.must == RefMust.COPY:
|
||||
return copy
|
||||
return symlink if symlinks else copy
|
||||
|
||||
|
||||
class ExePathRef(PathRef, ABC):
|
||||
"""Base class that checks if a executable can be references via symlink/copy."""
|
||||
|
||||
def __init__(self, src: Path, must: str = RefMust.NA, when: str = RefWhen.ANY) -> None:
|
||||
super().__init__(src, must, when)
|
||||
self._can_run: bool | None = None
|
||||
|
||||
@property
|
||||
def can_symlink(self) -> bool:
|
||||
if self.FS_SUPPORTS_SYMLINK:
|
||||
return self.can_run
|
||||
return False
|
||||
|
||||
@property
|
||||
def can_run(self) -> bool:
|
||||
if self._can_run is None:
|
||||
mode = self.src.stat().st_mode
|
||||
for key in [S_IXUSR, S_IXGRP, S_IXOTH]:
|
||||
if mode & key:
|
||||
self._can_run = True
|
||||
break
|
||||
else:
|
||||
self._can_run = False
|
||||
return self._can_run # ty: ignore[invalid-return-type]
|
||||
|
||||
|
||||
class PathRefToDest(PathRef):
|
||||
"""Link a path on the file system."""
|
||||
|
||||
def __init__(self, src: Path, dest: Callable[..., Path], must: str = RefMust.NA, when: str = RefWhen.ANY) -> None:
|
||||
super().__init__(src, must, when)
|
||||
self.dest = dest
|
||||
|
||||
def run(self, creator: object, symlinks: bool) -> None:
|
||||
dest = self.dest(creator, self.src)
|
||||
method = self.method(symlinks)
|
||||
dest_iterable = dest if isinstance(dest, list) else (dest,)
|
||||
if not dest.parent.exists():
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
for dst in dest_iterable:
|
||||
method(self.src, dst)
|
||||
|
||||
|
||||
class ExePathRefToDest(PathRefToDest, ExePathRef):
|
||||
"""Link a exe path on the file system."""
|
||||
|
||||
def __init__(
|
||||
self, src: Path, targets: list[str], dest: Callable[..., Path], must: str = RefMust.NA, when: str = RefWhen.ANY
|
||||
) -> None:
|
||||
ExePathRef.__init__(self, src, must, when)
|
||||
PathRefToDest.__init__(self, src, dest, must, when)
|
||||
if not self.FS_CASE_SENSITIVE:
|
||||
targets = list(OrderedDict((i.lower(), None) for i in targets).keys())
|
||||
self.base = targets[0]
|
||||
self.aliases = targets[1:]
|
||||
self.dest = dest
|
||||
|
||||
def run(self, creator: object, symlinks: bool) -> None:
|
||||
bin_dir = self.dest(creator, self.src).parent
|
||||
dest = bin_dir / self.base
|
||||
method = self.method(symlinks)
|
||||
method(self.src, dest)
|
||||
if not symlinks:
|
||||
make_exe(dest)
|
||||
for extra in self.aliases:
|
||||
link_file = bin_dir / extra
|
||||
if link_file.exists():
|
||||
link_file.unlink()
|
||||
if symlinks:
|
||||
link_file.symlink_to(self.base)
|
||||
else:
|
||||
copy(self.src, link_file)
|
||||
if not symlinks:
|
||||
make_exe(link_file)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(src={self.src}, alias={self.aliases})"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ExePathRef",
|
||||
"ExePathRefToDest",
|
||||
"PathRef",
|
||||
"PathRefToDest",
|
||||
"RefMust",
|
||||
"RefWhen",
|
||||
]
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.create.describe import PosixSupports, Python3Supports, WindowsSupports
|
||||
from virtualenv.create.via_global_ref.builtin.ref import RefMust, RefWhen
|
||||
from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
|
||||
class RustPython(ViaGlobalRefVirtualenvBuiltin, Python3Supports, ABC):
|
||||
@classmethod
|
||||
def can_describe(cls, interpreter: PythonInfo) -> bool:
|
||||
return interpreter.implementation == "RustPython" and super().can_describe(interpreter)
|
||||
|
||||
@classmethod
|
||||
def exe_stem(cls) -> str:
|
||||
return "rustpython"
|
||||
|
||||
@classmethod
|
||||
def exe_names(cls, interpreter: PythonInfo) -> set[str]:
|
||||
return {
|
||||
cls.exe_stem(),
|
||||
"python",
|
||||
f"python{interpreter.version_info.major}",
|
||||
f"python{interpreter.version_info.major}.{interpreter.version_info.minor}",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], RefMust, RefWhen], None, None]: # ty: ignore[invalid-method-override]
|
||||
host = Path(interpreter.system_executable) # ty: ignore[invalid-argument-type]
|
||||
targets = sorted(f"{name}{cls.suffix}" for name in cls.exe_names(interpreter))
|
||||
yield host, targets, RefMust.NA, RefWhen.ANY
|
||||
|
||||
|
||||
class RustPythonPosix(RustPython, PosixSupports):
|
||||
"""RustPython on POSIX."""
|
||||
|
||||
|
||||
class RustPythonWindows(RustPython, WindowsSupports):
|
||||
"""RustPython on Windows."""
|
||||
|
||||
|
||||
__all__ = [
|
||||
"RustPythonPosix",
|
||||
"RustPythonWindows",
|
||||
]
|
||||
BIN
Binary file not shown.
+135
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.create.via_global_ref.api import ViaGlobalRefApi, ViaGlobalRefMeta
|
||||
from virtualenv.create.via_global_ref.builtin.ref import (
|
||||
ExePathRefToDest,
|
||||
RefMust,
|
||||
RefWhen,
|
||||
)
|
||||
from virtualenv.util.path import ensure_dir
|
||||
|
||||
from .builtin_way import VirtualenvBuiltin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
from virtualenv.config.cli.parser import VirtualEnvOptions
|
||||
from virtualenv.create.via_global_ref.builtin.ref import PathRef
|
||||
from virtualenv.create.via_global_ref.venv import Venv
|
||||
|
||||
|
||||
class BuiltinViaGlobalRefMeta(ViaGlobalRefMeta):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.sources: list[PathRef] = []
|
||||
|
||||
|
||||
class ViaGlobalRefVirtualenvBuiltin(ViaGlobalRefApi, VirtualenvBuiltin, ABC):
|
||||
def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None:
|
||||
super().__init__(options, interpreter)
|
||||
self._sources: list[PathRef] = (
|
||||
getattr(options.meta, "sources", None) or []
|
||||
) # if created as a describer this might be missing
|
||||
|
||||
@classmethod
|
||||
def can_create(cls, interpreter: PythonInfo) -> BuiltinViaGlobalRefMeta | None:
|
||||
"""By default, all built-in methods assume that if we can describe it we can create it."""
|
||||
# first we must be able to describe it
|
||||
if not cls.can_describe(interpreter):
|
||||
return None
|
||||
meta = cls.setup_meta(interpreter)
|
||||
if meta is not None and meta:
|
||||
cls._sources_can_be_applied(interpreter, meta)
|
||||
return meta
|
||||
|
||||
@classmethod
|
||||
def _sources_can_be_applied(cls, interpreter: PythonInfo, meta: BuiltinViaGlobalRefMeta) -> None:
|
||||
for src in cls.sources(interpreter):
|
||||
if src.exists:
|
||||
if meta.can_copy and not src.can_copy:
|
||||
meta.copy_error = f"cannot copy {src}"
|
||||
if meta.can_symlink and not src.can_symlink:
|
||||
meta.symlink_error = f"cannot symlink {src}"
|
||||
else:
|
||||
msg = f"missing required file {src}"
|
||||
if src.when == RefMust.NA:
|
||||
meta.error = msg
|
||||
elif src.when == RefMust.COPY:
|
||||
meta.copy_error = msg
|
||||
elif src.when == RefMust.SYMLINK:
|
||||
meta.symlink_error = msg
|
||||
if not meta.can_copy and not meta.can_symlink:
|
||||
meta.error = f"neither copy or symlink supported, copy: {meta.copy_error} symlink: {meta.symlink_error}"
|
||||
if meta.error:
|
||||
break
|
||||
meta.sources.append(src)
|
||||
|
||||
@classmethod
|
||||
def setup_meta(cls, interpreter: PythonInfo) -> BuiltinViaGlobalRefMeta: # noqa: ARG003
|
||||
return BuiltinViaGlobalRefMeta()
|
||||
|
||||
@classmethod
|
||||
def sources(cls, interpreter: PythonInfo) -> Generator[ExePathRefToDest]:
|
||||
for host_exe, targets, must, when in cls._executables(interpreter):
|
||||
yield ExePathRefToDest(host_exe, dest=cls.to_bin, targets=targets, must=must, when=when)
|
||||
|
||||
def to_bin(self, src: Path) -> Path:
|
||||
return self.bin_dir / src.name
|
||||
|
||||
@classmethod
|
||||
def _executables(cls, interpreter: PythonInfo) -> Generator[tuple[Path, list[str], str, str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
def create(self) -> None:
|
||||
dirs = self.ensure_directories()
|
||||
for directory in list(dirs):
|
||||
if any(i for i in dirs if i is not directory and directory.parts == i.parts[: len(directory.parts)]):
|
||||
dirs.remove(directory)
|
||||
for directory in sorted(dirs):
|
||||
ensure_dir(directory)
|
||||
|
||||
self.set_pyenv_cfg()
|
||||
self.pyenv_cfg.write()
|
||||
true_system_site = self.enable_system_site_package
|
||||
try:
|
||||
self.enable_system_site_package = False
|
||||
for src in self._sources:
|
||||
if (
|
||||
src.when == RefWhen.ANY
|
||||
or (src.when == RefWhen.SYMLINK and self.symlinks is True)
|
||||
or (src.when == RefWhen.COPY and self.symlinks is False)
|
||||
):
|
||||
src.run(self, self.symlinks)
|
||||
finally:
|
||||
if true_system_site != self.enable_system_site_package:
|
||||
self.enable_system_site_package = true_system_site
|
||||
super().create()
|
||||
|
||||
@property
|
||||
def include_dir(self) -> Path:
|
||||
return self.dest / ("Include" if self.interpreter.os == "nt" else "include")
|
||||
|
||||
def install_venv_shared_libs(self, venv_creator: Venv) -> None:
|
||||
pass
|
||||
|
||||
def ensure_directories(self) -> set[Path]:
|
||||
return {self.dest, self.bin_dir, self.script_dir, self.stdlib, self.include_dir} | set(self.libs)
|
||||
|
||||
def set_pyenv_cfg(self) -> None:
|
||||
"""We directly inject the base prefix and base exec prefix to avoid site.py needing to discover these from home (which usually is done within the interpreter itself)."""
|
||||
super().set_pyenv_cfg()
|
||||
self.pyenv_cfg["base-prefix"] = self.interpreter.system_prefix
|
||||
self.pyenv_cfg["base-exec-prefix"] = self.interpreter.system_exec_prefix
|
||||
self.pyenv_cfg["base-executable"] = self.interpreter.system_executable # ty: ignore[invalid-assignment]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BuiltinViaGlobalRefMeta",
|
||||
"ViaGlobalRefVirtualenvBuiltin",
|
||||
]
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
from virtualenv.create.via_global_ref.api import ViaGlobalRefMeta
|
||||
|
||||
|
||||
def handle_store_python(meta: ViaGlobalRefMeta, interpreter: PythonInfo) -> ViaGlobalRefMeta:
|
||||
if is_store_python(interpreter):
|
||||
meta.symlink_error = "Windows Store Python does not support virtual environments via symlink"
|
||||
return meta
|
||||
|
||||
|
||||
def is_store_python(interpreter: PythonInfo) -> bool:
|
||||
parts = Path(interpreter.system_executable).parts # ty: ignore[invalid-argument-type]
|
||||
return (
|
||||
len(parts) > 4 # noqa: PLR2004
|
||||
and parts[-4] == "Microsoft"
|
||||
and parts[-3] == "WindowsApps"
|
||||
and parts[-2].startswith("PythonSoftwareFoundation.Python.3.")
|
||||
and parts[-1].startswith("python")
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"handle_store_python",
|
||||
"is_store_python",
|
||||
]
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from copy import copy
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from python_discovery import PythonInfo
|
||||
|
||||
from virtualenv.create.via_global_ref.store import handle_store_python
|
||||
from virtualenv.util.error import ProcessCallFailedError
|
||||
from virtualenv.util.path import ensure_dir
|
||||
from virtualenv.util.subprocess import run_cmd
|
||||
|
||||
from .api import ViaGlobalRefApi, ViaGlobalRefMeta
|
||||
from .builtin.cpython.common import is_mac_os_framework
|
||||
from .builtin.cpython.mac_os import CPython3macOsBrew
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from virtualenv.config.cli.parser import VirtualEnvOptions
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Venv(ViaGlobalRefApi):
|
||||
def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None:
|
||||
self.describe = options.describe
|
||||
super().__init__(options, interpreter)
|
||||
current = PythonInfo.current()
|
||||
self.can_be_inline = interpreter is current and interpreter.executable == interpreter.system_executable
|
||||
self._context = None
|
||||
|
||||
def _args(self) -> list[tuple[str, Any]]:
|
||||
return super()._args() + ([("describe", self.describe.__class__.__name__)] if self.describe else [])
|
||||
|
||||
@classmethod
|
||||
def can_create(cls, interpreter: PythonInfo) -> ViaGlobalRefMeta | None:
|
||||
if interpreter.has_venv:
|
||||
if CPython3macOsBrew.can_describe(interpreter):
|
||||
return CPython3macOsBrew.setup_meta(interpreter)
|
||||
meta = ViaGlobalRefMeta()
|
||||
if interpreter.platform == "win32":
|
||||
meta = handle_store_python(meta, interpreter)
|
||||
if is_mac_os_framework(interpreter):
|
||||
meta.copy_error = "macOS framework builds do not support copy-based virtual environments"
|
||||
return meta
|
||||
return None
|
||||
|
||||
def create(self) -> None:
|
||||
if self.can_be_inline:
|
||||
self.create_inline()
|
||||
else:
|
||||
self.create_via_sub_process()
|
||||
for lib in self.libs: # ty: ignore[not-iterable]
|
||||
ensure_dir(lib)
|
||||
if self.describe is not None:
|
||||
self.describe.install_venv_shared_libs(self)
|
||||
super().create()
|
||||
|
||||
def create_inline(self) -> None:
|
||||
from venv import EnvBuilder # noqa: PLC0415
|
||||
|
||||
builder = EnvBuilder(
|
||||
system_site_packages=self.enable_system_site_package,
|
||||
clear=False,
|
||||
symlinks=self.symlinks,
|
||||
with_pip=False,
|
||||
)
|
||||
builder.create(str(self.dest))
|
||||
|
||||
def create_via_sub_process(self) -> None:
|
||||
cmd = self.get_host_create_cmd()
|
||||
LOGGER.info("using host built-in venv to create via %s", " ".join(cmd))
|
||||
code, out, err = run_cmd(cmd)
|
||||
if code != 0:
|
||||
raise ProcessCallFailedError(code, out, err, cmd)
|
||||
|
||||
def get_host_create_cmd(self) -> list[str]:
|
||||
cmd = [self.interpreter.system_executable, "-m", "venv", "--without-pip"]
|
||||
if self.interpreter.version_info >= (3, 13):
|
||||
cmd.append("--without-scm-ignore-files")
|
||||
if self.enable_system_site_package:
|
||||
cmd.append("--system-site-packages")
|
||||
cmd.extend(("--symlinks" if self.symlinks else "--copies", str(self.dest)))
|
||||
return cmd # ty: ignore[invalid-return-type]
|
||||
|
||||
def set_pyenv_cfg(self) -> None:
|
||||
# prefer venv options over ours, but keep our extra
|
||||
venv_content = copy(self.pyenv_cfg.refresh())
|
||||
super().set_pyenv_cfg()
|
||||
self.pyenv_cfg.update(venv_content)
|
||||
|
||||
def __getattribute__(self, item: str) -> object:
|
||||
describe = object.__getattribute__(self, "describe")
|
||||
if describe is not None and hasattr(describe, item):
|
||||
element = getattr(describe, item)
|
||||
if not callable(element) or item == "script":
|
||||
return element
|
||||
return object.__getattribute__(self, item)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Venv",
|
||||
]
|
||||
Reference in New Issue
Block a user