Merge branch 'main' into feature/protobuf_config_and_dynamic_ui

This commit is contained in:
ExPikaPaka
2026-07-03 08:38:15 +02:00
8337 changed files with 283518 additions and 201332 deletions

View File

@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""
Assign deterministic, globally-unique setting_id to OrcaSlicer system profiles.
Policy (see AGENTS.md "Critical Constraints"):
* A preset's setting_id is a pure function of its identity:
setting_id = base62_16( uuid5(NAMESPACE, "<vendor>/<type>/<name>") )
The same value is recomputed on the fly by the C++ app
(Slic3r::generate_preset_setting_id); the two MUST stay byte-identical. The rule
(generate_preset_setting_id, below) is also imported by the validator
(orca_extra_profile_check.py). Uniqueness is therefore automatic: two presets
collide only if they share vendor + type + name, which the validator flags.
* Bambu (BBL) owns the authoritative "G*" id space and is the only reserved vendor:
its ids are never rewritten (preserves backward-compat with Bambu-synced presets).
Every other vendor - including OrcaFilamentLibrary and Custom - follows the
deterministic rule.
* Only instantiated presets (instantiation == "true") carry a setting_id; base /
template profiles do not.
Only setting_id is rewritten. filament_id is deliberately left untouched: it is a
per-material id, shared across a filament's nozzle variants and inherited from base
templates, so it must not be made per-file unique.
Run from anywhere: python3 scripts/assign_vendor_setting_ids.py
The script is idempotent: a second run over an unchanged tree produces no diff.
"""
import json
import os
import re
import sys
import uuid
# Deterministic preset setting_id rule. Imported by the validator
# (orca_extra_profile_check.py) and kept byte-identical to the C++
# Slic3r::generate_preset_setting_id. Dedicated namespace, distinct from the cloud
# namespace (f47ac10b-...) so the two id spaces never coincide; this constant is baked
# into both languages - never change it.
NAMESPACE = uuid.UUID("c1f4d9e2-7a3b-5c8d-9e0f-1a2b3c4d5e6f")
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
ID_LENGTH = 16
def generate_preset_setting_id(vendor, type_name, name):
"""Deterministic 16-char base62 setting_id for a preset.
input = f"{vendor}/{type_name}/{name}"; u = uuid5(NAMESPACE, input);
id = the low ID_LENGTH base62 digits of int(u.bytes, "big"), most-significant first.
"""
u = uuid.uuid5(NAMESPACE, f"{vendor}/{type_name}/{name}")
n = int.from_bytes(u.bytes, "big")
digits = []
for _ in range(ID_LENGTH):
digits.append(ALPHABET[n % 62])
n //= 62
return "".join(reversed(digits))
PROFILES_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "resources", "profiles"))
# Bambu (BBL) is the only reserved vendor: it keeps its authoritative "G*" cloud ids.
RESERVED_VENDORS = {"BBL"}
PROFILE_SUBDIRS = ("filament", "process", "machine")
def iter_profile_files(vendor_dir):
"""Yield (json path, type) under a vendor, in a deterministic order.
type is the subdir name ("filament"/"process"/"machine"), which matches
Preset::get_type_string() on the C++ side.
"""
for sub in PROFILE_SUBDIRS:
base = os.path.join(vendor_dir, sub)
if not os.path.isdir(base):
continue
for root, dirs, files in os.walk(base):
dirs.sort() # deterministic traversal across filesystems
for name in sorted(files):
if name.endswith(".json"):
yield os.path.join(root, name), sub
def read_profile(path):
"""Return (setting_id, instantiation, name) as present (or None)."""
try:
with open(path, "rb") as f:
data = json.loads(f.read())
except (ValueError, OSError):
return None, None, None
if not isinstance(data, dict):
return None, None, None
return data.get("setting_id"), data.get("instantiation"), data.get("name")
def list_vendors():
return sorted(
d for d in os.listdir(PROFILES_DIR)
if os.path.isdir(os.path.join(PROFILES_DIR, d))
)
_JSON_STR = r'"(?:[^"\\]|\\.)*"'
def remove_key_line(text, key):
"""Remove a top-level `"key": "..."` member, preserving formatting.
Handles both the common case (member has a trailing comma) and the member
being the LAST in its object (consume the preceding comma instead, so no
dangling comma is left). Returns (new_text, count).
"""
# Member followed by a comma (not the last in the object).
trailing = re.compile(
r'[ \t]*"' + re.escape(key) + r'"[ \t]*:[ \t]*' + _JSON_STR + r'[ \t]*,[ \t]*\r?\n'
)
new, n = trailing.subn("", text, count=1)
if n:
return new, n
# Member is the last one: drop the preceding comma and the member itself.
leading = re.compile(
r',[ \t]*\r?\n[ \t]*"' + re.escape(key) + r'"[ \t]*:[ \t]*' + _JSON_STR
)
return leading.subn("", text, count=1)
def _remove_key_in_tree(key, should_remove):
"""Remove `key` from files where should_remove(sid, inst, text) is True."""
removed = 0
for vendor in list_vendors():
for path, _type in iter_profile_files(os.path.join(PROFILES_DIR, vendor)):
with open(path, "rb") as f:
text = f.read().decode("utf-8")
sid, inst, _name = read_profile(path)
if not should_remove(sid, inst, text):
continue
new_text, n = remove_key_line(text, key)
if n == 0:
raise RuntimeError(f"Could not locate {key} line to remove: {path}")
json.loads(new_text) # fail loudly if removal broke the JSON
with open(path, "wb") as f:
f.write(new_text.encode("utf-8"))
removed += 1
return removed
def remove_misspelled_settings_id():
"""Delete the misspelled "settings_id" key (extra "s") wherever it appears.
The app never reads that key, so those presets effectively had no setting_id
and get a correct one assigned by the normal pass; here we drop the junk key.
"""
return _remove_key_in_tree(
"settings_id", lambda sid, inst, text: '"settings_id"' in text
)
def strip_base_setting_ids():
"""Remove setting_id from every base profile (instantiation != "true").
Convention: only instantiated, user-selectable presets carry a setting_id;
base/template profiles do not. Applied across all vendors.
"""
return _remove_key_in_tree(
"setting_id", lambda sid, inst, text: bool(sid) and inst != "true"
)
def replace_id_value(text, key, new_value):
"""Replace the first top-level `"key": "..."` value, preserving all formatting."""
pattern = re.compile(r'("' + re.escape(key) + r'"\s*:\s*)"(?:[^"\\]|\\.)*"')
repl = lambda m: m.group(1) + json.dumps(new_value, ensure_ascii=False)
new_text, n = pattern.subn(repl, text, count=1)
return new_text, n
def insert_setting_id(text, new_id):
"""Insert a `"setting_id"` line into a preset that lacks one.
Placed just before `filament_id` (or, failing that, `instantiation`) so it
matches the canonical key order, reusing that anchor line's indentation and
line ending. Only setting_id is added; filament_id is left untouched.
"""
for key in ("filament_id", "instantiation"):
m = re.search(r'^([ \t]*)"' + key + r'"[ \t]*:.*?(\r?\n)', text, re.MULTILINE)
if m:
line = f'{m.group(1)}"setting_id": {json.dumps(new_id, ensure_ascii=False)},{m.group(2)}'
return text[:m.start()] + line + text[m.start():], 1
return text, 0
def rewrite_file(path, new_id, has_setting_id):
"""Set the preset's setting_id to new_id (replacing or inserting as needed).
filament_id is intentionally left untouched. Uses binary IO so the file's
original line endings (LF or CRLF) and exact formatting are preserved
byte-for-byte apart from the changed/added line. The result is re-parsed to
guarantee it is still valid JSON.
"""
with open(path, "rb") as f:
text = f.read().decode("utf-8")
if has_setting_id:
text, n = replace_id_value(text, "setting_id", new_id)
else:
text, n = insert_setting_id(text, new_id)
if n == 0:
raise RuntimeError(f"Could not set setting_id on {path}")
json.loads(text) # fail loudly if the edit broke the JSON
with open(path, "wb") as f:
f.write(text.encode("utf-8"))
return True
def main():
# 0. Drop the misspelled "settings_id" key wherever it appears.
typos = remove_misspelled_settings_id()
# 1. Strip setting_id from base profiles everywhere (only instantiated presets keep one).
stripped = strip_base_setting_ids()
# 2. Assign the deterministic setting_id to every instantiated preset of every
# non-reserved vendor.
changed = added = 0
vendors_touched = []
for vendor in list_vendors():
if vendor in RESERVED_VENDORS:
continue
vendor_changed = 0
for path, type_name in iter_profile_files(os.path.join(PROFILES_DIR, vendor)):
sid, inst, name = read_profile(path)
if inst != "true":
continue
if not name:
raise RuntimeError(f"instantiated preset has no \"name\": {path}")
new_id = generate_preset_setting_id(vendor, type_name, name)
if sid == new_id:
continue # already correct - idempotent
rewrite_file(path, new_id, has_setting_id=sid is not None)
changed += 1
vendor_changed += 1
if sid is None:
added += 1
if vendor_changed:
vendors_touched.append((vendor, vendor_changed))
print(f"Misspelled settings_id removed : {typos}")
print(f"Base setting_ids stripped : {stripped}")
print(f"Reserved vendors : {sorted(RESERVED_VENDORS)}")
print(f"Vendors updated : {len(vendors_touched)}")
for v, n in vendors_touched:
print(f" {v} ({n} files)")
print(f"Files rewritten : {changed} (of which newly assigned: {added})")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -45,7 +45,7 @@
<color type="primary" scheme_preference="dark">#00695C</color>
</branding>
<releases>
<release version="2.4.0-dev" date="2026-05-22" type="development">
<release version="2.5.0-dev" date="2026-06-22" type="development">
<url type="details">https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds</url>
<description>
<p>See the release page for detailed changelog.</p>

View File

@@ -31,7 +31,7 @@ finish-args:
- --filesystem=/mnt
- --filesystem=/run/spnav.sock:ro
# Allow read-only access to OrcaSlicer's legacy config and cache directories (if they exist) for migration purposes.
- --filesystem=~/.var/app/io.github.orcaslicer.OrcaSlicer:ro
- --filesystem=~/.var/app/io.github.softfever.OrcaSlicer:ro
# Allow OrcaSlicer to own and talk to instance-check D-Bus names (InstanceCheck.cpp)
- --talk-name=com.orcaslicer.OrcaSlicer.InstanceCheck.*
- --own-name=com.orcaslicer.OrcaSlicer.InstanceCheck.*

View File

@@ -14,7 +14,6 @@ export REQUIRED_DEV_PACKAGES=(
glew
gst-plugins-good
gstreamer
gstreamermm
gtk3
libmspack
libsecret
@@ -24,7 +23,7 @@ export REQUIRED_DEV_PACKAGES=(
openssl
texinfo
wayland-protocols
webkit2gtk
webkit2gtk-4.1
wget
)
@@ -37,7 +36,7 @@ then
done
if [[ "${#NEEDED_PKGS[*]}" -gt 0 ]]; then
sudo pacman -Syy --noconfirm "${NEEDED_PKGS[@]}"
sudo pacman -Syu --noconfirm "${NEEDED_PKGS[@]}"
fi
echo -e "done\n"
exit 0

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:rescap3="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/3"
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
xmlns:virtualization="http://schemas.microsoft.com/appx/manifest/virtualization/windows10"
IgnorableNamespaces="uap rescap rescap3 desktop6 virtualization">
<Identity Name="@MSIX_IDENTITY_NAME@"
Publisher="@MSIX_PUBLISHER@"
Version="@MSIX_VERSION@"
ProcessorArchitecture="@MSIX_ARCH@" />
<Properties>
<DisplayName>OrcaSlicer</DisplayName>
<PublisherDisplayName>@MSIX_PUBLISHER_DISPLAY_NAME@</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
<!-- Keep config in the real %APPDATA%\OrcaSlicer so it survives uninstall and
is shared with the classic (NSIS/portable) install.
Win10 1903+: coarse switch disables AppData write virtualization entirely.
Win11+: fine-grained exclusion below takes precedence over the coarse switch. -->
<desktop6:FileSystemWriteVirtualization>disabled</desktop6:FileSystemWriteVirtualization>
<virtualization:FileSystemWriteVirtualization>
<virtualization:ExcludedDirectories>
<virtualization:ExcludedDirectory>$(KnownFolder:RoamingAppData)\OrcaSlicer</virtualization:ExcludedDirectory>
</virtualization:ExcludedDirectories>
</virtualization:FileSystemWriteVirtualization>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.18362.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Resources>
<Resource Language="en-us" />
</Resources>
<Applications>
<Application Id="OrcaSlicer" Executable="orca-slicer.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="OrcaSlicer"
Description="Open-source slicer for FDM 3D printers"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png" />
<Extensions>
<uap:Extension Category="windows.fileTypeAssociation">
<uap:FileTypeAssociation Name="orcaslicer-models">
<uap:SupportedFileTypes>
<uap:FileType>.3mf</uap:FileType>
<uap:FileType>.stl</uap:FileType>
<uap:FileType>.step</uap:FileType>
<uap:FileType>.stp</uap:FileType>
<uap:FileType>.gcode</uap:FileType>
<uap:FileType>.drc</uap:FileType>
</uap:SupportedFileTypes>
<rescap3:MigrationProgIds>
<rescap3:MigrationProgId>Orca.Slicer.1</rescap3:MigrationProgId>
</rescap3:MigrationProgIds>
</uap:FileTypeAssociation>
</uap:Extension>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="orcaslicer" />
</uap:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<rescap:Capability Name="unvirtualizedResources" />
</Capabilities>
</Package>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,74 @@
<#
Builds the unsigned MSIX Store package from an existing install tree.
The package is intentionally NOT signed: the Microsoft Store strips and
re-signs uploads with Microsoft's certificate. For local installs use
Developer Mode loose-layout registration instead:
./scripts/msix/build_msix.ps1 -StageOnly
Add-AppxPackage -Register <staging>\AppxManifest.xml
Requires the Windows SDK (makeappx.exe) unless -StageOnly is used.
#>
param(
[string]$InstallDir = "build/OrcaSlicer",
[string]$OutputPath = "build/OrcaSlicer_Windows_MSIX.msix",
[ValidateSet("x64", "arm64")]
[string]$Architecture = "x64",
[string]$StagingDir = "",
[switch]$StageOnly,
[string]$IdentityName = "OrcaSlicer.OrcaSlicer",
[string]$Publisher = "CN=38F7EA55-C73B-4072-B3B2-C8E0EA15BB82",
[string]$PublisherDisplayName = "OrcaSlicer"
)
$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
# MSIX version = MAJOR.MINOR.PATCH.0 from the SoftFever_VERSION semver triplet
# (Store requires the revision field to be 0).
$versionContent = Get-Content (Join-Path $repoRoot 'version.inc') -Raw
if ($versionContent -notmatch 'set\(SoftFever_VERSION "(\d+)\.(\d+)\.(\d+)') {
throw "Could not parse SoftFever_VERSION from version.inc"
}
$msixVersion = "$($Matches[1]).$($Matches[2]).$($Matches[3]).0"
Write-Output "MSIX version: $msixVersion"
if (-not (Test-Path (Join-Path $InstallDir 'orca-slicer.exe'))) {
throw "orca-slicer.exe not found in '$InstallDir' - build the install tree first"
}
if ([string]::IsNullOrEmpty($StagingDir)) {
$StagingDir = Join-Path ([System.IO.Path]::GetTempPath()) 'orca-msix-staging'
}
if (Test-Path $StagingDir) { Remove-Item $StagingDir -Recurse -Force }
New-Item -ItemType Directory -Force $StagingDir | Out-Null
Copy-Item -Path (Join-Path $InstallDir '*') -Destination $StagingDir -Recurse
Copy-Item -Path (Join-Path $PSScriptRoot 'assets') -Destination (Join-Path $StagingDir 'Assets') -Recurse
$manifest = Get-Content (Join-Path $PSScriptRoot 'AppxManifest.xml') -Raw
$manifest = $manifest.Replace('@MSIX_VERSION@', $msixVersion)
$manifest = $manifest.Replace('@MSIX_IDENTITY_NAME@', $IdentityName)
$manifest = $manifest.Replace('@MSIX_PUBLISHER@', $Publisher)
$manifest = $manifest.Replace('@MSIX_PUBLISHER_DISPLAY_NAME@', $PublisherDisplayName)
$manifest = $manifest.Replace('@MSIX_ARCH@', $Architecture)
Set-Content -Path (Join-Path $StagingDir 'AppxManifest.xml') -Value $manifest -Encoding utf8
if ($StageOnly) {
Write-Output "Staged loose layout at: $StagingDir"
return
}
# makeappx is a host tool: x64 runners ship only x64, arm64 runners ship arm64.
# Pick the build host's architecture (not the target $Architecture, which only
# affects the manifest ProcessorArchitecture above).
$hostArch = switch ($env:PROCESSOR_ARCHITECTURE) { 'ARM64' { 'arm64' } 'x86' { 'x86' } default { 'x64' } }
$makeappx = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.*\$hostArch\makeappx.exe" -ErrorAction SilentlyContinue |
Sort-Object { [version]$_.Directory.Parent.Name } -Descending |
Select-Object -First 1 -ExpandProperty FullName
if (-not $makeappx) {
throw "makeappx.exe not found under '${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.*\$hostArch' - install the Windows SDK"
}
Write-Output "Using makeappx: $makeappx"
& $makeappx pack /d $StagingDir /p $OutputPath /o
if ($LASTEXITCODE -ne 0) { throw "makeappx pack failed with exit code $LASTEXITCODE" }
Write-Output "Packed: $OutputPath"

View File

@@ -0,0 +1,56 @@
# Generates the MSIX package logo assets from the master vector logo
# (resources\images\OrcaSlicer_gradient_circle.svg). Each PNG is rendered from
# the SVG at its exact target size (true per-size vector rasterization, not
# downscaled from one bitmap), preserving alpha transparency in the corners
# outside the circle (the manifest uses BackgroundColor="transparent").
#
# Run once locally on Windows (re-run only if the logo changes), then commit
# the PNGs in assets/. CI never runs this script.
#
# Prerequisite: Python 3 with the resvg-py package (pip install resvg-py).
# It bundles the resvg SVG renderer, needed because the master SVG uses
# gradients with alpha-fade stops that System.Drawing cannot rasterize.
param(
[string]$Python = 'python'
)
$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$source = Join-Path $repoRoot 'resources\images\OrcaSlicer_gradient_circle.svg'
$outDir = Join-Path $PSScriptRoot 'assets'
New-Item -ItemType Directory -Force $outDir | Out-Null
$sizes = [ordered]@{
'Square150x150Logo.png' = 150
'Square44x44Logo.png' = 44
'Square44x44Logo.targetsize-44_altform-unplated.png' = 44
'StoreLogo.png' = 50
}
$py = @'
import sys
from pathlib import Path
import resvg_py
svg, out_dir = sys.argv[1], Path(sys.argv[2])
for spec in sys.argv[3:]:
name, px = spec.rsplit('=', 1)
px = int(px)
data = resvg_py.svg_to_bytes(svg_path=svg, width=px, height=px)
(out_dir / name).write_bytes(bytes(data))
print(f'Wrote {name} ({px}x{px})')
'@
$renderScript = Join-Path $env:TEMP 'orca_msix_render.py'
Set-Content -Path $renderScript -Value $py -Encoding utf8
try {
$specs = foreach ($name in $sizes.Keys) { "$name=$($sizes[$name])" }
& $Python $renderScript $source $outDir @specs
if ($LASTEXITCODE -ne 0) {
throw 'resvg render failed. Is resvg-py installed? (pip install resvg-py)'
}
}
finally {
Remove-Item $renderScript -ErrorAction SilentlyContinue
}

View File

@@ -3,6 +3,8 @@ import json
import argparse
from pathlib import Path
from assign_vendor_setting_ids import generate_preset_setting_id
OBSOLETE_KEYS = {
"acceleration", "scale", "rotate", "duplicate", "duplicate_grid",
"bed_size", "print_center", "g0", "wipe_tower_per_color_wipe",
@@ -450,6 +452,101 @@ def check_conflict_keys(profiles_dir, vendor_name):
return error_count, warn_count
# Bambu (BBL) keeps its authoritative "G*" cloud ids, which are NOT produced by the
# deterministic formula, so BBL is exempt from the formula match (Rule 2) only. It is
# still checked for presence, uniqueness, base-no-id and the typo key like every other
# vendor. Every other vendor (incl. OrcaFilamentLibrary and Custom) must also match the
# formula.
SETTING_ID_FORMULA_EXEMPT_VENDORS = {"BBL"}
PROFILE_SUBDIRS = ("filament", "process", "machine")
def check_setting_id_uniqueness(profiles_dir):
"""
Validate setting_id across every vendor (see scripts/assign_vendor_setting_ids.py):
1. Every instantiated preset must HAVE a setting_id. (all vendors)
2. A stored setting_id must equal generate_preset_setting_id(vendor, type, name); a stale
value means the JSON was edited without rerunning assign_vendor_setting_ids.py.
(all vendors EXCEPT the formula-exempt ones, e.g. BBL)
3. Base profiles (instantiation != "true") must not carry a setting_id. (all vendors)
4. setting_id must be globally unique - no two files may share one. (all vendors)
5. No profile may use the misspelled key "settings_id". (all vendors)
Formula-exempt vendors (BBL) keep their authoritative ids, so only Rule 2 is skipped
for them; they are still held to presence, uniqueness, base-no-id and the typo check.
"""
errors = 0
owners = {} # setting_id -> list of relative_path (every vendor)
for vendor_dir in sorted(profiles_dir.iterdir()):
if not vendor_dir.is_dir():
continue
vendor = vendor_dir.name
formula_exempt = vendor in SETTING_ID_FORMULA_EXEMPT_VENDORS
for sub in PROFILE_SUBDIRS:
base = vendor_dir / sub
if not base.is_dir():
continue
for file_path in base.rglob("*.json"):
try:
data = json.loads(file_path.read_bytes())
except (ValueError, OSError):
continue
if not isinstance(data, dict):
continue
rel = file_path.relative_to(profiles_dir)
# Rule 5: catch the misspelled "settings_id" key.
if "settings_id" in data:
errors += 1
print_error(
f'profile {rel} uses the misspelled key "settings_id" '
f'(should be "setting_id"); run assign_vendor_setting_ids.py'
)
sid = data.get("setting_id")
instantiated = data.get("instantiation") == "true"
if not instantiated:
# Rule 3: base/template profiles must not carry a setting_id.
if sid:
errors += 1
print_error(
f'base profile {rel} (instantiation != "true") must not have a '
f'setting_id ("{sid}"); run assign_vendor_setting_ids.py'
)
continue
# Rule 1: every instantiated preset must have a setting_id.
if not sid:
errors += 1
print_error(
f"instantiated preset {rel} is missing a setting_id; "
f"run assign_vendor_setting_ids.py"
)
continue
# Rule 2: the stored id must match the deterministic rule. BBL keeps its
# authoritative G* ids and is exempt from this check only.
if not formula_exempt:
expected = generate_preset_setting_id(vendor, sub, data.get("name", ""))
if sid != expected:
errors += 1
print_error(
f'setting_id "{sid}" in {rel} does not match the expected '
f'"{expected}" for {vendor}/{sub}/{data.get("name", "")}; '
f"run assign_vendor_setting_ids.py"
)
continue
# Rule 4: collect for the global-uniqueness check below.
owners.setdefault(sid, []).append(rel)
# Rule 4: a setting_id shared by two files is an error. For managed vendors this means
# a duplicate vendor/type/name; for formula-exempt vendors (BBL) a copy-pasted id.
for sid, locs in sorted(owners.items()):
if len(locs) < 2:
continue
errors += 1
print_error(
f'setting_id "{sid}" is shared by {len(locs)} files ({sorted(map(str, locs))}); '
f"setting_id must be globally unique"
)
return errors
def main():
parser = argparse.ArgumentParser(
description="Check 3D printer profiles for common issues",
@@ -505,6 +602,10 @@ def main():
continue
run_checks(vendor_dir.name)
# Global (cross-vendor) check: setting_id must be unique and stay in-namespace.
# Runs once over the whole tree regardless of the --vendor filter.
errors_found += check_setting_id_uniqueness(profiles_dir)
# ✨ Output finale in stile "compilatore"
print("\n==================== SUMMARY ====================")
print_info(f"Checked vendors : {checked_vendor_count}")

16
scripts/retry.sh Normal file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Sourceable helper: `retry <cmd...>` runs a command, retrying up to 3 times
# with a 5-minute wait between attempts. Useful for flaky commands such as
# `hdiutil create` intermittently failing with "Resource busy".
retry() {
local attempt=1 max_attempts=3 delay=300
until "$@"; do
if [ "$attempt" -ge "$max_attempts" ]; then
echo "::error::Command failed after $attempt attempts: $*"
return 1
fi
echo "Attempt $attempt failed: $*. Retrying in $((delay / 60)) minutes..."
sleep "$delay"
attempt=$((attempt + 1))
done
}

View File

@@ -23,7 +23,7 @@ if %FULL_MODE%==1 (
call :prepareGettextList "%list_file%" "%filtered_list%" "%missing_list%"
if "!has_sources!"=="1" (
if not exist "%generated_i18n%" mkdir "%generated_i18n%"
.\tools\xgettext.exe --keyword=L --keyword=_L --keyword=_u8L --keyword=L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2 --add-comments=TRN --from-code=UTF-8 --no-location --debug --boost -f "%filtered_list%" -o "%generated_pot%"
.\tools\xgettext.exe --keyword=L --keyword=_L --keyword=_u8L --keyword=L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2 --add-comments=TRN --from-code=UTF-8 --no-location --debug --boost --no-wrap -f "%filtered_list%" -o "%generated_pot%"
if errorlevel 1 (
set "script_exit_code=1"
) else (
@@ -122,7 +122,7 @@ exit /b %errorlevel%
set "lang=%name:OrcaSlicer_=%"
if %FULL_MODE%==1 if exist "%pot_file%" (
set "merged_file=%TEMP%\orca_gettext_merged_%RANDOM%_%RANDOM%.po"
.\tools\msgmerge.exe -N -o "!merged_file!" "%file%" "%pot_file%"
.\tools\msgmerge.exe -N --no-wrap -o "!merged_file!" "%file%" "%pot_file%"
if errorlevel 1 (
if exist "!merged_file!" del "!merged_file!"
echo Error encountered with msgmerge command for language !lang!.

View File

@@ -85,7 +85,7 @@ if $FULL_MODE; then
generated_pot_file="${generated_i18n_dir}/OrcaSlicer.pot"
mkdir -p "$generated_i18n_dir"
xgettext --keyword=L --keyword=_L --keyword=_u8L --keyword=L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2 --add-comments=TRN --from-code=UTF-8 --no-location --debug --boost -f "$filtered_list" -o "$generated_pot_file"
xgettext --keyword=L --keyword=_L --keyword=_u8L --keyword=L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2 --add-comments=TRN --from-code=UTF-8 --no-location --debug --boost --no-wrap -f "$filtered_list" -o "$generated_pot_file"
python3 scripts/HintsToPot.py ./resources "$generated_i18n_dir"
if [ -f "$pot_file" ] && files_equal_ignoring_pot_date "$pot_file" "$generated_pot_file"; then
@@ -108,7 +108,7 @@ do
if [ -f "$dir/OrcaSlicer_${lang}.po" ]; then
if $FULL_MODE && [ -f "$pot_file" ]; then
merged_po=$(mktemp)
if ! msgmerge -N -o "$merged_po" "$dir/OrcaSlicer_${lang}.po" "$pot_file"; then
if ! msgmerge -N --no-wrap -o "$merged_po" "$dir/OrcaSlicer_${lang}.po" "$pot_file"; then
echo "Error encountered with msgmerge command for language ${lang}."
rm -f "$merged_po"
exit 1

View File

@@ -9,4 +9,6 @@ ROOT_DIR="$(dirname "$0")/.."
cd "${ROOT_DIR}" || exit 1
ctest --test-dir build/tests -L "Http|PlaceholderParser" --output-junit "$(pwd)/ctest_results.xml" --output-on-failure -j
# Run the whole suite, excluding tests tagged [NotWorking].
# --no-tests=error fails the job if the filter matches nothing (instead of passing green).
ctest --test-dir build/tests -LE "NotWorking" --no-tests=error --output-junit "$(pwd)/ctest_results.xml" --output-on-failure -j