"""
Install a wheel / install a conda.
"""
import os
import subprocess
import tempfile
from pathlib import Path
from unittest.mock import patch
import logging
from conda.cli.main import main_subshell
from conda.core.package_cache_data import PackageCacheData
from installer import install
from installer.destinations import SchemeDictionaryDestination
from installer.records import Hash, RecordEntry
from installer.sources import WheelFile
from conda_pypi.utils import hash_as_base64url
log = logging.getLogger(__name__)
# We've seen some wheels placing identical files in multiple .data/ schemes
# (e.g., pybind11-global duplicates headers in both data/include/ and
# headers/). SchemeDictionaryDestination raises FileExistsError on the
# second copy. Here, we record the already-written file instead.
#
# TODO: https://github.com/pypa/installer/pull/216 adds an overwrite_existing
# flag to SchemeDictionaryDestination that would let us avoid this subclass
# entirely. However, it has not been released yet, see https://github.com/pypa/installer/issues/218.
# So we'll have carry this workaround for now.
class _CondaWheelDestination(SchemeDictionaryDestination):
"""Skip files that already exist at the target path."""
def write_to_fs(self, scheme, path, stream, is_executable):
target_path = self._path_with_destdir(scheme, path)
if os.path.exists(target_path):
log.debug(f"Skipping already-installed file: {target_path}")
data = Path(target_path).read_bytes()
digest = hash_as_base64url(data, self.hash_algorithm)
return RecordEntry(path, Hash(self.hash_algorithm, digest), len(data))
return super().write_to_fs(scheme, path, stream, is_executable)
[docs]
def install_installer(python_executable: str, whl: Path, build_path: Path):
# Handler for installation directories and writing into them.
# Create site-packages directory if it doesn't exist
site_packages = build_path / "site-packages"
site_packages.mkdir(parents=True, exist_ok=True)
# Scheme keys are defined by PEP 427 (the wheel format spec) and must match
# what the installer library extracts from .data/ subdirectory names.
# https://packaging.python.org/en/latest/specifications/binary-distribution-format/
scheme = {
"purelib": str(site_packages), # Pure Python packages
"platlib": str(site_packages), # Platform-specific packages
"scripts": str(build_path / "bin"), # Console scripts
"data": str(build_path), # Data files (JS, CSS, templates, etc.)
"headers": str(build_path / "include"), # C/C++ headers (PEP 427 .data/headers/)
}
destination = _CondaWheelDestination(
scheme,
interpreter=str(python_executable),
script_kind="posix",
)
with WheelFile.open(whl) as source:
install(
source=source,
destination=destination,
# Additional metadata that is generated by the installation tool.
additional_metadata={
"INSTALLER": b"conda-pypi",
},
)
log.debug(f"Installed to {build_path}")
[docs]
def install_pip(python_executable: str, whl: Path, build_path: Path):
command = [
python_executable,
"-m",
"pip",
"install",
"--quiet",
"--no-deps",
"--target",
str(build_path / "site-packages"),
whl,
]
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
log.debug(f"Installed to {build_path}")
[docs]
def install_ephemeral_conda(prefix: Path, package: Path):
"""
Install [editable] conda package without adding it to the environment's
package cache, since we don't want to accidentally re-install "a link to a
source checkout" elsewhere.
Installing packages directly from a file does not resolve dependencies.
Should we automatically install the project's dependencies also?
"""
persistent_pkgs = PackageCacheData.first_writable().pkgs_dir
with (
tempfile.TemporaryDirectory(dir=persistent_pkgs, prefix="ephemeral") as cache_dir,
patch.dict(os.environ, {"CONDA_PKGS_DIRS": cache_dir}),
):
main_subshell("install", "--prefix", str(prefix), str(package))