diff --git a/extension/ZAZPip_v0.10.2.oxt b/extension/ZAZPip_v0.10.2.oxt index 897da46..bd1ba7b 100644 Binary files a/extension/ZAZPip_v0.10.2.oxt and b/extension/ZAZPip_v0.10.2.oxt differ diff --git a/source/pythonpath/easymacro.py b/source/pythonpath/easymacro.py index 4ab44ce..5dc49b3 100644 --- a/source/pythonpath/easymacro.py +++ b/source/pythonpath/easymacro.py @@ -167,6 +167,7 @@ OBJ_GRAPHIC = 'SwXTextGraphicObject' OBJ_TEXTS = 'SwXTextRanges' OBJ_TEXT = 'SwXTextRange' +IS_FLATPAK = bool(os.getenv("FLATPAK_ID", "")) # ~ from com.sun.star.sheet.FilterOperator import EMPTY, NO_EMPTY, EQUAL, NOT_EQUAL @@ -624,6 +625,15 @@ 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" + ) + 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) class TimerThread(threading.Thread): diff --git a/source/pythonpath/install_flatpak.py b/source/pythonpath/install_flatpak.py new file mode 100644 index 0000000..cd310fa --- /dev/null +++ b/source/pythonpath/install_flatpak.py @@ -0,0 +1,75 @@ +from __future__ import annotations +from typing import cast +import os +import sys +import subprocess +from pathlib import Path +from typing import Any, List, Dict +import easymacro as app +from install_pip_from_wheel import InstallPipFromWheel + + +class FlatpakInstaller: + """class for the PIP install.""" + + def __init__(self, pip_wheel_url: str, lo_identifier: str) -> None: + self.path_python = app.Paths.python + app.debug(f"Python path: {self.path_python}") + self._pip_url = pip_wheel_url + self._lo_identifier = lo_identifier + self._site_packages = cast(str, app.get_flatpak_site_packages_dir()) + + def install_pip(self) -> None: + if self.is_pip_installed(): + app.info("PIP is already installed") + return + if self._install_wheel(): + if self.is_pip_installed(): + app.info("PIP was installed successfully") + else: + app.error("PIP installation has failed") + + return + + def _get_pip_cmd(self, filename: Path) -> List[str]: + return [str(self.path_python), f"{filename}", "--user"] + + def _get_env(self) -> Dict[str, str]: + """ + Gets Environment used for subprocess. + """ + my_env = os.environ.copy() + py_path = "" + p_sep = ";" if app.IS_WIN else ":" + for d in sys.path: + py_path = py_path + d + p_sep + my_env["PYTHONPATH"] = py_path + return my_env + + def _cmd_pip(self, *args: str) -> List[str]: + cmd: List[str] = [str(self.path_python), "-m", "pip", *args] + return cmd + + def _install_wheel(self) -> bool: + result = False + + installer = InstallPipFromWheel(pip_wheel_url=self._pip_url, lo_identifier=self._lo_identifier) + try: + installer.install(self._site_packages) + if self._site_packages not in sys.path: + sys.path.append(self._site_packages) + result = True + except Exception as err: + app.error(err) + return result + return result + + + def is_pip_installed(self) -> bool: + """Check if PIP is installed.""" + # cmd = self._cmd_pip("--version") + # cmd = '"{}" -m pip -V'.format(self.path_python) + cmd = [str(self.path_python), "-m", "pip", "-V"] + result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self._get_env()) + return result.returncode == 0 + diff --git a/source/pythonpath/install_pip_from_wheel.py b/source/pythonpath/install_pip_from_wheel.py new file mode 100644 index 0000000..3f9950b --- /dev/null +++ b/source/pythonpath/install_pip_from_wheel.py @@ -0,0 +1,141 @@ +from __future__ import annotations +import os +from typing import Any +import tempfile +from pathlib import Path +import easymacro as app +from contextlib import contextmanager +import uno + + +@contextmanager +def change_dir(directory): + """ + A context manager that changes the current working directory to the specified directory + temporarily and then changes it back when the context is exited. + """ + current_dir = os.getcwd() + os.chdir(directory) + try: + yield + finally: + os.chdir(current_dir) + + +class InstallPipFromWheel: + """Download and install PIP from wheel url""" + + def __init__(self, pip_wheel_url: str, lo_identifier: str) -> None: + self._pip_url = pip_wheel_url + self._lo_identifier = lo_identifier + + def install(self, dst: str | Path = "") -> None: + """ + Install pip from wheel file. + + Downloads the pip wheel file from the url provided in the config file and unzips it to the destination directory. + + Args: + dst (str | Path, Optional): The destination directory where the pip wheel file will be installed. If not provided, the ``pythonpath`` location will be used. + + Returns: + None: + + Raises: + None: + """ + if not self._pip_url: + app.error("PIP installation has failed - No wheel url") + return + + if not dst: + root_pth = Path(app.Paths.from_id(self._lo_identifier)) + dst = root_pth / "pythonpath" + + with tempfile.TemporaryDirectory() as temp_dir: + # temp_dir = tempfile.gettempdir() + path_pip = Path(temp_dir) + + filename = path_pip / "pip-wheel.whl" + + data, _, err = app.url_open(self._pip_url, verify=False) + if err: + app.error("Unable to download PIP installation wheel file") + return + filename.write_bytes(data) + + if filename.exists(): + app.info("PIP wheel file has been saved") + else: + app.error("PIP wheel file has not been saved") + return + + try: + self._unzip_wheel(filename=filename, dst=dst) + except Exception: + return + # now that pip has been installed from wheel force a reinstall to ensure it is the latest version + self._force_install_pip() + + def _unzip_wheel(self, filename: Path, dst: str | Path) -> None: + """Unzip the downloaded wheel file""" + if isinstance(dst, str): + dst = Path(dst) + try: + # app.zip.unzip(source=str(filename), target=str(dst)) + self.unzip_file(zip_file=filename, dest_dir=dst) + if dst.exists(): + app.debug(f"PIP wheel file has been unzipped to {dst}") + else: + app.error("PIP wheel file has not been unzipped") + raise Exception("PIP wheel file has not been unzipped") + except Exception as err: + app.error(f"Unable to unzip wheel file: {err}", exc_info=True) + raise + + def _force_install_pip(self) -> None: + """Now that pip has been installed, force reinstall it to ensure it is the latest version""" + cmd = [app.paths.python, "-m", "pip", "install", "--upgrade", "pip"] + app.popen(command=" ".join(cmd)) + + def unzip_file(self, zip_file: str | Path, dest_dir: str | Path = "") -> None: + """ + Unzip the given zip file to the specified destination directory. + + Args: + zip_file (str | Path): The zip file to unzip. + dest_dir (str | Path, optional): The destination directory to unzip to. + + Returns: + None: + """ + from zipfile import ZipFile + + zip_file_path = Path(zip_file) if isinstance(zip_file, str) else zip_file + if not zip_file_path.is_file(): + raise ValueError(f"Expected file, got '{zip_file_path}'") + if not zip_file_path.is_absolute(): + zip_file_path = zip_file_path.absolute() + if not zip_file_path.exists(): + raise FileNotFoundError(f"File '{zip_file_path}' not found") + + if isinstance(dest_dir, str): + dest_dir = zip_file_path.parent if dest_dir == "" else Path(dest_dir) + else: + dest_dir = dest_dir.absolute() + + if not dest_dir.is_dir(): + raise ValueError(f"Expected folder, got '{dest_dir}'") + if not dest_dir.exists(): + try: + dest_dir.mkdir(parents=True) + except Exception as e: + raise FileNotFoundError(f"Folder '{dest_dir}' not found, unable to create folder.") from e + if not dest_dir.exists(): + raise FileNotFoundError(f"Folder '{dest_dir}' not found") + + with change_dir(dest_dir): + with ZipFile(zip_file_path) as f: + f.extractall(dest_dir) + # with change_dir(dest_dir): + # shutil.unpack_archive(zip_file_path, dest_dir) diff --git a/source/pythonpath/main.py b/source/pythonpath/main.py index fc8a41a..bd66353 100644 --- a/source/pythonpath/main.py +++ b/source/pythonpath/main.py @@ -10,6 +10,7 @@ _ = None TITLE = 'ZAZ-PIP' URL_PIP = 'https://bootstrap.pypa.io/get-pip.py' +URL_PIP_WHEEL = 'https://files.pythonhosted.org/packages/47/6a/453160888fab7c6a432a6e25f8afe6256d0d9f2cbd25971021da6491d899/pip-23.3.1-py3-none-any.whl' URL_TEST = 'http://duckduckgo.com' PIP = 'pip' URL_GIT = 'https://git.cuates.net/elmau/zaz-pip' @@ -66,6 +67,7 @@ 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 = { @@ -85,8 +87,7 @@ class Controllers(object): self._install_pip() return - @app.run_in_thread - def _install_pip(self): + def _install_pip_normal(self) -> None: self.d.link_proyect.visible = False self.d.lst_log.visible = True path_pip = app.paths.tmp() @@ -129,6 +130,19 @@ class Controllers(object): return + def _install_pip_flatpak(self) -> None: + from install_flatpak import FlatpakInstaller + installer = FlatpakInstaller(pip_wheel_url=URL_PIP_WHEEL, lo_identifier=ID_EXTENSION) + installer.install_pip() + + @app.run_in_thread + def _install_pip(self): + if app.IS_FLATPAK: + self._install_pip_flatpak() + else: + self._install_pip_normal() + return + def _cmd_pip(self, args): cmd = '"{}" -m pip {}'.format(self.path_python, args) return cmd