diff --git a/AUTHORING_EXTRACTOR_SKILL.md b/AUTHORING_EXTRACTOR_SKILL.md index d7bcc37..70a8ff7 100644 --- a/AUTHORING_EXTRACTOR_SKILL.md +++ b/AUTHORING_EXTRACTOR_SKILL.md @@ -88,7 +88,7 @@ Capture as prerequisites or notes in the skill body: - Restore first if the project's targets come from a package: `msbuild /t:restore`. - Omit `--validate` for IntelliSense setup; it recompiles every file and is slow. - Always regenerate fresh; never reuse a stale `compile_commands.json`. -- Pin the extractor version and verify its SHA256 (releases ship a self-contained single-file exe). +- Use the extractor installed by the plugin's `sessionStart` hook, not a separate download. ## Example: Windows driver samples with the EWDK @@ -132,6 +132,7 @@ activated EWDK shell and let the extractor read the toolchain from the environme ## Prerequisites +- .NET 10 SDK and the extractor installed by the plugin's `sessionStart` hook. - Mount the EWDK ISO (for example as `D:`) and start its shell: `D:\LaunchBuildEnv.cmd`. - Confirm the toolchain is active: `where cl.exe` resolves under the mounted EWDK. @@ -155,15 +156,16 @@ activated toolchain from the environment with `useDevEnv`: From the activated EWDK shell, in the repo root: ```powershell -.tools\msbuild-extractor-sample.exe # uses ./msbuild-extractor.json +$extractor = (Get-ChildItem "$env:LOCALAPPDATA\mscppls\msbuild-extractor\public\Microsoft.VisualStudio.Cpp.MSBuildExtractor.*\tools\msbuild-extractor-sample.exe" | Select-Object -First 1).FullName +& $extractor # uses ./msbuild-extractor.json # or, if the config lives elsewhere: -.tools\msbuild-extractor-sample.exe --config path\to\msbuild-extractor.json +& $extractor --config path\to\msbuild-extractor.json ``` Equivalent one-off invocation with explicit flags (no committed config): ```powershell -.tools\msbuild-extractor-sample.exe ` +& $extractor ` --use-dev-env ` --solution general\echo\kmdf\kmdfecho.sln ` -c Debug -a x64 ` @@ -194,7 +196,7 @@ toolchain. As a committed config (`msbuild-extractor.json`): Or the equivalent one-off invocation: ```powershell -.tools\msbuild-extractor-sample.exe ` +& $extractor ` --msbuild-path "D:\Program Files\Microsoft Visual Studio\18\BuildTools\MSBuild\Current\Bin\MSBuild.exe" ` --solution general\echo\kmdf\kmdfecho.sln ` -c Debug -a x64 ` diff --git a/plugins/cpp-language-server/hooks/hooks.json b/plugins/cpp-language-server/hooks/hooks.json new file mode 100644 index 0000000..5d588f2 --- /dev/null +++ b/plugins/cpp-language-server/hooks/hooks.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "hooks": { + "sessionStart": [ + { + "type": "command", + "bash": ":", + "powershell": "if (${env:COPILOT_PLUGIN_ROOT}) { $ps = (Get-Command pwsh -ErrorAction SilentlyContinue).Source; if (-not $ps) { $ps = 'powershell' }; & $ps -NoProfile -ExecutionPolicy Bypass -NonInteractive -File \"${env:COPILOT_PLUGIN_ROOT}\\scripts\\install-msbuild-extractor.ps1\" }", + "timeoutSec": 120 + } + ] + } +} diff --git a/plugins/cpp-language-server/scripts/install-msbuild-extractor.ps1 b/plugins/cpp-language-server/scripts/install-msbuild-extractor.ps1 new file mode 100644 index 0000000..b419d6b --- /dev/null +++ b/plugins/cpp-language-server/scripts/install-msbuild-extractor.ps1 @@ -0,0 +1,137 @@ +<# +.SYNOPSIS + Installs the pinned MSBuild extractor from the public cpp_PublicPackages feed. +#> +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$packageId = 'Microsoft.VisualStudio.Cpp.MSBuildExtractor' +$version = '1.0.2609.39' +$nupkgSha256 = '9670F13F974FEBD360D7B8F747CF025E1E1CC88394419234ECD9590E7B1805AE' # pragma: allowlist secret + +$installDir = Join-Path $env:LOCALAPPDATA "mscppls\msbuild-extractor\public\$packageId.$version" +$exePath = Join-Path $installDir 'tools\msbuild-extractor-sample.exe' +$feedBase = 'https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/nuget/v3/flat2' + +function Get-Sha256 ([string]$Path) { + $sha = [Security.Cryptography.SHA256]::Create() + try { + $stream = [IO.File]::OpenRead($Path) + try { [BitConverter]::ToString($sha.ComputeHash($stream)).Replace('-', '') } + finally { $stream.Dispose() } + } + finally { $sha.Dispose() } +} + +function Test-Installed ([string]$Root) { + Test-Path -LiteralPath (Join-Path $Root 'tools\msbuild-extractor-sample.exe') -PathType Leaf +} + +function Write-Ready { + Write-Host "msbuild-extractor $version ready: $exePath" + + $dotnetRoot = if ($env:DOTNET_ROOT) { $env:DOTNET_ROOT } else { Join-Path $env:ProgramFiles 'dotnet' } + $sharedDir = Join-Path $dotnetRoot 'shared\Microsoft.NETCore.App' + $hasNet10 = (Test-Path -LiteralPath $sharedDir) -and + @(Get-ChildItem -LiteralPath $sharedDir -Directory -Filter '10.*' -ErrorAction SilentlyContinue).Count -gt 0 + if (-not $hasNet10) { + Write-Warning "A .NET 10 runtime was not detected. Install the .NET 10 SDK from https://dotnet.microsoft.com/download/dotnet/10.0." + } +} + +# Installed copies need no package download or hash check. +if (Test-Installed $installDir) { + Write-Ready + return +} + +$cacheRoot = Split-Path -Parent $installDir +if (Test-Path -LiteralPath $cacheRoot) { + Get-ChildItem -LiteralPath $cacheRoot -Directory -Filter "$packageId.*" -ErrorAction SilentlyContinue | + Where-Object { $_.Name -ne "$packageId.$version" } | + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + Get-ChildItem -LiteralPath $cacheRoot -Directory -Filter '_staging.*' -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -lt (Get-Date).AddHours(-1) } | + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue +} + +$lowerId = $packageId.ToLowerInvariant() +$nupkgUrl = "$feedBase/$lowerId/$version/$lowerId.$version.nupkg" +$nupkgTmp = Join-Path ([IO.Path]::GetTempPath()) "$lowerId.$version.$PID.nupkg" +$stageDir = Join-Path $cacheRoot "_staging.$PID" +$retired = Join-Path $cacheRoot "_staging.retired.$PID" + +# Serialize concurrent session starts; publish the staged directory by rename. +$mutex = $null +try { $mutex = New-Object Threading.Mutex($false, 'Global\mscppls-msbuild-extractor-public-install') } +catch [UnauthorizedAccessException] { $mutex = New-Object Threading.Mutex($false, 'Local\mscppls-msbuild-extractor-public-install') } +$held = $false + +try { + try { $held = $mutex.WaitOne([TimeSpan]::FromSeconds(100)) } + catch [Threading.AbandonedMutexException] { $held = $true } + + if (Test-Installed $installDir) { + Write-Ready + return + } + if (-not $held) { + throw "Timed out waiting for another session to finish installing." + } + + # Windows PowerShell 5.1 needs the ZIP assembly and TLS 1.2 enabled explicitly. + Add-Type -AssemblyName System.IO.Compression.FileSystem + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.ServicePointManager]::SecurityProtocol + + Write-Host "Downloading $packageId $version from cpp_PublicPackages..." + $progress = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + try { + Invoke-WebRequest -Uri $nupkgUrl -OutFile $nupkgTmp -UseBasicParsing + } + finally { + $ProgressPreference = $progress + } + + $actual = Get-Sha256 $nupkgTmp + if ($actual -ne $nupkgSha256) { + throw "SHA256 mismatch for $lowerId.$version.nupkg. Expected $nupkgSha256, got $actual." + } + + if (Test-Path -LiteralPath $stageDir) { + Remove-Item -LiteralPath $stageDir -Recurse -Force + } + [IO.Compression.ZipFile]::ExtractToDirectory($nupkgTmp, $stageDir) + + if (-not (Test-Installed $stageDir)) { + throw "$lowerId.$version.nupkg did not contain tools\msbuild-extractor-sample.exe." + } + + if (Test-Path -LiteralPath $installDir) { + [IO.Directory]::Move($installDir, $retired) + } + [IO.Directory]::Move($stageDir, $installDir) + + if (-not (Test-Installed $installDir)) { + throw "msbuild-extractor-sample.exe is missing after publishing to $installDir." + } +} +catch { + if ((Test-Path -LiteralPath $installDir) -and -not (Test-Installed $installDir)) { + Remove-Item -LiteralPath $installDir -Recurse -Force -ErrorAction SilentlyContinue + } + Write-Error "Failed to install $packageId $version from cpp_PublicPackages. $($_.Exception.Message)" + exit 1 +} +finally { + Remove-Item -LiteralPath $nupkgTmp -Force -ErrorAction SilentlyContinue + foreach ($d in $stageDir, $retired) { + if (Test-Path -LiteralPath $d) { + Remove-Item -LiteralPath $d -Recurse -Force -ErrorAction SilentlyContinue + } + } + if ($held) { $mutex.ReleaseMutex() } + $mutex.Dispose() +} + +Write-Ready diff --git a/plugins/cpp-language-server/skills/generate-compile-commands/SKILL.md b/plugins/cpp-language-server/skills/generate-compile-commands/SKILL.md index d646c51..f3bb1f9 100644 --- a/plugins/cpp-language-server/skills/generate-compile-commands/SKILL.md +++ b/plugins/cpp-language-server/skills/generate-compile-commands/SKILL.md @@ -49,6 +49,8 @@ For MSBuild (`.sln`, `.slnx`, `.vcxproj`), the **only** correct way to produce ` - **Do not invoke `cl.exe`, `clang`, or `clang-cl` directly** to build a compile database, and do not scrape a build log. The extractor does this correctly via design-time evaluation. - **Do not compile the project first.** Extraction is design-time: it evaluates projects and runs `GetClCommandLines`; it does **not** compile anything and does not need build outputs to exist. +The plugin's `sessionStart` hook installs the extractor on Windows. Running it requires the .NET 10 SDK. + ### Run it directly in one step The extractor is fast and design-time, so a **single invocation** is expected. Do not explore the repository extensively, run a test build, or re-run the extractor to "confirm" the result. Locate the project file, resolve the configuration once (tiered procedure below), run the command once, and stop. One retry is allowed only after a nonzero extractor exit: if it failed because restore/init-generated props, targets, or package files are missing (common in C++/WinRT and restore-heavy repos), run the repo's documented restore or init once (e.g. `nuget restore`; not a test build), then retry extraction once. After a successful extraction with output present, stop. @@ -64,11 +66,12 @@ The correct configuration and platform are the **project's own default**, never #### Tier 1: a committed `msbuild-extractor.json` exists (primary, correct by construction) -A committed `msbuild-extractor.json` next to the solution already declares the intended `configuration`, `platform`, and inputs, and reproduces identically on any machine and installed platform set. Pass its path **explicitly** with `--config`, and pass the declared `solutions` value as `--solution`; that plus `-o` is the whole command. Do **not** pass `-c`/`-a` (they would override the committed configuration and platform), and do **not** rely on bare auto-discovery: the v0.3.0 extractor auto-discovers `msbuild-extractor.json` only from the current working directory, so running from any other directory silently falls back to `Debug`/`x64` and produces a wrong-platform database. Passing `--config` is robust to the working directory. +A committed `msbuild-extractor.json` next to the solution already declares the intended `configuration`, `platform`, and inputs, and reproduces identically on any machine and installed platform set. Pass its path **explicitly** with `--config`, and pass the declared `solutions` value as `--solution`; that plus `-o` is the whole command. Do **not** pass `-c`/`-a` (they would override the committed configuration and platform), and do **not** rely on bare auto-discovery: the extractor auto-discovers `msbuild-extractor.json` only from the current working directory, so running from any other directory silently falls back to `Debug`/`x64` and produces a wrong-platform database. Passing `--config` is robust to the working directory. ```powershell +$extractor = (Get-ChildItem "$env:LOCALAPPDATA\mscppls\msbuild-extractor\public\Microsoft.VisualStudio.Cpp.MSBuildExtractor.*\tools\msbuild-extractor-sample.exe" | Select-Object -First 1).FullName New-Item -ItemType Directory -Path .mscppls -Force | Out-Null -msbuild-extractor-sample.exe ` +& $extractor ` --config \msbuild-extractor.json ` --solution ` -o .mscppls\compile_commands.json @@ -87,8 +90,9 @@ This tier applies **only when no committed `msbuild-extractor.json` exists**. If Then run the extractor with that exact configuration and platform: ```powershell +$extractor = (Get-ChildItem "$env:LOCALAPPDATA\mscppls\msbuild-extractor\public\Microsoft.VisualStudio.Cpp.MSBuildExtractor.*\tools\msbuild-extractor-sample.exe" | Select-Object -First 1).FullName New-Item -ItemType Directory -Path .mscppls -Force | Out-Null -msbuild-extractor-sample.exe ` +& $extractor ` --solution ` -c -a ` -o .mscppls\compile_commands.json @@ -121,38 +125,12 @@ Changing the flag set changes the entry set, the most common way to produce the - **Do not pass** `--all-configurations`, `--merge`, `--deduplicate`, or `--validate` unless the user asks; `--validate` compiles every TU with `cl.exe` and is slow. - **Do not create, edit, or delete a committed `msbuild-extractor.json`.** Authoring a new one is only the offered follow-up after a Tier 2 run. -### If the extractor is not already available - -The extractor is typically already built and available: try `msbuild-extractor-sample.exe` on `PATH`, or `.tools\msbuild-extractor-sample.exe`, before downloading. If present, use it and skip the download. - -If it is not present, download the pinned release and verify its SHA256 before trusting it. The binary is self-contained, with no .NET runtime needed. - -```powershell -$version = 'v0.3.0' -$expected = '543c5cc6b57a1b3eb46b11e56b8f35a9ca8676106426bde6041ca2dc2e06f13c' -$exe = ".tools\msbuild-extractor-sample.exe" - -if (-not (Test-Path $exe) -or (Get-FileHash $exe -Algorithm SHA256).Hash -ne $expected.ToUpper()) { - New-Item -ItemType Directory -Path ".tools" -Force | Out-Null - $tmp = "$exe.download" - Invoke-WebRequest -Uri "https://github.com/microsoft/msbuild-extractor-sample/releases/download/$version/msbuild-extractor-sample.exe" -OutFile $tmp - $actual = (Get-FileHash $tmp -Algorithm SHA256).Hash - if ($actual -ne $expected.ToUpper()) { - Remove-Item $tmp -Force - throw "SHA256 mismatch`n expected: $expected`n actual: $actual" - } - Move-Item $tmp $exe -Force -} -``` - -When upgrading the pinned version, get the new SHA256 from the `digest` field of the asset at `https://api.github.com/repos/microsoft/msbuild-extractor-sample/releases/latest`. If downloaded to `.tools\`, invoke `.tools\msbuild-extractor-sample.exe` in the recipes above. - ### Developer environment (fallback only) The default path passes no toolchain-selection flags and lets the extractor resolve MSVC via `vswhere`. Do not add `--use-dev-env` routinely. Use it only when default `vswhere` resolution fails, or when the user explicitly wants to index the already-active Developer environment (a Developer Command Prompt / Developer PowerShell, or a shell where `vcvarsall.bat` set `VCINSTALLDIR`, `INCLUDE`, `LIB`). It makes the extractor honor that environment instead of re-resolving via `vswhere`: ```powershell -msbuild-extractor-sample.exe ` +& $extractor ` --use-dev-env ` --solution ` -o .mscppls\compile_commands.json @@ -165,8 +143,8 @@ msbuild-extractor-sample.exe ` If `vswhere` picks the wrong one, list and select explicitly: ```powershell -msbuild-extractor-sample.exe --list-instances -msbuild-extractor-sample.exe --vs-instance --solution -o .mscppls\compile_commands.json +& $extractor --list-instances +& $extractor --vs-instance --solution -o .mscppls\compile_commands.json ``` (Add `-c`/`-a` in Tier 2 as above.) @@ -198,7 +176,7 @@ The fix is **out-of-process mode**: spawn the repo's own MSBuild.exe via `--msbu Example (the Windows driver samples built with the Enterprise WDK; the version folders track the EWDK build, here `18` / `v180` / `14.50.35717`, so confirm them after mounting): ```powershell -.tools\msbuild-extractor-sample.exe ` +& $extractor ` --msbuild-path "D:\Program Files\Microsoft Visual Studio\18\BuildTools\MSBuild\Current\Bin\MSBuild.exe" ` --solution general\echo\kmdf\kmdfecho.sln ` -c Debug -a x64 `