mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 19:01:21 +00:00
Keep User Filament Presets on Printers Their Parent Listed in 2.4.2 (#16367)
* Keep User Filament Presets on Printers Their Parent Listed in 2.4.2
User presets store only their diff, so they inherit the parent's
compatible_printers. Profile changes since 2.4.2 renamed and narrowed many
system filament presets, so user presets made against them disappear from
printers they were made for and the saved selection silently falls back to
another filament at startup.
orca_profile_tool.py legacy-compat writes, per current system filament
preset, the printers it covered in an older release and no longer lists,
kept only where a preset with the same filament_id still covers the printer
and they share an extruder variant. The loader gives these printers to user
presets that inherit their parent's list, without writing any file; saving
a preset detached, cloned or with its own list drops them.
* Ask Before Migrating User Filament Presets to Their Legacy Printers
Instead of giving user presets their parent's 2.4.2 printers in memory,
detect the user presets that inherit their parent's compatible_printers
and are missing printers from the legacy map, and offer to migrate them
at startup or from Help > Migrate Filament Presets.
Migrating first backs up the user folder to
filament_preset_migration_<datetime>_backup.zip in the configuration
folder, and migrates nothing if that fails. It then writes the printers
into each preset's compatible_printers, patching only that key so the
preset keeps the parent name it was saved with and older releases can
still resolve it. Cloud-synced presets are marked for upload.
No asks again at the next launch; "Don't remind me again" stops the
startup prompt.
* Key the legacy filament preset map by the parent's 2.4.2 name
* Check the legacy filament preset map in CI
* Limit the 2.4.2 filament preset migration to presets that need it
Only presets that still name their parent as 2.4.2 did and have no printer
list of their own are offered, and a preset changed after it was found is
left alone. The startup prompt appears once, after upgrading from 2.4.2 or
earlier, as a standard dialog that keeps "Don't remind me again"; Help >
Migrate Filament Presets is unchanged. Migrating no longer overwrites an
unsaved printer-list edit or marks the project modified.
* Ask on Every Start and Take the User Presets Lock Once When Migrating Filament Presets
The startup prompt no longer depends on the version the previous run
recorded: whoever still has user filament presets that lost printers is
asked, including users already on a 2.5.0 nightly or alpha. Answering No
asks again at the next start; "Don't remind me again" stops it.
Migration takes the user presets lock once, around the backup and every
file write, instead of once per preset. A lock another instance holds is
reported once ("another OrcaSlicer instance is saving presets") with
nothing written, rather than as one failed preset after another once the
lock's cool-down kicks in. Detection records each preset's file and the
list it will get, so the writes need no collection lock while the user
presets lock is held, the order InstanceLock requires; the loaded presets
are updated afterwards.
* Run the Legacy Compatible Printers Map Check in check_profile.sh and .ps1
CI's "Check profiles" job checks resources/info/legacy_compatible_printers.json
against the v2.4.2 profiles, and its failure message points at these scripts
as the local reproduction, so they run the same check: legacy_compat, right
after profile_tool. The v2.4.2 tree comes from git archive of the tag, is
cached in the work dir like the other downloads, re-extracted with --refresh,
and fetched from upstream first when the checkout lacks the tag.
* Back Up Before the User Presets Lock and Merge Migrated Printers Into the Live List
The lock guard is meant for a few file operations, and zipping a large user
folder is not one, so the backup is taken before it and the guard covers the
writes only; each write re-reads its file, so a save that slipped in before
is still respected.
The in-memory update adds the lost printers to the list the preset holds at
that moment rather than replacing it with the one found at detection, and
leaves a pending cloud sync state alone: a download waiting to be saved kept
its newer list and its "save" flag, which "update" would have cancelled.
The lock message no longer blames another instance alone, since the lock is
also refused for a while after a miss and when the lock file cannot be
opened.
---------
Co-authored-by: SoftFever <softfeverever@gmail.com>
This commit is contained in:
@@ -5,11 +5,13 @@
|
||||
.DESCRIPTION
|
||||
The Windows counterpart of scripts/check_profile.sh, and kept deliberately close to it.
|
||||
|
||||
Runs the same five checks, in the same order, with the same validator flags, and with the
|
||||
Runs the same six checks, in the same order, with the same validator flags, and with the
|
||||
same semantics: every check runs even after an earlier one fails (the workflow's
|
||||
continue-on-error), then the script exits non-zero once at the end.
|
||||
|
||||
profile_tool scripts/orca_profile_tool.py check
|
||||
legacy_compat scripts/orca_profile_tool.py legacy-compat --check against
|
||||
the v2.4.2 profiles
|
||||
validate_system validator -p <profiles> -l <level>
|
||||
validate_slice validator -p <profiles> -s -l <level>
|
||||
validate_filament_subtypes validator -p <profiles> -l <level> -f
|
||||
@@ -20,8 +22,9 @@
|
||||
never sees a profile no <vendor>.json indexes, a preset name two files claim, or a file
|
||||
normalize and update-index would still rewrite.
|
||||
|
||||
Everything that has to be downloaded - the profile validator and the custom-preset fixture
|
||||
archives - lands under a per-user cache directory (%LOCALAPPDATA%\orca-profile-check) and
|
||||
Everything that has to be downloaded - the profile validator, the custom-preset fixture
|
||||
archives and the v2.4.2 profile tree - lands under a per-user cache directory
|
||||
(%LOCALAPPDATA%\orca-profile-check) and
|
||||
is reused on the next run. Being outside the checkout, that directory is shared by every
|
||||
worktree on the machine. It also holds one log per check plus a copy of the comment CI would
|
||||
post on the PR.
|
||||
@@ -48,7 +51,8 @@
|
||||
check it cannot narrow is validate_slice for a vendor that ships no printers; the summary
|
||||
reports that one as skipped, and naming it explicitly still runs it. profile_tool keeps its
|
||||
two cross-vendor checks (setting_id and filament_id) tree-wide, so a scoped run can still
|
||||
fail on another vendor's files.
|
||||
fail on another vendor's files, and legacy_compat is tree-wide too: the map it checks covers
|
||||
every vendor.
|
||||
|
||||
.PARAMETER Validator
|
||||
OrcaSlicer_profile_validator.exe to use; also $env:ORCA_PROFILE_VALIDATOR. Default: the local
|
||||
@@ -116,7 +120,7 @@ $HostArch = switch ($HostArch) {
|
||||
default { 'x86' }
|
||||
}
|
||||
|
||||
$AllChecks = @('profile_tool', 'validate_system', 'validate_slice', 'validate_filament_subtypes', 'validate_custom')
|
||||
$AllChecks = @('profile_tool', 'legacy_compat', 'validate_system', 'validate_slice', 'validate_filament_subtypes', 'validate_custom')
|
||||
|
||||
$script:LogWriter = $null
|
||||
$script:Python = ''
|
||||
@@ -439,6 +443,43 @@ function Expand-VendorPresets([string] $Zip, [string] $Tree, [string] $Prefix) {
|
||||
return $kept
|
||||
}
|
||||
|
||||
# The profile tree of the release resources\info\legacy_compatible_printers.json is generated
|
||||
# against. The tag is immutable, so the extracted tree is cached in the work dir and -Refresh
|
||||
# re-extracts it. A checkout without the tag (a shallow clone, a fork that never fetched tags)
|
||||
# gets it from upstream, as CI does. git archive writes a file rather than a pipe, which
|
||||
# PowerShell would re-encode, and tar.exe ships with Windows 10 and later.
|
||||
$LegacyBaseTag = 'v2.4.2'
|
||||
$LegacyBaseUrl = 'https://github.com/OrcaSlicer/OrcaSlicer.git'
|
||||
|
||||
function Get-LegacyBaseProfiles {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$base = Join-Path (Join-Path $WorkDir 'legacy-base') $LegacyBaseTag
|
||||
$profiles = Join-Path (Join-Path $base 'resources') 'profiles'
|
||||
if ($Refresh -or -not (Test-Path -LiteralPath $profiles -PathType Container)) {
|
||||
& git -C $RepoRoot rev-parse -q --verify "refs/tags/$LegacyBaseTag^{commit}" 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "fetching tag $LegacyBaseTag from $LegacyBaseUrl ..."
|
||||
& git -C $RepoRoot fetch --no-tags $LegacyBaseUrl "refs/tags/${LegacyBaseTag}:refs/tags/${LegacyBaseTag}"
|
||||
if ($LASTEXITCODE -ne 0) { throw "cannot fetch tag $LegacyBaseTag" }
|
||||
}
|
||||
# Extracted beside its final place and moved there whole, so an interrupted extraction is
|
||||
# never mistaken for the tree on the next run.
|
||||
$partial = "$base.partial"
|
||||
foreach ($dir in $base, $partial) {
|
||||
if (Test-Path -LiteralPath $dir) { Remove-Item -LiteralPath $dir -Recurse -Force }
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $partial | Out-Null
|
||||
$archive = Join-Path $partial 'profiles.tar'
|
||||
& git -C $RepoRoot archive -o $archive $LegacyBaseTag resources/profiles
|
||||
if ($LASTEXITCODE -ne 0) { throw "cannot archive resources/profiles of $LegacyBaseTag" }
|
||||
& tar -xf $archive -C $partial
|
||||
if ($LASTEXITCODE -ne 0) { throw "cannot extract $archive" }
|
||||
Remove-Item -LiteralPath $archive -Force
|
||||
Move-Item -LiteralPath $partial -Destination $base
|
||||
}
|
||||
return $profiles
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------- checks
|
||||
|
||||
$CheckBodies = @{
|
||||
@@ -447,6 +488,14 @@ $CheckBodies = @{
|
||||
Invoke-Tool -Exe (Resolve-Python) -Arguments (@((Join-Path $RepoRoot 'scripts\orca_profile_tool.py'), 'check', '--profiles', $ProfilesDir) + $VendorPyArgs)
|
||||
}
|
||||
|
||||
# The map says which printers a user filament preset made against 2.4.2 can be migrated
|
||||
# back to, so a profile change that renames a filament preset or narrows its
|
||||
# compatible_printers has to regenerate it (the CI comment gives the command).
|
||||
legacy_compat = {
|
||||
$base = Get-LegacyBaseProfiles
|
||||
Invoke-Tool -Exe (Resolve-Python) -Arguments @((Join-Path $RepoRoot 'scripts\orca_profile_tool.py'), 'legacy-compat', '--profiles', $ProfilesDir, '--base', $base, '--check')
|
||||
}
|
||||
|
||||
validate_system = {
|
||||
Invoke-Tool -Exe $Validator -Arguments (@('-p', $ProfilesDir) + $VendorArgs + @('-l', "$LogLevel"))
|
||||
}
|
||||
@@ -581,6 +630,7 @@ $CheckBodies = @{
|
||||
# Heading CI puts above this check's log in the PR comment.
|
||||
$CommentHeadings = @{
|
||||
profile_tool = '### Profile Check Failed (orca_profile_tool.py)'
|
||||
legacy_compat = '### Legacy Compatible Printers Map Out of Date'
|
||||
validate_system = '### System Profile Validation Failed'
|
||||
validate_slice = '### Slice Validation Failed (custom g-code expansion)'
|
||||
validate_filament_subtypes = '### Filament Subtype Validation Failed'
|
||||
@@ -627,7 +677,7 @@ try {
|
||||
[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
|
||||
Push-UserPresets
|
||||
|
||||
if ($Checks | Where-Object { $_ -ne 'profile_tool' }) { $Validator = Resolve-Validator }
|
||||
if ($Checks | Where-Object { $_ -ne 'profile_tool' -and $_ -ne 'legacy_compat' }) { $Validator = Resolve-Validator }
|
||||
|
||||
# An empty printer set is a failure to the sweep, so validate_slice is recorded as skipped
|
||||
# rather than run for a vendor that ships no printers (the filament-only OrcaFilamentLibrary);
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
#
|
||||
# Local twin of the "Check profiles" CI job (.github/workflows/check_profiles.yml).
|
||||
#
|
||||
# Runs the same five checks, in the same order, with the same validator flags, and with the
|
||||
# Runs the same six checks, in the same order, with the same validator flags, and with the
|
||||
# same semantics: every check runs even after an earlier one fails (the workflow's
|
||||
# continue-on-error), then the script exits non-zero once at the end.
|
||||
#
|
||||
# Everything that has to be downloaded - the profile validator and the custom-preset fixture
|
||||
# archives - lands under a per-user cache directory and is reused on the next run. Being outside
|
||||
# Everything that has to be downloaded - the profile validator, the custom-preset fixture
|
||||
# archives and the v2.4.2 profile tree - lands under a per-user cache directory and is reused on
|
||||
# the next run. Being outside
|
||||
# the checkout, that directory is shared by every worktree on the machine. It also holds one log
|
||||
# per check plus a copy of the comment CI would post on the PR.
|
||||
#
|
||||
@@ -58,7 +59,7 @@ LOG_LEVEL=2
|
||||
PREFER_DOWNLOAD=0
|
||||
REFRESH=0
|
||||
|
||||
ALL_CHECKS=(profile_tool validate_system validate_slice validate_filament_subtypes validate_custom)
|
||||
ALL_CHECKS=(profile_tool legacy_compat validate_system validate_slice validate_filament_subtypes validate_custom)
|
||||
CHECKS=()
|
||||
# "<check><TAB>pass|fail" per check that ran, plus "<check><TAB>skip<TAB>why" for one a vendor
|
||||
# scope left out; a string rather than an array because bash 3.2 (still the /bin/bash on macOS)
|
||||
@@ -73,6 +74,8 @@ Usage: scripts/check_profile.sh [OPTION]... [CHECK]...
|
||||
|
||||
Checks (default: all, in this order):
|
||||
profile_tool scripts/orca_profile_tool.py check
|
||||
legacy_compat scripts/orca_profile_tool.py legacy-compat --check against the
|
||||
v2.4.2 profiles
|
||||
validate_system validator -p <profiles> -l <level>
|
||||
validate_slice validator -p <profiles> -s -l <level>
|
||||
validate_filament_subtypes validator -p <profiles> -l <level> -f
|
||||
@@ -100,7 +103,8 @@ Note: --vendor narrows validate_custom too, by keeping only that vendor's preset
|
||||
fixture tree. The one check it cannot narrow is validate_slice for a vendor that ships no
|
||||
printers; the summary reports that one as skipped, and naming it explicitly still runs it.
|
||||
profile_tool keeps its two cross-vendor checks (setting_id and filament_id) tree-wide, so a
|
||||
scoped run can still fail on another vendor's files.
|
||||
scoped run can still fail on another vendor's files, and legacy_compat is tree-wide too: the map
|
||||
it checks covers every vendor.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -379,6 +383,44 @@ check_profile_tool() {
|
||||
python3 "${REPO_ROOT}/scripts/orca_profile_tool.py" check --profiles "${PROFILES_DIR}" --vendor "${VENDOR}"
|
||||
}
|
||||
|
||||
# The profile tree of the release resources/info/legacy_compatible_printers.json is generated
|
||||
# against. The tag is immutable, so the extracted tree is cached in the work dir and --refresh
|
||||
# re-extracts it. A checkout without the tag (a shallow clone, a fork that never fetched tags)
|
||||
# gets it from upstream, as CI does.
|
||||
LEGACY_BASE_TAG="v2.4.2"
|
||||
LEGACY_BASE_URL="https://github.com/OrcaSlicer/OrcaSlicer.git"
|
||||
|
||||
legacy_base_profiles() {
|
||||
local base="${WORK_DIR}/legacy-base/${LEGACY_BASE_TAG}"
|
||||
if [ "${REFRESH}" -eq 1 ] || [ ! -d "${base}/resources/profiles" ]; then
|
||||
if ! git -C "${REPO_ROOT}" rev-parse -q --verify "refs/tags/${LEGACY_BASE_TAG}^{commit}" >/dev/null; then
|
||||
msg "fetching tag ${LEGACY_BASE_TAG} from ${LEGACY_BASE_URL} ..."
|
||||
git -C "${REPO_ROOT}" fetch --no-tags "${LEGACY_BASE_URL}" \
|
||||
"refs/tags/${LEGACY_BASE_TAG}:refs/tags/${LEGACY_BASE_TAG}" || return 1
|
||||
fi
|
||||
# Extracted beside its final place and moved there whole, so an interrupted extraction is
|
||||
# never mistaken for the tree on the next run.
|
||||
local partial="${base}.partial"
|
||||
rm -rf "${base}" "${partial}"
|
||||
mkdir -p "${partial}" || return 1
|
||||
if ! git -C "${REPO_ROOT}" archive "${LEGACY_BASE_TAG}" resources/profiles | tar -x -C "${partial}"; then
|
||||
rm -rf "${partial}"
|
||||
return 1
|
||||
fi
|
||||
mv "${partial}" "${base}" || return 1
|
||||
fi
|
||||
printf '%s\n' "${base}/resources/profiles"
|
||||
}
|
||||
|
||||
# The map says which printers a user filament preset made against 2.4.2 can be migrated back
|
||||
# to, so a profile change that renames a filament preset or narrows its compatible_printers has
|
||||
# to regenerate it (the CI comment gives the command).
|
||||
check_legacy_compat() {
|
||||
local base
|
||||
base="$(legacy_base_profiles)" || return 1
|
||||
python3 "${REPO_ROOT}/scripts/orca_profile_tool.py" legacy-compat --profiles "${PROFILES_DIR}" --base "${base}" --check
|
||||
}
|
||||
|
||||
check_validate_system() {
|
||||
"${VALIDATOR}" -p "${PROFILES_DIR}" -v "${VENDOR}" -l "${LOG_LEVEL}"
|
||||
}
|
||||
@@ -563,6 +605,7 @@ EOF
|
||||
comment_heading() {
|
||||
case "$1" in
|
||||
profile_tool) echo "### Profile Check Failed (orca_profile_tool.py)" ;;
|
||||
legacy_compat) echo "### Legacy Compatible Printers Map Out of Date" ;;
|
||||
validate_system) echo "### System Profile Validation Failed" ;;
|
||||
validate_slice) echo "### Slice Validation Failed (custom g-code expansion)" ;;
|
||||
validate_filament_subtypes) echo "### Filament Subtype Validation Failed" ;;
|
||||
|
||||
@@ -11,6 +11,7 @@ commands:
|
||||
normalize rewrite profile files into their canonical shape
|
||||
trim delete profile files no <vendor>.json list references
|
||||
update-index regenerate the *_list sections of <vendor>.json
|
||||
legacy-compat write the printers user presets can be migrated back to
|
||||
|
||||
options shared by several commands:
|
||||
--vendor VENDOR act on one vendor bundle only; repeatable, empty means all
|
||||
@@ -24,6 +25,9 @@ After adding, renaming or deleting profile files, run:
|
||||
normalize -> update-index -> generate-id -> check
|
||||
normalize supplies missing types; update-index registers presets before id
|
||||
generation.
|
||||
After renaming filament presets or narrowing their compatible_printers, also
|
||||
rerun legacy-compat --base <2.4.2 profiles> so user presets made against 2.4.2
|
||||
can be migrated back to their printers; CI runs it with --check.
|
||||
Use trim only for deliberate cleanup, previewed with --dry-run: it judges against
|
||||
the current index and can delete newly added, unindexed presets.
|
||||
|
||||
@@ -3072,6 +3076,139 @@ def fix_variant_arrays(profiles_dir=PROFILES_DIR, vendors=None, dry_run=False, s
|
||||
return len(changed), errors
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# legacy-compat
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
LEGACY_COMPAT_PATH = os.path.normpath(
|
||||
os.path.join(SCRIPTS_DIR, "..", "resources", "info", "legacy_compatible_printers.json"))
|
||||
|
||||
|
||||
def _renamed_from(data):
|
||||
"""A preset's renamed_from names. Profiles write one ';'-separated string, as the
|
||||
loader's unescape_strings_cstyle reads it; none of them quote or escape a name."""
|
||||
value = data.get("renamed_from") or []
|
||||
return [n for v in _as_list(value) for n in v.split(";") if n]
|
||||
|
||||
|
||||
def _flatten_presets(profiles_dir):
|
||||
"""Every bundle's composed filament and printer presets, for legacy-compat.
|
||||
|
||||
Returns (filaments, printers): filaments maps (vendor, name) to a dict with the
|
||||
resolved compatible_printers, filament_id, extruder variants, renamed_from and
|
||||
whether it is instantiated; printers maps each instantiated printer name to a
|
||||
dict with its renamed_from and extruder variants. instantiation is read from the
|
||||
preset's own file, as the loader requires every file to state it.
|
||||
"""
|
||||
def instantiated(data):
|
||||
return str(data.get("instantiation", "")).lower() == "true"
|
||||
|
||||
ofl = load_vendor_configs(profiles_dir, OFL)["filament"]
|
||||
filaments, printers = {}, {}
|
||||
for vendor in list_vendor_names(profiles_dir):
|
||||
configs = load_vendor_configs(profiles_dir, vendor)
|
||||
cache = {}
|
||||
for name, (_rel, data) in configs["filament"].items():
|
||||
config = compose_config(name, configs["filament"], ofl, cache, vendor == OFL)
|
||||
filaments[(vendor, name)] = {
|
||||
"compatible_printers": set(_as_list(config.get("compatible_printers", ([],))[0])),
|
||||
"filament_id": config.get("filament_id", (None,))[0],
|
||||
"variants": set(_as_list(config.get("filament_extruder_variant", ([],))[0])),
|
||||
"instantiation": instantiated(data),
|
||||
"renamed_from": _renamed_from(data),
|
||||
}
|
||||
cache = {}
|
||||
for name, (_rel, data) in configs["machine"].items():
|
||||
if instantiated(data):
|
||||
config = compose_config(name, configs["machine"], {}, cache)
|
||||
printers[name] = {
|
||||
"renamed_from": _renamed_from(data),
|
||||
"variants": set(_as_list(config.get("printer_extruder_variant", ([],))[0])),
|
||||
}
|
||||
return filaments, printers
|
||||
|
||||
|
||||
def legacy_compatible_printers(base_dir, profiles_dir=PROFILES_DIR):
|
||||
"""Printers each filament preset of the base release covered but the current
|
||||
preset it loads as no longer lists, kept only where a current preset with
|
||||
the same filament_id still covers that printer.
|
||||
|
||||
User presets store only their diff, so they inherit their parent's
|
||||
compatible_printers; when a profile change narrows that list, a user preset
|
||||
made against the base release disappears from printers it was used on. The
|
||||
app offers to write these printers into such user presets. Requiring a same-id
|
||||
successor leaves out printers a filament was deliberately taken off, and
|
||||
requiring a shared extruder variant keeps a printer from picking up another
|
||||
variant's values.
|
||||
|
||||
Keyed by the base release's preset name, which is what such a user preset's
|
||||
inherits still says. One current preset can stand in for several old ones
|
||||
through renamed_from, each with its own old list, so the key tells them
|
||||
apart. An old name resolves as the loader resolves an inherits: to the
|
||||
current selectable preset of that name, else to one listing it in
|
||||
renamed_from. A preset that is not instantiated never enters the loader's
|
||||
collection, so it does not stop the renamed_from lookup. Printer names are
|
||||
translated through the printers' renamed_from. Returns
|
||||
{old preset name: sorted printer names}.
|
||||
"""
|
||||
old_filaments, _ = _flatten_presets(base_dir)
|
||||
new_filaments, new_printers = _flatten_presets(profiles_dir)
|
||||
renamed_printer = {old: new for new, rec in new_printers.items() for old in rec["renamed_from"]}
|
||||
|
||||
by_name, by_old_name = {}, {}
|
||||
for (vendor, name), rec in new_filaments.items():
|
||||
if rec["instantiation"]:
|
||||
by_name.setdefault(name, []).append((vendor, rec))
|
||||
for old_name in rec["renamed_from"]:
|
||||
by_old_name.setdefault(old_name, []).append((vendor, rec))
|
||||
|
||||
covering = {} # filament_id -> printers some current instantiated preset covers
|
||||
for rec in new_filaments.values():
|
||||
if rec["instantiation"] and rec["filament_id"]:
|
||||
covering.setdefault(rec["filament_id"], set()).update(rec["compatible_printers"])
|
||||
|
||||
def shares_variant(filament, printer):
|
||||
variants = new_printers[printer]["variants"]
|
||||
return not filament["variants"] or not variants or bool(filament["variants"] & variants)
|
||||
|
||||
result = {}
|
||||
for (vendor, name), old in sorted(old_filaments.items()):
|
||||
# Only a selectable preset can be a user preset's parent.
|
||||
if not old["instantiation"]:
|
||||
continue
|
||||
candidates = by_name.get(name) or by_old_name.get(name, [])
|
||||
same_vendor = [r for v, r in candidates if v == vendor]
|
||||
rec = (same_vendor or [r for _, r in candidates] or [None])[0]
|
||||
# An empty list means every printer, so nothing can have been lost.
|
||||
if rec is None or not rec["compatible_printers"]:
|
||||
continue
|
||||
lost = {renamed_printer.get(p, p) for p in old["compatible_printers"]} - rec["compatible_printers"]
|
||||
lost &= covering.get(rec["filament_id"], set()) & set(new_printers)
|
||||
lost = {p for p in lost if shares_variant(rec, p)}
|
||||
if lost:
|
||||
result.setdefault(name, set()).update(lost)
|
||||
return {name: sorted(printers) for name, printers in result.items()}
|
||||
|
||||
|
||||
def run_legacy_compat(base_dir, profiles_dir, output, check):
|
||||
presets = legacy_compatible_printers(base_dir, profiles_dir)
|
||||
if check:
|
||||
try:
|
||||
with open(output, encoding="utf-8") as f:
|
||||
current = json.load(f).get("presets")
|
||||
except (OSError, ValueError, AttributeError):
|
||||
current = None
|
||||
if current != presets:
|
||||
print_error(f"{output} is out of date; rerun legacy-compat --base {base_dir}")
|
||||
return 1
|
||||
print_success(f"{output} is up to date")
|
||||
return 0
|
||||
with open(output, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(json.dumps({"presets": presets}, indent=4, sort_keys=True, ensure_ascii=False) + "\n")
|
||||
print_info(f"wrote {len(presets)} presets / {sum(map(len, presets.values()))} printers to {output}")
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -3093,11 +3230,17 @@ examples:
|
||||
preview resizing the variant arrays check rejects in that bundle
|
||||
orca_profile_tool.py check --strict --vendor BBL
|
||||
also hold every preset's inherited variant arrays to its own width
|
||||
orca_profile_tool.py legacy-compat --base /tmp/v2.4.2/resources/profiles
|
||||
rewrite resources/info/legacy_compatible_printers.json against 2.4.2's
|
||||
profiles (git archive v2.4.2 resources/profiles | tar -x -C /tmp/v2.4.2)
|
||||
|
||||
after adding, renaming or deleting profile files, run in this order:
|
||||
normalize -> update-index -> generate-id -> check
|
||||
normalize supplies missing types; update-index registers presets before id
|
||||
generation.
|
||||
After renaming filament presets or narrowing their compatible_printers, also
|
||||
rerun legacy-compat --base <2.4.2 profiles> so user presets made against 2.4.2
|
||||
can be migrated back to their printers; CI runs it with --check.
|
||||
Use trim only for deliberate cleanup, previewed with --dry-run: it judges against
|
||||
the current index and can delete newly added, unindexed presets.
|
||||
"""
|
||||
@@ -3227,6 +3370,23 @@ def build_parser():
|
||||
"Use trim only for deliberate unindexed-file cleanup, previewed with\n"
|
||||
"--dry-run; it can also delete newly authored presets.")
|
||||
|
||||
legacy_cmd = add(
|
||||
"legacy-compat", [profiles_opt],
|
||||
"write the printers user presets can be migrated back to",
|
||||
"Compare the tree with an older release's profiles and write, per filament\n"
|
||||
"preset of that release, the printers it covered then that the preset it now\n"
|
||||
"loads as no longer lists, where a preset with the same filament_id still\n"
|
||||
"covers them. The app offers to write these into user presets that inherit\n"
|
||||
"that preset by its old name and do not set compatible_printers, so a\n"
|
||||
"narrowed list does not hide them on printers they were made for.")
|
||||
legacy_cmd.add_argument("--base", required=True, metavar="DIR",
|
||||
help="the older release's resources/profiles directory")
|
||||
legacy_cmd.add_argument("--output", default=LEGACY_COMPAT_PATH, metavar="FILE",
|
||||
help="map to write (default: resources/info/"
|
||||
"legacy_compatible_printers.json)")
|
||||
legacy_cmd.add_argument("--check", action="store_true",
|
||||
help="fail if the map differs from what --base implies; write nothing")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
@@ -3260,6 +3420,9 @@ def main(argv=None):
|
||||
return run_generate_id(profiles_dir, vendors, args.filament_id, args.setting_id,
|
||||
args.dry_run)
|
||||
|
||||
if args.command == "legacy-compat":
|
||||
return run_legacy_compat(args.base, profiles_dir, args.output, args.check)
|
||||
|
||||
if args.command == "fix-variant":
|
||||
_changed, errors = fix_variant_arrays(profiles_dir, vendors, dry_run=args.dry_run,
|
||||
strict=args.strict)
|
||||
|
||||
@@ -1258,6 +1258,118 @@ class TestVariantNames(TreeCase):
|
||||
self.assertIn('holds the legacy variant "Direct Drive Normal"', out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# legacy-compat
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestLegacyCompat(unittest.TestCase):
|
||||
"""A base release and the current tree, each with one vendor."""
|
||||
|
||||
def setUp(self):
|
||||
self.base, self.cur = Tree(), Tree()
|
||||
self.addCleanup(self.base.cleanup)
|
||||
self.addCleanup(self.cur.cleanup)
|
||||
|
||||
@staticmethod
|
||||
def printer(tree, name, **extra):
|
||||
tree.write("V", f"machine/{name}.json",
|
||||
{"type": "machine", "name": name, "instantiation": "true", **extra})
|
||||
tree.index("V", "machine", name, f"machine/{name}.json")
|
||||
|
||||
@staticmethod
|
||||
def filament(tree, name, printers, filament_id="OFpetg01", **extra):
|
||||
tree.write("V", f"filament/{name}.json",
|
||||
{"type": "filament", "name": name, "instantiation": "true",
|
||||
"filament_id": filament_id, "compatible_printers": printers, **extra})
|
||||
tree.index("V", "filament", name, f"filament/{name}.json")
|
||||
|
||||
def generate(self):
|
||||
return apt.legacy_compatible_printers(self.base.profiles, self.cur.profiles)
|
||||
|
||||
def test_renamed_and_narrowed_preset_keeps_printers_a_same_id_sibling_covers(self):
|
||||
for tree in (self.base, self.cur):
|
||||
for p in ("A 0.4", "A 0.6", "B 0.4"):
|
||||
self.printer(tree, p)
|
||||
self.filament(self.base, "PETG @A-all", ["A 0.4", "A 0.6", "B 0.4"])
|
||||
self.filament(self.cur, "PETG @V A-all", ["A 0.6"], renamed_from="PETG @A-all;PETG A-all")
|
||||
self.filament(self.cur, "PETG @A 0.4", ["A 0.4"])
|
||||
self.assertEqual(self.generate(), {"PETG @A-all": ["A 0.4"]})
|
||||
|
||||
def test_each_old_name_gets_back_only_its_own_list(self):
|
||||
# The current "PC @0.4" existed in the base and also replaces the split "PC".
|
||||
for tree in (self.base, self.cur):
|
||||
for p in ("A 0.4", "A 0.8"):
|
||||
self.printer(tree, p)
|
||||
self.filament(self.base, "PC", ["A 0.4", "A 0.8"])
|
||||
self.filament(self.base, "PC @0.4", ["A 0.4"])
|
||||
self.filament(self.cur, "PC @0.4", ["A 0.4"], renamed_from="PC")
|
||||
self.filament(self.cur, "PC @0.8", ["A 0.8"])
|
||||
self.assertEqual(self.generate(), {"PC": ["A 0.8"]})
|
||||
|
||||
def test_old_name_now_a_base_preset_resolves_through_renamed_from(self):
|
||||
for tree in (self.base, self.cur):
|
||||
for p in ("I 0.4", "Plus 0.4"):
|
||||
self.printer(tree, p)
|
||||
self.filament(self.base, "PLA @base", ["I 0.4", "Plus 0.4"])
|
||||
self.filament(self.cur, "PLA @base", [], instantiation="false")
|
||||
self.filament(self.cur, "PLA @I 0.4", ["I 0.4"], renamed_from="PLA @base")
|
||||
self.filament(self.cur, "PLA @Plus 0.4", ["Plus 0.4"])
|
||||
self.assertEqual(self.generate(), {"PLA @base": ["Plus 0.4"]})
|
||||
|
||||
def test_printer_without_a_successor_is_left_out(self):
|
||||
for tree in (self.base, self.cur):
|
||||
for p in ("A 0.4", "A 0.6"):
|
||||
self.printer(tree, p)
|
||||
self.filament(self.base, "PETG @A", ["A 0.4", "A 0.6"])
|
||||
self.filament(self.cur, "PETG @A", ["A 0.6"])
|
||||
self.filament(self.cur, "Other @A", ["A 0.4"], filament_id="OFother1")
|
||||
self.assertEqual(self.generate(), {})
|
||||
|
||||
def test_printer_names_follow_printer_renames(self):
|
||||
self.printer(self.base, "Old 0.4")
|
||||
self.printer(self.base, "A 0.6")
|
||||
self.printer(self.cur, "New 0.4", renamed_from="Old 0.4")
|
||||
self.printer(self.cur, "A 0.6")
|
||||
self.filament(self.base, "PETG @A", ["Old 0.4", "A 0.6"])
|
||||
self.filament(self.cur, "PETG @A", ["A 0.6"])
|
||||
self.filament(self.cur, "PETG @New", ["New 0.4"])
|
||||
self.assertEqual(self.generate(), {"PETG @A": ["New 0.4"]})
|
||||
|
||||
def test_printer_without_a_shared_extruder_variant_is_left_out(self):
|
||||
for tree in (self.base, self.cur):
|
||||
self.printer(tree, "Direct 0.4", printer_extruder_variant=["Direct Drive Standard"])
|
||||
self.printer(tree, "Bowden 0.4", printer_extruder_variant=["Bowden Standard"])
|
||||
self.filament(self.base, "PETG @all", ["Direct 0.4", "Bowden 0.4"])
|
||||
self.filament(self.cur, "PETG @all", ["Direct 0.4"],
|
||||
filament_extruder_variant=["Direct Drive Standard"])
|
||||
self.filament(self.cur, "PETG @Bowden", ["Bowden 0.4"])
|
||||
self.assertEqual(self.generate(), {})
|
||||
|
||||
def test_preset_now_listing_no_printer_is_skipped(self):
|
||||
for tree in (self.base, self.cur):
|
||||
self.printer(tree, "A 0.4")
|
||||
self.filament(self.base, "PETG @A", ["A 0.4"])
|
||||
self.filament(self.cur, "PETG @A", [])
|
||||
self.assertEqual(self.generate(), {})
|
||||
|
||||
def test_check_passes_on_a_current_map_and_fails_on_a_stale_one(self):
|
||||
for tree in (self.base, self.cur):
|
||||
for p in ("A 0.4", "A 0.6"):
|
||||
self.printer(tree, p)
|
||||
self.filament(self.base, "PETG @A", ["A 0.4", "A 0.6"])
|
||||
self.filament(self.cur, "PETG @A", ["A 0.6"])
|
||||
self.filament(self.cur, "PETG @A 0.4", ["A 0.4"])
|
||||
output = os.path.join(self.cur.dir, "map.json")
|
||||
argv = ["legacy-compat", "--base", self.base.profiles, "--profiles", self.cur.profiles,
|
||||
"--output", output]
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
self.assertEqual(apt.main(argv), 0)
|
||||
self.assertEqual(apt.main(argv + ["--check"]), 0)
|
||||
with open(output, "w", encoding="utf-8") as f:
|
||||
json.dump({"presets": {}}, f)
|
||||
self.assertEqual(apt.main(argv + ["--check"]), 1)
|
||||
|
||||
|
||||
class TestDispatch(TreeCase):
|
||||
def test_each_command_reaches_its_own_writer(self):
|
||||
self.t.write("V", "filament/A.json", {"type": "filament", "name": "A"})
|
||||
|
||||
Reference in New Issue
Block a user