def sources(cls, interpreter):
     for src in super(PyPy3Posix, cls).sources(interpreter):
         yield src
     host_lib = Path(interpreter.system_prefix) / "lib"
     if host_lib.exists() and host_lib.is_dir():
         for path in host_lib.iterdir():
             yield PathRefToDest(path, dest=cls.to_lib)
Beispiel #2
0
 def sources(cls, interpreter):
     for src in super(PyPy3Posix, cls).sources(interpreter):
         yield src
     # 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():
         for path in host_lib.iterdir():
             if stdlib == path:
                 # For PyPy3.8+ the stdlib lives in lib/pypy3.8
                 # We need to avoid creating a symlink to it since that
                 # will defeat the purpose of a virtualenv
                 continue
             yield PathRefToDest(path, dest=cls.to_lib)
class PipInstall(object):
    def __init__(self, wheel, creator, image_folder):
        self._wheel = wheel
        self._creator = creator
        self._image_dir = image_folder
        self._extracted = False
        self.__dist_info = None
        self._console_entry_points = None

    @abstractmethod
    def _sync(self, src, dst):
        raise NotImplementedError

    def install(self, version_info):
        self._extracted = True
        self._uninstall_previous_version()
        # sync image
        for filename in self._image_dir.iterdir():
            into = self._creator.purelib / filename.name
            self._sync(filename, into)
        # generate console executables
        consoles = set()
        script_dir = self._creator.script_dir
        for name, module in self._console_scripts.items():
            consoles.update(
                self._create_console_entry_point(name, module, script_dir,
                                                 version_info))
        logging.debug("generated console scripts %s",
                      " ".join(i.name for i in consoles))

    def build_image(self):
        # 1. first extract the wheel
        logging.debug("build install image for %s to %s", self._wheel.name,
                      self._image_dir)
        with zipfile.ZipFile(str(self._wheel)) as zip_ref:
            self._shorten_path_if_needed(zip_ref)
            zip_ref.extractall(str(self._image_dir))
            self._extracted = True
        # 2. now add additional files not present in the distribution
        new_files = self._generate_new_files()
        # 3. finally fix the records file
        self._fix_records(new_files)

    def _shorten_path_if_needed(self, zip_ref):
        if os.name == "nt":
            to_folder = str(self._image_dir)
            # https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
            zip_max_len = max(len(i) for i in zip_ref.namelist())
            path_len = zip_max_len + len(to_folder)
            if path_len > 260:
                self._image_dir.mkdir(
                    exist_ok=True)  # to get a short path must exist

                from virtualenv.util.path import get_short_path_name

                to_folder = get_short_path_name(to_folder)
                self._image_dir = Path(to_folder)

    def _records_text(self, files):
        record_data = "\n".join("{},,".format(
            os.path.relpath(ensure_text(str(rec)),
                            ensure_text(str(self._image_dir))))
                                for rec in files)
        return record_data

    def _generate_new_files(self):
        new_files = set()
        installer = self._dist_info / "INSTALLER"
        installer.write_text("pip\n")
        new_files.add(installer)
        # inject a no-op root element, as workaround for bug in https://github.com/pypa/pip/issues/7226
        marker = self._image_dir / "{}.virtualenv".format(self._dist_info.stem)
        marker.write_text("")
        new_files.add(marker)
        folder = mkdtemp()
        try:
            to_folder = Path(folder)
            rel = os.path.relpath(ensure_text(str(self._creator.script_dir)),
                                  ensure_text(str(self._creator.purelib)))
            version_info = self._creator.interpreter.version_info
            for name, module in self._console_scripts.items():
                new_files.update(
                    Path(
                        os.path.normpath(
                            ensure_text(str(self._image_dir / rel / i.name))))
                    for i in self._create_console_entry_point(
                        name, module, to_folder, version_info))
        finally:
            safe_delete(folder)
        return new_files

    @property
    def _dist_info(self):
        if self._extracted is False:
            return None  # pragma: no cover
        if self.__dist_info is None:
            files = []
            for filename in self._image_dir.iterdir():
                files.append(filename.name)
                if filename.suffix == ".dist-info":
                    self.__dist_info = filename
                    break
            else:
                msg = "no .dist-info at {}, has {}".format(
                    self._image_dir, ", ".join(files))  # pragma: no cover
                raise RuntimeError(msg)  # pragma: no cover
        return self.__dist_info

    @abstractmethod
    def _fix_records(self, extra_record_data):
        raise NotImplementedError

    @property
    def _console_scripts(self):
        if self._extracted is False:
            return None  # pragma: no cover
        if self._console_entry_points is None:
            self._console_entry_points = {}
            entry_points = self._dist_info / "entry_points.txt"
            if entry_points.exists():
                parser = ConfigParser.ConfigParser()
                with entry_points.open() as file_handler:
                    reader = getattr(parser, "read_file" if PY3 else "readfp")
                    reader(file_handler)
                if "console_scripts" in parser.sections():
                    for name, value in parser.items("console_scripts"):
                        match = re.match(r"(.*?)-?\d\.?\d*", name)
                        if match:
                            name = match.groups(1)[0]
                        self._console_entry_points[name] = value
        return self._console_entry_points

    def _create_console_entry_point(self, name, value, to_folder,
                                    version_info):
        result = []
        maker = ScriptMakerCustom(to_folder, version_info, self._creator.exe,
                                  name)
        specification = "{} = {}".format(name, value)
        new_files = maker.make(specification)
        result.extend(Path(i) for i in new_files)
        return result

    def _uninstall_previous_version(self):
        dist_name = self._dist_info.stem.split("-")[0]
        in_folders = chain.from_iterable([
            i.iterdir()
            for i in {self._creator.purelib, self._creator.platlib}
        ])
        paths = (p for p in in_folders if p.stem.split("-")[0] == dist_name
                 and p.suffix == ".dist-info" and p.is_dir())
        existing_dist = next(paths, None)
        if existing_dist is not None:
            self._uninstall_dist(existing_dist)

    @staticmethod
    def _uninstall_dist(dist):
        dist_base = dist.parent
        logging.debug("uninstall existing distribution %s from %s", dist.stem,
                      dist_base)

        top_txt = dist / "top_level.txt"  # add top level packages at folder level
        paths = {
            dist.parent / i.strip()
            for i in top_txt.read_text().splitlines()
        } if top_txt.exists() else set()
        paths.add(dist)  # add the dist-info folder itself

        base_dirs, record = paths.copy(
        ), dist / "RECORD"  # collect entries in record that we did not register yet
        for name in (i.split(",")[0] for i in record.read_text().splitlines()
                     ) if record.exists() else ():
            path = dist_base / name
            if not any(p in base_dirs for p in path.parents
                       ):  # only add if not already added as a base dir
                paths.add(path)

        for path in sorted(paths):  # actually remove stuff in a stable order
            if path.exists():
                if path.is_dir() and not path.is_symlink():
                    safe_delete(path)
                else:
                    path.unlink()

    def clear(self):
        if self._image_dir.exists():
            safe_delete(self._image_dir)

    def has_image(self):
        return self._image_dir.exists() and next(
            self._image_dir.iterdir()) is not None