diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index b2d2c3c5bf..d9fbe5e792 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -69,6 +69,7 @@ from manage_python_dependencies import ( from addonmanager_devmode import DeveloperMode from addonmanager_firstrun import FirstRunDialog from addonmanager_connection_checker import ConnectionCheckerGUI +from addonmanager_devmode_metadata_checker import MetadataValidators import NetworkManager @@ -192,7 +193,6 @@ class CommandAddonManager: self.connection_checker.start() - def launch(self) -> None: """Shows the Addon Manager UI""" @@ -826,6 +826,9 @@ class CommandAddonManager: self.developer_mode = DeveloperMode() self.developer_mode.show() + checker = MetadataValidators() + checker.validate_all(self.item_model.repos) + def add_addon_repo(self, addon_repo: Addon) -> None: """adds a workbench to the list""" @@ -1679,151 +1682,4 @@ class CommandAddonManager: ).format(repo.name) + "\n" ) - - def validate(self): - """Developer tool: check all repos for validity and print report""" - - FreeCAD.Console.PrintLog(f"\n\nADDON MANAGER DEVELOPER MODE CHECKS\n") - FreeCAD.Console.PrintLog(f"-----------------------------------\n") - - counter = 0 - for addon in self.item_model.repos: - counter += 1 - self.update_progress_bar(counter, len(self.item_model.repos)) - if addon.metadata is not None: - self.validate_package_xml(addon) - elif addon.repo_type == Addon.Kind.MACRO: - if addon.macro.parsed: - if len(addon.macro.icon) == 0 and len(addon.macro.xpm) == 0: - FreeCAD.Console.PrintLog( - f"Macro '{addon.name}' does not have an icon\n" - ) - else: - FreeCAD.Console.PrintLog( - f"Addon '{addon.name}' does not have a package.xml file\n" - ) - - FreeCAD.Console.PrintLog(f"-----------------------------------\n\n") - self.do_next_startup_phase() - - def validate_package_xml(self, addon: Addon): - if addon.metadata is None: - return - - # The package.xml standard has some required elements that the basic XML reader is not actually checking - # for. In developer mode, actually make sure that all of the rules are being followed for each element. - - errors = [] - - # Top-level required elements - - if not addon.metadata.Name or len(addon.metadata.Name) == 0: - errors.append( - f"No top-level element found, or element is empty" - ) - if not addon.metadata.Version or addon.metadata.Version == "0.0.0": - errors.append( - f"No top-level element found, or element is invalid" - ) - # if not addon.metadata.Date or len(addon.metadata.Date) == 0: - # errors.append(f"No top-level element found, or element is invalid") - if not addon.metadata.Description or len(addon.metadata.Description) == 0: - errors.append( - f"No top-level element found, or element is invalid" - ) - - maintainers = addon.metadata.Maintainer - if len(maintainers) == 0: - errors.append(f"No top-level found, at least one is required") - for maintainer in maintainers: - if len(maintainer["email"]) == 0: - errors.append( - f"No email address specified for maintainer '{maintainer['name']}'" - ) - - licenses = addon.metadata.License - if len(licenses) == 0: - errors.append(f"No top-level found, at least one is required") - - urls = addon.metadata.Urls - if len(urls) == 0: - errors.append( - f"No elements found, at least a repo url must be provided" - ) - else: - found_repo = False - found_readme = False - for url in urls: - if url["type"] == "repository": - found_repo = True - if len(url["branch"]) == 0: - errors.append( - " element is missing the 'branch' attribute" - ) - elif url["type"] == "readme": - found_readme = True - location = url["location"] - p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(location) - if not p: - errors.append( - f"Could not access specified readme at {location}" - ) - else: - p = p.data().decode("utf8") - if "" in p: - pass - else: - errors.append( - f"Readme data found at {location} does not appear to be rendered HTML" - ) - if not found_repo: - errors.append("No repo url specified") - if not found_readme: - errors.append( - "No readme url specified (not required, but highly recommended)" - ) - - contents = addon.metadata.Content - if not contents or len(contents) == 0: - errors.append("No content items found") - - missing_icon = True - if addon.metadata.Icon and len(addon.metadata.Icon) > 0: - missing_icon = False - else: - if "workbench" in contents: - wb = contents["workbench"][0] - if wb.Icon: - missing_icon = False - if missing_icon: - errors.append(f"No element found, or element is invalid") - - if "workbench" in contents: - for wb in contents["workbench"]: - errors.extend(self.validate_workbench_metadata(wb)) - - if "preferencepack" in contents: - for wb in contents["preferencepack"]: - errors.extend(self.validate_preference_pack_metadata(wb)) - - if len(errors) > 0: - FreeCAD.Console.PrintLog( - f"Errors found in package.xml file for '{addon.name}'\n" - ) - for error in errors: - FreeCAD.Console.PrintLog(f" * {error}\n") - - def validate_workbench_metadata(self, workbench) -> List[str]: - errors = [] - if not workbench.Classname or len(workbench.Classname) == 0: - errors.append("No specified for workbench") - return errors - - def validate_preference_pack_metadata(self, pack) -> List[str]: - errors = [] - if not pack.Name or len(pack.Name) == 0: - errors.append("No specified for preference pack") - return errors - - # @} diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt index 5e8bf8c0e4..03ef4e2f1e 100644 --- a/src/Mod/AddonManager/CMakeLists.txt +++ b/src/Mod/AddonManager/CMakeLists.txt @@ -12,6 +12,7 @@ SET(AddonManager_SRCS addonmanager_devmode_add_content.py addonmanager_devmode_license_selector.py addonmanager_devmode_licenses_table.py + addonmanager_devmode_metadata_checker.py addonmanager_devmode_person_editor.py addonmanager_devmode_people_table.py addonmanager_devmode_predictor.py diff --git a/src/Mod/AddonManager/addonmanager_devmode_metadata_checker.py b/src/Mod/AddonManager/addonmanager_devmode_metadata_checker.py new file mode 100644 index 0000000000..4eeb6b38af --- /dev/null +++ b/src/Mod/AddonManager/addonmanager_devmode_metadata_checker.py @@ -0,0 +1,197 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2022 FreeCAD Project Association * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +""" Metadata validation functions """ + +from typing import List + +import FreeCAD + +from Addon import Addon +import NetworkManager + + +class MetadataValidators: + """A collection of tools for validating various pieces of metadata. Prints validation + information to the console.""" + + def validate_all(self, repos): + """Developer tool: check all repos for validity and print report""" + + FreeCAD.Console.PrintMessage("\n\nADDON MANAGER DEVELOPER MODE CHECKS\n") + FreeCAD.Console.PrintMessage("-----------------------------------\n") + + counter = 0 + for addon in repos: + counter += 1 + if addon.metadata is not None: + self.validate_package_xml(addon) + elif addon.repo_type == Addon.Kind.MACRO: + if addon.macro.parsed: + if len(addon.macro.icon) == 0 and len(addon.macro.xpm) == 0: + FreeCAD.Console.PrintMessage( + f"Macro '{addon.name}' does not have an icon\n" + ) + else: + FreeCAD.Console.PrintMessage( + f"Addon '{addon.name}' does not have a package.xml file\n" + ) + + FreeCAD.Console.PrintMessage("-----------------------------------\n\n") + + def validate_package_xml(self, addon: Addon): + """Check the package.xml file for the specified Addon""" + if addon.metadata is None: + return + + # The package.xml standard has some required elements that the basic XML reader is not + # actually checking for. In developer mode, actually make sure that all of the rules are + # being followed for each element. + + errors = [] + + errors.extend(self.validate_top_level(addon)) + errors.extend(self.validate_content(addon)) + + if len(errors) > 0: + FreeCAD.Console.PrintError( + f"Errors found in package.xml file for '{addon.name}'\n" + ) + for error in errors: + FreeCAD.Console.PrintError(f" * {error}\n") + + def validate_content(self, addon: Addon) -> List[str]: + """Validate the Content items for this Addon""" + errors = [] + contents = addon.metadata.Content + + missing_icon = True + if addon.metadata.Icon and len(addon.metadata.Icon) > 0: + missing_icon = False + else: + if "workbench" in contents: + wb = contents["workbench"][0] + if wb.Icon: + missing_icon = False + if missing_icon: + errors.append("No element found, or element is invalid") + + if "workbench" in contents: + for wb in contents["workbench"]: + errors.extend(self.validate_workbench_metadata(wb)) + + if "preferencepack" in contents: + for wb in contents["preferencepack"]: + errors.extend(self.validate_preference_pack_metadata(wb)) + + return errors + + def validate_top_level(self, addon) -> List[str]: + """Check for the presence of the required top-level elements""" + errors = [] + if not addon.metadata.Name or len(addon.metadata.Name) == 0: + errors.append( + "No top-level element found, or element is empty" + ) + if not addon.metadata.Version or addon.metadata.Version == "0.0.0": + errors.append( + "No top-level element found, or element is invalid" + ) + # if not addon.metadata.Date or len(addon.metadata.Date) == 0: + # errors.append(f"No top-level element found, or element is invalid") + if not addon.metadata.Description or len(addon.metadata.Description) == 0: + errors.append( + "No top-level element found, or element is invalid" + ) + + maintainers = addon.metadata.Maintainer + if len(maintainers) == 0: + errors.append("No top-level found, at least one is required") + for maintainer in maintainers: + if len(maintainer["email"]) == 0: + errors.append( + f"No email address specified for maintainer '{maintainer['name']}'" + ) + + licenses = addon.metadata.License + if len(licenses) == 0: + errors.append("No top-level found, at least one is required") + + urls = addon.metadata.Urls + errors.extend(self.validate_urls(urls)) + return errors + + def validate_urls(self, urls) -> List[str]: + """Check the URLs provided by the addon""" + errors = [] + if len(urls) == 0: + errors.append( + "No elements found, at least a repo url must be provided" + ) + else: + found_repo = False + found_readme = False + for url in urls: + if url["type"] == "repository": + found_repo = True + if len(url["branch"]) == 0: + errors.append( + " element is missing the 'branch' attribute" + ) + elif url["type"] == "readme": + found_readme = True + location = url["location"] + p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(location) + if not p: + errors.append( + f"Could not access specified readme at {location}" + ) + else: + p = p.data().decode("utf8") + if "" in p: + pass + else: + errors.append( + f"Readme data found at {location}" + " does not appear to be rendered HTML" + ) + if not found_repo: + errors.append("No repo url specified") + if not found_readme: + errors.append( + "No readme url specified (not required, but highly recommended)" + ) + return errors + + def validate_workbench_metadata(self, workbench) -> List[str]: + """Validate the required element(s) for a workbench""" + errors = [] + if not workbench.Classname or len(workbench.Classname) == 0: + errors.append("No specified for workbench") + return errors + + def validate_preference_pack_metadata(self, pack) -> List[str]: + """Validate the required element(s) for a preference pack""" + errors = [] + if not pack.Name or len(pack.Name) == 0: + errors.append("No specified for preference pack") + return errors