Make Visual Studio + CMake optional on Windows (prebuilt llama.cpp needs no build tools) (#6499)
* studio/setup.ps1: complete Visual Studio 2026 support for the CUDA llama.cpp build Builds on #6038 (VS 2026 / v18 detection). Once the generator is detected as Visual Studio 18 2026, two things still broke the CUDA llama.cpp build: - the CUDA to VS MSBuild integration copied the CUDA .targets into a hardcoded VC\v170 (VS 2022) BuildCustomizations folder, so a VS 2026 (v180) toolchain saw no CUDA toolset and cmake failed with "No CUDA toolset found". - cmake was installed with no version check, but the "Visual Studio 18 2026" generator requires CMake 4.2+. This adds Get-VcBuildCustomizationsDir (derives v160/v170/v180 from the detected generator, falls back to v170), a CMake 4.2 guard for the VS 2026 generator (upgrades via winget once, else fails with a clear message), and routes both the copy target and the failure hint through the derived path. No behavior change for VS 2022/2019/2017: the folder resolves to v170 and the guard is skipped. Adds windows-latest Pester unit tests (tests/studio_setup_ps1) plus a workflow that runs them. * Address review: make VS 2026 self-contained + gate CMake guard to source build - Find-VsBuildTools now detects VS 2026: vswhere catalog_productLineVersion 2026 -> "Visual Studio 18 2026", and the filesystem scan covers the "18"/"2026" dirs (incl. non-standard editions like Preview). Adapted from #6038 by @LeoBorcherding, so the v180 BuildCustomizations path and the CMake guard are actually reachable on a VS 2026-only host. - Move the CMake 4.2 guard out of Phase 1 into the committed-source-build branch. The preferred prebuilt llama.cpp path never reaches it, so a VS 2026 host on CMake < 4.2 is no longer blocked from using the prebuilt. - winget upgrade -> install fallback when the on-PATH cmake is not the Kitware winget package, and log winget failures instead of swallowing them. - Add a windows-latest Find-VsBuildTools VS 2026 discovery regression test. * tests(vs2026): define New-FakeVsTree in BeforeAll so It blocks can see it The Find-VsBuildTools discovery tests are Windows-only (-Skip on non-Windows), so they first ran on the windows-latest Pester job, where New-FakeVsTree raised CommandNotFoundException: it was defined in the Describe body, which Pester 5 executes only during discovery, so the function did not persist into the run-phase It scope. Move it into a BeforeAll block (which runs in the run phase and is visible to the It blocks). No production code change. * Address review: probe cmake generator support, fall back to older VS, fix cmake PATH after winget The VS 2026 CMake guard previously gated only on the cmake version (>= 4.2) and hard-failed otherwise. Review on #6473 raised three real gaps: - A VS-bundled cmake below 4.2 can still drive the VS 2026 generator. Probe cmake --help (Test-CmakeListsGenerator / Test-CmakeCanDriveGenerator) and accept it when the generator is advertised, not just on the version floor. - After winget upgrade/install, an older cmake earlier on PATH kept being resolved. Add-DefaultCmakeToPath prepends the default install dir so the new cmake wins before re-probing. - When cmake cannot drive VS 2026 but an older Visual Studio (2022/2019/2017) is installed and usable, fall back to it (Get-FallbackVsGenerator) instead of hard-failing, preserving the pre-VS-2026 build path. Tests mock the cmake command rather than dropping a shim on PATH: PowerShell caches its application-path table, so a real cmake on the runner (present on windows-latest) wins over a PATH shim. A function mock is resolved first and is cache-proof cross-platform. * Detect VS installed under the Preview edition dir for older versions Find-VsBuildTools already scans every subdir for VS 2026, but the older-version (2017/2019/2022) filesystem fallback and Get-FallbackVsGenerator only checked BuildTools/Community/Professional/Enterprise. A Preview-channel install lives under a 'Preview' edition folder, so it was missed when vswhere was also unavailable. Add 'Preview' to both edition lists and guard each with a Windows Pester test. * Add real-VS integration matrix: detect actual VS 2022 and VS 2026 in parallel The unit tests validate VS detection logic with mocked vswhere and fake install trees (all five versions). This adds a parallel integration job that runs the real Find-VsBuildTools / Get-VcBuildCustomizationsDir against the Visual Studio actually preinstalled on GitHub-hosted runners: - windows-2022 -> real Visual Studio 2022, expect generator v170 - windows-2025-vs2026 -> real Visual Studio 2026, expect generator v180 It asserts our detection matches the real install, the install path exists, the derived toolset matches, and that the derived v-number is a real folder on the VS install. VS 2017/2019/2015 are retired from hosted images, so only 2022 and 2026 can be exercised against a genuine install; the rest stay covered by the mocks. * Detect VS 2026 via vswhere: it reports productLineVersion '18', not '2026' Real-VS CI on the windows-2025-vs2026 runner showed vswhere reports catalog_productLineVersion='18' (the internal major) for Visual Studio 2026, not the marketing year '2026' that VS <= 2022 report. The vswhere map only had '2026', so on a real VS 2026 host the vswhere branch returned null and detection survived only via the filesystem scan (Source='filesystem'); a VS 2026 installed outside the default Program Files location would not be found at all. Extract a pure Resolve-VsGeneratorFromLabel that accepts both the year and the internal-major form ('18'/'17'/'16'/'15' as well as '2026'/'2022'/'2019'/'2017') and use it for both the vswhere and filesystem branches. Add pure unit tests (cross-platform) for the mapping, including the '18' -> VS 2026 case. * ci: dot-source Resolve-VsGeneratorFromLabel in the real-VS integration job Find-VsBuildTools now calls Resolve-VsGeneratorFromLabel, so the integration step must extract it too; without it the job failed with the helper not recognized. * Defer Visual Studio + CMake to the llama.cpp source build (prebuilt path needs no build tools) The Windows installer required Visual Studio Build Tools and CMake eagerly in Phase 1 (winget install + exit 1 if absent), before the llama.cpp prebuilt-vs- source decision. But the preferred path downloads a prebuilt llama.cpp (no compiler), the backend only shells out to the prebuilt llama-server.exe, and PyTorch is pip wheels -- so VS and CMake are only needed for the from-source build last resort. The eager requirement forced every Windows user to install multi-GB Visual Studio + CMake they never use, or the installer failed. Change (mirrors the already-lazy Resolve-CudaToolkit / OpenSSL): - Phase 1c/1d now only DETECT cmake / VS and log; they never winget-install or exit. The prebuilt install runs zero build-tool installs and is unblocked on hosts without build tools. - New Ensure-BuildToolsForLlamaSourceBuild installs CMake (best effort) + VS (hard requirement, exit 1 with the existing guidance if it cannot be found), called only when a source build is actually committed, before Resolve-CudaToolkit. git stays eager (pip needs it for git+ deps). Tests: - Pester: the early probe (Find-VsBuildTools) returns null without exiting when no VS is present; Ensure-BuildToolsForLlamaSourceBuild no-ops when VS is already detected. - New studio-windows-no-vs-smoke.yml: Job A renames Visual Studio + vswhere away and hides cmake, runs the real install.ps1 --local --no-torch, and asserts the prebuilt llama.cpp installed (no source-build fallback, no VS/CMake install), PyTorch CPU imports, the backend is healthy, and a /v1/chat/completions inference returns a reply -- all with no Visual Studio. Job B confirms the GPU CUDA prebuilt is available and the resolver runs without VS. * Fix VS 2026 CUDA source build ordering and fallback VS discovery Same fix as on the stacked base branch (studio-vs2026-cuda-msbuild): - Move Resolve-CudaToolkit below the CMake gate/fallback in the source build path. It copies the CUDA MSBuild .targets into the current VS generator's BuildCustomizations folder, so running it before a VS 2026 to older-VS fallback left the .targets under v180 while cmake configured v170 ("No CUDA toolset found"). It now runs after the final generator is selected. - Get-FallbackVsGenerator now queries vswhere first, matching Find-VsBuildTools, so a VS installed outside the default Program Files roots is found instead of failing with a hard exit. - Add Pester regression tests: the source build resolves CUDA after the fallback, and the fallback queries vswhere. * Ensure the Visual C++ Redistributable is present for the prebuilt llama.cpp and PyTorch The prebuilt llama-server.exe and the PyTorch wheels dynamically link the MSVC runtime (VCRUNTIME140.dll, MSVCP140.dll, VCRUNTIME140_1.dll). The Universal CRT ships with Windows 10+, but the VC++ 2015-2022 redistributable does not, so a clean box can fail to launch llama-server or import torch with a missing VCRUNTIME140.dll. - Add Test-VCRedistInstalled (System32 vcruntime140_1.dll, with a registry fallback gated on version 14.20+) and Ensure-VCRedist (winget Microsoft.VCRedist.2015+.x64, non-fatal), called as Phase 1b.5 so it runs even on the no-build-tools prebuilt path. It is a no-op when the runtime is already present, which is the common case. - Add Pester tests for the detection: present via the DLL, present via the registry, absent, and an old 2015-only redist that is too low. * Add a CI job that validates the VC++ runtime detection on a real Windows runner Runs on windows-latest and windows-2025-vs2026: asserts Test-VCRedistInstalled reports present on the stock image, removes both detection signals (the System32 DLL via a redirected SystemRoot and the HKLM runtime keys, restorably) to confirm detection fires on a genuinely clean box, then does a literal uninstall/reinstall round trip with the official installer and the Ensure-VCRedist winget path. The runtime is restored before the job ends. * Dot-source the full logging closure in the VC++ runtime CI job Ensure-VCRedist calls step/substep, which reach Write-StudioStdoutMirror and Get-StudioAnsi; extract those too so the job does not fail with an unrecognized command. Also note that the runtime is ref-counted by Visual Studio on the hosted image, so the literal package uninstall is a no-op there (the clean-box section already proves detection fires when the runtime is genuinely absent). * Tighten comments in setup.ps1, the VS2026 tests and workflow Comment-only: condense the verbose helper/test/CI comments to one or two lines, drop the obvious ones, keep the non-obvious rationale. Verified comment-only by comparing the PowerShell code-token stream before and after (no code tokens changed); Pester suite still green. * Fold the no-VS and setup.ps1 VS2026 Windows CI into studio-windows-inference-smoke.yml Move the no-vs-cpu/no-vs-gpu-resolve and pester/vs-integration/vcredist-clean-box jobs into the existing Windows GGUF CI workflow and delete the two standalone files, so a studio change triggers one Windows workflow instead of three. Path filter gains tests/studio_setup_ps1/**; job keys and artifact names stay unique. * CI: assert a Windows ROCm prebuilt exists in the no-VS resolve job The no-vs-gpu-resolve job confirmed a Windows CUDA asset but never a ROCm one, and the resolver step resolves to CPU on hosted runners (no AMD GPU), so the AMD no-VS guarantee rode only on shared resolver code. Grep the per-gfx windows-x64-rocm-gfx bundles in the same asset-availability step so a release that drops the Windows ROCm prebuilts fails loudly. --------- Co-authored-by: Daniel Han <michaelhan2050@gmail.com>
This commit is contained in:
parent
d77845ebc0
commit
a41b8c7a44
4 changed files with 1316 additions and 93 deletions
535
.github/workflows/studio-windows-inference-smoke.yml
vendored
535
.github/workflows/studio-windows-inference-smoke.yml
vendored
|
|
@ -26,6 +26,7 @@ on:
|
|||
- 'unsloth_cli/**'
|
||||
- 'install.ps1'
|
||||
- 'pyproject.toml'
|
||||
- 'tests/studio_setup_ps1/**'
|
||||
- '.github/workflows/studio-windows-inference-smoke.yml'
|
||||
push:
|
||||
branches: [main, pip]
|
||||
|
|
@ -1244,3 +1245,537 @@ jobs:
|
|||
logs/install.log
|
||||
logs/llama-server/*.log
|
||||
retention-days: 7
|
||||
|
||||
# ── folded from studio-windows-no-vs-smoke.yml: install + run with no Visual Studio ──
|
||||
no-vs-cpu:
|
||||
name: Studio install + inference without Visual Studio
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 35
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
GGUF_REPO: unsloth/gemma-3-270m-it-GGUF
|
||||
GGUF_VARIANT: UD-Q4_K_XL
|
||||
GGUF_FILE: gemma-3-270m-it-UD-Q4_K_XL.gguf
|
||||
STUDIO_PORT: '18820'
|
||||
HF_HOME: ${{ github.workspace }}/hf-cache
|
||||
PYTHONIOENCODING: utf-8
|
||||
PYTHONUTF8: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Restore HF_HOME for ${{ env.GGUF_REPO }}
|
||||
id: cache-hf
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: hf-cache
|
||||
key: ${{ runner.os }}-hf-${{ env.GGUF_REPO }}-${{ env.GGUF_VARIANT }}-v2
|
||||
|
||||
- name: Prime HF_HOME with the GGUF
|
||||
id: prime-hf
|
||||
if: steps.cache-hf.outputs.cache-hit != 'true' || steps.cache-hf.outcome != 'success'
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
run: |
|
||||
python -m pip install --upgrade huggingface_hub
|
||||
mkdir -p hf-cache
|
||||
bash .github/scripts/hf-download-with-retry.sh "$GGUF_REPO" "$GGUF_FILE"
|
||||
bash .github/scripts/hf-download-with-retry.sh ggml-org/models tinyllamas/stories260K.gguf
|
||||
|
||||
- name: Save HF_HOME for ${{ env.GGUF_REPO }}
|
||||
if: always() && steps.prime-hf.outcome == 'success'
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: hf-cache
|
||||
key: ${{ runner.os }}-hf-${{ env.GGUF_REPO }}-${{ env.GGUF_VARIANT }}-v2
|
||||
|
||||
- name: Pre-install Windows tweaks (npm 11 + Defender exclusions)
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
npm install -g 'npm@^11' 2>&1 | Out-Host
|
||||
foreach ($p in @(
|
||||
"$env:USERPROFILE\.unsloth",
|
||||
"$env:USERPROFILE\AppData\Local\uv",
|
||||
"$env:GITHUB_WORKSPACE\studio\frontend\node_modules",
|
||||
"$env:GITHUB_WORKSPACE\studio\frontend\dist"
|
||||
)) {
|
||||
try { Add-MpPreference -ExclusionPath $p -ErrorAction Stop } catch { }
|
||||
}
|
||||
|
||||
- name: Hide Visual Studio + CMake (simulate a host with no build tools)
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# Rename the Visual Studio install roots (incl. the Installer that holds
|
||||
# vswhere.exe) so Find-VsBuildTools' vswhere + filesystem scan both miss.
|
||||
foreach ($d in @("$env:ProgramFiles\Microsoft Visual Studio", "${env:ProgramFiles(x86)}\Microsoft Visual Studio")) {
|
||||
if (Test-Path -LiteralPath $d) {
|
||||
Rename-Item -LiteralPath $d -NewName ((Split-Path $d -Leaf) + '.vsoff')
|
||||
Write-Host "Hid VS: $d"
|
||||
}
|
||||
}
|
||||
# Surgically rename each cmake executable on PATH (not its parent dir --
|
||||
# cmake can share a dir with other shims) so Get-Command cmake fails.
|
||||
$hidden = @()
|
||||
foreach ($c in (Get-Command cmake -All -ErrorAction SilentlyContinue)) {
|
||||
if ($c.Source -and (Test-Path -LiteralPath $c.Source)) {
|
||||
Rename-Item -LiteralPath $c.Source -NewName ((Split-Path $c.Source -Leaf) + '.off')
|
||||
$hidden += $c.Source
|
||||
Write-Host "Hid cmake: $($c.Source)"
|
||||
}
|
||||
}
|
||||
("HIDDEN_CMAKE=" + ($hidden -join '|')) | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||
|
||||
- name: Assert Visual Studio + CMake are genuinely undetectable
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. (Join-Path $env:GITHUB_WORKSPACE 'tests/studio_setup_ps1/Get-FunctionSource.ps1')
|
||||
$setup = Join-Path $env:GITHUB_WORKSPACE 'studio/setup.ps1'
|
||||
foreach ($fn in @('Resolve-VsGeneratorFromLabel', 'Find-VsBuildTools')) {
|
||||
. ([scriptblock]::Create((Get-FunctionSource -Path $setup -Name $fn)))
|
||||
}
|
||||
$vs = Find-VsBuildTools
|
||||
if ($vs) { Write-Error "Find-VsBuildTools still detects VS: $($vs.Generator) @ $($vs.InstallPath)"; exit 1 }
|
||||
if (Get-Command cmake -ErrorAction SilentlyContinue) { Write-Error "cmake is still on PATH"; exit 1 }
|
||||
if (Get-Command cl.exe -ErrorAction SilentlyContinue) { Write-Error "cl.exe is still on PATH"; exit 1 }
|
||||
Write-Host "Confirmed: no Visual Studio, no cmake, no cl.exe."
|
||||
|
||||
- name: PyTorch CPU wheel installs and imports (no Visual Studio)
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||
python -c "import torch; print('torch', torch.__version__, 'cuda?', torch.cuda.is_available())"
|
||||
|
||||
- name: Install Studio (--local, --no-torch) with no build tools present
|
||||
shell: pwsh
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path logs | Out-Null
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
& ./install.ps1 --local --no-torch *>&1 | Tee-Object -FilePath logs/install.log
|
||||
|
||||
- name: Assert prebuilt used AND no build tools were installed
|
||||
run: |
|
||||
LLAMA_DIR=~/.unsloth/llama.cpp
|
||||
INFO="$LLAMA_DIR/UNSLOTH_PREBUILT_INFO.json"
|
||||
BIN="$LLAMA_DIR/build/bin/Release/llama-server.exe"
|
||||
fail=0
|
||||
if grep -q "falling back to source build" logs/install.log; then
|
||||
echo "::error::install.ps1 fell back to source-build llama.cpp without VS."; fail=1
|
||||
fi
|
||||
# The deferred build-tool installs must NOT run on the prebuilt path.
|
||||
for pat in "Kitware.CMake" "Microsoft.VisualStudio.2022.BuildTools" "installing via winget"; do
|
||||
if grep -qi "$pat" logs/install.log; then
|
||||
echo "::error::unexpected build-tool install on the prebuilt path: '$pat'"; fail=1
|
||||
fi
|
||||
done
|
||||
[ -f "$INFO" ] || { echo "::error::no UNSLOTH_PREBUILT_INFO.json"; ls -la "$LLAMA_DIR" || true; fail=1; }
|
||||
[ -f "$BIN" ] || { echo "::error::no llama-server.exe"; ls -la "$LLAMA_DIR/build/bin" || true; fail=1; }
|
||||
if [ "$fail" != "0" ]; then grep -iE "cmake|visual studio|prebuilt|source build" logs/install.log | tail -60; exit 1; fi
|
||||
echo "Prebuilt installed with no build tools:"
|
||||
cat "$INFO"
|
||||
|
||||
- name: Add Studio shim to GITHUB_PATH
|
||||
run: |
|
||||
SHIM_DIR=~/.unsloth/studio/bin
|
||||
[ -f "$SHIM_DIR/unsloth.exe" ] || { echo "::error::unsloth.exe shim not found"; ls -la ~/.unsloth/studio/ || true; exit 1; }
|
||||
cygpath -w "$SHIM_DIR" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Reset auth + boot Studio (API-only)
|
||||
run: |
|
||||
unsloth studio reset-password
|
||||
mkdir -p logs
|
||||
UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \
|
||||
> logs/studio.log 2>&1 &
|
||||
echo "STUDIO_PID=$!" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Wait for /api/health, log in, load the GGUF
|
||||
run: |
|
||||
for i in $(seq 1 180); do
|
||||
if curl -fs "http://127.0.0.1:${STUDIO_PORT}/api/health" > /tmp/health.json; then
|
||||
jq -e '.status == "healthy"' /tmp/health.json && break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
jq -e '.status == "healthy"' /tmp/health.json || { tail -200 logs/studio.log; exit 1; }
|
||||
OLD=$(cat ~/.unsloth/studio/auth/.bootstrap_password)
|
||||
NEW="CINoVS-$(python -c 'import secrets; print(secrets.token_urlsafe(12))')"
|
||||
echo "::add-mask::$OLD"
|
||||
echo "::add-mask::$NEW"
|
||||
OLD_TOKEN=$(curl -fs -X POST "http://127.0.0.1:${STUDIO_PORT}/api/auth/login" \
|
||||
-H 'content-type: application/json' \
|
||||
-d "{\"username\":\"unsloth\",\"password\":\"$OLD\"}" | jq -r .access_token)
|
||||
curl -fs -X POST "http://127.0.0.1:${STUDIO_PORT}/api/auth/change-password" \
|
||||
-H "Authorization: Bearer $OLD_TOKEN" -H 'content-type: application/json' \
|
||||
-d "{\"current_password\":\"$OLD\",\"new_password\":\"$NEW\"}" > /dev/null
|
||||
TOKEN=$(curl -fs -X POST "http://127.0.0.1:${STUDIO_PORT}/api/auth/login" \
|
||||
-H 'content-type: application/json' \
|
||||
-d "{\"username\":\"unsloth\",\"password\":\"$NEW\"}" | jq -r .access_token)
|
||||
echo "API_KEY=$TOKEN" >> "$GITHUB_ENV"
|
||||
LOAD_OK=0
|
||||
for attempt in 1 2 3; do
|
||||
HTTP=$(curl -s -o /tmp/load.json -w '%{http_code}' \
|
||||
-X POST "http://127.0.0.1:${STUDIO_PORT}/api/inference/load" \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
|
||||
--max-time 600 \
|
||||
-d "{\"model_path\":\"$GGUF_REPO\",\"gguf_variant\":\"$GGUF_VARIANT\",\"is_lora\":false,\"max_seq_length\":2048}")
|
||||
if [ "$HTTP" = "200" ]; then LOAD_OK=1; break; fi
|
||||
echo "::warning::/api/inference/load attempt $attempt returned $HTTP"; cat /tmp/load.json || true; sleep 10
|
||||
done
|
||||
[ "$LOAD_OK" = "1" ] || { echo "::error::/api/inference/load failed 3 attempts"; exit 22; }
|
||||
jq '{status, display_name, is_gguf}' /tmp/load.json
|
||||
|
||||
- name: Inference works via the prebuilt llama.cpp (no VS)
|
||||
run: |
|
||||
RESP=$(curl -fs -X POST "http://127.0.0.1:${STUDIO_PORT}/v1/chat/completions" \
|
||||
-H "Authorization: Bearer $API_KEY" -H 'content-type: application/json' \
|
||||
--max-time 240 \
|
||||
-d '{"model":"default","messages":[{"role":"user","content":"What is 1+1? Answer briefly."}],"temperature":0,"max_tokens":32,"stream":false}')
|
||||
echo "$RESP" | jq '.choices[0].message' || { echo "$RESP"; exit 1; }
|
||||
CONTENT=$(echo "$RESP" | jq -r '.choices[0].message.content')
|
||||
[ -n "$CONTENT" ] && [ "$CONTENT" != "null" ] || { echo "::error::empty completion"; exit 1; }
|
||||
echo "Inference OK without Visual Studio: $CONTENT"
|
||||
|
||||
- name: Restore Visual Studio + CMake
|
||||
if: always()
|
||||
shell: pwsh
|
||||
run: |
|
||||
foreach ($d in @("$env:ProgramFiles\Microsoft Visual Studio", "${env:ProgramFiles(x86)}\Microsoft Visual Studio")) {
|
||||
$off = "$d.vsoff"
|
||||
if (Test-Path -LiteralPath $off) { Rename-Item -LiteralPath $off -NewName (Split-Path $d -Leaf); Write-Host "Restored $d" }
|
||||
}
|
||||
if ($env:HIDDEN_CMAKE) {
|
||||
foreach ($src in ($env:HIDDEN_CMAKE -split '\|')) {
|
||||
if ($src -and (Test-Path -LiteralPath "$src.off")) { Rename-Item -LiteralPath "$src.off" -NewName (Split-Path $src -Leaf) }
|
||||
}
|
||||
}
|
||||
|
||||
- name: Stop Studio
|
||||
if: always()
|
||||
shell: cmd
|
||||
run: echo Stop Studio (no-op; runner reclaims STUDIO_PID=%STUDIO_PID% at job end)
|
||||
|
||||
- name: Collect llama-server logs
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
run: |
|
||||
mkdir -p logs/llama-server
|
||||
cp -v ~/.unsloth/studio/logs/llama-server/*.log logs/llama-server/ 2>/dev/null || echo "no llama-server logs"
|
||||
|
||||
- name: Upload logs
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: windows-no-vs-cpu-log
|
||||
path: |
|
||||
logs/install.log
|
||||
logs/studio.log
|
||||
logs/llama-server/*.log
|
||||
retention-days: 7
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Job B: the GPU (CUDA) prebuilt path is also VS-free (resolve/availability)
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
no-vs-gpu-resolve:
|
||||
name: GPU prebuilt resolves without Visual Studio
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
PYTHONIOENCODING: utf-8
|
||||
PYTHONUTF8: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Hide Visual Studio
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
foreach ($d in @("$env:ProgramFiles\Microsoft Visual Studio", "${env:ProgramFiles(x86)}\Microsoft Visual Studio")) {
|
||||
if (Test-Path -LiteralPath $d) { Rename-Item -LiteralPath $d -NewName ((Split-Path $d -Leaf) + '.vsoff'); Write-Host "Hid VS: $d" }
|
||||
}
|
||||
|
||||
- name: Windows CUDA and ROCm prebuilts exist in unslothai/llama.cpp (what GPU users download, no VS)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
curl -fsSL -H "Authorization: Bearer $GH_TOKEN" \
|
||||
"https://api.github.com/repos/unslothai/llama.cpp/releases/latest" > /tmp/rel.json
|
||||
echo "release: $(jq -r .tag_name /tmp/rel.json)"
|
||||
ASSETS=$(jq -r '.assets[].name' /tmp/rel.json)
|
||||
echo "$ASSETS" | grep -iE 'windows-x64-cuda[0-9]' || {
|
||||
echo "::error::no Windows x64 CUDA prebuilt asset found in unslothai/llama.cpp latest release"
|
||||
echo "$ASSETS"; exit 1; }
|
||||
# AMD parity: hosted runners have no AMD GPU, so the resolver step below
|
||||
# can't exercise the ROCm path (it resolves to CPU). Pin the per-gfx
|
||||
# Windows ROCm bundles here so a release that drops them fails loudly --
|
||||
# the AMD no-VS guarantee otherwise rides only on shared resolver code.
|
||||
echo "$ASSETS" | grep -iE 'windows-x64-rocm-gfx' || {
|
||||
echo "::error::no Windows x64 ROCm (per-gfx) prebuilt asset found in unslothai/llama.cpp latest release"
|
||||
echo "$ASSETS"; exit 1; }
|
||||
echo "Windows CUDA and ROCm prebuilts are available -- GPU users get them without compiling."
|
||||
|
||||
- name: The prebuilt resolver runs without Visual Studio
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Resolver-only (no GPU on hosted runners, so the host resolves to the
|
||||
# CPU bundle). The point is that resolution needs no compiler/VS.
|
||||
python -m pip install --upgrade huggingface_hub
|
||||
python studio/install_llama_prebuilt.py --resolve-prebuilt latest --output-format json > /tmp/resolve.json || {
|
||||
echo "::error::resolver exited non-zero"; cat /tmp/resolve.json || true; exit 1; }
|
||||
cat /tmp/resolve.json
|
||||
echo "Prebuilt resolver ran with no Visual Studio present."
|
||||
|
||||
- name: Restore Visual Studio
|
||||
if: always()
|
||||
shell: pwsh
|
||||
run: |
|
||||
foreach ($d in @("$env:ProgramFiles\Microsoft Visual Studio", "${env:ProgramFiles(x86)}\Microsoft Visual Studio")) {
|
||||
$off = "$d.vsoff"
|
||||
if (Test-Path -LiteralPath $off) { Rename-Item -LiteralPath $off -NewName (Split-Path $d -Leaf); Write-Host "Restored $d" }
|
||||
}
|
||||
|
||||
# ── folded from studio-setup-ps1-vs2026.yml: setup.ps1 unit tests + real-VS detection + vcredist ──
|
||||
pester:
|
||||
name: setup.ps1 unit tests (VS 2026 / CMake guard)
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Pester v5
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-PSRepository PSGallery -InstallationPolicy Trusted
|
||||
Install-Module Pester -MinimumVersion 5.5.0 -Force -SkipPublisherCheck -Scope CurrentUser
|
||||
Import-Module Pester -MinimumVersion 5.5.0
|
||||
Get-Module Pester | Select-Object Name, Version | Format-Table
|
||||
|
||||
- name: Run Pester suite
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$testDir = Join-Path $env:GITHUB_WORKSPACE 'tests/studio_setup_ps1'
|
||||
if (-not (Test-Path $testDir)) {
|
||||
Write-Error "Test directory not found: $testDir"
|
||||
exit 1
|
||||
}
|
||||
$cfg = New-PesterConfiguration
|
||||
$cfg.Run.Path = $testDir
|
||||
$cfg.Run.Exit = $true # non-zero exit => job fails
|
||||
$cfg.Run.Throw = $true # also throw on test failure / 0 tests
|
||||
$cfg.TestResult.Enabled = $true
|
||||
$cfg.TestResult.OutputFormat = 'NUnitXml'
|
||||
$cfg.TestResult.OutputPath = Join-Path $env:GITHUB_WORKSPACE 'pester-results.xml'
|
||||
$cfg.Output.Verbosity = 'Detailed'
|
||||
Invoke-Pester -Configuration $cfg
|
||||
|
||||
- name: Upload Pester results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: pester-results-setup-ps1
|
||||
path: pester-results.xml
|
||||
if-no-files-found: warn
|
||||
|
||||
vs-integration:
|
||||
# Real detection against the VS installed on the runner image (no mocks).
|
||||
name: real-VS detection (${{ matrix.label }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { os: windows-2022, label: 'VS 2022', expectGen: 'Visual Studio 17 2022', expectToolset: 'v170' }
|
||||
- { os: windows-2025-vs2026, label: 'VS 2026', expectGen: 'Visual Studio 18 2026', expectToolset: 'v180' }
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Detect the real Visual Studio with setup.ps1 functions
|
||||
shell: pwsh
|
||||
env:
|
||||
EXPECT_GEN: ${{ matrix.expectGen }}
|
||||
EXPECT_TOOLSET: ${{ matrix.expectToolset }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. (Join-Path $env:GITHUB_WORKSPACE 'tests/studio_setup_ps1/Get-FunctionSource.ps1')
|
||||
$setup = Join-Path $env:GITHUB_WORKSPACE 'studio/setup.ps1'
|
||||
foreach ($fn in @('Resolve-VsGeneratorFromLabel', 'Get-VcBuildCustomizationsDir', 'Find-VsBuildTools')) {
|
||||
. ([scriptblock]::Create((Get-FunctionSource -Path $setup -Name $fn)))
|
||||
}
|
||||
|
||||
# Ground truth from the real vswhere (independent of our code), for visibility.
|
||||
$vsw = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (Test-Path $vsw) {
|
||||
$year = (& $vsw -latest -property catalog_productLineVersion 2>$null | Select-Object -First 1)
|
||||
$path = (& $vsw -latest -property installationPath 2>$null | Select-Object -First 1)
|
||||
Write-Host "Real vswhere: productLineVersion='$year' installPath='$path'"
|
||||
} else {
|
||||
Write-Host "vswhere not present at $vsw (relying on filesystem fallback)"
|
||||
}
|
||||
|
||||
# Our detection must find the real VS and report the expected generator.
|
||||
$r = Find-VsBuildTools
|
||||
if (-not $r) { throw "Find-VsBuildTools returned null on a host with real $env:EXPECT_GEN" }
|
||||
Write-Host "Find-VsBuildTools -> Generator='$($r.Generator)' Source='$($r.Source)' InstallPath='$($r.InstallPath)'"
|
||||
if ($r.Generator -ne $env:EXPECT_GEN) {
|
||||
throw "Detection mismatch: got '$($r.Generator)', expected '$env:EXPECT_GEN'"
|
||||
}
|
||||
if (-not (Test-Path $r.InstallPath)) { throw "Detected InstallPath does not exist: $($r.InstallPath)" }
|
||||
|
||||
# Toolset path derivation must match the expected v-number...
|
||||
$bc = Get-VcBuildCustomizationsDir -VsInstallPath $r.InstallPath -Generator $r.Generator
|
||||
$derived = Split-Path (Split-Path $bc -Parent) -Leaf # e.g. v170 / v180
|
||||
Write-Host "Get-VcBuildCustomizationsDir -> '$bc' (toolset='$derived')"
|
||||
if ($derived -ne $env:EXPECT_TOOLSET) {
|
||||
throw "Toolset mismatch: derived '$derived', expected '$env:EXPECT_TOOLSET'"
|
||||
}
|
||||
|
||||
# ...and that v-number is a real folder on the VS install (where CUDA's
|
||||
# BuildCustomizations would land).
|
||||
$vcRoot = Join-Path $r.InstallPath 'MSBuild\Microsoft\VC'
|
||||
if (Test-Path $vcRoot) {
|
||||
$realToolsets = @((Get-ChildItem -Path $vcRoot -Directory -ErrorAction SilentlyContinue).Name)
|
||||
Write-Host "Real VC toolset dirs: $($realToolsets -join ', ')"
|
||||
if ($realToolsets -notcontains $derived) {
|
||||
throw "Derived toolset '$derived' is not present on the real $env:EXPECT_GEN install (have: $($realToolsets -join ', '))"
|
||||
}
|
||||
Write-Host "OK: toolset '$derived' exists on the real VS install."
|
||||
} else {
|
||||
Write-Warning "VC MSBuild root absent ($vcRoot) - C++ workload not installed; skipping on-disk toolset check."
|
||||
}
|
||||
|
||||
Write-Host "PASS: real $env:EXPECT_GEN detected correctly with toolset '$derived'."
|
||||
|
||||
vcredist-clean-box:
|
||||
# Validate Test-VCRedistInstalled + Ensure-VCRedist on a throwaway runner:
|
||||
# present on the stock image, fires on a clean box (signals removed restorably),
|
||||
# then a literal uninstall/reinstall round trip. Always restored before the end.
|
||||
name: VC++ runtime detect + install round-trip (${{ matrix.os }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, windows-2025-vs2026]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Detect present, fire on a clean box, and round-trip the install
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. (Join-Path $env:GITHUB_WORKSPACE 'tests/studio_setup_ps1/Get-FunctionSource.ps1')
|
||||
$setup = Join-Path $env:GITHUB_WORKSPACE 'studio/setup.ps1'
|
||||
# Dot-source the guard + the logging closure it reaches
|
||||
# (step/substep -> Write-StudioStdoutMirror / Get-StudioAnsi).
|
||||
$script:StudioVtOk = $false
|
||||
$script:UnslothVerbose = $false
|
||||
foreach ($fn in @('Get-StudioAnsi', 'Write-StudioStdoutMirror', 'step', 'substep',
|
||||
'Invoke-SetupCommand', 'Refresh-Environment',
|
||||
'Test-VCRedistInstalled', 'Ensure-VCRedist')) {
|
||||
$src = Get-FunctionSource -Path $setup -Name $fn
|
||||
if (-not $src) { throw "Function '$fn' not found in setup.ps1" }
|
||||
. ([scriptblock]::Create($src))
|
||||
}
|
||||
|
||||
$regKeys = @(
|
||||
'HKLM\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64',
|
||||
'HKLM\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\x64'
|
||||
)
|
||||
function Show-GroundTruth {
|
||||
$dll = Join-Path $env:SystemRoot 'System32\vcruntime140_1.dll'
|
||||
Write-Host (" System32\vcruntime140_1.dll present: {0}" -f (Test-Path $dll))
|
||||
foreach ($k in $regKeys) {
|
||||
$r = Get-ItemProperty -Path "HKLM:\$($k.Substring(5))" -ErrorAction SilentlyContinue
|
||||
if ($r) { Write-Host (" {0}: Installed={1} {2}.{3}" -f $k, $r.Installed, $r.Major, $r.Minor) }
|
||||
else { Write-Host (" {0}: (absent)" -f $k) }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host '== A. Detection on the stock runner (expect present) =='
|
||||
Show-GroundTruth
|
||||
if (-not (Test-VCRedistInstalled)) { throw 'Test-VCRedistInstalled reported ABSENT on a stock runner that ships the VC++ runtime (detection regression).' }
|
||||
Write-Host ' Test-VCRedistInstalled -> present OK'
|
||||
|
||||
Write-Host '== B. Genuinely clean box (restorable): detection must FIRE =='
|
||||
$scratch = Join-Path $env:RUNNER_TEMP 'cleanwin'
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $scratch 'System32') | Out-Null
|
||||
$backup = Join-Path $env:RUNNER_TEMP 'vcreg_backup'
|
||||
New-Item -ItemType Directory -Force -Path $backup | Out-Null
|
||||
$origSysRoot = $env:SystemRoot
|
||||
try {
|
||||
for ($i = 0; $i -lt $regKeys.Count; $i++) {
|
||||
reg query $regKeys[$i] *> $null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
reg export $regKeys[$i] (Join-Path $backup "$i.reg") /y *> $null
|
||||
reg delete $regKeys[$i] /f *> $null
|
||||
}
|
||||
}
|
||||
$env:SystemRoot = $scratch
|
||||
if (Test-VCRedistInstalled) { throw 'Detection still PRESENT after both signals were removed (it would never trigger an install on a clean box).' }
|
||||
Write-Host ' Test-VCRedistInstalled -> absent OK (detection fires on a clean box)'
|
||||
} finally {
|
||||
$env:SystemRoot = $origSysRoot
|
||||
for ($i = 0; $i -lt $regKeys.Count; $i++) {
|
||||
$f = Join-Path $backup "$i.reg"
|
||||
if (Test-Path $f) { reg import $f *> $null }
|
||||
}
|
||||
}
|
||||
Show-GroundTruth
|
||||
if (-not (Test-VCRedistInstalled)) { throw 'Detection did not recover after restoring the registry (test restore bug).' }
|
||||
|
||||
Write-Host '== C. Literal uninstall on this throwaway VM (official installer), observe detection =='
|
||||
$exe = Join-Path $env:RUNNER_TEMP 'vc_redist.x64.exe'
|
||||
Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vc_redist.x64.exe' -OutFile $exe
|
||||
Start-Process -FilePath $exe -ArgumentList '/uninstall', '/quiet', '/norestart' -Wait
|
||||
Show-GroundTruth
|
||||
Write-Host (" Test-VCRedistInstalled after uninstall -> {0}" -f (Test-VCRedistInstalled))
|
||||
if (Test-VCRedistInstalled) {
|
||||
Write-Host ' Note: the Visual Studio on this image ref-counts the runtime, so the package'
|
||||
Write-Host ' uninstall is a no-op here; section B already proved detection on a clean box.'
|
||||
}
|
||||
|
||||
Write-Host '== D. Restore via Ensure-VCRedist (winget product path), installer fallback if needed =='
|
||||
Ensure-VCRedist
|
||||
if (-not (Test-VCRedistInstalled)) {
|
||||
Write-Host ' winget path did not restore it; using the official installer to close the round trip.'
|
||||
Start-Process -FilePath $exe -ArgumentList '/install', '/quiet', '/norestart' -Wait
|
||||
}
|
||||
Show-GroundTruth
|
||||
if (-not (Test-VCRedistInstalled)) { throw 'VC++ runtime could not be restored after the uninstall round-trip.' }
|
||||
Write-Host ' Test-VCRedistInstalled -> present OK'
|
||||
Write-Host 'PASS: detection is correct on a real install, fires on a clean box, and the install round-trip restores the runtime.'
|
||||
|
|
|
|||
451
studio/setup.ps1
451
studio/setup.ps1
|
|
@ -390,45 +390,199 @@ function Get-PytorchCudaTag {
|
|||
return "cu126"
|
||||
}
|
||||
|
||||
# Find Visual Studio Build Tools for cmake -G flag.
|
||||
# Strategy: (1) vswhere, (2) scan filesystem (handles broken vswhere registration).
|
||||
# Returns @{ Generator = "Visual Studio 17 2022"; InstallPath = "C:\..."; Source = "..." } or $null.
|
||||
function Find-VsBuildTools {
|
||||
$map = @{ '2022' = '17'; '2019' = '16'; '2017' = '15' }
|
||||
# VS generator -> MSBuild BuildCustomizations dir; toolset tracks the VS major
|
||||
# (18->v180, 17->v170), defaulting to v170 when unparseable.
|
||||
function Get-VcBuildCustomizationsDir {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$VsInstallPath,
|
||||
[string]$Generator
|
||||
)
|
||||
$toolset = 'v170'
|
||||
if ($Generator -and ($Generator -match 'Visual Studio (\d+)\b')) {
|
||||
$toolset = "v$($Matches[1])0"
|
||||
}
|
||||
return (Join-Path $VsInstallPath "MSBuild\Microsoft\VC\$toolset\BuildCustomizations")
|
||||
}
|
||||
|
||||
# --- Try vswhere first (works when VS is properly registered) ---
|
||||
# Installed cmake version, or $null if absent/unparseable.
|
||||
function Get-CmakeVersion {
|
||||
$raw = & cmake --version 2>$null | Select-Object -First 1
|
||||
if ($raw -and ($raw -match '(\d+)\.(\d+)(?:\.(\d+))?')) {
|
||||
$patch = if ($Matches[3]) { $Matches[3] } else { '0' }
|
||||
return [version]"$($Matches[1]).$($Matches[2]).$patch"
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# VS 18 2026 generator needs cmake >= 4.2 (added there); true for older VS generators.
|
||||
function Test-CmakeSupportsGenerator {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$CmakeVersion,
|
||||
[Parameter(Mandatory)][string]$Generator
|
||||
)
|
||||
if ($Generator -match 'Visual Studio 18\b') {
|
||||
$clean = ($CmakeVersion -replace '[^0-9.].*$', '').TrimEnd('.')
|
||||
try { $v = [version]$clean } catch { return $false }
|
||||
return ($v -ge [version]'4.2')
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
function Test-CmakeListsGenerator {
|
||||
# Does `cmake --help` actually list the generator? A VS-bundled cmake can drive
|
||||
# VS 2026 below the 4.2 floor, so probe rather than trust the version. (#6473)
|
||||
param([Parameter(Mandatory)][string]$Generator)
|
||||
$help = & cmake --help 2>$null | Out-String
|
||||
if (-not $help) { return $false }
|
||||
$haystack = ($help -replace '\s+', ' ')
|
||||
$needle = ($Generator -replace '\s+', ' ')
|
||||
return $haystack.Contains($needle)
|
||||
}
|
||||
|
||||
function Test-CmakeCanDriveGenerator {
|
||||
# cmake can drive $Generator if it lists it (VS-bundled below 4.2) or meets the floor.
|
||||
param([Parameter(Mandatory)][string]$Generator)
|
||||
if (Test-CmakeListsGenerator -Generator $Generator) { return $true }
|
||||
$verObj = Get-CmakeVersion
|
||||
$verStr = if ($verObj) { $verObj.ToString() } else { '0.0' }
|
||||
return (Test-CmakeSupportsGenerator -CmakeVersion $verStr -Generator $Generator)
|
||||
}
|
||||
|
||||
function Add-DefaultCmakeToPath {
|
||||
# Prepend the default CMake dir so a freshly winget-installed cmake wins over an
|
||||
# older one already on PATH. $true if found. (#6473)
|
||||
$cmakeDefaults = @(
|
||||
"$env:ProgramFiles\CMake\bin",
|
||||
"${env:ProgramFiles(x86)}\CMake\bin",
|
||||
"$env:LOCALAPPDATA\CMake\bin"
|
||||
)
|
||||
foreach ($d in $cmakeDefaults) {
|
||||
if (Test-Path (Join-Path $d "cmake.exe")) {
|
||||
$env:Path = "$d;$env:Path"
|
||||
Add-ToUserPath -Directory $d -Position 'Prepend' | Out-Null
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-FallbackVsGenerator {
|
||||
# Newest pre-2026 VS whose generator the current cmake can drive, for when the
|
||||
# VS 2026 generator is unusable (old/offline cmake) but an older toolchain exists.
|
||||
# vswhere first (catches non-default roots like D:\), then Program Files; matches
|
||||
# Find-VsBuildTools. Returns @{ Generator; InstallPath } or $null. (#6473)
|
||||
$knownEditions = @('BuildTools', 'Community', 'Professional', 'Enterprise', 'Preview')
|
||||
|
||||
# install path if it holds a usable cl.exe, else $null
|
||||
$tryCandidate = {
|
||||
param($gen, $installPath)
|
||||
if (-not $installPath) { return $null }
|
||||
$vcDir = Join-Path $installPath "VC\Tools\MSVC"
|
||||
if (-not (Test-Path $vcDir)) { return $null }
|
||||
$cl = Get-ChildItem -Path $vcDir -Filter "cl.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($cl) { return @{ Generator = $gen; InstallPath = $installPath } }
|
||||
return $null
|
||||
}
|
||||
|
||||
# vswhere (non-default roots)
|
||||
$vsw = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (Test-Path $vsw) {
|
||||
$json = & $vsw -all -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -format json 2>$null | Out-String
|
||||
if ($json) {
|
||||
try { $instances = @($json | ConvertFrom-Json) } catch { $instances = @() }
|
||||
$ranked = $instances | ForEach-Object {
|
||||
$label = if ($_.catalog -and $_.catalog.productLineVersion) { [string]$_.catalog.productLineVersion } else { '' }
|
||||
[pscustomobject]@{ Gen = (Resolve-VsGeneratorFromLabel $label); Path = [string]$_.installationPath }
|
||||
} | Where-Object { $_.Gen -and ($_.Gen -notmatch 'Visual Studio 18\b') }
|
||||
# newest first: 2022 > 2019 > 2017
|
||||
$ranked = $ranked | Sort-Object { switch -regex ($_.Gen) { '17 2022' {0} '16 2019' {1} '15 2017' {2} default {9} } }
|
||||
foreach ($cand in $ranked) {
|
||||
if (-not (Test-CmakeListsGenerator -Generator $cand.Gen)) { continue }
|
||||
$res = & $tryCandidate $cand.Gen $cand.Path
|
||||
if ($res) { return $res }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Program Files scan
|
||||
$roots = @($env:ProgramFiles, ${env:ProgramFiles(x86)}) | Where-Object { $_ }
|
||||
$older = @(
|
||||
@{ Dir = '2022'; Generator = 'Visual Studio 17 2022' },
|
||||
@{ Dir = '2019'; Generator = 'Visual Studio 16 2019' },
|
||||
@{ Dir = '2017'; Generator = 'Visual Studio 15 2017' }
|
||||
)
|
||||
foreach ($entry in $older) {
|
||||
if (-not (Test-CmakeListsGenerator -Generator $entry.Generator)) { continue }
|
||||
foreach ($r in $roots) {
|
||||
$vsBase = Join-Path $r "Microsoft Visual Studio\$($entry.Dir)"
|
||||
if (-not (Test-Path $vsBase)) { continue }
|
||||
foreach ($ed in $knownEditions) {
|
||||
$candidate = Join-Path $vsBase $ed
|
||||
if (-not (Test-Path $candidate)) { continue }
|
||||
$res = & $tryCandidate $entry.Generator $candidate
|
||||
if ($res) { return $res }
|
||||
}
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# VS version label -> cmake generator. vswhere's productLineVersion is the year for
|
||||
# VS <= 2022 but the internal major "18" for VS 2026, and dir names use either form,
|
||||
# so accept both. (VS 2026 detection adapted from @LeoBorcherding's #6038.)
|
||||
function Resolve-VsGeneratorFromLabel {
|
||||
param([string]$Label)
|
||||
if (-not $Label) { return $null }
|
||||
$map = @{
|
||||
'2026' = 'Visual Studio 18 2026'; '18' = 'Visual Studio 18 2026'
|
||||
'2022' = 'Visual Studio 17 2022'; '17' = 'Visual Studio 17 2022'
|
||||
'2019' = 'Visual Studio 16 2019'; '16' = 'Visual Studio 16 2019'
|
||||
'2017' = 'Visual Studio 15 2017'; '15' = 'Visual Studio 15 2017'
|
||||
}
|
||||
return $map[$Label.Trim()]
|
||||
}
|
||||
|
||||
# Find VS Build Tools for cmake -G: vswhere, then a filesystem scan (handles broken
|
||||
# vswhere registration). Returns @{ Generator; InstallPath; Source } or $null.
|
||||
function Find-VsBuildTools {
|
||||
# vswhere first (works when VS is properly registered)
|
||||
$vsw = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (Test-Path $vsw) {
|
||||
$info = & $vsw -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property catalog_productLineVersion 2>$null
|
||||
$path = & $vsw -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null
|
||||
if ($info -and $path) {
|
||||
$y = $info.Trim()
|
||||
$n = $map[$y]
|
||||
if ($n) {
|
||||
return @{ Generator = "Visual Studio $n $y"; InstallPath = $path.Trim(); Source = 'vswhere' }
|
||||
$gen = Resolve-VsGeneratorFromLabel $info
|
||||
if ($gen) {
|
||||
return @{ Generator = $gen; InstallPath = $path.Trim(); Source = 'vswhere' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Scan filesystem (handles broken vswhere registration after winget cycles) ---
|
||||
$roots = @($env:ProgramFiles, ${env:ProgramFiles(x86)})
|
||||
$editions = @('BuildTools', 'Community', 'Professional', 'Enterprise')
|
||||
$years = @('2022', '2019', '2017')
|
||||
# filesystem scan (handles broken vswhere registration); VS 2026+ dir is "18"
|
||||
$roots = @($env:ProgramFiles, ${env:ProgramFiles(x86)}) | Where-Object { $_ }
|
||||
$knownEditions = @('BuildTools', 'Community', 'Professional', 'Enterprise', 'Preview')
|
||||
$dirs = @('18', '2026', '2022', '2019', '2017')
|
||||
|
||||
foreach ($y in $years) {
|
||||
foreach ($d in $dirs) {
|
||||
$gen = Resolve-VsGeneratorFromLabel $d
|
||||
if (-not $gen) { continue }
|
||||
foreach ($r in $roots) {
|
||||
foreach ($ed in $editions) {
|
||||
$candidate = Join-Path $r "Microsoft Visual Studio\$y\$ed"
|
||||
if (Test-Path $candidate) {
|
||||
$vcDir = Join-Path $candidate "VC\Tools\MSVC"
|
||||
if (Test-Path $vcDir) {
|
||||
$cl = Get-ChildItem -Path $vcDir -Filter "cl.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($cl) {
|
||||
$n = $map[$y]
|
||||
if ($n) {
|
||||
return @{ Generator = "Visual Studio $n $y"; InstallPath = $candidate; Source = "filesystem ($ed)"; ClExe = $cl.FullName }
|
||||
}
|
||||
}
|
||||
$vsBase = Join-Path $r "Microsoft Visual Studio\$d"
|
||||
if (-not (Test-Path $vsBase)) { continue }
|
||||
# VS 2026 (dir "18") may use non-standard edition names, so scan every subdir
|
||||
if ($d -eq '18' -or $d -eq '2026') {
|
||||
$editionCandidates = Get-ChildItem -Path $vsBase -Directory -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
|
||||
} else {
|
||||
$editionCandidates = $knownEditions | ForEach-Object { Join-Path $vsBase $_ }
|
||||
}
|
||||
foreach ($candidate in $editionCandidates) {
|
||||
if (-not (Test-Path $candidate)) { continue }
|
||||
$vcDir = Join-Path $candidate "VC\Tools\MSVC"
|
||||
if (Test-Path $vcDir) {
|
||||
$cl = Get-ChildItem -Path $vcDir -Filter "cl.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($cl) {
|
||||
$ed = Split-Path $candidate -Leaf
|
||||
return @{ Generator = $gen; InstallPath = $candidate; Source = "filesystem ($ed)"; ClExe = $cl.FullName }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -438,6 +592,103 @@ function Find-VsBuildTools {
|
|||
return $null
|
||||
}
|
||||
|
||||
# Install CMake + VS Build Tools, deferred here from Phase 1 so the prebuilt path
|
||||
# never pays for a multi-GB install. Called only when a source build is committed.
|
||||
# CMake is best-effort (build skips downstream if absent); VS Build Tools are
|
||||
# required, so exit 1 with guidance if missing. No-ops for VS when already detected.
|
||||
function Ensure-BuildToolsForLlamaSourceBuild {
|
||||
# CMake
|
||||
if ($null -eq (Get-Command cmake -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "CMake not found -- installing via winget (needed for the llama.cpp source build)..." -ForegroundColor Yellow
|
||||
if ($null -ne (Get-Command winget -ErrorAction SilentlyContinue)) {
|
||||
try {
|
||||
Invoke-SetupCommand { winget install Kitware.CMake --source winget --accept-package-agreements --accept-source-agreements } | Out-Null
|
||||
Refresh-Environment
|
||||
} catch { }
|
||||
}
|
||||
# winget may install cmake but not put it on PATH yet; try the default dir
|
||||
if ($null -eq (Get-Command cmake -ErrorAction SilentlyContinue)) {
|
||||
$cmakeDefaults = @(
|
||||
"$env:ProgramFiles\CMake\bin",
|
||||
"${env:ProgramFiles(x86)}\CMake\bin",
|
||||
"$env:LOCALAPPDATA\CMake\bin"
|
||||
)
|
||||
foreach ($d in $cmakeDefaults) {
|
||||
if (Test-Path (Join-Path $d "cmake.exe")) {
|
||||
$env:Path = "$d;$env:Path"
|
||||
Add-ToUserPath -Directory $d -Position 'Prepend' | Out-Null
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($null -ne (Get-Command cmake -ErrorAction SilentlyContinue)) { step "cmake" "installed" }
|
||||
}
|
||||
|
||||
# VS Build Tools
|
||||
if ($script:VsInstallPath) { return } # already detected by the early probe
|
||||
$vsResult = Find-VsBuildTools
|
||||
if (-not $vsResult) {
|
||||
Write-Host "Visual Studio Build Tools not found -- installing via winget..." -ForegroundColor Yellow
|
||||
Write-Host " (Needed only for the llama.cpp source build; may take several minutes)" -ForegroundColor Gray
|
||||
if ($null -ne (Get-Command winget -ErrorAction SilentlyContinue)) {
|
||||
$prevEAPTemp = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
winget install Microsoft.VisualStudio.2022.BuildTools --source winget --accept-package-agreements --accept-source-agreements --override "--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --passive --wait"
|
||||
$ErrorActionPreference = $prevEAPTemp
|
||||
# Re-scan after install (don't trust vswhere catalog)
|
||||
$vsResult = Find-VsBuildTools
|
||||
}
|
||||
}
|
||||
if ($vsResult) {
|
||||
$script:CmakeGenerator = $vsResult.Generator
|
||||
$script:VsInstallPath = $vsResult.InstallPath
|
||||
step "vs" "$($vsResult.Generator) ($($vsResult.Source))"
|
||||
if ($vsResult.ClExe) { substep "cl.exe: $($vsResult.ClExe)" }
|
||||
} else {
|
||||
Write-Host "[ERROR] Visual Studio Build Tools are required for the llama.cpp source build but could not be found or installed." -ForegroundColor Red
|
||||
Write-Host " Manual install:" -ForegroundColor Red
|
||||
Write-Host ' 1. winget install Microsoft.VisualStudio.2022.BuildTools --source winget' -ForegroundColor Yellow
|
||||
Write-Host ' 2. Open Visual Studio Installer -> Modify -> check "Desktop development with C++"' -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Detect the VC++ 2015-2022 Redistributable that the prebuilt llama-server and
|
||||
# PyTorch need (they link VCRUNTIME140_1.dll etc., which the Universal CRT lacks).
|
||||
# Signal is System32\vcruntime140_1.dll (VS 2019+), registry as fallback.
|
||||
function Test-VCRedistInstalled {
|
||||
$sys = $env:SystemRoot
|
||||
if ($sys -and (Test-Path (Join-Path $sys 'System32\vcruntime140_1.dll'))) { return $true }
|
||||
foreach ($k in @(
|
||||
'HKLM:\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64',
|
||||
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\x64'
|
||||
)) {
|
||||
try {
|
||||
$r = Get-ItemProperty -Path $k -ErrorAction Stop
|
||||
if ($r.Installed -eq 1 -and [int]$r.Major -ge 14 -and [int]$r.Minor -ge 20) { return $true }
|
||||
} catch { }
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Install the VC++ 2015-2022 runtime if missing (non-fatal; usually a no-op).
|
||||
function Ensure-VCRedist {
|
||||
if (Test-VCRedistInstalled) { step "vcredist" "present"; return }
|
||||
Write-Host "Microsoft Visual C++ Redistributable (2015-2022) is missing; the prebuilt llama.cpp and PyTorch need it. Installing the runtime..." -ForegroundColor Yellow
|
||||
if ($null -ne (Get-Command winget -ErrorAction SilentlyContinue)) {
|
||||
try {
|
||||
Invoke-SetupCommand { winget install --id Microsoft.VCRedist.2015+.x64 --source winget --accept-package-agreements --accept-source-agreements } | Out-Null
|
||||
Refresh-Environment
|
||||
} catch { substep "VCRedist install failed: $($_.Exception.Message)" "Yellow" }
|
||||
}
|
||||
if (Test-VCRedistInstalled) { step "vcredist" "installed" }
|
||||
else {
|
||||
substep "Could not install the VC++ Redistributable automatically." "Yellow"
|
||||
substep "If llama-server or torch reports a missing VCRUNTIME140.dll, install:" "Yellow"
|
||||
substep "https://aka.ms/vs/17/release/vc_redist.x64.exe" "Yellow"
|
||||
}
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Output style (aligned with studio/setup.sh: step / substep)
|
||||
# ─────────────────────────────────────────────
|
||||
|
|
@ -1110,83 +1361,39 @@ if (-not $HasGit) {
|
|||
}
|
||||
|
||||
# ============================================
|
||||
# 1c. CMake (required for llama.cpp build)
|
||||
# 1b.5. Visual C++ Redistributable (runtime for the prebuilt llama.cpp + PyTorch)
|
||||
# ============================================
|
||||
# Runtime dep, not a build tool: the prebuilt llama-server and PyTorch load it.
|
||||
Ensure-VCRedist
|
||||
|
||||
# ============================================
|
||||
# 1c. CMake (only needed for a llama.cpp SOURCE build -- detection only)
|
||||
# ============================================
|
||||
# Detection only: the prebuilt path needs no compiler, so do not install or exit
|
||||
# here. Ensure-BuildToolsForLlamaSourceBuild installs CMake if a source build runs.
|
||||
$HasCmake = $null -ne (Get-Command cmake -ErrorAction SilentlyContinue)
|
||||
if (-not $HasCmake) {
|
||||
Write-Host "CMake not found -- installing via winget..." -ForegroundColor Yellow
|
||||
$HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue)
|
||||
if ($HasWinget) {
|
||||
try {
|
||||
Invoke-SetupCommand { winget install Kitware.CMake --source winget --accept-package-agreements --accept-source-agreements } | Out-Null
|
||||
Refresh-Environment
|
||||
$HasCmake = $null -ne (Get-Command cmake -ErrorAction SilentlyContinue)
|
||||
} catch { }
|
||||
}
|
||||
# winget may succeed but cmake isn't on PATH yet (MSI PATH changes need a
|
||||
# new shell). Try the default install location as a fallback.
|
||||
if (-not $HasCmake) {
|
||||
$cmakeDefaults = @(
|
||||
"$env:ProgramFiles\CMake\bin",
|
||||
"${env:ProgramFiles(x86)}\CMake\bin",
|
||||
"$env:LOCALAPPDATA\CMake\bin"
|
||||
)
|
||||
foreach ($d in $cmakeDefaults) {
|
||||
if (Test-Path (Join-Path $d "cmake.exe")) {
|
||||
$env:Path = "$d;$env:Path"
|
||||
# Persist to user PATH (Prepend so this cmake wins over older ones).
|
||||
Add-ToUserPath -Directory $d -Position 'Prepend' | Out-Null
|
||||
$HasCmake = $null -ne (Get-Command cmake -ErrorAction SilentlyContinue)
|
||||
if ($HasCmake) {
|
||||
Write-Host " Found cmake at $d (added to PATH)" -ForegroundColor Gray
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($HasCmake) {
|
||||
step "cmake" "installed"
|
||||
} else {
|
||||
Write-Host "[ERROR] CMake is required but could not be installed." -ForegroundColor Red
|
||||
Write-Host " Install CMake from https://cmake.org/download/ and re-run." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
if ($HasCmake) {
|
||||
step "cmake" "$(cmake --version | Select-Object -First 1)"
|
||||
} else {
|
||||
step "cmake" "not detected (only needed if a llama.cpp source build is required)" "Yellow"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# 1d. Visual Studio Build Tools (C++ compiler for llama.cpp)
|
||||
# 1d. Visual Studio Build Tools (only needed for a llama.cpp SOURCE build -- detection only)
|
||||
# ============================================
|
||||
# Detection only: detect VS for a possible source build, but never install or exit
|
||||
# here. Install is deferred to Ensure-BuildToolsForLlamaSourceBuild.
|
||||
$CmakeGenerator = $null
|
||||
$VsInstallPath = $null
|
||||
$vsResult = Find-VsBuildTools
|
||||
|
||||
if (-not $vsResult) {
|
||||
Write-Host "Visual Studio Build Tools not found -- installing via winget..." -ForegroundColor Yellow
|
||||
Write-Host " (This is a one-time install, may take several minutes)" -ForegroundColor Gray
|
||||
$HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue)
|
||||
if ($HasWinget) {
|
||||
$prevEAPTemp = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
winget install Microsoft.VisualStudio.2022.BuildTools --source winget --accept-package-agreements --accept-source-agreements --override "--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --passive --wait"
|
||||
$ErrorActionPreference = $prevEAPTemp
|
||||
# Re-scan after install (don't trust vswhere catalog)
|
||||
$vsResult = Find-VsBuildTools
|
||||
}
|
||||
}
|
||||
|
||||
if ($vsResult) {
|
||||
$CmakeGenerator = $vsResult.Generator
|
||||
$VsInstallPath = $vsResult.InstallPath
|
||||
step "vs" "$CmakeGenerator ($($vsResult.Source))"
|
||||
step "vs" "$CmakeGenerator ($($vsResult.Source)) (only used if a source build is needed)"
|
||||
if ($vsResult.ClExe) { substep "cl.exe: $($vsResult.ClExe)" }
|
||||
} else {
|
||||
Write-Host "[ERROR] Visual Studio Build Tools could not be found or installed." -ForegroundColor Red
|
||||
Write-Host " Manual install:" -ForegroundColor Red
|
||||
Write-Host ' 1. winget install Microsoft.VisualStudio.2022.BuildTools --source winget' -ForegroundColor Yellow
|
||||
Write-Host ' 2. Open Visual Studio Installer -> Modify -> check "Desktop development with C++"' -ForegroundColor Yellow
|
||||
exit 1
|
||||
step "vs" "not detected (only needed if a llama.cpp source build is required)" "Yellow"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -1429,7 +1636,7 @@ if (Add-ToUserPath -Directory $nvccBinDir -Position 'Prepend') {
|
|||
# the MSBuild .targets/.props files that let VS compile .cu files are missing.
|
||||
# cmake fails with "No CUDA toolset found". Fix: copy from CUDA extras dir.
|
||||
if ($VsInstallPath -and $CudaToolkitRoot) {
|
||||
$vsCustomizations = Join-Path $VsInstallPath "MSBuild\Microsoft\VC\v170\BuildCustomizations"
|
||||
$vsCustomizations = Get-VcBuildCustomizationsDir -VsInstallPath $VsInstallPath -Generator $CmakeGenerator
|
||||
$cudaExtras = Join-Path $CudaToolkitRoot "extras\visual_studio_integration\MSBuildExtensions"
|
||||
if ((Test-Path $cudaExtras) -and (Test-Path $vsCustomizations)) {
|
||||
$hasTargets = Get-ChildItem $vsCustomizations -Filter "CUDA *.targets" -ErrorAction SilentlyContinue
|
||||
|
|
@ -3070,6 +3277,17 @@ if (Test-Path -LiteralPath $LlamaServerBin) {
|
|||
}
|
||||
}
|
||||
|
||||
# Install build tools now (last resort) rather than eagerly in Phase 1, so the
|
||||
# prebuilt path stays fast. Same condition as the if/elseif chain below: a source
|
||||
# build runs only when needed and no usable binary is already present.
|
||||
$WillBuildLlamaFromSource = $NeedLlamaSourceBuild -and `
|
||||
-not ((Test-Path -LiteralPath $LlamaServerBin) -and -not $NeedRebuild -and $RequestedLlamaTag -ne "master")
|
||||
if ($WillBuildLlamaFromSource) {
|
||||
Ensure-BuildToolsForLlamaSourceBuild
|
||||
# refresh so the chain below sees a newly installed cmake
|
||||
$HasCmakeForBuild = $null -ne (Get-Command cmake -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
if (-not $NeedLlamaSourceBuild) {
|
||||
Write-Host ""
|
||||
step "llama.cpp" "prebuilt (validated)"
|
||||
|
|
@ -3092,10 +3310,56 @@ if (-not $NeedLlamaSourceBuild) {
|
|||
substep "Install CMake from https://cmake.org/download/ and re-run setup." "Yellow"
|
||||
$script:LlamaCppDegraded = $true
|
||||
} else {
|
||||
# A source build is committed here. The CUDA toolkit is only needed now, so
|
||||
# resolve (and winget-install if needed) it lazily, failing fast if no
|
||||
# driver-compatible toolkit exists. The prebuilt path never reaches this.
|
||||
# Finalize the VS generator (gate/fallback below) BEFORE Resolve-CudaToolkit,
|
||||
# which copies the CUDA .targets into the current generator's dir; a later swap
|
||||
# would strand them. The CMake 4.2 gate for VS 2026 is checked only here, in the
|
||||
# source-build path, so a VS 2026 + cmake < 4.2 host can still use the prebuilt. (#6473)
|
||||
if ($CmakeGenerator -match 'Visual Studio 18\b') {
|
||||
if (-not (Test-CmakeCanDriveGenerator -Generator $CmakeGenerator)) {
|
||||
$cmakeVerObj = Get-CmakeVersion
|
||||
$cmakeVerStr = if ($cmakeVerObj) { $cmakeVerObj.ToString() } else { '0.0' }
|
||||
substep "CMake $cmakeVerStr cannot drive the Visual Studio 2026 generator (need 4.2+ or a VS-bundled cmake) -- updating via winget..." "Yellow"
|
||||
if ($null -ne (Get-Command winget -ErrorAction SilentlyContinue)) {
|
||||
# upgrade first (fast if Kitware.CMake is already a winget app), then
|
||||
# prepend the default dir so the new cmake wins over an older one on PATH
|
||||
try {
|
||||
Invoke-SetupCommand { winget upgrade Kitware.CMake --source winget --accept-package-agreements --accept-source-agreements } | Out-Null
|
||||
Refresh-Environment
|
||||
} catch { substep "CMake winget upgrade failed: $($_.Exception.Message)" "Yellow" }
|
||||
Add-DefaultCmakeToPath | Out-Null
|
||||
# upgrade no-ops if the cmake came from Scoop/Chocolatey/VS, not the
|
||||
# Kitware winget package; install it so a 4.2+ cmake is available
|
||||
if (-not (Test-CmakeCanDriveGenerator -Generator $CmakeGenerator)) {
|
||||
try {
|
||||
Invoke-SetupCommand { winget install Kitware.CMake --source winget --accept-package-agreements --accept-source-agreements } | Out-Null
|
||||
Refresh-Environment
|
||||
} catch { substep "CMake winget install failed: $($_.Exception.Message)" "Yellow" }
|
||||
Add-DefaultCmakeToPath | Out-Null
|
||||
}
|
||||
}
|
||||
if (-not (Test-CmakeCanDriveGenerator -Generator $CmakeGenerator)) {
|
||||
# cmake still cannot drive VS 2026; before failing, fall back to an
|
||||
# older installed VS whose generator it can drive (e.g. VS 2022 + old
|
||||
# cmake on an offline box keeps building)
|
||||
$fallback = Get-FallbackVsGenerator
|
||||
if ($fallback) {
|
||||
substep "CMake cannot drive $CmakeGenerator; falling back to $($fallback.Generator)" "Yellow"
|
||||
$CmakeGenerator = $fallback.Generator
|
||||
$VsInstallPath = $fallback.InstallPath
|
||||
} else {
|
||||
Write-Host "[ERROR] CMake 4.2+ is required to build llama.cpp with the Visual Studio 2026 generator, and no older Visual Studio toolchain was found to fall back to." -ForegroundColor Red
|
||||
Write-Host " Upgrade CMake from https://cmake.org/download/ and re-run, or use a prebuilt llama.cpp bundle." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
substep "CMake can drive the $CmakeGenerator generator"
|
||||
}
|
||||
|
||||
# CUDA resolved here (fail fast if none), after the final VS generator so its
|
||||
# .targets land in the toolset cmake actually uses.
|
||||
if ($HasNvidiaSmi) { Resolve-CudaToolkit -RequireOrExit }
|
||||
|
||||
Write-Host ""
|
||||
if ($HasNvidiaSmi) {
|
||||
substep "building llama.cpp with CUDA support..."
|
||||
|
|
@ -3447,7 +3711,8 @@ if (-not $NeedLlamaSourceBuild) {
|
|||
Write-Host " Copy contents of:" -ForegroundColor Yellow
|
||||
Write-Host " <CUDA_PATH>\extras\visual_studio_integration\MSBuildExtensions" -ForegroundColor Yellow
|
||||
Write-Host " into:" -ForegroundColor Yellow
|
||||
Write-Host " <VS_PATH>\MSBuild\Microsoft\VC\v170\BuildCustomizations" -ForegroundColor Yellow
|
||||
$hintCustomizations = if ($VsInstallPath) { Get-VcBuildCustomizationsDir -VsInstallPath $VsInstallPath -Generator $CmakeGenerator } else { "<VS_PATH>\MSBuild\Microsoft\VC\v170\BuildCustomizations" }
|
||||
Write-Host " $hintCustomizations" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
55
tests/studio_setup_ps1/Get-FunctionSource.ps1
Normal file
55
tests/studio_setup_ps1/Get-FunctionSource.ps1
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Extracts a single `function NAME { ... }` block from a PowerShell script by
|
||||
brace-matching, WITHOUT executing the script.
|
||||
|
||||
.DESCRIPTION
|
||||
studio/setup.ps1 is a top-level executing installer (it runs install steps at
|
||||
load), so it cannot be dot-sourced directly in a test. This helper pulls just
|
||||
the requested function's source text out of the file so a test can dot-source
|
||||
ONLY that function.
|
||||
|
||||
Brace matching is naive (it counts '{' / '}' without a full tokenizer). It is
|
||||
safe for the pure helper functions targeted here because their bodies contain
|
||||
only balanced braces (e.g. `${env:ProgramFiles(x86)}` is self-balanced) and no
|
||||
here-strings/comments with stray unbalanced braces.
|
||||
|
||||
.EXAMPLE
|
||||
$src = Get-FunctionSource -Path studio/setup.ps1 -Name Get-VcBuildCustomizationsDir
|
||||
. ([scriptblock]::Create($src)) # defines the function in the current scope
|
||||
#>
|
||||
function Get-FunctionSource {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Path,
|
||||
[Parameter(Mandatory)][string]$Name
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return $null }
|
||||
$text = Get-Content -Raw -LiteralPath $Path
|
||||
if ([string]::IsNullOrEmpty($text)) { return $null }
|
||||
|
||||
# Match "function <Name>" at the start of a line (multiline, case-insensitive).
|
||||
$pattern = "(?im)^\s*function\s+$([regex]::Escape($Name))\b"
|
||||
$m = [regex]::Match($text, $pattern)
|
||||
if (-not $m.Success) { return $null }
|
||||
|
||||
# Locate the opening brace at/after the match.
|
||||
$braceStart = $text.IndexOf('{', $m.Index)
|
||||
if ($braceStart -lt 0) { return $null }
|
||||
|
||||
# Walk braces to the matching close.
|
||||
$depth = 0
|
||||
$end = -1
|
||||
for ($i = $braceStart; $i -lt $text.Length; $i++) {
|
||||
$c = $text[$i]
|
||||
if ($c -eq '{') { $depth++ }
|
||||
elseif ($c -eq '}') {
|
||||
$depth--
|
||||
if ($depth -eq 0) { $end = $i; break }
|
||||
}
|
||||
}
|
||||
if ($end -lt 0) { return $null }
|
||||
|
||||
return $text.Substring($m.Index, $end - $m.Index + 1)
|
||||
}
|
||||
368
tests/studio_setup_ps1/Studio.Setup.Vs2026.Tests.ps1
Normal file
368
tests/studio_setup_ps1/Studio.Setup.Vs2026.Tests.ps1
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
<#
|
||||
Pester v5 unit tests for the Visual Studio 2026 completion helpers in
|
||||
studio/setup.ps1:
|
||||
- Get-VcBuildCustomizationsDir : derive the VC MSBuild BuildCustomizations
|
||||
folder (v160 / v170 / v180) from the detected VS generator.
|
||||
- Test-CmakeSupportsGenerator : gate the "Visual Studio 18 2026" generator
|
||||
on CMake >= 4.2 (no-op for older VS generators).
|
||||
|
||||
Both are pure functions (no GPU, no Visual Studio, no CUDA, no network), so the
|
||||
suite runs on a stock windows-latest runner - and on any pwsh host.
|
||||
|
||||
The real functions are extracted from setup.ps1 and dot-sourced (the script is
|
||||
a top-level installer and cannot be loaded wholesale). Path resolution honors
|
||||
$env:SETUP_PS1_PATH (set by the PR-validate workflow) and falls back to the
|
||||
repo-relative path. If a target function cannot be found, the suite FAILS
|
||||
loudly rather than silently passing.
|
||||
#>
|
||||
|
||||
BeforeAll {
|
||||
. (Join-Path $PSScriptRoot 'Get-FunctionSource.ps1')
|
||||
|
||||
$candidates = @(
|
||||
$env:SETUP_PS1_PATH,
|
||||
(Join-Path $PSScriptRoot '..\..\studio\setup.ps1')
|
||||
) | Where-Object { $_ }
|
||||
$script:SetupPs1 = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
if (-not $script:SetupPs1) { throw "Could not locate studio/setup.ps1 (set SETUP_PS1_PATH)." }
|
||||
Write-Host "setup.ps1 under test: $script:SetupPs1"
|
||||
|
||||
foreach ($fn in @('Resolve-VsGeneratorFromLabel', 'Find-VsBuildTools', 'Get-VcBuildCustomizationsDir',
|
||||
'Test-CmakeSupportsGenerator', 'Get-CmakeVersion', 'Test-CmakeListsGenerator',
|
||||
'Test-CmakeCanDriveGenerator', 'Get-FallbackVsGenerator',
|
||||
'Ensure-BuildToolsForLlamaSourceBuild', 'Test-VCRedistInstalled')) {
|
||||
$src = Get-FunctionSource -Path $script:SetupPs1 -Name $fn
|
||||
if (-not $src) { throw "Function '$fn' not found in $script:SetupPs1 - cannot test the real code." }
|
||||
. ([scriptblock]::Create($src))
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Resolve-VsGeneratorFromLabel (vswhere/dir label -> generator)' {
|
||||
# Guards that detection accepts both '18' (the internal major vswhere reports
|
||||
# for VS 2026) and the year form.
|
||||
It 'maps the VS 2026 internal major "18" to the VS 2026 generator' {
|
||||
Resolve-VsGeneratorFromLabel '18' | Should -Be 'Visual Studio 18 2026'
|
||||
}
|
||||
It 'maps the VS 2026 year label "2026" to the VS 2026 generator' {
|
||||
Resolve-VsGeneratorFromLabel '2026' | Should -Be 'Visual Studio 18 2026'
|
||||
}
|
||||
It 'maps the VS 2022 year "2022" and major "17" to the VS 2022 generator' {
|
||||
Resolve-VsGeneratorFromLabel '2022' | Should -Be 'Visual Studio 17 2022'
|
||||
Resolve-VsGeneratorFromLabel '17' | Should -Be 'Visual Studio 17 2022'
|
||||
}
|
||||
It 'maps 2019/2017 (year and major) to their generators' {
|
||||
Resolve-VsGeneratorFromLabel '2019' | Should -Be 'Visual Studio 16 2019'
|
||||
Resolve-VsGeneratorFromLabel '16' | Should -Be 'Visual Studio 16 2019'
|
||||
Resolve-VsGeneratorFromLabel '2017' | Should -Be 'Visual Studio 15 2017'
|
||||
Resolve-VsGeneratorFromLabel '15' | Should -Be 'Visual Studio 15 2017'
|
||||
}
|
||||
It 'trims whitespace (vswhere output can carry a trailing newline)' {
|
||||
Resolve-VsGeneratorFromLabel " 18 `n" | Should -Be 'Visual Studio 18 2026'
|
||||
}
|
||||
It 'returns null for unknown or empty labels' {
|
||||
Resolve-VsGeneratorFromLabel '2015' | Should -BeNullOrEmpty
|
||||
Resolve-VsGeneratorFromLabel '' | Should -BeNullOrEmpty
|
||||
Resolve-VsGeneratorFromLabel $null | Should -BeNullOrEmpty
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Find-VsBuildTools (VS 2026 generator discovery)' {
|
||||
# Exercises the real discovery entry point. Windows-only: Find-VsBuildTools builds
|
||||
# backslash candidate paths that only resolve as directories on Windows.
|
||||
BeforeAll {
|
||||
# Define in BeforeAll, not the Describe body: Pester 5 runs the body only at
|
||||
# discovery, so body-level functions are not visible in the run-phase It blocks.
|
||||
function New-FakeVsTree {
|
||||
param([string]$Root, [string]$VersionDir, [string]$Edition = 'BuildTools')
|
||||
$clDir = Join-Path $Root "Microsoft Visual Studio\$VersionDir\$Edition\VC\Tools\MSVC\14.50.00000\bin\Hostx64\x64"
|
||||
New-Item -ItemType Directory -Path $clDir -Force | Out-Null
|
||||
New-Item -ItemType File -Path (Join-Path $clDir 'cl.exe') -Force | Out-Null
|
||||
}
|
||||
}
|
||||
BeforeEach {
|
||||
$script:OrigPF = ${env:ProgramFiles}
|
||||
$script:OrigPFx86 = ${env:ProgramFiles(x86)}
|
||||
}
|
||||
AfterEach {
|
||||
${env:ProgramFiles} = $script:OrigPF
|
||||
${env:ProgramFiles(x86)} = $script:OrigPFx86
|
||||
}
|
||||
|
||||
It 'detects a filesystem-only VS 2026 BuildTools install (dir "18")' -Skip:(-not $IsWindows) {
|
||||
$root = Join-Path $TestDrive 'PF'
|
||||
New-FakeVsTree -Root $root -VersionDir '18'
|
||||
${env:ProgramFiles} = $root
|
||||
${env:ProgramFiles(x86)} = Join-Path $TestDrive 'PFx86' # no vswhere here -> filesystem fallback
|
||||
$r = Find-VsBuildTools
|
||||
$r.Generator | Should -Be 'Visual Studio 18 2026'
|
||||
}
|
||||
|
||||
It 'detects a filesystem-only VS 2026 install under the year dir ("2026")' -Skip:(-not $IsWindows) {
|
||||
$root = Join-Path $TestDrive 'PF2026'
|
||||
New-FakeVsTree -Root $root -VersionDir '2026'
|
||||
${env:ProgramFiles} = $root
|
||||
${env:ProgramFiles(x86)} = Join-Path $TestDrive 'PFx86b'
|
||||
(Find-VsBuildTools).Generator | Should -Be 'Visual Studio 18 2026'
|
||||
}
|
||||
|
||||
It 'still detects VS 2022 (no regression)' -Skip:(-not $IsWindows) {
|
||||
$root = Join-Path $TestDrive 'PF2022'
|
||||
New-FakeVsTree -Root $root -VersionDir '2022'
|
||||
${env:ProgramFiles} = $root
|
||||
${env:ProgramFiles(x86)} = Join-Path $TestDrive 'PFx86c'
|
||||
(Find-VsBuildTools).Generator | Should -Be 'Visual Studio 17 2022'
|
||||
}
|
||||
|
||||
It 'detects an older VS installed under the Preview edition dir' -Skip:(-not $IsWindows) {
|
||||
# Preview installs under a "Preview" edition folder; the fallback must include it.
|
||||
$root = Join-Path $TestDrive 'PF2022prev'
|
||||
New-FakeVsTree -Root $root -VersionDir '2022' -Edition 'Preview'
|
||||
${env:ProgramFiles} = $root
|
||||
${env:ProgramFiles(x86)} = Join-Path $TestDrive 'PFx86d'
|
||||
(Find-VsBuildTools).Generator | Should -Be 'Visual Studio 17 2022'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Get-VcBuildCustomizationsDir (CUDA to VS MSBuild integration path)' {
|
||||
# Use TestDrive as the root so Join-Path resolves on any OS; assertions accept
|
||||
# either path separator.
|
||||
|
||||
It 'derives v180 for the VS 2026 generator' {
|
||||
Get-VcBuildCustomizationsDir -VsInstallPath "$TestDrive" -Generator 'Visual Studio 18 2026' |
|
||||
Should -Match 'VC[\\/]v180[\\/]BuildCustomizations$'
|
||||
}
|
||||
|
||||
It 'derives v170 for VS 2022 (unchanged behavior)' {
|
||||
Get-VcBuildCustomizationsDir -VsInstallPath "$TestDrive" -Generator 'Visual Studio 17 2022' |
|
||||
Should -Match 'VC[\\/]v170[\\/]BuildCustomizations$'
|
||||
}
|
||||
|
||||
It 'derives v160 for VS 2019' {
|
||||
Get-VcBuildCustomizationsDir -VsInstallPath "$TestDrive" -Generator 'Visual Studio 16 2019' |
|
||||
Should -Match 'VC[\\/]v160[\\/]BuildCustomizations$'
|
||||
}
|
||||
|
||||
It 'falls back to v170 when the generator is empty/unparseable (backwards compatible)' {
|
||||
Get-VcBuildCustomizationsDir -VsInstallPath "$TestDrive" -Generator '' |
|
||||
Should -Match 'VC[\\/]v170[\\/]BuildCustomizations$'
|
||||
}
|
||||
|
||||
It 'roots the path under the supplied VS install path' {
|
||||
$p = Get-VcBuildCustomizationsDir -VsInstallPath "$TestDrive" -Generator 'Visual Studio 18 2026'
|
||||
$p.StartsWith("$TestDrive") | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Test-CmakeSupportsGenerator (CMake 4.2 guard for VS 2026)' {
|
||||
|
||||
It 'rejects CMake 3.31.0 with the VS 2026 generator' {
|
||||
Test-CmakeSupportsGenerator -CmakeVersion '3.31.0' -Generator 'Visual Studio 18 2026' | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'accepts CMake 4.2.1 with the VS 2026 generator' {
|
||||
Test-CmakeSupportsGenerator -CmakeVersion '4.2.1' -Generator 'Visual Studio 18 2026' | Should -BeTrue
|
||||
}
|
||||
|
||||
It 'accepts CMake exactly 4.2 with the VS 2026 generator (boundary)' {
|
||||
Test-CmakeSupportsGenerator -CmakeVersion '4.2' -Generator 'Visual Studio 18 2026' | Should -BeTrue
|
||||
}
|
||||
|
||||
It 'rejects CMake 4.1.0 with the VS 2026 generator (boundary)' {
|
||||
Test-CmakeSupportsGenerator -CmakeVersion '4.1.0' -Generator 'Visual Studio 18 2026' | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'is a no-op (accepts any CMake) for the VS 2022 generator' {
|
||||
Test-CmakeSupportsGenerator -CmakeVersion '3.20.0' -Generator 'Visual Studio 17 2022' | Should -BeTrue
|
||||
}
|
||||
|
||||
It 'is a no-op (accepts any CMake) for the VS 2019 generator' {
|
||||
Test-CmakeSupportsGenerator -CmakeVersion '3.10.0' -Generator 'Visual Studio 16 2019' | Should -BeTrue
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Test-CmakeListsGenerator (probe cmake --help)' {
|
||||
# Mock cmake as a function (resolved before any on-PATH exe): PowerShell caches
|
||||
# its app-path table, so a $env:Path shim would not reliably beat a real cmake.
|
||||
|
||||
It 'returns true when cmake --help lists the generator' {
|
||||
Mock cmake { "Generators`n Visual Studio 18 2026 = Generates VS 2026 project files.`n Visual Studio 17 2022 = Generates VS 2022 project files." }
|
||||
Test-CmakeListsGenerator -Generator 'Visual Studio 18 2026' | Should -BeTrue
|
||||
}
|
||||
|
||||
It 'returns false when cmake --help does not list the generator' {
|
||||
Mock cmake { "Generators`n Visual Studio 17 2022 = Generates VS 2022 project files." }
|
||||
Test-CmakeListsGenerator -Generator 'Visual Studio 18 2026' | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'returns false when cmake produces no help output' {
|
||||
Mock cmake { $null }
|
||||
Test-CmakeListsGenerator -Generator 'Visual Studio 18 2026' | Should -BeFalse
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Test-CmakeCanDriveGenerator (probe OR version floor)' {
|
||||
It 'accepts a sub-4.2 cmake that lists the VS 2026 generator (bundled cmake)' {
|
||||
# 3.31.0 is below the 4.2 floor but lists the generator, so the help-probe accepts it.
|
||||
Mock cmake {
|
||||
if ($args -contains '--version') { 'cmake version 3.31.0' }
|
||||
else { "Generators`n Visual Studio 18 2026 = Generates VS 2026 project files." }
|
||||
}
|
||||
Test-CmakeCanDriveGenerator -Generator 'Visual Studio 18 2026' | Should -BeTrue
|
||||
}
|
||||
|
||||
It 'accepts a 4.2 cmake via the version floor when the help probe misses it' {
|
||||
# Help omits the generator but 4.2.0 meets the floor, so the version branch accepts it.
|
||||
Mock cmake {
|
||||
if ($args -contains '--version') { 'cmake version 4.2.0' }
|
||||
else { 'Generators' }
|
||||
}
|
||||
Test-CmakeCanDriveGenerator -Generator 'Visual Studio 18 2026' | Should -BeTrue
|
||||
}
|
||||
|
||||
It 'rejects a sub-4.2 cmake that does not list the VS 2026 generator' {
|
||||
Mock cmake {
|
||||
if ($args -contains '--version') { 'cmake version 3.31.0' }
|
||||
else { "Generators`n Visual Studio 17 2022 = Generates VS 2022 project files." }
|
||||
}
|
||||
Test-CmakeCanDriveGenerator -Generator 'Visual Studio 18 2026' | Should -BeFalse
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Get-FallbackVsGenerator (older VS the cmake can drive)' {
|
||||
BeforeAll {
|
||||
function New-FakeVsTree2 {
|
||||
param([string]$Root, [string]$VersionDir, [string]$Edition = 'BuildTools')
|
||||
$clDir = Join-Path $Root "Microsoft Visual Studio\$VersionDir\$Edition\VC\Tools\MSVC\14.39.00000\bin\Hostx64\x64"
|
||||
New-Item -ItemType Directory -Path $clDir -Force | Out-Null
|
||||
New-Item -ItemType File -Path (Join-Path $clDir 'cl.exe') -Force | Out-Null
|
||||
}
|
||||
}
|
||||
BeforeEach {
|
||||
$script:OrigPF = ${env:ProgramFiles}
|
||||
$script:OrigPFx86 = ${env:ProgramFiles(x86)}
|
||||
}
|
||||
AfterEach {
|
||||
${env:ProgramFiles} = $script:OrigPF
|
||||
${env:ProgramFiles(x86)} = $script:OrigPFx86
|
||||
}
|
||||
|
||||
It 'returns the VS 2022 generator when VS 2022 is installed and cmake lists it' -Skip:(-not $IsWindows) {
|
||||
$root = Join-Path $TestDrive 'PF_fb'
|
||||
New-FakeVsTree2 -Root $root -VersionDir '2022'
|
||||
${env:ProgramFiles} = $root
|
||||
${env:ProgramFiles(x86)} = Join-Path $TestDrive 'PFx86_fb'
|
||||
Mock cmake { "Generators`n Visual Studio 17 2022 = Generates VS 2022 project files." }
|
||||
$r = Get-FallbackVsGenerator
|
||||
$r.Generator | Should -Be 'Visual Studio 17 2022'
|
||||
}
|
||||
|
||||
It 'returns null when the cmake cannot drive any installed older VS' -Skip:(-not $IsWindows) {
|
||||
$root = Join-Path $TestDrive 'PF_none'
|
||||
New-FakeVsTree2 -Root $root -VersionDir '2022'
|
||||
${env:ProgramFiles} = $root
|
||||
${env:ProgramFiles(x86)} = Join-Path $TestDrive 'PFx86_none'
|
||||
# cmake lists only VS 2026 (not 2022/2019/2017), so no older fallback is usable.
|
||||
Mock cmake { "Generators`n Visual Studio 18 2026 = Generates VS 2026 project files." }
|
||||
$r = Get-FallbackVsGenerator
|
||||
$r | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'falls back to an older VS installed under the Preview edition dir' -Skip:(-not $IsWindows) {
|
||||
$root = Join-Path $TestDrive 'PF_prev'
|
||||
New-FakeVsTree2 -Root $root -VersionDir '2022' -Edition 'Preview'
|
||||
${env:ProgramFiles} = $root
|
||||
${env:ProgramFiles(x86)} = Join-Path $TestDrive 'PFx86_prev'
|
||||
Mock cmake { "Generators`n Visual Studio 17 2022 = Generates VS 2022 project files." }
|
||||
(Get-FallbackVsGenerator).Generator | Should -Be 'Visual Studio 17 2022'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Deferred build tools (prebuilt path needs no VS/CMake)' {
|
||||
# Phase-1 detection must be non-fatal (prebuilt path never blocked) and the
|
||||
# deferred installer must no-op when VS was already detected. The install +
|
||||
# exit-1 path is covered by studio-windows-no-vs-smoke.yml.
|
||||
BeforeEach {
|
||||
$script:OrigPF = ${env:ProgramFiles}
|
||||
$script:OrigPFx86 = ${env:ProgramFiles(x86)}
|
||||
}
|
||||
AfterEach {
|
||||
${env:ProgramFiles} = $script:OrigPF
|
||||
${env:ProgramFiles(x86)} = $script:OrigPFx86
|
||||
$script:VsInstallPath = $null
|
||||
$script:CmakeGenerator = $null
|
||||
}
|
||||
|
||||
It 'Find-VsBuildTools returns null when no VS is present (probe stays non-fatal)' {
|
||||
# Empty discovery roots so no VS is found; the probe must return null
|
||||
# (then log and continue, never exit).
|
||||
${env:ProgramFiles} = (Join-Path $TestDrive 'EmptyPF')
|
||||
${env:ProgramFiles(x86)} = (Join-Path $TestDrive 'EmptyPFx86')
|
||||
New-Item -ItemType Directory -Force -Path ${env:ProgramFiles}, ${env:ProgramFiles(x86)} | Out-Null
|
||||
Find-VsBuildTools | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'Ensure-BuildToolsForLlamaSourceBuild no-ops when VS is already detected' {
|
||||
# With $VsInstallPath already set, the deferred installer must return without
|
||||
# re-scanning or installing.
|
||||
$script:VsInstallPath = 'C:\Program Files\Microsoft Visual Studio\2022\BuildTools'
|
||||
$script:CmakeGenerator = 'Visual Studio 17 2022'
|
||||
{ Ensure-BuildToolsForLlamaSourceBuild } | Should -Not -Throw
|
||||
$script:VsInstallPath | Should -Be 'C:\Program Files\Microsoft Visual Studio\2022\BuildTools'
|
||||
$script:CmakeGenerator | Should -Be 'Visual Studio 17 2022'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Source-build ordering invariant: CUDA integration runs AFTER the VS generator is finalized (#6473 review)' {
|
||||
# Resolve-CudaToolkit copies the CUDA .targets into the current generator's dir,
|
||||
# so it must run after the VS 2026 gate/fallback; otherwise a fallback to VS 2022
|
||||
# builds v170 while the .targets went to v180 ("No CUDA toolset found").
|
||||
It 'the source-build Resolve-CudaToolkit call appears AFTER the Get-FallbackVsGenerator fallback' {
|
||||
$text = Get-Content -Raw -LiteralPath $script:SetupPs1
|
||||
$idxFallback = $text.IndexOf('$fallback = Get-FallbackVsGenerator')
|
||||
$idxResolve = $text.IndexOf('Resolve-CudaToolkit -RequireOrExit')
|
||||
$idxFallback | Should -BeGreaterThan 0
|
||||
$idxResolve | Should -BeGreaterThan 0
|
||||
$idxResolve | Should -BeGreaterThan $idxFallback
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Get-FallbackVsGenerator discovery is symmetric with Find-VsBuildTools (#6473 review)' {
|
||||
# The fallback must also query vswhere, else a VS in a custom location is found
|
||||
# as primary but missed as fallback -> avoidable hard exit.
|
||||
It 'queries vswhere as part of fallback discovery' {
|
||||
$src = Get-FunctionSource -Path $script:SetupPs1 -Name Get-FallbackVsGenerator
|
||||
$src | Should -Match 'vswhere'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Test-VCRedistInstalled (VC++ 2015-2022 runtime needed by the prebuilt llama.cpp + PyTorch)' {
|
||||
# The prebuilts link the VC++ runtime DLLs (which the Universal CRT lacks);
|
||||
# detection is System32\vcruntime140_1.dll with a registry fallback.
|
||||
BeforeEach { $script:OrigSysRoot = $env:SystemRoot }
|
||||
AfterEach { $env:SystemRoot = $script:OrigSysRoot }
|
||||
|
||||
# Probes Test-Path once (System32 DLL), then the registry; mock both.
|
||||
It 'returns true when vcruntime140_1.dll is present in System32' {
|
||||
$env:SystemRoot = 'C:\Windows'
|
||||
Mock Test-Path { $true }
|
||||
Test-VCRedistInstalled | Should -BeTrue
|
||||
}
|
||||
It 'returns true via the registry when the DLL is not found (Installed=1, >= 14.20)' {
|
||||
$env:SystemRoot = 'C:\Windows'
|
||||
Mock Test-Path { $false }
|
||||
Mock Get-ItemProperty { [pscustomobject]@{ Installed = 1; Major = 14; Minor = 29 } }
|
||||
Test-VCRedistInstalled | Should -BeTrue
|
||||
}
|
||||
It 'returns false when neither the DLL nor a >= 14.20 registry entry exists' {
|
||||
$env:SystemRoot = 'C:\Windows'
|
||||
Mock Test-Path { $false }
|
||||
Mock Get-ItemProperty { throw 'no key' }
|
||||
Test-VCRedistInstalled | Should -BeFalse
|
||||
}
|
||||
It 'returns false for an old 2015-only redist (Installed=1 but < 14.20)' {
|
||||
$env:SystemRoot = 'C:\Windows'
|
||||
Mock Test-Path { $false }
|
||||
Mock Get-ItemProperty { [pscustomobject]@{ Installed = 1; Major = 14; Minor = 0 } }
|
||||
Test-VCRedistInstalled | Should -BeFalse
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue