Skip to content

Commit 1145adf

Browse files
committed
Merge remote-tracking branch 'origin/main' into CURA-12580_paint-on-support
2 parents 881f40e + e5508f5 commit 1145adf

File tree

116 files changed

+6104
-1920
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+6104
-1920
lines changed

.github/workflows/find-packages.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ on:
1313
default: true
1414
required: false
1515
type: boolean
16+
cura_conan_version:
17+
description: 'Cura Conan Version (optional, overrides discovered packages)'
18+
default: ''
19+
type: string
20+
package_overrides:
21+
description: 'List of specific packages to be used (space-separated, in addition to discovered packages)'
22+
default: ''
23+
type: string
1624
conan_args:
1725
description: 'Conan args'
1826
default: ''
@@ -43,8 +51,8 @@ jobs:
4351
if: ${{ inputs.start_builds == true && needs.find-packages.outputs.discovered_packages != '' }}
4452
uses: ultimaker/cura-workflows/.github/workflows/cura-installers.yml@main
4553
with:
46-
cura_conan_version: ${{ needs.find-packages.outputs.cura_package }}
47-
package_overrides: ${{ needs.find-packages.outputs.package_overrides }}
54+
cura_conan_version: ${{ inputs.cura_conan_version != '' && inputs.cura_conan_version || needs.find-packages.outputs.cura_package }}
55+
package_overrides: ${{ needs.find-packages.outputs.package_overrides }} ${{ inputs.package_overrides }}
4856
conan_args: ${{ inputs.conan_args }}
4957
enterprise: ${{ inputs.enterprise }}
5058
staging: ${{ inputs.staging }}

conandata.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
version: "5.11.0"
1+
version: "5.12.0-alpha.0"
22
commit: "unknown"
33
requirements:
4-
- "cura_resources/5.11.0"
5-
- "uranium/5.11.0"
6-
- "curaengine/5.11.0"
7-
- "cura_binary_data/5.11.0"
8-
- "fdm_materials/5.11.0"
4+
- "cura_resources/5.12.0-alpha.0@ultimaker/testing"
5+
- "uranium/5.12.0-alpha.0@ultimaker/testing"
6+
- "curaengine/5.12.0-alpha.0@ultimaker/testing"
7+
- "cura_binary_data/5.12.0-alpha.0@ultimaker/testing"
8+
- "fdm_materials/5.12.0-alpha.0@ultimaker/testing"
99
- "dulcificum/5.10.0"
1010
- "pysavitar/5.11.0-alpha.0"
1111
- "pynest2d/5.10.0"
1212
requirements_internal:
13-
- "fdm_materials/5.11.0"
14-
- "cura_private_data/5.11.0-alpha.0@internal/testing"
13+
- "fdm_materials/5.12.0-alpha.0@ultimaker/testing"
14+
- "cura_private_data/5.12.0-alpha.0@internal/testing"
1515
requirements_enterprise:
1616
- "native_cad_plugin/2.0.0"
1717
urls:

conanfile.py

Lines changed: 89 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -153,71 +153,110 @@ def _is_repository_url(url):
153153
def _retrieve_pip_license(self, package, sources_url, dependency_description):
154154
# Download the sources to get the license file inside
155155
self.output.info(f"Retrieving license for {package}")
156-
response = requests.get(sources_url)
157-
response.raise_for_status()
158-
159-
with tempfile.TemporaryDirectory() as temp_dir:
160-
sources_path = os.path.join(temp_dir, "sources.tar.gz")
161-
with open(sources_path, 'wb') as sources_file:
162-
sources_file.write(response.content)
163-
164-
with tarfile.open(sources_path, 'r:gz') as sources_archive:
165-
license_file = "LICENSE"
166-
167-
for source_file in sources_archive.getnames():
168-
if Path(source_file).name == license_file:
169-
sources_archive.extract(source_file, temp_dir)
170-
171-
license_file_path = os.path.join(temp_dir, source_file)
172-
with open(license_file_path, 'r', encoding='utf8') as file:
173-
dependency_description["license_full"] = file.read()
156+
try:
157+
response = requests.get(sources_url)
158+
response.raise_for_status()
159+
160+
with tempfile.TemporaryDirectory() as temp_dir:
161+
sources_path = os.path.join(temp_dir, "sources.tar.gz")
162+
with open(sources_path, 'wb') as sources_file:
163+
sources_file.write(response.content)
164+
165+
with tarfile.open(sources_path, 'r:gz') as sources_archive:
166+
license_file = "LICENSE"
167+
168+
for source_file in sources_archive.getnames():
169+
if Path(source_file).name == license_file:
170+
sources_archive.extract(source_file, temp_dir)
171+
172+
license_file_path = os.path.join(temp_dir, source_file)
173+
with open(license_file_path, 'r', encoding='utf8') as file:
174+
dependency_description["license_full"] = file.read()
175+
break
176+
except Exception as e:
177+
self.output.warning(f"Failed to retrieve license for {package} from {sources_url}: {e}")
178+
# Don't fail the build, just continue without the license
174179

