mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-21 18:02:09 +00:00
Merge branch 'main' into feature/protobuf_config_and_dynamic_ui
This commit is contained in:
258
scripts/assign_vendor_setting_ids.py
Normal file
258
scripts/assign_vendor_setting_ids.py
Normal 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())
|
||||
@@ -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>
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -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
|
||||
|
||||
75
scripts/msix/AppxManifest.xml
Normal file
75
scripts/msix/AppxManifest.xml
Normal 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>
|
||||
BIN
scripts/msix/assets/Square150x150Logo.png
Normal file
BIN
scripts/msix/assets/Square150x150Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
scripts/msix/assets/Square44x44Logo.png
Normal file
BIN
scripts/msix/assets/Square44x44Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
scripts/msix/assets/StoreLogo.png
Normal file
BIN
scripts/msix/assets/StoreLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
74
scripts/msix/build_msix.ps1
Normal file
74
scripts/msix/build_msix.ps1
Normal 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"
|
||||
56
scripts/msix/generate_assets.ps1
Normal file
56
scripts/msix/generate_assets.ps1
Normal 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
|
||||
}
|
||||
@@ -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
16
scripts/retry.sh
Normal 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
|
||||
}
|
||||
@@ -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!.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user