Skip to content

Add PackageValidator tool for NuGet package inspection and validation#4407

Draft
paulmedynski wants to merge 3 commits into
mainfrom
dev/paul/version-tool
Draft

Add PackageValidator tool for NuGet package inspection and validation#4407
paulmedynski wants to merge 3 commits into
mainfrom
dev/paul/version-tool

Conversation

@paulmedynski

@paulmedynski paulmedynski commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a standalone .NET 10 CLI under tools/PackageValidator that inspects one or more .nupkg files and validates their versions, signing, and symbols using metadata-only reading (no assembly loading). This keeps it cross-platform and able to read assemblies built for TFMs the host does not run (e.g. net462 from a Linux host).

This is exploratory maintainer tooling that mirrors the PackageCompatibility tool layout (src/ + test/, its own Directory.*.props / global.json) and is isolated from the main driver build.

Capabilities

  • Package id/version, dependency groups, and NuGet signature from the .nuspec.
  • Per-assembly AssemblyVersion / FileVersion / InformationalVersion, culture, public key token, strong-name signing status, and target framework.
  • Native binary Win32 version info and architecture.
  • Binary classification (implementation / reference / satellite / native) so symbol coverage is judged only over implementation assemblies.
  • Sibling .snupkg symbol matching by debug GUID with portable-PDB checksum verification, embedded-symbol detection, and orphan/mismatch reporting.
  • Intrinsic validation rules emitting severity-tagged findings, plus optional --expect-package/file/assembly-version assertions for inter-package version-match validation.
  • Batch/directory input, JSON or human output, --fail-on exit-code gating, and a free-form Notes help section.

Command-line arguments

PackageValidator <paths>... [options]
Argument / option Description
<paths>... One or more .nupkg files, or directories scanned recursively for .nupkg files.
--json Emit machine-readable JSON instead of the human-readable layout.
--no-snupkg Do not look for or process sibling .snupkg symbol packages (embedded symbols are still evaluated).
--fail-on <value> Exit non-zero when a finding matches the given value. Repeatable.
--expect-package-version [id=]VALUE Confirm the package version equals VALUE. Repeatable.
--expect-file-version [id=]VALUE Confirm every assembly's file version equals VALUE. Repeatable.
--expect-assembly-version [id=]VALUE Confirm every assembly's assembly version equals VALUE. Repeatable.

--fail-on accepts a severity (error, warning, info), the token any, or a finding category:
version-inconsistency, missing-symbols, symbol-mismatch, symbol-checksum-mismatch, symbol-orphan, symbol-duplicate, delay-signed, unsigned, package-unsigned, dependency-inconsistency, unexpected-package-version, unexpected-file-version, unexpected-assembly-version.

--expect-* values are either VALUE (applies to every package in the run) or id=VALUE (applies only to the package whose id is id). A specific id overrides a bare VALUE (wildcard), and because the options repeat, a family-wide expectation can coexist with a per-package override — for example, assert one family file version while letting Microsoft.SqlServer.Server differ. A missing version counts as a mismatch, so the assertion never silently passes.

Exit codes: 0 pass · 1 runtime error · 2 --fail-on gate tripped.

These same details are printed in a free-form Notes section appended to --help.

Example output

Inspect a single package (human-readable)

PackageValidator Microsoft.Data.SqlClient.7.1.0-preview1.nupkg

Package file:     Microsoft.Data.SqlClient.7.1.0-preview1.nupkg
NuGet package id: Microsoft.Data.SqlClient
NuGet version:    7.1.0-preview1
Package signed:   no
Symbol package:   Microsoft.Data.SqlClient.7.1.0-preview1.snupkg
Dependencies:
  [.NETFramework4.6.2]
    Microsoft.Data.SqlClient.SNI [6.0.2, 7.0.0)
    Microsoft.IdentityModel.JsonWebTokens 8.16.0
    System.Text.Json 10.0.3
    ... (trimmed)
  [net8.0]
    ... (trimmed)

lib/net8.0/Microsoft.Data.SqlClient.dll  [Implementation]
  Assembly version:       7.0.0.0
  File version:           7.1.0.26176
  Informational version:  7.1.0-preview1+9c72dc0b4e8eed7fe6e83684e86800f9b4ac0b57
  Target framework:       .NETCoreApp,Version=v8.0
  Public key token:       23ec7fc2d6eaa4a5
  Signing status:         Signed
  Strong name:            Microsoft.Data.SqlClient, Version=7.0.0.0, Culture=neutral, PublicKeyToken=23ec7fc2d6eaa4a5
  Debug (PDB) id:         546ac289-357a-4bf7-8c42-dabbcb8963ec
  Embedded symbols:       no
  Symbol package PDB:     lib/net8.0/Microsoft.Data.SqlClient.pdb (matches assembly; checksum verified)

... (one block per DLL)

Symbol summary:
  Implementation assemblies have symbols: yes
  All symbol-package symbols match:       yes
  Symbol files with no matching assembly:
    runtimes/win/lib/net462/Microsoft.Data.SqlClient.pdb

Findings:
  [WARNING] symbol-orphan: runtimes/win/lib/net462/Microsoft.Data.SqlClient.pdb - symbol-package PDB does not correspond to any assembly in the package.
  [INFO] package-unsigned: Microsoft.Data.SqlClient.7.1.0-preview1.nupkg - package is not signed (no .signature.p7s entry).

Run summary:
  Packages inspected: 1
  Errors:             0
  Warnings:           1
  Info:               1
  Gate:               passed

Machine-readable output (--json)

Per-binary object (symbols matched and checksum-verified against the sibling .snupkg):

{
  "path": "lib/net8.0/Microsoft.Data.SqlClient.dll",
  "kind": "Implementation",
  "isManagedAssembly": true,
  "assemblyName": "Microsoft.Data.SqlClient",
  "assemblyVersion": "7.0.0.0",
  "fileVersion": "7.1.0.26176",
  "informationalVersion": "7.1.0-preview1+9c72dc0b4e8eed7fe6e83684e86800f9b4ac0b57",
  "targetFramework": ".NETCoreApp,Version=v8.0",
  "culture": "neutral",
  "publicKeyToken": "23ec7fc2d6eaa4a5",
  "signingStatus": "Signed",
  "debugId": "546ac289-357a-4bf7-8c42-dabbcb8963ec",
  "pdbChecksums": [ "SHA256:89c26a547a35f7fbcc42dabbcb8963ec78d7cd167992f9de97b134e4db63bc73" ],
  "hasEmbeddedSymbols": false,
  "hasSymbols": true,
  "hasSymbolPackageSymbols": true,
  "symbolPackageSymbolsMatch": true,
  "symbolPackageVerifiedByChecksum": true,
  "symbolPackageFile": "lib/net8.0/Microsoft.Data.SqlClient.pdb"
}

Gate a directory against expected versions (--expect-* + --fail-on)

Assert the family file version uniformly (with a per-package override for Microsoft.SqlServer.Server) and fail on any error:

PackageValidator <dir-of-nupkgs> \
  --expect-file-version *=7.1.0.17604 \
  --expect-file-version Microsoft.SqlServer.Server=1.1.0.17604 \
  --fail-on error

On a build that stamps the file-version revision on only some family packages, the gate trips (exit code 2):

  [ERROR] unexpected-file-version: lib/netstandard2.0/Microsoft.Data.SqlClient.Extensions.Abstractions.dll - file version is '7.1.0.0', expected '7.1.0.17604'.
  [ERROR] unexpected-file-version: lib/net462/Microsoft.Data.SqlClient.Extensions.Azure.dll - file version is '7.1.0.0', expected '7.1.0.17604'.
  [ERROR] unexpected-file-version: lib/netstandard2.0/Microsoft.Data.SqlClient.Internal.Logging.dll - file version is '7.1.0.0', expected '7.1.0.17604'.
  [ERROR] unexpected-file-version: lib/net46/Microsoft.SqlServer.Server.dll - file version is '1.1.0.0', expected '1.1.0.17604'.
  ...
Run summary:
  Errors:             6
  Gate:               FAILED

On a build that stamps the revision uniformly, the same assertion passes (exit code 0).

