Skip to content
Open
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
2 changes: 1 addition & 1 deletion Addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ def get_zip_url(self) -> str:
else:
# The ZIP url is based on the location of the main cache file:
if self.relative_cache_path:
cache_file_url = fci.Preferences().get("addon_catalog_cache_url")
cache_file_url = fci.Preferences().get("addon_index_cache_url")
parsed_url = urlparse(cache_file_url)
path_parts = parsed_url.path.rpartition("/")
new_path = path_parts[0] + "/" + self.relative_cache_path
Expand Down
53 changes: 47 additions & 6 deletions AddonManagerTest/app/test_workers_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ def test_no_new_catalog_available(self, mock_network_manager, mock_preferences_c
)

def get_side_effect(key):
if key == "last_fetched_addon_catalog_cache_hash":
if key == "last_fetched_addon_index_cache_hash":
return "1234567890abcdef"
elif key == "addon_catalog_cache_url":
elif key == "addon_index_cache_url":
return "https://some.url"
return None

mock_preferences_instance.get = MagicMock(side_effect=get_side_effect)

# Act
result = addonmanager_workers_startup.CreateAddonListWorker.new_cache_available(
"addon_catalog"
"addon_index"
)

# Assert
Expand All @@ -71,17 +71,17 @@ def test_new_catalog_is_available(self, mock_network_manager, mock_preferences_c
)

def get_side_effect(key):
if key == "last_fetched_addon_catalog_cache_hash":
if key == "last_fetched_addon_index_cache_hash":
return "fedcba0987654321" # NOT the same hash
elif key == "addon_catalog_cache_url":
elif key == "addon_index_cache_url":
return "https://some.url"
return None

mock_preferences_instance.get = MagicMock(side_effect=get_side_effect)

# Act
result = addonmanager_workers_startup.CreateAddonListWorker.new_cache_available(
"addon_catalog"
"addon_index"
)

