Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
a1f8490
Strengthen winmd comparisons for compiler upgrades
jevansaks Oct 14, 2026
b5b7b26
Add opt-in header annotations to C# metadata generation
jevansaks Oct 14, 2026
e065daa
Avoid duplicate retval annotations from SAL and MIDL
jevansaks Oct 14, 2026
461d84f
Encode instance constructor signatures correctly
jevansaks Oct 14, 2026
f24da67
Preserve native types described by AssociatedEnum
jevansaks Oct 14, 2026
2983809
Isolate discovered remaps from annotated header inputs
jevansaks Oct 14, 2026
00ade0a
Keep cached binding settings out of native discovery
jevansaks Oct 14, 2026
efc2082
Support scoped Win32 OS defaults and native field counts
jevansaks Oct 15, 2026
43af342
Gate native field array inference behind explicit opt-in
jevansaks Oct 15, 2026
7ea6c85
Preserve OS annotations on individual native constants
jevansaks Oct 16, 2026
746d812
Read shift-left annotations from the first Clang AST
jevansaks Oct 16, 2026
5200b89
Prioritize annotation authoring in the shift-left spec
jevansaks Oct 16, 2026
07679ec
Keep annotation diagnostic policy in the scraper
jevansaks Oct 17, 2026
8d6d529
Carry original-IDL method annotations through first-parse references
jevansaks Oct 17, 2026
da358a6
Canonicalize result flags and publish the header migration playbook
jevansaks Oct 17, 2026
3dc9ff3
Document exact SDK preparation and bounded scrape commands
jevansaks Oct 17, 2026
afdb1fe
Stage shared annotation helper for the debug SDK sample
jevansaks Oct 17, 2026
217cda9
Support source-owned logical enum contributions
jevansaks Oct 17, 2026
d8262ba
Preserve primitive aliases of header-owned logical enums
jevansaks Oct 17, 2026
bf7d1ad
Merge main and reconcile SupportedOS scope specification
jevansaks Oct 17, 2026
8de8a7b
Preserve complete API diff artifacts (#2374)
jevansaks Oct 18, 2026
980747d
Add missing COM output annotations (#2372)
jevansaks Oct 19, 2026
825a73d
Add RAIIFree annotations for HidD_GetPreparsedData and related produc…
jevansaks Oct 19, 2026
c16762d
Update Windows SDK to 10.0.28000.2705 (#2373)
jevansaks Oct 19, 2026
2f76767
Bump dotnet-sdk from 10.0.303 to 10.0.402 (#2378)
dependabot[bot] Oct 21, 2026
0da6284
Merge current main with tooling-only annotation support
jevansaks Oct 22, 2026
c915725
Run emitter tests in the standard test pipeline
jevansaks Oct 23, 2026
6d897be
Consolidate annotation tooling with legacy SDK compatibility
jevansaks Oct 24, 2026
5053dc9
Restore legacy annotation input compatibility
jevansaks Oct 24, 2026
909a851
Preserve legacy providers during header-aware composition
jevansaks Oct 24, 2026
a73bff9
Include scraper runtime dependencies in generator SDK package
jevansaks Oct 24, 2026
96d3888
Require checkout before always-run local artifact uploads
jevansaks Oct 24, 2026
93a1cc0
Share Unicode SDK context with native constant evaluation
jevansaks Oct 24, 2026
450bf4d
Retain established native typedef binding decisions
jevansaks Oct 24, 2026
a826894
Preserve classified SDK preparation warnings across MSBuild
jevansaks Oct 24, 2026
756e2da
Compose proven DECLARE_HANDLE providers without changing public storage
jevansaks Oct 24, 2026
2b750f6
Recover guarded ANSI exports and authored macro remaps
jevansaks Oct 24, 2026
dfc297a
Preserve source-proven legacy member names and opaque typedef storage
jevansaks Oct 24, 2026
86629ee
Preserve existing enum Flags and MIDL command spelling
jevansaks Oct 24, 2026
6c76fce
Strengthen native comparison and retained metadata controls
jevansaks Oct 24, 2026
7ed7148
Preserve synthesized SAL on remote IDL methods
jevansaks Oct 24, 2026
06350aa
Reduce native constant probe reparses
jevansaks Oct 24, 2026
4c96069
Preserve compiler-backed constant ownership and dependency storage
jevansaks Oct 25, 2026
0fdd8a8
Correct incomplete native typedef storage fixture
jevansaks Oct 25, 2026
aaa8776
Use scanner values before selective Clang constant evaluation
jevansaks Oct 26, 2026
a606fcc
Reduce redundant default native provider work
jevansaks Oct 26, 2026
d3d8551
Reduce native inventory overhead and parallelize scraper fixtures
jevansaks Oct 26, 2026
4eb822f
Batch native scraper contracts and retain test results
jevansaks Oct 26, 2026
5eadc50
Collect generation diagnostics and validate native SDK identity
jevansaks Oct 27, 2026
415c504
Keep implicit generator settings optional
jevansaks Oct 27, 2026
ba7d2d5
Collect generated auto-type retirement I/O diagnostics
jevansaks Oct 27, 2026
b5ae213
Validate selected toolchain header ownership in native constants
jevansaks Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
14 changes: 12 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,25 @@ dotnet build generation/WinSDK -c Release -p:ScanArch=crossarch -t:ScrapeHeaders
## Test Commands

```powershell
# Run all tests
# Run all tests after generating the WinMD (requires Python 3)
./scripts/DoTests.ps1

# Individual test suites
dotnet test tests/MetadataUtils.Tests -c Release
dotnet test tests/Windows.Win32.Tests -c Release # winmd integrity tests
dotnet test tests/Win32MetadataScraperTests -c Release # scraper heuristic tests
dotnet test tests/ClangSharpSourceToWinmdTests -c Release
dotnet test tests/ClangSharpSourceToWinmdTests -c Release # emitter and cross-architecture composition
python -m unittest discover -s tests\SdkAnnotationCoverage.Tests -p "test_*.py"

# Single test
dotnet test tests/Win32MetadataScraperTests -c Release --filter "TransitiveInclude"
```

`DoTests.ps1` runs the utility, emitter, SDK annotation coverage-auditor, scraper,
and WinMD integrity suites. GitHub Actions and Azure Pipelines invoke this same
runner after generating the WinMD. The Python suite uses the standard library
and tests the annotation coverage auditor, not the separate Node workflow tests.

## Architecture

The toolchain converts Windows SDK C/C++ headers into a .NET metadata assembly (`Windows.Win32.winmd`).
Expand Down Expand Up @@ -66,6 +72,10 @@ Each arch job scrapes ALL partitions for that architecture.

**NuGet feed:** The repo uses a private ADO Artifact Feed (`Win32Metadata-Dependencies`) as its sole package source. New packages from nuget.org must be saved to this feed first.

**Import discovery before header overrides:** Before adding `_Win32_ImportLibrary_` or a manual DLL mapping, inspect the selected SDK's import libraries and the library-processing set in `scripts\CreateProcLibMappingForAllLibs.ps1`. Use `WinmdUtils dumpImports --lib <path>` to verify the actual symbols and DLL, not just the library filename. If a matching `.lib` was omitted or its imports were missed, fix shared discovery instead of adding a redundant header annotation. Use an authored override only when no authoritative SDK library mapping is available or the native contract genuinely requires an override; record the SDK/library search evidence.

**Header-annotation migration:** Follow `docs\copilot\plans\header-annotation-migration-playbook.md`. Classify the original artifact before editing: patch authored `.h` files directly, but patch original `.idl` and regenerate all affected outputs for IDL-owned declarations. Never patch a generated `.h` or independently patch the same shared artifact in different partitions. Use exact qualified tooling/pilot pins and keep raw OLD-to-NEW differences visible.

**Winmd equivalence:** After any change, the winmd must be compared against the baseline. The `NoSuggestedRemappings` test in `Windows.Win32.Tests` validates that no new unhandled remappings were introduced. API diffs are automatically posted as PR comments by CI. Run `.\scripts\DiffWinmdToBaseline.ps1` locally to see a full C# declaration diff against the last release.

**Cross-partition remaps:** Remaps in `scraper.settings.rsp` are global — they apply to all partitions. Auto-discovered remaps (from `Win32MetadataScraper`) are per-partition. If a tag name is used across partitions but the typedef is only in one partition's headers, the remap must be global (in `scraper.settings.rsp`) OR the referencing partition must `#include` the header with the typedef.
Expand Down
180 changes: 175 additions & 5 deletions .github/tests/pr-api-diff-comment.test.cjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const { execFileSync } = require('node:child_process');
const { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } = require('node:fs');
const { tmpdir } = require('node:os');
const path = require('node:path');
const { test } = require('node:test');

const workflow = readFileSync(path.join(__dirname, '..', 'workflows', 'pr-api-diff-comment.yml'), 'utf8')
.replace(/\r\n/g, '\n');
const steps = new Map(workflow.split(/^ - name: /m).slice(1).map(block => {
const endOfName = block.indexOf('\n');
return [block.slice(0, endOfName), block.slice(endOfName + 1)];
}));
const validationWorkflow = readFileSync(path.join(__dirname, '..', 'workflows', 'pr-validation.yml'), 'utf8')
.replace(/\r\n/g, '\n');
const steps = workflowSteps(workflow);
const validationSteps = workflowSteps(validationWorkflow);
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;

function workflowSteps(source) {
return new Map(source.split(/^ - name: /m).slice(1).map(block => {
const endOfName = block.indexOf('\n');
return [block.slice(0, endOfName), block.slice(endOfName + 1)];
}));
}

function validationJob(name) {
const marker = ` ${name}:\n`;
const start = validationWorkflow.indexOf(marker);
assert.notEqual(start, -1, `Missing validation job: ${name}`);
return validationWorkflow.slice(start + marker.length).split(/^ [\w-]+:\s*$/m)[0];
}

function script(name) {
const step = steps.get(name);
assert.ok(step, `Missing workflow step: ${name}`);
Expand All @@ -23,6 +39,14 @@ function script(name) {

const checkArtifact = script('Check API diff artifact');
const postComment = script('Post API diff comment');
const oversizedPatch = [
'diff --git a/bin/baseline.apidump.cs b/bin/current.apidump.cs',
'--- a/bin/baseline.apidump.cs',
'+++ b/bin/current.apidump.cs',
'@@ -1,4000 +1,8000 @@',
...Array.from({ length: 4000 }, (_, index) => `-public const int OldValue${index} = ${index};`),
...Array.from({ length: 8000 }, (_, index) => `+public const int NewValue${index} = ${index};`)
].join('\r\n') + '\r\n';

function fixture(documentationOnly = false) {
return {
Expand Down Expand Up @@ -145,6 +169,112 @@ test('download and comment both require the preflight output', () => {
assert.doesNotMatch(workflow, /uses: actions\/checkout/);
});

test('both artifact uploads still include the generated patch', () => {
const upload = validationSteps.get('Upload API diff');
assert.match(upload, /^ name: api-diff$/m);
assert.match(upload, /^ path: bin\/api-diff\.patch$/m);
const winmdUpload = validationSteps.get('Upload winmd and API dump');
assert.match(winmdUpload, /^ name: winmd$/m);
for (const file of ['Windows.Win32.winmd', 'current.apidump.cs', 'baseline.apidump.cs', 'api-diff.patch']) {
assert.ok(winmdUpload.includes(` bin/${file}\n`));
}
});

for (const [jobName, uploadCount] of [['scrape', 1], ['build-test', 2]]) {
test(`${jobName} always-run local uploads require a successful checkout`, () => {
const job = validationJob(jobName);
assert.match(job, /^ - uses: actions\/checkout@[^\n]+\n id: checkout$/m);
const uploads = [...workflowSteps(job).values()].filter(step =>
/^ uses: \.\/\.github\/actions\/upload-artifact-with-retry$/m.test(step) &&
/^ if: always\(\)/m.test(step));
assert.equal(uploads.length, uploadCount);
for (const upload of uploads) {
const condition = upload.match(/^ if: (.+)$/m)[1];
const shouldUpload = new Function('needs', 'steps', 'always', `return (${condition});`);
const needs = { changes: { outputs: { build_needed: 'true' } } };
for (const outcome of ['', 'skipped', 'failure', 'cancelled']) {
assert.equal(shouldUpload(needs, { checkout: { outcome } }, () => true), false,
`Local action must not load after checkout outcome ${JSON.stringify(outcome)}.`);
}
assert.equal(shouldUpload(needs, { checkout: { outcome: 'success' } }, () => true), true,
'Diagnostics must still upload after later build or test failures.');
if (jobName === 'build-test') {
needs.changes.outputs.build_needed = 'false';
assert.equal(shouldUpload(needs, { checkout: { outcome: 'success' } }, () => true), false);
}
}
});
}

test('failed prerequisite scrapes still fail before checkout without masking the primary error', () => {
const job = validationJob('build-test');
const verify = workflowSteps(job).get('Verify scrape jobs');
const condition = verify.match(/^ if: (.+)$/m)[1];
const shouldFail = new Function('needs', `return (${condition});`);
for (const result of ['failure', 'cancelled', 'skipped']) {
assert.equal(shouldFail({
changes: { outputs: { build_needed: 'true' } }, scrape: { result }
}), true);
}
assert.equal(shouldFail({
changes: { outputs: { build_needed: 'true' } }, scrape: { result: 'success' }
}), false);
assert.equal(shouldFail({
changes: { outputs: { build_needed: 'false' } }, scrape: { result: 'skipped' }
}), false);
assert.ok(job.indexOf(' - name: Verify scrape jobs\n') <
job.indexOf(' - uses: actions/checkout@'));
assert.doesNotMatch(verify, /continue-on-error: true/);
const command = verify.match(/^ run: (.+)$/m)[1];
assert.throws(() => execFileSync('pwsh',
['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', command], { windowsHide: true }),
error => error.status !== 0 &&
/One or more header scrape jobs did not succeed\./.test(error.stderr.toString()));
});

for (const [name, patch, expectedOutputs] of [
['an oversized diff', oversizedPatch, { has_diff: 'true', additions: '8000', deletions: '4000' }],
['an empty diff', '', { has_diff: 'false' }]
]) {
test(`the producer preserves ${name} byte-for-byte`, () => {
const directory = mkdtempSync(path.join(tmpdir(), 'win32metadata-api-diff-'));
try {
const baselineDirectory = path.join(directory, 'bin', 'baseline-artifact');
mkdirSync(baselineDirectory, { recursive: true });
writeFileSync(path.join(baselineDirectory, 'Windows.Win32.winmd'), '');
writeFileSync(path.join(directory, 'expected.patch'), patch);
const outputFile = path.join(directory, 'step-output.txt');
const step = validationSteps.get('Generate API surface diff');
const marker = ' run: |\n';
const start = step.indexOf(marker);
assert.notEqual(start, -1, 'Missing inline diff producer');
const producer = step.slice(start + marker.length).replace(/^ {10}/gm, '');
const commands = String.raw`
function dotnet { $global:LASTEXITCODE = 0 }
function cmd {
if ($args.Count -ne 2 -or $args[0] -ne '/c' -or
$args[1] -ne 'git diff --no-index --unified=3 bin\baseline.apidump.cs bin\current.apidump.cs > bin\api-diff.patch 2>&1') {
throw 'Unexpected diff command'
}
Copy-Item -LiteralPath expected.patch -Destination bin\api-diff.patch
$global:LASTEXITCODE = ${patch.length === 0 ? 0 : 1}
}
`;
execFileSync('pwsh', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', commands + producer], {
cwd: directory,
env: { ...process.env, GITHUB_OUTPUT: outputFile },
windowsHide: true
});
const actual = readFileSync(path.join(directory, 'bin', 'api-diff.patch'));
assert.ok(actual.equals(Buffer.from(patch)), `Patch changed: expected ${Buffer.byteLength(patch)} bytes, got ${actual.length}`);
const outputs = Object.fromEntries(readFileSync(outputFile, 'utf8').trim().split(/\r?\n/).map(line => line.split('=')));
assert.deepEqual(outputs, expectedOutputs);
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
}

test('recorded documentation-only execution skips the download and comment', async () => {
const result = await runWorkflow(fixture(true));
assert.deepEqual(result.outputs, { available: 'false' });
Expand All @@ -162,6 +292,46 @@ test('a current build artifact downloads and posts the actual diff', async () =>
assert.equal(result.comments[0].issue_number, 42);
assert.match(result.comments[0].body, /\+1 additions \/ -1 deletions/);
assert.match(result.comments[0].body, /\+added\n-removed/);
assert.doesNotMatch(result.comments[0].body, /Diff truncated/);
});

test('an oversized diff retains full statistics and links to the complete artifact', async () => {
const data = fixture();
data.patch = oversizedPatch;
const result = await runWorkflow(data);
const body = result.comments[0].body;
assert.match(body, /\+8000 additions \/ -4000 deletions/);
assert.ok(body.includes('Diff truncated'));
assert.ok(body.includes(`[api-diff artifact](${data.run.html_url}#artifacts)`));
assert.ok(body.length <= 65536, `Comment is ${body.length} characters`);
assert.ok(!body.includes('+public const int NewValue7999 = 7999;'));
assert.equal(data.patch, oversizedPatch);
});

for (const [name, patch] of [
['wrapper and truncation notice', '+' + 'x'.repeat(65500)],
['long Markdown fences', '+' + '`'.repeat(34000) + '\n'],
['many backtick runs', '+`value`\n'.repeat(70000)]
]) {
test(`the complete comment fits the limit including ${name}`, async () => {
const data = fixture();
data.patch = patch;
const result = await runWorkflow(data);
const body = result.comments[0].body;
assert.ok(body.length <= 65536, `Comment is ${body.length} characters`);
assert.ok(body.includes('Diff truncated'));
assert.ok(body.startsWith('<!-- win32metadata-api-diff -->'));
assert.match(body, /<\/details>\n/);
assert.ok(body.endsWith('> This comment is automatically updated on each push.'));
});
}

test('backticks in an untruncated diff remain inside its Markdown fence', async () => {
const data = fixture();
data.patch = '+```embedded fence```\n';
const result = await runWorkflow(data);
assert.ok(result.comments[0].body.includes('````diff\n' + data.patch + '\n````'));
assert.doesNotMatch(result.comments[0].body, /Diff truncated/);
});

test('an empty actual patch still posts no API differences', async () => {
Expand Down
25 changes: 19 additions & 6 deletions .github/workflows/pr-api-diff-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,16 @@ jobs:
const diffLines = diff.split(/\r?\n/);
const additions = diffLines.filter(line => /^\+[^+]/.test(line)).length;
const deletions = diffLines.filter(line => /^-[^-]/.test(line)).length;
const longestBacktickRun = Math.max(0, ...Array.from(diff.matchAll(/`+/g), match => match[0].length));
const fence = '`'.repeat(Math.max(3, longestBacktickRun + 1));

body = `${marker}
const maxCommentLength = 65536;
const truncationNotice = `\n\n> Diff truncated to fit this comment. Download the complete \`api-diff.patch\` from the [api-diff artifact](${runUrl}#artifacts).`;
const renderComment = (preview, notice = '') => {
let longestBacktickRun = 0;
for (const match of preview.matchAll(/`+/g)) {
longestBacktickRun = Math.max(longestBacktickRun, match[0].length);
}
const fence = '`'.repeat(Math.max(3, longestBacktickRun + 1));

return `${marker}
## 📋 API Surface Diff

**+${additions} additions / -${deletions} deletions** vs main branch ([full validation run](${runUrl}))
Expand All @@ -171,12 +177,19 @@ jobs:
<summary>Click to expand API diff</summary>

${fence}diff
${diff}
${preview}
${fence}

</details>
</details>${notice}

> This comment is automatically updated on each push.`;
};
body = renderComment(diff);
if (body.length > maxCommentLength) {
// Reserve space for the wrapper, both fences, and the truncation notice.
const previewLength = Math.max(0, maxCommentLength - (body.length - diff.length) - truncationNotice.length);
body = renderComment(diff.slice(0, previewLength), truncationNotice);
}
}

const comments = await github.paginate(github.rest.issues.listComments, {
Expand Down
16 changes: 5 additions & 11 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ jobs:

steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
id: checkout
with:
lfs: true
fetch-depth: 0
Expand Down Expand Up @@ -145,7 +146,7 @@ jobs:

- name: Upload build logs
uses: ./.github/actions/upload-artifact-with-retry
if: always()
if: always() && steps.checkout.outcome == 'success'
with:
name: scrape_logs_${{ matrix.arch }}
path: bin/logs
Expand Down Expand Up @@ -174,6 +175,7 @@ jobs:
run: throw 'One or more header scrape jobs did not succeed.'

- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
id: checkout
if: needs.changes.outputs.build_needed == 'true'
with:
lfs: true
Expand Down Expand Up @@ -335,14 +337,6 @@ jobs:
"deletions=$deletions" | Out-File -FilePath $env:GITHUB_OUTPUT -Append

Write-Host "+$additions additions / -$deletions deletions"

# Truncate if too large for a GH comment
$diffText = $diffLines -join "`n"
$maxLen = 60000
if ($diffText.Length -gt $maxLen) {
$diffText = $diffText.Substring(0, $maxLen) + "`n`n... (diff truncated - see full artifact)"
Set-Content -Path bin\api-diff.patch -Value $diffText -Encoding UTF8
}
}

# Reset LASTEXITCODE so GitHub Actions doesn't treat this step as failed
Expand All @@ -358,7 +352,7 @@ jobs:

- name: Upload winmd and API dump
uses: ./.github/actions/upload-artifact-with-retry
if: always() && needs.changes.outputs.build_needed == 'true'
if: always() && needs.changes.outputs.build_needed == 'true' && steps.checkout.outcome == 'success'
with:
name: winmd
path: |
Expand All @@ -370,7 +364,7 @@ jobs:

- name: Upload build logs
uses: ./.github/actions/upload-artifact-with-retry
if: always() && needs.changes.outputs.build_needed == 'true'
if: always() && needs.changes.outputs.build_needed == 'true' && steps.checkout.outcome == 'success'
with:
name: build_logs
path: bin/logs
Expand Down
4 changes: 2 additions & 2 deletions BuildTools/BuildTools.proj
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

<ItemGroup>
<!-- These are here to support CommonUtils.ps1, which parses the .props file that nuget restore generates based on these. -->
<PackageReference Include="Microsoft.Windows.SDK.CPP" Version="10.0.26100.7705" GeneratePathProperty="true"/>
<PackageReference Include="Microsoft.Windows.SDK.CPP.x64" Version="10.0.26100.7705" GeneratePathProperty="true"/>
<PackageReference Include="Microsoft.Windows.SDK.CPP" Version="10.0.28000.2705" GeneratePathProperty="true"/>
<PackageReference Include="Microsoft.Windows.SDK.CPP.x64" Version="10.0.28000.2705" GeneratePathProperty="true"/>
<!-- Used by CommonUtils.ps1 to get the path to the last release .winmd -->
<PackageReference Include="Microsoft.Windows.SDK.Win32Metadata" Version="$(LastWin32MetadataReleaseVersion)" GeneratePathProperty="True">
<PrivateAssets>all</PrivateAssets>
Expand Down
Loading
Loading