175180
def _make_pip_dependency_description(self, package, version, dependencies):
176181
url = ["https://pypi.org/pypi", package]
177182
if version is not None:
178-
url.append(version)
183+
# Strip local version identifiers (everything after '+') for PyPI API compatibility
184+
# e.g., "1.26.1+mkl" becomes "1.26.1"
185+
clean_version = version.split('+')[0] if '+' in version else version
186+
url.append(clean_version)
179187
url.append("json")
180188

181-
data = requests.get("/".join(url)).json()
189+
try:
190+
response = requests.get("/".join(url))
191+
response.raise_for_status()
192+
data = response.json()
193+
except (requests.RequestException, ValueError) as e:
194+
self.output.warning(f"Failed to retrieve PyPI data for {package}: {e}")
195+
# Create minimal dependency description with fallback values
196+
dependencies[package] = {
197+
"summary": f"Package {package}",
198+
"version": version or "unknown",
199+
"license": "unknown"
200+
}
201+
return
202+
203+
# Check if the response has the expected structure
204+
if "info" not in data:
205+
self.output.warning(f"PyPI response for {package} missing 'info' field")
206+
dependencies[package] = {
207+
"summary": f"Package {package}",
208+
"version": version or "unknown",
209+
"license": "unknown"
210+
}
211+
return
182212

213+
info = data["info"]
183214
dependency_description = {
184-
"summary": data["info"]["summary"],
185-
"version": data["info"]["version"],
186-
"license": data["info"]["license"]
215+
"summary": info.get("summary", f"Package {package}"),
216+
"version": version or info.get("version", "unknown"), # Use original version if available
217+
"license": info.get("license", "unknown")
187218
}
188219

189-
for url_data in data["urls"]:
190-
if url_data["packagetype"] == "sdist":
191-
sources_url = url_data["url"]
192-
dependency_description["sources_url"] = sources_url
193-
194-
if not self.options.skip_licenses_download:
195-
self._retrieve_pip_license(package, sources_url, dependency_description)
196-
197-
for source_url, check_source in [("source", False),
198-
("Source", False),
199-
("Source Code", False),
200-
("Repository", False),
201-
("Code", False),
202-
("homepage", True),
203-
("Homepage", True)]:
204-
try:
205-
url = data["info"]["project_urls"][source_url]
206-
if check_source and not self._is_repository_url(url):
207-
# That will not work for ALL open-source projects, but should already get a large majority of them
208-
self.output.warning(f"Source URL for {package} ({url}) doesn't seem to be a supported repository")
209-
continue
210-
dependency_description["sources_url"] = url
211-
break
212-
except KeyError:
213-
pass
220+
# Handle URLs section safely
221+
if "urls" in data:
222+
for url_data in data["urls"]:
223+
if url_data.get("packagetype") == "sdist":
224+
sources_url = url_data.get("url")
225+
if sources_url:
226+
dependency_description["sources_url"] = sources_url
227+
228+
if not self.options.skip_licenses_download:
229+
try:
230+
self._retrieve_pip_license(package, sources_url, dependency_description)
231+
except Exception as e:
232+
self.output.warning(f"Failed to retrieve license for {package}: {e}")
233+
234+
# Handle project URLs safely
235+
if "project_urls" in info:
236+
for source_url, check_source in [("source", False),
237+
("Source", False),
238+
("Source Code", False),
239+
("Repository", False),
240+
("Code", False),
241+
("homepage", True),
242+
("Homepage", True)]:
243+
try:
244+
url = info["project_urls"][source_url]
245+
if check_source and not self._is_repository_url(url):
246+
# That will not work for ALL open-source projects, but should already get a large majority of them
247+
self.output.warning(f"Source URL for {package} ({url}) doesn't seem to be a supported repository")
248+
continue
249+
dependency_description["sources_url"] = url
250+
break
251+
except (KeyError, TypeError):
252+
pass
214253

