Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions AUTHORING_EXTRACTOR_SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <sln> /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

Expand Down Expand Up @@ -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.

Expand All @@ -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 `
Expand Down Expand Up @@ -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 `
Expand Down
13 changes: 13 additions & 0 deletions plugins/cpp-language-server/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
}
137 changes: 137 additions & 0 deletions plugins/cpp-language-server/scripts/install-msbuild-extractor.ps1
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 <path>\msbuild-extractor.json `
--solution <the declared solution> `
-o .mscppls\compile_commands.json
Expand All @@ -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 <the committed .sln or .slnx> `
-c <the solution's default configuration> -a <the solution's default platform> `
-o .mscppls\compile_commands.json
Expand Down Expand Up @@ -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 <the committed .sln or .slnx> `
-o .mscppls\compile_commands.json
Expand All @@ -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 <id> --solution <the committed .sln or .slnx> -o .mscppls\compile_commands.json
& $extractor --list-instances
& $extractor --vs-instance <id> --solution <the committed .sln or .slnx> -o .mscppls\compile_commands.json
```

(Add `-c`/`-a` in Tier 2 as above.)
Expand Down Expand Up @@ -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 `
Expand Down