From 9f2ac70cc8039a5d48ea0b0390f33fe0ea6628f1 Mon Sep 17 00:00:00 2001 From: "Barry-Thomas-Paul: Moss" Date: Thu, 2 Nov 2023 22:05:56 -0400 Subject: [PATCH] link cpython --- source/pythonpath/easymacro.py | 67 ++++++++++++-- source/pythonpath/link_cpython.py | 146 ++++++++++++++++++++++++++++++ source/pythonpath/main.py | 33 ++++++- 3 files changed, 233 insertions(+), 13 deletions(-) create mode 100644 source/pythonpath/link_cpython.py diff --git a/source/pythonpath/easymacro.py b/source/pythonpath/easymacro.py index a7f7099..14fadd8 100644 --- a/source/pythonpath/easymacro.py +++ b/source/pythonpath/easymacro.py @@ -31,6 +31,7 @@ import logging import os import platform import re +import site import shlex import shutil import socket @@ -168,6 +169,8 @@ OBJ_GRAPHIC = 'SwXTextGraphicObject' OBJ_TEXTS = 'SwXTextRanges' OBJ_TEXT = 'SwXTextRange' IS_FLATPAK = bool(os.getenv("FLATPAK_ID", "")) +IS_APP_IMAGE = bool(os.getenv("APPIMAGE", "")) + # ~ from com.sun.star.sheet.FilterOperator import EMPTY, NO_EMPTY, EQUAL, NOT_EQUAL @@ -636,15 +639,60 @@ def sleep(seconds): time.sleep(seconds) return -def get_flatpak_site_packages_dir() -> str: - # should never be all users - sand_box = os.getenv("FLATPAK_SANDBOX_DIR", "") or str( - Path.home() / ".var/app/org.libreoffice.LibreOffice/sandbox" - ) + + +def get_site_packages_dir() -> str: + """Gets the site-packages folder for the current user.""" major_minor = f"{sys.version_info.major}.{sys.version_info.minor}" - site_packages = Path(sand_box) / f"lib/python{major_minor}/site-packages" - site_packages.mkdir(parents=True, exist_ok=True) - return str(site_packages) + + def get_windows_site_packages_dir() -> str: + nonlocal major_minor + if site.USER_SITE: + site_packages = Path(site.USER_SITE).resolve() + else: + site_packages = ( + Path.home() / f"'/AppData/Roaming/Python/Python{major_minor}/site-packages'" + ) + site_packages.mkdir(parents=True, exist_ok=True) + return str(site_packages) + + def get_flatpak_site_packages_dir() -> str: + # should never be all users + nonlocal major_minor + sand_box = os.getenv("FLATPAK_SANDBOX_DIR", "") or str( + Path.home() / ".var/app/org.libreoffice.LibreOffice/sandbox" + ) + site_packages = Path(sand_box) / f"lib/python{major_minor}/site-packages" + site_packages.mkdir(parents=True, exist_ok=True) + return str(site_packages) + + def get_mac_site_packages_dir() -> str: + nonlocal major_minor + if site.USER_SITE: + site_packages = Path(site.USER_SITE).resolve() + else: + site_packages = ( + Path.home() / f"Library/LibreOfficePython/{major_minor}/lib/python/site-packages" + ) + site_packages.mkdir(parents=True, exist_ok=True) + return str(site_packages) + + def get_default_site_packages_dir() -> str: + nonlocal major_minor + if site.USER_SITE: + site_packages = Path(site.USER_SITE).resolve() + else: + site_packages = Path.home() / f".local/lib/python{major_minor}/site-packages" + site_packages.mkdir(parents=True, exist_ok=True) + return str(site_packages) + + if IS_WIN: + return get_windows_site_packages_dir() + if IS_MAC: + return get_mac_site_packages_dir() + if IS_FLATPAK: + return get_flatpak_site_packages_dir() + return get_default_site_packages_dir() class TimerThread(threading.Thread): @@ -6206,8 +6254,7 @@ class Paths(object): elif IS_MAC: path = self.join(self.config('Module'), '..', 'Resources', PYTHON) else: - is_app_image = bool(os.getenv("APPIMAGE", "")) - if is_app_image: + if IS_APP_IMAGE: path = self.join(self.config("Module"), PYTHON) else: path = sys.executable diff --git a/source/pythonpath/link_cpython.py b/source/pythonpath/link_cpython.py new file mode 100644 index 0000000..e9d2237 --- /dev/null +++ b/source/pythonpath/link_cpython.py @@ -0,0 +1,146 @@ +""" +On some systems such as Mac and AppImage (Linux) the python extension suffix does not match the +cpython suffix used by the embedded python interpreter. + +This class creates symlinks for all .so files in dest folder that match the current python embedded suffix. + +For example a file named ``indexers.cpython-38-x86_64-linux-gnu.so`` would be symlinked to ``indexers.cpython-3.8.so``. +This renaming allows the python interpreter to find the import. +""" +from __future__ import annotations +from typing import List +from pathlib import Path +from importlib import machinery +import easymacro as app + + +class LinkCPython: + def __init__(self, pth: str) -> None: + """ + Constructor + + Args: + pth (str): Path to site-packages folder. + """ + self._current_suffix = self._get_current_suffix() + app.debug("CPythonLink.__init__") + self._link_root = Path(pth) + if not self._link_root.exists(): + raise FileNotFoundError(f"Path does not exist {self._link_root}") + app.debug("CPythonLink.__init__ done") + + def _get_current_suffix(self) -> str: + """Gets suffix currently used by the embedded python interpreter such as ``cpython-3.8``""" + for suffix in machinery.EXTENSION_SUFFIXES: + if suffix.startswith(".cpython-") and suffix.endswith(".so"): + # remove leading . and trailing .so + return suffix[1:][:-3] + return "" + + def _get_all_files(self, path: Path) -> List[Path]: + return [p for p in path.glob(f"**/*{self.file_suffix}.so") if p.is_file()] + + def _get_all_links(self, path: Path) -> List[Path]: + return [p for p in path.glob(f"**/*{self.current_suffix}.so") if p.is_symlink()] + + def _create_symlink(self, src: Path, dst: Path, overwrite: bool) -> None: + if dst.is_symlink(): + if overwrite: + app.debug(f"Removing existing symlink {dst}") + dst.unlink() + else: + app.debug(f"Symlink already exists {dst}") + return + dst.symlink_to(src) + app.debug(f"Created symlink {dst} -> {src}") + + def _find_current_installed_suffix(self, path: Path) -> str: + """ + Finds the current suffix from the current installed python so files such as ``cpython-38-x86_64-linux-gnu``. + + Args: + path (Path): Path to search in. Usually site-packages. + + Returns: + str: suffix if found, otherwise empty string. + """ + return next( + (str(p).rsplit(".", 2)[1] for p in path.glob("**/*.cpython-*.so") if not p.is_symlink()), + "", + ) + + def link(self, overwrite:bool = False) -> None: + """ + Creates symlinks for all .so files in site-packages that match the current suffix. + + Args: + overwrite (bool, optional): Override any existing sys links. Defaults to False. + """ + app.debug("CPythonLink.link starting") + if not self._link_root: + app.debug("No site-packages found") + return + if not self.file_suffix: + app.debug("No current file suffix found") + return + if not self._link_root.exists(): + app.debug(f"Site-packages does not exist {self._link_root}") + return + app.debug(f"Python current suffix: {self._current_suffix}") + app.debug(f"Found file suffix: {self.file_suffix}") + files = self._get_all_files(self._link_root) + if not files: + app.debug(f"No files found in {self._link_root}") + return + cp_old = self.file_suffix + cp_new = self._current_suffix + if cp_old == cp_new: + app.debug(f"Suffixes match, no need to link: {cp_old} == {cp_new}") + return + + for file in files: + ln_name = file.name.replace(cp_old, cp_new) + src = file + if not src.is_absolute(): + src = file.resolve() + dst = src.parent / ln_name + self._create_symlink(src, dst, overwrite) + app.debug("CPythonLink.link done") + + def unlink(self) -> None: + """Unlinks all broken sys links""" + links = self._get_all_links(self._link_root) + if not links: + app.debug(f"No links found in {self._link_root}") + return + for link in links: + if not link.exists(): + app.debug(f"Removing broken symlink {link}") + link.unlink() + + + # region Properties + @property + def cpy_name(self) -> str: + """Gets/Sets CPython name, e.g. cpython-3.8""" + return self._current_suffix + + @cpy_name.setter + def cpy_name(self, value: str) -> None: + self._current_suffix = value + + @property + def current_suffix(self) -> str: + """Current Suffix such as ``cpython-3.8``""" + return self._current_suffix + + @property + def file_suffix(self) -> str: + """Current Suffix such as ``cpython-38-x86_64-linux-gnu``""" + try: + return self._file_suffix + except AttributeError: + self._file_suffix = self._find_current_installed_suffix(self._link_root) + return self._file_suffix + + # endregion Properties diff --git a/source/pythonpath/main.py b/source/pythonpath/main.py index e0c47ce..4232ce3 100644 --- a/source/pythonpath/main.py +++ b/source/pythonpath/main.py @@ -67,7 +67,6 @@ class Controllers(object): def __init__(self, dialog): self.d = dialog self.path_python = app.paths.python - self.is_flatpak = app.IS_FLATPAK def _set_state(self, state): self._states = { @@ -260,8 +259,8 @@ class Controllers(object): self.d.lst_log.visible = True line = '' - if self.is_flatpak: - cmd = f' install --upgrade --target={app.get_flatpak_site_packages_dir()}' + if app.IS_FLATPAK: + cmd = f' install --upgrade --target={app.get_site_packages_dir()}' else: cmd = ' install --upgrade --user' if value: @@ -275,6 +274,33 @@ class Controllers(object): self.d.lst_log.insert(line, 'ok.png') else: self.d.lst_log.insert(line) + self._link_cpython() + return + + def _link_cpython(self): + if not app.IS_MAC and not app.IS_APP_IMAGE: + app.debug('Not Mac or not AppImage') + return + try: + app.debug("Linking CPython") + from link_cpython import LinkCPython + cpy_link = LinkCPython(pth=app.get_site_packages_dir()) + cpy_link.link() + except Exception as err: + app.error(err) + return + + def _unlink_cpython(self): + if not app.IS_MAC and not app.IS_APP_IMAGE: + app.debug('Not Mac or not AppImage') + return + try: + app.debug("Unlinking CPython") + from link_cpython import LinkCPython + cpy_link = LinkCPython(pth=app.get_site_packages_dir()) + cpy_link.unlink() + except Exception as err: + app.error(err) return def lst_package_double_click(self, event): @@ -305,6 +331,7 @@ class Controllers(object): self.d.lst_log.insert(line, 'ok.png') else: self.d.lst_log.insert(line) + self._unlink_cpython() return def cmd_uninstall_action(self, event):