From bcb60d7d7e42a35c94c23aa9790490024a3a8c3f Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 26 Jan 2024 06:46:09 -0600 Subject: [PATCH] Addon Manager: Allow .py filenames Also improve macro removal. --- .../AddonManager/addonmanager_installer.py | 22 +++++++++++++++++-- src/Mod/AddonManager/addonmanager_macro.py | 9 ++++++++ .../AddonManager/addonmanager_uninstaller.py | 22 ++++++++++++++++--- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/Mod/AddonManager/addonmanager_installer.py b/src/Mod/AddonManager/addonmanager_installer.py index a8a9e888e2..d35a3bc4b9 100644 --- a/src/Mod/AddonManager/addonmanager_installer.py +++ b/src/Mod/AddonManager/addonmanager_installer.py @@ -24,7 +24,7 @@ """ Contains the classes to manage Addon installation: intended as a stable API, safe for external code to call and to rely upon existing. See classes AddonInstaller and MacroInstaller for details. """ - +import json from datetime import datetime, timezone from enum import IntEnum, auto import os @@ -503,16 +503,34 @@ class MacroInstaller(QtCore.QObject): self.finished.emit() return False - # If it succeeded, move all of the files to the macro install location + # If it succeeded, move all the files to the macro install location, + # keeping a list of all the files we installed, so they can be removed later + # if this macro is uninstalled. + manifest = [] for item in os.listdir(temp_dir): src = os.path.join(temp_dir, item) dst = os.path.join(self.installation_path, item) shutil.move(src, dst) + manifest.append(dst) + self._write_installation_manifest(manifest) self.success.emit(self.addon_to_install) self.addon_to_install.set_status(Addon.Status.NO_UPDATE_AVAILABLE) self.finished.emit() return True + def _write_installation_manifest(self, manifest): + manifest_file = os.path.join( + self.installation_path, self.addon_to_install.macro.filename + ".manifest" + ) + try: + with open(manifest_file, "w", encoding="utf-8") as f: + f.write(json.dumps(manifest, indent=" ")) + except OSError as e: + FreeCAD.Console.PrintWarning( + translate("AddonsInstaller", "Failed to create installation manifest " "file:\n") + ) + FreeCAD.Console.PrintWarning(manifest_file) + @classmethod def _validate_object(cls, addon: object): """Make sure this object provides an attribute called "macro" with a method called diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index c133a0a9b3..b01a7fde78 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -69,6 +69,7 @@ class Macro: self.version = "" self.date = "" self.src_filename = "" + self.filename_from_url = "" self.author = "" self.icon = "" self.icon_source = None @@ -104,6 +105,8 @@ class Macro: """The filename of this macro""" if self.on_git: return os.path.basename(self.src_filename) + elif self.filename_from_url: + return self.filename_from_url return (self.name + ".FCMacro").replace(" ", "_") def is_installed(self): @@ -211,8 +214,14 @@ class Macro: ) return None code = u2.decode("utf8") + self._set_filename_from_url(self.raw_code_url) return code + def _set_filename_from_url(self, url: str): + lhs, slash, rhs = url.rpartition("/") + if rhs.endswith(".py") or rhs.lower().endswith(".fcmacro"): + self.filename_from_url = rhs + @staticmethod def _read_code_from_wiki(p: str) -> Optional[str]: code = re.findall(r"
(.*?)
", p.replace("\n", "--endl--")) diff --git a/src/Mod/AddonManager/addonmanager_uninstaller.py b/src/Mod/AddonManager/addonmanager_uninstaller.py index b83c8085f2..0c42f591dd 100644 --- a/src/Mod/AddonManager/addonmanager_uninstaller.py +++ b/src/Mod/AddonManager/addonmanager_uninstaller.py @@ -24,7 +24,7 @@ """ Contains the classes to manage Addon removal: intended as a stable API, safe for external code to call and to rely upon existing. See classes AddonUninstaller and MacroUninstaller for details.""" - +import json import os from typing import List @@ -228,7 +228,10 @@ class MacroUninstaller(QObject): directories = set() for f in self._get_files_to_remove(): normed = os.path.normpath(f) - full_path = os.path.join(self.installation_location, normed) + if os.path.isabs(normed): + full_path = normed + else: + full_path = os.path.join(self.installation_location, normed) if "/" in f: directories.add(os.path.dirname(full_path)) try: @@ -246,6 +249,10 @@ class MacroUninstaller(QObject): + str(e) ) success = False + except Exception: + # Generic catch-all, just in case (because failure to catch an exception + # here can break things pretty badly) + success = False self._cleanup_directories(directories) @@ -256,8 +263,17 @@ class MacroUninstaller(QObject): self.addon_to_remove.set_status(Addon.Status.NOT_INSTALLED) self.finished.emit() - def _get_files_to_remove(self) -> List[os.PathLike]: + def _get_files_to_remove(self) -> List[str]: """Get the list of files that should be removed""" + manifest_file = os.path.join( + self.installation_location, self.addon_to_remove.macro.filename + ".manifest" + ) + if os.path.exists(manifest_file): + with open(manifest_file, "r", encoding="utf-8") as f: + manifest_data = f.read() + manifest = json.loads(manifest_data) + manifest.append(manifest_file) # Remove the manifest itself as well + return manifest files_to_remove = [self.addon_to_remove.macro.filename] if self.addon_to_remove.macro.icon: files_to_remove.append(self.addon_to_remove.macro.icon)