diff --git a/.gitattributes b/.gitattributes index 441bdfe1eb..472a0b2d1f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,8 @@ # with CRLF line endings: it fails on the first line. Windows checkouts default # to core.autocrlf=true, so keep these LF whatever the platform. *.sh text eol=lf + +# Batch files are read by cmd.exe, which tracks a byte offset into the file to +# resume after `call :label`. With LF endings that offset can land wrong and the +# label lookup fails, so keep these CRLF whatever the platform. +*.bat text eol=crlf diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index 3de2a9184b..d991bedca6 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -33,6 +33,8 @@ on: - 'build_linux.sh' - 'build_release_vs.bat' - 'build_release_vs2022.bat' + - 'build_win.bat' + - 'scripts/test_build_win.ps1' - 'build_release_macos.sh' - 'scripts/build_preset_cache.*' - 'scripts/flatpak/**' @@ -56,6 +58,22 @@ concurrency: jobs: + # build_win.bat ships a test suite. Run it before the Windows builds. + check_build_script: + name: Windows build script tests + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + lfs: 'false' + + # Windows PowerShell rather than pwsh: the suite drives build_win.bat + # through cmd, and the two differ in how they quote native arguments. + - name: Run the build script test suite + shell: powershell + run: .\scripts\test_build_win.ps1 + build_linux: strategy: fail-fast: false @@ -81,14 +99,16 @@ jobs: # SELF_HOSTED skips arm64 (the self-hosted Windows server is x64-only). matrix: include: ${{ fromJSON(vars.SELF_HOSTED - && '[{"arch":"x64","os":"orca-win-server"}]' - || '[{"arch":"x64","os":"windows-latest"},{"arch":"arm64","os":"windows-11-arm"}]') }} + && '[{"arch":"x64","os":"orca-win-server","compiler":"clang"}]' + || '[{"arch":"x64","os":"windows-latest","compiler":"clang"},{"arch":"arm64","os":"windows-11-arm","compiler":"clang"}]') }} + needs: check_build_script # Don't run scheduled builds on forks: - if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }} + if: ${{ !cancelled() && needs.check_build_script.result == 'success' && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }} uses: ./.github/workflows/build_check_cache.yml with: os: ${{ matrix.os }} arch: ${{ matrix.arch }} + compiler: ${{ matrix.compiler }} build-deps-only: ${{ inputs.build-deps-only || false }} force-build: ${{ github.event_name == 'schedule' }} secrets: inherit diff --git a/.github/workflows/build_check_cache.yml b/.github/workflows/build_check_cache.yml index b5c5b28b95..91ea51ac60 100644 --- a/.github/workflows/build_check_cache.yml +++ b/.github/workflows/build_check_cache.yml @@ -9,6 +9,10 @@ on: arch: required: false type: string + compiler: + required: false + type: string + default: msvc build-deps-only: required: false type: boolean @@ -33,10 +37,10 @@ jobs: - name: set outputs id: set_outputs env: - # Keep macOS/Windows cache keys architecture-specific. amd64 Linux passes - # no arch (key stays 'linux-clang', preserving the existing cache); - # aarch64 gets its own 'linux-clang-aarch64' key. - cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}', inputs.arch) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }} + # Anything that changes how the tree is built belongs in the key, or a job + # restores one it cannot use. Linux amd64 passes no arch deliberately, so + # 'linux-clang' keeps the cache it already has. + cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }} # ARM64 builds use the build-arm64 tree (see build_release_vs.bat); x64/other use build. dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }} output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}} @@ -62,6 +66,7 @@ jobs: valid-cache: ${{ needs.check_cache.outputs.valid-cache == 'true' }} os: ${{ inputs.os }} arch: ${{ inputs.arch }} + compiler: ${{ inputs.compiler }} build-deps-only: ${{ inputs.build-deps-only }} force-build: ${{ inputs.force-build }} secrets: inherit diff --git a/.github/workflows/build_deps.yml b/.github/workflows/build_deps.yml index f9e5c57c34..f7082a8a06 100644 --- a/.github/workflows/build_deps.yml +++ b/.github/workflows/build_deps.yml @@ -16,6 +16,10 @@ on: arch: required: false type: string + compiler: + required: false + type: string + default: msvc build-deps-only: required: false type: boolean @@ -135,11 +139,22 @@ jobs: choco install strawberryperl } $arch = "${{ inputs.arch }}" + # -l selects clang-cl and -x Ninja; together they build the deps with clang. + $clang = "${{ inputs.compiler }}" -eq "clang" + $flags = if ($clang) { "-l", "-x" } else { @() } + if ($clang) { + # OpenSSL builds with nmake, which needs a VC environment. + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $vs = & $vswhere -latest -property installationPath + $devArch = if ($arch -eq "arm64") { "arm64" } else { "amd64" } + Import-Module "$vs\Common7\Tools\Microsoft.VisualStudio.DevShell.dll" + Enter-VsDevShell -VsInstallPath $vs -SkipAutomaticLocation -DevCmdArguments "-arch=$devArch" + } if ($arch -eq "arm64") { - .\build_release_vs.bat deps arm64 + .\build_release_vs.bat deps arm64 @flags .\build_release_vs.bat pack arm64 } else { - .\build_release_vs.bat deps + .\build_release_vs.bat deps @flags .\build_release_vs.bat pack } shell: pwsh @@ -204,4 +219,5 @@ jobs: cache-path: ${{ inputs.cache-path }} os: ${{ inputs.os }} arch: ${{ inputs.arch }} + compiler: ${{ inputs.compiler }} secrets: inherit diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index a9efaa7561..8e1e28db13 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -13,6 +13,10 @@ on: arch: required: false type: string + compiler: + required: false + type: string + default: msvc macos-combine-only: required: false type: boolean @@ -395,7 +399,18 @@ jobs: # "tests" builds the unit tests too; the unit_tests_windows_* jobs run them. run: | $arch = "${{ inputs.arch }}" - if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 tests } else { .\build_release_vs.bat slicer tests } + # -l selects clang-cl and -x Ninja; together they build the slicer with clang. + $clang = "${{ inputs.compiler }}" -eq "clang" + $flags = if ($clang) { "-l", "-x" } else { @() } + if ($clang) { + # Build against the same VC toolchain and SDK as the dependencies. + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $vs = & $vswhere -latest -property installationPath + $devArch = if ($arch -eq "arm64") { "arm64" } else { "amd64" } + Import-Module "$vs\Common7\Tools\Microsoft.VisualStudio.DevShell.dll" + Enter-VsDevShell -VsInstallPath $vs -SkipAutomaticLocation -DevCmdArguments "-arch=$devArch" + } + if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 @flags tests } else { .\build_release_vs.bat slicer @flags tests } shell: pwsh - name: Build system preset cache (Windows) diff --git a/.gitignore b/.gitignore index cdcd1c90b4..d994d922c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ Build Build.bat /build*/ +/out/ CMakeLists.txt.user CMakeUserPresets.json **/CMakeLists.txt.autosave @@ -51,3 +52,4 @@ internal_docs/ __pycache__/ *.pyc *.opc +/.test/ diff --git a/build_win.bat b/build_win.bat new file mode 100644 index 0000000000..926468687b --- /dev/null +++ b/build_win.bat @@ -0,0 +1,1268 @@ +@echo off + +REM OrcaSlicer build script for Windows. Run with -h for the options. + +REM =========================================================================== +REM Script setup +REM =========================================================================== + +setlocal enableDelayedExpansion + +REM A manually set errorlevel shadows the real one, and cmd then stops +REM updating it after each command. Clear it, inside the scope above so +REM the caller keeps whatever it had. +set errorlevel= + +REM Everything here is relative to the repository root: deps/, build/ and +REM the 7z in tools/. Work from there whatever directory this was called +REM from. setlocal above restores the caller directory on exit. +cd /d "%~dp0" +set "WP=%CD%" +set "script_name=%~nx0" + +set argdefn=0 + +REM Both macros stop on a non-zero errorlevel; error_check also says so. +REM +REM error_check jumps to :die rather than calling exit /b where it stands. +REM Every use of it is inside a parenthesised block, and an exit /b there +REM ends the script but leaves the process exit code at zero, so a failed +REM build reported success. goto unwinds the block first. +set "repeat_error=if not ^!errorlevel^! == 0 exit /b ^!errorlevel^!" +set "error_check=if not ^!errorlevel^! == 0 (set rc=^!errorlevel^!& goto :die)" + +set VSWHERE="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" + +REM =========================================================================== +REM Command line options +REM =========================================================================== + +call :add_section "Actions" +call :add_arg build_deps bool d deps "Download and build the dependencies, needed before -s" +call :add_arg build_slicer bool s slicer "Build OrcaSlicer" +call :add_arg build_tests bool "" tests "Build the unit tests" +call :add_arg run_tests bool "" run-tests "Build the unit tests and run them" +call :add_arg pack_deps bool p pack "Bundle the built dependencies into a zip file" +call :add_arg install_deps bool u install-deps "Install or update CMake, Perl and Git with WinGet" +call :add_arg install_vs string "" install-vs "Also install Visual Studio: buildtools or ide" +call :add_arg kill_jobs bool k kill-jobs "Kill any running build and compiler processes" +call :add_arg print_help bool h help "Print this help message" + +call :add_section "Build configuration" +call :add_arg config string "" config "release, debug, relwithdebinfo or minsizerel (default: release)" +call :add_arg target_arch string "" arch "x64 or arm64 (default: the host architecture)" +call :add_arg slicer_asan bool a asan "Build the slicer with ASAN enabled" + +call :add_section "Toolchain" +call :add_arg use_clang_cl bool l clang-cl "Use clang-cl as the compiler" +call :add_arg use_msvc bool "" msvc "Use cl as the compiler (default)" +call :add_arg use_ninja bool x ninja "Use the Ninja Multi-Config generator" +call :add_arg use_msbuild bool "" msbuild "Use the Visual Studio generator (default)" +call :add_arg vs_version string "" vs "Visual Studio release: 2019, 2022 or 2026 (default: autodetect)" +call :add_arg clang_path string "" clang-path "Path to clang-cl.exe, requires -x (default: the one from Visual Studio)" + +call :add_section "How much gets rebuilt" +call :add_arg slicer_target string "" slicer-target "Build one slicer target instead of all, e.g. libslic3r" +call :add_arg deps_target string t deps-target "Build one dependency instead of all, e.g. dep_Boost" +call :add_arg no_configure bool "" no-configure "Build the existing tree without configuring" +call :add_arg no_gettext bool "" no-gettext "Skip regenerating the translations" +call :add_arg install_slicer bool i install "Install into the build tree's OrcaSlicer folder" +call :add_arg jobs string j jobs "Limit the build to N parallel jobs" +call :add_arg clean bool c clean "Remove the trees this run builds, deps with -d, slicer with -s" + +call :add_section "Paths and extra arguments" +call :add_arg deps_dir string "" deps-dir "Dependency tree to build in or use, instead of the one named for this build" +call :add_arg slicer_dir string "" build-dir "Slicer build directory, instead of the one named for this build" +call :add_arg deps_args rawstring "" deps-args "Extra arguments for the deps configure, quoted" +call :add_arg slicer_args rawstring "" slicer-args "Extra arguments for the slicer configure, quoted" + +call :add_section "Diagnostics" +call :add_arg verbose bool v verbose "Show the compiler command lines" +call :add_arg dry_run bool D dry-run "Print the commands instead of running them" + +REM =========================================================================== +REM Lookup tables +REM =========================================================================== + +REM Known build configurations: the CMake build type, the directory to build +REM in, and the dependency tree to build against. Adding one means adding all +REM three entries in its group. The name is matched case-insensitively because +REM batch variable names are, so --config Debug finds cfg_type_debug. +REM +REM Release, RelWithDebInfo and MinSizeRel all link the /MD dependencies, so +REM they share one tree rather than each paying for its own. Debug needs /MDd +REM and cannot. deps/CMakeLists.txt makes the same split: only Debug turns on +REM DEP_DEBUG, while RelWithDebInfo just adds debug info to a release build. +set "cfg_type_release=Release" +set "cfg_dir_release=build" +set "cfg_dep_release=build" + +set "cfg_type_debug=Debug" +set "cfg_dir_debug=build-dbg" +set "cfg_dep_debug=build-dbg" + +set "cfg_type_relwithdebinfo=RelWithDebInfo" +set "cfg_dir_relwithdebinfo=build-dbginfo" +set "cfg_dep_relwithdebinfo=build" + +set "cfg_type_minsizerel=MinSizeRel" +set "cfg_dir_minsizerel=build-minsize" +set "cfg_dep_minsizerel=build" + +set "cfg_default=release" + +REM Known Visual Studio releases: the generator name, the suffix in the +REM WinGet id, and the major version vswhere and msbuild report. Adding a +REM release means adding all three entries in its group. +set "vs_gen_2019=Visual Studio 16 2019" +set "vs_winget_2019=.2019" +set "vs_year_16=2019" + +set "vs_gen_2022=Visual Studio 17 2022" +set "vs_winget_2022=.2022" +set "vs_year_17=2022" + +REM 2026 is the unversioned WinGet id: there is no Microsoft.VisualStudio.2026. +set "vs_gen_2026=Visual Studio 18 2026" +set "vs_winget_2026=" +set "vs_year_18=2026" + +set "vs_default=2026" + +REM What --install-vs asks WinGet for. +set "vs_edition_buildtools=BuildTools" +set "vs_edition_ide=Community" + +REM =========================================================================== +REM Command line handling +REM =========================================================================== + +call :handle_args %* +%error_check% + +if "%debugscript%" == "ON" ( + set /A range_end = %argdefn% - 1 + for /L %%i in (0, 1, !range_end!) do ( + call :echo_var !argdefs[%%i].VARIABLE_NAME! + ) +) + +if "%~1" == "" ( + set print_help=ON + goto :before_print_help +) + +set "all_args=%*" +if "%all_args:"=%" == "" ( + set print_help=ON +) + +:before_print_help + +if "%print_help%" == "ON" ( + call :print_help_msg + exit /b 0 +) + +REM Say so before the first + line scrolls past. +if "%dry_run%" == "ON" echo Dry run: printing commands without running them. + +if "%kill_jobs%" == "ON" ( + echo Stopping build processes. + call :kill_image MSBuild.exe + call :kill_image ninja.exe + call :kill_image cl.exe + call :kill_image clang-cl.exe + exit /b 0 +) + +REM Neither test option can happen without building the slicer, so asking for +REM one asks for that, unless another action was already named. +if "%build_deps%%build_slicer%%pack_deps%%install_deps%%install_vs%" == "" ( + if "%build_tests%" == "ON" set "build_slicer=ON" + if "%run_tests%" == "ON" set "build_slicer=ON" +) + +REM Options like --config or -j only shape a build. Without one of the actions +REM there is nothing for them to shape, so say so rather than resolving a whole +REM build and reporting it took no time. The block above may have added one. +if "%build_deps%%build_slicer%%pack_deps%%install_deps%%install_vs%" == "" ( + echo Nothing to do. Pick an action: -d, -s, -p or -u. Run -h for the full list. + exit /b 1 +) + +REM =========================================================================== +REM Visual Studio and target architecture +REM =========================================================================== + +REM Asking for Visual Studio is asking to install prerequisites. Resolve the +REM edition here rather than testing whether the name is defined: `if defined` +REM stops at the first space, so "ide " would pass and then expand to nothing. +set "vs_edition=" +if not "%install_vs%" == "" ( + set "install_deps=ON" + set "vs_edition=!vs_edition_%install_vs%!" + if "!vs_edition!" == "" ( + echo Unknown Visual Studio edition "%install_vs%". Known editions: buildtools, ide. + exit /b 1 + ) +) +REM Autodetection overwrites vs_version, so snapshot whether it was pinned. +set "vs_pinned=%vs_version%" + +if not "%vs_version%" == "" if "%use_ninja%" == "ON" ( + echo --vs and --ninja select different generators. + exit /b 1 +) +REM install(TARGETS OrcaSlicer) carries no OPTIONAL, so installing a tree +REM whose executable was never built fails. Say so before the build starts. +if "%build_slicer%" == "ON" if "%install_slicer%" == "ON" if not "%slicer_target%" == "" if /I not "%slicer_target%" == "OrcaSlicer" ( + echo --install needs the executable, but --slicer-target names "%slicer_target%". + exit /b 1 +) +if not "%vs_version%" == "" if "!vs_gen_%vs_version%!" == "" ( + echo Unknown Visual Studio release "%vs_version%". Known releases: 2019, 2022, 2026. + exit /b 1 +) + +call :autodetect_vs +%error_check% + +REM autodetect leaves this empty when it finds nothing usable. +if "%vs_version%" == "" set "vs_version=%vs_default%" + +REM Default to the host CPU. PROCESSOR_ARCHITEW6432 covers a 32-bit shell on +REM a 64-bit OS, where PROCESSOR_ARCHITECTURE reads x86. +set arch=x64 +if /I "%PROCESSOR_ARCHITECTURE%" == "ARM64" set arch=ARM64 +if /I "%PROCESSOR_ARCHITEW6432%" == "ARM64" set arch=ARM64 +if not "%target_arch%" == "" ( + if /I "%target_arch%" == "arm64" ( + set arch=ARM64 + ) else ( + if /I "%target_arch%" == "x64" ( + set arch=x64 + ) else ( + echo Unknown architecture "%target_arch%". Expected x64 or arm64. + exit /b 1 + ) + ) +) + +REM =========================================================================== +REM Installing prerequisites +REM =========================================================================== + +if "%install_deps%" == "ON" ( + where winget >nul 2>nul + if not !errorlevel! == 0 ( + echo WinGet was not found + exit /b 1 + ) + REM Keep going after one failure so the rest still get installed, then + REM name the ones that did not rather than claiming they all did. + set "install_failed=" + set "winget_args=-e --source=winget" + if not "%install_vs%" == "" ( + set "vs_year=!vs_winget_%vs_version%!" + set "ide_component_flag=" + if /I "%install_vs%" == "ide" set "ide_component_flag=Microsoft.VisualStudio.Component.VC.CoreIde" + REM Two components: the clang-cl compiler itself, and the MSBuild + REM toolset that lets the Visual Studio generator drive it. One + REM install covers both x64 and ARM64. + set "clang_cl_flag=" + if "%use_clang_cl%" == "ON" set "clang_cl_flag=Microsoft.VisualStudio.Component.VC.Llvm.Clang Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset" + REM The x64 tools build the host tooling either way; targeting ARM64 + REM needs its own toolset on top of them. + set "arm64_tools_flag=" + if /I "%arch%" == "ARM64" set "arm64_tools_flag=Microsoft.VisualStudio.Component.VC.Tools.ARM64" + call :print_and_run winget install !winget_args! --id=Microsoft.VisualStudio!vs_year!.!vs_edition! --force --custom "--add !ide_component_flag! Microsoft.VisualStudio.Component.VC.Tools.x86.x64 !arm64_tools_flag! Microsoft.VisualStudio.Component.VC.CMake.Project Microsoft.VisualStudio.Component.Windows11SDK.22621 !clang_cl_flag!" + call :note_failed "Visual Studio" !errorlevel! + ) + + REM CMake 4 dropped pre-3.5 policy support and ships incomplete ASM_ARMASM + REM linker modules, which breaks Boost.Context on ARM64. CI pins the same way. + set "cmake_version_flag=" + if /I "%arch%" == "ARM64" set "cmake_version_flag=--version 3.31.8" + call :print_and_run winget install !winget_args! --id=Kitware.CMake !cmake_version_flag! + call :note_failed CMake !errorlevel! + call :print_and_run winget install !winget_args! --id=StrawberryPerl.StrawberryPerl + call :note_failed Perl !errorlevel! + call :print_and_run winget install !winget_args! --id=Git.Git + call :note_failed Git !errorlevel! + + if defined install_failed ( + set "die_reason=Failed to install:!install_failed!" + set rc=1 + goto :die + ) + + REM Flags to repeat back in the deferred build command. + set "next_flags=" + if "%use_clang_cl%" == "ON" set "next_flags=!next_flags! -l" + if "%use_ninja%" == "ON" set "next_flags=!next_flags! -x" + if not "%target_arch%" == "" set "next_flags=!next_flags! --arch %target_arch%" + + REM Name the build that was deferred, not a fuller one. + set "next_actions=" + if "%build_deps%" == "ON" set "next_actions=!next_actions!d" + if "%build_slicer%" == "ON" set "next_actions=!next_actions!s" + if "%pack_deps%" == "ON" set "next_actions=!next_actions!p" + if "!next_actions!" == "" set "next_actions=ds" + + echo. + echo ------------------------------------------------------------- + REM A dry run installed nothing, so do not report that it did. + if "%dry_run%" == "ON" ( + echo Dry run: nothing was installed. + ) else ( + echo Installed the prerequisites. + ) + echo. + echo Next + REM The new PATH cannot reach this shell, so the build runs in the next one. + echo Restart this shell so the new PATH takes effect, then + echo build_win.bat -!next_actions!!next_flags! + echo ------------------------------------------------------------- + exit /b 0 +) + +REM =========================================================================== +REM Resolving the build +REM =========================================================================== + +REM A value ending in a backslash escapes the closing quote when it is +REM spliced into a command line: -B "D:\tree\" reaches cmake as D:\tree" +REM and swallows the argument after it. No path here needs one. +call :trim_slash deps_dir +call :trim_slash slicer_dir +call :trim_slash clang_path + +REM Naming a specific clang-cl is naming clang-cl. Doing it before the +REM conflict check below means --clang-path with --msvc is caught there. +if not "%clang_path%" == "" set "use_clang_cl=ON" +if not "%clang_path%" == "" if not exist "%clang_path%" ( + echo No clang-cl at "%clang_path%". + exit /b 1 +) +REM A trailing backslash matches directories only, and a directory would +REM otherwise reach CMake as the compiler. +if not "%clang_path%" == "" if exist "%clang_path%\" ( + echo "%clang_path%" is a directory. Name clang-cl.exe itself. + exit /b 1 +) + +REM Naming a default explicitly is fine; naming both sides is not. +if "%use_clang_cl%" == "ON" if "%use_msvc%" == "ON" ( + echo --clang-cl and --msvc select different compilers. + exit /b 1 +) +if "%use_ninja%" == "ON" if "%use_msbuild%" == "ON" ( + echo --ninja and --msbuild select different generators. + exit /b 1 +) + +set "generator=!vs_gen_%vs_version%!" +if "%use_ninja%" == "ON" ( + set "generator=Ninja Multi-Config" + call :setup_dev_env + %error_check% + set "using_ninja=ON" +) + +if "%using_ninja%" == "ON" ( + if "%use_clang_cl%" == "ON" ( + REM Bare, so it resolves from the PATH the dev shell just set up, which + REM is the clang shipped with Visual Studio. --clang-path names another. + set "clang_exe=clang-cl.exe" + if not "%clang_path%" == "" set "clang_exe="%clang_path%"" + set "gen_args=-DCMAKE_C_COMPILER=!clang_exe! -DCMAKE_CXX_COMPILER=!clang_exe!" + ) +) else ( + set "gen_args=-A !arch!" + if "%use_clang_cl%" == "ON" ( + set "gen_args=!gen_args! -T ClangCL" + ) +) + +if not "%clang_path%" == "" if not "%using_ninja%" == "ON" ( + echo --clang-path needs the Ninja generator; add -x. The Visual Studio + echo generator builds clang-cl through its own ClangCL toolset. + exit /b 1 +) + +REM Ninja prints [123/456] by default, which says nothing about how far +REM along it is or how long is left. %p is a percentage, %w and %W are +REM elapsed and remaining, but those two arrived in ninja 1.12 and an +REM unknown placeholder is fatal rather than a warning, so an older ninja +REM would abort the build instead of degrading. Ask which one is on PATH. +REM Keep %p first inside the brackets: VS Code reads the first digits% +REM it finds there to drive its progress bar. +if "%using_ninja%" == "ON" if not defined NINJA_STATUS ( + set "nj_major=0" + set "nj_minor=0" + for /f "tokens=1,2 delims=." %%a in ('ninja --version 2^>nul') do ( + set "nj_major=%%a" + set "nj_minor=%%b" + ) + set "NINJA_STATUS=[%%s/%%t %%p :: %%e] " + if !nj_major! GTR 1 set "NINJA_STATUS=[%%f/%%t %%p :: %%w / %%W] " + if !nj_major! EQU 1 if !nj_minor! GEQ 12 set "NINJA_STATUS=[%%f/%%t %%p :: %%w / %%W] " +) +if "%verbose%" == "ON" if defined NINJA_STATUS echo Ninja progress format: !NINJA_STATUS! + +if "%deps_target%" == "" ( + set "deps_target=deps" +) + +REM Only the builds need CMake. -p zips an existing tree with 7z. +if "%build_deps%%build_slicer%" == "" goto :cmake_ready + +cmake --version >nul 2>nul +if not !errorlevel! == 0 ( + echo CMake was not found. Have you installed the system dependencies? + exit /b 1 +) + +REM Strawberry Perl ships a c/bin full of GNU tools, and the top-level +REM CMakeLists refuses to configure when it precedes CMake on PATH. Put a +REM real CMake first, skipping any hit from Strawberry's own cmake.exe, or +REM this pins exactly the order it is meant to undo. The findstr needle +REM must not end in a backslash, which escapes the quote and matches nothing. +set "cmake_bin=" +for /f "delims=" %%i in ('where cmake 2^>nul') do ( + if not defined cmake_bin ( + echo %%~dpi| findstr /i /c:"\Strawberry\c\bin" >nul || set "cmake_bin=%%~dpi" + ) +) +if defined cmake_bin set "PATH=%cmake_bin%;%PATH%" +:cmake_ready + +if "%config%" == "" set "config=%cfg_default%" + +REM Judge what the lookup returned rather than whether the name is defined. +REM "release " passed `if defined` and then expanded to nothing, leaving an +REM empty build directory for --clean to remove. +set "build_type=!cfg_type_%config%!" +set "tree_name=!cfg_dir_%config%!" +set "dep_name=!cfg_dep_%config%!" +set "bad_config=" +if "!build_type!" == "" set "bad_config=ON" +if "!tree_name!" == "" set "bad_config=ON" +if "!dep_name!" == "" set "bad_config=ON" +if defined bad_config ( + echo Unknown configuration "%config%". Known configurations: release, debug, relwithdebinfo, minsizerel. + exit /b 1 +) + +REM A tree is only good for the configuration, compiler and architecture it +REM was made with. Change one underneath it and CMake resets its cache and +REM carries on, leaving ExternalProject stamps from the old toolchain and +REM sub-builds that fail scattershot on things that build fine from scratch. +REM So name the tree for all three. MSVC x64 keeps the historical names. +if "%use_clang_cl%" == "ON" set "tree_name=!tree_name!-clang" +if /I "%arch%" == "ARM64" set "tree_name=!tree_name!-arm64" +if "%use_clang_cl%" == "ON" set "dep_name=!dep_name!-clang" +if /I "%arch%" == "ARM64" set "dep_name=!dep_name!-arm64" + +set "build_dir=!tree_name!" +if not "%slicer_dir%" == "" set "build_dir=%slicer_dir%" + +REM Resolve it once. --build-dir may arrive absolute, relative, or with +REM forward slashes, and anything that prints the directory has to show a +REM real path rather than one glued onto the repository root. +for %%p in ("!build_dir!") do set "build_full=%%~fp" +echo Configuration: %build_type%, %arch% + +set "SIG_FLAG=" +if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%" + +set "TESTS_FLAG=-DBUILD_TESTS=OFF" +if "%build_tests%" == "ON" set "TESTS_FLAG=-DBUILD_TESTS=ON" +if "%run_tests%" == "ON" set "TESTS_FLAG=-DBUILD_TESTS=ON" + +set "SLICER_TARGET_FLAG=" +if not "%slicer_target%" == "" set "SLICER_TARGET_FLAG=--target %slicer_target%" + +set "DEP_TREE=deps/!dep_name!" +set "DEP_TREE_PACK=%WP%\deps\!dep_name!" +set "DEP_TREE_FLAG=" +if not "%deps_dir%" == "" ( + set "DEP_TREE=%deps_dir%" + set "DEP_TREE_PACK=%deps_dir%" + set "DEP_TREE_FLAG=-DDEP_BUILD_DIR="%deps_dir%"" +) + +REM CMakeLists derives DEP_BUILD_DIR from the build directory NAME, which is +REM wrong whenever the build tree and the dependency tree are named +REM differently, as they are for every configuration that shares the release +REM dependencies. Name it outright once the two stop matching. +if "!DEP_TREE_FLAG!" == "" if not "%build_dir%" == "!dep_name!" ( + set "DEP_TREE_FLAG=-DDEP_BUILD_DIR="%WP%\deps\!dep_name!"" +) + +set "VERBOSE_FLAG=" +if "%verbose%" == "ON" set "VERBOSE_FLAG=--verbose" + +REM The two generators count differently. +REM +REM Ninja counts compilers, so -j is exact. The deps superbuild reads +REM CMAKE_BUILD_PARALLEL_LEVEL for the sub-builds it drives. +REM +REM MSBuild turns -j into /m, which counts projects, not compilers: /MP still +REM runs one cl per core inside each one, so -j 1 would not give one +REM compiler. CL_MPCount sets the /MP degree instead, and MSBuild reads +REM properties from the environment, so nested deps builds inherit it. /m +REM stays at its default of one project at a time. +set "JOBS_FLAG=" +if "%jobs%" == "" goto :jobs_ready +echo %jobs%| findstr /r /c:"^[1-9][0-9]*$" >nul +if errorlevel 1 ( + echo Invalid --jobs value "%jobs%". Expected a positive integer. + exit /b 1 +) +if "%using_ninja%" == "ON" ( + set "JOBS_FLAG=-j %jobs%" + set "CMAKE_BUILD_PARALLEL_LEVEL=%jobs%" +) else ( + set "CL_MPCount=%jobs%" +) +echo Parallel jobs: %jobs% +:jobs_ready + +REM The flags that reproduce this run, for the commands the summary suggests. +REM Kept in three parts because they are not all relevant everywhere: a deps +REM retry has no use for --build-dir, and the bundle line names its own tree. +set "recall_tc=" +if "%use_clang_cl%" == "ON" set "recall_tc=!recall_tc! -l" +if "%using_ninja%" == "ON" set "recall_tc=!recall_tc! -x" +if /I not "%config%" == "%cfg_default%" set "recall_tc=!recall_tc! --config %config%" +if not "%target_arch%" == "" set "recall_tc=!recall_tc! --arch %target_arch%" +if not "%vs_pinned%" == "" set "recall_tc=!recall_tc! --vs %vs_pinned%" +if not "%clang_path%" == "" set "recall_tc=!recall_tc! --clang-path "%clang_path%"" + +set "recall_i=" +if "%install_slicer%" == "ON" set "recall_i= -i" +set "recall_dd=" +if not "%deps_dir%" == "" set "recall_dd= --deps-dir "%deps_dir%"" +set "recall_bd=" +if not "%slicer_dir%" == "" set "recall_bd= --build-dir "%slicer_dir%"" +set "recall=!recall_tc!!recall_i!!recall_dd!!recall_bd!" + +REM =========================================================================== +REM Running the build +REM =========================================================================== + +REM CMake 4 refuses a pre-3.5 policy version; several deps still ask for one. +set CMAKE_POLICY_VERSION_MINIMUM=3.5 + +set _START_TIME=%TIME% + +if "%build_deps%" == "ON" ( + REM Which stage is running, so a failure can name it and suggest a + REM retry scoped to it rather than to the whole run. + set "stage=d" + echo Building the dependencies... + + if "%clean%" == "ON" ( + call :clean_tree "!DEP_TREE!" + %error_check% + ) + + if not "%no_configure%" == "ON" ( + call :print_and_run cmake -S deps -B "!DEP_TREE!" -G "%generator%" %gen_args% -DCMAKE_BUILD_TYPE=%build_type% !deps_args! %ORCA_DEPS_CMAKE_ARGS% + %error_check% + ) + + call :print_and_run cmake --build "!DEP_TREE!" --config %build_type% --target %deps_target% %JOBS_FLAG% %VERBOSE_FLAG% + %error_check% +) + +if "%pack_deps%" == "ON" ( + set "stage=p" + setlocal ENABLEDELAYEDEXPANSION + call :print_and_run cd /d "!DEP_TREE_PACK!" + %error_check% + REM date /t prints in the machine locale and its field order varies by + REM region, which is how the inherited parse produced YYYYDDMM. Ask for + REM an unambiguous stamp instead. powershell.exe lives under + REM System32\WindowsPowerShell rather than System32, so a trimmed PATH + REM cannot find it and the stamp comes back empty. + set "ps=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" + set "build_date=" + for /f %%d in ('!ps! -NoProfile -Command "Get-Date -Format yyyyMMdd"') do set "build_date=%%d" + if "!build_date!" == "" ( + set "die_reason=Could not read the date from !ps!." + set rc=1 + goto :die + ) + + REM A bundle is only good for what built it, so it carries the same parts + REM as the dependency tree. Release x64 on cl keeps the plain name. + set "dep_flavour=!cfg_dep_%config%!" + set "dep_flavour=!dep_flavour:build=!" + set "dep_variant=%arch%" + if "%use_clang_cl%" == "ON" set "dep_variant=!dep_variant!-clang" + set "dep_variant=!dep_variant!!dep_flavour!" + echo Packing the dependencies: OrcaSlicer_dep_win-!dep_variant!_!build_date!.zip + + REM tools\7z.exe loads its codecs from a 7z.dll, and the repo does not + REM carry one, so it can only archive on a machine that has 7-Zip + REM installed. Windows has shipped bsdtar in System32 since 10 1803 and + REM it writes an ordinary deflate zip, so fall back to that rather than + REM failing on a machine that has everything it needs. + set "zipper=" + if exist "%WP%\tools\7z.dll" set "zipper=%WP%/tools/7z.exe a" + if not defined zipper if exist "%SystemRoot%\System32\tar.exe" set "zipper=%SystemRoot%\System32\tar.exe -a -c -f" + if not defined zipper ( + set "die_reason=No archiver. tools\7z.exe needs a 7z.dll, and this Windows has no System32\tar.exe." + set "die_hint=Install 7-Zip, or copy its 7z.dll into tools\." + set rc=1 + goto :die + ) + + call :print_and_run !zipper! OrcaSlicer_dep_win-!dep_variant!_!build_date!.zip OrcaSlicer_dep + %error_check% + REM endlocal is about to discard the name, so carry the path out with it. + for %%z in ("!DEP_TREE_PACK!\OrcaSlicer_dep_win-!dep_variant!_!build_date!.zip") do endlocal & set "bundle=%%~fz" +) + +if "%build_slicer%" == "ON" ( + set "stage=s" + echo Building OrcaSlicer... + + if "%clean%" == "ON" ( + call :clean_tree "%build_dir%" + %error_check% + ) + + if "%slicer_asan%" == "ON" ( + set "slicer_args=!slicer_args! -DSLIC3R_ASAN=ON" + ) + + REM Configuring against a tree that was never built fails deep inside + REM package resolution. Name it here instead. Skipped when -d is about to + REM build it in this same run, and under a dry run, which configures + REM nothing and must not depend on what happens to be on the machine. + if not "%dry_run%" == "ON" if not "%no_configure%" == "ON" if not "%build_deps%" == "ON" if not exist "!DEP_TREE!\OrcaSlicer_dep\usr\local\" ( + for %%p in ("!DEP_TREE!") do set "die_reason=Dependencies not found at %%~fp" + set "die_hint=Build them with build_win.bat -d!recall!." + REM Only worth suggesting to someone who did not name a tree. + if "%deps_dir%" == "" set "die_hint=Build them with build_win.bat -d!recall!, or point --deps-dir at an existing tree." + set rc=1 + goto :die + ) + + if not "%no_configure%" == "ON" ( + call :print_and_run cmake -B "%build_dir%" -G "%generator%" %gen_args% -DORCA_TOOLS=ON %SIG_FLAG% %TESTS_FLAG% %DEP_TREE_FLAG% -DCMAKE_BUILD_TYPE=%build_type% !slicer_args! %ORCA_SLICER_CMAKE_ARGS% + %error_check% + ) + + call :print_and_run cmake --build "%build_dir%" --config %build_type% %SLICER_TARGET_FLAG% %JOBS_FLAG% %VERBOSE_FLAG% + %error_check% + + if "%run_tests%" == "ON" ( + call :print_and_run ctest --test-dir "%build_dir%/tests" -C %build_type% --output-on-failure + %error_check% + ) + + if not "%no_gettext%" == "ON" ( + call :print_and_run call scripts/run_gettext.bat + %error_check% + ) + + if "%install_slicer%" == "ON" ( + call :print_and_run cmake --build "%build_dir%" --target install --config %build_type% + %error_check% + ) +) + +REM Elapsed wall clock. The 1%%a-100 trick strips a leading zero, which set /A +REM would otherwise read as octal, and a negative total means the build ran +REM past midnight. +for /f "tokens=1-3 delims=:.," %%a in ("%_START_TIME: =0%") do set /a "_start_s=(1%%a-100)*3600+(1%%b-100)*60+(1%%c-100)" +for /f "tokens=1-3 delims=:.," %%a in ("%TIME: =0%") do set /a "_end_s=(1%%a-100)*3600+(1%%b-100)*60+(1%%c-100)" +set /a "_elapsed=_end_s - _start_s" +if %_elapsed% lss 0 set /a "_elapsed+=86400" +set /a "_hours=_elapsed / 3600" +set /a "_remainder=_elapsed - _hours * 3600" +set /a "_mins=_remainder / 60" +set /a "_secs=_remainder - _mins * 60" +call :summary +exit /b 0 + +REM Reached only by error_check, from the top level, where exit /b works. +REM The hash block is what CMakeLists already uses for a build that cannot +REM continue, so it means the same thing here. +:die +echo. +echo ############################################################# +REM The paths that set die_reason had no command fail, so last_cmd there +REM names one that succeeded. +if defined die_reason echo !die_reason! +if not defined die_reason if defined last_cmd echo Failed: !last_cmd! +if defined die_hint echo !die_hint! +echo Exit code %rc%. +REM -v only makes the build verbose, so it has nothing to offer a configure +REM that failed before any compiler ran. +set "failed_configure=" +if "!last_cmd:~0,9!" == "cmake -B " set "failed_configure=ON" +if "!last_cmd:~0,9!" == "cmake -S " set "failed_configure=ON" +REM Scoped to the stage that failed. Offering -c for the whole run would +REM discard a dependency tree that was not at fault, and neither flag does +REM anything for a failed pack. A named reason already carries its own advice. +if "!stage!" == "d" set "recall=!recall_tc!!recall_dd!" +if not defined die_reason if defined stage if not "!stage!" == "p" ( + echo. + echo Try + if not defined failed_configure echo build_win.bat -!stage!!recall! -v show the failing compiler command line + echo build_win.bat -!stage!!recall! -c discard that tree and configure from scratch +) +echo ############################################################# +exit /b %rc% + +REM =========================================================================== +REM Function definitions +REM =========================================================================== + +REM summary - what was produced, and what to do with it next. A dry run says +REM so rather than claiming the files exist, but every line below that is +REM worked out the same way in either run. +:summary + for %%p in ("!DEP_TREE!") do set "dep_full=%%~fp" + REM The binary only leaves the build tree when it is installed. + set "slicer_exe=%build_dir%\src\%build_type%\orca-slicer.exe" + if "%install_slicer%" == "ON" set "slicer_exe=%build_dir%\OrcaSlicer\orca-slicer.exe" + for %%p in ("!slicer_exe!") do set "slicer_full=%%~fp" + REM Naming a target builds it and its dependencies, not its dependents, + REM so only a full build or the executable's own target relinks. + set "linked=ON" + if not "%slicer_target%" == "" set "linked=" + if /I "%slicer_target%" == "OrcaSlicer" set "linked=ON" + + echo. + echo ------------------------------------------------------------- + if "%dry_run%" == "ON" ( + echo Dry run: nothing was built. A real run would report: + ) else ( + echo Build completed in %_hours%h %_mins%m %_secs%s + ) + + if "%build_deps%" == "ON" echo Dependencies !dep_full! + if "%build_slicer%" == "ON" if "%linked%" == "ON" echo OrcaSlicer !slicer_full! + if "%build_slicer%" == "ON" if not "%linked%" == "ON" echo Target %slicer_target% + if "%build_slicer%" == "ON" if not "%using_ninja%" == "ON" echo Solution %build_full%\OrcaSlicer.sln + if "%pack_deps%" == "ON" if defined bundle echo Bundle !bundle! + + echo. + echo Next + if "%build_slicer%" == "ON" ( + if "%linked%" == "ON" echo Run it !slicer_exe! + if not "%using_ninja%" == "ON" echo Open in Visual Studio %build_dir%\OrcaSlicer.sln + if "%linked%" == "ON" echo Rebuild after edits build_win.bat -s!recall! --no-configure + if not "%linked%" == "ON" echo Relink the binary build_win.bat -s!recall! --no-configure + if "%linked%" == "ON" if "%using_ninja%" == "ON" echo Rebuild one target build_win.bat -s!recall! --no-configure --slicer-target libslic3r + if "%run_tests%" == "ON" echo Re-run the tests ctest --test-dir %build_dir%/tests -C %build_type% --output-on-failure + ) else ( + if "%build_deps%" == "ON" echo Build the slicer build_win.bat -s!recall! + ) + if "%pack_deps%" == "ON" echo Share the bundle unzip it elsewhere, then build_win.bat -s!recall_tc! --deps-dir ^ + echo ------------------------------------------------------------- + exit /b 0 + +:debug_msg + if "%debugscript%" == "ON" echo %* + exit /b 0 + +REM get_str_len -> length in %ret% +:get_str_len + setlocal + set "in=%~1" + for /L %%i in (0, 1, 100) do ( + if "!in:~%%i,1!" == "" ( + set out=%%i + goto :break_str_len + ) + ) + + echo error in get_str_len: string is too long + endlocal + exit /b 1 + + :break_str_len + endlocal & set ret=%out% + exit /b 0 + +:print_help_msg + setlocal + + REM Measure the widest flag column. Section headers have no flags and + REM must not widen it. + set flags= + set max_len=0 + set /A range_end = %argdefn% - 1 + for /L %%i in (0, 1, %range_end%) do ( + if "!argdefs[%%i].TYPE!" == "section" ( + set "flags[%%i]=" + ) else ( + set str_placeholder= + if not "!argdefs[%%i].TYPE!" == "bool" ( + set "str_placeholder= " + ) + + REM The placeholder goes on the long form only. Repeating it on + REM the short one says nothing extra and costs eight columns. + set "flag_str=" + if not "!argdefs[%%i].SHORT_FLAG!" == "" ( + set "flag_str=-!argdefs[%%i].SHORT_FLAG!" + ) + + if not "!argdefs[%%i].LONG_FLAG!" == "" ( + if "!flag_str!" == "" ( + REM No short flag: pad so the long one lines up with the others. + set "flag_str= " + ) else ( + set "flag_str=!flag_str!, " + ) + set "flag_str=!flag_str!--!argdefs[%%i].LONG_FLAG!!str_placeholder!" + ) else ( + set "flag_str=!flag_str!!str_placeholder!" + ) + set "flag_str=!flag_str! " + set "flags[%%i]=!flag_str!" + call :get_str_len "!flag_str!" + if !ret! GTR !max_len! ( + set max_len=!ret! + ) + ) + ) + + set padding= + for /L %%i in (0, 1, %max_len%) do set "padding=!padding! " + + echo Builds OrcaSlicer and its dependencies on Windows. + echo. + echo Usage: %script_name% [options] + set /A range_end = %argdefn% - 1 + for /L %%i in (0, 1, !range_end!) do ( + if "!argdefs[%%i].TYPE!" == "section" ( + echo. + echo !argdefs[%%i].HELP_TEXT!: + ) else ( + set "flag=!flags[%%i]!%padding%" + echo !flag:~0,%max_len%!!argdefs[%%i].HELP_TEXT! + ) + ) + echo. + echo Examples: + echo %script_name% --install-vs ide Set up a new machine, then restart the shell + echo %script_name% -ds Build the dependencies, then the slicer + echo %script_name% -s -l -x Rebuild the slicer with clang-cl and Ninja + echo %script_name% -s --no-configure -j 8 Rebuild quickly while iterating + echo %script_name% -s --slicer-target glad Compile one target to check the toolchain + echo %script_name% -l -x --run-tests Test that toolchain's build, not the default one + echo. + echo Environment: + echo ORCA_DEPS_CMAKE_ARGS Extra arguments for the deps configure + echo ORCA_SLICER_CMAKE_ARGS Extra arguments for the slicer configure + echo ORCA_UPDATER_SIG_KEY Update signing key baked into the slicer + echo git_commit_hash Revision to stamp, so a commit does not rebuild everything + echo NINJA_STATUS Ninja progress format, if you want your own + echo debugscript Set to ON to trace this script + echo. + echo set ORCA_SLICER_CMAKE_ARGS=-DSLIC3R_PCH=OFF -DSLIC3R_MSVC_PDB=OFF + echo $env:ORCA_SLICER_CMAKE_ARGS = '-DSLIC3R_PCH=OFF' (PowerShell) + echo. + echo --deps-args and --slicer-args cannot carry a value with spaces; use + echo these instead. Neither form supports a value containing an ampersand. + endlocal + exit /b 0 + +REM add_section +:add_section + set argdefs[%argdefn%].VARIABLE_NAME= + set argdefs[%argdefn%].TYPE=section + set argdefs[%argdefn%].SHORT_FLAG= + set argdefs[%argdefn%].LONG_FLAG= + set argdefs[%argdefn%].HELP_TEXT=%~1 + set /A argdefn+=1 + exit /b 0 + +REM add_arg <variable> <type:bool,string,rawstring> <short> <long> <help> +:add_arg + set argdefs[%argdefn%].VARIABLE_NAME=%~1 + set argdefs[%argdefn%].TYPE=%~2 + set argdefs[%argdefn%].SHORT_FLAG=%~3 + set argdefs[%argdefn%].LONG_FLAG=%~4 + set argdefs[%argdefn%].HELP_TEXT=%~5 + + REM Start every option unset, so "defined" means the user gave it. + set %~1= + + if "%debugscript%" == "ON" ( + echo add_arg VARIABLE_NAME: !argdefs[%argdefn%].VARIABLE_NAME! + echo add_arg TYPE: !argdefs[%argdefn%].TYPE! + echo add_arg SHORT_FLAG: !argdefs[%argdefn%].SHORT_FLAG! + echo add_arg LONG_FLAG: !argdefs[%argdefn%].LONG_FLAG! + echo add_arg HELP_TEXT: !argdefs[%argdefn%].HELP_TEXT! + ) + + set /A argdefn+=1 + + exit /b 0 + +REM find_arg <type:short,long> <flag> -> index in %ret% +:find_arg + setlocal + call :debug_msg starting function: find_arg "%~1" "%~2" + + set type= + if /I "%~1" == "short" set type=SHORT + if /I "%~1" == "long" set type=LONG + if not defined type ( + endlocal + set ret= + exit /b 1 + ) + + call :debug_msg find_arg type=%type% + set /A range_end = %argdefn% - 1 + for /L %%i in (0, 1, %range_end%) do ( + if "!argdefs[%%i].%type%_FLAG!" == "%~2" ( + set idx=%%i + goto :find_arg_cont + ) + ) + + echo Error in find_arg: Failed to find arg "%~2" + call :print_help_msg + + endlocal + set ret= + exit /b 1 + + :find_arg_cont + call :debug_msg find_arg: found at %idx% + endlocal & ( + set ret=%idx% + ) + exit /b 0 + +REM set_arg <index> [<value>] +:set_arg + call :debug_msg starting function: set_arg "%~1" "%~2" + + if "%~1" == "" ( + echo Error in set_arg: no index provided + exit /b 1 + ) + + setlocal + if /I "!argdefs[%~1].TYPE!" == "bool" ( + call :debug_msg set_arg: setting bool type to ON + set val=ON + ) else ( + set "val=%~2" + set quote_char=" + REM Delayed expansion throughout: %val% here would be the value from the + REM previous call, since the whole block is parsed before it runs. + if "!val:~0,1!" == "!quote_char!" ( + if "!val:~-1,1!" == "!quote_char!" ( + set "val=!val:~1,-1!" + ) + ) + + call :debug_msg set_arg: setting string type to %~2 + ) + set var_name=!argdefs[%~1].VARIABLE_NAME! + + endlocal & ( + REM Set variable in parent scope + set "%var_name%=%val%" + + REM Add variable to finalize command + set "finalize_cmd=%finalize_cmd% & set "%var_name%=%val%"" + ) + + exit /b 0 + +REM get_arg_type <index> -> type in %ret% +:get_arg_type + call :debug_msg starting function get_arg_type "%~1" + setlocal + set type=!argdefs[%~1].TYPE! + endlocal & set ret=%type% + exit /b 0 + +REM echo_var <variable> +:echo_var + echo %~1=!%~1! + exit /b 0 + +:autodetect_vs + REM Nothing to detect once the release is pinned, or under Ninja. + if not "%vs_version%" == "" exit /b 0 + if "%use_ninja%" == "ON" exit /b 0 + + setlocal + + %VSWHERE% -nologo >nul 2>nul + if not !errorlevel! == 0 ( + REM vswhere is not in its usual place; try msbuild instead. + goto :msbuild_check + ) + + echo Detecting Visual Studio version using vswhere... + for /f "tokens=1 delims=." %%i in ('%VSWHERE% -nologo -products * -latest -property catalog_productDisplayVersion') do ( + set "VS_MAJOR=%%i" + goto :version_found + ) + + :msbuild_check + where msbuild >nul 2>nul + if not !errorlevel! == 0 ( + REM No msbuild either; leave the release empty and let the default apply. + endlocal + exit /b 0 + ) + + echo Detecting Visual Studio version using msbuild... + + REM The version line varies by release, so try two patterns for it. + set VS_MAJOR= + for /f "tokens=*" %%i in ('msbuild -version 2^>^&1 ^| findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"') do ( + for /f "tokens=1 delims=." %%a in ("%%i") do set VS_MAJOR=%%a + set MSBUILD_OUTPUT=%%i + goto :version_found + ) + + REM The same pattern unanchored, for releases that print a banner first. + if "%VS_MAJOR%"=="" ( + for /f "tokens=*" %%i in ('msbuild -version 2^>^&1 ^| findstr /r "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"') do ( + for /f "tokens=1 delims=." %%a in ("%%i") do set VS_MAJOR=%%a + set MSBUILD_OUTPUT=%%i + goto :version_found + ) + ) + + :version_found + set "detected=!vs_year_%VS_MAJOR%!" + if not defined detected ( + echo Error: Unsupported Visual Studio major version: %VS_MAJOR% + endlocal + exit /b 1 + ) + echo Detected Visual Studio %VS_MAJOR% ^(!detected!^) + endlocal & set "vs_version=%detected%" + + exit /b 0 + +:setup_dev_env + REM A dry run runs nothing, and VsDevCmd costs seconds. + if "%dry_run%" == "ON" exit /b 0 + set "dev_arch=x64" + if /I "!arch!" == "ARM64" set "dev_arch=arm64" + %VSWHERE% -nologo >nul 2>nul + if not !errorlevel! == 0 ( + REM vswhere is not in its usual place; use the environment as it stands. + exit /b 0 + ) + + for /f "tokens=*" %%i in ('%VSWHERE% -nologo -products * -latest -property resolvedInstallationPath') do ( + set "VS_PATH=%%i" + goto :vs_path_found + ) + exit /b 0 + + :vs_path_found + call "%VS_PATH%\Common7\Tools\VsDevCmd.bat" -arch=!dev_arch! >nul 2>nul + set "VS_PATH=" + + exit /b 0 + +REM clean_tree <path> - remove a build tree, refusing anything that is not +REM one. Nothing here should ever fire; it is a floor under a bug that +REM produced a path far shorter than it looks. +:clean_tree + setlocal + set "target=%~1" + if not defined target ( + echo Refusing to clean: no directory to remove. + exit /b 1 + ) + + REM Resolve first, because "." and "deps/.." are shorter than they read. + for %%p in ("!target!") do set "full=%%~fp" + if "!full:~1!" == ":\" ( + echo Refusing to clean "!full!": that is a drive root. + exit /b 1 + ) + if /I "!full!" == "%WP%" ( + echo Refusing to clean "!full!": that is the repository itself. + exit /b 1 + ) + + call :print_and_run rmdir /S /Q "!full!" + exit /b !errorlevel! + +REM note_failed <name> <code> - record a failed install. Two winget codes +REM mean the tool is already there, which is what -u is for: +REM -1978335135 (0x8A150061) winget declined: a version is already there +REM -1978335189 (0x8A15002B) installed and already the newest version +:note_failed + if "%~2" == "0" exit /b 0 + if "%~2" == "-1978335135" exit /b 0 + if "%~2" == "-1978335189" exit /b 0 + set "install_failed=%install_failed% %~1" + exit /b 0 + +REM trim_slash <variable> - drop a trailing backslash from a path value. +REM A drive root keeps its own: "C:\" is not the same place as "C:". +:trim_slash + set "trim_value=!%~1!" + if not defined trim_value exit /b 0 + if not "!trim_value:~-1!" == "\" exit /b 0 + if "!trim_value:~-2!" == ":\" exit /b 0 + set "%~1=!trim_value:~0,-1!" + exit /b 0 + +REM kill_image <name> - stop every process of one image. The count is printed +REM first because taskkill can take a while and says nothing until it returns. +REM Never fails, so one stubborn image cannot stop the rest. +:kill_image + setlocal + REM A dry run does not count, so it prints the same on every machine. + if "%dry_run%" == "ON" ( + call :print_and_run taskkill /F /IM %~1 + endlocal + exit /b 0 + ) + set "img_count=0" + for /f "tokens=1" %%a in ('tasklist /nh /fi "IMAGENAME eq %~1" 2^>nul') do ( + if /I "%%a" == "%~1" set /a img_count+=1 + ) + if "!img_count!" == "0" ( + echo %~1: none running + endlocal + exit /b 0 + ) + echo %~1: stopping !img_count! + call :print_and_run taskkill /F /IM %~1 + endlocal + exit /b 0 + +REM print_and_run <command...> +:print_and_run + echo + %* + REM Recorded before the command runs. A set afterwards would clear the + REM errorlevel the next line returns, reporting every failure as a pass. + set "last_cmd=%*" + if not "%dry_run%" == "ON" ( + %* + exit /b !errorlevel! + ) + exit /b 0 + +REM handle_args <args...> +:handle_args + call :debug_msg starting function handle_args "%*" + if "%~1" == "" exit /b 0 + + setlocal + set arg=%~1 + set finalize_cmd= + + call :debug_msg Begin handling arg "%arg%" + + if not "%arg:~0,2%" == "--" goto :HAL_check_short + + call :debug_msg Processing long arg + + call :find_arg long %arg:~2% + %repeat_error% + set idx=%ret% + + call :get_arg_type %idx% + %repeat_error% + set type=%ret% + + if /I not "%type%" == "bool" ( + set "string_val=%~2" + if not defined string_val ( + echo Error in handle_args_loop: The option "%arg:~2%" requires a value + exit /b 1 + ) + + REM rawstring options carry cmake arguments, which start with a dash, so + REM only plain string options reject a value that looks like an option. + if /I "%type%" == "string" ( + if "!string_val:~0,1!" == "-" ( + echo Error in handle_args_loop: The option "%arg:~2%" requires a value. "!string_val!" looks like another option. + exit /b 1 + ) + ) + shift + ) + + call :set_arg %idx% "%string_val%" + %repeat_error% + goto :handle_args_loop_reset + + :HAL_check_short + if not "%arg:~0,1%" == "-" ( + echo Unknown argument: %arg% + call :print_help_msg + exit /b 1 + ) + + call :debug_msg processing short args + set /A charidx=1 + :short_arg_loop + if "!arg:~%charidx%,1!" == "" goto :handle_args_loop_reset + + call :find_arg short !arg:~%charidx%,1! + %repeat_error% + set idx=%ret% + + call :get_arg_type %idx% + %repeat_error% + set type=%ret% + + set /A start_idx = %charidx% + 1 + if /I not "%type%" == "bool" ( + REM A value may be attached (-j8) or be the next argument (-j 8). + set remaining=!arg:~%start_idx%! + if defined remaining ( + set string_val=!remaining! + ) else ( + set "string_val=%~2" + shift + ) + if not defined string_val ( + echo Error in handle_args_loop: The option "!arg:~%charidx%,1!" requires a value + exit /b 1 + ) + + REM As in the long-option branch above. + if /I "%type%" == "string" ( + if "!string_val:~0,1!" == "-" ( + echo Error in handle_args_loop: The option "!arg:~%charidx%,1!" requires a value. "!string_val!" looks like another option. + exit /b 1 + ) + ) + call :set_arg !idx! "!string_val!" + %repeat_error% + goto :handle_args_loop_reset + ) + + call :set_arg %idx% + %repeat_error% + set /A charidx+=1 + goto :short_arg_loop + + :handle_args_loop_reset + REM endlocal drops this iteration scope; finalize_cmd re-applies the options + REM it set, in the caller scope. + endlocal %finalize_cmd% + shift + goto :handle_args diff --git a/deps/OCCT/OCCT.cmake b/deps/OCCT/OCCT.cmake index b4cd9facd2..62bfcf8e76 100644 --- a/deps/OCCT/OCCT.cmake +++ b/deps/OCCT/OCCT.cmake @@ -1,3 +1,10 @@ +# clang-cl cannot emit IGESAppli_GeneralModule.cxx on ARM64 +# (llvm/llvm-project#62081). cl and clang-cl share an ABI. +set(_occt_compiler_args "") +if ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang) + set(_occt_compiler_args -DCMAKE_C_COMPILER:STRING=cl -DCMAKE_CXX_COMPILER:STRING=cl) +endif () + if(WIN32) set(library_build_type "Shared") else() @@ -31,6 +38,7 @@ orcaslicer_add_cmake_project(OCCT -DBUILD_MODULE_ModelingAlgorithms=OFF -DBUILD_MODULE_ModelingData=OFF -DBUILD_MODULE_Visualization=OFF + ${_occt_compiler_args} ) # add_dependencies(dep_OCCT ${FREETYPE_PKG}) diff --git a/deps/OpenCV/OpenCV.cmake b/deps/OpenCV/OpenCV.cmake index d8c7cbac3b..0290b3f47a 100644 --- a/deps/OpenCV/OpenCV.cmake +++ b/deps/OpenCV/OpenCV.cmake @@ -10,6 +10,13 @@ else () set(_options "") endif () +# carotene is OpenCV's ARM NEON HAL. It uses M_PI without _USE_MATH_DEFINES +# and does not compile with clang-cl. +set(_disable_carotene "") +if ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang) + set(_disable_carotene "-DWITH_CAROTENE=OFF") +endif () + if (IN_GIT_REPO) set(OpenCV_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OpenCV-prefix/src/dep_OpenCV) endif () @@ -83,5 +90,6 @@ orcaslicer_add_cmake_project(OpenCV -DWITH_PROTOBUF=OFF -DWITH_WIN32UI=OFF -DHAVE_WIN32UI=FALSE + ${_disable_carotene} ) diff --git a/deps/OpenSSL/OpenSSL.cmake b/deps/OpenSSL/OpenSSL.cmake index ddeb680052..2fb4b51757 100644 --- a/deps/OpenSSL/OpenSSL.cmake +++ b/deps/OpenSSL/OpenSSL.cmake @@ -6,7 +6,7 @@ if(DEFINED OPENSSL_ARCH) set(_cross_arch ${OPENSSL_ARCH}) else() if(WIN32) - if("${CMAKE_GENERATOR_PLATFORM}" STREQUAL "ARM64") + if("${DEPS_ARCH}" STREQUAL "arm64") set(_cross_arch "VC-WIN64-ARM") else() set(_cross_arch "VC-WIN64A") diff --git a/deps/PNG/PNG.cmake b/deps/PNG/PNG.cmake index 332fdfe274..a73e96a0c9 100644 --- a/deps/PNG/PNG.cmake +++ b/deps/PNG/PNG.cmake @@ -1,6 +1,10 @@ if (APPLE) # Only disable NEON extension for Apple ARM builds, leave it enabled for Raspberry PI. set(_disable_neon_extension "-DPNG_ARM_NEON=off") +elseif ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang) + # libpng's CMake ignores PNG_ARM_NEON on Windows ARM64 and skips the NEON + # sources, but pngpriv.h enables NEON anyway. + set(_disable_neon_extension "-DCMAKE_C_FLAGS=/DWIN32 /D_WINDOWS /DPNG_ARM_NEON_OPT=0") else () set(_disable_neon_extension "") endif () diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index 6ec0ecd9cf..b183df098b 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -4497,7 +4497,7 @@ msgid "" "The value will be reset to 0." msgstr "" -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "" msgid "" @@ -4528,13 +4528,13 @@ msgid "" msgstr "" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index b7eb022c0d..d78487b947 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -4893,7 +4893,7 @@ msgstr "" "\n" "El valor es restablirà a 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "La paret addicional alternativa no funciona bé quan assegurar el gruix de la closca vertical està establert a Tot." msgid "" @@ -4939,7 +4939,7 @@ msgstr "" "NO - Mantenir l'alçada de la capa de suport independent" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height ha de ser més petit que layer_height.\n" @@ -4947,7 +4947,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "La profunditat de bloqueig ha de ser menor que la profunditat de la pell.\n" diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index b521a8073b..7eeb1072c0 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -4851,7 +4851,7 @@ msgstr "" "\n" "Hodnota bude nastavena na 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Střídavá přídavná stěna nefunguje správně, pokud je zajištění tloušťky svislé stěny nastaveno na Vše." msgid "" @@ -4897,7 +4897,7 @@ msgstr "" "NE – Ponechat nezávislou výšku podpůrné vrstvy" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height musí být menší než layer_height.\n" @@ -4905,7 +4905,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Hloubka zamčení musí být menší než hloubka krycí vrstvy.\n" diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index 436966457a..92143b907b 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -4757,7 +4757,7 @@ msgstr "" "\n" "Der Wert wird auf 0 zurückgesetzt." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Der alternative zusätzliche Wandmodus funktioniert nicht gut, wenn die vertikale Wanddicke auf Alle eingestellt ist." msgid "" @@ -4803,7 +4803,7 @@ msgstr "" "NEIN - unabhängige Stütz-Schichthöhen beibehalten" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height muss kleiner als layer_height sein.\n" @@ -4811,7 +4811,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Die Verriegelungstiefe sollte kleiner als die Hauttiefe sein.\n" diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index 88fb455959..eb0a7d774c 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -4493,7 +4493,7 @@ msgid "" "The value will be reset to 0." msgstr "" -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "" msgid "" @@ -4524,13 +4524,13 @@ msgid "" msgstr "" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index 9c5127e50a..23294fddbd 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -4629,7 +4629,7 @@ msgstr "" "\n" "El valor se restablecerá a 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Perímetro adicional alternado no funciona bien cuando \"Garantizar el grosor vertical de las cubiertas\" se establece en Todos." msgid "" @@ -4675,7 +4675,7 @@ msgstr "" "NO - Mantener la altura de capa de soportes independiente" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height debe ser menor que layer_height.\n" @@ -4683,7 +4683,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "La profundidad de bloqueo debe ser menor que la profundidad de piel.\n" diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index 03e6d6685d..a5a49e8325 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -4670,7 +4670,7 @@ msgstr "" "\n" "Balioa 0ra berrezarriko da." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Horma gehigarri txandakatuak ez du ondo funtzionatzen \"Bermatu oskolaren lodiera bertikala\" Guztiak gisa ezarrita dagoenean." msgid "" @@ -4716,7 +4716,7 @@ msgstr "" "NO - 'Euskarrien Geruza-Altuera Independentea' mantendu" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height txigiagoa izan behar da layer_height baino.\n" @@ -4724,7 +4724,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Blokeo-sakonera gainazalaren sakonera baino txikiagoa izan behar da.\n" diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index fc58381106..65589bc5ff 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n==0 || n==1) ? 0 : 1;\n" -"X-Generator: Poedit 3.6\n" +"X-Generator: Poedit 3.9\n" # AI Translated msgid "Main Extruder" @@ -4707,7 +4707,7 @@ msgstr "" "\n" "La valeur sera remise à 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "La paroi supplémentaire alternée ne fonctionne pas bien lorsque le paramètre Assurer l’épaisseur de la coque verticale est réglée sur Tous." msgid "" @@ -4753,7 +4753,7 @@ msgstr "" "NON - Conserver la hauteur de la couche de support indépendante" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height doit être inférieur à la hauteur de couche.\n" @@ -4761,7 +4761,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "La profondeur de verrouillage doit être inférieure à la profondeur de la peau.\n" @@ -10707,7 +10707,7 @@ msgid "Acceleration limitation" msgstr "Limitation d'accélération" msgid "Jerk limitation" -msgstr "Limitation des secousses" +msgstr "Limitation du jerk" msgid "Single extruder multi-material setup" msgstr "Configuration multi-matériaux pour extrudeur unique" @@ -12226,7 +12226,7 @@ msgid "Plate %d: %s does not support filament %s" msgstr "Plaque %d : %s ne prend pas en charge le filament %s" msgid "Setting the jerk speed too low could lead to artifacts on curved surfaces" -msgstr "Un réglage trop bas de la vitesse de saccade peut entraîner des artefacts sur les surfaces courbes" +msgstr "Un réglage trop bas de la vitesse de jerk peut entraîner des artefacts sur les surfaces courbes" msgid "" "The jerk setting exceeds the printer's maximum jerk (machine_max_jerk_x/machine_max_jerk_y).\n" @@ -12234,7 +12234,7 @@ msgid "" "You can adjust the maximum jerk setting in your printer's configuration to get higher speeds." msgstr "" "Le réglage du jerk dépasse le jerk maximum de l’imprimante (machine_max_jerk_x/machine_max_jerk_y).\n" -"Orca plafonne automatiquement la vitesse de l’impulsion pour s’assurer qu’elle ne dépasse pas les capacités de l’imprimante.\n" +"Orca plafonne automatiquement la vitesse du jerk pour s’assurer qu’elle ne dépasse pas les capacités de l’imprimante.\n" "Vous pouvez ajuster le réglage du jerk maximum dans la configuration de votre imprimante pour obtenir des vitesses plus élevées." msgid "" @@ -21188,8 +21188,7 @@ msgstr "" "Divisez vos impressions en plateaux\n" "Saviez-vous que vous pouvez diviser un modèle comportant de nombreuses pièces en plateaux individuels prêts à être imprimés ? Cela simplifie le processus de suivi de toutes les pièces." -#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer -#: Height] +#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer Height] msgid "" "Speed up your print with Adaptive Layer Height\n" "Did you know that you can print a model even faster by using the Adaptive Layer Height option? Check it out!" @@ -21261,8 +21260,7 @@ msgstr "" "Améliorer la solidité\n" "Saviez-vous que vous pouvez définir un plus grand nombre de périmètre et une densité de remplissage plus élevée pour améliorer la résistance du modèle ?" -#: resources/data/hints.ini: [hint:When do you need to print with the printer -#: door opened] +#: resources/data/hints.ini: [hint:When do you need to print with the printer door opened] msgid "" "When do you need to print with the printer door opened?\n" "Did you know that opening the printer door can reduce the probability of extruder/hotend clogging when printing lower temperature filament with a higher enclosure temperature? There is more info about this in the Wiki." diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index 9f8a849884..9d966bf80a 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -4803,7 +4803,7 @@ msgstr "" "\n" "Az érték 0-ra áll vissza." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "A váltakozó extra fal nem működik jól, ha a függőleges héjvastagság biztosítása \"Mind\" értékre van állítva." msgid "" @@ -4849,7 +4849,7 @@ msgstr "" "NEM - Független támasz rétegmagasság megtartása" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "A seam_slope_start_height értékének kisebbnek kell lennie, mint a layer_height.\n" @@ -4857,7 +4857,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "A rögzítési mélységnek kisebbnek kell lennie, mint a felületi réteg mélysége.\n" diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index bc36e08d25..6190d2ab44 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -4805,7 +4805,7 @@ msgstr "" "\n" "Il valore verrà reimpostato a 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Parete aggiuntiva alternativa non funziona bene quando \"Garantisci spessore verticale del guscio\" è impostato su Tutto." msgid "" @@ -4851,7 +4851,7 @@ msgstr "" "NO - Mantieni Altezza strato di supporto indipendente" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height deve essere inferiore a layer_height.\n" @@ -4859,7 +4859,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "La profondità di intersezione deve essere inferiore alla profondità della pelle.\n" diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index 1b70ddbf67..d67a39463d 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -4818,7 +4818,7 @@ msgstr "" "\n" "値は0にリセットされます。" -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "垂直シェル厚さを「すべて」に設定すると、交互追加壁が適切に機能しません。" msgid "" @@ -4864,7 +4864,7 @@ msgstr "" "いいえ - 「独立サポート積層ピッチ」を有効にする" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_heightはlayer_heightより小さくする必要があります。\n" @@ -4872,7 +4872,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "ロック深さはスキン深さより小さくする必要があります。\n" @@ -10617,7 +10617,7 @@ msgid "Junction Deviation" msgstr "接合偏差" msgid "Jerk(XY)" -msgstr "ジャーク(XY)" +msgstr "Jerk(XY)" msgid "Raft" msgstr "ラフト" @@ -10914,7 +10914,7 @@ msgid "Acceleration limitation" msgstr "加速制限" msgid "Jerk limitation" -msgstr "振動特性" +msgstr "Jerk制限" msgid "Single extruder multi-material setup" msgstr "シングルエクストルーダー マルチマテリアル設定" @@ -12485,7 +12485,7 @@ msgid "Plate %d: %s does not support filament %s" msgstr "プレート %d: %s がフィラメント %s を使用できません" msgid "Setting the jerk speed too low could lead to artifacts on curved surfaces" -msgstr "ジャーク速度を低く設定しすぎると曲面にアーティファクトが発生する可能性があります" +msgstr "Jerk速度を低く設定しすぎると曲面にアーティファクトが発生する可能性があります" # AI Translated msgid "" @@ -14750,7 +14750,7 @@ msgid "Klipper's max_accel_to_decel will be adjusted to this %% of acceleration. msgstr "Klipperのmax_accel_to_decelが、加速度のこの%%に調整されます。" msgid "Default jerk." -msgstr "デフォルトジャーク。" +msgstr "デフォルトのJerkです。" # AI Translated msgid "Marlin Firmware Junction Deviation (replaces the traditional XY Jerk setting)." @@ -15841,28 +15841,28 @@ msgid "Maximum acceleration of the E axis" msgstr "E軸最大加速度" msgid "Maximum jerk X" -msgstr "最大振動 X" +msgstr "最大Jerk X" msgid "Maximum jerk Y" -msgstr "最大振動 Y" +msgstr "最大Jerk Y" msgid "Maximum jerk Z" -msgstr "最大振動 Z" +msgstr "最大Jerk Z" msgid "Maximum jerk E" -msgstr "最大振動 E" +msgstr "最大Jerk E" msgid "Maximum jerk of the X axis" -msgstr "最大振動 X" +msgstr "X軸最大Jerk" msgid "Maximum jerk of the Y axis" -msgstr "最大振動 Y" +msgstr "Y軸最大Jerk" msgid "Maximum jerk of the Z axis" -msgstr "最大振動 Z" +msgstr "Z軸最大Jerk" msgid "Maximum jerk of the E axis" -msgstr "最大振動 E" +msgstr "E軸最大Jerk" msgid "Maximum Junction Deviation" msgstr "最大接合偏差" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index 767674e525..e8dfca968f 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -4829,7 +4829,7 @@ msgstr "" "\n" "값이 0으로 재설정됩니다." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "세로 쉘 두께가 모두로 설정된 경우 대체 여분의 벽이 제대로 작동하지 않습니다." msgid "" @@ -4875,7 +4875,7 @@ msgstr "" "아니요 - 독립적 서포트 레이어 높이 유지" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "심_경사_시작_높이는 레이어_높이보다 작아야 합니다.\n" @@ -4883,7 +4883,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "잠금 깊이는 스킨 깊이보다 작아야 합니다.\n" diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index ad5edffc9a..8fd32bdd26 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -4792,7 +4792,7 @@ msgstr "" "\n" "Reikšmė bus atstatyta į 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Alternatyvi papildoma sienelė veikia prastai, kai vertikalaus apvalkalo storio užtikrinimas nustatytas į „Visi“." msgid "" @@ -4838,7 +4838,7 @@ msgstr "" "NE – palikti nepriklausomą atramų sluoksnio aukštį" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "„seam_slope_start_height“ turi būti mažesnis už „layer_height“\n" @@ -4846,7 +4846,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Fiksavimo gylis turėtų būti mažesnis už išorinio sluoksnio gylį.\n" diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index 28ad63d455..96c22fcc8b 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -5217,7 +5217,7 @@ msgstr "" "De waarde wordt teruggezet naar 0." # AI Translated -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Afwisselende extra wand werkt niet goed wanneer 'verticale wanddikte waarborgen' op Alles is ingesteld." # AI Translated @@ -5265,7 +5265,7 @@ msgstr "" # AI Translated msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height moet kleiner zijn dan layer_height.\n" @@ -5274,7 +5274,7 @@ msgstr "" # AI Translated #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "De vergrendeldiepte moet kleiner zijn dan de huiddiepte.\n" diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index e8acc62a9b..4a21d38a27 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -4907,7 +4907,7 @@ msgstr "" "\n" "Wartość zostanie zresetowana do 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Alternatywna dodatkowa ściana działa tylko wtedy, gdy jest wyłączona opcja „zapewnij stałą grubość pionowej powłoki”." msgid "" @@ -4953,7 +4953,7 @@ msgstr "" "NIE - Zachowaj Niezależną wysokość warstwy podpory" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height musi być mniejsza niż wysokość warstwy.\n" @@ -4962,7 +4962,7 @@ msgstr "" # AI Translated #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Głębokość blokady powinna być mniejsza niż głębokość skóry.\n" diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 1b39d4a159..931d3d510b 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -4641,7 +4641,7 @@ msgstr "" "\n" "O valor será redefinido para 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "A parede extra alternada não funciona bem quando a espessura vertical da casca está definida para Todos." msgid "" @@ -4687,7 +4687,7 @@ msgstr "" "NÃO — Manter a Altura da Camada de Suporte Independente" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height precisa ser menor que layer_height.\n" @@ -4695,7 +4695,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "A profundidade do travamento deve ser menor que a profundidade da textura.\n" diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index 2372471707..eb24ebc759 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -4780,7 +4780,7 @@ msgstr "" "\n" "Значение будет сброшено до 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Чередующаяся дополнительная стенка не работает, если для «Сохранение толщины вертикальной оболочки» установлено значение «Все»." msgid "" @@ -4833,7 +4833,7 @@ msgstr "" "Нет – отключить черновую башню" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "Начальная высота не должна превышать высоту слоя.\n" @@ -4841,7 +4841,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Перекрытие должно быть меньше внутренней оболочки.\n" diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index 432000f96d..52972e241a 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -5279,7 +5279,7 @@ msgstr "" "Värdet kommer att återställas till 0." # AI Translated -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Alternerande extra vägg fungerar inte bra när säkerställ vertikal skaltjocklek är inställt på Alla." # AI Translated @@ -5327,7 +5327,7 @@ msgstr "" # AI Translated msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height måste vara mindre än layer_height.\n" @@ -5336,7 +5336,7 @@ msgstr "" # AI Translated #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Låsdjupet bör vara mindre än ytskiktets djup.\n" diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index a0c0079125..4c4ac2d25f 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -4785,7 +4785,7 @@ msgstr "" "\n" "ค่าจะถูกรีเซ็ตเป็น 0" -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "ผนังเสริมสำรองทำงานได้ไม่ดีเมื่อตั้งค่าความหนาของเปลือกแนวตั้งเป็นทั้งหมด" msgid "" @@ -4831,7 +4831,7 @@ msgstr "" "ไม่ - รักษาความสูงของชั้นรองรับที่เป็นอิสระ" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height ต้องเล็กกว่า layer_height\n" @@ -4839,7 +4839,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "ความลึกของล็อคควรน้อยกว่าความลึกของผิวหนัง\n" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index c3deff8bd8..833706ad70 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -4847,7 +4847,7 @@ msgstr "" "\n" "Değer 0'a sıfırlanacaktır." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Alternatif ekstra duvar, dikey kabuk kalınlığının Tümü olarak ayarlandığından emin olunduğunda iyi çalışmaz." msgid "" @@ -4893,7 +4893,7 @@ msgstr "" "HAYIR - Bağımsız Destek Katmanı Yüksekliğini Koruyun" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height, layer_height değerinden küçük olmalıdır.\n" @@ -4901,7 +4901,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Kilit derinliği cilt derinliğinden daha küçük olmalıdır.\n" diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index 9f57b6b71a..df06c01684 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -4781,7 +4781,7 @@ msgstr "" "\n" "Значення буде скинуто на 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Чергування додаткової стінки не працює добре, якщо для параметра \"Забезпечити товщину вертикальної оболонки\" встановлено значення \"Всі\"." msgid "" @@ -4827,7 +4827,7 @@ msgstr "" "НІ - Залишити незалежну висоту шарів підтримки" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height має бути менше висоти шару.\n" @@ -4836,7 +4836,7 @@ msgstr "" # AI Translated #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Глибина зчеплення має бути меншою за глибину оболонки.\n" diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index 00a9a558ba..e78fc51687 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -5040,7 +5040,7 @@ msgstr "" "\n" "Giá trị sẽ được đặt lại về 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Luân phiên wall phụ không hoạt động tốt khi đảm bảo độ dày shell dọc được đặt thành Tất cả." msgid "" @@ -5086,7 +5086,7 @@ msgstr "" "NO - Giữ chiều cao lớp support độc lập" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height cần nhỏ hơn layer_height.\n" @@ -5094,7 +5094,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Độ sâu khóa phải nhỏ hơn độ sâu skin.\n" diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index 133e0815a4..d2f07d19ae 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -4641,7 +4641,7 @@ msgstr "" "\n" "这个数值将被重置为0。" -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "“交替添加额外”与“确保垂直外壳厚度”的”全部“选项不兼容。" msgid "" @@ -4687,7 +4687,7 @@ msgstr "" "否 - 选择保留支撑独立层高" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height需要小于layer_height。\n" @@ -4695,7 +4695,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "锁定深度应小于表皮深度。\n" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index cf6a2519c3..456092deb5 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -4758,7 +4758,7 @@ msgstr "" "\n" "該值將會重設為 0。" -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "當確保垂直外殼厚度設為『全部』時,交錯額外牆壁效果不佳。" msgid "" @@ -4815,7 +4815,7 @@ msgstr "" "否 - 選擇保留獨立支撐層高" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height 必須小於 layer_height。\n" @@ -4824,7 +4824,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "鎖定深度應小於表皮深度。\n" diff --git a/scripts/check_profile.bat b/scripts/check_profile.bat new file mode 100644 index 0000000000..231fa721d4 --- /dev/null +++ b/scripts/check_profile.bat @@ -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% diff --git a/scripts/check_profile.ps1 b/scripts/check_profile.ps1 new file mode 100644 index 0000000000..5c5dcb20c8 --- /dev/null +++ b/scripts/check_profile.ps1 @@ -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 +} diff --git a/scripts/check_profile.sh b/scripts/check_profile.sh new file mode 100755 index 0000000000..9b841d0612 --- /dev/null +++ b/scripts/check_profile.sh @@ -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 diff --git a/scripts/test_build_win.ps1 b/scripts/test_build_win.ps1 new file mode 100644 index 0000000000..62eaaae77b --- /dev/null +++ b/scripts/test_build_win.ps1 @@ -0,0 +1,847 @@ +<# +.SYNOPSIS + Tests build_win.bat's option handling and the commands it generates. + +.DESCRIPTION + Cases run the script with --dry-run, so nothing is configured, built or + deleted and the suite finishes in seconds. Each case asserts on the exit + code and on the command lines the script echoes. + + Adding a case means adding one row to $cases. A bare string starts a new + group. Defaults: ExpectExit is 0 and --dry-run is appended, so a row only + states what is unusual about it. + + Name what the case proves, in words + Args arguments, as an array + ExpectExit expected exit code (default 0) + DryRun append --dry-run (default $true) + First regex the first output line must match + Env environment for this case only + Contains literal strings the output must have + NotContains literal strings it must not have + Match regexes; each must match at least one output line + NotMatch regexes; none may match any output line + NotExists paths that must not exist after the case runs + +.PARAMETER Name + Run only the cases whose name matches this regex. Headings with no + matching case are not printed, and a pattern that matches nothing is a + failure rather than an empty pass. + +.EXAMPLE + powershell -File scripts/test_build_win.ps1 + +.EXAMPLE + powershell -File scripts/test_build_win.ps1 -Name solution +#> +[CmdletBinding()] +param( + [string] $Script, + [string] $Name +) + +$ErrorActionPreference = 'Stop' + +if (-not $Script) { + $here = $PSScriptRoot + if (-not $here) { $here = Split-Path -Parent $MyInvocation.MyCommand.Path } + $Script = Join-Path (Split-Path -Parent $here) 'build_win.bat' +} +if (-not (Test-Path $Script)) { throw "build_win.bat not found at $Script" } + +# cmd resumes a batch file by byte offset after `call :label`, and with LF +# endings that offset lands wrong and the label lookup fails. .gitattributes +# pins CRLF; this catches a checkout or an editor that did not honour it. +if ((Get-Content -Raw $Script) -match "(?<!`r)`n") { + throw "$Script has LF line endings; cmd needs CRLF to resume after call :label" +} +$Script = (Resolve-Path $Script).Path + +# Read the long options out of the script itself, so this cannot go stale when +# an option is added. Field order is: add_arg <var> <type> <short> <long>. +$longFlags = @( + Select-String -Path $Script -Pattern '^call :add_arg \S+ \S+ \S+ (\S+) ' | + ForEach-Object { '--' + $_.Matches[0].Groups[1].Value } +) +if ($longFlags.Count -lt 20) { throw "only found $($longFlags.Count) options in $Script; the parser above is wrong" } + +# A winget that always fails, so the prerequisite failure path runs without +# touching the machine. It has to be an .exe: a .bat invoked without `call` +# transfers control and never comes back, which would end the script instead. +# where.exe returns 1 when its patterns match nothing and never prompts. +$fixtures = Join-Path ([IO.Path]::GetTempPath()) 'build_win_test_fixtures' +New-Item -ItemType Directory -Force -Path $fixtures | Out-Null +Copy-Item "$env:SystemRoot\System32\where.exe" (Join-Path $fixtures 'winget.exe') -Force +$stubPath = "$fixtures;C:\Windows\system32;C:\Windows" + +# Stand-in ninjas that only report a version, so the 1.12 boundary in the +# progress format can be exercised on a machine whose real ninja is newer. +# A dry run skips the dev shell, so PATH here is what the script sees. +$ninjaPaths = @{} +foreach ($v in @{ old = '1.11.1'; new = '1.12.0' }.GetEnumerator()) { + $d = Join-Path $fixtures "ninja-$($v.Key)" + New-Item -ItemType Directory -Force -Path $d | Out-Null + Set-Content -Path (Join-Path $d 'ninja.bat') -Encoding ascii -Value @('@echo off', "echo $($v.Value)") + $ninjaPaths[$v.Key] = "$d;$env:PATH" +} + +# The pack stamp is checked against real dates, so a locale-dependent parse +# in the script cannot pass by looking date-shaped. Yesterday is accepted too, +# so a run that crosses midnight does not flake. +$dateStamps = @((Get-Date -Format 'yyyyMMdd'), (Get-Date).AddDays(-1).ToString('yyyyMMdd')) +$stampPattern = '_(' + ($dateStamps -join '|') + ')\.zip$' + +$cases = @( + 'argument handling' + @{ Name = 'no arguments prints help'; Args = @(); DryRun = $false + Contains = @('Usage: build_win.bat [options]', '--clang-cl') } + @{ Name = "--help lists all $($longFlags.Count) options the script defines"; Args = @('--help'); DryRun = $false + Contains = $longFlags } + @{ Name = 'help is grouped and shows usage, examples and environment'; Args = @('--help'); DryRun = $false + Contains = @('Usage: build_win.bat [options]', 'Actions:', 'Build configuration:', 'Toolchain:', + 'How much gets rebuilt:', 'Paths and extra arguments:', 'Diagnostics:', + 'Examples:', 'Environment:') } + @{ Name = 'the environment section shows what to set'; Args = @('--help'); DryRun = $false + Contains = @('ORCA_DEPS_CMAKE_ARGS', 'ORCA_SLICER_CMAKE_ARGS', 'ORCA_UPDATER_SIG_KEY', 'NINJA_STATUS', + 'set ORCA_SLICER_CMAKE_ARGS=-DSLIC3R_PCH=OFF', '(PowerShell)', 'debugscript') } + @{ Name = 'section headers do not widen the flag column'; Args = @('--help'); DryRun = $false + Match = @('^ -d, --deps +Download') } + # Windows Terminal opens at 120 columns and wraps at 120, so 119 is the + # limit. Anyone still on the old conhost gets 80 and will see wrapping. + @{ Name = 'every help line fits a 120 column console'; Args = @('--help'); DryRun = $false + NotMatch = @('^.{120,}$') } + @{ Name = 'an unknown long option is rejected'; Args = @('--nonsense'); ExpectExit = 1 + Contains = @('Failed to find arg') } + @{ Name = 'an unknown short option is rejected'; Args = @('-Z'); ExpectExit = 1 + Contains = @('Failed to find arg') } + @{ Name = 'a bare argument is rejected'; Args = @('deps'); ExpectExit = 1 + Contains = @('Unknown argument') } + @{ Name = 'an unknown architecture is rejected'; Args = @('-d', '--arch', 'sparc'); ExpectExit = 1 + Contains = @('Unknown architecture') } + @{ Name = 'a string option without a value is rejected'; Args = @('-d', '--arch'); ExpectExit = 1 + Contains = @('requires a value') } + @{ Name = 'short options can be bundled'; Args = @('-dx') + Contains = @('-G "Ninja Multi-Config"', '--target deps') } + + 'generator and compiler selection' + @{ Name = 'deps default to the Visual Studio generator'; Args = @('-d') + Contains = @('-G "Visual Studio', '-A x64', '--target deps') + NotContains = @('Ninja', 'clang-cl') } + @{ Name = '-x selects Ninja without changing compiler'; Args = @('-d', '-x') + Contains = @('-G "Ninja Multi-Config"') + NotContains = @('clang-cl', '-A x64') } + @{ Name = '-l -x builds with clang-cl under Ninja'; Args = @('-d', '-l', '-x') + Contains = @('-G "Ninja Multi-Config"', '-DCMAKE_C_COMPILER=clang-cl.exe', '-DCMAKE_CXX_COMPILER=clang-cl.exe') } + @{ Name = '-l alone uses the ClangCL toolset on the VS generator'; Args = @('-d', '-l') + Contains = @('-G "Visual Studio', '-T ClangCL') + NotContains = @('-DCMAKE_C_COMPILER') } + # --msvc and --msbuild name the defaults, so a caller can be explicit and a + # contradictory pair can be caught rather than silently resolved. + @{ Name = '--msvc is the compiler default spelled out'; Args = @('-d', '--msvc') + Contains = @('-G "Visual Studio', '-A x64') + NotContains = @('clang-cl') } + @{ Name = '--msbuild is the generator default spelled out'; Args = @('-d', '--msbuild') + Contains = @('-G "Visual Studio') + NotContains = @('Ninja') } + @{ Name = '--msvc with -x gives Ninja driving cl'; Args = @('-d', '--msvc', '-x') + Contains = @('-G "Ninja Multi-Config"') + NotContains = @('clang-cl') } + @{ Name = '--msbuild with -l gives the VS generator and the ClangCL toolset'; Args = @('-d', '--msbuild', '-l') + Contains = @('-G "Visual Studio', '-T ClangCL') } + # A developer with a standalone LLVM points at it; the VS-bundled clang + # is what a bare clang-cl.exe resolves to after the dev shell runs. + @{ Name = '--clang-path names the compiler, quoted for its spaces'; Args = @('-d', '-x', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe') + Contains = @('-DCMAKE_C_COMPILER="C:\Program Files\LLVM\bin\clang-cl.exe"', + '-DCMAKE_CXX_COMPILER="C:\Program Files\LLVM\bin\clang-cl.exe"') } + @{ Name = '--clang-path is a clang request on its own'; Args = @('-d', '-x', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe') + Contains = @('deps/build-clang') } + @{ Name = '--clang-path needs Ninja to take effect'; Args = @('-d', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe'); ExpectExit = 1 + Contains = @('needs the Ninja generator') } + # `exist` is true for a directory too, and a directory would reach CMake + # as the compiler. + @{ Name = '--clang-path must name the exe, not its folder'; Args = @('-d', '-x', '--clang-path', 'C:\Program Files\LLVM'); ExpectExit = 1 + Contains = @('is a directory') } + @{ Name = 'a clang-cl that is not there is caught early'; Args = @('-d', '-x', '--clang-path', 'C:\nope\clang-cl.exe'); ExpectExit = 1 + Contains = @('No clang-cl at') + NotContains = @('cmake -S deps') } + @{ Name = '--clang-path contradicting --msvc is rejected'; Args = @('-d', '-x', '--msvc', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe'); ExpectExit = 1 + Contains = @('select different compilers') } + @{ Name = '--clang-cl and --msvc together are rejected'; Args = @('-d', '-l', '--msvc'); ExpectExit = 1 + Contains = @('select different compilers') } + @{ Name = '--ninja and --msbuild together are rejected'; Args = @('-d', '-x', '--msbuild'); ExpectExit = 1 + Contains = @('select different generators') } + # One option with a value, driven by a table, rather than a flag per + # release. Adding a release should not need a new flag. + @{ Name = '--vs 2019 pins that release and skips autodetect'; Args = @('-d', '--vs', '2019') + Contains = @('-G "Visual Studio 16 2019"') + NotContains = @('Detecting Visual Studio') } + @{ Name = '--vs 2022 pins that release'; Args = @('-d', '--vs', '2022') + Contains = @('-G "Visual Studio 17 2022"') } + @{ Name = '--vs 2026 pins that release'; Args = @('-d', '--vs', '2026') + Contains = @('-G "Visual Studio 18 2026"') } + @{ Name = 'an unknown release is rejected and the known ones listed'; Args = @('-d', '--vs', '2015'); ExpectExit = 1 + Contains = @('Unknown Visual Studio release', '2019, 2022, 2026') } + @{ Name = '--vs and --ninja together are rejected'; Args = @('-d', '--vs', '2022', '-x'); ExpectExit = 1 + Contains = @('select different generators') } + @{ Name = 'without --vs the release is autodetected'; Args = @('-d') + Contains = @('Detecting Visual Studio') } + + 'architecture' + @{ Name = 'x64 is the default'; Args = @('-d') + Contains = @('Configuration: Release, x64') + NotContains = @('build-arm64') } + @{ Name = 'arm64 sets the generator platform and deps tree'; Args = @('-d', '--arch', 'arm64') + Contains = @('-A ARM64', 'deps/build-arm64') } + @{ Name = 'the architecture is matched case-insensitively'; Args = @('-d', '--arch', 'ARM64') + Contains = @('-A ARM64', 'deps/build-arm64') } + @{ Name = 'arm64 under Ninja has no -A but keeps the arm64 tree'; Args = @('-d', '--arch', 'arm64', '-x', '-l') + Contains = @('deps/build-clang-arm64', '-DCMAKE_C_COMPILER=clang-cl.exe') + NotContains = @('-A ') } + + 'build configurations' + # One option with a value, driven by a table, so Release is named rather + # than being whatever is left when no flag is passed. + @{ Name = 'release is the default'; Args = @('-s') + Contains = @('-DCMAKE_BUILD_TYPE=Release', 'cmake -B "build" ') } + @{ Name = '--config release is the default spelled out'; Args = @('-s', '--config', 'release') + Contains = @('-DCMAKE_BUILD_TYPE=Release', 'cmake -B "build" ') } + @{ Name = '--config debug builds into build-dbg'; Args = @('-s', '--config', 'debug') + Contains = @('-DCMAKE_BUILD_TYPE=Debug', 'cmake -B "build-dbg" ') } + @{ Name = '--config relwithdebinfo builds into build-dbginfo'; Args = @('-s', '--config', 'relwithdebinfo') + Contains = @('-DCMAKE_BUILD_TYPE=RelWithDebInfo', 'cmake -B "build-dbginfo" ') } + @{ Name = '--config minsizerel builds into build-minsize'; Args = @('-s', '--config', 'minsizerel') + Contains = @('-DCMAKE_BUILD_TYPE=MinSizeRel', 'cmake -B "build-minsize" ') } + # Batch variable names are case-insensitive, so the table lookup is too. + @{ Name = 'the configuration name is matched case-insensitively'; Args = @('-s', '--config', 'RelWithDebInfo') + Contains = @('-DCMAKE_BUILD_TYPE=RelWithDebInfo') } + @{ Name = 'an unknown configuration is rejected and the known ones listed'; Args = @('-s', '--config', 'bogus'); ExpectExit = 1 + Contains = @('Unknown configuration', 'release, debug, relwithdebinfo, minsizerel') } + # Release, RelWithDebInfo and MinSizeRel all link the /MD dependencies, so + # one tree serves all three. Debug is /MDd and cannot share. + @{ Name = 'minsizerel builds against the release deps'; Args = @('-d', '-s', '--config', 'minsizerel') + Contains = @('cmake -S deps -B "deps/build"', 'cmake -B "build-minsize" ') + NotContains = @('deps/build-minsize') } + @{ Name = 'relwithdebinfo builds against them too'; Args = @('-d', '-s', '-l', '--config', 'relwithdebinfo') + Contains = @('cmake -S deps -B "deps/build-clang"', 'cmake -B "build-dbginfo-clang" ') + NotContains = @('deps/build-dbginfo') } + @{ Name = 'debug keeps a dependency tree of its own'; Args = @('-d', '--config', 'debug') + Contains = @('cmake -S deps -B "deps/build-dbg"') } + # The build tree and the deps tree now have different names, so the + # derivation from the binary directory name cannot work and it is named. + @{ Name = 'a shared deps tree is named outright'; Args = @('-s', '-l', '--config', 'relwithdebinfo') + Match = @('-DDEP_BUILD_DIR="[A-Za-z]:\\.*\\deps\\build-clang"') } + @{ Name = 'configuration and arch combine into build-dbg-arm64'; Args = @('-s', '--config', 'debug', '--arch', 'arm64') + Contains = @('cmake -B "build-dbg-arm64" ') } + + 'what gets built' + @{ Name = 'the deps target defaults to deps'; Args = @('-d') + Contains = @('--target deps') } + @{ Name = '-t overrides the deps target'; Args = @('-d', '-t', 'dep_Boost') + Contains = @('--target dep_Boost') } + @{ Name = 'unit tests are off unless asked for'; Args = @('-s') + Contains = @('-DBUILD_TESTS=OFF') } + @{ Name = '--tests turns the unit tests on'; Args = @('-s', '--tests') + Contains = @('-DBUILD_TESTS=ON') } + @{ Name = '-a enables ASAN for the slicer'; Args = @('-s', '-a') + Contains = @('-DSLIC3R_ASAN=ON') } + @{ Name = 'the slicer build runs gettext'; Args = @('-s') + Contains = @('run_gettext.bat') } + # tools\7z.exe needs a 7z.dll beside it, which the repo does not carry, + # so the pack falls back to the bsdtar Windows ships. Either is correct; + # what matters is that one is chosen and handed the right names. + @{ Name = '-p packs the deps tree with whichever archiver is usable'; Args = @('-d', '-p') + Match = @('^\+ .*(7z\.exe a|tar\.exe -a -c -f) OrcaSlicer_dep_win-\S+\.zip OrcaSlicer_dep$') } + # The bundle is only good for what built it, so it carries the same three + # axes as the tree. Release x64 on cl keeps the historical plain name. + @{ Name = 'the bundle name carries compiler and deps flavour'; Args = @('-p', '-l', '--config', 'debug', '--arch', 'arm64') + Contains = @('OrcaSlicer_dep_win-ARM64-clang-dbg_') } + @{ Name = 'a relwithdebinfo pack is the release bundle'; Args = @('-p', '--config', 'relwithdebinfo') + Contains = @('OrcaSlicer_dep_win-x64_') + NotContains = @('-dbg', 'dbginfo') } + @{ Name = 'a plain release bundle keeps its old name'; Args = @('-p') + Contains = @('OrcaSlicer_dep_win-x64_') + NotContains = @('-clang', '-Release') } + @{ Name = 'the bundle is stamped with today, not a shuffled date'; Args = @('-p') + Match = @($stampPattern) } + # powershell.exe is not in System32 itself, so a trimmed PATH used to + # leave the stamp empty and the bundle named OrcaSlicer_dep_win-x64_.zip. + @{ Name = 'the bundle is stamped even with a bare PATH'; Args = @('-p') + Env = @{ PATH = 'C:\Windows\system32;C:\Windows' } + Match = @($stampPattern) } + @{ Name = '-p packs without rebuilding'; Args = @('-p') + Match = @('^\+ .*(7z\.exe a|tar\.exe -a -c -f) ') + NotContains = @('cmake -S deps') } + @{ Name = 'deps and slicer build in one invocation'; Args = @('-d', '-s', '-x', '-l') + Contains = @('cmake -S deps', 'cmake -B "build-clang" ') } + + 'the developer loop' + @{ Name = '--slicer-target builds one target'; Args = @('-s', '--slicer-target', 'libslic3r') + Contains = @('--config Release --target libslic3r') } + @{ Name = '--no-configure skips the slicer configure'; Args = @('-s', '--no-configure') + Contains = @('cmake --build "build"') + NotContains = @('cmake -B "build" ') } + @{ Name = '--no-configure skips the deps configure'; Args = @('-d', '--no-configure') + Contains = @('cmake --build "deps/build"') + NotContains = @('cmake -S deps') } + @{ Name = '--no-gettext skips the translation step'; Args = @('-s', '--no-gettext') + Contains = @('cmake --build "build"') + NotContains = @('run_gettext') } + # Installing copies the whole tree again for a layout only releases need, + # so it is asked for rather than assumed. + @{ Name = 'nothing is installed unless asked'; Args = @('-s') + Contains = @('run_gettext') + NotContains = @('--target install') } + @{ Name = '-i adds the install step'; Args = @('-s', '-i') + Contains = @('--target install') } + # install(TARGETS OrcaSlicer) has no OPTIONAL, so this would fail partway + # through a build instead of before it. + @{ Name = 'installing a tree with no executable is refused'; Args = @('-s', '-i', '--slicer-target', 'glad'); ExpectExit = 1 + Contains = @('--install needs the executable') + NotContains = @('cmake --build') } + # Without -s there is no install step, so there is nothing to refuse. + @{ Name = 'the same flags without a slicer build are left alone'; Args = @('-d', '-i', '--slicer-target', 'glad') + Contains = @('cmake -S deps') + NotContains = @('--install needs the executable') } + @{ Name = 'naming the executable target is allowed'; Args = @('-s', '-i', '--slicer-target', 'OrcaSlicer') + Contains = @('--target install') } + # Ninja counts compilers, so -j goes on the command line. MSBuild's -j + # counts projects while /MP still runs one cl per core inside each, so the + # cap goes to CL_MPCount in the environment instead. A dry run can only + # show that no -j is passed. + @{ Name = '-j on Ninja passes -j to cmake'; Args = @('-s', '-x', '-j', '4') + Contains = @('Parallel jobs: 4', '--config Release -j 4') } + @{ Name = '-j on Ninja reaches the deps build'; Args = @('-d', '-x', '-j', '2') + Contains = @('--target deps -j 2') } + @{ Name = '-j on MSBuild does not pass -j, which would count projects'; Args = @('-s', '-j', '4') + Contains = @('Parallel jobs: 4') + NotContains = @('-j 4') } + @{ Name = '-j on MSBuild leaves the deps build without -j too'; Args = @('-d', '-j', '2') + Contains = @('--target deps') + NotContains = @('-j 2') } + @{ Name = 'no -j means no job limit'; Args = @('-s') + NotContains = @('parallel jobs', '-j ') } + @{ Name = 'a non-numeric -j is rejected'; Args = @('-s', '-j', 'abc'); ExpectExit = 1 + Contains = @('Invalid --jobs value') } + @{ Name = 'a zero -j is rejected'; Args = @('-s', '-j', '0'); ExpectExit = 1 + Contains = @('Invalid --jobs value') } + @{ Name = 'the loop options combine into a single build command'; Args = @('-s', '-x', '--no-configure', '--no-gettext', '--slicer-target', 'libslic3r_tests', '-j', '8') + Contains = @('--target libslic3r_tests -j 8') + NotContains = @('cmake -B "build" ', 'run_gettext', '--target install') } + + 'one tree per configuration, compiler and architecture' + # CMake resets its cache and carries on when the compiler changes under an + # existing tree, leaving stamps from the old toolchain. Give each its own. + @{ Name = 'clang builds land in their own trees'; Args = @('-d', '-s', '-l') + Contains = @('cmake -S deps -B "deps/build-clang"', 'cmake -B "build-clang" ') } + @{ Name = 'MSVC keeps the historical plain names'; Args = @('-d', '-s', '--msvc') + Contains = @('cmake -S deps -B "deps/build"', 'cmake -B "build" ') + NotContains = @('build-clang') } + @{ Name = 'configuration, compiler and arch all name the tree'; Args = @('-d', '-s', '-l', '--config', 'debug', '--arch', 'arm64') + Contains = @('cmake -S deps -B "deps/build-dbg-clang-arm64"', 'cmake -B "build-dbg-clang-arm64" ') } + + 'locating the dependency tree' + # Without --deps-dir nothing is passed, so CMakeLists derives the path from + # the binary directory name as it always has. + @{ Name = 'the deps path is left to CMake by default'; Args = @('-s') + NotContains = @('-DDEP_BUILD_DIR') } + @{ Name = '--deps-dir tells the slicer where the deps are'; Args = @('-s', '--deps-dir', 'D:\orca-deps') + Contains = @('-DDEP_BUILD_DIR="D:\orca-deps"') } + @{ Name = '--deps-dir also redirects the deps build'; Args = @('-d', '--deps-dir', 'D:\orca-deps') + Contains = @('cmake -S deps -B "D:\orca-deps"', 'cmake --build "D:\orca-deps"') + NotContains = @('deps/build') } + # Paths are quoted throughout, so one with spaces survives the whole run. + @{ Name = 'a deps path with spaces survives'; Args = @('-d', '-s', '--deps-dir', 'C:\Program Files\deps') + Contains = @('-B "C:\Program Files\deps"', '-DDEP_BUILD_DIR="C:\Program Files\deps"') } + @{ Name = '--deps-dir also redirects the pack'; Args = @('-p', '--deps-dir', 'D:\orca-deps') + Contains = @('cd /d "D:\orca-deps"') } + @{ Name = 'pack uses an absolute default path'; Args = @('-p') + Match = @('^\+ cd /d "[A-Za-z]:\\.*\\deps\\build"') } + # CMakeLists derives DEP_BUILD_DIR from the build directory's name, so + # pointing the build elsewhere has to name the deps tree outright. + @{ Name = '--build-dir moves the slicer build'; Args = @('-s', '-l', '-x', '--build-dir', 'out/build/x64-clang') + Contains = @('cmake -B "out/build/x64-clang" ') } + @{ Name = '--build-dir still names the deps tree'; Args = @('-s', '-l', '-x', '--build-dir', 'out/build/x64-clang') + Match = @('-DDEP_BUILD_DIR="[A-Za-z]:\\.*\\deps\\build-clang"') } + @{ Name = '--deps-dir wins over the derived tree'; Args = @('-s', '--build-dir', 'out/build/x64-clang', '--deps-dir', 'D:\orca-deps') + Contains = @('-DDEP_BUILD_DIR="D:\orca-deps"') } + # A value ending in a backslash escapes the closing quote it is spliced + # into, so cmake would receive D:\tree" and swallow the next argument. + @{ Name = 'a trailing backslash is trimmed off a path'; Args = @('-s', '--build-dir', 'D:\tree\') + Contains = @('cmake -B "D:\tree" ') } + @{ Name = 'a build directory with spaces survives'; Args = @('-s', '--build-dir', 'C:\Program Files\tree') + Contains = @('cmake -B "C:\Program Files\tree" ') } + @{ Name = 'the default build names no deps tree'; Args = @('-s') + NotContains = @('DEP_BUILD_DIR') } + + 'saying what mode and toolchain are in play' + @{ Name = 'a dry run announces itself before the first command'; Args = @('-u') + First = '^Dry run: printing commands without running them\.$' } + @{ Name = 'the announcement leads even a full build'; Args = @('-ds') + First = '^Dry run: ' } + # Autodetect only runs for the Visual Studio generator, and only when no + # release was pinned, so it needs a Visual Studio on the machine. + @{ Name = 'the detected Visual Studio names its release year'; Args = @('-s') + Match = @('^Detected Visual Studio \d+ \(20\d\d\)$') } + @{ Name = 'pinning a release skips detection'; Args = @('-s', '--vs', '2022') + NotContains = @('Detected Visual Studio') } + + 'an action has to be asked for' + # Neither can happen without building the slicer, so they stand alone the + # way --install-vs does. + @{ Name = '--run-tests is an action on its own'; Args = @('--run-tests') + Contains = @('-DBUILD_TESTS=ON', 'ctest --test-dir') + NotContains = @('Nothing to do') } + @{ Name = '--tests is too'; Args = @('--tests') + Contains = @('-DBUILD_TESTS=ON') + NotContains = @('Nothing to do', 'ctest --test-dir') } + # Naming an action means that action, not a fuller build. + @{ Name = 'they do not add a slicer build to one already asked for'; Args = @('-d', '--tests') + Contains = @('cmake -S deps') + NotContains = @('cmake -B "build"') } + @{ Name = 'shaping options alone are not an action'; Args = @('--config', 'debug'); ExpectExit = 1 + Contains = @('Nothing to do.') + NotContains = @('Build completed') } + @{ Name = '-j alone is not an action either'; Args = @('-j', '4'); ExpectExit = 1 + Contains = @('Nothing to do.') } + @{ Name = '--install-vs counts as an action on its own'; Args = @('--install-vs', 'ide') + NotContains = @('Nothing to do.') } + + 'diagnostics' + @{ Name = '-v asks cmake for the command lines'; Args = @('-s', '-v') + Contains = @('--verbose') } + @{ Name = '-v applies to the deps build too'; Args = @('-d', '-v') + Contains = @('--target deps', '--verbose') } + # ninja's own default shows neither a percentage nor a time. %w and %W + # need ninja 1.12, and an unknown placeholder is fatal, so the format is + # chosen from the version rather than hardcoded. + @{ Name = '-v names the ninja progress format'; Args = @('-s', '-x', '-v') + Match = @('^Ninja progress format: \[.*%p.*\]') } + @{ Name = 'a ninja older than 1.12 gets the format it understands'; Args = @('-s', '-x', '-v') + Env = @{ PATH = $ninjaPaths['old'] } + Contains = @('Ninja progress format: [%s/%t %p :: %e]') } + @{ Name = 'ninja 1.12 gets elapsed and remaining'; Args = @('-s', '-x', '-v') + Env = @{ PATH = $ninjaPaths['new'] } + Contains = @('Ninja progress format: [%f/%t %p :: %w / %W]') } + @{ Name = 'a format you set yourself is left alone'; Args = @('-s', '-x', '-v') + Env = @{ NINJA_STATUS = '[mine] ' } + Contains = @('Ninja progress format: [mine]') } + @{ Name = 'MSBuild builds mention no ninja format'; Args = @('-s', '-v') + NotContains = @('Ninja progress format') } + @{ Name = 'builds are quiet without -v'; Args = @('-s') + NotContains = @('--verbose') } + + 'installing prerequisites' + # -u installs CMake, Perl and Git. Visual Studio is a separate ask, because + # most people already have it, and which one you want is a real choice. + @{ Name = '-u alone installs no Visual Studio'; Args = @('-u') + Contains = @('Kitware.CMake', 'StrawberryPerl', 'Git.Git') + NotContains = @('Microsoft.VisualStudio') } + # Asking for Visual Studio is asking to install prerequisites, so it does + # not also need -u; requiring both meant --install-vs alone did nothing. + @{ Name = '--install-vs works without -u'; Args = @('--install-vs', 'ide') + Contains = @('id=Microsoft.VisualStudio.Community', 'Kitware.CMake', 'Git.Git') } + @{ Name = '--install-vs buildtools asks for the build tools'; Args = @('-u', '--install-vs', 'buildtools') + Contains = @('id=Microsoft.VisualStudio.BuildTools') + NotContains = @('VC.CoreIde') } + @{ Name = '--install-vs ide asks for Community with the IDE component'; Args = @('-u', '--install-vs', 'ide') + Contains = @('id=Microsoft.VisualStudio.Community', 'VC.CoreIde') } + @{ Name = 'an unknown edition is rejected and the known ones listed'; Args = @('-u', '--install-vs', 'bogus'); ExpectExit = 1 + Contains = @('Unknown Visual Studio edition', 'buildtools, ide') } + @{ Name = '--vs picks which release to install'; Args = @('-u', '--vs', '2022', '--install-vs', 'buildtools') + Contains = @('id=Microsoft.VisualStudio.2022.BuildTools') } + @{ Name = '-l adds the clang compiler and the MSBuild toolset'; Args = @('-u', '--install-vs', 'buildtools', '-l') + Contains = @('VC.Llvm.Clang ', 'VC.Llvm.ClangToolset') } + # CMake 4.x breaks Boost.Context on ARM64, so the installer pins 3.31 there + # and leaves x64 on the current release. + @{ Name = 'CMake is pinned to 3.31 when installing for arm64'; Args = @('-u', '--arch', 'arm64') + Contains = @('Kitware.CMake --version 3.31.8') } + @{ Name = 'CMake is not pinned for x64'; Args = @('-u', '--arch', 'x64') + Contains = @('Kitware.CMake') + NotContains = @('--version') } + @{ Name = 'installing for arm64 asks for the ARM64 toolset'; Args = @('--install-vs', 'buildtools', '--arch', 'arm64') + Contains = @('Microsoft.VisualStudio.Component.VC.Tools.ARM64') } + @{ Name = 'an x64 install asks only for the x64 toolset'; Args = @('--install-vs', 'buildtools') + Contains = @('Microsoft.VisualStudio.Component.VC.Tools.x86.x64') + NotContains = @('VC.Tools.ARM64') } + + 'dry run changes nothing' + # clean_tree resolves the path before removing it, so the line echoed is + # absolute. It is the last thing printed before a directory goes. + @{ Name = '-c echoes the deps rmdir rather than running it'; Args = @('-d', '-c') + Match = @('^\+ rmdir /S /Q "[A-Za-z]:\\.*\\deps\\build"$') } + @{ Name = '-c removes the tree --deps-dir named, not the default one'; Args = @('-d', '-c', '--deps-dir', 'D:\orca-deps') + Match = @('^\+ rmdir /S /Q "D:\\orca-deps"$') + NotContains = @('\deps\build') } + @{ Name = '-c echoes the slicer rmdir rather than running it'; Args = @('-s', '-c') + Match = @('^\+ rmdir /S /Q "[A-Za-z]:\\.*\\build"$') } + @{ Name = 'cleaning a slicer build leaves the deps tree alone'; Args = @('-s', '-c') + NotMatch = @('rmdir.*\\deps\\') } + @{ Name = 'cleaning both builds removes both trees'; Args = @('-d', '-s', '-c') + Match = @('^\+ rmdir /S /Q "[A-Za-z]:\\[^"]*\\deps\\build"$', + '^\+ rmdir /S /Q "[A-Za-z]:\\(?!.*\\deps\\)[^"]*\\build"$') } + + # Nothing below should be reachable. They are a floor under a bug that + # hands clean_tree a path far shorter than it looks. + @{ Name = 'a configuration whose lookup comes back empty is rejected'; Args = @('-d', '-c', '--config', 'release '); ExpectExit = 1 + Contains = @('Unknown configuration') + NotContains = @('rmdir') } + @{ Name = 'a drive root is refused'; Args = @('-d', '-c', '--deps-dir', 'D:\'); ExpectExit = 1 + Contains = @('that is a drive root') + NotContains = @('+ rmdir') } + @{ Name = 'the repository itself is refused'; Args = @('-d', '-c', '--deps-dir', '.'); ExpectExit = 1 + Contains = @('that is the repository itself') + NotContains = @('+ rmdir') } + @{ Name = '-k echoes the taskkills rather than running them'; Args = @('-k') + Contains = @('+ taskkill /F /IM MSBuild.exe', '+ taskkill /F /IM cl.exe') } + @{ Name = '-k also covers the Ninja toolchain'; Args = @('-k') + Contains = @('+ taskkill /F /IM ninja.exe', '+ taskkill /F /IM clang-cl.exe') } + @{ Name = '-k announces the dry run like every other action'; Args = @('-k') + First = '^Dry run: ' } + @{ Name = '-u echoes the winget installs rather than running them'; Args = @('-u') + Contains = @('+ winget install', 'Kitware.CMake') + NotContains = @('cmake -S deps') } + # Not a dry run: winget is a stub that fails, so nothing is installed. + @{ Name = 'a failed install is reported, not claimed as success'; Args = @('-u') + DryRun = $false; ExpectExit = 1 + Env = @{ PATH = $stubPath } + Contains = @('Failed to install:', 'CMake', 'Perl', 'Git') + NotContains = @('Installed the prerequisites') } + # The reason belongs inside the frame. Every command this path ran had + # already succeeded, so naming the last one would point at the wrong thing. + @{ Name = 'a failed install names the reason, not the last command'; Args = @('-u') + DryRun = $false; ExpectExit = 1 + Env = @{ PATH = $stubPath } + Contains = @('####', 'Failed to install:') + NotContains = @('Failed: winget') } + @{ Name = 'a dry run does not claim the install happened'; Args = @('-u') + Contains = @('Dry run: nothing was installed.') + NotContains = @('are in place') } + @{ Name = '-u with a build defers that build, not a fuller one'; Args = @('-u', '-d') + Contains = @('build_win.bat -d') + NotContains = @('build_win.bat -ds') } + @{ Name = '-u on its own suggests the whole build'; Args = @('-u') + Contains = @('build_win.bat -ds') } + @{ Name = 'a dry run only echoes, it never configures'; Args = @('-d') + Contains = @('+ cmake') + NotContains = @('CMake Error', 'Configuring done') } + + 'extra configure arguments' + # --deps-args and --slicer-args are declared "rawstring", so a value that + # looks like an option is allowed through. Plain string options still + # reject one, since there it almost always means a forgotten value. + @{ Name = '--deps-args reaches the deps configure'; Args = @('-d', '--deps-args', 'FOO') + Contains = @('-DCMAKE_BUILD_TYPE=Release FOO') } + @{ Name = '--slicer-args reaches the slicer configure'; Args = @('-s', '--slicer-args', 'BAZ') + Contains = @('BAZ') } + @{ Name = '--deps-args accepts a value that starts with a dash'; Args = @('-d', '--deps-args', '-DFOO') + Contains = @('-DFOO') } + @{ Name = 'a plain string option still rejects a dash-leading value'; Args = @('-d', '--deps-target', '--tests'); ExpectExit = 1 + Contains = @('looks like another option') } + # cmd splits arguments on "=" as well as spaces, so an unquoted -D reaches + # the script as two arguments however it was invoked. + @{ Name = 'an unquoted value containing = is rejected'; Args = @('-d', '--deps-args', 'FOO=BAR'); ExpectExit = 1 + Contains = @('Unknown argument') } + # The environment overrides exist for that reason; nothing tokenises them. + @{ Name = 'ORCA_DEPS_CMAKE_ARGS reaches the deps configure'; Args = @('-d') + Env = @{ ORCA_DEPS_CMAKE_ARGS = '-DFOO=BAR -DBAZ=QUX' } + Contains = @('-DFOO=BAR -DBAZ=QUX') } + @{ Name = 'ORCA_SLICER_CMAKE_ARGS reaches the slicer configure'; Args = @('-s') + Env = @{ ORCA_SLICER_CMAKE_ARGS = '-DWANTED=1' } + Contains = @('-DWANTED=1') } + @{ Name = 'the deps override does not leak into the slicer configure'; Args = @('-s') + Env = @{ ORCA_DEPS_CMAKE_ARGS = '-DDEPSONLY=1' } + NotContains = @('-DDEPSONLY=1') } + @{ Name = 'the help points at the environment for a spaced argument'; Args = @('--help'); DryRun = $false + Contains = @('Neither form supports a value containing an ampersand') } + @{ Name = 'the help lists the environment overrides'; Args = @('--help'); DryRun = $false + Contains = @('Environment:', 'ORCA_DEPS_CMAKE_ARGS', 'ORCA_SLICER_CMAKE_ARGS') } + + 'running the unit tests' + @{ Name = '--tests builds them without running them'; Args = @('-s', '--tests') + Contains = @('-DBUILD_TESTS=ON') + NotContains = @('ctest') } + @{ Name = '--run-tests builds and runs them'; Args = @('-s', '--run-tests') + Contains = @('-DBUILD_TESTS=ON', 'ctest --test-dir "build/tests" -C Release --output-on-failure') } + @{ Name = '--run-tests follows the build type and directory'; Args = @('-s', '--run-tests', '--config', 'debug') + Contains = @('ctest --test-dir "build-dbg/tests" -C Debug') } + @{ Name = 'no tests are run by default'; Args = @('-s') + NotContains = @('ctest') } + + 'failures are reported' + @{ Name = 'a missing cmake is caught and exits non-zero'; Args = @('-d'); ExpectExit = 1 + Env = @{ PATH = 'C:\Windows\system32;C:\Windows' } + Contains = @('CMake was not found') } + @{ Name = 'packing does not need cmake, only an archiver'; Args = @('-p') + Env = @{ PATH = 'C:\Windows\system32;C:\Windows' } + NotContains = @('CMake was not found') } + # Not a dry run: a cd to a missing drive is a real failure inside a + # parenthesised block, which is where exit /b silently loses its code. + # Without the jump to :die this exits 0 and a failed build reads as a + # successful one. + @{ Name = 'a failure inside a build block reaches the caller'; Args = @('-p', '--deps-dir', 'Z:\nope') + DryRun = $false; ExpectExit = 1 + Contains = @('Exit code 1.', '####') + NotContains = @('Build completed', 'Try') } + # The retry follows the stage that failed. Offering it for the whole run + # would clean a dependency tree that was not at fault. + @{ Name = 'a failure names a retry scoped to the stage that failed'; Args = @('-d', '-s', '--deps-dir', 'Z:\nope') + DryRun = $false; ExpectExit = 1 + Contains = @('build_win.bat -d --deps-dir "Z:\nope" -c') + NotContains = @('build_win.bat -ds') } + # CMake's own failure here is hundreds of lines about package resolution. + @{ Name = 'a missing dependency tree is named, not left to CMake'; Args = @('-s', '--deps-dir', 'Z:\nope') + DryRun = $false; ExpectExit = 1 + Contains = @('Dependencies not found at', 'Build them with build_win.bat -d --deps-dir "Z:\nope"') + NotContains = @('cmake -B', 'Try') } + # Every other suggestion carries the flags that reproduce the run; a bare + # -d would point at the MSVC tree after a clang build. + @{ Name = 'the missing-deps hint names this toolchain'; Args = @('-s', '-l', '-x', '--deps-dir', 'deps/not-built') + DryRun = $false; ExpectExit = 1 + Contains = @('Build them with build_win.bat -d -l -x --deps-dir "deps/not-built"') + NotExists = @('deps/not-built') } + # A dry run configures nothing, so it must not depend on which trees happen + # to exist on the machine running the suite. + @{ Name = 'a dry run does not check for the deps tree'; Args = @('-s', '--deps-dir', 'Z:\nope') + Contains = @('cmake -B "build"') + NotContains = @('Dependencies not found') } + # -d is about to build them, so there is nothing to report yet. + @{ Name = 'building the deps in the same run skips the check'; Args = @('-d', '-s', '--deps-dir', 'Z:\nope') + DryRun = $false; ExpectExit = 1 + NotContains = @('Dependencies not found') } + # A configure fails before any compiler runs, so -v has nothing to show. + @{ Name = 'a configure failure is not offered a verbose rebuild'; Args = @('-d', '--deps-dir', 'Z:\nope') + DryRun = $false; ExpectExit = 1 + Contains = @('-c discard that tree') + NotContains = @('-v show the failing') } + # A bare --no-configure succeeds on a machine that already has a usable + # build tree, so name one that cannot exist instead. + @{ Name = 'a build failure is'; Args = @('-s', '--no-configure', '--build-dir', 'deps/no-such-tree') + DryRun = $false; ExpectExit = 1 + Contains = @('-v show the failing') + NotExists = @('deps/no-such-tree') } + # --build-dir names the slicer tree, which a deps failure has nothing to do + # with. --deps-dir stays, because that is the tree that failed. + @{ Name = 'a deps retry leaves out the slicer tree'; Args = @('-d', '-s', '--deps-dir', 'Z:\nope', '--build-dir', 'D:\b') + DryRun = $false; ExpectExit = 1 + Contains = @('build_win.bat -d --deps-dir "Z:\nope" -c') + NotContains = @('--build-dir') } + @{ Name = 'an unknown configuration stays a single line'; Args = @('-s', '--config', 'bogus'); ExpectExit = 1 + Contains = @('Unknown configuration') + NotContains = @('####', 'Try') } + @{ Name = 'a bad --jobs value is not framed either'; Args = @('-s', '-j', 'x'); ExpectExit = 1 + Contains = @('Invalid --jobs value') + NotContains = @('####') } + + 'the summary says what to do next' + # Every suggested command carries the flags that reproduce this run. + @{ Name = 'a deps build points at the slicer build'; Args = @('-d', '-l') + Contains = @('Build the slicer build_win.bat -s -l') + NotContains = @('Run it') } + @{ Name = 'a ninja slicer build offers a single target'; Args = @('-s', '-l', '-x') + Contains = @('Rebuild after edits build_win.bat -s -l -x --no-configure', 'Rebuild one target') + NotContains = @('Solution', 'Open in Visual Studio') } + @{ Name = 'a visual studio build names the solution instead'; Args = @('-s') + Contains = @('Solution ', 'Open in Visual Studio build\OrcaSlicer.sln', + 'Rebuild after edits build_win.bat -s --no-configure') + NotContains = @('Rebuild one target') } + @{ Name = 'the configuration and architecture come back'; Args = @('-s', '-l', '-x', '--config', 'debug', '--arch', 'arm64') + Contains = @('build_win.bat -s -l -x --config debug --arch arm64 --no-configure') } + @{ Name = 'the tree overrides come back quoted'; Args = @('-s', '--deps-dir', 'D:\d', '--build-dir', 'D:\b') + Contains = @('--deps-dir "D:\d" --build-dir "D:\b"') } + @{ Name = 'a pinned visual studio release comes back'; Args = @('-s', '--vs', '2022') + Contains = @('build_win.bat -s --vs 2022 --no-configure') } + # Autodetection writes what it found into the same variable, so a detected + # release must not come back as though it had been asked for. + @{ Name = 'a detected release does not'; Args = @('-s') + NotContains = @('--vs') } + @{ Name = 'the binary is named in the build tree it was built in'; Args = @('-s', '-l', '-x') + Contains = @('build-clang\src\Release\orca-slicer.exe') } + @{ Name = 'installing names the installed copy instead'; Args = @('-s', '-l', '-x', '-i') + Contains = @('build-clang\OrcaSlicer\orca-slicer.exe') } + # -i changes where the binary lands, so a rebuild that dropped it would + # leave the path above pointing at a stale copy. + @{ Name = 'the rebuild suggestion keeps -i'; Args = @('-s', '-l', '-x', '-i') + Contains = @('Rebuild after edits build_win.bat -s -l -x -i --no-configure') } + # A deps retry has no install step to repeat. + @{ Name = 'a deps retry drops it'; Args = @('-d', '-s', '-i', '--deps-dir', 'Z:\nope') + DryRun = $false; ExpectExit = 1 + Contains = @('build_win.bat -d --deps-dir "Z:\nope" -c') + NotContains = @('-d -i') } + # Naming a target builds it and its dependencies, not its dependents, so + # the binary on disk is whatever the last full build left there. + @{ Name = 'a single-target build does not claim the whole binary'; Args = @('-s', '-l', '-x', '--slicer-target', 'glad') + Contains = @('Target glad', 'Relink the binary build_win.bat -s -l -x --no-configure') + NotContains = @('Run it', 'orca-slicer.exe', 'Rebuild after edits') } + # The executable has a target of its own, and naming that one does relink. + @{ Name = 'naming the executable target still claims the binary'; Args = @('-s', '-l', '-x', '--slicer-target', 'OrcaSlicer') + Contains = @('Run it', 'orca-slicer.exe', 'Rebuild after edits') + NotContains = @('Target OrcaSlicer', 'Relink the binary') } + @{ Name = '--run-tests offers the ctest line'; Args = @('-s', '-l', '-x', '--run-tests') + Contains = @('Re-run the tests ctest --test-dir build-clang/tests -C Release') } + @{ Name = 'packing names the bundle and how to use it'; Args = @('-p', '-l') + Contains = @('Bundle ', 'Share the bundle') } + # The line supplies its own tree, so the one this run used must not ride + # along and contradict it. + @{ Name = 'the bundle line names one tree, not two'; Args = @('-s', '-p', '-l', '-x', '--deps-dir', 'D:\shared') + Contains = @('then build_win.bat -s -l -x --deps-dir <path>') + NotContains = @('--deps-dir "D:\shared" --deps-dir') } + @{ Name = 'installing prerequisites suggests the build that follows'; Args = @('-u', '-l') + Contains = @('Restart this shell', 'build_win.bat -ds -l') + NotContains = @('Run it') } + # Everything below the header line is worked out the same way in either + # run, which is why a dry run can cover it. + @{ Name = 'a dry run does not claim a build happened'; Args = @('-s', '-l', '-x') + Contains = @('Dry run: nothing was built.') + NotContains = @('Build completed in') } + # --no-configure is the iteration loop and still gets the block; four + # lines after a rebuild is not enough to be worth suppressing. + @{ Name = '--no-configure still gets the summary'; Args = @('-s', '-l', '-x', '--no-configure') + Contains = @('Next', 'Rebuild after edits') } + + 'pointing at the solution' + @{ Name = 'the VS generator says where the solution is'; Args = @('-s') + Match = @('^ Solution .*\\build\\OrcaSlicer\.sln$') } + @{ Name = 'the solution path follows the configuration'; Args = @('-s', '--config', 'debug') + Match = @('^ Solution .*\\build-dbg\\OrcaSlicer\.sln$') } + @{ Name = 'the solution line survives an install'; Args = @('-s', '-i') + Contains = @(' Solution ') } + # The path is resolved, not pasted onto the repository root, so it is + # right whether --build-dir came absolute or with forward slashes. + @{ Name = 'a moved build still prints one real path'; Args = @('-s', '--build-dir', 'out/build/x64-clang') + Match = @('^ Solution [A-Za-z]:\\[^/]+\\OrcaSlicer\.sln$') } + @{ Name = 'an absolute --build-dir is not glued onto the repo root'; Args = @('-s', '--build-dir', 'D:\tree') + Contains = @('Solution D:\tree\OrcaSlicer.sln') } +) + +function Invoke-BuildScript { + param([string[]] $Arguments, [hashtable] $Environment) + + $saved = @{} + if ($Environment) { + foreach ($key in $Environment.Keys) { + $saved[$key] = [Environment]::GetEnvironmentVariable($key) + Set-Item -Path "env:$key" -Value $Environment[$key] + } + } + try { + # 'Stop' turns a native command's stderr into a terminating error, and + # a case that exercises a real failure writes to stderr. Let the output + # through and judge the run by its exit code instead. The assignment is + # scoped to this function, so the rest of the suite keeps 'Stop'. + $ErrorActionPreference = 'Continue' + if ($Arguments.Count -eq 0) { + $out = & $Script 2>&1 | Out-String + } else { + $out = & $Script @Arguments 2>&1 | Out-String + } + return [pscustomobject]@{ Output = $out; Exit = $LASTEXITCODE } + } finally { + foreach ($key in $saved.Keys) { + if ($null -eq $saved[$key]) { Remove-Item -Path "env:$key" -ErrorAction SilentlyContinue } + else { Set-Item -Path "env:$key" -Value $saved[$key] } + } + } +} + +$knownFields = @( + 'Name', 'Args', 'ExpectExit', 'DryRun', 'First', 'Env', + 'Contains', 'NotContains', 'Match', 'NotMatch', 'NotExists' +) + +function Test-Case { + param([hashtable] $Case) + + # Read fields with the indexer, not dot notation. A hashtable exposes its + # own members too, so $Case.Contains returns the Contains *method* whenever + # the case has no key by that name. + $argv = @($Case['Args']) + if (-not $Case.ContainsKey('DryRun') -or $Case['DryRun']) { $argv += '--dry-run' } + + $expect = 0 + if ($Case.ContainsKey('ExpectExit')) { $expect = $Case['ExpectExit'] } + + $result = Invoke-BuildScript -Arguments $argv -Environment $Case['Env'] + + $problems = @() + + # A misspelled field is silently ignored by the checks below, which + # leaves the case asserting nothing at all and passing. + foreach ($field in $Case.Keys) { + if ($knownFields -notcontains $field) { $problems += "unknown field '$field'" } + } + + if ($result.Exit -ne $expect) { $problems += "exit $($result.Exit), expected $expect" } + foreach ($needle in $Case['Contains']) { + if (-not $result.Output.Contains($needle)) { $problems += "missing '$needle'" } + } + foreach ($needle in $Case['NotContains']) { + if ($result.Output.Contains($needle)) { $problems += "unexpected '$needle'" } + } + $lines = $result.Output -split "`r?`n" + if ($Case['First'] -and $lines[0] -notmatch $Case['First']) { + $problems += "first line was '$($lines[0])'" + } + foreach ($pattern in $Case['Match']) { + if (@($lines | Where-Object { $_ -match $pattern }).Count -eq 0) { + $problems += "no line matching /$pattern/" + } + } + foreach ($pattern in $Case['NotMatch']) { + foreach ($line in @($lines | Where-Object { $_ -match $pattern })) { + $problems += "line matches /$pattern/: $line" + } + } + # Output cannot show what a run did not create. + foreach ($path in $Case['NotExists']) { + $full = Join-Path (Split-Path -Parent $Script) $path + if (Test-Path $full) { + $problems += "created '$path'" + } + } + return ,$problems +} + +$pass = 0 +$failed = @() +# Held back so a filtered run does not print headings for groups it skipped. +$heading = $null + +foreach ($case in $cases) { + if ($case -is [string]) { + $heading = $case + continue + } + if ($Name -and $case['Name'] -notmatch $Name) { continue } + if ($heading) { + Write-Host '' + Write-Host $heading -ForegroundColor Cyan + $heading = $null + } + + $problems = Test-Case -Case $case + if ($problems.Count -eq 0) { + $pass++ + Write-Host (' ok ' + $case['Name']) + } else { + $failed += $case['Name'] + Write-Host (' FAIL ' + $case['Name']) -ForegroundColor Red + foreach ($problem in $problems) { Write-Host (' ' + $problem) -ForegroundColor Red } + Write-Host (' args: ' + (@($case['Args']) -join ' ')) + } +} + +Remove-Item -Recurse -Force $fixtures -ErrorAction SilentlyContinue + +Write-Host '' +# A pattern that matched nothing has proved nothing, so do not report it as +# a clean run. +if ($Name -and $pass -eq 0 -and $failed.Count -eq 0) { + Write-Host "no case matched /$Name/" -ForegroundColor Red + exit 1 +} +Write-Host "$pass passed, $($failed.Count) failed" +if ($failed.Count -gt 0) { + foreach ($name in $failed) { Write-Host " failed: $name" -ForegroundColor Red } + exit 1 +} +exit 0 diff --git a/src/libslic3r/Format/OBJ.cpp b/src/libslic3r/Format/OBJ.cpp index e066925a98..10abe8e4de 100644 --- a/src/libslic3r/Format/OBJ.cpp +++ b/src/libslic3r/Format/OBJ.cpp @@ -10,6 +10,7 @@ #include <string> #include <boost/log/trivial.hpp> +#include <boost/nowide/fstream.hpp> #ifdef _WIN32 #define DIR_SEPARATOR '\\' diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index e947be47a1..e9cdb620e0 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8197,9 +8197,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } variable_speed = std::any_of(new_points.begin(), new_points.end(), [speed](const ProcessedPoint &p) { return fabs(double(p.speed) - speed) > 1; }); // Ignore small speed variations (under 1mm/sec) - if (!NOZZLE_CONFIG(enable_overhang_speed) && FILAMENT_CONFIG(enable_overhang_bridge_fan) && m_enable_cooling_markers) { - for (ProcessedPoint &point : new_points) - point.speed = speed; + if (FILAMENT_CONFIG(enable_overhang_bridge_fan) && m_enable_cooling_markers) { + if (!NOZZLE_CONFIG(enable_overhang_speed)) + for (ProcessedPoint &point : new_points) + point.speed = speed; variable_speed = new_points.size() > 1; } } diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 3bca1f8df9..e55b89212e 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1024,28 +1024,29 @@ std::string CoolingBuffer::apply_layer_cooldown( } if (need_set_fan) { + const auto set_fan = [&](int speed) { + if (m_current_fan_speed != speed) { + new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, speed, part_cooling_fan_min_pwm); + m_current_fan_speed = speed; + } + }; if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){ - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, overhang_fan_speed, part_cooling_fan_min_pwm); - m_current_fan_speed = overhang_fan_speed; + set_fan(overhang_fan_speed); } else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, internal_bridge_fan_speed, part_cooling_fan_min_pwm); - m_current_fan_speed = internal_bridge_fan_speed; + set_fan(internal_bridge_fan_speed); } else if (fan_speed_change_requests[CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START]){ - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, supp_interface_fan_speed, part_cooling_fan_min_pwm); - m_current_fan_speed = supp_interface_fan_speed; + set_fan(supp_interface_fan_speed); } else if (fan_speed_change_requests[CoolingLine::TYPE_IRONING_FAN_START]){ - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, ironing_fan_speed, part_cooling_fan_min_pwm); - m_current_fan_speed = ironing_fan_speed; + set_fan(ironing_fan_speed); } else if(fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] && m_current_fan_speed != -1){ new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_current_fan_speed, part_cooling_fan_min_pwm); fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false; } else { - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed, part_cooling_fan_min_pwm); - m_current_fan_speed = m_fan_speed; + set_fan(m_fan_speed); } need_set_fan = false; } diff --git a/src/libslic3r/GCode/FanMover.cpp b/src/libslic3r/GCode/FanMover.cpp index 112066bb85..9d77d1d04c 100644 --- a/src/libslic3r/GCode/FanMover.cpp +++ b/src/libslic3r/GCode/FanMover.cpp @@ -170,14 +170,13 @@ void FanMover::_put_in_middle_G1(std::list<BufferData>::iterator item_to_split, void FanMover::_print_in_middle_G1(BufferData& line_to_split, float nb_sec, const std::string &line_to_write) { if (nb_sec < line_to_split.time * 0.1) { - // doesn't really need to be split, print it after - m_process_output += line_to_split.raw + "\n"; + // Doesn't need to be split: the insertion point is at the start. m_process_output += line_to_write + (line_to_write.back() == '\n'?"":"\n"); - } else if (nb_sec > line_to_split.time * 0.9) { - // doesn't really need to be split, print it before - //will also print before if line_to_split.time == 0 - m_process_output += line_to_write + (line_to_write.back() == '\n' ? "" : "\n"); m_process_output += line_to_split.raw + "\n"; + } else if (nb_sec > line_to_split.time * 0.9) { + // Doesn't need to be split: the insertion point is at the end. + m_process_output += line_to_split.raw + "\n"; + m_process_output += line_to_write + (line_to_write.back() == '\n' ? "" : "\n"); }else if(line_to_split.raw.size() > 2 && line_to_split.raw[0] == 'G' && line_to_split.raw[1] == '1' && line_to_split.raw[2] == ' ') { float percent = nb_sec / line_to_split.time; diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 4f3f95f297..2ac8bd45a6 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -532,7 +532,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P const float height = interpolate ? lerp(prev_move.height, curr_move.height, t) : curr_move.height; // ORCA: Fix issue with flow rate changes being visualized incorrectly const float mm3_per_mm = curr_move.mm3_per_mm; - const float fan_speed = interpolate ? lerp(prev_move.fan_speed, curr_move.fan_speed, t) : curr_move.fan_speed; + const float fan_speed = curr_move.fan_speed; const float temperature = interpolate ? lerp(prev_move.temperature, curr_move.temperature, t) : curr_move.temperature; actual_speed_moves.push_back({ block.move_id, @@ -563,7 +563,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P const float height = interpolate ? lerp(prev_move.height, curr_move.height, t) : curr_move.height; // ORCA: Fix issue with flow rate changes being visualized incorrectly const float mm3_per_mm = curr_move.mm3_per_mm; - const float fan_speed = interpolate ? lerp(prev_move.fan_speed, curr_move.fan_speed, t) : curr_move.fan_speed; + const float fan_speed = curr_move.fan_speed; const float temperature = interpolate ? lerp(prev_move.temperature, curr_move.temperature, t) : curr_move.temperature; actual_speed_moves.push_back({ block.move_id, diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 2a4eb8d7a7..17d7cee678 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -10091,6 +10091,10 @@ int DynamicPrintConfig::update_values_from_single_to_multi(DynamicPrintConfig& m for (int index = 0; index < variant_count; index++) { + //variant_count is the variant column width, src_opt the value array; + //they disagree when the source was authored at a different width + if (index >= (int)src_opt->values.size()) + break; if (opt->values[index] > src_opt->values[index]) opt->values[index] = src_opt->values[index]; } @@ -10108,6 +10112,8 @@ int DynamicPrintConfig::update_values_from_single_to_multi(DynamicPrintConfig& m for (int index = 0; index < variant_count; index++) { + if (index >= (int)src_opt->values.size()) + break; if (opt->values[index].value > src_opt->values[index].value) opt->values[index] = src_opt->values[index]; } @@ -10302,6 +10308,10 @@ int DynamicPrintConfig::update_values_from_multi_to_multi(DynamicPrintConfig& ne for(auto idx : variant_indices){ assert(idx < old_count); + //the counts come from the variant columns, the arrays from the options; + //they disagree when a config was authored at a different variant width + if (idx >= old_count || new_variant_index >= (int)opt->values.size()) + continue; if (old_values[idx] < opt->values[new_variant_index]) opt->values[new_variant_index] = old_values[idx]; } @@ -10332,6 +10342,10 @@ int DynamicPrintConfig::update_values_from_multi_to_multi(DynamicPrintConfig& ne for(auto idx : variant_indices){ assert(idx < old_count); + //the counts come from the variant columns, the arrays from the options; + //they disagree when a config was authored at a different variant width + if (idx >= old_count || new_variant_index >= (int)opt->values.size()) + continue; if (old_values[idx] < opt->values[new_variant_index]) opt->values[new_variant_index] = old_values[idx]; } @@ -10362,6 +10376,8 @@ int DynamicPrintConfig::update_values_from_multi_to_multi(DynamicPrintConfig& ne for(auto idx : variant_indices){ assert(idx < old_count); + if (idx >= old_count || new_variant_index >= (int)opt->values.size()) + continue; if (old_values[idx]) //enabled opt->values[new_variant_index] = old_values[idx]; } @@ -10402,6 +10418,15 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st same_variant_indices.emplace_back(indices); } + //dst_values below is the destination PRINT preset's per-variant row, sized to its own + //print_extruder_variant; dst_extruder_variants is the PRINTER's list. They disagree until + //the print preset is re-selected, so size the row to the variant count before indexing it. + const size_t dst_variant_count = dst_extruder_variants.size(); + if (dst_variant_count == 0) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", Line %1%: empty destination variant list")%__LINE__; + return -1; + } + t_config_option_keys keys = this->keys(); for(auto& key : keys){ if(key_sets.find(key) == key_sets.end()) @@ -10417,7 +10442,13 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st { ConfigOptionFloatsNullable* opt = this->option<ConfigOptionFloatsNullable>(key); auto src_values = opt->values; - auto dst_values = dst_config.option<ConfigOptionFloatsNullable>(key) ->values; + const auto* dst_opt = dst_config.option<ConfigOptionFloatsNullable>(key); + if(!dst_opt){ + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: %2% missing from destination config")%__LINE__%key; + break; + } + auto dst_values = dst_opt->values; + dst_values.resize(dst_variant_count, ConfigOptionFloatsNullable::nil_value()); for(size_t dst_idx =0; dst_idx < same_variant_indices.size(); ++dst_idx){ auto& indices = same_variant_indices[dst_idx]; if(indices.empty()) @@ -10425,7 +10456,7 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st bool has_value = false; double target_value = std::numeric_limits<double>::max(); for(auto idx : indices){ - if(opt && idx < opt->values.size() && !opt->is_nil(idx)){ + if(idx < (int)opt->values.size() && !opt->is_nil(idx)){ has_value = true; target_value = std::min(target_value, src_values[idx]); } @@ -10441,7 +10472,13 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st { ConfigOptionFloatsOrPercentsNullable* opt = this->option<ConfigOptionFloatsOrPercentsNullable>(key); auto src_values = opt->values; - auto dst_values = dst_config.option<ConfigOptionFloatsOrPercentsNullable>(key) ->values; + const auto* dst_opt = dst_config.option<ConfigOptionFloatsOrPercentsNullable>(key); + if(!dst_opt){ + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: %2% missing from destination config")%__LINE__%key; + break; + } + auto dst_values = dst_opt->values; + dst_values.resize(dst_variant_count, ConfigOptionFloatsOrPercentsNullable::nil_value()); for(size_t dst_idx =0; dst_idx < same_variant_indices.size(); ++dst_idx){ auto& indices = same_variant_indices[dst_idx]; if(indices.empty()) @@ -10449,7 +10486,7 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st bool has_value = false; FloatOrPercent target_value{9999.f, true}; for(auto idx : indices){ - if(opt && !opt->is_nil(idx)){ + if(idx < (int)opt->values.size() && !opt->is_nil(idx)){ has_value = true; target_value = src_values[idx].value < target_value.value ? src_values[idx] : target_value; } @@ -10465,15 +10502,21 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st { ConfigOptionBoolsNullable* opt = this->option<ConfigOptionBoolsNullable>(key); auto src_values = opt->values; - auto dst_values = dst_config.option<ConfigOptionBoolsNullable>(key) ->values; + const auto* dst_opt = dst_config.option<ConfigOptionBoolsNullable>(key); + if(!dst_opt){ + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: %2% missing from destination config")%__LINE__%key; + break; + } + auto dst_values = dst_opt->values; + dst_values.resize(dst_variant_count, ConfigOptionBoolsNullable::nil_value()); for(size_t dst_idx =0; dst_idx < same_variant_indices.size(); ++dst_idx){ auto indices = same_variant_indices[dst_idx]; if(indices.empty()) continue; bool has_value = false; - bool target_value; + bool target_value = false; for(auto idx : indices){ - if(opt && !opt->is_nil(idx)){ + if(idx < (int)opt->values.size() && !opt->is_nil(idx)){ has_value = true; target_value = src_values[idx]; break; diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index d15164ef63..09b7fcfad4 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -453,7 +453,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con if (config->opt_bool("alternate_extra_wall") && (config->opt_enum<EnsureVerticalShellThickness>("ensure_vertical_shell_thickness") == evstAll)) { - wxString msg_text = _(L("Alternate extra wall does't work well when ensure vertical shell thickness is set to All.")); + wxString msg_text = _(L("Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All.")); if (is_global_config) msg_text += "\n\n" + _(L("Change these settings automatically?\n" @@ -642,7 +642,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con if (config->opt_enum<SeamScarfType>("seam_slope_type") != SeamScarfType::None && config->get_abs_value("seam_slope_start_height") >= layer_height) { - const wxString msg_text = _(L("seam_slope_start_height need to be smaller than layer_height.\nReset to 0.")); + const wxString msg_text = _(L("seam_slope_start_height needs to be smaller than layer_height.\nReset to 0.")); MessageDialog dialog(m_msg_dlg_parent, msg_text, "", wxICON_WARNING | wxOK); DynamicPrintConfig new_conf = *config; is_msg_dlg_already_exist = true; @@ -656,7 +656,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con float skin_depth = config->opt_float("skin_infill_depth"); if (config->opt_float("infill_lock_depth") > skin_depth) { // xgettext:no-c-format, no-boost-format - const wxString msg_text = _(L("Lock depth should smaller than skin depth.\nReset to 50% of skin depth.")); + const wxString msg_text = _(L("Lock depth should be smaller than skin depth.\nReset to 50% of skin depth.")); MessageDialog dialog(m_msg_dlg_parent, msg_text, "", wxICON_WARNING | wxOK); DynamicPrintConfig new_conf = *config; is_msg_dlg_already_exist = true; diff --git a/src/slic3r/Utils/FixModelByCgal.cpp b/src/slic3r/Utils/FixModelByCgal.cpp index 8d196f7201..9d973844bb 100644 --- a/src/slic3r/Utils/FixModelByCgal.cpp +++ b/src/slic3r/Utils/FixModelByCgal.cpp @@ -12,6 +12,7 @@ #include "libslic3r/MeshBoolean.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/format.hpp" #include "libslic3r/Thread.hpp" #include "../GUI/I18N.hpp" @@ -69,6 +70,9 @@ public: // Returns false if fixing was canceled. fix_result contains error message if failed. bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dialog, const wxString &msg_header, std::string &fix_result, bool keep_painting) { + // Hold SaveObjectGaurd to prevent backup manager from racing concurrent mesh mutations (use-after-free). + SaveObjectGaurd backup_gaurd(model_object); + // Orca: Synchronization primitives for progress updates between worker thread and GUI. std::mutex mtx; std::condition_variable condition; diff --git a/tests/fff_print/test_cooling.cpp b/tests/fff_print/test_cooling.cpp index ec29b5fdc9..a21ff236ce 100644 --- a/tests/fff_print/test_cooling.cpp +++ b/tests/fff_print/test_cooling.cpp @@ -2,7 +2,10 @@ #include "test_helpers.hpp" +#include <algorithm> +#include <sstream> #include <string> +#include <vector> using namespace Slic3r; using namespace Slic3r::Test; @@ -25,3 +28,68 @@ TEST_CASE("Cooling consumes its internal speed markers", "[Cooling]") const std::string gcode = slice({ cube(20) }, { { "layer_height", 0.2 } }); CHECK(gcode.find(";_EXTRUDE_SET_SPEED") == std::string::npos); } + +TEST_CASE("Overhang fan transitions do not depend on overhang speed", "[Cooling][Regression]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "bridge_speed", 2.0 }, + { "enable_arc_fitting", false }, + { "enable_overhang_bridge_fan", true }, + { "enable_overhang_speed", false }, + { "initial_layer_print_height", 0.3 }, + { "inner_wall_speed", 30.0 }, + { "layer_height", 0.3 }, + { "outer_wall_speed", 30.0 }, + { "overhang_1_4_speed", "30" }, + { "overhang_2_4_speed", "29" }, + { "overhang_3_4_speed", "6" }, + { "overhang_4_4_speed", "3" }, + { "slow_down_for_layer_cooling", false }, + { "slowdown_for_curled_perimeters", false }, + }); + config.set_key_value("fan_max_speed", new ConfigOptionFloats{20.0}); + config.set_key_value("fan_min_speed", new ConfigOptionFloats{20.0}); + config.set_key_value("overhang_fan_speed", new ConfigOptionInts{100}); + config.set_key_value("overhang_fan_threshold", new ConfigOptionEnumsGeneric{Overhang_threshold_2_4}); + config.set_key_value("layer_change_gcode", new ConfigOptionString{";TEST_LAYER_Z=[layer_z]"}); + + const auto fan_commands = [](const std::string &gcode) { + std::vector<std::pair<std::string, std::string>> commands; + std::istringstream input(gcode); + std::string layer; + std::string line; + while (std::getline(input, line)) { + if (line.rfind(";TEST_LAYER_Z=", 0) == 0) + layer = line; + else if (!layer.empty() && (line.rfind("M106", 0) == 0 || line.rfind("M107", 0) == 0)) + commands.emplace_back(layer, line); + } + return commands; + }; + const auto feedrates = [](const std::string &gcode) { + std::vector<std::string> values; + std::istringstream input(gcode); + std::string word; + while (input >> word) + if (!word.empty() && word.front() == 'F') + values.push_back(word); + return values; + }; + + constexpr double sphere_radius = 50.0; // 100 mm diameter. + const std::string without_speed_gcode = slice({make_sphere(sphere_radius, PI / 24.0)}, config); + config.set_deserialize_strict({{"enable_overhang_speed", true}}); + const std::string with_speed_gcode = slice({make_sphere(sphere_radius, PI / 24.0)}, config); + + const auto without_speed_fan = fan_commands(without_speed_gcode); + const auto with_speed_fan = fan_commands(with_speed_gcode); + const auto without_speed_feedrates = feedrates(without_speed_gcode); + const auto with_speed_feedrates = feedrates(with_speed_gcode); + + REQUIRE_FALSE(without_speed_fan.empty()); + REQUIRE(std::any_of(without_speed_fan.begin(), without_speed_fan.end(), + [](const auto &command) { return command.second.find("S255") != std::string::npos; })); + REQUIRE(with_speed_feedrates != without_speed_feedrates); + CHECK(with_speed_fan == without_speed_fan); +} diff --git a/tests/libslic3r/test_config_variant_expansion.cpp b/tests/libslic3r/test_config_variant_expansion.cpp index dd7de2af44..28901702cc 100644 --- a/tests/libslic3r/test_config_variant_expansion.cpp +++ b/tests/libslic3r/test_config_variant_expansion.cpp @@ -479,3 +479,84 @@ TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves pe REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({1, 2})); } } + +// update_values_from_multi_to_multi_2 walks the DESTINATION PRINTER's variant list while writing +// into a row taken from the destination PRINT preset, whose arrays are sized to its own +// print_extruder_variant. Those two widths disagree until the print preset is re-selected for the +// new printer -- Tab::load_current_preset() runs this migration first -- so a project authored on +// a single-variant printer, opened and switched to a wider one, wrote past the end of the row. +TEST_CASE("update_values_from_multi_to_multi_2 sizes the destination row to the variant count", + "[Config][VariantExpansion]") +{ + const std::vector<std::string> src_variants{"Direct Drive Standard"}; + const std::vector<std::string> dst_variants{"Direct Drive Standard", "Direct Drive High Flow", + "Direct Drive Standard", "Direct Drive High Flow"}; + const std::set<std::string> keys{"outer_wall_speed"}; + + // The per-object override as authored on the single-variant printer. + const auto object_override = [] { + DynamicPrintConfig c; + c.option<ConfigOptionFloatsNullable>("outer_wall_speed", true)->values = {42.}; + return c; + }; + + SECTION("a row narrower than the variant list is grown, not overrun") { + DynamicPrintConfig object_config = object_override(); + DynamicPrintConfig dst; + dst.option<ConfigOptionFloatsNullable>("outer_wall_speed", true)->values = {200.}; + + REQUIRE(object_config.update_values_from_multi_to_multi_2(src_variants, dst_variants, dst, keys) == 0); + + const auto& out = object_config.option<ConfigOptionFloatsNullable>("outer_wall_speed")->values; + REQUIRE(out.size() == dst_variants.size()); + // Both "Direct Drive Standard" columns match the source variant, so they take the override. + CHECK(out[0] == Catch::Approx(42.)); + CHECK(out[2] == Catch::Approx(42.)); + // The High Flow columns have no matching source variant: nil, so the destination keeps + // tracking the print preset rather than being pinned to another variant's value. + CHECK(std::isnan(out[1])); + CHECK(std::isnan(out[3])); + } + + // The regression guard: where the row already matches the variant list -- every case that was + // not corrupting the heap -- the resize is a no-op and the output is unchanged. + SECTION("a correctly sized row is untouched") { + DynamicPrintConfig object_config = object_override(); + DynamicPrintConfig dst; + dst.option<ConfigOptionFloatsNullable>("outer_wall_speed", true)->values = {200., 500., 210., 510.}; + + REQUIRE(object_config.update_values_from_multi_to_multi_2(src_variants, dst_variants, dst, keys) == 0); + + const auto& out = object_config.option<ConfigOptionFloatsNullable>("outer_wall_speed")->values; + REQUIRE(out.size() == 4); + CHECK(out[0] == Catch::Approx(42.)); // matched -> override + CHECK(out[1] == Catch::Approx(500.)); // unmatched -> preset value preserved + CHECK(out[2] == Catch::Approx(42.)); + CHECK(out[3] == Catch::Approx(510.)); + } + + // is_nil(idx) indexes values[idx] with no bounds check, so a source shorter than its own + // variant list read out of range before the guard was added. + SECTION("a source shorter than its variant list is read in range") { + DynamicPrintConfig object_config = object_override(); // one value... + DynamicPrintConfig dst; + dst.option<ConfigOptionFloatsNullable>("outer_wall_speed", true)->values = {200., 500.}; + + REQUIRE(object_config.update_values_from_multi_to_multi_2( + {"Direct Drive Standard", "Direct Drive Standard"}, // ...but two source variants + {"Direct Drive Standard", "Direct Drive High Flow"}, dst, keys) == 0); + + const auto& out = object_config.option<ConfigOptionFloatsNullable>("outer_wall_speed")->values; + REQUIRE(out.size() == 2); + CHECK(out[0] == Catch::Approx(42.)); + CHECK(out[1] == Catch::Approx(500.)); + } + + SECTION("an empty destination variant list is refused") { + DynamicPrintConfig object_config = object_override(); + DynamicPrintConfig dst; + dst.option<ConfigOptionFloatsNullable>("outer_wall_speed", true)->values = {200.}; + + CHECK(object_config.update_values_from_multi_to_multi_2(src_variants, {}, dst, keys) == -1); + } +}