Tests

  • xUnit v3 test project (running on Microsoft.Testing.Platform) at tools/PackageValidator/test48 tests covering public-key-token computation, binary classification, SemVer 2.0 range evaluation (including prerelease ordering), the rules engine, and expected-version assertions.
  • Wired into build.proj: BuildPackageValidator / BuildPackageValidatorTest / TestPackageValidator (and the matching PackageCompatibility test targets); BuildTools now builds the tools and their test projects.

Notes

  • Tests added (48, xUnit v3)
  • Builds clean (0 warnings/errors); validated against real nuget.org packages and PR pipeline artifacts
  • Tool + tests wired into build.proj (BuildTools); no changes to the main driver or CI pipelines
  • Addressed automated review feedback (native-version exception handling, expected-version null handling, SemVer range semantics, PDB reader disposal)

Copilot AI review requested due to automatic review settings June 25, 2026 18:36
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board Jun 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new standalone maintainer CLI (tools/PackageValidator) to inspect .nupkg/.snupkg artifacts (versions, signing, dependencies, and symbols) using metadata-only inspection, plus a dedicated xUnit test project for the tool.

Changes:

  • Added a net10.0 PackageValidator console app with JSON/human output and --fail-on gating.
  • Implemented inspection/validation components: nuspec/dependencies parsing, managed/native binary inspection, symbol-package resolution, and a rules engine emitting categorized findings.
  • Added xUnit tests covering version range evaluation, expectation parsing, symbol/signing/version validations, and helper utilities.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tools/PackageValidator/PackageValidator.csproj Defines the standalone CLI tool project and its package dependencies.
tools/PackageValidator/Directory.Build.props Isolates build settings for the tool directory (central package management).
tools/PackageValidator/Directory.Packages.props Centralizes tool package versions (System.CommandLine).
tools/PackageValidator/Program.cs CLI surface: args/options, help augmentation, run orchestration, gating/exit codes.
tools/PackageValidator/PackageInspector.cs Reads .nuspec, signature entry, and enumerates DLLs in a .nupkg.
tools/PackageValidator/AssemblyInspector.cs Metadata-only inspection of managed assemblies + native fallback handling.
tools/PackageValidator/BinaryClassifier.cs Categorizes binaries (implementation/reference/satellite/native/other) from paths.
tools/PackageValidator/Models.Common.cs Shared model types (enums/findings/dependency models/native info).
tools/PackageValidator/Models.Report.cs Report models for run/package/binary/symbol-package output.
tools/PackageValidator/Validator.cs Intrinsic rule engine + cross-package dependency consistency checks.
tools/PackageValidator/VersionExpectations.cs Parses/holds --expect-* inputs and resolves wildcard/id overrides.
tools/PackageValidator/VersionRange.cs Minimal NuGet version-range evaluator used for dependency checks.
tools/PackageValidator/SymbolResolver.cs Matches .snupkg PDBs to assemblies by GUID and verifies checksums.
tools/PackageValidator/PortablePdb.cs Portable PDB GUID extraction + checksum verification support.
tools/PackageValidator/NativeVersionReader.cs Reads native PE architecture and Win32 version-resource info (temp-file based).
tools/PackageValidator/HumanReporter.cs Human-readable console renderer for reports/findings/summary.
tools/PackageValidator/Json.cs Source-generated JSON serialization context/options for machine output.
tools/PackageValidator/tests/PackageValidator.Tests.csproj Test project wiring (net10.0) and tool project reference.
tools/PackageValidator/tests/Directory.Build.props Isolates build settings for the test directory.
tools/PackageValidator/tests/Directory.Packages.props Centralizes test package versions (xUnit, test SDK).
tools/PackageValidator/tests/VersionRangeTests.cs Unit tests for version-range evaluation behavior.
tools/PackageValidator/tests/VersionExpectationsTests.cs Unit tests for expectation parsing and validation integration.
tools/PackageValidator/tests/ValidatorTests.cs Unit tests for rules engine findings and batch dependency validation.
tools/PackageValidator/tests/BinaryClassifierTests.cs Unit tests for binary path classification.
tools/PackageValidator/tests/AssemblyInspectorTests.cs Unit tests for PKT computation and portable PDB helpers.

