Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,23 @@ def import_from_addon(self, repo: Addon, all_repos: List[Addon]):
option for option in self.python_optional if option not in self.python_requires
]

def join(self, other: "MissingDependencies"):
"""Join two sets of missing dependencies together"""
self.external_addons.extend(
[x for x in other.external_addons if x not in self.external_addons]
)
self.wbs.extend([x for x in other.wbs if x not in self.wbs])
self.python_requires.extend(
[x for x in other.python_requires if x not in self.python_requires]
)
self.python_optional.extend(
[x for x in other.python_optional if x not in self.python_optional]
)
self.python_min_version = max(self.python_min_version, other.python_min_version)

# Clean up optional:
self.python_optional = [x for x in self.python_optional if x not in self.python_requires]

@staticmethod
def package_is_installed(package_name: str) -> bool:
"""Check to see if a Python package is installed (i.e., if it can be imported).
Expand Down
83 changes: 65 additions & 18 deletions AddonManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@
CheckWorkbenchesForUpdatesWorker,
GetBasicAddonStatsWorker,
GetAddonScoreWorker,
CheckForMissingDependenciesWorker,
)
from addonmanager_installer_gui import (
AddonInstallerGUI,
MacroInstallerGUI,
AddonDependencyInstallerGUI,
)
from addonmanager_installer_gui import AddonInstallerGUI, MacroInstallerGUI
from addonmanager_icon_utilities import get_icon_for_addon
from addonmanager_uninstaller_gui import AddonUninstallerGUI
from addonmanager_update_all_gui import UpdateAllGUI
Expand All @@ -46,8 +51,9 @@
from composite_view import CompositeView
from Widgets.addonmanager_widget_global_buttons import WidgetGlobalButtonBar
from Widgets.addonmanager_widget_progress_bar import Progress
from Widgets.addonmanager_utility_dialogs import MessageDialog
from package_list import PackageListItemModel
from Addon import Addon, cycle_to_sub_addon
from Addon import Addon, cycle_to_sub_addon, MissingDependencies
from addonmanager_python_deps_gui import (
PythonPackageManagerGui,
)
Expand Down Expand Up @@ -104,6 +110,7 @@ class CommandAddonManager(QtCore.QObject):
"check_for_python_package_updates_worker",
"get_basic_addon_stats_worker",
"get_addon_score_worker",
"check_missing_dependencies_worker",
]

lock = threading.Lock()
Expand Down Expand Up @@ -139,13 +146,17 @@ def __init__(self):
self.create_addon_list_worker = None
self.get_addon_score_worker = None
self.get_basic_addon_stats_worker = None
self.check_missing_dependencies_worker = None

self.manage_python_packages_dialog = None
self.missing_dependency_installer = None

# Set up the connection checker
self.connection_checker = ConnectionCheckerGUI()
self.connection_checker.connection_available.connect(self.launch)

self.missing_dependencies = MissingDependencies()

# Give other parts of the AM access to the current instance
global INSTANCE
INSTANCE = self
Expand Down Expand Up @@ -338,7 +349,7 @@ def startup(self) -> None:
self.populate_packages_table,
self.activate_table_widgets,
self.check_updates,
self.check_python_updates,
self.check_missing_dependencies,
self.fetch_addon_stats,
self.fetch_addon_score,
self.select_addon,
Expand All @@ -358,6 +369,7 @@ def do_next_startup_phase(self) -> None:
else:
self.hide_progress_widgets()
self.composite_view.package_list.item_filter.invalidateFilter()
self.post_startup()

def populate_packages_table(self) -> None:
self.item_model.clear()
Expand Down Expand Up @@ -451,21 +463,15 @@ def update_check_complete(self) -> None:
self.enable_updates(len(self.packages_with_updates))
self.button_bar.check_for_updates.setEnabled(True)

def check_python_updates(self) -> None:
# TODO: Run the checker to see if we need to do any Python updates as well

# Really, there are two different things to check here: first, run our normal dependency
# checker and display the dependency resolution dialog. This will handle addons that have
# disappeared/been uninstalled (but were required by other addons) as well as Python required
# and optional dependencies. The only catch is, if we ONLY have optional Python dependencies
# missing, we should ignore them.

# Second, if this is a version of Python we've used before, do any of our Python libraries
# installed into the custom directory, or the venv, need to be updated?

# To the user these are two quite different things, so their interface should reflect that.

self.do_next_startup_phase()
def check_missing_dependencies(self) -> None:
"""See if we have any missing dependencies"""
self.check_missing_dependencies_worker = CheckForMissingDependenciesWorker(
self.item_model.repos
)
self.update_progress_bar(translate("AddonsInstaller", "Checking dependencies"), 0, 100)
self.check_missing_dependencies_worker.progress.connect(self.update_progress_bar)
self.check_missing_dependencies_worker.finished.connect(self.do_next_startup_phase)
self.check_missing_dependencies_worker.start()

def show_python_updates_dialog(self) -> None:
if not self.manage_python_packages_dialog:
Expand Down Expand Up @@ -627,6 +633,47 @@ def update_progress_bar(self, message: str, current_value: int, max_value: int)
)
self.composite_view.package_list.update_loading_progress(progress)

def post_startup(self) -> None:
"""This is called after the startup sequence has completed"""
if self.check_missing_dependencies_worker:
deps: MissingDependencies = self.check_missing_dependencies_worker.missing_dependencies
if deps.wbs or deps.external_addons or deps.python_requires:
ignored_deps_string = fci.Preferences().get("ignored_missing_deps")
ignored_deps = ignored_deps_string.split(";") if ignored_deps_string else []

proceed = False
all_deps = set()
all_deps.update(deps.wbs)
all_deps.update(deps.external_addons)
all_deps.update(deps.python_requires)
for dep in all_deps:
if dep not in ignored_deps:
proceed = True
break

if proceed:
self.missing_dependency_installer = AddonDependencyInstallerGUI([], deps)
self.missing_dependency_installer.dependency_dialog.label.setText(
translate(
"AddonsInstaller",
"Some installed addons are missing dependencies. Would you like to install them now?",
)
)
self.missing_dependency_installer.dependency_dialog.buttonBox.button(
QtWidgets.QDialogButtonBox.Ignore
).clicked.connect(self.ignore_missing_dependencies)
self.missing_dependency_installer.run()

def ignore_missing_dependencies(self):
old_deps_string = fci.Preferences().get("ignored_missing_deps")
old_deps = set(old_deps_string.split(";") if old_deps_string else [])
deps = self.check_missing_dependencies_worker.missing_dependencies
new_deps = old_deps.union(deps.wbs)
new_deps = new_deps.union(deps.external_addons)
new_deps = new_deps.union(deps.python_requires)
new_deps_string = ";".join(new_deps)
fci.Preferences().set("ignored_missing_deps", new_deps_string)

def stop_update(self) -> None:
self.cleanup_workers()
self.hide_progress_widgets()
Expand Down
9 changes: 5 additions & 4 deletions addonmanager_installer_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,11 @@ def __init__(self, addons: List[Addon], deps: MissingDependencies):
self.installer: AddonInstaller = None
self.dependency_installer: DependencyInstaller = None

self.dependency_dialog = fci.loadUi(
os.path.join(os.path.dirname(__file__), "dependency_resolution_dialog.ui")
)
self.dependency_dialog.setObjectName("AddonManager_DependencyResolutionDialog")

def shutdown(self):
try:
self._stop_thread(self.dependency_worker_thread)
Expand Down Expand Up @@ -591,10 +596,6 @@ def _report_missing_workbenches(self) -> bool:

def _resolve_dependencies_then_continue(self) -> None:
"""Ask the user how they want to handle dependencies, do that, then install."""
self.dependency_dialog = fci.loadUi(
os.path.join(os.path.dirname(__file__), "dependency_resolution_dialog.ui")
)
self.dependency_dialog.setObjectName("AddonManager_DependencyResolutionDialog")

for addon in self.deps.external_addons:
self.dependency_dialog.listWidgetAddons.addItem(addon)
Expand Down
1 change: 1 addition & 0 deletions addonmanager_preferences_defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"alwaysAskForToolbar": true,
"dontShowAddMacroButtonDialog": false,
"force_git_in_repos": "parts_library",
"ignored_missing_deps": "",
"last_fetched_addon_catalog_cache_hash": "Cache never fetched, no hash available",
"last_fetched_macro_cache_hash": "Cache never fetched, no hash available",
"macro_cache_url": "https://addons.freecad.org/macro_cache.zip",
Expand Down
55 changes: 54 additions & 1 deletion addonmanager_workers_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from addonmanager_installation_manifest import InstallationManifest

from addonmanager_macro import Macro
from Addon import Addon
from Addon import Addon, MissingDependencies
from AddonCatalog import AddonCatalog
from AddonStats import AddonStats
import NetworkManager
Expand Down Expand Up @@ -638,3 +638,56 @@ def run(self):
fci.Console.PrintLog(
f"Failed to convert score value '{score}' to an integer for {addon.name}"
)


class CheckForMissingDependenciesWorker(QtCore.QThread):
"""A worker class to examine installed addons and check for missing dependencies"""

progress = QtCore.Signal(str, int, int)

def __init__(self, addons: List[Addon], parent: QtCore.QObject = None):
super().__init__(parent)
self.addons = addons
self.missing_dependencies = MissingDependencies()

def run(self):
self.progress.emit(
translate("AddonsInstaller", "Checking for missing dependencies"),
0,
len(self.addons),
)

installed_addons = [
addon for addon in self.addons if addon.status() != Addon.Status.NOT_INSTALLED
]
counter = 0
details = ""
for addon in installed_addons:
counter += 1
self.progress.emit(
translate("AddonsInstaller", "Checking for missing dependencies"),
counter,
len(installed_addons),
)
deps = MissingDependencies()
deps.import_from_addon(addon, self.addons)
if deps.wbs:
details += f"{addon.display_name} is missing workbenches {', '.join(deps.wbs)}\n"
if deps.external_addons:
details += (
f"{addon.display_name} is missing addons {', '.join(deps.external_addons)}\n"
)
if deps.python_requires:
details += f"{addon.display_name} is missing python packages {', '.join(deps.python_requires)}\n"
self.missing_dependencies.join(deps)

md = self.missing_dependencies
message = "\nAddon Missing Dependency Analysis\n"
message += "---------------------------------\n"
message += f"Missing FreeCAD Workbenches: {len(md.wbs)}\n"
message += f"Missing addons: {len(md.external_addons)}\n"
message += f"Missing required Python packages: {len(md.python_requires)}\n"
message += f"Missing optional Python packages: {len(md.python_optional)}\n"
message += f"Minimum required Python version evaluated to {md.python_min_version}\n\n"
fci.Console.PrintMessage(message)
fci.Console.PrintLog(details)