<# .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. profile_tool scripts/orca_profile_tool.py check validate_system validator -p -l validate_slice validator -p -s -l validate_filament_subtypes validator -p -l -f validate_custom validator against every released custom-preset fixture profile_tool is the only check that is not the validator binary; it makes the static checks the validator cannot, because the validator loads the tree the way the slicer does and so never sees a profile no .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 \.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. -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. 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). .PARAMETER Vendor Check only this vendor, named after its .json (e.g. "Co Print"). validate_custom is narrowed with it 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. .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 .EXAMPLE scripts\check_profile.bat -Vendor Elegoo #> # 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, [Alias('v')] [string] $Vendor, [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 = @('profile_tool', '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)" } } # A check named on the command line always runs; only the default set is narrowed (see the run # loop below). $NamedChecks = [bool] $Checks 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 # 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 Test-Path - the # validator compares the .json stem case-sensitively, while Windows' case-insensitive # filesystem would let "creality" pass a path test and then match no vendor. A vendor is a # .json with a sibling directory; that pair is also what tells one apart from # blacklist.json, which sits in the same folder. if ($Vendor) { $found = $false $suggestion = '' foreach ($file in (Get-ChildItem -LiteralPath $ProfilesDir -Filter '*.json' -File)) { $name = [IO.Path]::GetFileNameWithoutExtension($file.Name) if (-not (Test-Path -LiteralPath (Join-Path $ProfilesDir $name) -PathType Container)) { continue } if ($name -ceq $Vendor) { $found = $true; break } if ($name -eq $Vendor) { $suggestion = $name } } if (-not $found) { if ($suggestion) { Die "unknown vendor '$Vendor'; vendor names are case-sensitive, did you mean '$suggestion'?" } Die "unknown vendor '$Vendor': no such vendor in $ProfilesDir" } } # The validator's -v and orca_profile_tool.py check's --vendor both take that stem; an unscoped # run passes neither, so the checks below splat these in either way. $VendorArgs = if ($Vendor) { @('-v', $Vendor) } else { @() } $VendorPyArgs = if ($Vendor) { @('--vendor', $Vendor) } else { @() } 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 - # \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\. 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' } # The fixtures name every preset "__orca_test", after the "name" inside # .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. function Get-VendorDisplayName { $name = '' try { $name = (Get-Content -LiteralPath (Join-Path $ProfilesDir "$Vendor.json") -Raw -ErrorAction Stop | ConvertFrom-Json).name } catch { } if ($name) { return "$name" } return $Vendor } # Unpack just the presets one vendor's profiles own, and return how many that was. Each fixture # preset was generated from a single system preset and inherits it by name - none inherits another # user preset - so the selection is self-contained. function Expand-VendorPresets([string] $Zip, [string] $Tree, [string] $Prefix) { # .NET rather than Expand-Archive: a fixture holds around 20,000 entries, and unpacking every # one of them to keep a twentieth costs more than the validation this is speeding up. Add-Type -AssemblyName System.IO.Compression.FileSystem $kept = 0 $archive = [IO.Compression.ZipFile]::OpenRead($Zip) try { foreach ($entry in $archive.Entries) { # A directory entry has an empty Name, so it never matches and is never created. if (-not $entry.Name.StartsWith($Prefix, [StringComparison]::Ordinal)) { continue } $dest = Join-Path $Tree $entry.FullName.Replace('/', [IO.Path]::DirectorySeparatorChar) [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($dest)) | Out-Null [IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $dest, $true) $kept++ } } finally { $archive.Dispose() } return $kept } # ---------------------------------------------------------------------------- checks $CheckBodies = @{ profile_tool = { Invoke-Tool -Exe (Resolve-Python) -Arguments (@((Join-Path $RepoRoot 'scripts\orca_profile_tool.py'), 'check', '--profiles', $ProfilesDir) + $VendorPyArgs) } validate_system = { Invoke-Tool -Exe $Validator -Arguments (@('-p', $ProfilesDir) + $VendorArgs + @('-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) + $VendorArgs + @('-s', '-l', "$LogLevel")) } validate_filament_subtypes = { Invoke-Tool -Exe $Validator -Arguments (@('-p', $ProfilesDir) + $VendorArgs + @('-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. # # 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 "_" # 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. 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 } $vendorPrefix = if ($Vendor) { "$(Get-VendorDisplayName)_" } else { '' } $totalKept = 0 $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 '' -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 \*, so a vendor directory whose name # holds a wildcard character is still copied by its literal path. Under -Vendor the # validator opens only that vendor and OrcaFilamentLibrary and skips the other sixty-odd # (PresetBundle::load_system_presets_from_json), so the rest are left out of the copy: # ~3s a fixture that was going on files nothing opens. Get-ChildItem -LiteralPath $ProfilesDir -Force | Where-Object { -not $Vendor -or -not $_.PSIsContainer -or $_.Name -eq $Vendor -or $_.Name -eq 'OrcaFilamentLibrary' } | Copy-Item -Destination $profileTree -Recurse -Force Remove-Item -LiteralPath (Join-Path $profileTree 'user') -Recurse -Force -ErrorAction SilentlyContinue if ($Vendor) { $kept = Expand-VendorPresets $fixtureZip $profileTree $vendorPrefix $totalKept += $kept Write-CheckLog " $kept $Vendor preset file(s)" } else { Expand-Archive -LiteralPath $fixtureZip -DestinationPath $profileTree -Force } $result = Invoke-Tool -Exe $Validator -Arguments (@('-p', $profileTree) + $VendorArgs + @('-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 } } # A vendor added after the newest fixture was cut appears in none of them: nothing failed, # but nothing was checked either. if ($Vendor -and $totalKept -eq 0) { Write-CheckLog "no $Vendor presets in any fixture; validate_custom checked nothing" } [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 = @{ profile_tool = '### Profile Check Failed (orca_profile_tool.py)' 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$(if ($Vendor) { " ($Vendor)" })" -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 'profile_tool' }) { $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); # naming the check explicitly still runs it. $results = [ordered] @{} $skipReasons = @{} foreach ($name in $AllChecks) { if ($Checks -notcontains $name) { continue } if ($name -eq 'validate_slice' -and -not $NamedChecks -and $Vendor -and -not (Test-Path -LiteralPath (Join-Path (Join-Path $ProfilesDir $Vendor) 'machine') -PathType Container)) { $results[$name] = 'skip' $skipReasons[$name] = "$Vendor ships no printers" } else { $results[$name] = if (Invoke-Check $name) { 'pass' } else { 'fail' } } } Write-Host '' Write-Host '==> summary' -ForegroundColor Cyan foreach ($name in $results.Keys) { switch ($results[$name]) { 'pass' { Write-Host " PASS $name" -ForegroundColor Green } 'skip' { Write-Host " SKIP $name ($($skipReasons[$name]))" -ForegroundColor Yellow } default { Write-Host " FAIL $name ($(Join-Path $LogDir "$name.log"))" -ForegroundColor Red } } } $failed = @($results.Keys | Where-Object { $results[$_] -eq 'fail' }) if (-not $failed) { Remove-Item -LiteralPath (Join-Path $WorkDir 'pr_comment.md') -Force -ErrorAction SilentlyContinue Write-Host '' if (@($results.Values) -contains 'skip') { Write-Host "Every check that ran passed, but CI runs the skipped ones too. Logs: $LogDir" -ForegroundColor Green } else { 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. '' '## :x: Profile Validation Errors' '' foreach ($name in $failed) { $CommentHeadings[$name] '' '```' Get-LogHead (Join-Path $LogDir "$name.log") 30000 '```' '' } '---' '*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.*' ) $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 }