mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-11-04 21:41:26 +00:00
The app installs a bundled profile set only when its version is newer than the installed one, and the OTA profile update is named after the version, so a vendor bundle merged without a bump in its <Vendor>.json never reaches a user. Nothing checked that: two recent OrcaFilamentLibrary additions and a Qidi printer model merged with their version unchanged. Add a version-check command to orca_profile_tool.py that diffs two revisions (or the working tree against the commit it left main at), groups the changed files by vendor bundle, and fails unless every changed bundle's version is newer than before and written as four numbers. The fourth component folds into the third as the app reads it, so 02.04.00.100 is not newer than 02.04.01.00. A bundle the base does not have, blacklist.json and the user preset directory are not judged. Run it as the version_bump check in the Check profiles workflow against the branch a PR merges into, and in check_profile.sh and check_profile.ps1, which take --base / -Base to compare with another ref and skip the check for a --profiles tree outside a git checkout.
745 lines
32 KiB
Bash
Executable File
745 lines
32 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Local twin of the "Check profiles" CI job (.github/workflows/check_profiles.yml).
|
|
#
|
|
# Runs the same seven 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, 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.
|
|
#
|
|
# resources/profiles/user, which the validator creates as its data dir but a CI checkout never
|
|
# has, is moved aside for the duration of the run and restored on exit. Only one run per work
|
|
# dir at a time.
|
|
#
|
|
# -v/--vendor narrows a run to one vendor while working on that vendor's profiles - the one
|
|
# deliberate divergence from CI, which always checks the whole tree. A check that cannot be
|
|
# narrowed is left out of the run and reported as skipped; see usage(). version_bump compares
|
|
# the tree with the commit it left main at, where CI compares a PR with the branch it merges
|
|
# into; --base points it elsewhere.
|
|
#
|
|
# Usage: scripts/check_profile.sh [OPTION]... [CHECK]...
|
|
|
|
# The check_* functions run through run_check, which dispatches on the check name, so
|
|
# ShellCheck cannot see that they (and what they call) are used.
|
|
# shellcheck disable=SC2329
|
|
|
|
set -uo pipefail
|
|
|
|
VALIDATOR_RELEASE_URL="https://github.com/OrcaSlicer/OrcaSlicer/releases/download/nightly-builds"
|
|
FIXTURE_RELEASE_URL="https://github.com/OrcaSlicer/OrcaSlicer-profile-validator/releases/download/fixture-archive"
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
|
|
|
HOST_ARCH="$(uname -m)"
|
|
HOST_OS="$(uname -s)"
|
|
case "${HOST_OS}" in
|
|
Darwin*) HOST_OS=Darwin ;;
|
|
MINGW*|MSYS*|CYGWIN*) HOST_OS=Windows ;;
|
|
Linux*) HOST_OS=Linux ;;
|
|
esac
|
|
|
|
PROFILES_DIR="${REPO_ROOT}/resources/profiles"
|
|
case "${HOST_OS}" in
|
|
Darwin) DEFAULT_WORK_DIR="${HOME}/Library/Caches/orca-profile-check" ;;
|
|
Windows) DEFAULT_WORK_DIR="${LOCALAPPDATA:-${HOME}/AppData/Local}/orca-profile-check" ;;
|
|
*) DEFAULT_WORK_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/orca-profile-check" ;;
|
|
esac
|
|
WORK_DIR="${DEFAULT_WORK_DIR}"
|
|
VALIDATOR="${ORCA_PROFILE_VALIDATOR:-}"
|
|
# Vendor to check, named after its <Vendor>.json - empty means every vendor, which is exactly what
|
|
# both the validator's -v and orca_profile_tool.py check's --vendor take an empty value to mean.
|
|
# So the flag is passed unconditionally below rather than kept in an array bash 3.2 cannot expand
|
|
# empty under `set -u`.
|
|
VENDOR=""
|
|
BASE_REF=""
|
|
LOG_LEVEL=2
|
|
PREFER_DOWNLOAD=0
|
|
REFRESH=0
|
|
|
|
ALL_CHECKS=(profile_tool version_bump 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)
|
|
# cannot expand an empty array under `set -u`.
|
|
RESULTS=""
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Run the profile checks from .github/workflows/check_profiles.yml locally.
|
|
|
|
Usage: scripts/check_profile.sh [OPTION]... [CHECK]...
|
|
|
|
Checks (default: all, in this order):
|
|
profile_tool scripts/orca_profile_tool.py check
|
|
version_bump scripts/orca_profile_tool.py version-check: every changed
|
|
bundle's <Vendor>.json has a newer version than at --base
|
|
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
|
|
validate_custom validator against every released custom-preset fixture
|
|
|
|
Options:
|
|
-p, --profiles DIR profile tree to validate (default: resources/profiles)
|
|
-v, --vendor NAME check only this vendor, named after its <Vendor>.json (e.g. "Co Print")
|
|
--base REF the git ref version_bump compares the tree with (default: where the
|
|
branch left upstream/main, else origin/main)
|
|
--validator BIN OrcaSlicer_profile_validator to use; also \$ORCA_PROFILE_VALIDATOR.
|
|
Default: the local build*/ Release build (then RelWithDebInfo, then
|
|
Debug) for this architecture, else the nightly release build is
|
|
downloaded for this platform
|
|
--download ignore local builds and use the downloaded nightly validator
|
|
--refresh re-download the validator and fixtures instead of using the cache
|
|
--work-dir DIR downloads, logs and fixture trees (default: ${DEFAULT_WORK_DIR})
|
|
-l, --log-level N validator log level (default: ${LOG_LEVEL}, as in CI)
|
|
-h, --help show this help
|
|
|
|
Note: profile_tool, version_bump and legacy_compat are the checks that are not the validator
|
|
binary; they make the static checks the validator cannot, because the validator loads the tree
|
|
the way the slicer does and so never sees a profile no <vendor>.json indexes, a preset name
|
|
two files claim, a file normalize and update-index would still rewrite, or a bundle that
|
|
changed without its version being bumped. version_bump is skipped, unless named, for a
|
|
--profiles tree that is not inside a git checkout, or one with neither upstream/main nor
|
|
origin/main to compare with when no --base is given.
|
|
|
|
Note: --vendor narrows validate_custom too, by keeping only that vendor's presets in each
|
|
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, and legacy_compat is tree-wide too: the map
|
|
it checks covers every vendor.
|
|
EOF
|
|
}
|
|
|
|
msg() { printf '%s\n' "$*" >&2; }
|
|
die() { printf 'check_profile.sh: %s\n' "$*" >&2; exit 2; }
|
|
|
|
if [ -t 1 ]; then
|
|
C_RED=$'\033[91m'; C_GREEN=$'\033[92m'; C_BOLD=$'\033[1m'; C_RESET=$'\033[0m'
|
|
else
|
|
C_RED=''; C_GREEN=''; C_BOLD=''; C_RESET=''
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------- arguments
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
-h|--help) usage; exit 0 ;;
|
|
-p|--profiles) [ $# -ge 2 ] || die "$1 needs a directory"; PROFILES_DIR="$2"; shift 2 ;;
|
|
-v|--vendor) [ $# -ge 2 ] || die "$1 needs a vendor name"; VENDOR="$2"; shift 2 ;;
|
|
--base) [ $# -ge 2 ] || die "$1 needs a git ref"; BASE_REF="$2"; shift 2 ;;
|
|
--validator) [ $# -ge 2 ] || die "$1 needs a path"; VALIDATOR="$2"; shift 2 ;;
|
|
--work-dir) [ $# -ge 2 ] || die "$1 needs a directory"; WORK_DIR="$2"; shift 2 ;;
|
|
-l|--log-level) [ $# -ge 2 ] || die "$1 needs a number"; LOG_LEVEL="$2"; shift 2 ;;
|
|
--download) PREFER_DOWNLOAD=1; shift ;;
|
|
--refresh) REFRESH=1; shift ;;
|
|
-*) die "unknown option '$1' (try --help)" ;;
|
|
*)
|
|
known=0
|
|
for check in "${ALL_CHECKS[@]}"; do
|
|
[ "$1" = "${check}" ] && known=1
|
|
done
|
|
[ "${known}" -eq 1 ] || die "unknown check '$1' (try --help)"
|
|
CHECKS[${#CHECKS[@]}]="$1"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# A check named on the command line always runs; only the default set is narrowed (see the run
|
|
# loop below).
|
|
NAMED_CHECKS="${#CHECKS[@]}"
|
|
[ "${NAMED_CHECKS}" -gt 0 ] || CHECKS=("${ALL_CHECKS[@]}")
|
|
|
|
[ -d "${PROFILES_DIR}" ] || die "profile directory not found: ${PROFILES_DIR}"
|
|
PROFILES_DIR="$(cd -- "${PROFILES_DIR}" && pwd)"
|
|
|
|
# A vendor neither tool knows is not an error to them: the static checks load nothing of their own
|
|
# and still report success, so a typo would otherwise be three green checks and one baffling slice
|
|
# failure. The match has to be made on the names themselves rather than with a -f test - the
|
|
# validator compares the <Vendor>.json stem case-sensitively, while a case-insensitive filesystem
|
|
# (macOS, Windows) would let "creality" pass a file test and then match no vendor. A vendor is a
|
|
# <name>.json with a sibling <name>/ directory; that pair is also what tells one apart from
|
|
# blacklist.json, which sits in the same folder.
|
|
if [ -n "${VENDOR}" ]; then
|
|
wanted="$(printf '%s' "${VENDOR}" | tr '[:upper:]' '[:lower:]')"
|
|
found=""
|
|
suggestion=""
|
|
for file in "${PROFILES_DIR}"/*.json; do
|
|
name="${file##*/}"; name="${name%.json}"
|
|
[ -d "${PROFILES_DIR}/${name}" ] || continue
|
|
if [ "${name}" = "${VENDOR}" ]; then
|
|
found=1
|
|
break
|
|
fi
|
|
[ "$(printf '%s' "${name}" | tr '[:upper:]' '[:lower:]')" = "${wanted}" ] && suggestion="${name}"
|
|
done
|
|
if [ -z "${found}" ]; then
|
|
[ -z "${suggestion}" ] || die "unknown vendor '${VENDOR}'; vendor names are case-sensitive, did you mean '${suggestion}'?"
|
|
die "unknown vendor '${VENDOR}': no such vendor in ${PROFILES_DIR}"
|
|
fi
|
|
fi
|
|
|
|
LOG_DIR="${WORK_DIR}/logs"
|
|
mkdir -p "${LOG_DIR}" || die "cannot create ${LOG_DIR}"
|
|
WORK_DIR="$(cd -- "${WORK_DIR}" && pwd)"
|
|
LOG_DIR="${WORK_DIR}/logs"
|
|
|
|
wants() {
|
|
local check
|
|
for check in "${CHECKS[@]}"; do
|
|
[ "${check}" = "$1" ] && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# ------------------------------------------------------------------- clean profile tree
|
|
|
|
# The validator points its data dir at the profile tree, so it creates - and, with -g, fills -
|
|
# <profiles>/user. A CI checkout never has that directory, and anything left in it from an
|
|
# earlier local run would be loaded as user presets and validated too. Move it aside for the
|
|
# duration of the run so what gets checked is what CI checks.
|
|
STASHED_USER_DIR=""
|
|
|
|
stash_user_presets() {
|
|
[ -d "${PROFILES_DIR}/user" ] || return 0
|
|
STASHED_USER_DIR="${WORK_DIR}/user-presets-$$"
|
|
rm -rf "${STASHED_USER_DIR}"
|
|
mv "${PROFILES_DIR}/user" "${STASHED_USER_DIR}" || { STASHED_USER_DIR=""; die "cannot move ${PROFILES_DIR}/user aside"; }
|
|
msg "moved ${PROFILES_DIR}/user aside for the run (restored on exit)"
|
|
}
|
|
|
|
restore_user_presets() {
|
|
# The validator leaves an empty user/default/{filament,machine,process} skeleton behind.
|
|
# Prune it with rmdir, never rm -rf: a directory that holds a real file survives and is
|
|
# reported instead of being deleted. Runs even when nothing was stashed, so a tree that had
|
|
# no user/ before the run does not gain one.
|
|
find "${PROFILES_DIR}/user" -depth -type d -exec rmdir {} + 2>/dev/null
|
|
[ -n "${STASHED_USER_DIR}" ] || return 0
|
|
if [ -d "${PROFILES_DIR}/user" ]; then
|
|
msg "${PROFILES_DIR}/user is not empty; your presets stay in ${STASHED_USER_DIR}"
|
|
STASHED_USER_DIR=""
|
|
return 0
|
|
fi
|
|
mv "${STASHED_USER_DIR}" "${PROFILES_DIR}/user"
|
|
STASHED_USER_DIR=""
|
|
}
|
|
|
|
# The fixture trees under the work dir are shared scratch space keyed by fixture version, so a
|
|
# second run would delete a tree the first one is validating.
|
|
LOCK_DIR="${WORK_DIR}/.lock"
|
|
mkdir "${LOCK_DIR}" 2>/dev/null ||
|
|
die "another run is using ${WORK_DIR} (pass --work-dir, or remove ${LOCK_DIR} if no run is active)"
|
|
|
|
cleanup() {
|
|
restore_user_presets
|
|
rmdir "${LOCK_DIR}" 2>/dev/null
|
|
}
|
|
|
|
# Installed only once the lock is ours, so a refused start never releases someone else's.
|
|
trap cleanup EXIT
|
|
trap 'cleanup; exit 130' INT TERM
|
|
|
|
stash_user_presets
|
|
|
|
# ---------------------------------------------------------------------------- downloads
|
|
|
|
# fetch URL DEST [force] - cached; --refresh, or a non-empty third argument, forces a new
|
|
# download. Release assets never change, so caching them is safe; the fixture manifest is the
|
|
# index that grows with every release, and CI re-reads it on every run.
|
|
fetch() {
|
|
local url="$1" dest="$2" force="${3:-}"
|
|
if [ "${REFRESH}" -eq 0 ] && [ -z "${force}" ] && [ -s "${dest}" ]; then
|
|
return 0
|
|
fi
|
|
mkdir -p "$(dirname -- "${dest}")" || return 1
|
|
msg "downloading $(basename -- "${dest}") ..."
|
|
if ! curl -fsSL --retry 3 -o "${dest}.part" "${url}"; then
|
|
rm -f "${dest}.part"
|
|
msg "download failed: ${url}"
|
|
return 1
|
|
fi
|
|
mv -f "${dest}.part" "${dest}"
|
|
}
|
|
|
|
sha256_of() {
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum "$1" | cut -d' ' -f1
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
shasum -a 256 "$1" | cut -d' ' -f1
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Directories a built validator can sit in, best first: every Ninja Multi-Config Release tree,
|
|
# then RelWithDebInfo, then Debug, then a single-config generator's plain src/. Each pattern is
|
|
# matched both directly under a build root (build/) and one level down (build/arm64, build/x64),
|
|
# and unmatched globs are dropped by the caller's -d test.
|
|
validator_search_dirs() {
|
|
local config
|
|
for config in Release RelWithDebInfo Debug; do
|
|
printf '%s\n' "${REPO_ROOT}"/build*/src/"${config}" "${REPO_ROOT}"/build*/*/src/"${config}"
|
|
done
|
|
printf '%s\n' "${REPO_ROOT}"/build*/src "${REPO_ROOT}"/build*/*/src
|
|
}
|
|
|
|
# A build tree named for the other architecture (build/x86_64 on an arm64 host, build/arm64 on
|
|
# an x64 one) is a last resort: on Linux it will not run at all, on macOS it goes via Rosetta.
|
|
# Takes a repo-relative path - an absolute one would also match an arch name in the checkout path.
|
|
is_foreign_arch_dir() {
|
|
case "${HOST_ARCH}" in
|
|
arm64|aarch64) case "$1" in *x86_64*|*x86-64*|*x64*|*amd64*) return 0 ;; esac ;;
|
|
x86_64|amd64) case "$1" in *arm64*|*aarch64*) return 0 ;; esac ;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
# First locally built OrcaSlicer_profile_validator, in the order above. macOS puts it in an .app
|
|
# bundle, Linux and Windows next to the other binaries.
|
|
find_local_validator() {
|
|
local dir candidate foreign=""
|
|
while IFS= read -r dir; do
|
|
[ -d "${dir}" ] || continue
|
|
for candidate in \
|
|
"${dir}/OrcaSlicer_profile_validator" \
|
|
"${dir}/OrcaSlicer_profile_validator.exe" \
|
|
"${dir}/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator"; do
|
|
[ -f "${candidate}" ] && [ -x "${candidate}" ] || continue
|
|
if ! is_foreign_arch_dir "${dir#"${REPO_ROOT}"/}"; then
|
|
printf '%s\n' "${candidate}"
|
|
return 0
|
|
fi
|
|
[ -n "${foreign}" ] || foreign="${candidate}"
|
|
done
|
|
done <<EOF
|
|
$(validator_search_dirs)
|
|
EOF
|
|
[ -n "${foreign}" ] || return 1
|
|
msg "no ${HOST_ARCH} build found, falling back to ${foreign}"
|
|
printf '%s\n' "${foreign}"
|
|
}
|
|
|
|
# The nightly release build, same one CI uses. Linux ships the bare binary, macOS a .dmg
|
|
# holding the signed .app, Windows an .exe.
|
|
download_validator() {
|
|
local dest="${WORK_DIR}/validator" binary dmg app mounted app_src
|
|
case "${HOST_OS}" in
|
|
Linux)
|
|
case "${HOST_ARCH}" in
|
|
arm64|aarch64) msg "the nightly Linux validator is x86_64; build it locally for ${HOST_ARCH}" ;;
|
|
esac
|
|
binary="${dest}/OrcaSlicer_profile_validator"
|
|
fetch "${VALIDATOR_RELEASE_URL}/OrcaSlicer_profile_validator_Linux_Ubuntu2404_nightly" "${binary}" || return 1
|
|
chmod +x "${binary}" || return 1
|
|
;;
|
|
Darwin)
|
|
dmg="${dest}/OrcaSlicer_profile_validator.dmg"
|
|
app="${dest}/OrcaSlicer_profile_validator.app"
|
|
binary="${app}/Contents/MacOS/OrcaSlicer_profile_validator"
|
|
fetch "${VALIDATOR_RELEASE_URL}/OrcaSlicer_profile_validator_Mac_universal_nightly.dmg" "${dmg}" || return 1
|
|
if [ ! -x "${binary}" ] || [ "${REFRESH}" -eq 1 ]; then
|
|
mounted="${dest}/mnt"
|
|
rm -rf "${mounted}" "${app}"
|
|
mkdir -p "${mounted}" || return 1
|
|
hdiutil attach -nobrowse -readonly -mountpoint "${mounted}" "${dmg}" >/dev/null || return 1
|
|
app_src="$(find "${mounted}" -maxdepth 1 -name '*.app' -print 2>/dev/null | head -n 1)"
|
|
if [ -n "${app_src}" ]; then
|
|
cp -R "${app_src}" "${app}"
|
|
fi
|
|
hdiutil detach "${mounted}" >/dev/null 2>&1
|
|
rmdir "${mounted}" 2>/dev/null
|
|
[ -x "${binary}" ] || { msg "no validator app inside ${dmg}"; return 1; }
|
|
fi
|
|
;;
|
|
Windows)
|
|
binary="${dest}/OrcaSlicer_profile_validator.exe"
|
|
fetch "${VALIDATOR_RELEASE_URL}/OrcaSlicer_profile_validator_Windows_nightly.exe" "${binary}" || return 1
|
|
chmod +x "${binary}" || return 1
|
|
;;
|
|
*)
|
|
msg "no nightly validator published for ${HOST_OS}; build it (-DORCA_TOOLS=ON) and pass --validator"
|
|
return 1
|
|
;;
|
|
esac
|
|
printf '%s\n' "${binary}"
|
|
}
|
|
|
|
resolve_validator() {
|
|
if [ -n "${VALIDATOR}" ]; then
|
|
[ -x "${VALIDATOR}" ] || die "validator not executable: ${VALIDATOR}"
|
|
return 0
|
|
fi
|
|
if [ "${PREFER_DOWNLOAD}" -eq 0 ]; then
|
|
VALIDATOR="$(find_local_validator)"
|
|
if [ -n "${VALIDATOR}" ]; then
|
|
msg "using locally built validator: ${VALIDATOR}"
|
|
return 0
|
|
fi
|
|
fi
|
|
VALIDATOR="$(download_validator)" || die "could not obtain a profile validator"
|
|
msg "using downloaded validator: ${VALIDATOR}"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------- checks
|
|
|
|
check_profile_tool() {
|
|
python3 "${REPO_ROOT}/scripts/orca_profile_tool.py" check --profiles "${PROFILES_DIR}" --vendor "${VENDOR}"
|
|
}
|
|
|
|
# The app installs a bundle only when its version is newer than the installed one, and the OTA
|
|
# profile update is named after it, so a changed bundle whose <Vendor>.json keeps its version
|
|
# never reaches users.
|
|
check_version_bump() {
|
|
python3 "${REPO_ROOT}/scripts/orca_profile_tool.py" version-check --profiles "${PROFILES_DIR}" \
|
|
--vendor "${VENDOR}" ${BASE_REF:+--base "${BASE_REF}"}
|
|
}
|
|
|
|
# Why the default run leaves version_bump out, on stdout; nothing, and status 1, when it can run.
|
|
version_bump_skip_reason() {
|
|
local main
|
|
if ! git -C "${PROFILES_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
echo "${PROFILES_DIR} is not in a git checkout"
|
|
return 0
|
|
fi
|
|
[ -z "${BASE_REF}" ] || return 1
|
|
for main in upstream/main origin/main; do
|
|
git -C "${PROFILES_DIR}" rev-parse -q --verify "${main}^{commit}" >/dev/null 2>&1 && return 1
|
|
done
|
|
echo "neither upstream/main nor origin/main exists to compare with; pass --base"
|
|
return 0
|
|
}
|
|
|
|
# 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}"
|
|
}
|
|
|
|
# Slices a two-colour cube through every printer so all custom g-code (incl. change_filament_gcode)
|
|
# is expanded - catches undefined-placeholder / invalid-flow bugs the static checks cannot see.
|
|
check_validate_slice() {
|
|
"${VALIDATOR}" -p "${PROFILES_DIR}" -v "${VENDOR}" -s -l "${LOG_LEVEL}"
|
|
}
|
|
|
|
check_validate_filament_subtypes() {
|
|
"${VALIDATOR}" -p "${PROFILES_DIR}" -v "${VENDOR}" -l "${LOG_LEVEL}" -f
|
|
}
|
|
|
|
# The fixtures name every preset "<vendor>_<parent preset>_orca_test", after the "name" inside
|
|
# <Vendor>.json rather than the file stem --vendor takes - BBL.json is "Bambulab", iQ.json is
|
|
# "innovatiQ". Falls back to the stem for a vendor file with no readable name.
|
|
vendor_display_name() {
|
|
local name
|
|
name="$(VENDOR_JSON="${PROFILES_DIR}/${VENDOR}.json" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
try:
|
|
with open(os.environ["VENDOR_JSON"], encoding="utf-8") as fh:
|
|
print(json.load(fh).get("name", ""))
|
|
except Exception:
|
|
pass
|
|
PY
|
|
)"
|
|
printf '%s\n' "${name:-${VENDOR}}"
|
|
}
|
|
|
|
# Every released fixture is a snapshot of user presets saved by that OrcaSlicer version; each is
|
|
# unpacked over the current system profiles and validated, so a profile change that would break
|
|
# an existing user's presets fails here.
|
|
#
|
|
# Under --vendor only that vendor's presets are unpacked, which is what makes the validator's -v
|
|
# usable here: -v filters the system vendors but never the user presets, so on a whole-tree
|
|
# snapshot it reports every other vendor's presets as unresolvable parents - thousands of errors
|
|
# saying nothing about the vendor under test. Teaching -v to filter user presets too is the deeper
|
|
# fix, but this runs against whatever validator is to hand, the published nightly included, so the
|
|
# picking has to happen here. It picks on the "<vendor display name>_" filename prefix that
|
|
# generate_custom_presets() (the validator's -g mode) gave every preset in these fixtures; a
|
|
# fixture cut from a renamed generator would surface as the "checked nothing" warning below.
|
|
# Presets whose source had no vendor carry no prefix - a few of those still name one in the
|
|
# middle, like "PET @BBL A1_orca_test" - and only an unscoped run covers them.
|
|
check_validate_custom() {
|
|
local fixtures_dir="${WORK_DIR}/profile-fixtures"
|
|
local output_dir="${WORK_DIR}/custom-preset-validation"
|
|
local summary="${output_dir}/summary.md"
|
|
local status=0 failed_logs="" vendor_prefix="" total_kept=0
|
|
local version asset expected_sha256 asset_url fixture_zip profile_tree log_path actual_sha256 result kept
|
|
|
|
command -v unzip >/dev/null 2>&1 || { msg "unzip is required for validate_custom"; return 1; }
|
|
[ -z "${VENDOR}" ] || vendor_prefix="$(vendor_display_name)_"
|
|
mkdir -p "${fixtures_dir}" "${output_dir}" || return 1
|
|
|
|
fetch "${FIXTURE_RELEASE_URL}/manifest.json" "${fixtures_dir}/manifest.json" force || return 1
|
|
|
|
MANIFEST_PATH="${fixtures_dir}/manifest.json" python3 - > "${fixtures_dir}/fixtures.tsv" <<'PY'
|
|
import json
|
|
import os
|
|
|
|
with open(os.environ["MANIFEST_PATH"], encoding="utf-8") as fh:
|
|
manifest = json.load(fh)
|
|
|
|
if isinstance(manifest, dict):
|
|
entries = manifest.get("fixtures", [])
|
|
else:
|
|
entries = manifest
|
|
|
|
for entry in entries:
|
|
version = entry.get("version", "")
|
|
asset = entry.get("asset", "")
|
|
sha256 = entry.get("asset_sha256", "")
|
|
if not version or not asset:
|
|
continue
|
|
print(f"{version}\t{asset}\t{sha256}")
|
|
PY
|
|
|
|
if [ ! -s "${fixtures_dir}/fixtures.tsv" ]; then
|
|
echo "No custom preset fixtures found in ${FIXTURE_RELEASE_URL}/manifest.json"
|
|
return 1
|
|
fi
|
|
|
|
{
|
|
echo "## Custom Preset Fixture Validation"
|
|
echo ""
|
|
echo "| Version | Status | Log |"
|
|
echo "| --- | --- | --- |"
|
|
} > "${summary}"
|
|
|
|
while IFS=$'\t' read -r version asset expected_sha256; do
|
|
[ -n "${version}" ] || continue
|
|
fixture_zip="${fixtures_dir}/${asset}"
|
|
profile_tree="${output_dir}/profiles-${version}"
|
|
log_path="${output_dir}/${version}.log"
|
|
|
|
asset_url="${FIXTURE_RELEASE_URL}/$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "${asset}")"
|
|
fetch "${asset_url}" "${fixture_zip}" || return 1
|
|
|
|
if [ -n "${expected_sha256}" ] && [ "${expected_sha256}" != "<sha256>" ]; then
|
|
actual_sha256="$(sha256_of "${fixture_zip}")"
|
|
if [ -z "${actual_sha256}" ]; then
|
|
msg "no sha256 tool available, skipping checksum of ${asset}"
|
|
elif [ "${actual_sha256}" != "${expected_sha256}" ]; then
|
|
# A cached zip can be stale or truncated; the release asset itself is immutable,
|
|
# so deleting it forces fetch to pull a fresh copy.
|
|
msg "checksum mismatch for ${asset}, re-downloading"
|
|
rm -f "${fixture_zip}"
|
|
fetch "${asset_url}" "${fixture_zip}" || return 1
|
|
actual_sha256="$(sha256_of "${fixture_zip}")"
|
|
[ "${actual_sha256}" = "${expected_sha256}" ] || { msg "${asset}: expected ${expected_sha256}, got ${actual_sha256}"; return 1; }
|
|
fi
|
|
fi
|
|
|
|
msg "validating custom presets from ${version} ..."
|
|
rm -rf "${profile_tree}"
|
|
mkdir -p "${profile_tree}" || return 1
|
|
if [ -n "${VENDOR}" ]; then
|
|
# -v has the validator open this vendor and OrcaFilamentLibrary and skip the rest
|
|
# (PresetBundle::load_system_presets_from_json), and unzip's * spans '/', so one
|
|
# pattern reaches every preset type. Copying and unpacking only those turns a ~5s
|
|
# setup per fixture into ~0.5s. Naming the vendor twice, when it is the library
|
|
# itself, is not an error to cp; exit 11 is unzip's "nothing matched", which is the
|
|
# fixture-predates-the-vendor case the warning below reports.
|
|
cp -a "${PROFILES_DIR}"/*.json "${PROFILES_DIR}/${VENDOR}" \
|
|
"${PROFILES_DIR}/OrcaFilamentLibrary" "${profile_tree}/" || return 1
|
|
unzip -q "${fixture_zip}" "user/*/${vendor_prefix}*" -d "${profile_tree}"
|
|
result=$?
|
|
[ "${result}" -eq 0 ] || [ "${result}" -eq 11 ] || return 1
|
|
kept="$(find "${profile_tree}/user" -type f 2>/dev/null | wc -l | tr -d ' ')"
|
|
total_kept=$((total_kept + kept))
|
|
msg " ${kept} ${VENDOR} preset file(s)"
|
|
else
|
|
cp -a "${PROFILES_DIR}/." "${profile_tree}/" || return 1
|
|
rm -rf "${profile_tree}/user"
|
|
unzip -q "${fixture_zip}" -d "${profile_tree}" || return 1
|
|
fi
|
|
|
|
"${VALIDATOR}" -p "${profile_tree}" -v "${VENDOR}" -l "${LOG_LEVEL}" > "${log_path}" 2>&1
|
|
result=$?
|
|
|
|
if [ "${result}" -eq 0 ]; then
|
|
echo "| ${version} | PASS | ${version}.log |" >> "${summary}"
|
|
# Only failures are worth keeping; each tree is a full copy of resources/profiles.
|
|
rm -rf "${profile_tree}"
|
|
else
|
|
echo "| ${version} | FAIL | ${version}.log |" >> "${summary}"
|
|
failed_logs="${failed_logs}${log_path}"$'\n'
|
|
status=1
|
|
fi
|
|
done < "${fixtures_dir}/fixtures.tsv"
|
|
|
|
# A vendor added after the newest fixture was cut appears in none of them: nothing failed, but
|
|
# nothing was checked either.
|
|
if [ -n "${VENDOR}" ] && [ "${total_kept}" -eq 0 ]; then
|
|
msg "no ${VENDOR} presets in any fixture; validate_custom checked nothing"
|
|
fi
|
|
|
|
cat "${summary}"
|
|
if [ -n "${failed_logs}" ]; then
|
|
echo ""
|
|
echo "## Failed Fixture Logs"
|
|
while IFS= read -r log_path; do
|
|
[ -n "${log_path}" ] || continue
|
|
echo ""
|
|
echo "### $(basename "${log_path}" .log)"
|
|
echo '```'
|
|
head -c 12000 "${log_path}" || echo "No output captured"
|
|
echo '```'
|
|
done <<EOF
|
|
${failed_logs}
|
|
EOF
|
|
fi
|
|
|
|
return "${status}"
|
|
}
|
|
|
|
# Heading CI puts above this check's log in the PR comment.
|
|
comment_heading() {
|
|
case "$1" in
|
|
profile_tool) echo "### Profile Check Failed (orca_profile_tool.py)" ;;
|
|
version_bump) echo "### Profile Version Not Bumped" ;;
|
|
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" ;;
|
|
validate_custom) echo "### Custom Preset Validation Failed" ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------- run
|
|
|
|
run_check() {
|
|
local name="$1"
|
|
local log="${LOG_DIR}/${name}.log"
|
|
local result
|
|
printf '\n%s==> %s%s%s\n' "${C_BOLD}" "${name}" "${VENDOR:+ (${VENDOR})}" "${C_RESET}"
|
|
"check_${name}" 2>&1 | tee "${log}"
|
|
result="${PIPESTATUS[0]}"
|
|
if [ "${result}" -eq 0 ]; then
|
|
printf '%s %s passed%s\n' "${C_GREEN}" "${name}" "${C_RESET}"
|
|
RESULTS="${RESULTS}${name}"$'\t'"pass"$'\n'
|
|
else
|
|
printf '%s %s failed (exit %s)%s\n' "${C_RED}" "${name}" "${result}" "${C_RESET}"
|
|
RESULTS="${RESULTS}${name}"$'\t'"fail"$'\n'
|
|
fi
|
|
}
|
|
|
|
if wants validate_system || wants validate_slice || wants validate_filament_subtypes || wants validate_custom; then
|
|
resolve_validator
|
|
fi
|
|
|
|
# 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); naming the
|
|
# check explicitly still runs it. So is version_bump for a --profiles tree git cannot diff, such
|
|
# as a copy made to try a change on, or one with no main to diff against.
|
|
for check in "${ALL_CHECKS[@]}"; do
|
|
wants "${check}" || continue
|
|
if [ "${check}" = validate_slice ] && [ "${NAMED_CHECKS}" -eq 0 ] && [ -n "${VENDOR}" ] &&
|
|
[ ! -d "${PROFILES_DIR}/${VENDOR}/machine" ]; then
|
|
RESULTS="${RESULTS}${check}"$'\t'"skip"$'\t'"${VENDOR} ships no printers"$'\n'
|
|
elif [ "${check}" = version_bump ] && [ "${NAMED_CHECKS}" -eq 0 ] &&
|
|
why="$(version_bump_skip_reason)"; then
|
|
RESULTS="${RESULTS}${check}"$'\t'"skip"$'\t'"${why}"$'\n'
|
|
else
|
|
run_check "${check}"
|
|
fi
|
|
done
|
|
|
|
# Summary, plus the comment check_profiles_comment.yml would post when something fails.
|
|
failed=0
|
|
skipped=0
|
|
printf '\n%s==> summary%s\n' "${C_BOLD}" "${C_RESET}"
|
|
while IFS=$'\t' read -r name result why; do
|
|
[ -n "${name}" ] || continue
|
|
if [ "${result}" = "pass" ]; then
|
|
printf '%s PASS%s %s\n' "${C_GREEN}" "${C_RESET}" "${name}"
|
|
elif [ "${result}" = "skip" ]; then
|
|
printf '%s SKIP%s %s (%s)\n' "${C_BOLD}" "${C_RESET}" "${name}" "${why}"
|
|
skipped=1
|
|
else
|
|
printf '%s FAIL%s %s (%s)\n' "${C_RED}" "${C_RESET}" "${name}" "${LOG_DIR}/${name}.log"
|
|
failed=1
|
|
fi
|
|
done <<EOF
|
|
${RESULTS}
|
|
EOF
|
|
|
|
if [ "${failed}" -eq 0 ]; then
|
|
rm -f "${WORK_DIR}/pr_comment.md"
|
|
if [ "${skipped}" -eq 0 ]; then
|
|
printf '\n%sAll checks passed.%s Logs: %s\n' "${C_GREEN}" "${C_RESET}" "${LOG_DIR}"
|
|
else
|
|
printf '\n%sEvery check that ran passed%s, but CI runs the skipped ones too. Logs: %s\n' \
|
|
"${C_GREEN}" "${C_RESET}" "${LOG_DIR}"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
{
|
|
# Marker matched by check_profiles_comment.yml to delete prior comments.
|
|
echo "<!-- profile-validation-comment -->"
|
|
echo "## :x: Profile Validation Errors"
|
|
echo ""
|
|
while IFS=$'\t' read -r name result; do
|
|
[ "${result}" = "fail" ] || continue
|
|
comment_heading "${name}"
|
|
echo ""
|
|
echo '```'
|
|
head -c 30000 "${LOG_DIR}/${name}.log" || echo "No output captured"
|
|
echo '```'
|
|
echo ""
|
|
done <<INNER
|
|
${RESULTS}
|
|
INNER
|
|
echo "---"
|
|
# Single-quoted on purpose: the backticks below are markdown, not command substitution.
|
|
# shellcheck disable=SC2016
|
|
echo '*Fix the errors above and push a new commit. To reproduce this run locally: `scripts/check_profile.sh`, or `scripts\check_profile.bat` on Windows.*'
|
|
} > "${WORK_DIR}/pr_comment.md"
|
|
|
|
printf '\n%sOne or more profile checks failed.%s Logs: %s\n' "${C_RED}" "${C_RESET}" "${LOG_DIR}"
|
|
printf 'The comment CI would post: %s\n' "${WORK_DIR}/pr_comment.md"
|
|
exit 1
|