215254
if dependency_description["license"] is not None and len(dependency_description["license"]) > 32:
216255
# Some packages have their full license in this field
217256
dependency_description["license_full"] = dependency_description["license"]
218-
dependency_description["license"] = data["info"]["name"]
257+
dependency_description["license"] = info.get("name", package)
219258

220-
dependencies[data["info"]["name"]] = dependency_description
259+
dependencies[info.get("name", package)] = dependency_description
221260

222261
@staticmethod
223262
def _get_license_from_repository(sources_url, version, license_file_name = None):

cura/BuildVolume.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ def __init__(self, application: "CuraApplication", parent: Optional[SceneNode] =
116116
self._application.engineCreatedSignal.connect(self._onEngineCreated)
117117

118118
self._has_errors = False
119-
self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
119+
scene = self._application.getController().getScene()
120+
scene.sceneChanged.connect(self._onSceneChanged)
120121

121122
# Objects loaded at the moment. We are connected to the property changed events of these objects.
122123
self._scene_objects = set() # type: Set[SceneNode]
@@ -318,16 +319,6 @@ def updateNodeBoundaryCheck(self):
318319
if node_bounding_box and node_bounding_box.top < 0 and not node.getParent().callDecoration("isGroup"):
319320
node.setOutsideBuildArea(True)
320321
continue
321-
# Mark the node as outside build volume if the set extruder is disabled
322-
extruder_position = node.callDecoration("getActiveExtruderPosition")
323-
try:
324-
if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled and not node.callDecoration("isGroup"):
325-
node.setOutsideBuildArea(True)
326-
continue
327-
except IndexError: # Happens when the extruder list is too short. We're not done building the printer in memory yet.
328-
continue
329-
except TypeError: # Happens when extruder_position is None. This object has no extruder decoration.
330-
continue
331322

332323
node.setOutsideBuildArea(False)
333324

@@ -655,7 +646,7 @@ def _calculateExtraZClearance(self, extruders: List["ContainerStack"]) -> float:
655646
extra_z = retraction_hop
656647
return extra_z
657648

658-
def _onStackChanged(self):
649+
def _onStackChanged(self, *args) -> None:
659650
self._stack_change_timer.start()
660651

661652
def _onStackChangeTimerFinished(self) -> None:

cura/CuraApplication.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,7 @@ def registerObjects(self, engine):
13001300
qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 0, self.getCuraSceneController, "SceneController")
13011301
qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, self.getExtruderManager, "ExtruderManager")
13021302
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, self.getMachineManager, "MachineManager")
1303+
qmlRegisterSingletonType(MachineErrorChecker, "Cura", 1, 0, self.getMachineErrorChecker, "MachineErrorChecker")
13031304
qmlRegisterSingletonType(IntentManager, "Cura", 1, 6, self.getIntentManager, "IntentManager")
13041305
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, self.getSettingInheritanceManager, "SettingInheritanceManager")
13051306
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, self.getSimpleModeSettingsManagerWrapper, "SimpleModeSettingsManager")

cura/CuraPackageManager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from UM.PluginRegistry import PluginRegistry
1111
from cura.CuraApplication import CuraApplication # To find some resource types.
1212
from cura.Settings.GlobalStack import GlobalStack
13+
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
1314

