mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-09 18:27:00 +00:00
Add script to run full profile checks locally (#15496)
* Run the CI profile checks locally
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -52,3 +52,4 @@ internal_docs/
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.opc
|
*.opc
|
||||||
|
/.test/
|
||||||
|
|||||||
6
scripts/check_profile.bat
Normal file
6
scripts/check_profile.bat
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@echo off
|
||||||
|
REM Runs check_profile.ps1, the Windows twin of check_profile.sh, from cmd.
|
||||||
|
REM -ExecutionPolicy Bypass is needed because a Windows client defaults to Restricted,
|
||||||
|
REM which refuses to run a checked-out .ps1 at all.
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0check_profile.ps1" %*
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
564
scripts/check_profile.ps1
Normal file
564
scripts/check_profile.ps1
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Local twin of the "Check profiles" CI job (.github/workflows/check_profiles.yml), for Windows.
|
||||||
|
|
||||||
|
.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
|
||||||
|
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.
|
||||||
|
|
||||||
|
extra_json_check scripts/orca_extra_profile_check.py
|
||||||
|
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
|
||||||
|
|
||||||
|
Everything that has to be downloaded - the profile validator and the custom-preset fixture
|
||||||
|
archives - lands under <repo>\.test\check_profiles and is reused on the next run. That
|
||||||
|
directory 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.
|
||||||
|
|
||||||
|
x64 and ARM64 hosts are both supported. A locally built validator is chosen by the machine
|
||||||
|
type in its PE header rather than by the name of its build tree, so build\ (x64) and
|
||||||
|
build-arm64\ side by side resolve correctly; the published nightly is x64 only and runs
|
||||||
|
under emulation on ARM64.
|
||||||
|
|
||||||
|
.PARAMETER ProfilesDir
|
||||||
|
Profile tree to validate (default: resources\profiles). extra_json_check always looks at the
|
||||||
|
tree next to the script, so this only redirects the validator checks.
|
||||||
|
|
||||||
|
.PARAMETER Validator
|
||||||
|
OrcaSlicer_profile_validator.exe to use; also $env:ORCA_PROFILE_VALIDATOR. Default: the local
|
||||||
|
build*\ Release build (then RelWithDebInfo, MinSizeRel, Debug) for this architecture, else
|
||||||
|
the nightly release build is downloaded.
|
||||||
|
|
||||||
|
.PARAMETER Download
|
||||||
|
Ignore local builds and use the downloaded nightly validator.
|
||||||
|
|
||||||
|
.PARAMETER Refresh
|
||||||
|
Re-download the validator and fixtures instead of using the cache.
|
||||||
|
|
||||||
|
.PARAMETER WorkDir
|
||||||
|
Downloads, logs and fixture trees (default: .test\check_profiles). Point it somewhere short,
|
||||||
|
such as D:\t, if a fixture tree trips Windows' 260-character path limit.
|
||||||
|
|
||||||
|
.PARAMETER LogLevel
|
||||||
|
Validator log level (default: 2, as in CI).
|
||||||
|
|
||||||
|
.PARAMETER Checks
|
||||||
|
Checks to run, by name (default: all of them, in the order listed above).
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
scripts\check_profile.bat
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts\check_profile.ps1 validate_system validate_slice
|
||||||
|
#>
|
||||||
|
|
||||||
|
# PositionalBinding is off so that the check names are the only positional arguments; left on,
|
||||||
|
# a bare "validate_system" would bind to whichever named parameter came next in this block.
|
||||||
|
[CmdletBinding(PositionalBinding = $false)]
|
||||||
|
param(
|
||||||
|
[Alias('p')] [string] $ProfilesDir,
|
||||||
|
[string] $Validator,
|
||||||
|
[string] $WorkDir,
|
||||||
|
[Alias('l')] [int] $LogLevel = 2,
|
||||||
|
[switch] $Download,
|
||||||
|
[switch] $Refresh,
|
||||||
|
[Alias('h')] [switch] $Help,
|
||||||
|
[Parameter(Position = 0, ValueFromRemainingArguments = $true)] [string[]] $Checks
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Windows PowerShell 5.1 still negotiates TLS 1.0/1.1, which github.com refuses.
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol =
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
||||||
|
|
||||||
|
$ValidatorReleaseUrl = 'https://github.com/OrcaSlicer/OrcaSlicer/releases/download/nightly-builds'
|
||||||
|
$FixtureReleaseUrl = 'https://github.com/OrcaSlicer/OrcaSlicer-profile-validator/releases/download/fixture-archive'
|
||||||
|
|
||||||
|
$RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
|
||||||
|
# PROCESSOR_ARCHITECTURE reports the architecture of the *shell*, so a 32-bit PowerShell on a
|
||||||
|
# 64-bit OS says x86; ARCHITEW6432 is the machine's own in that case.
|
||||||
|
$HostArch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
|
||||||
|
$HostArch = switch ($HostArch) {
|
||||||
|
'ARM64' { 'arm64' }
|
||||||
|
'AMD64' { 'x64' }
|
||||||
|
default { 'x86' }
|
||||||
|
}
|
||||||
|
|
||||||
|
$AllChecks = @('extra_json_check', 'validate_system', 'validate_slice', 'validate_filament_subtypes', 'validate_custom')
|
||||||
|
|
||||||
|
$script:LogWriter = $null
|
||||||
|
$script:Python = ''
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------- helpers
|
||||||
|
|
||||||
|
function Die([string] $Message) {
|
||||||
|
Write-Host "check_profile.ps1: $Message" -ForegroundColor Red
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# UTF-8 without a BOM, so a log reads the same here as the artifact CI uploads.
|
||||||
|
function New-LogWriter([string] $Path) {
|
||||||
|
New-Object IO.StreamWriter($Path, $false, (New-Object Text.UTF8Encoding($false)))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Output of the check being run: shown, and kept in that check's log.
|
||||||
|
function Write-CheckLog([string] $Text) {
|
||||||
|
Write-Host $Text
|
||||||
|
if ($script:LogWriter) { $script:LogWriter.WriteLine($Text) }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Runs a program and returns its exit code, streaming stdout and stderr into the current check's
|
||||||
|
# log. -OutFile sends that output to a file of its own instead, silently.
|
||||||
|
function Invoke-Tool {
|
||||||
|
param([string] $Exe, [string[]] $Arguments, [string] $OutFile)
|
||||||
|
|
||||||
|
# Under 'Stop', 2>&1 turns every stderr line of a native command into a terminating error.
|
||||||
|
# Assigning the preference here scopes it to this function, so it undoes itself on return.
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
|
||||||
|
$writer = if ($OutFile) { New-LogWriter $OutFile } else { $null }
|
||||||
|
try {
|
||||||
|
& $Exe @Arguments 2>&1 | ForEach-Object {
|
||||||
|
if ($writer) { $writer.WriteLine("$_") } else { Write-CheckLog "$_" }
|
||||||
|
}
|
||||||
|
return $LASTEXITCODE
|
||||||
|
} catch {
|
||||||
|
if ($writer) { $writer.WriteLine("$_") } else { Write-CheckLog "$_" }
|
||||||
|
return 1
|
||||||
|
} finally {
|
||||||
|
if ($writer) { $writer.Dispose() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# The first $Limit characters of a log, as CI truncates them for the PR comment.
|
||||||
|
function Get-LogHead([string] $Path, [int] $Limit) {
|
||||||
|
$text = if (Test-Path -LiteralPath $Path) { [IO.File]::ReadAllText($Path) } else { '' }
|
||||||
|
if (-not $text) { return 'No output captured' }
|
||||||
|
if ($text.Length -gt $Limit) { return $text.Substring(0, $Limit) }
|
||||||
|
return $text
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------- arguments
|
||||||
|
|
||||||
|
if ($Help) { Get-Help $PSCommandPath -Detailed; exit 0 }
|
||||||
|
|
||||||
|
foreach ($name in $Checks) {
|
||||||
|
if ($name -like '-*') { Die "unknown option '$name' (try -Help)" }
|
||||||
|
if ($AllChecks -notcontains $name) { Die "unknown check '$name' (try -Help)" }
|
||||||
|
}
|
||||||
|
if (-not $Checks) { $Checks = $AllChecks }
|
||||||
|
|
||||||
|
if (-not $ProfilesDir) { $ProfilesDir = Join-Path $RepoRoot 'resources\profiles' }
|
||||||
|
if (-not (Test-Path -LiteralPath $ProfilesDir -PathType Container)) { Die "profile directory not found: $ProfilesDir" }
|
||||||
|
$ProfilesDir = (Resolve-Path -LiteralPath $ProfilesDir).Path
|
||||||
|
|
||||||
|
if (-not $WorkDir) { $WorkDir = Join-Path $RepoRoot '.test\check_profiles' }
|
||||||
|
$LogDir = Join-Path $WorkDir 'logs'
|
||||||
|
try { New-Item -ItemType Directory -Force -Path $LogDir | Out-Null } catch { Die "cannot create ${LogDir}: $_" }
|
||||||
|
$WorkDir = (Resolve-Path -LiteralPath $WorkDir).Path
|
||||||
|
$LogDir = Join-Path $WorkDir 'logs'
|
||||||
|
|
||||||
|
if (-not $Validator) { $Validator = $env:ORCA_PROFILE_VALIDATOR }
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- 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.
|
||||||
|
$script:StashedUserDir = ''
|
||||||
|
|
||||||
|
function Push-UserPresets {
|
||||||
|
$user = Join-Path $ProfilesDir 'user'
|
||||||
|
if (-not (Test-Path -LiteralPath $user -PathType Container)) { return }
|
||||||
|
$script:StashedUserDir = Join-Path $WorkDir "user-presets-$PID"
|
||||||
|
Remove-Item -LiteralPath $script:StashedUserDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
try { Move-Item -LiteralPath $user -Destination $script:StashedUserDir }
|
||||||
|
catch { $script:StashedUserDir = ''; Die "cannot move $user aside: $_" }
|
||||||
|
Write-Host "moved $user aside for the run (restored on exit)"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Pop-UserPresets {
|
||||||
|
# The validator leaves an empty user\default\{filament,machine,process} skeleton behind.
|
||||||
|
# Prune it directory by directory, never wholesale: one 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.
|
||||||
|
$user = Join-Path $ProfilesDir 'user'
|
||||||
|
Remove-EmptyDirs $user
|
||||||
|
if (-not $script:StashedUserDir) { return }
|
||||||
|
if (Test-Path -LiteralPath $user) {
|
||||||
|
Write-Host "$user is not empty; your presets stay in $($script:StashedUserDir)"
|
||||||
|
} else {
|
||||||
|
Move-Item -LiteralPath $script:StashedUserDir -Destination $user
|
||||||
|
}
|
||||||
|
$script:StashedUserDir = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-EmptyDirs([string] $Path) {
|
||||||
|
if (-not (Test-Path -LiteralPath $Path -PathType Container)) { return }
|
||||||
|
Get-ChildItem -LiteralPath $Path -Recurse -Directory -Force |
|
||||||
|
Sort-Object { $_.FullName.Length } -Descending |
|
||||||
|
ForEach-Object {
|
||||||
|
if (-not (Get-ChildItem -LiteralPath $_.FullName -Force)) {
|
||||||
|
Remove-Item -LiteralPath $_.FullName -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not (Get-ChildItem -LiteralPath $Path -Force)) { Remove-Item -LiteralPath $Path -Force }
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------- downloads
|
||||||
|
|
||||||
|
# Cached; -Refresh, or -Force, pulls a new copy. 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.
|
||||||
|
function Save-Asset {
|
||||||
|
param([string] $Url, [string] $Dest, [switch] $Force)
|
||||||
|
|
||||||
|
if (-not $Refresh -and -not $Force -and (Test-Path -LiteralPath $Dest -PathType Leaf) -and
|
||||||
|
(Get-Item -LiteralPath $Dest).Length -gt 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Dest) | Out-Null
|
||||||
|
Write-Host "downloading $(Split-Path -Leaf $Dest) ..."
|
||||||
|
|
||||||
|
# Invoke-WebRequest spends most of a large download repainting its progress bar.
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
$part = "$Dest.part"
|
||||||
|
for ($attempt = 1; ; $attempt++) {
|
||||||
|
try {
|
||||||
|
Invoke-WebRequest -Uri $Url -OutFile $part -UseBasicParsing
|
||||||
|
break
|
||||||
|
} catch {
|
||||||
|
if ($attempt -ge 3) {
|
||||||
|
Remove-Item -LiteralPath $part -Force -ErrorAction SilentlyContinue
|
||||||
|
throw "download failed: $Url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Move-Item -LiteralPath $part -Destination $Dest -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-Sha256([string] $Path) {
|
||||||
|
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
# Directories a built validator can sit in, best first: build_win.bat names its trees build,
|
||||||
|
# build-dbginfo, build-minsize and build-dbg, each optionally -clang and -arm64 suffixed, and
|
||||||
|
# CMake puts the binary in src\<config>. Each pattern is matched both directly under a build root
|
||||||
|
# and one level down (build\x64), for trees laid out the way the macOS ones are.
|
||||||
|
function Get-ValidatorSearchDirs {
|
||||||
|
foreach ($config in 'Release', 'RelWithDebInfo', 'MinSizeRel', 'Debug') {
|
||||||
|
Join-Path $RepoRoot "build*\src\$config"
|
||||||
|
Join-Path $RepoRoot "build*\*\src\$config"
|
||||||
|
}
|
||||||
|
Join-Path $RepoRoot 'build*\src'
|
||||||
|
Join-Path $RepoRoot 'build*\*\src'
|
||||||
|
}
|
||||||
|
|
||||||
|
# The machine type from the PE header, which is what actually decides whether an .exe can run
|
||||||
|
# here - the name of the build tree only says what it was meant to be.
|
||||||
|
function Get-ExeArch([string] $Path) {
|
||||||
|
try {
|
||||||
|
$stream = [IO.File]::OpenRead($Path)
|
||||||
|
try {
|
||||||
|
$reader = New-Object IO.BinaryReader($stream)
|
||||||
|
$stream.Position = 0x3C # e_lfanew: offset of the PE header
|
||||||
|
$stream.Position = $reader.ReadInt32()
|
||||||
|
if ($reader.ReadUInt32() -ne 0x00004550) { return '' } # "PE\0\0"
|
||||||
|
switch ($reader.ReadUInt16()) {
|
||||||
|
0x8664 { 'x64' }
|
||||||
|
0xAA64 { 'arm64' }
|
||||||
|
0x014C { 'x86' }
|
||||||
|
default { '' }
|
||||||
|
}
|
||||||
|
} finally { $stream.Dispose() }
|
||||||
|
} catch { '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
# First locally built OrcaSlicer_profile_validator.exe, in the order above. An x86 build, and on
|
||||||
|
# ARM64 an x64 one, is kept only as a fallback: it runs, but emulated. An ARM64 build on an x64
|
||||||
|
# host does not run at all and is never offered.
|
||||||
|
function Find-LocalValidator {
|
||||||
|
$emulated = ''
|
||||||
|
foreach ($pattern in (Get-ValidatorSearchDirs)) {
|
||||||
|
foreach ($dir in @(Resolve-Path -Path $pattern -ErrorAction SilentlyContinue)) {
|
||||||
|
$candidate = Join-Path $dir.Path 'OrcaSlicer_profile_validator.exe'
|
||||||
|
if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) { continue }
|
||||||
|
$arch = Get-ExeArch $candidate
|
||||||
|
if (-not $arch -or $arch -eq $HostArch) { return $candidate }
|
||||||
|
if (-not $emulated -and ($arch -eq 'x86' -or $HostArch -eq 'arm64')) { $emulated = $candidate }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($emulated) { Write-Host "no $HostArch build found, falling back to $emulated (emulated)" }
|
||||||
|
return $emulated
|
||||||
|
}
|
||||||
|
|
||||||
|
# The nightly release build, same one CI uses. Windows ships the bare .exe, x64 only.
|
||||||
|
function Save-NightlyValidator {
|
||||||
|
if ($HostArch -ne 'x64') {
|
||||||
|
Write-Host "the nightly Windows validator is x64; it runs here under emulation"
|
||||||
|
}
|
||||||
|
$exe = Join-Path $WorkDir 'validator\OrcaSlicer_profile_validator.exe'
|
||||||
|
Save-Asset -Url "$ValidatorReleaseUrl/OrcaSlicer_profile_validator_Windows_nightly.exe" -Dest $exe
|
||||||
|
return $exe
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-Validator {
|
||||||
|
if ($Validator) {
|
||||||
|
if (-not (Test-Path -LiteralPath $Validator -PathType Leaf)) { Die "validator not found: $Validator" }
|
||||||
|
return (Resolve-Path -LiteralPath $Validator).Path
|
||||||
|
}
|
||||||
|
if (-not $Download) {
|
||||||
|
$local = Find-LocalValidator
|
||||||
|
if ($local) {
|
||||||
|
Write-Host "using locally built validator: $local"
|
||||||
|
return $local
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try { $downloaded = Save-NightlyValidator } catch { Die "could not obtain a profile validator: $_" }
|
||||||
|
Write-Host "using downloaded validator: $downloaded"
|
||||||
|
return $downloaded
|
||||||
|
}
|
||||||
|
|
||||||
|
# python3 is rarely on PATH on Windows: the py launcher is the reliable way in, and a bare
|
||||||
|
# `python` may be the Store stub, which prints an advert and exits non-zero. Probe each for the
|
||||||
|
# interpreter it actually resolves to, and use that.
|
||||||
|
function Resolve-Python {
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
if ($script:Python) { return $script:Python }
|
||||||
|
foreach ($candidate in 'py -3', 'python', 'python3') {
|
||||||
|
$words = $candidate -split ' '
|
||||||
|
$exe = Get-Command $words[0] -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
if (-not $exe) { continue }
|
||||||
|
$leading = @($words | Select-Object -Skip 1)
|
||||||
|
$found = & $exe.Source @leading -c 'import sys; print(sys.executable)' 2>$null
|
||||||
|
if ($LASTEXITCODE -eq 0 -and $found) {
|
||||||
|
$script:Python = "$found"
|
||||||
|
return $script:Python
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Die 'no Python 3 found; install it (or the py launcher) and re-run'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------- checks
|
||||||
|
|
||||||
|
$CheckBodies = @{
|
||||||
|
|
||||||
|
extra_json_check = {
|
||||||
|
Invoke-Tool -Exe (Resolve-Python) -Arguments @((Join-Path $RepoRoot 'scripts\orca_extra_profile_check.py'))
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_system = {
|
||||||
|
Invoke-Tool -Exe $Validator -Arguments @('-p', $ProfilesDir, '-l', "$LogLevel")
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
validate_slice = {
|
||||||
|
Invoke-Tool -Exe $Validator -Arguments @('-p', $ProfilesDir, '-s', '-l', "$LogLevel")
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_filament_subtypes = {
|
||||||
|
Invoke-Tool -Exe $Validator -Arguments @('-p', $ProfilesDir, '-l', "$LogLevel", '-f')
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
validate_custom = {
|
||||||
|
$fixturesDir = Join-Path $WorkDir 'profile-fixtures'
|
||||||
|
$outputDir = Join-Path $WorkDir 'custom-preset-validation'
|
||||||
|
New-Item -ItemType Directory -Force -Path $fixturesDir, $outputDir | Out-Null
|
||||||
|
|
||||||
|
$manifestPath = Join-Path $fixturesDir 'manifest.json'
|
||||||
|
Save-Asset -Url "$FixtureReleaseUrl/manifest.json" -Dest $manifestPath -Force
|
||||||
|
|
||||||
|
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
||||||
|
$entries = if ($manifest -is [array]) { $manifest } else { $manifest.fixtures }
|
||||||
|
$fixtures = @($entries | Where-Object { $_.version -and $_.asset })
|
||||||
|
if (-not $fixtures) {
|
||||||
|
Write-CheckLog "No custom preset fixtures found in $FixtureReleaseUrl/manifest.json"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = 0
|
||||||
|
$failedLogs = @()
|
||||||
|
$summary = @('## Custom Preset Fixture Validation', '', '| Version | Status | Log |', '| --- | --- | --- |')
|
||||||
|
|
||||||
|
foreach ($fixture in $fixtures) {
|
||||||
|
$version = $fixture.version
|
||||||
|
$asset = $fixture.asset
|
||||||
|
$fixtureZip = Join-Path $fixturesDir $asset
|
||||||
|
$profileTree = Join-Path $outputDir "profiles-$version"
|
||||||
|
$logPath = Join-Path $outputDir "$version.log"
|
||||||
|
$assetUrl = "$FixtureReleaseUrl/$([uri]::EscapeDataString($asset))"
|
||||||
|
|
||||||
|
Save-Asset -Url $assetUrl -Dest $fixtureZip
|
||||||
|
|
||||||
|
$expected = $fixture.asset_sha256
|
||||||
|
if ($expected -and $expected -ne '<sha256>' -and (Get-Sha256 $fixtureZip) -ne $expected.ToUpperInvariant()) {
|
||||||
|
# A cached zip can be stale or truncated; the release asset itself is immutable,
|
||||||
|
# so deleting it forces Save-Asset to pull a fresh copy.
|
||||||
|
Write-Host "checksum mismatch for $asset, re-downloading"
|
||||||
|
Remove-Item -LiteralPath $fixtureZip -Force
|
||||||
|
Save-Asset -Url $assetUrl -Dest $fixtureZip
|
||||||
|
$actual = Get-Sha256 $fixtureZip
|
||||||
|
if ($actual -ne $expected.ToUpperInvariant()) {
|
||||||
|
Write-CheckLog "${asset}: expected $expected, got $actual"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "validating custom presets from $version ..."
|
||||||
|
Remove-Item -LiteralPath $profileTree -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
New-Item -ItemType Directory -Force -Path $profileTree | Out-Null
|
||||||
|
# Piped rather than copied through <profiles>\*, so a vendor directory whose name
|
||||||
|
# holds a wildcard character is still copied by its literal path.
|
||||||
|
Get-ChildItem -LiteralPath $ProfilesDir -Force | Copy-Item -Destination $profileTree -Recurse -Force
|
||||||
|
Remove-Item -LiteralPath (Join-Path $profileTree 'user') -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Expand-Archive -LiteralPath $fixtureZip -DestinationPath $profileTree -Force
|
||||||
|
|
||||||
|
$result = Invoke-Tool -Exe $Validator -Arguments @('-p', $profileTree, '-l', "$LogLevel") -OutFile $logPath
|
||||||
|
if ($result -eq 0) {
|
||||||
|
$summary += "| $version | PASS | $version.log |"
|
||||||
|
# Only failures are worth keeping; each tree is a full copy of resources\profiles.
|
||||||
|
Remove-Item -LiteralPath $profileTree -Recurse -Force
|
||||||
|
} else {
|
||||||
|
$summary += "| $version | FAIL | $version.log |"
|
||||||
|
$failedLogs += $logPath
|
||||||
|
$status = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[IO.File]::WriteAllLines((Join-Path $outputDir 'summary.md'), [string[]] $summary)
|
||||||
|
$summary | ForEach-Object { Write-CheckLog $_ }
|
||||||
|
|
||||||
|
if ($failedLogs) {
|
||||||
|
Write-CheckLog ''
|
||||||
|
Write-CheckLog '## Failed Fixture Logs'
|
||||||
|
foreach ($logPath in $failedLogs) {
|
||||||
|
Write-CheckLog ''
|
||||||
|
Write-CheckLog "### $([IO.Path]::GetFileNameWithoutExtension($logPath))"
|
||||||
|
Write-CheckLog '```'
|
||||||
|
Write-CheckLog (Get-LogHead $logPath 12000)
|
||||||
|
Write-CheckLog '```'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Heading CI puts above this check's log in the PR comment.
|
||||||
|
$CommentHeadings = @{
|
||||||
|
extra_json_check = '### Extra JSON Check Failed'
|
||||||
|
validate_system = '### System Profile Validation Failed'
|
||||||
|
validate_slice = '### Slice Validation Failed (custom g-code expansion)'
|
||||||
|
validate_filament_subtypes = '### Filament Subtype Validation Failed'
|
||||||
|
validate_custom = '### Custom Preset Validation Failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------- run
|
||||||
|
|
||||||
|
function Invoke-Check([string] $Name) {
|
||||||
|
$log = Join-Path $LogDir "$Name.log"
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host "==> $Name" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$script:LogWriter = New-LogWriter $log
|
||||||
|
try {
|
||||||
|
$result = & $CheckBodies[$Name] | Select-Object -Last 1
|
||||||
|
} catch {
|
||||||
|
Write-CheckLog "$_"
|
||||||
|
$result = 1
|
||||||
|
} finally {
|
||||||
|
$script:LogWriter.Dispose()
|
||||||
|
$script:LogWriter = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([int] $result -eq 0) {
|
||||||
|
Write-Host " $Name passed" -ForegroundColor Green
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
Write-Host " $Name failed (exit $result)" -ForegroundColor Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
$LockDir = Join-Path $WorkDir '.lock'
|
||||||
|
try { New-Item -ItemType Directory -Path $LockDir -ErrorAction Stop | Out-Null }
|
||||||
|
catch { Die "another run is using $WorkDir (pass -WorkDir, or remove $LockDir if no run is active)" }
|
||||||
|
|
||||||
|
# The validator writes UTF-8, but PowerShell decodes a child process's output using the console
|
||||||
|
# code page, which mangles the accented and CJK preset names in its messages.
|
||||||
|
$PreviousOutputEncoding = [Console]::OutputEncoding
|
||||||
|
|
||||||
|
try {
|
||||||
|
[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
|
||||||
|
Push-UserPresets
|
||||||
|
|
||||||
|
if ($Checks | Where-Object { $_ -ne 'extra_json_check' }) { $Validator = Resolve-Validator }
|
||||||
|
|
||||||
|
$results = [ordered] @{}
|
||||||
|
foreach ($name in $AllChecks) {
|
||||||
|
if ($Checks -contains $name) { $results[$name] = Invoke-Check $name }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '==> summary' -ForegroundColor Cyan
|
||||||
|
foreach ($name in $results.Keys) {
|
||||||
|
if ($results[$name]) {
|
||||||
|
Write-Host " PASS $name" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " FAIL $name ($(Join-Path $LogDir "$name.log"))" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$failed = @($results.Keys | Where-Object { -not $results[$_] })
|
||||||
|
if (-not $failed) {
|
||||||
|
Remove-Item -LiteralPath (Join-Path $WorkDir 'pr_comment.md') -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host "All checks passed. Logs: $LogDir" -ForegroundColor Green
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# The comment check_profiles_comment.yml would post when something fails.
|
||||||
|
$comment = @(
|
||||||
|
# Marker matched by check_profiles_comment.yml to delete prior comments.
|
||||||
|
'<!-- profile-validation-comment -->'
|
||||||
|
'## :x: Profile Validation Errors'
|
||||||
|
''
|
||||||
|
foreach ($name in $failed) {
|
||||||
|
$CommentHeadings[$name]
|
||||||
|
''
|
||||||
|
'```'
|
||||||
|
Get-LogHead (Join-Path $LogDir "$name.log") 30000
|
||||||
|
'```'
|
||||||
|
''
|
||||||
|
}
|
||||||
|
'---'
|
||||||
|
'*Please fix the above errors and push a new commit.*'
|
||||||
|
)
|
||||||
|
$commentPath = Join-Path $WorkDir 'pr_comment.md'
|
||||||
|
[IO.File]::WriteAllLines($commentPath, [string[]] $comment)
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host "One or more profile checks failed. Logs: $LogDir" -ForegroundColor Red
|
||||||
|
Write-Host "The comment CI would post: $commentPath"
|
||||||
|
exit 1
|
||||||
|
} finally {
|
||||||
|
Pop-UserPresets
|
||||||
|
Remove-Item -LiteralPath $LockDir -Force -ErrorAction SilentlyContinue
|
||||||
|
[Console]::OutputEncoding = $PreviousOutputEncoding
|
||||||
|
}
|
||||||
527
scripts/check_profile.sh
Executable file
527
scripts/check_profile.sh
Executable file
@@ -0,0 +1,527 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# 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 <repo>/.test/check_profiles/ and is reused on the next run. That
|
||||||
|
# directory 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.
|
||||||
|
#
|
||||||
|
# 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)"
|
||||||
|
|
||||||
|
PROFILES_DIR="${REPO_ROOT}/resources/profiles"
|
||||||
|
WORK_DIR="${REPO_ROOT}/.test/check_profiles"
|
||||||
|
VALIDATOR="${ORCA_PROFILE_VALIDATOR:-}"
|
||||||
|
LOG_LEVEL=2
|
||||||
|
PREFER_DOWNLOAD=0
|
||||||
|
REFRESH=0
|
||||||
|
|
||||||
|
ALL_CHECKS=(extra_json_check validate_system validate_slice validate_filament_subtypes validate_custom)
|
||||||
|
CHECKS=()
|
||||||
|
# "<check><TAB>pass|fail" per check that ran; 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):
|
||||||
|
extra_json_check scripts/orca_extra_profile_check.py
|
||||||
|
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)
|
||||||
|
--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: .test/check_profiles)
|
||||||
|
-l, --log-level N validator log level (default: ${LOG_LEVEL}, as in CI)
|
||||||
|
-h, --help show this help
|
||||||
|
|
||||||
|
Note: extra_json_check always looks at the tree next to the script
|
||||||
|
(<repo>/resources/profiles); --profiles only redirects the validator checks.
|
||||||
|
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 ;;
|
||||||
|
--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
|
||||||
|
|
||||||
|
[ "${#CHECKS[@]}" -gt 0 ] || CHECKS=("${ALL_CHECKS[@]}")
|
||||||
|
|
||||||
|
[ -d "${PROFILES_DIR}" ] || die "profile directory not found: ${PROFILES_DIR}"
|
||||||
|
PROFILES_DIR="$(cd -- "${PROFILES_DIR}" && pwd)"
|
||||||
|
|
||||||
|
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 "$(uname -s)" 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
|
||||||
|
;;
|
||||||
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
|
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 $(uname -s); 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_extra_json_check() {
|
||||||
|
python3 "${REPO_ROOT}/scripts/orca_extra_profile_check.py"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_validate_system() {
|
||||||
|
"${VALIDATOR}" -p "${PROFILES_DIR}" -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}" -s -l "${LOG_LEVEL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_validate_filament_subtypes() {
|
||||||
|
"${VALIDATOR}" -p "${PROFILES_DIR}" -l "${LOG_LEVEL}" -f
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
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=""
|
||||||
|
local version asset expected_sha256 asset_url fixture_zip profile_tree log_path actual_sha256 result
|
||||||
|
|
||||||
|
command -v unzip >/dev/null 2>&1 || { msg "unzip is required for validate_custom"; return 1; }
|
||||||
|
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
|
||||||
|
cp -a "${PROFILES_DIR}/." "${profile_tree}/" || return 1
|
||||||
|
rm -rf "${profile_tree}/user"
|
||||||
|
unzip -q "${fixture_zip}" -d "${profile_tree}" || return 1
|
||||||
|
|
||||||
|
"${VALIDATOR}" -p "${profile_tree}" -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"
|
||||||
|
|
||||||
|
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
|
||||||
|
extra_json_check) echo "### Extra JSON Check Failed" ;;
|
||||||
|
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\n' "${C_BOLD}" "${name}" "${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
|
||||||
|
|
||||||
|
for check in "${ALL_CHECKS[@]}"; do
|
||||||
|
wants "${check}" && run_check "${check}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Summary, plus the comment check_profiles_comment.yml would post when something fails.
|
||||||
|
failed=0
|
||||||
|
printf '\n%s==> summary%s\n' "${C_BOLD}" "${C_RESET}"
|
||||||
|
while IFS=$'\t' read -r name result; do
|
||||||
|
[ -n "${name}" ] || continue
|
||||||
|
if [ "${result}" = "pass" ]; then
|
||||||
|
printf '%s PASS%s %s\n' "${C_GREEN}" "${C_RESET}" "${name}"
|
||||||
|
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"
|
||||||
|
printf '\n%sAll checks passed.%s Logs: %s\n' "${C_GREEN}" "${C_RESET}" "${LOG_DIR}"
|
||||||
|
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 "---"
|
||||||
|
echo "*Please fix the above errors and push a new commit.*"
|
||||||
|
} > "${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
|
||||||
Reference in New Issue
Block a user