fix: build_win.bat builds with whatever clang-cl is first on PATH (#15504)

* fix: build_win.bat builds with whatever clang-cl is first on PATH

VsDevCmd appends the Visual Studio LLVM directory to the end of PATH, so
a standalone LLVM already on it shadows the Visual Studio one. -l -x
passed a bare clang-cl.exe for CMake to resolve, so the build ran on
whichever copy came first. For one reporter that was an LLVM 11, which
failed the compiler check before anything was compiled:

    -- Check for working C compiler: C:/Program Files/LLVM/bin/clang-cl.exe - broken
    lld-link: error: undefined symbol: __guard_eh_cont_table

The compiler is now resolved through vswhere and passed as a full path,
so PATH order no longer matters. CMake derives the linker from the
compiler directory, so lld-link follows. Only a configure passes it to
CMake, so -p and --no-configure resolve nothing and stay buildable on a
machine with no clang installed.

When Visual Studio has no clang toolset the script falls back to the
first clang-cl on PATH and names it. With none installed at all it now
errors with what to add, instead of failing later inside CMake. Every
clang-cl run that configures prints the compiler it resolved.

The suite gains a clang-cl fixture earlier on PATH than the Visual
Studio one, and an empty ProgramFiles(x86) to put vswhere out of reach,
which covers both fallbacks without touching the machine.

* fix: build_win.bat pointed at a solution file that is not there

The Visual Studio 2026 generator writes OrcaSlicer.slnx and the releases
before it OrcaSlicer.sln. The summary hard-coded the second, so the path
it printed after an MSVC build against 2026 was wrong.
This commit is contained in:
Kris Austin
2026-09-03 07:56:33 -05:00
committed by GitHub
parent e6501bb1ce
commit f92bd81190
2 changed files with 142 additions and 17 deletions

View File

@@ -370,13 +370,13 @@ if "%use_ninja%" == "ON" (
set "using_ninja=ON"
)
call :resolve_clang_cl
%repeat_error%
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!"
REM Quoted, because the resolved path has spaces in it.
set "gen_args=-DCMAKE_C_COMPILER="!clang_exe!" -DCMAKE_CXX_COMPILER="!clang_exe!""
)
) else (
set "gen_args=-A !arch!"
@@ -473,6 +473,7 @@ 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%
if not "%clang_exe%" == "" echo Compiler: %clang_exe%
set "SIG_FLAG="
if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%"
@@ -734,6 +735,14 @@ REM worked out the same way in either run.
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 The 2026 generator writes OrcaSlicer.slnx, the releases before it
REM OrcaSlicer.sln. A file already there wins, in case an older CMake
REM configured the build.
set "solution=OrcaSlicer.sln"
if "%vs_version%" == "2026" set "solution=OrcaSlicer.slnx"
if exist "!build_full!\OrcaSlicer.sln" set "solution=OrcaSlicer.sln"
if exist "!build_full!\OrcaSlicer.slnx" set "solution=OrcaSlicer.slnx"
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"
@@ -751,14 +760,14 @@ REM worked out the same way in either run.
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 "%build_slicer%" == "ON" if not "%using_ninja%" == "ON" echo Solution %build_full%\!solution!
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 not "%using_ninja%" == "ON" echo Open in Visual Studio %build_dir%\!solution!
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
@@ -1080,6 +1089,62 @@ REM echo_var <variable>
exit /b 0
REM resolve_clang_cl - set clang_exe to the clang-cl a Ninja build uses.
REM VsDevCmd appends the Visual Studio LLVM directory to the end of PATH,
REM so a standalone LLVM already there shadows it. Name a full path.
:resolve_clang_cl
set "clang_exe="
if not "%using_ninja%" == "ON" exit /b 0
if not "%use_clang_cl%" == "ON" exit /b 0
REM Only a configure uses it, so -p and --no-configure need none.
if "%no_configure%" == "ON" exit /b 0
if "%build_deps%%build_slicer%" == "" exit /b 0
REM --clang-path wins. Forward slashes either way, so CMake does not
REM read a backslash as an escape.
if not "%clang_path%" == "" (
set "clang_exe=%clang_path:\=/%"
exit /b 0
)
setlocal
REM Keyed on the host, not %arch%, because the x64 compiler
REM cross-compiles to ARM64.
set "llvm_host=x64"
if /I "%PROCESSOR_ARCHITECTURE%" == "ARM64" set "llvm_host=ARM64"
set "found="
%VSWHERE% -nologo >nul 2>nul
if !errorlevel! == 0 (
for /f "tokens=*" %%i in ('%VSWHERE% -nologo -products * -latest -property resolvedInstallationPath') do (
if exist "%%i\VC\Tools\Llvm\!llvm_host!\bin\clang-cl.exe" (
set "found=%%i\VC\Tools\Llvm\!llvm_host!\bin\clang-cl.exe"
)
)
)
REM No clang toolset in Visual Studio. where lists every match, and the
REM first is the one PATH would resolve.
if "!found!" == "" (
for /f "tokens=*" %%i in ('where clang-cl.exe 2^>nul') do (
if "!found!" == "" set "found=%%i"
)
if not "!found!" == "" echo Visual Studio has no clang-cl; using !found! from PATH.
)
if "!found!" == "" (
echo No clang-cl found. Add the C++ Clang Compiler component with
echo %script_name% --install-vs ide -l
echo or name a standalone one with --clang-path.
endlocal
exit /b 1
)
endlocal & set "clang_exe=%found:\=/%"
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.

View File

@@ -85,6 +85,24 @@ foreach ($v in @{ old = '1.11.1'; new = '1.12.0' }.GetEnumerator()) {
$ninjaPaths[$v.Key] = "$d;$env:PATH"
}
# A clang-cl earlier on PATH than the Visual Studio one, which is what the
# compiler used to resolve to. Nothing runs it; the script only locates it.
$clangDir = Join-Path $fixtures 'clang'
New-Item -ItemType Directory -Force -Path $clangDir | Out-Null
Copy-Item "$env:SystemRoot\System32\where.exe" (Join-Path $clangDir 'clang-cl.exe') -Force
$clangOnPath = "$clangDir;$env:PATH"
# ProgramFiles(x86) is where the script looks for vswhere, so an empty one
# stands in for a machine whose Visual Studio has no clang toolset.
$noVs = Join-Path $fixtures 'no-vs'
New-Item -ItemType Directory -Force -Path $noVs | Out-Null
# A build directory that already holds a classic solution, for the case where
# what is on disk disagrees with what the generator would write.
$slnDir = Join-Path $fixtures 'sln'
New-Item -ItemType Directory -Force -Path $slnDir | Out-Null
Set-Content -Path (Join-Path $slnDir 'OrcaSlicer.sln') -Value '' -Encoding ascii
# 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.
@@ -131,7 +149,36 @@ $cases = @(
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') }
Contains = @('-G "Ninja Multi-Config"')
Match = @('-DCMAKE_C_COMPILER="[^"]+/clang-cl\.exe"', '-DCMAKE_CXX_COMPILER="[^"]+/clang-cl\.exe"') }
# PATH order used to decide the compiler. VsDevCmd appends the Visual
# Studio LLVM directory to the end of PATH, so a standalone LLVM already
# there was resolved instead, and an old one failed the compiler check.
@{ Name = 'the compiler is resolved from Visual Studio, not PATH'; Args = @('-s', '-l', '-x')
Env = @{ PATH = $clangOnPath }
Match = @('^Compiler: .*/VC/Tools/Llvm/[^/]+/bin/clang-cl\.exe$') }
@{ Name = 'msvc names no compiler, having resolved none'; Args = @('-s')
NotContains = @('Compiler: ') }
@{ Name = '-l without -x names none either, the toolset picks it'; Args = @('-s', '-l')
NotContains = @('Compiler: ') }
# An empty ProgramFiles(x86) puts vswhere out of reach, which is a machine
# whose Visual Studio has no clang toolset.
@{ Name = 'without a Visual Studio clang the one on PATH is used and named'; Args = @('-s', '-l', '-x')
Env = @{ 'ProgramFiles(x86)' = $noVs; PATH = $clangOnPath }
Contains = @('Visual Studio has no clang-cl')
Match = @('^Compiler: .*/clang/clang-cl\.exe$') }
@{ Name = 'no clang-cl anywhere stops before configuring'; Args = @('-s', '-l', '-x'); ExpectExit = 1
Env = @{ 'ProgramFiles(x86)' = $noVs; PATH = 'C:\Windows\system32;C:\Windows' }
Contains = @('No clang-cl found', '--install-vs ide -l')
NotContains = @('cmake -B') }
# Only a configure passes the compiler to CMake, so an action that does
# not configure resolves none, and cannot start needing one installed.
@{ Name = 'packing resolves no compiler'; Args = @('-p', '-l', '-x')
Contains = @('Packing the dependencies')
NotContains = @('Compiler: ') }
@{ Name = '--no-configure resolves none either'; Args = @('-s', '-l', '-x', '--no-configure')
Contains = @('cmake --build "build-clang"')
NotContains = @('Compiler: ') }
@{ Name = '-l alone uses the ClangCL toolset on the VS generator'; Args = @('-d', '-l')
Contains = @('-G "Visual Studio', '-T ClangCL')
NotContains = @('-DCMAKE_C_COMPILER') }
@@ -148,11 +195,13 @@ $cases = @(
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.
# A developer with a standalone LLVM points at it, and the path is passed
# with forward slashes so CMake cannot read a backslash as an escape.
@{ 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"') }
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 beats the Visual Studio clang'; Args = @('-s', '-x', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe')
Contains = @('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
@@ -195,7 +244,8 @@ $cases = @(
@{ 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')
Contains = @('deps/build-clang-arm64')
Match = @('-DCMAKE_C_COMPILER="[^"]+/clang-cl\.exe"')
NotContains = @('-A ') }
'build configurations'
@@ -699,18 +749,28 @@ $cases = @(
Contains = @('Next', 'Rebuild after edits') }
'pointing at the solution'
# The extension follows the generator, so these two pin the release and a
# build directory that cannot already hold a solution of either kind.
@{ Name = 'the 2026 generator gets the XML solution'; Args = @('-s', '--vs', '2026', '--build-dir', 'D:\tree')
Contains = @('Solution D:\tree\OrcaSlicer.slnx', 'Open in Visual Studio D:\tree\OrcaSlicer.slnx') }
@{ Name = 'the releases before it get the classic one'; Args = @('-s', '--vs', '2022', '--build-dir', 'D:\tree')
Contains = @('Solution D:\tree\OrcaSlicer.sln', 'Open in Visual Studio D:\tree\OrcaSlicer.sln') }
@{ Name = 'a solution already on disk wins over the generator'; Args = @('-s', '--vs', '2026', '--build-dir', $slnDir)
Match = @('^ Solution .*\\OrcaSlicer\.sln$') }
# Extension-agnostic from here: these cases are about the directory, and
# the release is whatever is installed.
@{ Name = 'the VS generator says where the solution is'; Args = @('-s')
Match = @('^ Solution .*\\build\\OrcaSlicer\.sln$') }
Match = @('^ Solution .*\\build\\OrcaSlicer\.slnx?$') }
@{ Name = 'the solution path follows the configuration'; Args = @('-s', '--config', 'debug')
Match = @('^ Solution .*\\build-dbg\\OrcaSlicer\.sln$') }
Match = @('^ Solution .*\\build-dbg\\OrcaSlicer\.slnx?$') }
@{ 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$') }
Match = @('^ Solution [A-Za-z]:\\[^/]+\\OrcaSlicer\.slnx?$') }
@{ Name = 'an absolute --build-dir is not glued onto the repo root'; Args = @('-s', '--build-dir', 'D:\tree')
Contains = @('Solution D:\tree\OrcaSlicer.sln') }
Match = @('^ Solution D:\\tree\\OrcaSlicer\.slnx?$') }
)
function Invoke-BuildScript {