diff --git a/build_win.bat b/build_win.bat index 926468687b..15399d5e3d 100644 --- a/build_win.bat +++ b/build_win.bat @@ -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 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 - 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. diff --git a/scripts/test_build_win.ps1 b/scripts/test_build_win.ps1 index 62eaaae77b..563430559f 100644 --- a/scripts/test_build_win.ps1 +++ b/scripts/test_build_win.ps1 @@ -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 {