1415
from UM.PackageManager import PackageManager # The class we're extending.
1516
from UM.Resources import Resources # To find storage paths for some resource types.
@@ -123,8 +124,7 @@ def getMachinesUsingPackage(self, package_id: str) -> Tuple[List[Tuple[GlobalSta
123124
"""
124125

125126
ids = self.getPackageContainerIds(package_id)
126-
container_stacks = self._application.getContainerRegistry().findContainerStacks()
127-
global_stacks = [container_stack for container_stack in container_stacks if isinstance(container_stack, GlobalStack)]
127+
global_stacks = CuraContainerRegistry.getInstance().findGlobalStacks()
128128
machine_with_materials = []
129129
machine_with_qualities = []
130130
for container_id in ids:

cura/Machines/ContainerTree.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# Copyright (c) 2019 Ultimaker B.V.
22
# Cura is released under the terms of the LGPLv3 or higher.
3-
43
from UM.Job import Job # For our background task of loading MachineNodes lazily.
54
from UM.JobQueue import JobQueue # For our background task of loading MachineNodes lazily.
65
from UM.Logger import Logger
@@ -10,7 +9,7 @@
109
from cura.Machines.MachineNode import MachineNode
1110
from cura.Settings.GlobalStack import GlobalStack # To listen only to global stacks being added.
1211

13-
from typing import Dict, List, Optional, TYPE_CHECKING
12+
from typing import Dict, List, Optional, Any, Set, TYPE_CHECKING
1413
import time
1514

1615
if TYPE_CHECKING:
@@ -78,9 +77,10 @@ def getCurrentQualityChangesGroups(self) -> List["QualityChangesGroup"]:
7877

7978
def _onStartupFinished(self) -> None:
8079
"""Ran after completely starting up the application."""
81-
82-
currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks.
83-
JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added))
80+
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
81+
currently_added_stacks = CuraContainerRegistry.getInstance().findGlobalStacks()
82+
definitions_ids = set([stack.definition.getId() for stack in currently_added_stacks])
83+
JobQueue.getInstance().add(self._MachineNodeLoadJob(self, definitions_ids))
8484

8585
class _MachineNodeMap:
8686
"""Dictionary-like object that contains the machines.
@@ -153,17 +153,17 @@ class _MachineNodeLoadJob(Job):
153153
faster.
154154
"""
155155

156-
def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]) -> None:
156+
def __init__(self, tree_root: "ContainerTree", definitions_ids: Set[str]) -> None:
157157
"""Creates a new background task.
158158
159159
:param tree_root: The container tree instance. This cannot be obtained through the singleton static
160160
function since the instance may not yet be constructed completely.
161-
:param container_stacks: All of the stacks to pre-load the container trees for. This needs to be provided
162-
from here because the stacks need to be constructed on the main thread because they are QObject.
161+
:param definitions_ids: The IDs of all the definitions to pre-load the container trees for. This needs to be
162+
provided from here because the stacks need to be constructed on the main thread because they are QObject.
163163
"""
164164

165165
self.tree_root = tree_root
166-
self.container_stacks = container_stacks
166+
self.definitions_ids: Set[str] = definitions_ids
167167
super().__init__()
168168

169169
def run(self) -> None:
@@ -172,14 +172,12 @@ def run(self) -> None:
172172
The ``JobQueue`` will schedule this on a different thread.
173173
"""
174174
Logger.log("d", "Started background loading of MachineNodes")
175-
for stack in self.container_stacks: # Load all currently-added containers.
176-
if not isinstance(stack, GlobalStack):
177-
continue
175+
for definition_id in self.definitions_ids: # Load all currently-added containers.
178176
# Allow a thread switch after every container.
179177
# Experimentally, sleep(0) didn't allow switching. sleep(0.1) or sleep(0.2) neither.
180178
# We're in no hurry though. Half a second is fine.
181179
time.sleep(0.5)
182-
definition_id = stack.definition.getId()
180+
183181
if not self.tree_root.machines.is_loaded(definition_id):
184182
_ = self.tree_root.machines[definition_id]
185183
Logger.log("d", "All MachineNode loading completed")

0 commit comments

Comments
 (0)