juc500 정상동작

This commit is contained in:
Macbook
2026-07-21 21:04:25 +09:00
parent a423db0556
commit bb9874b931
2917 changed files with 708025 additions and 668 deletions
@@ -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",
]
@@ -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())
@@ -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",
]
@@ -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",
]
@@ -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",
]
@@ -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",
]
@@ -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",
]
@@ -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",
]
@@ -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",
]
@@ -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",
]
@@ -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",
]
@@ -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",
]
@@ -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",
]
@@ -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",
]
@@ -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",
]