Current File : /home/inlingua/miniconda3/lib/python3.12/site-packages/conda/core/path_actions.py |
# Copyright (C) 2012 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
"""Atomic actions that make up a package installation or removal transaction."""
import re
import sys
from abc import ABCMeta, abstractmethod, abstractproperty
from itertools import chain
from json import JSONDecodeError
from logging import getLogger
from os.path import basename, dirname, getsize, isdir, join
from uuid import uuid4
from .. import CondaError
from ..auxlib.ish import dals
from ..base.constants import CONDA_TEMP_EXTENSION
from ..base.context import context
from ..common.compat import on_win
from ..common.constants import TRACE
from ..common.path import (
BIN_DIRECTORY,
get_leaf_directories,
get_python_noarch_target_path,
get_python_short_path,
parse_entry_point_def,
pyc_path,
url_to_path,
win_path_ok,
)
from ..common.url import has_platform, path_to_url
from ..exceptions import (
CondaUpgradeError,
CondaVerificationError,
NotWritableError,
PaddingError,
SafetyError,
)
from ..gateways.connection.download import download
from ..gateways.disk.create import (
compile_multiple_pyc,
copy,
create_hard_link_or_copy,
create_link,
create_python_entry_point,
extract_tarball,
make_menu,
mkdir_p,
write_as_json_to_file,
)
from ..gateways.disk.delete import rm_rf
from ..gateways.disk.permissions import make_writable
from ..gateways.disk.read import compute_sum, islink, lexists, read_index_json
from ..gateways.disk.update import backoff_rename, touch
from ..history import History
from ..models.channel import Channel
from ..models.enums import LinkType, NoarchType, PathType
from ..models.match_spec import MatchSpec
from ..models.records import (
Link,
PackageCacheRecord,
PackageRecord,
PathDataV1,
PathsData,
PrefixRecord,
)
from .envs_manager import get_user_environments_txt_file, register_env, unregister_env
from .portability import _PaddingError, update_prefix
from .prefix_data import PrefixData
try:
FileNotFoundError
except NameError:
FileNotFoundError = IOError
log = getLogger(__name__)
_MENU_RE = re.compile(r"^menu/.*\.json$", re.IGNORECASE)
REPR_IGNORE_KWARGS = (
"transaction_context",
"package_info",
"hold_path",
)
class _Action(metaclass=ABCMeta):
_verified = False
@abstractmethod
def verify(self):
# if verify fails, it should return an exception object rather than raise
# at the end of a verification run, all errors will be raised as a CondaMultiError
# after successful verification, the verify method should set self._verified = True
raise NotImplementedError()
@abstractmethod
def execute(self):
raise NotImplementedError()
@abstractmethod
def reverse(self):
raise NotImplementedError()
@abstractmethod
def cleanup(self):
raise NotImplementedError()
@property
def verified(self):
return self._verified
def __repr__(self):
args = (
f"{key}={value!r}"
for key, value in vars(self).items()
if key not in REPR_IGNORE_KWARGS
)
return "{}({})".format(self.__class__.__name__, ", ".join(args))
class PathAction(_Action, metaclass=ABCMeta):
@abstractproperty
def target_full_path(self):
raise NotImplementedError()
class MultiPathAction(_Action, metaclass=ABCMeta):
@abstractproperty
def target_full_paths(self):
raise NotImplementedError()
class PrefixPathAction(PathAction, metaclass=ABCMeta):
def __init__(self, transaction_context, target_prefix, target_short_path):
self.transaction_context = transaction_context
self.target_prefix = target_prefix
self.target_short_path = target_short_path
@property
def target_short_paths(self):
return (self.target_short_path,)
@property
def target_full_path(self):
trgt, shrt_pth = self.target_prefix, self.target_short_path
if trgt is not None and shrt_pth is not None:
return join(trgt, win_path_ok(shrt_pth))
else:
return None
# ######################################################
# Creation of Paths within a Prefix
# ######################################################
class CreateInPrefixPathAction(PrefixPathAction, metaclass=ABCMeta):
# All CreatePathAction subclasses must create a SINGLE new path
# the short/in-prefix version of that path must be returned by execute()
def __init__(
self,
transaction_context,
package_info,
source_prefix,
source_short_path,
target_prefix,
target_short_path,
):
super().__init__(transaction_context, target_prefix, target_short_path)
self.package_info = package_info
self.source_prefix = source_prefix
self.source_short_path = source_short_path
def verify(self):
self._verified = True
def cleanup(self):
# create actions typically won't need cleanup
pass
@property
def source_full_path(self):
prfx, shrt_pth = self.source_prefix, self.source_short_path
return join(prfx, win_path_ok(shrt_pth)) if prfx and shrt_pth else None
class LinkPathAction(CreateInPrefixPathAction):
@classmethod
def create_file_link_actions(
cls, transaction_context, package_info, target_prefix, requested_link_type
):
def get_prefix_replace(source_path_data):
if source_path_data.path_type == PathType.softlink:
link_type = LinkType.copy
prefix_placehoder, file_mode = "", None
elif source_path_data.prefix_placeholder:
link_type = LinkType.copy
prefix_placehoder = source_path_data.prefix_placeholder
file_mode = source_path_data.file_mode
elif source_path_data.no_link:
link_type = LinkType.copy
prefix_placehoder, file_mode = "", None
else:
link_type = requested_link_type
prefix_placehoder, file_mode = "", None
return link_type, prefix_placehoder, file_mode
def make_file_link_action(source_path_data):
# TODO: this inner function is still kind of a mess
noarch = package_info.repodata_record.noarch
if noarch is None and package_info.package_metadata is not None:
# Look in package metadata in case it was omitted from repodata (see issue #8311)
noarch = package_info.package_metadata.noarch
if noarch is not None:
noarch = noarch.type
if noarch == NoarchType.python:
sp_dir = transaction_context["target_site_packages_short_path"]
if sp_dir is None:
raise CondaError(
"Unable to determine python site-packages "
"dir in target_prefix!\nPlease make sure "
f"python is installed in {target_prefix}"
)
target_short_path = get_python_noarch_target_path(
source_path_data.path, sp_dir
)
elif noarch is None or noarch == NoarchType.generic:
target_short_path = source_path_data.path
else:
raise CondaUpgradeError(
dals(
"""
The current version of conda is too old to install this package.
Please update conda."""
)
)
link_type, placeholder, fmode = get_prefix_replace(source_path_data)
if placeholder:
return PrefixReplaceLinkAction(
transaction_context,
package_info,
package_info.extracted_package_dir,
source_path_data.path,
target_prefix,
target_short_path,
requested_link_type,
placeholder,
fmode,
source_path_data,
)
else:
return LinkPathAction(
transaction_context,
package_info,
package_info.extracted_package_dir,
source_path_data.path,
target_prefix,
target_short_path,
link_type,
source_path_data,
)
return tuple(
make_file_link_action(spi) for spi in package_info.paths_data.paths
)
@classmethod
def create_directory_actions(
cls,
transaction_context,
package_info,
target_prefix,
requested_link_type,
file_link_actions,
):
leaf_directories = get_leaf_directories(
axn.target_short_path for axn in file_link_actions
)
return tuple(
cls(
transaction_context,
package_info,
None,
None,
target_prefix,
directory_short_path,
LinkType.directory,
None,
)
for directory_short_path in leaf_directories
)
@classmethod
def create_python_entry_point_windows_exe_action(
cls,
transaction_context,
package_info,
target_prefix,
requested_link_type,
entry_point_def,
):
source_directory = context.conda_prefix
source_short_path = "Scripts/conda.exe"
command, _, _ = parse_entry_point_def(entry_point_def)
target_short_path = f"Scripts/{command}.exe"
source_path_data = PathDataV1(
_path=target_short_path,
path_type=PathType.windows_python_entry_point_exe,
)
return cls(
transaction_context,
package_info,
source_directory,
source_short_path,
target_prefix,
target_short_path,
requested_link_type,
source_path_data,
)
def __init__(
self,
transaction_context,
package_info,
extracted_package_dir,
source_short_path,
target_prefix,
target_short_path,
link_type,
source_path_data,
):
super().__init__(
transaction_context,
package_info,
extracted_package_dir,
source_short_path,
target_prefix,
target_short_path,
)
self.link_type = link_type
self._execute_successful = False
self.source_path_data = source_path_data
self.prefix_path_data = None
def verify(self):
if self.link_type != LinkType.directory and not lexists(
self.source_full_path
): # pragma: no cover
return CondaVerificationError(
dals(
f"""
The package for {self.package_info.repodata_record.name} located at {self.package_info.extracted_package_dir}
appears to be corrupted. The path '{self.source_short_path}'
specified in the package manifest cannot be found.
"""
)
)
source_path_data = self.source_path_data
try:
source_path_type = source_path_data.path_type
except AttributeError:
source_path_type = None
if source_path_type in PathType.basic_types:
# this let's us keep the non-generic path types like windows_python_entry_point_exe
source_path_type = None
if self.link_type == LinkType.directory:
self.prefix_path_data = None
elif self.link_type == LinkType.softlink:
self.prefix_path_data = PathDataV1.from_objects(
self.source_path_data,
path_type=source_path_type or PathType.softlink,
)
elif (
self.link_type == LinkType.copy
and source_path_data.path_type == PathType.softlink
):
self.prefix_path_data = PathDataV1.from_objects(
self.source_path_data,
path_type=source_path_type or PathType.softlink,
)
elif source_path_data.path_type == PathType.hardlink:
try:
reported_size_in_bytes = source_path_data.size_in_bytes
except AttributeError:
reported_size_in_bytes = None
source_size_in_bytes = 0
if reported_size_in_bytes:
source_size_in_bytes = getsize(self.source_full_path)
if reported_size_in_bytes != source_size_in_bytes:
return SafetyError(
dals(
f"""
The package for {self.package_info.repodata_record.name} located at {self.package_info.extracted_package_dir}
appears to be corrupted. The path '{self.source_short_path}'
has an incorrect size.
reported size: {reported_size_in_bytes} bytes
actual size: {source_size_in_bytes} bytes
"""
)
)
try:
reported_sha256 = source_path_data.sha256
except AttributeError:
reported_sha256 = None
# sha256 is expensive. Only run if file sizes agree, and then only if enabled
if (
source_size_in_bytes
and reported_size_in_bytes == source_size_in_bytes
and context.extra_safety_checks
):
source_sha256 = compute_sum(self.source_full_path, "sha256")
if reported_sha256 and reported_sha256 != source_sha256:
return SafetyError(
dals(
f"""
The package for {self.package_info.repodata_record.name} located at {self.package_info.extracted_package_dir}
appears to be corrupted. The path '{self.source_short_path}'
has a sha256 mismatch.
reported sha256: {reported_sha256}
actual sha256: {source_sha256}
"""
)
)
self.prefix_path_data = PathDataV1.from_objects(
source_path_data,
sha256=reported_sha256,
sha256_in_prefix=reported_sha256,
path_type=source_path_type or PathType.hardlink,
)
elif source_path_data.path_type == PathType.windows_python_entry_point_exe:
self.prefix_path_data = source_path_data
else:
raise NotImplementedError()
self._verified = True
def execute(self):
log.log(TRACE, "linking %s => %s", self.source_full_path, self.target_full_path)
create_link(
self.source_full_path,
self.target_full_path,
self.link_type,
force=context.force,
)
self._execute_successful = True
def reverse(self):
if self._execute_successful:
log.log(TRACE, "reversing link creation %s", self.target_prefix)
if not isdir(self.target_full_path):
rm_rf(self.target_full_path, clean_empty_parents=True)
class PrefixReplaceLinkAction(LinkPathAction):
def __init__(
self,
transaction_context,
package_info,
extracted_package_dir,
source_short_path,
target_prefix,
target_short_path,
link_type,
prefix_placeholder,
file_mode,
source_path_data,
):
# This link_type used in execute(). Make sure we always respect LinkType.copy request.
link_type = LinkType.copy if link_type == LinkType.copy else LinkType.hardlink
super().__init__(
transaction_context,
package_info,
extracted_package_dir,
source_short_path,
target_prefix,
target_short_path,
link_type,
source_path_data,
)
self.prefix_placeholder = prefix_placeholder
self.file_mode = file_mode
self.intermediate_path = None
def verify(self):
validation_error = super().verify()
if validation_error:
return validation_error
if islink(self.source_full_path):
log.log(
TRACE,
"ignoring prefix update for symlink with source path %s",
self.source_full_path,
)
# return
assert False, "I don't think this is the right place to ignore this"
mkdir_p(self.transaction_context["temp_dir"])
self.intermediate_path = join(
self.transaction_context["temp_dir"], str(uuid4())
)
log.log(
TRACE, "copying %s => %s", self.source_full_path, self.intermediate_path
)
create_link(self.source_full_path, self.intermediate_path, LinkType.copy)
make_writable(self.intermediate_path)
try:
log.log(TRACE, "rewriting prefixes in %s", self.target_full_path)
update_prefix(
self.intermediate_path,
context.target_prefix_override or self.target_prefix,
self.prefix_placeholder,
self.file_mode,
subdir=self.package_info.repodata_record.subdir,
)
except _PaddingError:
raise PaddingError(
self.target_full_path,
self.prefix_placeholder,
len(self.prefix_placeholder),
)
sha256_in_prefix = compute_sum(self.intermediate_path, "sha256")
self.prefix_path_data = PathDataV1.from_objects(
self.prefix_path_data,
file_mode=self.file_mode,
path_type=PathType.hardlink,
prefix_placeholder=self.prefix_placeholder,
sha256_in_prefix=sha256_in_prefix,
)
self._verified = True
def execute(self):
if not self._verified:
self.verify()
source_path = self.intermediate_path or self.source_full_path
log.log(TRACE, "linking %s => %s", source_path, self.target_full_path)
create_link(source_path, self.target_full_path, self.link_type)
self._execute_successful = True
class MakeMenuAction(CreateInPrefixPathAction):
@classmethod
def create_actions(
cls, transaction_context, package_info, target_prefix, requested_link_type
):
shorcuts_lower = [name.lower() for name in (context.shortcuts_only or ())]
if context.shortcuts and (
not context.shortcuts_only
or (shorcuts_lower and package_info.name.lower() in shorcuts_lower)
):
return tuple(
cls(transaction_context, package_info, target_prefix, spi.path)
for spi in package_info.paths_data.paths
if bool(_MENU_RE.match(spi.path))
)
else:
return ()
def __init__(
self, transaction_context, package_info, target_prefix, target_short_path
):
super().__init__(
transaction_context,
package_info,
None,
None,
target_prefix,
target_short_path,
)
self._execute_successful = False
def execute(self):
log.log(TRACE, "making menu for %s", self.target_full_path)
make_menu(self.target_prefix, self.target_short_path, remove=False)
self._execute_successful = True
def reverse(self):
if self._execute_successful:
log.log(TRACE, "removing menu for %s", self.target_full_path)
make_menu(self.target_prefix, self.target_short_path, remove=True)
class CompileMultiPycAction(MultiPathAction):
@classmethod
def create_actions(
cls,
transaction_context,
package_info,
target_prefix,
requested_link_type,
file_link_actions,
):
noarch = package_info.package_metadata and package_info.package_metadata.noarch
if noarch is not None and noarch.type == NoarchType.python:
noarch_py_file_re = re.compile(r"^site-packages[/\\][^\t\n\r\f\v]+\.py$")
py_ver = transaction_context["target_python_version"]
py_files = tuple(
axn.target_short_path
for axn in file_link_actions
if getattr(axn, "source_short_path")
and noarch_py_file_re.match(axn.source_short_path)
)
pyc_files = tuple(pyc_path(pf, py_ver) for pf in py_files)
return (
cls(
transaction_context,
package_info,
target_prefix,
py_files,
pyc_files,
),
)
else:
return ()
def __init__(
self,
transaction_context,
package_info,
target_prefix,
source_short_paths,
target_short_paths,
):
self.transaction_context = transaction_context
self.package_info = package_info
self.target_prefix = target_prefix
self.source_short_paths = source_short_paths
self.target_short_paths = target_short_paths
self.prefix_path_data = None
self.prefix_paths_data = [
PathDataV1(
_path=p,
path_type=PathType.pyc_file,
)
for p in self.target_short_paths
]
self._execute_successful = False
@property
def target_full_paths(self):
def join_or_none(prefix, short_path):
if prefix is None or short_path is None:
return None
else:
return join(prefix, win_path_ok(short_path))
return (join_or_none(self.target_prefix, p) for p in self.target_short_paths)
@property
def source_full_paths(self):
def join_or_none(prefix, short_path):
if prefix is None or short_path is None:
return None
else:
return join(prefix, win_path_ok(short_path))
return (join_or_none(self.target_prefix, p) for p in self.source_short_paths)
def verify(self):
self._verified = True
def cleanup(self):
# create actions typically won't need cleanup
pass
def execute(self):
# compile_pyc is sometimes expected to fail, for example a python 3.6 file
# installed into a python 2 environment, but no code paths actually importing it
# technically then, this file should be removed from the manifest in conda-meta, but
# at the time of this writing that's not currently happening
log.log(TRACE, "compiling %s", " ".join(self.target_full_paths))
target_python_version = self.transaction_context["target_python_version"]
python_short_path = get_python_short_path(target_python_version)
python_full_path = join(self.target_prefix, win_path_ok(python_short_path))
compile_multiple_pyc(
python_full_path,
self.source_full_paths,
self.target_full_paths,
self.target_prefix,
self.transaction_context["target_python_version"],
)
self._execute_successful = True
def reverse(self):
# this removes all pyc files even if they were not created
if self._execute_successful:
log.log(
TRACE, "reversing pyc creation %s", " ".join(self.target_full_paths)
)
for target_full_path in self.target_full_paths:
rm_rf(target_full_path)
class AggregateCompileMultiPycAction(CompileMultiPycAction):
"""Bunch up all of our compile actions, so that they all get carried out at once.
This avoids clobbering and is faster when we have several individual packages requiring
compilation.
"""
def __init__(self, *individuals, **kw):
transaction_context = individuals[0].transaction_context
# not used; doesn't matter
package_info = individuals[0].package_info
target_prefix = individuals[0].target_prefix
source_short_paths = set()
target_short_paths = set()
for individual in individuals:
source_short_paths.update(individual.source_short_paths)
target_short_paths.update(individual.target_short_paths)
super().__init__(
transaction_context,
package_info,
target_prefix,
source_short_paths,
target_short_paths,
)
class CreatePythonEntryPointAction(CreateInPrefixPathAction):
@classmethod
def create_actions(
cls, transaction_context, package_info, target_prefix, requested_link_type
):
noarch = package_info.package_metadata and package_info.package_metadata.noarch
if noarch is not None and noarch.type == NoarchType.python:
def this_triplet(entry_point_def):
command, module, func = parse_entry_point_def(entry_point_def)
target_short_path = f"{BIN_DIRECTORY}/{command}"
if on_win:
target_short_path += "-script.py"
return target_short_path, module, func
actions = tuple(
cls(
transaction_context,
package_info,
target_prefix,
*this_triplet(ep_def),
)
for ep_def in noarch.entry_points or ()
)
if on_win: # pragma: unix no cover
actions += tuple(
LinkPathAction.create_python_entry_point_windows_exe_action(
transaction_context,
package_info,
target_prefix,
requested_link_type,
ep_def,
)
for ep_def in noarch.entry_points or ()
)
return actions
else:
return ()
def __init__(
self,
transaction_context,
package_info,
target_prefix,
target_short_path,
module,
func,
):
super().__init__(
transaction_context,
package_info,
None,
None,
target_prefix,
target_short_path,
)
self.module = module
self.func = func
if on_win:
path_type = PathType.windows_python_entry_point_script
else:
path_type = PathType.unix_python_entry_point
self.prefix_path_data = PathDataV1(
_path=self.target_short_path,
path_type=path_type,
)
self._execute_successful = False
def execute(self):
log.log(TRACE, "creating python entry point %s", self.target_full_path)
if on_win:
python_full_path = None
else:
target_python_version = self.transaction_context["target_python_version"]
python_short_path = get_python_short_path(target_python_version)
python_full_path = join(
context.target_prefix_override or self.target_prefix,
win_path_ok(python_short_path),
)
create_python_entry_point(
self.target_full_path, python_full_path, self.module, self.func
)
self._execute_successful = True
def reverse(self):
if self._execute_successful:
log.log(
TRACE, "reversing python entry point creation %s", self.target_full_path
)
rm_rf(self.target_full_path)
class CreatePrefixRecordAction(CreateInPrefixPathAction):
# this is the action that creates a packages json file in the conda-meta/ directory
@classmethod
def create_actions(
cls,
transaction_context,
package_info,
target_prefix,
requested_link_type,
requested_spec,
all_link_path_actions,
):
extracted_package_dir = package_info.extracted_package_dir
target_short_path = f"conda-meta/{basename(extracted_package_dir)}.json"
return (
cls(
transaction_context,
package_info,
target_prefix,
target_short_path,
requested_link_type,
requested_spec,
all_link_path_actions,
),
)
def __init__(
self,
transaction_context,
package_info,
target_prefix,
target_short_path,
requested_link_type,
requested_spec,
all_link_path_actions,
):
super().__init__(
transaction_context,
package_info,
None,
None,
target_prefix,
target_short_path,
)
self.requested_link_type = requested_link_type
self.requested_spec = requested_spec
self.all_link_path_actions = list(all_link_path_actions)
self._execute_successful = False
def execute(self):
link = Link(
source=self.package_info.extracted_package_dir,
type=self.requested_link_type,
)
extracted_package_dir = self.package_info.extracted_package_dir
package_tarball_full_path = self.package_info.package_tarball_full_path
def files_from_action(link_path_action):
if isinstance(link_path_action, CompileMultiPycAction):
return link_path_action.target_short_paths
else:
return (
(link_path_action.target_short_path,)
if isinstance(link_path_action, CreateInPrefixPathAction)
and (
not hasattr(link_path_action, "link_type")
or link_path_action.link_type != LinkType.directory
)
else ()
)
def paths_from_action(link_path_action):
if isinstance(link_path_action, CompileMultiPycAction):
return link_path_action.prefix_paths_data
else:
if (
not hasattr(link_path_action, "prefix_path_data")
or link_path_action.prefix_path_data is None
):
return ()
else:
return (link_path_action.prefix_path_data,)
files = list(
chain.from_iterable(
files_from_action(x) for x in self.all_link_path_actions if x
)
)
paths_data = PathsData(
paths_version=1,
paths=chain.from_iterable(
paths_from_action(x) for x in self.all_link_path_actions if x
),
)
self.prefix_record = PrefixRecord.from_objects(
self.package_info.repodata_record,
# self.package_info.index_json_record,
self.package_info.package_metadata,
requested_spec=str(self.requested_spec),
paths_data=paths_data,
files=files,
link=link,
url=self.package_info.url,
extracted_package_dir=extracted_package_dir,
package_tarball_full_path=package_tarball_full_path,
)
log.log(TRACE, "creating linked package record %s", self.target_full_path)
PrefixData(self.target_prefix).insert(self.prefix_record)
self._execute_successful = True
def reverse(self):
log.log(
TRACE, "reversing linked package record creation %s", self.target_full_path
)
if self._execute_successful:
PrefixData(self.target_prefix).remove(
self.package_info.repodata_record.name
)
class UpdateHistoryAction(CreateInPrefixPathAction):
@classmethod
def create_actions(
cls,
transaction_context,
target_prefix,
remove_specs,
update_specs,
neutered_specs,
):
target_short_path = join("conda-meta", "history")
return (
cls(
transaction_context,
target_prefix,
target_short_path,
remove_specs,
update_specs,
neutered_specs,
),
)
def __init__(
self,
transaction_context,
target_prefix,
target_short_path,
remove_specs,
update_specs,
neutered_specs,
):
super().__init__(
transaction_context, None, None, None, target_prefix, target_short_path
)
self.remove_specs = remove_specs
self.update_specs = update_specs
self.neutered_specs = neutered_specs
self.hold_path = self.target_full_path + CONDA_TEMP_EXTENSION
def execute(self):
log.log(TRACE, "updating environment history %s", self.target_full_path)
if lexists(self.target_full_path):
copy(self.target_full_path, self.hold_path)
h = History(self.target_prefix)
h.update()
h.write_specs(self.remove_specs, self.update_specs, self.neutered_specs)
def reverse(self):
if lexists(self.hold_path):
log.log(TRACE, "moving %s => %s", self.hold_path, self.target_full_path)
backoff_rename(self.hold_path, self.target_full_path, force=True)
def cleanup(self):
rm_rf(self.hold_path)
class RegisterEnvironmentLocationAction(PathAction):
def __init__(self, transaction_context, target_prefix):
self.transaction_context = transaction_context
self.target_prefix = target_prefix
self._execute_successful = False
def verify(self):
user_environments_txt_file = get_user_environments_txt_file()
try:
touch(user_environments_txt_file, mkdir=True, sudo_safe=True)
self._verified = True
except NotWritableError:
log.warning(
"Unable to create environments file. Path not writable.\n"
" environment location: %s\n",
user_environments_txt_file,
)
def execute(self):
log.log(TRACE, "registering environment in catalog %s", self.target_prefix)
register_env(self.target_prefix)
self._execute_successful = True
def reverse(self):
pass
def cleanup(self):
pass
@property
def target_full_path(self):
raise NotImplementedError()
# ######################################################
# Removal of Paths within a Prefix
# ######################################################
class RemoveFromPrefixPathAction(PrefixPathAction, metaclass=ABCMeta):
def __init__(
self, transaction_context, linked_package_data, target_prefix, target_short_path
):
super().__init__(transaction_context, target_prefix, target_short_path)
self.linked_package_data = linked_package_data
def verify(self):
# inability to remove will trigger a rollback
# can't definitely know if path can be removed until it's attempted and failed
self._verified = True
class UnlinkPathAction(RemoveFromPrefixPathAction):
def __init__(
self,
transaction_context,
linked_package_data,
target_prefix,
target_short_path,
link_type=LinkType.hardlink,
):
super().__init__(
transaction_context, linked_package_data, target_prefix, target_short_path
)
self.holding_short_path = self.target_short_path + CONDA_TEMP_EXTENSION
self.holding_full_path = self.target_full_path + CONDA_TEMP_EXTENSION
self.link_type = link_type
def execute(self):
if self.link_type != LinkType.directory:
log.log(
TRACE,
"renaming %s => %s",
self.target_short_path,
self.holding_short_path,
)
backoff_rename(self.target_full_path, self.holding_full_path, force=True)
def reverse(self):
if self.link_type != LinkType.directory and lexists(self.holding_full_path):
log.log(
TRACE,
"reversing rename %s => %s",
self.holding_short_path,
self.target_short_path,
)
backoff_rename(self.holding_full_path, self.target_full_path, force=True)
def cleanup(self):
if not isdir(self.holding_full_path):
rm_rf(self.holding_full_path, clean_empty_parents=True)
class RemoveMenuAction(RemoveFromPrefixPathAction):
@classmethod
def create_actions(cls, transaction_context, linked_package_data, target_prefix):
return tuple(
cls(transaction_context, linked_package_data, target_prefix, trgt)
for trgt in linked_package_data.files
if bool(_MENU_RE.match(trgt))
)
def __init__(
self, transaction_context, linked_package_data, target_prefix, target_short_path
):
super().__init__(
transaction_context, linked_package_data, target_prefix, target_short_path
)
def execute(self):
log.log(TRACE, "removing menu for %s ", self.target_prefix)
make_menu(self.target_prefix, self.target_short_path, remove=True)
def reverse(self):
log.log(TRACE, "re-creating menu for %s ", self.target_prefix)
make_menu(self.target_prefix, self.target_short_path, remove=False)
def cleanup(self):
pass
class RemoveLinkedPackageRecordAction(UnlinkPathAction):
def __init__(
self, transaction_context, linked_package_data, target_prefix, target_short_path
):
super().__init__(
transaction_context, linked_package_data, target_prefix, target_short_path
)
def execute(self):
super().execute()
PrefixData(self.target_prefix).remove(self.linked_package_data.name)
def reverse(self):
super().reverse()
PrefixData(self.target_prefix)._load_single_record(self.target_full_path)
class UnregisterEnvironmentLocationAction(PathAction):
def __init__(self, transaction_context, target_prefix):
self.transaction_context = transaction_context
self.target_prefix = target_prefix
self._execute_successful = False
def verify(self):
self._verified = True
def execute(self):
log.log(TRACE, "unregistering environment in catalog %s", self.target_prefix)
unregister_env(self.target_prefix)
self._execute_successful = True
def reverse(self):
pass
def cleanup(self):
pass
@property
def target_full_path(self):
raise NotImplementedError()
# ######################################################
# Fetch / Extract Actions
# ######################################################
class CacheUrlAction(PathAction):
def __init__(
self,
url,
target_pkgs_dir,
target_package_basename,
sha256=None,
size=None,
md5=None,
):
self.url = url
self.target_pkgs_dir = target_pkgs_dir
self.target_package_basename = target_package_basename
self.sha256 = sha256
self.size = size
self.md5 = md5
self.hold_path = self.target_full_path + CONDA_TEMP_EXTENSION
def verify(self):
assert "::" not in self.url
self._verified = True
def execute(self, progress_update_callback=None):
# I hate inline imports, but I guess it's ok since we're importing from the conda.core
# The alternative is passing the PackageCache class to CacheUrlAction __init__
from .package_cache_data import PackageCacheData
target_package_cache = PackageCacheData(self.target_pkgs_dir)
log.log(TRACE, "caching url %s => %s", self.url, self.target_full_path)
if lexists(self.hold_path):
rm_rf(self.hold_path)
if lexists(self.target_full_path):
if self.url.startswith("file:/") and self.url == path_to_url(
self.target_full_path
):
# the source and destination are the same file, so we're done
return
else:
backoff_rename(self.target_full_path, self.hold_path, force=True)
if self.url.startswith("file:/"):
source_path = url_to_path(self.url)
self._execute_local(
source_path, target_package_cache, progress_update_callback
)
else:
self._execute_channel(target_package_cache, progress_update_callback)
def _execute_local(
self, source_path, target_package_cache, progress_update_callback=None
):
from .package_cache_data import PackageCacheData
if dirname(source_path) in context.pkgs_dirs:
# if url points to another package cache, link to the writable cache
create_hard_link_or_copy(source_path, self.target_full_path)
source_package_cache = PackageCacheData(dirname(source_path))
# the package is already in a cache, so it came from a remote url somewhere;
# make sure that remote url is the most recent url in the
# writable cache urls.txt
origin_url = source_package_cache._urls_data.get_url(
self.target_package_basename
)
if origin_url and has_platform(origin_url, context.known_subdirs):
target_package_cache._urls_data.add_url(origin_url)
else:
# so our tarball source isn't a package cache, but that doesn't mean it's not
# in another package cache somewhere
# let's try to find the actual, remote source url by matching md5sums, and then
# record that url as the remote source url in urls.txt
# we do the search part of this operation before the create_link so that we
# don't md5sum-match the file created by 'create_link'
# there is no point in looking for the tarball in the cache that we are writing
# this file into because we have already removed the previous file if there was
# any. This also makes sure that we ignore the md5sum of a possible extracted
# directory that might exist in this cache because we are going to overwrite it
# anyway when we extract the tarball.
source_md5sum = compute_sum(source_path, "md5")
exclude_caches = (self.target_pkgs_dir,)
pc_entry = PackageCacheData.tarball_file_in_cache(
source_path, source_md5sum, exclude_caches=exclude_caches
)
if pc_entry:
origin_url = target_package_cache._urls_data.get_url(
pc_entry.extracted_package_dir
)
else:
origin_url = None
# copy the tarball to the writable cache
create_link(
source_path,
self.target_full_path,
link_type=LinkType.copy,
force=context.force,
)
if origin_url and has_platform(origin_url, context.known_subdirs):
target_package_cache._urls_data.add_url(origin_url)
else:
target_package_cache._urls_data.add_url(self.url)
def _execute_channel(self, target_package_cache, progress_update_callback=None):
kwargs = {}
if self.size is not None:
kwargs["size"] = self.size
if self.sha256:
kwargs["sha256"] = self.sha256
elif self.md5:
kwargs["md5"] = self.md5
download(
self.url,
self.target_full_path,
progress_update_callback=progress_update_callback,
**kwargs,
)
target_package_cache._urls_data.add_url(self.url)
def reverse(self):
if lexists(self.hold_path):
log.log(TRACE, "moving %s => %s", self.hold_path, self.target_full_path)
backoff_rename(self.hold_path, self.target_full_path, force=True)
def cleanup(self):
rm_rf(self.hold_path)
@property
def target_full_path(self):
return join(self.target_pkgs_dir, self.target_package_basename)
def __str__(self):
return f"CacheUrlAction<url={self.url!r}, target_full_path={self.target_full_path!r}>"
class ExtractPackageAction(PathAction):
def __init__(
self,
source_full_path,
target_pkgs_dir,
target_extracted_dirname,
record_or_spec,
sha256,
size,
md5,
):
self.source_full_path = source_full_path
self.target_pkgs_dir = target_pkgs_dir
self.target_extracted_dirname = target_extracted_dirname
self.hold_path = self.target_full_path + CONDA_TEMP_EXTENSION
self.record_or_spec = record_or_spec
self.sha256 = sha256
self.size = size
self.md5 = md5
def verify(self):
self._verified = True
def execute(self, progress_update_callback=None):
# I hate inline imports, but I guess it's ok since we're importing from the conda.core
# The alternative is passing the the classes to ExtractPackageAction __init__
from .package_cache_data import PackageCacheData
log.log(
TRACE, "extracting %s => %s", self.source_full_path, self.target_full_path
)
if lexists(self.target_full_path):
rm_rf(self.target_full_path)
extract_tarball(
self.source_full_path,
self.target_full_path,
progress_update_callback=progress_update_callback,
)
try:
raw_index_json = read_index_json(self.target_full_path)
except (OSError, JSONDecodeError, FileNotFoundError):
# At this point, we can assume the package tarball is bad.
# Remove everything and move on.
print(
f"ERROR: Encountered corrupt package tarball at {self.source_full_path}. Conda has "
"left it in place. Please report this to the maintainers "
"of the package."
)
sys.exit(1)
if isinstance(self.record_or_spec, MatchSpec):
url = self.record_or_spec.get_raw_value("url")
assert url
channel = (
Channel(url)
if has_platform(url, context.known_subdirs)
else Channel(None)
)
fn = basename(url)
sha256 = self.sha256 or compute_sum(self.source_full_path, "sha256")
size = getsize(self.source_full_path)
if self.size is not None:
assert size == self.size, (size, self.size)
md5 = self.md5 or compute_sum(self.source_full_path, "md5")
repodata_record = PackageRecord.from_objects(
raw_index_json,
url=url,
channel=channel,
fn=fn,
sha256=sha256,
size=size,
md5=md5,
)
else:
repodata_record = PackageRecord.from_objects(
self.record_or_spec, raw_index_json
)
repodata_record_path = join(
self.target_full_path, "info", "repodata_record.json"
)
write_as_json_to_file(repodata_record_path, repodata_record)
target_package_cache = PackageCacheData(self.target_pkgs_dir)
package_cache_record = PackageCacheRecord.from_objects(
repodata_record,
package_tarball_full_path=self.source_full_path,
extracted_package_dir=self.target_full_path,
)
target_package_cache.insert(package_cache_record)
def reverse(self):
rm_rf(self.target_full_path)
if lexists(self.hold_path):
log.log(TRACE, "moving %s => %s", self.hold_path, self.target_full_path)
rm_rf(self.target_full_path)
backoff_rename(self.hold_path, self.target_full_path)
def cleanup(self):
rm_rf(self.hold_path)
@property
def target_full_path(self):
return join(self.target_pkgs_dir, self.target_extracted_dirname)
def __str__(self):
return f"ExtractPackageAction<source_full_path={self.source_full_path!r}, target_full_path={self.target_full_path!r}>"