mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-15 11:51:33 +00:00
Merge branch 'main' into feat/texdisp-tooltip-media
This commit is contained in:
@@ -12,8 +12,11 @@ docker run \
|
||||
--net=host \
|
||||
`# Some X installs will not have permissions to talk to sockets for shared memory` \
|
||||
--ipc host \
|
||||
`# Run as your workstations username to keep permissions the same` \
|
||||
-u "$USER" \
|
||||
`# Run as your workstations UID and GID to keep permissions the same.` \
|
||||
`# Numeric IDs work even when the image has no user with your name` \
|
||||
-u "$(id -u):$(id -g)" \
|
||||
-e HOME="/home/$USER" \
|
||||
-w "/home/$USER" \
|
||||
`# Bind mount your home directory into the container for loading/saving files` \
|
||||
-v "$HOME:/home/$USER" \
|
||||
`# Pass the X display number to the container` \
|
||||
|
||||
@@ -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" ;;
|
||||
|
||||
+43
-17
@@ -1,21 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Lets a pull request's macOS deps build wait for a push build of the base
|
||||
# branch that builds the same deps, from a Linux job. `check` runs after a cache
|
||||
# miss and names a queued or running base build with the same deps/ and .github/
|
||||
# to wait for, unless the pull request changes either. `wait` waits until that
|
||||
# build saves KEY. actions/cache then confirms the entry can be restored.
|
||||
# Lets a pull request's macOS deps build, or its clang-tidy job, wait for a
|
||||
# build that saves the deps cache it needs, from a Linux job. `check` runs after
|
||||
# a cache miss and names a queued or running push build of the base branch with
|
||||
# the same deps/ and .github/ to wait for, unless the pull request changes either.
|
||||
# `own` names the pull request's own build instead, for clang-tidy when the pull
|
||||
# request changes deps/. `wait` waits until that build saves KEY under CACHE_REF,
|
||||
# or its deps job fails. actions/cache then confirms the entry can be restored.
|
||||
#
|
||||
# Environment: GH_TOKEN, REPO, WORKFLOW_REF and BASE_REF, plus KEY_INPUTS,
|
||||
# WAIT_FOR, WAIT_SINCE, WAIT_MINUTES, KEY and ARCH for `wait`.
|
||||
# Environment: GH_TOKEN, REPO, WORKFLOW_REF (or WORKFLOW, the build workflow's
|
||||
# file name) and BASE_REF, plus HEAD_SHA for `own`, and KEY_INPUTS, WAIT_FOR,
|
||||
# WAIT_SINCE, WAIT_MINUTES, KEY, CACHE_REF (default the base branch) and ARCH
|
||||
# (or JOB, the matrix job's name) for `wait`.
|
||||
# Writes key-inputs, wait-for and wait-since to $GITHUB_OUTPUT.
|
||||
set -uo pipefail
|
||||
|
||||
workflow=${WORKFLOW_REF%%@*}
|
||||
workflow=${workflow##*/}
|
||||
workflow=${WORKFLOW_REF:-}
|
||||
workflow=${workflow%%@*}
|
||||
workflow=${WORKFLOW:-${workflow##*/}}
|
||||
|
||||
# Must match the job names in build_all.yml, build_check_cache.yml and build_deps.yml.
|
||||
check_job_name="build_macos_arch (${ARCH:-}) / Check Cache"
|
||||
deps_job_name="build_macos_arch (${ARCH:-}) / Build Deps / Build Deps"
|
||||
job=${JOB:-build_macos_arch (${ARCH:-})}
|
||||
check_job_name="$job / Check Cache"
|
||||
deps_job_name="$job / Build Deps / Build Deps"
|
||||
|
||||
out() { echo "$1" >> "${GITHUB_OUTPUT:-/dev/stdout}"; }
|
||||
|
||||
@@ -63,8 +69,27 @@ check() {
|
||||
fi
|
||||
}
|
||||
|
||||
own() {
|
||||
out "key-inputs=$(local_key_inputs HEAD)"
|
||||
local id
|
||||
# The build can be created a moment after this job.
|
||||
for _ in 1 2 3 4 5; do
|
||||
id=$(gh api -X GET "repos/$REPO/actions/workflows/$workflow/runs" \
|
||||
-f head_sha="$HEAD_SHA" -f event=pull_request -f per_page=1 --jq '.workflow_runs[0].id // empty' < /dev/null)
|
||||
[ -n "$id" ] && break
|
||||
sleep 30
|
||||
done
|
||||
if [ -n "$id" ]; then
|
||||
echo "Waiting for the deps cache of build $id of this pull request."
|
||||
out "wait-for=$id"
|
||||
out "wait-since=$(date +%s)"
|
||||
else
|
||||
echo "No build of this pull request is building its deps."
|
||||
fi
|
||||
}
|
||||
|
||||
key_saved() {
|
||||
gh cache list -R "$REPO" --ref "refs/heads/$BASE_REF" --key "$KEY" --json key < /dev/null \
|
||||
gh cache list -R "$REPO" --ref "${CACHE_REF:-refs/heads/$BASE_REF}" --key "$KEY" --json key < /dev/null \
|
||||
| jq -e --arg key "$KEY" 'any(.[]; .key == $key)' > /dev/null
|
||||
}
|
||||
|
||||
@@ -87,11 +112,11 @@ wait_for() {
|
||||
key_saved && { echo "Found $KEY."; return; }
|
||||
sleep 20
|
||||
done
|
||||
echo "Build $id of $BASE_REF finished its deps without saving $KEY."
|
||||
echo "Build $id finished its deps without saving $KEY."
|
||||
return ;;
|
||||
"completed cancelled") ;;
|
||||
completed*)
|
||||
echo "The deps job of build $id of $BASE_REF did not succeed."
|
||||
echo "The deps job of build $id did not succeed."
|
||||
return ;;
|
||||
# The deps job exists only once the build's cache check has succeeded.
|
||||
"")
|
||||
@@ -100,7 +125,7 @@ wait_for() {
|
||||
completed*)
|
||||
check=$(jq -r --arg name "$check_job_name" 'first(.jobs[] | select(.name == $name) | .conclusion) // ""' <<< "$jobs")
|
||||
if [ -z "$warned" ] && { [ -z "$check" ] || [ "$check" = success ]; }; then
|
||||
echo "::warning title=Deps wait::Build $id of $BASE_REF has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request."
|
||||
echo "::warning title=Deps wait::Build $id has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request."
|
||||
warned=1
|
||||
fi ;;
|
||||
*)
|
||||
@@ -118,11 +143,12 @@ wait_for() {
|
||||
seen="$seen $id"
|
||||
echo "Following build $id of $BASE_REF."
|
||||
done
|
||||
echo "No build of $BASE_REF saved $KEY."
|
||||
echo "No build saved $KEY."
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
check) check ;;
|
||||
own) own ;;
|
||||
wait) wait_for ;;
|
||||
*) echo "usage: $0 check|wait" >&2; exit 2 ;;
|
||||
*) echo "usage: $0 check|own|wait" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
@@ -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