From d92196da5346cba0e36d46882599f20c57c8e15a Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 12 Feb 2025 17:22:27 +0100 Subject: [PATCH 01/21] Exclude plugins available in Marketplace from backups. part of CURA-12156 --- cura/API/Backups.py | 15 +++---- cura/Backups/Backup.py | 35 ++++++++++++--- cura/Backups/BackupsManager.py | 9 ++-- plugins/CuraDrive/src/CreateBackupJob.py | 54 +++++++++++++++++++----- 4 files changed, 87 insertions(+), 26 deletions(-) diff --git a/cura/API/Backups.py b/cura/API/Backups.py index 1940d38a36..07001d22e2 100644 --- a/cura/API/Backups.py +++ b/cura/API/Backups.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. from typing import Tuple, Optional, TYPE_CHECKING, Dict, Any @@ -9,14 +9,10 @@ if TYPE_CHECKING: class Backups: - """The back-ups API provides a version-proof bridge between Cura's - - BackupManager and plug-ins that hook into it. + """The back-ups API provides a version-proof bridge between Cura's BackupManager and plug-ins that hook into it. Usage: - .. code-block:: python - from cura.API import CuraAPI api = CuraAPI() api.backups.createBackup() @@ -26,13 +22,13 @@ class Backups: def __init__(self, application: "CuraApplication") -> None: self.manager = BackupsManager(application) - def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]: + def createBackup(self, available_remote_plugins: frozenset[str] = frozenset()) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]: """Create a new back-up using the BackupsManager. :return: Tuple containing a ZIP file with the back-up data and a dict with metadata about the back-up. """ - return self.manager.createBackup() + return self.manager.createBackup(available_remote_plugins) def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None: """Restore a back-up using the BackupsManager. @@ -42,3 +38,6 @@ class Backups: """ return self.manager.restoreBackup(zip_file, meta_data) + + def shouldReinstallDownloadablePlugins(self) -> bool: + return self.manager.shouldReinstallDownloadablePlugins() diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 19655df531..9f35e54ef1 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,5 +1,6 @@ -# Copyright (c) 2021 Ultimaker B.V. +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. +import json import io import os @@ -13,6 +14,7 @@ from UM import i18nCatalog from UM.Logger import Logger from UM.Message import Message from UM.Platform import Platform +from UM.PluginRegistry import PluginRegistry from UM.Resources import Resources from UM.Version import Version @@ -30,10 +32,14 @@ class Backup: """These files should be ignored when making a backup.""" IGNORED_FOLDERS = [] # type: List[str] + """These folders should be ignored when making a backup.""" SECRETS_SETTINGS = ["general/ultimaker_auth_data"] """Secret preferences that need to obfuscated when making a backup of Cura""" + TO_INSTALL_FILE = "packages.json" + """File that contains the 'to_install' dictionary, that manages plugins to be installed on next startup.""" + catalog = i18nCatalog("cura") """Re-use translation catalog""" @@ -42,7 +48,7 @@ class Backup: self.zip_file = zip_file # type: Optional[bytes] self.meta_data = meta_data # type: Optional[Dict[str, str]] - def makeFromCurrent(self) -> None: + def makeFromCurrent(self, available_remote_plugins: frozenset[str] = frozenset()) -> None: """Create a back-up from the current user config folder.""" cura_release = self._application.getVersion() @@ -92,21 +98,40 @@ class Backup: # Restore the obfuscated settings self._illuminate(**secrets) - def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]: + def _fillToInstallsJson(self, file_path: str, reinstall_on_restore: dict[str, str], archive: ZipFile) -> None: + pass # TODO! + + def _findRedownloadablePlugins(self, available_remote_plugins: frozenset) -> dict[str, str]: + """ Find all plugins that should be able to be reinstalled from the Marketplace. + + :param plugins_path: Path to all plugins in the user-space. + :return: Set of all package-id's of plugins that can be reinstalled from the Marketplace. + """ + plugin_reg = PluginRegistry.getInstance() + id = "id" + return {v["location"]: v[id] for v in plugin_reg.getAllMetaData() + if v[id] in available_remote_plugins and not plugin_reg.isBundledPlugin(v[id])} + + def _makeArchive(self, buffer: "io.BytesIO", root_path: str, available_remote_plugins: frozenset) -> Optional[ZipFile]: """Make a full archive from the given root path with the given name. :param root_path: The root directory to archive recursively. :return: The archive as bytes. """ ignore_string = re.compile("|".join(self.IGNORED_FILES + self.IGNORED_FOLDERS)) + reinstall_instead_plugins = self._findRedownloadablePlugins(available_remote_plugins) try: archive = ZipFile(buffer, "w", ZIP_DEFLATED) - for root, folders, files in os.walk(root_path): + for root, folders, files in os.walk(root_path, topdown=True): + folders[:] = [f for f in folders if f not in reinstall_instead_plugins] for item_name in folders + files: absolute_path = os.path.join(root, item_name) if ignore_string.search(absolute_path): continue - archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):]) + if item_name == self.TO_INSTALL_FILE: + self._fillToInstallsJson(absolute_path, reinstall_instead_plugins, archive) + else: + archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):]) archive.close() return archive except (IOError, OSError, BadZipfile) as error: diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 6c4670edb6..90dfc5e34e 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. from typing import Dict, Optional, Tuple, TYPE_CHECKING @@ -22,7 +22,10 @@ class BackupsManager: def __init__(self, application: "CuraApplication") -> None: self._application = application - def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]: + def shouldReinstallDownloadablePlugins(self) -> bool: + return True + + def createBackup(self, available_remote_plugins: frozenset[str] = frozenset()) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]: """ Get a back-up of the current configuration. @@ -31,7 +34,7 @@ class BackupsManager: self._disableAutoSave() backup = Backup(self._application) - backup.makeFromCurrent() + backup.makeFromCurrent(available_remote_plugins if self.shouldReinstallDownloadablePlugins() else frozenset()) self._enableAutoSave() # We don't return a Backup here because we want plugins only to interact with our API and not full objects. return backup.zip_file, backup.meta_data diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index 7d772769ed..cdd1d569c7 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Ultimaker B.V. +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. import json import threading @@ -13,11 +13,14 @@ from UM.Message import Message from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from UM.i18n import i18nCatalog +from cura.ApplicationMetadata import CuraSDKVersion from cura.CuraApplication import CuraApplication from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope +import cura.UltimakerCloud.UltimakerCloudConstants as UltimakerCloudConstants catalog = i18nCatalog("cura") +PACKAGES_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}/cura/v{CuraSDKVersion}/packages" class CreateBackupJob(Job): """Creates backup zip, requests upload url and uploads the backup file to cloud storage.""" @@ -40,23 +43,54 @@ class CreateBackupJob(Job): self._job_done = threading.Event() """Set when the job completes. Does not indicate success.""" self.backup_upload_error_message = "" - """After the job completes, an empty string indicates success. Othrerwise, the value is a translated message.""" + """After the job completes, an empty string indicates success. Otherwise, the value is a translated message.""" + + def _setPluginFetchErrorMessage(self, error_msg: str) -> None: + Logger.error(f"Fetching plugins for backup resulted in error: {error_msg}") + self.backup_upload_error_message = "Couldn't update currently available plugins, backup stopped." + self._upload_message.hide() + self._job_done.set() def run(self) -> None: - upload_message = Message(catalog.i18nc("@info:backup_status", "Creating your backup..."), + self._upload_message = Message(catalog.i18nc("@info:backup_status", "Fetch re-downloadable package-ids..."), title = self.MESSAGE_TITLE, progress = -1) - upload_message.show() + self._upload_message.show() + CuraApplication.getInstance().processEvents() + + if CuraApplication.getInstance().getCuraAPI().backups.shouldReinstallDownloadablePlugins(): + request_url = f"{PACKAGES_URL}?package_type=plugin" + scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) + HttpRequestManager.getInstance().get( + request_url, + scope=scope, + callback=self._continueRun, + error_callback=lambda reply, error: self._setPluginFetchErrorMessage(str(error)), + ) + else: + self._continueRun() + + def _continueRun(self, reply: "QNetworkReply" = None) -> None: + if reply is not None: + response_data = HttpRequestManager.readJSON(reply) + if "data" not in response_data: + self._setPluginFetchErrorMessage(f"Missing 'data' from response. Keys in response: {response_data.keys()}") + return + available_remote_plugins = frozenset({v["package_id"] for v in response_data["data"]}) + else: + available_remote_plugins = frozenset() + + self._upload_message.setText(catalog.i18nc("@info:backup_status", "Creating your backup...")) CuraApplication.getInstance().processEvents() cura_api = CuraApplication.getInstance().getCuraAPI() - self._backup_zip, backup_meta_data = cura_api.backups.createBackup() + self._backup_zip, backup_meta_data = cura_api.backups.createBackup(available_remote_plugins) if not self._backup_zip or not backup_meta_data: self.backup_upload_error_message = catalog.i18nc("@info:backup_status", "There was an error while creating your backup.") - upload_message.hide() + self._upload_message.hide() return - upload_message.setText(catalog.i18nc("@info:backup_status", "Uploading your backup...")) + self._upload_message.setText(catalog.i18nc("@info:backup_status", "Uploading your backup...")) CuraApplication.getInstance().processEvents() # Create an upload entry for the backup. @@ -66,11 +100,11 @@ class CreateBackupJob(Job): self._job_done.wait() if self.backup_upload_error_message == "": - upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading.")) - upload_message.setProgress(None) # Hide progress bar + self._upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading.")) + self._upload_message.setProgress(None) # Hide progress bar else: # some error occurred. This error is presented to the user by DrivePluginExtension - upload_message.hide() + self._upload_message.hide() def _requestUploadSlot(self, backup_metadata: Dict[str, Any], backup_size: int) -> None: """Request a backup upload slot from the API. From d167e3f28ed8843786414faa5a0331a5d3ee33fa Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Thu, 27 Feb 2025 16:52:27 +0100 Subject: [PATCH 02/21] Work in progress on pulling plugins out of the backups. It's now in a state where it can actually upload ... something (that should work). Not tested the restore yet. I did run into trouble with the max concurrent requests, which I had to up to [more than 4, now on 8] to get it to work -- I'm not sure if I'm just working around a bug here, or if that's expected behaviour. part of CURA-12156 --- cura/API/Backups.py | 4 +- cura/Backups/Backup.py | 67 ++++++++++++------ cura/Backups/BackupsManager.py | 5 +- plugins/CuraDrive/src/CreateBackupJob.py | 4 ++ plugins/CuraDrive/src/RestoreBackupJob.py | 82 +++++++++++++++++++++-- 5 files changed, 132 insertions(+), 30 deletions(-) diff --git a/cura/API/Backups.py b/cura/API/Backups.py index 07001d22e2..a52dcbfb6b 100644 --- a/cura/API/Backups.py +++ b/cura/API/Backups.py @@ -30,14 +30,14 @@ class Backups: return self.manager.createBackup(available_remote_plugins) - def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None: + def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any], auto_close: bool = True) -> None: """Restore a back-up using the BackupsManager. :param zip_file: A ZIP file containing the actual back-up data. :param meta_data: Some metadata needed for restoring a back-up, like the Cura version number. """ - return self.manager.restoreBackup(zip_file, meta_data) + return self.manager.restoreBackup(zip_file, meta_data, auto_close=auto_close) def shouldReinstallDownloadablePlugins(self) -> bool: return self.manager.shouldReinstallDownloadablePlugins() diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 9f35e54ef1..1163169b94 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. +import tempfile + import json import io @@ -8,7 +10,7 @@ import re import shutil from copy import deepcopy from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile -from typing import Dict, Optional, TYPE_CHECKING, List +from typing import Callable, Dict, Optional, TYPE_CHECKING, List from UM import i18nCatalog from UM.Logger import Logger @@ -37,9 +39,6 @@ class Backup: SECRETS_SETTINGS = ["general/ultimaker_auth_data"] """Secret preferences that need to obfuscated when making a backup of Cura""" - TO_INSTALL_FILE = "packages.json" - """File that contains the 'to_install' dictionary, that manages plugins to be installed on next startup.""" - catalog = i18nCatalog("cura") """Re-use translation catalog""" @@ -74,7 +73,7 @@ class Backup: # Create an empty buffer and write the archive to it. buffer = io.BytesIO() - archive = self._makeArchive(buffer, version_data_dir) + archive = self._makeArchive(buffer, version_data_dir, available_remote_plugins) if archive is None: return files = archive.namelist() @@ -83,9 +82,7 @@ class Backup: machine_count = max(len([s for s in files if "machine_instances/" in s]) - 1, 0) # If people delete their profiles but not their preferences, it can still make a backup, and report -1 profiles. Server crashes on this. material_count = max(len([s for s in files if "materials/" in s]) - 1, 0) profile_count = max(len([s for s in files if "quality_changes/" in s]) - 1, 0) - # We don't store plugins anymore, since if you can make backups, you have an account (and the plugins are - # on the marketplace anyway) - plugin_count = 0 + plugin_count = len([s for s in files if "plugin.json" in s]) # Store the archive and metadata so the BackupManager can fetch them when needed. self.zip_file = buffer.getvalue() self.meta_data = { @@ -98,19 +95,43 @@ class Backup: # Restore the obfuscated settings self._illuminate(**secrets) - def _fillToInstallsJson(self, file_path: str, reinstall_on_restore: dict[str, str], archive: ZipFile) -> None: - pass # TODO! + def _fillToInstallsJson(self, file_path: str, reinstall_on_restore: frozenset[str], add_to_archive: Callable[[str], None]) -> Optional[str]: + """ Moves all plugin-data (in a config-file) for plugins that could be (re)installed from the Marketplace from + 'installed' to 'to_installs' before adding that file to the archive. - def _findRedownloadablePlugins(self, available_remote_plugins: frozenset) -> dict[str, str]: + Note that the 'filename'-entry in the package-data (of the plugins) might not be valid anymore on restore. + We'll replace it on restore instead, as that's the time when the new package is downloaded. + + :param file_path: Absolute path to the packages-file. + :param reinstall_on_restore: A set of plugins that _can_ be reinstalled from the Marketplace. + :param add_to_archive: A function/lambda that takes a filename and adds it to the archive. + """ + with open(file_path, "r") as file: + data = json.load(file) + reinstall, keep_in = {}, {} + for install_id, install_info in data["installed"].items(): + (reinstall if install_id in reinstall_on_restore else keep_in)[install_id] = install_info + data["installed"] = keep_in + data["to_install"].update(reinstall) + if data is not None: + tmpfile = tempfile.NamedTemporaryFile(delete=False) + with open(tmpfile.name, "w") as outfile: + json.dump(data, outfile) + add_to_archive(tmpfile.name) + return tmpfile.name + return None + + def _findRedownloadablePlugins(self, available_remote_plugins: frozenset) -> (frozenset[str], frozenset[str]): """ Find all plugins that should be able to be reinstalled from the Marketplace. :param plugins_path: Path to all plugins in the user-space. - :return: Set of all package-id's of plugins that can be reinstalled from the Marketplace. + :return: Tuple of a set of plugin-ids and a set of plugin-paths. """ plugin_reg = PluginRegistry.getInstance() id = "id" - return {v["location"]: v[id] for v in plugin_reg.getAllMetaData() - if v[id] in available_remote_plugins and not plugin_reg.isBundledPlugin(v[id])} + plugins = [v for v in plugin_reg.getAllMetaData() + if v[id] in available_remote_plugins and not plugin_reg.isBundledPlugin(v[id])] + return frozenset([v[id] for v in plugins]), frozenset([v["location"] for v in plugins]) def _makeArchive(self, buffer: "io.BytesIO", root_path: str, available_remote_plugins: frozenset) -> Optional[ZipFile]: """Make a full archive from the given root path with the given name. @@ -119,20 +140,28 @@ class Backup: :return: The archive as bytes. """ ignore_string = re.compile("|".join(self.IGNORED_FILES + self.IGNORED_FOLDERS)) - reinstall_instead_plugins = self._findRedownloadablePlugins(available_remote_plugins) + reinstall_instead_ids, reinstall_instead_paths = self._findRedownloadablePlugins(available_remote_plugins) + tmpfiles = [] try: archive = ZipFile(buffer, "w", ZIP_DEFLATED) + add_path_to_archive = lambda path: archive.write(path, path[len(root_path) + len(os.sep):]) for root, folders, files in os.walk(root_path, topdown=True): - folders[:] = [f for f in folders if f not in reinstall_instead_plugins] + folders[:] = [f for f in folders if f not in reinstall_instead_paths] for item_name in folders + files: absolute_path = os.path.join(root, item_name) if ignore_string.search(absolute_path): continue - if item_name == self.TO_INSTALL_FILE: - self._fillToInstallsJson(absolute_path, reinstall_instead_plugins, archive) + if item_name == "packages.json": + tmpfiles.append( + self._fillToInstallsJson(absolute_path, reinstall_instead_ids, add_path_to_archive)) else: - archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):]) + add_path_to_archive(absolute_path) archive.close() + for tmpfile_path in tmpfiles: + try: + os.remove(tmpfile_path) + except IOError as ex: + Logger.warning(f"Couldn't remove temporary file '{tmpfile_path}' because '{ex}'.") return archive except (IOError, OSError, BadZipfile) as error: Logger.log("e", "Could not create archive from user data directory: %s", error) diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 90dfc5e34e..67d6c84601 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -39,12 +39,13 @@ class BackupsManager: # We don't return a Backup here because we want plugins only to interact with our API and not full objects. return backup.zip_file, backup.meta_data - def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str]) -> None: + def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str], auto_close: bool = True) -> None: """ Restore a back-up from a given ZipFile. :param zip_file: A bytes object containing the actual back-up. :param meta_data: A dict containing some metadata that is needed to restore the back-up correctly. + :param auto_close: Normally, Cura will need to close immediately after restoring the back-up. """ if not meta_data.get("cura_release", None): @@ -57,7 +58,7 @@ class BackupsManager: backup = Backup(self._application, zip_file = zip_file, meta_data = meta_data) restored = backup.restore() - if restored: + if restored and auto_close: # At this point, Cura will need to restart for the changes to take effect. # We don't want to store the data at this point as that would override the just-restored backup. self._application.windowClosed(save_data = False) diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index cdd1d569c7..6297af305f 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -118,6 +118,8 @@ class CreateBackupJob(Job): } }).encode() + CuraApplication.getInstance().processEvents() # Needed?? + HttpRequestManager.getInstance().put( self._api_backup_url, data = payload, @@ -125,6 +127,8 @@ class CreateBackupJob(Job): error_callback = self._onUploadSlotCompleted, scope = self._json_cloud_scope) + CuraApplication.getInstance().processEvents() # Needed?? + def _onUploadSlotCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if HttpRequestManager.safeHttpStatus(reply) >= 300: replyText = HttpRequestManager.readText(reply) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index 54c94b389e..c5fd1fceae 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -1,8 +1,12 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import tempfile + +import json import base64 import hashlib +import os import threading from tempfile import NamedTemporaryFile from typing import Optional, Any, Dict @@ -12,9 +16,16 @@ from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest from UM.Job import Job from UM.Logger import Logger from UM.PackageManager import catalog +from UM.Resources import Resources from UM.TaskManagement.HttpRequestManager import HttpRequestManager -from cura.CuraApplication import CuraApplication +from UM.Version import Version +from cura.ApplicationMetadata import CuraSDKVersion +from cura.CuraApplication import CuraApplication +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope +import cura.UltimakerCloud.UltimakerCloudConstants as UltimakerCloudConstants + +PACKAGES_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}/cura/v{CuraSDKVersion}/packages" class RestoreBackupJob(Job): """Downloads a backup and overwrites local configuration with the backup. @@ -60,8 +71,8 @@ class RestoreBackupJob(Job): # We store the file in a temporary path fist to ensure integrity. try: - temporary_backup_file = NamedTemporaryFile(delete = False) - with open(temporary_backup_file.name, "wb") as write_backup: + self._temporary_backup_file = NamedTemporaryFile(delete = False) + with open(self._temporary_backup_file.name, "wb") as write_backup: app = CuraApplication.getInstance() bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) while bytes_read: @@ -74,18 +85,75 @@ class RestoreBackupJob(Job): self._job_done.set() return - if not self._verifyMd5Hash(temporary_backup_file.name, self._backup.get("md5_hash", "")): + if not self._verifyMd5Hash(self._temporary_backup_file.name, self._backup.get("md5_hash", "")): # Don't restore the backup if the MD5 hashes do not match. # This can happen if the download was interrupted. Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.") self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE # Tell Cura to place the backup back in the user data folder. - with open(temporary_backup_file.name, "rb") as read_backup: + metadata = self._backup.get("metadata", {}) + with open(self._temporary_backup_file.name, "rb") as read_backup: cura_api = CuraApplication.getInstance().getCuraAPI() - cura_api.backups.restoreBackup(read_backup.read(), self._backup.get("metadata", {})) + cura_api.backups.restoreBackup(read_backup.read(), metadata, auto_close=False) - self._job_done.set() + # Read packages data-file, to get the 'to_install' plugin-ids. + version_to_restore = Version(metadata.get("cura_release", "dev")) + version_str = f"{version_to_restore.getMajor()}.{version_to_restore.getMinor()}" + packages_path = os.path.abspath(os.path.join(os.path.abspath( + Resources.getConfigStoragePath()), "..", version_str, "packages.json")) + if not os.path.exists(packages_path): + self._job_done.set() + return + + to_install = set() + try: + with open(packages_path, "r") as packages_file: + packages_json = json.load(packages_file) + if "to_install" in packages_json and "package_id" in packages_json["to_install"]: + to_install.add(packages_json["to_install"]["package_id"]) + except IOError as ex: + pass # TODO! (log + message) + + if len(to_install) < 1: + self._job_done.set() + return + + # Download all re-installable plugins packages, so they can be put back on start-up. + redownload_errors = [] + def packageDownloadCallback(package_id: str, msg: "QNetworkReply", err: "QNetworkReply.NetworkError" = None) -> None: + if err is not None or HttpRequestManager.safeHttpStatus(msg) != 200: + redownload_errors.append(err) + to_install.remove(package_id) + + try: + with tempfile.NamedTemporaryFile(mode="wb+", suffix=".curapackage") as temp_file: + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + while bytes_read: + temp_file.write(bytes_read) + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + # self._app.processEvents() + # self._progress[package_id]["file_written"] = temp_file.name + if not CuraApplication.getInstance().getPackageManager().installPackage(temp_file.name): + redownload_errors.append(f"Couldn't install package '{package_id}'.") + except IOError as ex: + redownload_errors.append(f"Couldn't read package '{package_id}' because '{ex}'.") + + if len(to_install) < 1: + if len(redownload_errors) == 0: + self._job_done.set() + else: + print("|".join(redownload_errors)) # TODO: Message / Log instead. + self._job_done.set() # NOTE: Set job probably not the right call here... (depends on wether or not that in the end closes the app or not...) + + self._package_download_scope = UltimakerCloudScope(CuraApplication.getInstance()) + for package_id in to_install: + HttpRequestManager.getInstance().get( + f"{PACKAGES_URL}/{package_id}/download", + scope=self._package_download_scope, + callback=lambda msg: packageDownloadCallback(package_id, msg), + error_callback=lambda msg, err: packageDownloadCallback(package_id, msg, err) + ) @staticmethod def _verifyMd5Hash(file_path: str, known_hash: str) -> bool: From 82939b2644e598dab75970c9831b78233c444cb5 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:53:00 +0100 Subject: [PATCH 03/21] Clean up Windows Start Menu --- packaging/NSIS/Ultimaker-Cura.nsi.jinja | 32 +++++++++---------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/packaging/NSIS/Ultimaker-Cura.nsi.jinja b/packaging/NSIS/Ultimaker-Cura.nsi.jinja index ac826af0d9..335626da12 100644 --- a/packaging/NSIS/Ultimaker-Cura.nsi.jinja +++ b/packaging/NSIS/Ultimaker-Cura.nsi.jinja @@ -16,13 +16,11 @@ !define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${APP_NAME}-${VERSION}" !define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}-${VERSION}" -!define REG_START_MENU "Start Menu Folder" +!define REG_START_MENU "Start Menu Shortcut" ;Require administrator access RequestExecutionLevel admin -var SM_Folder - ###################################################################### VIProductVersion "${VIVERSION}" @@ -68,7 +66,6 @@ InstallDir "$PROGRAMFILES64\${APP_NAME}" !define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}" !define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}" -!insertmacro MUI_PAGE_STARTMENU Application $SM_Folder !endif !insertmacro MUI_PAGE_INSTFILES @@ -108,25 +105,21 @@ WriteUninstaller "$INSTDIR\uninstall.exe" !ifdef REG_START_MENU !insertmacro MUI_STARTMENU_WRITE_BEGIN Application -CreateDirectory "$SMPROGRAMS\$SM_Folder" -CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" -CreateShortCut "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe" +CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" !ifdef WEB_SITE WriteIniStr "$INSTDIR\UltiMaker Cura website.url" "InternetShortcut" "URL" "${WEB_SITE}" -CreateShortCut "$SMPROGRAMS\$SM_Folder\UltiMaker Cura website.lnk" "$INSTDIR\UltiMaker Cura website.url" +CreateShortCut "$SMPROGRAMS\UltiMaker Cura website.lnk" "$INSTDIR\UltiMaker Cura website.url" !endif !insertmacro MUI_STARTMENU_WRITE_END !endif !ifndef REG_START_MENU -CreateDirectory "$SMPROGRAMS\{{ app_name }}" -CreateShortCut "$SMPROGRAMS\{{ app_name }}\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" -CreateShortCut "$SMPROGRAMS\{{ app_name }}\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe" +CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" !ifdef WEB_SITE WriteIniStr "$INSTDIR\UltiMaker Cura website.url" "InternetShortcut" "URL" "${WEB_SITE}" -CreateShortCut "$SMPROGRAMS\{{ app_name }}\UltiMaker Cura website.lnk" "$INSTDIR\UltiMaker Cura website.url" +CreateShortCut "$SMPROGRAMS\UltiMaker Cura website.lnk" "$INSTDIR\UltiMaker Cura website.url" !endif !endif @@ -184,22 +177,19 @@ Delete "$INSTDIR\${APP_NAME} website.url" RmDir /r /REBOOTOK "$INSTDIR" !ifdef REG_START_MENU -!insertmacro MUI_STARTMENU_GETFOLDER "Application" $SM_Folder -Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" -Delete "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk" +Delete "$SMPROGRAMS\${APP_NAME}.lnk" +Delete "$SMPROGRAMS\Uninstall ${APP_NAME}.lnk" !ifdef WEB_SITE -Delete "$SMPROGRAMS\$SM_Folder\UltiMaker Cura website.lnk" +Delete "$SMPROGRAMS\UltiMaker Cura website.lnk" !endif -RmDir "$SMPROGRAMS\$SM_Folder" !endif !ifndef REG_START_MENU -Delete "$SMPROGRAMS\{{ app_name }}\${APP_NAME}.lnk" -Delete "$SMPROGRAMS\{{ app_name }}\Uninstall ${APP_NAME}.lnk" +Delete "$SMPROGRAMS\${APP_NAME}.lnk" +Delete "$SMPROGRAMS\Uninstall ${APP_NAME}.lnk" !ifdef WEB_SITE -Delete "$SMPROGRAMS\{{ app_name }}\UltiMaker Cura website.lnk" +Delete "$SMPROGRAMS\UltiMaker Cura website.lnk" !endif -RmDir "$SMPROGRAMS\{{ app_name }}" !endif !insertmacro APP_UNASSOCIATE "stl" "Cura.model" From c33a32209311f0fb96c60db25e6743141e2a4064 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 26 Mar 2025 08:57:43 +0100 Subject: [PATCH 04/21] Also remove website link. Would otherwise be hanging loose in the start-menu somehwere. done as part of CURA-12502 --- packaging/NSIS/Ultimaker-Cura.nsi.jinja | 24 +--------------------- packaging/NSIS/create_windows_installer.py | 3 +-- packaging/msi/create_windows_msi.py | 3 +-- 3 files changed, 3 insertions(+), 27 deletions(-) diff --git a/packaging/NSIS/Ultimaker-Cura.nsi.jinja b/packaging/NSIS/Ultimaker-Cura.nsi.jinja index 335626da12..8c5d48f9dd 100644 --- a/packaging/NSIS/Ultimaker-Cura.nsi.jinja +++ b/packaging/NSIS/Ultimaker-Cura.nsi.jinja @@ -1,9 +1,8 @@ -# Copyright (c) 2022 UltiMaker B.V. +# Copyright (c) 2025 UltiMaker # Cura's build system is released under the terms of the AGPLv3 or higher. !define APP_NAME "{{ app_name }}" !define COMP_NAME "{{ company }}" -!define WEB_SITE "{{ web_site }}" !define VERSION "{{ version }}" !define VIVERSION "{{ version_major }}.{{ version_minor }}.{{ version_patch }}.0" !define COPYRIGHT "Copyright (c) {{ year }} {{ company }}" @@ -107,20 +106,11 @@ WriteUninstaller "$INSTDIR\uninstall.exe" !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" -!ifdef WEB_SITE -WriteIniStr "$INSTDIR\UltiMaker Cura website.url" "InternetShortcut" "URL" "${WEB_SITE}" -CreateShortCut "$SMPROGRAMS\UltiMaker Cura website.lnk" "$INSTDIR\UltiMaker Cura website.url" -!endif !insertmacro MUI_STARTMENU_WRITE_END !endif !ifndef REG_START_MENU CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" - -!ifdef WEB_SITE -WriteIniStr "$INSTDIR\UltiMaker Cura website.url" "InternetShortcut" "URL" "${WEB_SITE}" -CreateShortCut "$SMPROGRAMS\UltiMaker Cura website.lnk" "$INSTDIR\UltiMaker Cura website.url" -!endif !endif WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}" @@ -131,9 +121,6 @@ WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\${MAIN_APP_ WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayVersion" "${VERSION}" WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}" -!ifdef WEB_SITE -WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "URLInfoAbout" "${WEB_SITE}" -!endif SectionEnd ###################################################################### @@ -170,26 +157,17 @@ RmDir "$INSTDIR\share\uranium" RmDir "$INSTDIR\share" Delete "$INSTDIR\uninstall.exe" -!ifdef WEB_SITE -Delete "$INSTDIR\${APP_NAME} website.url" -!endif RmDir /r /REBOOTOK "$INSTDIR" !ifdef REG_START_MENU Delete "$SMPROGRAMS\${APP_NAME}.lnk" Delete "$SMPROGRAMS\Uninstall ${APP_NAME}.lnk" -!ifdef WEB_SITE -Delete "$SMPROGRAMS\UltiMaker Cura website.lnk" -!endif !endif !ifndef REG_START_MENU Delete "$SMPROGRAMS\${APP_NAME}.lnk" Delete "$SMPROGRAMS\Uninstall ${APP_NAME}.lnk" -!ifdef WEB_SITE -Delete "$SMPROGRAMS\UltiMaker Cura website.lnk" -!endif !endif !insertmacro APP_UNASSOCIATE "stl" "Cura.model" diff --git a/packaging/NSIS/create_windows_installer.py b/packaging/NSIS/create_windows_installer.py index d15d62b951..e01c757dbb 100644 --- a/packaging/NSIS/create_windows_installer.py +++ b/packaging/NSIS/create_windows_installer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 UltiMaker +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. @@ -51,7 +51,6 @@ def generate_nsi(source_path: str, dist_path: str, filename: str, version: str): version_minor = str(parsed_version.minor), version_patch = str(parsed_version.patch), company = "UltiMaker", - web_site = "https://ultimaker.com", year = datetime.now().year, cura_license_file = str(source_loc.joinpath("packaging", "cura_license.txt")), compression_method = "LZMA", # ZLIB, BZIP2 or LZMA diff --git a/packaging/msi/create_windows_msi.py b/packaging/msi/create_windows_msi.py index e44a9a924b..12c64ed24f 100644 --- a/packaging/msi/create_windows_msi.py +++ b/packaging/msi/create_windows_msi.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 UltiMaker +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. @@ -40,7 +40,6 @@ def generate_wxs(source_path: Path, dist_path: Path, filename: Path, app_name: s version_minor=str(parsed_version.minor), version_patch=str(parsed_version.patch), company="UltiMaker", - web_site="https://ultimaker.com", year=datetime.now().year, upgrade_code=str(uuid.uuid5(uuid.NAMESPACE_DNS, app_name)), cura_license_file=str(source_loc.joinpath("packaging", "msi", "cura_license.rtf")), From 460df87d1d0ad4289b5cdcbefd6a669526f7eec5 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 27 Mar 2025 10:47:18 +0100 Subject: [PATCH 05/21] Indicates that changing option requires a restart CURA-12486 Also move the note at the bottom of the page, because now labels from different categories use it, and this is where people should look for it. --- resources/qml/Preferences/GeneralPage.qml | 27 +++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index 62cab53a78..42469c6cf6 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -360,17 +360,6 @@ UM.PreferencesPage } } - UM.Label - { - id: languageCaption - - //: Language change warning - text: catalog.i18nc("@label", "*You will need to restart the application for these changes to have effect.") - wrapMode: Text.WordWrap - font.italic: true - - } - Item { //: Spacer @@ -705,7 +694,7 @@ UM.PreferencesPage UM.CheckBox { id: singleInstanceCheckbox - text: catalog.i18nc("@option:check","Use a single instance of Cura") + text: catalog.i18nc("@option:check","Use a single instance of Cura *") checked: boolCheck(UM.Preferences.getValue("cura/single_instance")) onCheckedChanged: UM.Preferences.setValue("cura/single_instance", checked) @@ -1101,8 +1090,6 @@ UM.PreferencesPage } } - - /* Multi-buildplate functionality is disabled because it's broken. See CURA-4975 for the ticket to remove it. Item { //: Spacer @@ -1110,6 +1097,18 @@ UM.PreferencesPage width: UM.Theme.getSize("default_margin").height } + UM.Label + { + id: languageCaption + + //: Language change warning + text: catalog.i18nc("@label", "*You will need to restart the application for these changes to have effect.") + wrapMode: Text.WordWrap + font.italic: true + } + + /* Multi-buildplate functionality is disabled because it's broken. See CURA-4975 for the ticket to remove it. + Label { font.bold: true From b295ca7d041776d2f7c06be4146fbac0e043424f Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 1 Apr 2025 15:10:27 +0200 Subject: [PATCH 06/21] Workaround for process-events during multiple http requests. Would not work otherwise, but that should have been true in the old situation then as well? CURA-12156 --- plugins/CuraDrive/src/CreateBackupJob.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index 6297af305f..67083d2e83 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -98,7 +98,12 @@ class CreateBackupJob(Job): backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"]) self._requestUploadSlot(backup_meta_data, len(self._backup_zip)) - self._job_done.wait() + # Note: One 'process events' call wasn't enough with the changed situation somehow. + active_done_check = False + while not active_done_check: + CuraApplication.getInstance().processEvents() + active_done_check = self._job_done.wait(0.02) + if self.backup_upload_error_message == "": self._upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading.")) self._upload_message.setProgress(None) # Hide progress bar @@ -117,9 +122,6 @@ class CreateBackupJob(Job): "metadata": backup_metadata } }).encode() - - CuraApplication.getInstance().processEvents() # Needed?? - HttpRequestManager.getInstance().put( self._api_backup_url, data = payload, @@ -127,8 +129,6 @@ class CreateBackupJob(Job): error_callback = self._onUploadSlotCompleted, scope = self._json_cloud_scope) - CuraApplication.getInstance().processEvents() # Needed?? - def _onUploadSlotCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if HttpRequestManager.safeHttpStatus(reply) >= 300: replyText = HttpRequestManager.readText(reply) From c857dab0f76196a803871da1b10a331458b262ee Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 1 Apr 2025 16:47:17 +0200 Subject: [PATCH 07/21] Logging (mostly on errors). CURA-12156 --- plugins/CuraDrive/src/RestoreBackupJob.py | 42 ++++++++++++++--------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index c5fd1fceae..e7cd66daf8 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -1,11 +1,8 @@ -# Copyright (c) 2021 Ultimaker B.V. +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. -import tempfile - -import json - import base64 import hashlib +import json import os import threading from tempfile import NamedTemporaryFile @@ -49,7 +46,6 @@ class RestoreBackupJob(Job): self.restore_backup_error_message = "" def run(self) -> None: - url = self._backup.get("download_url") assert url is not None @@ -59,7 +55,11 @@ class RestoreBackupJob(Job): error_callback = self._onRestoreRequestCompleted ) - self._job_done.wait() # A job is considered finished when the run function completes + # Note: Just to be sure, use the same structure here as in CreateBackupJob. + active_done_check = False + while not active_done_check: + CuraApplication.getInstance().processEvents() + active_done_check = self._job_done.wait(0.02) def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if not HttpRequestManager.replyIndicatesSuccess(reply, error): @@ -80,7 +80,7 @@ class RestoreBackupJob(Job): bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) app.processEvents() except EnvironmentError as e: - Logger.log("e", f"Unable to save backed up files due to computer limitations: {str(e)}") + Logger.error(f"Unable to save backed up files due to computer limitations: {str(e)}") self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE self._job_done.set() return @@ -88,8 +88,10 @@ class RestoreBackupJob(Job): if not self._verifyMd5Hash(self._temporary_backup_file.name, self._backup.get("md5_hash", "")): # Don't restore the backup if the MD5 hashes do not match. # This can happen if the download was interrupted. - Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.") + Logger.error("Remote and local MD5 hashes do not match, not restoring backup.") self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE + self._job_done.set() + return # Tell Cura to place the backup back in the user data folder. metadata = self._backup.get("metadata", {}) @@ -103,6 +105,8 @@ class RestoreBackupJob(Job): packages_path = os.path.abspath(os.path.join(os.path.abspath( Resources.getConfigStoragePath()), "..", version_str, "packages.json")) if not os.path.exists(packages_path): + Logger.error(f"Can't find path '{packages_path}' to tell what packages should be redownloaded.") + self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE self._job_done.set() return @@ -113,9 +117,13 @@ class RestoreBackupJob(Job): if "to_install" in packages_json and "package_id" in packages_json["to_install"]: to_install.add(packages_json["to_install"]["package_id"]) except IOError as ex: - pass # TODO! (log + message) + Logger.error(f"Couldn't open '{packages_path}' because '{str(ex)}' to get packages to re-install.") + self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE + self._job_done.set() + return if len(to_install) < 1: + Logger.info("No packages to reinstall, early out.") self._job_done.set() return @@ -127,24 +135,26 @@ class RestoreBackupJob(Job): to_install.remove(package_id) try: - with tempfile.NamedTemporaryFile(mode="wb+", suffix=".curapackage") as temp_file: + with NamedTemporaryFile(mode="wb+", suffix=".curapackage") as temp_file: bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) while bytes_read: temp_file.write(bytes_read) bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) - # self._app.processEvents() - # self._progress[package_id]["file_written"] = temp_file.name + CuraApplication.getInstance().processEvents() if not CuraApplication.getInstance().getPackageManager().installPackage(temp_file.name): redownload_errors.append(f"Couldn't install package '{package_id}'.") except IOError as ex: - redownload_errors.append(f"Couldn't read package '{package_id}' because '{ex}'.") + redownload_errors.append(f"Couldn't process package '{package_id}' because '{ex}'.") if len(to_install) < 1: if len(redownload_errors) == 0: + Logger.info("All packages redownloaded!") self._job_done.set() else: - print("|".join(redownload_errors)) # TODO: Message / Log instead. - self._job_done.set() # NOTE: Set job probably not the right call here... (depends on wether or not that in the end closes the app or not...) + msgs = "\n - ".join(redownload_errors) + Logger.error(f"Couldn't re-install at least one package(s) because: {msgs}") + self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE + self._job_done.set() self._package_download_scope = UltimakerCloudScope(CuraApplication.getInstance()) for package_id in to_install: From 6458c17de5b2a92a3179e2747b75c6ea394f138e Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 2 Apr 2025 13:09:21 +0200 Subject: [PATCH 08/21] Save 'pluginless' bakcup correctly. - Fix: Save the tempfile to the archive under the 'original' name (it is a rewrite of) instead of saving it to the archive under it's own name, which skipped the original file completely in a sense (the info was there, but as a tempfile). - Fix: Also make sure the correct folders where ignored, as reinstall paths where the complete path, not the basename. part of CURA-12156 --- cura/Backups/Backup.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 1163169b94..a409c4c689 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -95,7 +95,7 @@ class Backup: # Restore the obfuscated settings self._illuminate(**secrets) - def _fillToInstallsJson(self, file_path: str, reinstall_on_restore: frozenset[str], add_to_archive: Callable[[str], None]) -> Optional[str]: + def _fillToInstallsJson(self, file_path: str, reinstall_on_restore: frozenset[str], add_to_archive: Callable[[str, str], None]) -> Optional[str]: """ Moves all plugin-data (in a config-file) for plugins that could be (re)installed from the Marketplace from 'installed' to 'to_installs' before adding that file to the archive. @@ -104,7 +104,7 @@ class Backup: :param file_path: Absolute path to the packages-file. :param reinstall_on_restore: A set of plugins that _can_ be reinstalled from the Marketplace. - :param add_to_archive: A function/lambda that takes a filename and adds it to the archive. + :param add_to_archive: A function/lambda that takes a filename and adds it to the archive (as the 2nd name). """ with open(file_path, "r") as file: data = json.load(file) @@ -117,7 +117,7 @@ class Backup: tmpfile = tempfile.NamedTemporaryFile(delete=False) with open(tmpfile.name, "w") as outfile: json.dump(data, outfile) - add_to_archive(tmpfile.name) + add_to_archive(tmpfile.name, file_path) return tmpfile.name return None @@ -144,18 +144,17 @@ class Backup: tmpfiles = [] try: archive = ZipFile(buffer, "w", ZIP_DEFLATED) - add_path_to_archive = lambda path: archive.write(path, path[len(root_path) + len(os.sep):]) + add_path_to_archive = lambda path, alt_path: archive.write(path, alt_path[len(root_path) + len(os.sep):]) for root, folders, files in os.walk(root_path, topdown=True): - folders[:] = [f for f in folders if f not in reinstall_instead_paths] for item_name in folders + files: absolute_path = os.path.join(root, item_name) - if ignore_string.search(absolute_path): + if ignore_string.search(absolute_path) or any([absolute_path.startswith(x) for x in reinstall_instead_paths]): continue if item_name == "packages.json": tmpfiles.append( self._fillToInstallsJson(absolute_path, reinstall_instead_ids, add_path_to_archive)) else: - add_path_to_archive(absolute_path) + add_path_to_archive(absolute_path, absolute_path) archive.close() for tmpfile_path in tmpfiles: try: From 1f4f432d49c2649521fde3233de4943bcacfb080 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 2 Apr 2025 13:13:25 +0200 Subject: [PATCH 09/21] Restore Backups: Fix reading which packages need to be reinstalled. Also save the version, so we can get the correct url when redownloading the package (see next commits). part of CURA-12156 --- plugins/CuraDrive/src/RestoreBackupJob.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index e7cd66daf8..4127df7aa6 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -110,12 +110,17 @@ class RestoreBackupJob(Job): self._job_done.set() return - to_install = set() + to_install = {} try: with open(packages_path, "r") as packages_file: packages_json = json.load(packages_file) - if "to_install" in packages_json and "package_id" in packages_json["to_install"]: - to_install.add(packages_json["to_install"]["package_id"]) + if "to_install" in packages_json: + for package_data in packages_json["to_install"].values(): + if "package_info" not in package_data: + continue + package_info = package_data["package_info"] + if "package_id" in package_info and "sdk_version_semver" in package_info: + to_install[package_info["package_id"]] = package_info["sdk_version_semver"] except IOError as ex: Logger.error(f"Couldn't open '{packages_path}' because '{str(ex)}' to get packages to re-install.") self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE @@ -132,7 +137,7 @@ class RestoreBackupJob(Job): def packageDownloadCallback(package_id: str, msg: "QNetworkReply", err: "QNetworkReply.NetworkError" = None) -> None: if err is not None or HttpRequestManager.safeHttpStatus(msg) != 200: redownload_errors.append(err) - to_install.remove(package_id) + del to_install[package_id] try: with NamedTemporaryFile(mode="wb+", suffix=".curapackage") as temp_file: From 43d9e1d522e449c9880428f7d7fd0f8f3a111073 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 2 Apr 2025 13:15:35 +0200 Subject: [PATCH 10/21] Restore Backups: Fix handling the (re)downloaded tempfile. Was opening the tempfile for handling when it was still open for writing. Also the wrong net-reply got used (reply instead of msg). part of CURA-12156 --- plugins/CuraDrive/src/RestoreBackupJob.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index 4127df7aa6..aa48ca5c18 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -140,14 +140,15 @@ class RestoreBackupJob(Job): del to_install[package_id] try: - with NamedTemporaryFile(mode="wb+", suffix=".curapackage") as temp_file: - bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + with NamedTemporaryFile(mode="wb", suffix=".curapackage", delete=False) as temp_file: + bytes_read = msg.read(self.DISK_WRITE_BUFFER_SIZE) while bytes_read: temp_file.write(bytes_read) - bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + bytes_read = msg.read(self.DISK_WRITE_BUFFER_SIZE) CuraApplication.getInstance().processEvents() - if not CuraApplication.getInstance().getPackageManager().installPackage(temp_file.name): - redownload_errors.append(f"Couldn't install package '{package_id}'.") + temp_file.close() + if not CuraApplication.getInstance().getPackageManager().installPackage(temp_file.name): + redownload_errors.append(f"Couldn't install package '{package_id}'.") except IOError as ex: redownload_errors.append(f"Couldn't process package '{package_id}' because '{ex}'.") From 1fb89e0e7d80635e60ce4bf38e2b1853c66d4ac8 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 2 Apr 2025 13:19:01 +0200 Subject: [PATCH 11/21] Restore Backups: Correctly handle requests to redownload plugins. - Fix: Need to fill the package's API version into the (template) URL, instead of using latest. - Fix: Loop was closing over the package ID, which caused the recieving function to always only get the last handled package ID instead of the one it needed to handle. part of CURA-12156 --- plugins/CuraDrive/src/RestoreBackupJob.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index aa48ca5c18..817d819abf 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -22,7 +22,7 @@ from cura.CuraApplication import CuraApplication from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope import cura.UltimakerCloud.UltimakerCloudConstants as UltimakerCloudConstants -PACKAGES_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}/cura/v{CuraSDKVersion}/packages" +PACKAGES_URL_TEMPLATE = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}/cura/v{{0}}/packages/{{1}}/download" class RestoreBackupJob(Job): """Downloads a backup and overwrites local configuration with the backup. @@ -163,13 +163,15 @@ class RestoreBackupJob(Job): self._job_done.set() self._package_download_scope = UltimakerCloudScope(CuraApplication.getInstance()) - for package_id in to_install: - HttpRequestManager.getInstance().get( - f"{PACKAGES_URL}/{package_id}/download", - scope=self._package_download_scope, - callback=lambda msg: packageDownloadCallback(package_id, msg), - error_callback=lambda msg, err: packageDownloadCallback(package_id, msg, err) - ) + for package_id, package_api_version in to_install.items(): + def handlePackageId(package_id: str = package_id): + HttpRequestManager.getInstance().get( + PACKAGES_URL_TEMPLATE.format(package_api_version, package_id), + scope=self._package_download_scope, + callback=lambda msg: packageDownloadCallback(package_id, msg), + error_callback=lambda msg, err: packageDownloadCallback(package_id, msg, err) + ) + handlePackageId(package_id) @staticmethod def _verifyMd5Hash(file_path: str, known_hash: str) -> bool: From 0ec825b1ac4e55e404f319df1a6ef50c4d322837 Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:02:58 +0200 Subject: [PATCH 12/21] New configurations for the S line printers. PP-602 --- resources/definitions/ultimaker_s6.def.json | 53 ++++++++++++++++++ resources/definitions/ultimaker_s8.def.json | 2 +- .../ultimaker_s6_extruder_left.def.json | 31 ++++++++++ .../ultimaker_s6_extruder_right.def.json | 31 ++++++++++ resources/images/UltimakerS6backplate.png | Bin 0 -> 24368 bytes ..._nylon-cf-slide_0.2mm_engineering.inst.cfg | 18 ++++++ ...plus_0.6_petcf_0.2mm_engineering.inst.cfg} | 4 +- .../um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg | 1 - .../um_s8_aa_plus_0.6_abs_0.2mm.inst.cfg | 23 ++++++++ .../um_s8_aa_plus_0.6_abs_0.3mm.inst.cfg | 25 +++++++++ .../um_s8_aa_plus_0.6_petg_0.2mm.inst.cfg | 20 +++++++ .../um_s8_aa_plus_0.6_petg_0.3mm.inst.cfg | 21 +++++++ .../um_s8_aa_plus_0.6_pla_0.2mm.inst.cfg | 20 +++++++ .../um_s8_aa_plus_0.6_pla_0.3mm.inst.cfg | 22 ++++++++ ...um_s8_aa_plus_0.6_tough-pla_0.2mm.inst.cfg | 20 +++++++ ...um_s8_aa_plus_0.6_tough-pla_0.3mm.inst.cfg | 22 ++++++++ ..._cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg | 17 ++++++ ..._cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg | 17 ++++++ .../um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg | 18 ++++++ .../um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg | 20 +++++++ .../variants/ultimaker_s6_aa_plus04.inst.cfg | 17 ++++++ .../variants/ultimaker_s6_bb0.8.inst.cfg | 35 ++++++++++++ resources/variants/ultimaker_s6_bb04.inst.cfg | 19 +++++++ .../variants/ultimaker_s6_cc_plus04.inst.cfg | 17 ++++++ resources/variants/ultimaker_s6_dd04.inst.cfg | 17 ++++++ .../variants/ultimaker_s8_aa_plus06.inst.cfg | 17 ++++++ .../variants/ultimaker_s8_cc_plus06.inst.cfg | 17 ++++++ 27 files changed, 520 insertions(+), 4 deletions(-) create mode 100644 resources/definitions/ultimaker_s6.def.json create mode 100644 resources/extruders/ultimaker_s6_extruder_left.def.json create mode 100644 resources/extruders/ultimaker_s6_extruder_right.def.json create mode 100644 resources/images/UltimakerS6backplate.png create mode 100644 resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg rename resources/intent/ultimaker_s8/{um_s8_aa_plus_0.4_nylon_0.2mm_engineering.inst.cfg => um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg} (86%) create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_abs_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_abs_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_petg_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_petg_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_pla_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_pla_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_tough-pla_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_tough-pla_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg create mode 100644 resources/variants/ultimaker_s6_aa_plus04.inst.cfg create mode 100644 resources/variants/ultimaker_s6_bb0.8.inst.cfg create mode 100644 resources/variants/ultimaker_s6_bb04.inst.cfg create mode 100644 resources/variants/ultimaker_s6_cc_plus04.inst.cfg create mode 100644 resources/variants/ultimaker_s6_dd04.inst.cfg create mode 100644 resources/variants/ultimaker_s8_aa_plus06.inst.cfg create mode 100644 resources/variants/ultimaker_s8_cc_plus06.inst.cfg diff --git a/resources/definitions/ultimaker_s6.def.json b/resources/definitions/ultimaker_s6.def.json new file mode 100644 index 0000000000..ec6c189eda --- /dev/null +++ b/resources/definitions/ultimaker_s6.def.json @@ -0,0 +1,53 @@ +{ + "version": 2, + "name": "UltiMaker S6", + "inherits": "ultimaker_s8", + "metadata": + { + "visible": true, + "author": "UltiMaker", + "manufacturer": "Ultimaker B.V.", + "file_formats": "application/x-ufp;text/x-gcode", + "platform": "ultimaker_s7_platform.obj", + "bom_numbers": [ + 10700 + ], + "firmware_update_info": + { + "check_urls": [ "https://software.ultimaker.com/releases/firmware/5078167/stable/um-update.swu.version" ], + "id": 5078167, + "update_url": "https://ultimaker.com/firmware?utm_source=cura&utm_medium=software&utm_campaign=fw-update" + }, + "first_start_actions": [ "DiscoverUM3Action" ], + "has_machine_quality": true, + "has_materials": true, + "has_variants": true, + "machine_extruder_trains": + { + "0": "ultimaker_s6_extruder_left", + "1": "ultimaker_s6_extruder_right" + }, + "nozzle_offsetting_for_disallowed_areas": false, + "platform_offset": [ + 0, + 0, + 0 + ], + "platform_texture": "UltimakerS6backplate.png", + "preferred_material": "ultimaker_pla_blue", + "preferred_variant_name": "AA+ 0.4", + "quality_definition": "ultimaker_s8", + "supported_actions": [ "DiscoverUM3Action" ], + "supports_material_export": true, + "supports_network_connection": true, + "supports_usb_connection": false, + "variants_name": "Print Core", + "variants_name_has_translation": true, + "weight": -2 + }, + "overrides": + { + "adhesion_type": { "value": "'brim'" }, + "machine_name": { "default_value": "Ultimaker S6" } + } +} \ No newline at end of file diff --git a/resources/definitions/ultimaker_s8.def.json b/resources/definitions/ultimaker_s8.def.json index 80e7986acf..5a4d44fa2f 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -412,7 +412,7 @@ "retraction_hop": { "value": 1 }, "retraction_hop_after_extruder_switch_height": { "value": 2 }, "retraction_hop_enabled": { "value": true }, - "retraction_min_travel": { "value": "5 if support_enable and support_structure=='tree' else line_width * 2" }, + "retraction_min_travel": { "value": "5 if support_enable and support_structure=='tree' else line_width * 2.5" }, "retraction_prime_speed": { "value": 15 }, "skin_edge_support_thickness": { "value": 0 }, "skin_material_flow": { "value": 95 }, diff --git a/resources/extruders/ultimaker_s6_extruder_left.def.json b/resources/extruders/ultimaker_s6_extruder_left.def.json new file mode 100644 index 0000000000..d3991222b2 --- /dev/null +++ b/resources/extruders/ultimaker_s6_extruder_left.def.json @@ -0,0 +1,31 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": + { + "machine": "ultimaker_s6", + "position": "0" + }, + "overrides": + { + "extruder_nr": + { + "default_value": 0, + "maximum_value": "1" + }, + "extruder_prime_pos_x": { "default_value": -3 }, + "extruder_prime_pos_y": { "default_value": 6 }, + "extruder_prime_pos_z": { "default_value": 2 }, + "machine_extruder_end_pos_abs": { "default_value": true }, + "machine_extruder_end_pos_x": { "default_value": 330 }, + "machine_extruder_end_pos_y": { "default_value": 237 }, + "machine_extruder_start_code": { "value": "\"M214 D0 K{material_pressure_advance_factor} R0.04\"" }, + "machine_extruder_start_pos_abs": { "default_value": true }, + "machine_extruder_start_pos_x": { "default_value": 330 }, + "machine_extruder_start_pos_y": { "default_value": 237 }, + "machine_nozzle_head_distance": { "default_value": 2.7 }, + "machine_nozzle_offset_x": { "default_value": 0 }, + "machine_nozzle_offset_y": { "default_value": 0 } + } +} \ No newline at end of file diff --git a/resources/extruders/ultimaker_s6_extruder_right.def.json b/resources/extruders/ultimaker_s6_extruder_right.def.json new file mode 100644 index 0000000000..5c70f36741 --- /dev/null +++ b/resources/extruders/ultimaker_s6_extruder_right.def.json @@ -0,0 +1,31 @@ +{ + "version": 2, + "name": "Extruder 2", + "inherits": "fdmextruder", + "metadata": + { + "machine": "ultimaker_s6", + "position": "1" + }, + "overrides": + { + "extruder_nr": + { + "default_value": 1, + "maximum_value": "1" + }, + "extruder_prime_pos_x": { "default_value": 333 }, + "extruder_prime_pos_y": { "default_value": 6 }, + "extruder_prime_pos_z": { "default_value": 2 }, + "machine_extruder_end_pos_abs": { "default_value": true }, + "machine_extruder_end_pos_x": { "default_value": 330 }, + "machine_extruder_end_pos_y": { "default_value": 219 }, + "machine_extruder_start_code": { "value": "\"M214 D0 K{material_pressure_advance_factor} R0.04\"" }, + "machine_extruder_start_pos_abs": { "default_value": true }, + "machine_extruder_start_pos_x": { "default_value": 330 }, + "machine_extruder_start_pos_y": { "default_value": 219 }, + "machine_nozzle_head_distance": { "default_value": 4.2 }, + "machine_nozzle_offset_x": { "default_value": 22 }, + "machine_nozzle_offset_y": { "default_value": 0 } + } +} \ No newline at end of file diff --git a/resources/images/UltimakerS6backplate.png b/resources/images/UltimakerS6backplate.png new file mode 100644 index 0000000000000000000000000000000000000000..d6e83781ccd922ffe02d4a838c961cfbba98c9e5 GIT binary patch literal 24368 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 80 else 'triangles' +infill_wipe_dist = 0 +layer_height = 0.2 +machine_min_cool_heat_time_window = 15 +machine_nozzle_heat_up_speed = 1.5 +machine_nozzle_id = BB 0.8 +machine_nozzle_size = 0.8 +machine_nozzle_tip_outer_diameter = 2.0 +multiple_mesh_overlap = 0 +prime_tower_enable = False +prime_tower_wipe_enabled = True +raft_surface_layers = 1 +retraction_amount = 4.5 +retraction_hop = 2 +retraction_hop_only_when_collides = True +support_angle = 60 +support_z_distance = 0 +switch_extruder_prime_speed = 15 +switch_extruder_retraction_amount = 12 +top_bottom_thickness = 1 +wall_0_inset = 0 + diff --git a/resources/variants/ultimaker_s6_bb04.inst.cfg b/resources/variants/ultimaker_s6_bb04.inst.cfg new file mode 100644 index 0000000000..756d6fd1d4 --- /dev/null +++ b/resources/variants/ultimaker_s6_bb04.inst.cfg @@ -0,0 +1,19 @@ +[general] +definition = ultimaker_s6 +name = BB 0.4 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_heat_up_speed = 1.5 +machine_nozzle_id = BB 0.4 +machine_nozzle_tip_outer_diameter = 1.0 +retraction_amount = 4.5 +support_bottom_height = =layer_height * 2 +support_interface_enable = True +switch_extruder_retraction_amount = 12 + diff --git a/resources/variants/ultimaker_s6_cc_plus04.inst.cfg b/resources/variants/ultimaker_s6_cc_plus04.inst.cfg new file mode 100644 index 0000000000..61206eb39c --- /dev/null +++ b/resources/variants/ultimaker_s6_cc_plus04.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s6 +name = CC+ 0.4 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = CC+ 0.4 +machine_nozzle_size = 0.4 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + diff --git a/resources/variants/ultimaker_s6_dd04.inst.cfg b/resources/variants/ultimaker_s6_dd04.inst.cfg new file mode 100644 index 0000000000..3125db405e --- /dev/null +++ b/resources/variants/ultimaker_s6_dd04.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s6 +name = DD 0.4 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = DD 0.4 +machine_nozzle_size = 0.4 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + diff --git a/resources/variants/ultimaker_s8_aa_plus06.inst.cfg b/resources/variants/ultimaker_s8_aa_plus06.inst.cfg new file mode 100644 index 0000000000..1eabef191c --- /dev/null +++ b/resources/variants/ultimaker_s8_aa_plus06.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s8 +name = AA+ 0.6 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = AA+ 0.6 +machine_nozzle_size = 0.6 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + diff --git a/resources/variants/ultimaker_s8_cc_plus06.inst.cfg b/resources/variants/ultimaker_s8_cc_plus06.inst.cfg new file mode 100644 index 0000000000..2a1c43873f --- /dev/null +++ b/resources/variants/ultimaker_s8_cc_plus06.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s8 +name = CC+ 0.6 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = CC+ 0.6 +machine_nozzle_size = 0.6 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + From 90848b90e4466f58faa44388320af5e3acc048a4 Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:11:37 +0200 Subject: [PATCH 13/21] Improve PVA support for S8 PP-602 --- .../ultimaker_s8/um_s8_bb0.4_pva_0.15mm.inst.cfg | 10 ++++++++-- .../ultimaker_s8/um_s8_bb0.4_pva_0.1mm.inst.cfg | 10 ++++++++-- .../ultimaker_s8/um_s8_bb0.4_pva_0.2mm.inst.cfg | 10 ++++++++-- .../ultimaker_s8/um_s8_bb0.4_pva_0.3mm.inst.cfg | 10 ++++++++-- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.15mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.15mm.inst.cfg index 9539bd42b1..b2e787724e 100644 --- a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.15mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.15mm.inst.cfg @@ -13,17 +13,23 @@ weight = -1 [values] acceleration_prime_tower = 1500 +acceleration_support = 1500 brim_replaces_support = False build_volume_temperature = =70 if extruders_enabled_count > 1 else 35 cool_fan_enabled = =not (support_enable and (extruder_nr == support_infill_extruder_nr)) default_material_bed_temperature = =0 if extruders_enabled_count > 1 else 60 initial_layer_line_width_factor = 150 +jerk_prime_tower = 4000 +jerk_support = 4000 minimum_support_area = 4 +retraction_amount = 6.5 retraction_count_max = 5 skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width)) -speed_prime_tower = 25 +speed_prime_tower = 50 speed_support = 50 -support_angle = 45 +speed_support_bottom = =2*speed_support_interface/5 +speed_support_interface = 50 +support_bottom_density = 70 support_infill_sparse_thickness = =2 * layer_height support_interface_enable = True support_z_distance = 0 diff --git a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.1mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.1mm.inst.cfg index d5e6084c76..a6d3010f60 100644 --- a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.1mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.1mm.inst.cfg @@ -13,18 +13,24 @@ weight = 0 [values] acceleration_prime_tower = 1500 +acceleration_support = 1500 brim_replaces_support = False build_volume_temperature = =70 if extruders_enabled_count > 1 else 35 cool_fan_enabled = =not (support_enable and (extruder_nr == support_infill_extruder_nr)) default_material_bed_temperature = =0 if extruders_enabled_count > 1 else 60 initial_layer_line_width_factor = 150 +jerk_prime_tower = 4000 +jerk_support = 4000 material_print_temperature = =default_material_print_temperature - 5 minimum_support_area = 4 +retraction_amount = 6.5 retraction_count_max = 5 skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width)) -speed_prime_tower = 25 +speed_prime_tower = 50 speed_support = 50 -support_angle = 45 +speed_support_bottom = =2*speed_support_interface/5 +speed_support_interface = 50 +support_bottom_density = 70 support_infill_sparse_thickness = =2 * layer_height support_interface_enable = True support_z_distance = 0 diff --git a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.2mm.inst.cfg index 3c29ca8186..9994a7c503 100644 --- a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.2mm.inst.cfg @@ -13,18 +13,24 @@ weight = -2 [values] acceleration_prime_tower = 1500 +acceleration_support = 1500 brim_replaces_support = False build_volume_temperature = =70 if extruders_enabled_count > 1 else 35 cool_fan_enabled = =not (support_enable and (extruder_nr == support_infill_extruder_nr)) default_material_bed_temperature = =0 if extruders_enabled_count > 1 else 60 initial_layer_line_width_factor = 150 +jerk_prime_tower = 4000 +jerk_support = 4000 material_print_temperature = =default_material_print_temperature + 5 minimum_support_area = 4 +retraction_amount = 6.5 retraction_count_max = 5 skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width)) -speed_prime_tower = 25 +speed_prime_tower = 50 speed_support = 50 -support_angle = 45 +speed_support_bottom = =2*speed_support_interface/5 +speed_support_interface = 50 +support_bottom_density = 70 support_interface_enable = True support_z_distance = 0 diff --git a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.3mm.inst.cfg index a49cea6817..4c9ca9c2cc 100644 --- a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.3mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.3mm.inst.cfg @@ -13,18 +13,24 @@ weight = -3 [values] acceleration_prime_tower = 1500 +acceleration_support = 1500 brim_replaces_support = False build_volume_temperature = =70 if extruders_enabled_count > 1 else 35 cool_fan_enabled = =not (support_enable and (extruder_nr == support_infill_extruder_nr)) default_material_bed_temperature = =0 if extruders_enabled_count > 1 else 60 initial_layer_line_width_factor = 150 +jerk_prime_tower = 4000 +jerk_support = 4000 material_print_temperature = =default_material_print_temperature - 5 minimum_support_area = 4 +retraction_amount = 6.5 retraction_count_max = 5 skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width)) -speed_prime_tower = 25 +speed_prime_tower = 50 speed_support = 50 -support_angle = 45 +speed_support_bottom = =2*speed_support_interface/5 +speed_support_interface = 50 +support_bottom_density = 70 support_infill_sparse_thickness = 0.3 support_interface_enable = True support_z_distance = 0 From e76e8432743ff8fa4df21f71a4853b181915f791 Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:32:15 +0200 Subject: [PATCH 14/21] PC: Prevent holes in outside wall of PC prints due to air capture during unretracts: - reduce retract length, inner wall before outer wall, inner wall speed equal to outer wall. We also lowered the flow rate to 95% for PC because we noticed that it was over extruding. CPE: CPE is VERY fragile to printer over itself or bumps. Lower all speeds to 40mm/s, goto 'lines' infill type to prevent crossing over lines, 3 walls to prevent issues with infill to reach the outer wall. PP-607 --- .../ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm.inst.cfg | 8 +++++--- .../ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm.inst.cfg index 25a277a06c..bff86d6fa4 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm.inst.cfg @@ -13,8 +13,10 @@ weight = -2 [values] infill_overlap = 20 -infill_pattern = ='zigzag' if infill_sparse_density > 80 else 'gyroid' -speed_print = 100 -speed_wall_0 = =speed_print +infill_pattern = lines +speed_print = 40 +speed_wall = =speed_print +speed_wall_0 = =speed_wall support_interface_enable = True +wall_thickness = =wall_line_width_0 + 2*wall_line_width_x diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg index f6c91375f9..ae64e07f03 100644 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg @@ -14,6 +14,8 @@ weight = -2 [values] cool_min_layer_time = 6 cool_min_layer_time_fan_speed_max = 12 -retraction_amount = 8 +inset_direction = inside_out +material_flow = 95 retraction_prime_speed = 15 +speed_wall_x = =speed_wall_0 From 93d9c084d757a137eb364f84612f8cbe711ed591 Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:44:20 +0200 Subject: [PATCH 15/21] Add new configuration files for AA+ and CC+ 0.6 nozzles PP-602 --- .../variants/ultimaker_s6_aa_plus06.inst.cfg | 17 +++++++++++++++++ .../variants/ultimaker_s6_cc_plus06.inst.cfg | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 resources/variants/ultimaker_s6_aa_plus06.inst.cfg create mode 100644 resources/variants/ultimaker_s6_cc_plus06.inst.cfg diff --git a/resources/variants/ultimaker_s6_aa_plus06.inst.cfg b/resources/variants/ultimaker_s6_aa_plus06.inst.cfg new file mode 100644 index 0000000000..95401be2c3 --- /dev/null +++ b/resources/variants/ultimaker_s6_aa_plus06.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s6 +name = AA+ 0.6 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = AA+ 0.6 +machine_nozzle_size = 0.6 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + diff --git a/resources/variants/ultimaker_s6_cc_plus06.inst.cfg b/resources/variants/ultimaker_s6_cc_plus06.inst.cfg new file mode 100644 index 0000000000..93564bada0 --- /dev/null +++ b/resources/variants/ultimaker_s6_cc_plus06.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s6 +name = CC+ 0.6 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = CC+ 0.6 +machine_nozzle_size = 0.6 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + From 1b80649e213d6d15d67c294eb95f2cbc70cc2b6e Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:32:19 +0200 Subject: [PATCH 16/21] Corrected review comment of Erwan. PP-602 --- resources/definitions/ultimaker_s6.def.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/definitions/ultimaker_s6.def.json b/resources/definitions/ultimaker_s6.def.json index ec6c189eda..bc0e6a0f4e 100644 --- a/resources/definitions/ultimaker_s6.def.json +++ b/resources/definitions/ultimaker_s6.def.json @@ -8,7 +8,7 @@ "author": "UltiMaker", "manufacturer": "Ultimaker B.V.", "file_formats": "application/x-ufp;text/x-gcode", - "platform": "ultimaker_s7_platform.obj", + "platform": "ultimaker_s5_platform.obj", "bom_numbers": [ 10700 ], @@ -30,8 +30,8 @@ "nozzle_offsetting_for_disallowed_areas": false, "platform_offset": [ 0, - 0, - 0 + -30, + -10 ], "platform_texture": "UltimakerS6backplate.png", "preferred_material": "ultimaker_pla_blue", @@ -48,6 +48,6 @@ "overrides": { "adhesion_type": { "value": "'brim'" }, - "machine_name": { "default_value": "Ultimaker S6" } + "machine_name": { "default_value": "UltiMaker S6" } } } \ No newline at end of file From a39033bc1fec46896ad1ec566227317e6373c4bc Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:05:37 +0200 Subject: [PATCH 17/21] Capital M in the Ultimaker name of the S7 and S8 to be consistent with machines names released after the merger. PP-602 --- resources/definitions/ultimaker_s7.def.json | 2 +- resources/definitions/ultimaker_s8.def.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/ultimaker_s7.def.json b/resources/definitions/ultimaker_s7.def.json index 14d9b21168..41120c23df 100644 --- a/resources/definitions/ultimaker_s7.def.json +++ b/resources/definitions/ultimaker_s7.def.json @@ -47,7 +47,7 @@ "overrides": { "default_material_print_temperature": { "maximum_value_warning": "320" }, - "machine_name": { "default_value": "Ultimaker S7" }, + "machine_name": { "default_value": "UltiMaker S7" }, "material_print_temperature_layer_0": { "maximum_value_warning": "320" } } } \ No newline at end of file diff --git a/resources/definitions/ultimaker_s8.def.json b/resources/definitions/ultimaker_s8.def.json index 5a4d44fa2f..8fa2ab8459 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -385,7 +385,7 @@ "unit": "m/s\u00b3", "value": "20000 if machine_gcode_flavor == 'Cheetah' else 100" }, - "machine_name": { "default_value": "Ultimaker S8" }, + "machine_name": { "default_value": "UltiMaker S8" }, "machine_nozzle_cool_down_speed": { "default_value": 1.3 }, "machine_nozzle_heat_up_speed": { "default_value": 0.6 }, "machine_start_gcode": { "default_value": "M213 U0.1 ;undercut 0.1mm" }, From 3990fd5d2af362a1254d7b7e0300104445702cb4 Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:10:38 +0200 Subject: [PATCH 18/21] Remove the BB0.8 for the S6 machine (for now). PP-602 --- .../variants/ultimaker_s6_bb0.8.inst.cfg | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 resources/variants/ultimaker_s6_bb0.8.inst.cfg diff --git a/resources/variants/ultimaker_s6_bb0.8.inst.cfg b/resources/variants/ultimaker_s6_bb0.8.inst.cfg deleted file mode 100644 index e4a97f7e9d..0000000000 --- a/resources/variants/ultimaker_s6_bb0.8.inst.cfg +++ /dev/null @@ -1,35 +0,0 @@ -[general] -definition = ultimaker_s6 -name = BB 0.8 -version = 4 - -[metadata] -hardware_type = nozzle -setting_version = 25 -type = variant - -[values] -brim_width = 3 -cool_fan_speed = 50 -infill_pattern = ='zigzag' if infill_sparse_density > 80 else 'triangles' -infill_wipe_dist = 0 -layer_height = 0.2 -machine_min_cool_heat_time_window = 15 -machine_nozzle_heat_up_speed = 1.5 -machine_nozzle_id = BB 0.8 -machine_nozzle_size = 0.8 -machine_nozzle_tip_outer_diameter = 2.0 -multiple_mesh_overlap = 0 -prime_tower_enable = False -prime_tower_wipe_enabled = True -raft_surface_layers = 1 -retraction_amount = 4.5 -retraction_hop = 2 -retraction_hop_only_when_collides = True -support_angle = 60 -support_z_distance = 0 -switch_extruder_prime_speed = 15 -switch_extruder_retraction_amount = 12 -top_bottom_thickness = 1 -wall_0_inset = 0 - From 14bf34d96a3fbccdc69c4c14fcef4f9486507e5d Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 15 Apr 2025 21:08:33 +0200 Subject: [PATCH 19/21] Adjust code to review comments. - Use delete-on-close instead. - Prevent infinite loops. part of CURA-12156 --- cura/Backups/Backup.py | 2 +- plugins/CuraDrive/src/CreateBackupJob.py | 6 +++--- plugins/CuraDrive/src/RestoreBackupJob.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index a409c4c689..1438ce293a 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -114,7 +114,7 @@ class Backup: data["installed"] = keep_in data["to_install"].update(reinstall) if data is not None: - tmpfile = tempfile.NamedTemporaryFile(delete=False) + tmpfile = tempfile.NamedTemporaryFile(delete_on_close=False) with open(tmpfile.name, "w") as outfile: json.dump(data, outfile) add_to_archive(tmpfile.name, file_path) diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index 67083d2e83..4820f886ab 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -99,10 +99,10 @@ class CreateBackupJob(Job): self._requestUploadSlot(backup_meta_data, len(self._backup_zip)) # Note: One 'process events' call wasn't enough with the changed situation somehow. - active_done_check = False - while not active_done_check: + for _ in range(5000): CuraApplication.getInstance().processEvents() - active_done_check = self._job_done.wait(0.02) + if self._job_done.wait(0.02): + break if self.backup_upload_error_message == "": self._upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading.")) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index 817d819abf..503b39547a 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -56,10 +56,10 @@ class RestoreBackupJob(Job): ) # Note: Just to be sure, use the same structure here as in CreateBackupJob. - active_done_check = False - while not active_done_check: + for _ in range(5000): CuraApplication.getInstance().processEvents() - active_done_check = self._job_done.wait(0.02) + if self._job_done.wait(0.02): + break def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if not HttpRequestManager.replyIndicatesSuccess(reply, error): @@ -71,7 +71,7 @@ class RestoreBackupJob(Job): # We store the file in a temporary path fist to ensure integrity. try: - self._temporary_backup_file = NamedTemporaryFile(delete = False) + self._temporary_backup_file = NamedTemporaryFile(delete_on_close = False) with open(self._temporary_backup_file.name, "wb") as write_backup: app = CuraApplication.getInstance() bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) From f45cbeb5f4a8e250a9e9d5d4532cd636de0ec37d Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 16 Apr 2025 13:18:40 +0200 Subject: [PATCH 20/21] Remove spurious (and maybe erroneous?) empty line. CURA-12502 --- packaging/NSIS/Ultimaker-Cura.nsi.jinja | 1 - 1 file changed, 1 deletion(-) diff --git a/packaging/NSIS/Ultimaker-Cura.nsi.jinja b/packaging/NSIS/Ultimaker-Cura.nsi.jinja index 8c5d48f9dd..53d8777e5f 100644 --- a/packaging/NSIS/Ultimaker-Cura.nsi.jinja +++ b/packaging/NSIS/Ultimaker-Cura.nsi.jinja @@ -105,7 +105,6 @@ WriteUninstaller "$INSTDIR\uninstall.exe" !ifdef REG_START_MENU !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" - !insertmacro MUI_STARTMENU_WRITE_END !endif From 2db896e80fcb7dcef778295e1ba778eaba77a963 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 22 Apr 2025 10:24:06 +0200 Subject: [PATCH 21/21] win/pacakging -- Start menu-macro is only needed for when in folder? Also it fails now since it seems like this macro _expects_ a folder to be set. CURA-12502 --- packaging/NSIS/Ultimaker-Cura.nsi.jinja | 3 --- 1 file changed, 3 deletions(-) diff --git a/packaging/NSIS/Ultimaker-Cura.nsi.jinja b/packaging/NSIS/Ultimaker-Cura.nsi.jinja index 53d8777e5f..9f61e6950c 100644 --- a/packaging/NSIS/Ultimaker-Cura.nsi.jinja +++ b/packaging/NSIS/Ultimaker-Cura.nsi.jinja @@ -61,7 +61,6 @@ InstallDir "$PROGRAMFILES64\${APP_NAME}" !ifdef REG_START_MENU !define MUI_STARTMENUPAGE_NODISABLE -!define MUI_STARTMENUPAGE_DEFAULTFOLDER "UltiMaker Cura" !define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}" !define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}" @@ -103,9 +102,7 @@ SetOutPath "$INSTDIR" WriteUninstaller "$INSTDIR\uninstall.exe" !ifdef REG_START_MENU -!insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" -!insertmacro MUI_STARTMENU_WRITE_END !endif !ifndef REG_START_MENU