# Assert
Expand Down Expand Up @@ -155,3 +155,44 @@ def test_process_addon_catalog_with_user_override(

# Assert
self.assertEqual(8, mock_addon_repo_signal.emit.call_count)

@patch("addonmanager_workers_startup.fci.Preferences")
@patch("addonmanager_workers_startup.fci.Console")
def test_migrate_catalog_to_index_no_custom_data(self, mock_console, mock_preferences_class):
# Arrange
def return_no_custom_data(key):
return None if key != "addon_catalog_cache_url" else "obsolete"

mock_preferences_instance = MagicMock()
mock_preferences_instance.get = return_no_custom_data
mock_preferences_class.return_value = mock_preferences_instance
worker = addonmanager_workers_startup.CreateAddonListWorker()

# Act
worker.migrate_catalog_to_index()

# Assert
mock_preferences_instance.set.assert_not_called()
mock_console.PrintWarning.assert_not_called()

@patch("addonmanager_workers_startup.fci.Preferences")
@patch("addonmanager_workers_startup.fci.Console")
def test_migrate_catalog_to_index_custom_data(self, mock_console, mock_preferences_class):

# Arrange
def return_custom_data(key):
return None if key != "addon_catalog_cache_url" else "some custom url"

mock_preferences_instance = MagicMock()
mock_preferences_instance.get = return_custom_data
mock_preferences_class.return_value = mock_preferences_instance
worker = addonmanager_workers_startup.CreateAddonListWorker()

# Act
worker.migrate_catalog_to_index()

# Assert
mock_preferences_instance.set.assert_called_once_with(
"addon_index_cache_url", "some custom url"
)
mock_console.PrintWarning.assert_called()
5 changes: 3 additions & 2 deletions addonmanager_preferences_defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@
"ViewStyle": 1,
"WindowHeight": 600,
"WindowWidth": 800,
"addon_catalog_cache_url": "https://addons.freecad.org/addon_catalog_cache.zip",
"addon_catalog_cache_url": "obsolete, replaced by addon_index_cache_url",
"addon_index_cache_url": "https://addons.freecad.org/addon_index_cache.zip",
"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_addon_index_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",
"old_backup_handling": "ask",
Expand Down
15 changes: 13 additions & 2 deletions addonmanager_workers_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,12 @@ def run(self):
self._get_custom_addons()
self.progress_made.emit("Custom addons loaded", 5, 100)

addon_cache = self.get_cache("addon_catalog")
CreateAddonListWorker.migrate_catalog_to_index()

addon_cache = self.get_cache("addon_index")
if addon_cache:
self.process_addon_cache(addon_cache)
self.progress_made.emit("Addon catalog loaded", 20, 100)
self.progress_made.emit("Addon index loaded", 20, 100)

macro_cache = self.get_cache("macro")
if macro_cache:
Expand All @@ -90,6 +92,15 @@ def run(self):
fci.Console.PrintError(str(e) + "\n")
return

@staticmethod
def migrate_catalog_to_index():
old_catalog_url = fci.Preferences().get("addon_catalog_cache_url")
if old_catalog_url.startswith("obsolete"):
return # Nothing to migrate, it was never set to anything else
fci.Console.PrintWarning("Custom catalog URL was detected: using as the index URL now\n")
fci.Console.PrintWarning(f"URL: {old_catalog_url}\n")
fci.Preferences().set("addon_index_cache_url", old_catalog_url)
Comment on lines +97 to +102
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

migrate_catalog_to_index() copies addon_catalog_cache_url directly into addon_index_cache_url. If a user previously pointed addon_catalog_cache_url at an .../addon_catalog_cache.zip, the startup code will then download that ZIP as the index and fail because it won’t contain addon_index_cache.json. Consider translating well-known catalog URLs (e.g. replacing addon_catalog_cache.zip with addon_index_cache.zip) and/or only migrating when the old value is truly custom vs the prior default.

Suggested change
old_catalog_url = fci.Preferences().get("addon_catalog_cache_url")
if old_catalog_url.startswith("obsolete"):
return # Nothing to migrate, it was never set to anything else
fci.Console.PrintWarning("Custom catalog URL was detected: using as the index URL now\n")
fci.Console.PrintWarning(f"URL: {old_catalog_url}\n")
fci.Preferences().set("addon_index_cache_url", old_catalog_url)
prefs = fci.Preferences()
old_catalog_url = prefs.get("addon_catalog_cache_url")
# Nothing to migrate if the old value was never set or explicitly marked obsolete
if not old_catalog_url or old_catalog_url.startswith("obsolete"):
return
# Do not overwrite an existing, non-obsolete index URL
existing_index_url = prefs.get("addon_index_cache_url")
if existing_index_url and not existing_index_url.startswith("obsolete"):
return
# Translate well-known catalog ZIP filenames to the corresponding index ZIP filenames
new_index_url = old_catalog_url
catalog_suffix = "addon_catalog_cache.zip"
index_suffix = "addon_index_cache.zip"
if old_catalog_url.endswith(catalog_suffix):
new_index_url = old_catalog_url[: -len(catalog_suffix)] + index_suffix
fci.Console.PrintWarning("Custom catalog URL was detected: using as the index URL now\n")
fci.Console.PrintWarning(f"URL: {new_index_url}\n")
prefs.set("addon_index_cache_url", new_index_url)

Copilot uses AI. Check for mistakes.

Comment on lines +96 to +103
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After migrating, the old addon_catalog_cache_url value is left intact. That means this migration will run (and warn) on every startup and can keep overwriting a user’s addon_index_cache_url if they later customize it. Consider clearing/removing addon_catalog_cache_url (or setting it to the new "obsolete" marker / storing a one-time migration flag) after a successful migration so it’s idempotent and doesn’t spam warnings.

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +103
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new migration path isn’t fully covered by tests for the important edge cases: (1) a stored old value equal to the previous default catalog URL should not be treated as "custom", and (2) a custom URL ending in addon_catalog_cache.zip should be translated to the index filename (if that’s the intended behavior), and (3) the old key should be cleared so migration is one-time. Adding focused unit tests for these cases would prevent regressions.

Copilot uses AI. Check for mistakes.
def _get_custom_addons(self):

# querying custom addons first
Expand Down
4 changes: 2 additions & 2 deletions package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<name>Addon Manager</name>
<description>Development branch of a tool to install workbenches, macros, themes, etc.</description>
<icon>Resources/icons/addon_manager.svg</icon>
<version>2026.2.9dev</version>
<date>2026-02-09</date>
<version>2026.2.11dev</version>
<date>2026-02-11</date>
<maintainer email='chennes@freecad.org'>Chris Hennes</maintainer>
<author email = 'yorik@uncreated.net'>Yorik van Havre</author>
<author>Jonathan Wiedemann</author>
Expand Down
Loading