Comment thread tools/PackageValidator/NativeVersionReader.cs Outdated
Comment thread tools/PackageValidator/NativeVersionReader.cs Outdated
Comment thread tools/PackageValidator/src/Validator.cs
Comment thread tools/PackageValidator/VersionRange.cs Outdated
Comment thread tools/PackageValidator/PortablePdb.cs Outdated
A standalone .NET 10 CLI under tools/PackageValidator that inspects one or
more .nupkg files and validates their versions, signing, and symbols using
metadata-only reading (no assembly loading), so it works cross-platform and
on assemblies built for TFMs the host does not run.

Capabilities:
- Package id/version, dependency groups, and NuGet signature from the .nuspec.
- Per-assembly AssemblyVersion/FileVersion/InformationalVersion, culture,
  public key token, strong-name signing status, and target framework.
- Native binary Win32 version info and architecture.
- Binary classification (implementation/reference/satellite/native) so symbol
  coverage is judged only over implementation assemblies.
- Sibling .snupkg symbol matching by debug GUID with portable-PDB checksum
  verification, embedded-symbol detection, and orphan/mismatch reporting.
- Intrinsic validation rules emitting severity-tagged findings, plus optional
  --expect-package/file/assembly-version assertions for inter-package
  version-match validation.
- Batch/directory input, JSON or human output, --fail-on exit-code gating, and
  a free-form Notes help section.
- xUnit test project (41 tests).

Exploratory maintainer tooling; isolated from the main build (its own
Directory.Build.props / Directory.Packages.props).
Layout:
- Move sources to src/ and tests to test/ to match the PackageCompatibility
  tool; share one Directory.*.props; add global.json (xUnit v3 / MTP).
- Convert the test project to xUnit v3 (Exe + IsTestProject, xunit.v3).
- Wire BuildPackageValidator(Test)/TestPackageValidator (and the
  PackageCompatibility equivalents) into build.proj; BuildTools builds tests.

Review feedback (PR #4407):
- NativeVersionReader: handle UnauthorizedAccessException/SecurityException on
  temp-file staging and cleanup instead of letting them escape.
- Validator: flag a missing AssemblyFileVersion against --expect-file-version
  (and assembly version) instead of silently passing.
- VersionRange: follow SemVer 2.0 precedence (prerelease ordering, ignore
  build metadata) instead of dropping the prerelease/metadata suffix.
- PortablePdb: dispose the BinaryReader/MemoryStream in FindPdbIdOffset.

Adds regression tests (48 total, all passing).
@paulmedynski paulmedynski force-pushed the dev/paul/version-tool branch from 7a8dc98 to eebc229 Compare June 26, 2026 11:19
@paulmedynski paulmedynski added this to the 7.1.0-preview3 milestone Jun 26, 2026
@paulmedynski paulmedynski moved this from To triage to In progress in SqlClient Board Jun 26, 2026
@paulmedynski paulmedynski added the Area\Engineering Use this for issues that are targeted for changes in the 'eng' folder or build systems. label Jun 26, 2026
- Add XML docs, intent comments, and arrange/act/assert structure to the
  PackageValidator test classes, methods, and helpers.
- Add a /tools/ solution folder to Microsoft.Data.SqlClient.slnx containing the
  PackageCompatibility and PackageValidator src and test projects.
Copilot AI review requested due to automatic review settings June 26, 2026 12:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 3 comments.

Comment on lines +35 to +46
range = range.Trim();

// A bare version (no brackets) is a minimum-inclusive bound in NuGet semantics.
if (range[0] != '[' && range[0] != '(')
{
SemanticVersion? min = Parse(range);
return min is null ? null : Compare(target, min) >= 0;
}

bool minInclusive = range[0] == '[';
bool maxInclusive = range[^1] == ']';
string inner = range[1..^1];
Comment on lines +123 to +127
string[] parts = release.Split('.', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0)
{
return null;
}
Comment on lines +32 to +37
// Implementation assemblies live under lib/ or runtimes/<rid>/lib/.
if (normalized.StartsWith("lib/", StringComparison.OrdinalIgnoreCase)
|| normalized.Contains("/lib/", StringComparison.OrdinalIgnoreCase))
{
return BinaryKind.Implementation;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area\Engineering Use this for issues that are targeted for changes in the 'eng' folder or build systems.

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

2 participants