Skip to content

Add migration inventory and public API baseline tooling (Phase 0) - #3

Merged
Redth merged 7 commits into
mainfrom
redth-migration-inventory-tooling
Aug 27, 2026
Merged

Redth merged 7 commits into
mainfrom
redth-migration-inventory-tooling

Conversation

@Redth

@Redth Redth commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 0, independent-and-reviewable tooling for the dotnet/maui -> Redth/Maui.Tizen extraction. No runtime/package scaffold, no git-history import -- just deterministic inventory + API baseline tooling and the checked-in artifacts it produces, designed to merge cleanly alongside the separate foundation/history-import branch (coordinated live with that session on eng/baselines.json, eng/manifests/, eng/api-baselines/, and eng/tools/ paths/shapes).

What's here

  • eng/tools/SourceInventory — walks a dotnet/maui checkout and classifies every Tizen-specific-path file and every shared file containing a Tizen conditional-compile branch (#if TIZEN) or platform-identity reference (DevicePlatform.Tizen, etc.), grouped by component (Core handlers, Core platform, Controls, Essentials, Blazor, Maps, Graphics, Resizetizer, templates/tests) with move/rename/rebuild/keep-upstream/upstream-cleanup-candidate/exclude disposition hints. Pure file-system + regex scan — no build, no Tizen workload.
  • eng/tools/ApiDump — reads a managed assembly's public API surface purely via System.Reflection.Metadata (never loads/executes it) — no Tizen workload needed. Used against the last-published MAUI 9.0.120 net9.0-tizen7.0 assemblies.
  • eng/scripts/*.ps1 — cross-platform pwsh orchestration to regenerate each artifact set and an offline validation entrypoint.
  • eng/manifests/source-disposition.{json,schema.json,summary.md} and eng/api-baselines/{published-9.0.120,net11-publicapi-inputs}/** — generated, checked-in artifacts recording source URLs/commits/versions.
  • docs/type-collision-classification.md — rules for when an extracted Tizen type may keep its Microsoft.Maui.* name vs. requiring a Tizen-prefixed standalone name, per the naming policy pinned in eng/baselines.json.
  • tests/Migration.Tooling.Tests — offline xUnit checks: JSON-Schema validation of the manifest, and re-hashing every generated artifact against its recorded checksum and against eng/baselines.json, so a stale/hand-edited artifact fails CI without network or a Tizen workload.

Pinned baselines

  • Source (net11) baseline: dotnet/maui net11.0 @ ee4d06cde6b49e297631b08426a33fb34f3152ef
  • Minimum API floor: PR #36657 @ 0b3bb76d2dd68d76b7c1302f43a76270d5949564 (confirmed ancestor of the source baseline)
  • Last-published Tizen baseline: NuGet 9.0.120 @ source commit c1f4f7d879f6126029009902289efd6a4bb1bda9
  • First coherent net11 preview package: 11.0.0-preview.7.26418.3 from the dnceng/dotnet11 public feed, repository commit 0b3bb76d2d... (confirms Tizen has already been fully dropped from net11 shipping packages — no net11.0-tizen* TFM exists in any of Core/Controls.Core/Controls.Xaml/Controls.Maps/Essentials/Graphics at this version, and Microsoft.Maui.Controls.Compatibility isn't published at all)

Generated counts (net11 primary + 9.0.120 legacy-recovery scan)

  • 542 total inventory entries — 401 Tizen-specific-path, 141 shared-conditional
  • Disposition: 303 move, 97 keep-upstream, 87 exclude (present at 9.0.120 only — e.g. the legacy top-level src/Compatibility/** renderer stack and dropped Tizen templates), 44 upstream-cleanup-candidate, 11 rebuild (legacy Compatibility-era renderers)
  • 223 files flagged collisionReviewNeeded for the type-collision doc's review process
  • 7 NuGet packages dumped for the 9.0.120 API baseline (1,688 public types total); 18 PublicAPI.{Shipped,Unshipped}.txt files collected across 9 net11 projects

Data-quality note

Initial investigation surfaced a naming collision worth flagging for reviewers: src/Controls/src/Core/Compatibility/** (an in-tree legacy renderer shim, still present and unchanged on net11.0) is distinct from the separate top-level src/Compatibility/** project (the old Xamarin.Forms.Platform.Compatibility stack, fully removed before net11.0). Both are captured correctly in the manifest with different dispositions (rebuild vs. exclude), but it's an easy mix-up when reasoning about "Compatibility" by name alone.

Validation

$ ./eng/scripts/validate-migration-artifacts.ps1
...
Passed! - Failed: 0, Passed: 17, Skipped: 0, Total: 17
All migration tooling checks passed.

Both tools build cleanly (dotnet build), and the full artifact set was regenerated from a clean checkout via the three eng/scripts/generate-*/fetch-* scripts to confirm reproducibility before committing.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

@Redth
Redth force-pushed the redth-migration-inventory-tooling branch from 96281a5 to b7c2a93 Compare August 26, 2026 19:38
@Redth
Redth changed the base branch from main to redth-tizen-foundation-import August 26, 2026 19:38
@Redth
Redth force-pushed the redth-migration-inventory-tooling branch from b7c2a93 to f2895a5 Compare August 26, 2026 19:42
Redth added a commit that referenced this pull request Aug 26, 2026
Found while trial-merging PR #3 (inventory tooling) onto this branch: its three
new projects - ApiDump, SourceInventory and Migration.Tooling.Tests - target
net10.0, which the repository does not support (eng/baselines.json >
policy.minimumDotNet = 11.0, supportsDotNet10 = false).

The failure mode is subtle enough to be worth writing down. A net10.0 project
BUILDS fine with the pinned .NET 11 SDK, so nothing complains at compile time.
Its testhost then fails at run time:

  You must install or update .NET to run this application.
  Framework: 'Microsoft.NETCore.App', version '10.0.0'
  The following frameworks were found:
    11.0.0-preview.7.26381.103

...unless the machine also happens to carry a .NET 10 runtime - which GitHub's
hosted images do. So it passes in CI and fails for anyone whose environment
matches global.json exactly. Green for the wrong reason.

The existing NoProjectFallsBackToANeutralTargetFramework test only scanned src/,
so it could not have caught this. The new test walks every .csproj in the
repository, parses each TFM, and compares against the floor declared in
baselines.json - so the policy is enforced from one source of truth rather than
restated per directory.

Tests: 33 -> 34.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth
Redth force-pushed the redth-migration-inventory-tooling branch 3 times, most recently from c817eac to e28428b Compare August 27, 2026 00:07
Redth and others added 5 commits August 26, 2026 20:09
Adds deterministic, offline-runnable tooling for the dotnet/maui -> Maui.Tizen
extraction, coordinated with the foundation/import branch's eng/baselines.json,
eng/manifests, and eng/api-baselines layout:

- eng/tools/SourceInventory: walks a dotnet/maui source checkout and classifies
  every Tizen-specific-path file and every shared file with a Tizen conditional-
  compile branch or platform-identity reference, grouping by component (Core
  handlers/platform, Controls, Essentials, Blazor, Maps, Graphics, Resizetizer,
  templates/tests) with initial move/rename/rebuild/keep-upstream/upstream-
  cleanup-candidate/exclude disposition hints. Also recovers Tizen-named files
  present only in the last-published (9.0.120) tree (e.g. the legacy top-level
  src/Compatibility renderer stack, removed upstream before net11.0).

- eng/tools/ApiDump: reads a managed assembly's public API surface purely via
  System.Reflection.Metadata (never loads/executes it), so no Tizen workload is
  required. Used to capture the last-published MAUI 9.0.120 net9.0-tizen7.0
  assemblies' public type/member surface.

- eng/scripts: cross-platform pwsh orchestration to (re)generate the source
  inventory, the 9.0.120 API baseline, and the net11 net-tizen
  PublicAPI.Shipped/Unshipped.txt inputs, plus an offline validation entrypoint.

- eng/manifests/source-disposition schema/summary and
  eng/api-baselines/published-9.0.120 and net11-publicapi-inputs are generated,
  checked-in artifacts with recorded source URLs/commits/versions.

- docs/type-collision-classification.md documents when an extracted Tizen type
  may preserve its Microsoft.Maui.* name versus requiring a Tizen-prefixed
  standalone name, per the naming policy pinned in eng/baselines.json.

- tests/Migration.Tooling.Tests: offline xUnit checks that schema-validate the
  manifest and re-hash every generated artifact against its recorded checksum
  and against eng/baselines.json, so a stale or hand-edited artifact fails CI
  without needing network access or a Tizen workload.

Source baseline: dotnet/maui net11.0 at ee4d06cde6b49e297631b08426a33fb34f3152ef
(contains PR 36657, 0b3bb76d2dd68d76b7c1302f43a76270d5949564, the mandated
minimum API floor). Published baseline: 9.0.120 at c1f4f7d879f6126029009902289efd6a4bb1bda9.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Per coordinator direction, adopts the foundation branch's (redth-tizen-foundation-import)
canonical eng/manifests/source-disposition.schema.json, eng/baselines.json, and
eng/api-baselines/{net9.0-tizen7.0,net11.0-publicapi} directory names as authoritative,
replacing this branch's earlier parallel schema/paths:

- eng/tools/SourceInventory now emits the foundation's entry shape exactly: sourceRef
  (sourceBaseline|behaviorBaseline|both), area (freeform upstream area string), package
  (the 10-value Maui.Tizen.* enum), kind (tizen-specific|shared-conditional|project|asset),
  disposition (move|rename|rebuild|keep-upstream|exclude|pending-audit), targetPath (computed
  from eng/import/normalize-layout.sh's move_children mapping, required whenever disposition
  implies a relocation), targetNamespace (only ever a preserved Microsoft.Maui.* namespace, per
  the schema's pattern constraint), collisionRisk, and notes -- validated end-to-end against
  the schema for all 542 generated entries.

- Added a "pending-audit" disposition to the schema for the legacy top-level
  src/Compatibility/** stack (present at 9.0.120 only): per the coordinator's ruling, the
  question is not "is it on net11.0" (it isn't) but "does a net11 Tizen handler depend on
  implementation that exists only here", which this generator cannot answer -- so the drop
  is recorded as a reviewed conclusion to reach, not a default the generator asserted.

- Renamed the API baseline output directories to the committed eng/api-baselines/
  net9.0-tizen7.0/ and net11.0-publicapi/ (previously published-9.0.120/ and
  net11-publicapi-inputs/ on this branch), matching eng/api-baselines/README.md's contract.

- Removed docs/type-collision-classification.md and eng/README.md: superseded by the
  foundation's docs/architecture.md (same collision rules, already landed) and
  eng/manifests/README.md + eng/api-baselines/README.md (same layout/ownership docs).

- Wired eng/tools/ApiDump, eng/tools/SourceInventory, and tests/Migration.Tooling.Tests into
  eng/build-workload-free.sh's workload-free build/test lane and into Maui.Tizen.slnx, so CI
  actually runs the 16 offline migration-tooling tests alongside the existing 20 repository
  invariant tests. Verified with a full ./eng/build-workload-free.sh run (all checks pass).

- Added JsonSchema.Net and coverlet.collector to Directory.Packages.props (central package
  management) for tests/Migration.Tooling.Tests; that project now follows the repo's other
  test project conventions (no inline package versions, GenerateDocumentationFile=false).

Regenerated all three artifact sets from the pinned baselines and re-validated: 542
source-disposition entries (303 move, 107 rebuild, 70 pending-audit, 45 keep-upstream,
17 exclude) schema-valid; 7-package/1,688-type net9.0-tizen7.0 API dump; 18
PublicAPI.{Shipped,Unshipped}.txt files across 9 net11 projects.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ity, script hardening

Per the coordinator's scope arbitration and independent review:

- Reverted the source-disposition schema to be byte-identical to the foundation's
  canonical eng/manifests/source-disposition.schema.json (removed the earlier
  "pending-audit" addition). It is treated as immutable input from here on;
  tests/Migration.Tooling.Tests/SchemaImmutabilityTests.cs asserts the disposition/kind
  enums, the shared-conditional move/rename restriction, and every manifest entry's
  disposition stay within the schema's own five/four-value enums.
- Reverted the legacy top-level src/Compatibility/** stack (absent from net11.0) to
  disposition "exclude" -- not "rebuild", not a new disposition value. Split its "area"
  into Compatibility.Core/.Material/.Maps (48/17/5 files) so the audit note is actionable
  per sub-stack without changing the disposition itself. src/Controls/src/Core/
  Compatibility/** (still present on net11.0) remains the separate "rebuild" candidate;
  both distinctions are now covered by dedicated tests.
- eng/tools/SourceInventory: added a duplicate-path hard rejection (non-zero exit, not
  just a downstream test), an EntryProvenance.historicalPaths field populated from
  eng/import/tizen-paths.txt's documented historical layouts, and removed the wall-clock
  generatedUtc timestamp (only emitted when SOURCE_DATE_EPOCH is set) so two runs against
  the same inputs now produce byte-identical output -- verified by running it twice and
  diffing.
- eng/tools/ApiDump: nested types are now qualified by their full declaring-type chain
  (e.g. "Button+VisualStateManagerOverride") instead of a bare simple name that collided
  across unrelated types; delegates record their Invoke parameter/return signature;
  enums record their underlying primitive type and each member's numeric constant value.
- eng/scripts/Get-MauiSourceSnapshot.ps1: atomic extraction via a staging directory (a
  killed run can never leave a half-extracted snapshot in place), always clears -OutDir
  before writing, and writes a .mt-snapshot.json provenance marker (repo, ref, file count,
  a streamed SHA-256 tree hash) that every consuming script now validates a caller-supplied
  -PrimaryRoot/-LegacyRoot against, rejecting an unverified or mismatched directory instead
  of silently scanning it. Fixed a Move-Item destination-exists bug that was nesting the
  extracted tree one level too deep.
- eng/scripts/generate-api-baseline.ps1: nupkg downloads are hash-pinned to a sidecar file
  on first download and re-verified against it on every reuse (an arbitrary nonempty cache
  file is never trusted); each package's NuGet signature is checked for presence and
  decoded with SignedCms for structural (non-corrupt) validity; the expected Tizen assembly
  filename per package is asserted exactly rather than "any .dll present"; output is fully
  cleared before regeneration; and package/assembly/output SHA-256 hashes are all recorded
  in manifest.json. Verified byte-identical output across two runs.
- Removed the remaining wall-clock timestamp from fetch-net11-publicapi-inputs.ps1's
  manifest for the same reproducibility reason.
- Rebased onto the foundation branch's current tip (26d06e2): resolved the Maui.Tizen.slnx
  conflict (both PackageGraphProbe and Migration.Tooling.Tests belong in /tests/), moved
  the migration-tooling-tests invocation inside build-workload-free.sh's BUILD_OK guard so
  it can no longer run --no-build after a failed build, and retargeted all three of this
  branch's own projects (ApiDump, SourceInventory, Migration.Tooling.Tests) from net10.0 to
  net11.0 to satisfy the repository's own .NET 11 floor (eng/baselines.json
  policy.minimumDotNet) and the new NoProjectTargetsBelowTheDotNetFloor invariant test.

Regenerated all three artifact sets from verified source snapshots and re-validated:
542 source-disposition entries (303 move, 107 rebuild, 87 exclude, 45 keep-upstream), all
schema-valid; 7-package/1,688-type net9.0-tizen7.0 API dump, every package signed and
hash-pinned; 18 PublicAPI.{Shipped,Unshipped}.txt files across 9 net11 projects. Full
./eng/build-workload-free.sh passes end to end.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…pmentPackageBaseline bump

The coordinator is upgrading eng/baselines.json's source.developmentPackageBaseline to
11.0.0-preview.7.26426.4 @ bedd1b18b7682193e05b47267509cec8c49c6853 (the dnceng dotnet11
feed), while the source import baseline stays pinned to ee4d06 (no re-import). The three
gesture/badge API commits recorded in source.sourceBaseline.knownGapAfterThisPin
(#37420, #37671, #37755) are now available via the published package, resolving what was
previously an API-surface-only gap between the source import and current net11.0.

Development_package_baseline_shares_the_required_ancestor_commit asserted strict equality
between developmentPackageBaseline.nuspecRepositoryCommit and requiredAncestor.commit,
which is exactly the invariant this intentional bump breaks by design: the package
baseline is meant to move forward past the source-import pin over time.

Replaced it with Development_package_baseline_commit_is_a_documented_ref, which accepts
either the required-ancestor commit or the commit already recorded as
knownGapAfterThisPin.observedAgainst -- i.e. any ref this manifest already explains, not
an exact match. An arbitrary, undocumented commit still fails (verified locally with a
deadbeef sentinel before restoring eng/baselines.json).

Verified against both the current eng/baselines.json and a locally-simulated post-bump
version (version 11.0.0-preview.7.26426.4, commit bedd1b18b7...) -- confirmed via a real
NuGet download that this version's nuspec repository commit is exactly
bedd1b18b7682193e05b47267509cec8c49c6853, matches across Microsoft.Maui.Core and
Microsoft.Maui.Controls.Core, and still carries no Tizen TFM. Does not touch
eng/baselines.json itself, Directory.Packages.props, or docs/migration.md/PROVENANCE.md
(foundation-owned) -- only this repository's own test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…eal signature verification

1. Snapshot provenance (eng/scripts/Get-MauiSourceSnapshot.ps1 and its consumers)

Test-SnapshotMarker/Assert-VerifiedSnapshot previously only compared the .mt-snapshot.json
marker's claimed repository/ref against what was requested -- it never recomputed the tree
hash from the files actually on disk, so a marker claiming the right repo/ref next to a
directory that had since had a file added, removed, or modified would be silently accepted.

Extracted the marker read/write/verify logic into eng/scripts/lib/Snapshot.ps1
(Get-SnapshotTreeHash, New-SnapshotMarker, Test-SnapshotIntegrity), and made
Test-SnapshotIntegrity ALWAYS recompute the tree hash from the current file system state and
compare it against the marker before reporting ok. Get-MauiSourceSnapshot.ps1 (cache-hit
check), generate-source-inventory.ps1 (Assert-VerifiedSnapshot), and
fetch-net11-publicapi-inputs.ps1 (its inline caller-supplied-root check) all now call this one
shared function, so there is a single place this guarantee is implemented rather than three
copies that can drift.

Added eng/tests/test-snapshot-verification.ps1: offline regression tests (synthetic
directories, no network) covering a fresh snapshot verifying successfully, a missing marker,
a wrong repo/ref, a file MODIFIED after the marker was written, a file ADDED after the marker
was written, a file DELETED after the marker was written, and a determinism check (two
independently-created snapshots of identical content produce the same tree hash). Wired into
eng/build-workload-free.sh alongside the existing test-workload-detection.sh regression lane.

2. NuGet authenticity (eng/scripts/generate-api-baseline.ps1)

The previous Test-PackageSignature called System.Security.Cryptography.Pkcs.SignedCms.Check
Signature() on the isolated .signature.p7s zip entry in isolation. That only proves the
signature blob itself is an internally well-formed, self-consistent PKCS#7 structure -- it
does not recompute a hash over the package's actual current content and compare it to the
hash embedded inside the signature, so a package modified after signing (while its original,
untouched .signature.p7s entry is left in place) still "passes". Verified this concretely: a
package with one byte flipped in a non-signature entry's compressed data (structure otherwise
untouched) passes a bare SignedCms.CheckSignature() call but is correctly rejected (NU3008)
by NuGet's own IntegrityVerificationProvider.

Added eng/tools/PackageVerify: a small tool that performs REAL NuGet package signature
verification via NuGet.Packaging.Signing (IntegrityVerificationProvider +
SignatureTrustAndValidityVerificationProvider), mirroring what `dotnet nuget verify --all`
does, since the `dotnet nuget verify` CLI itself was unusable in this environment's SDK
install. Prints a JSON verification result (isSigned, signatureType, isValid, errors,
warnings) that generate-api-baseline.ps1 parses.

Replaced trust-on-first-use entirely with a checked-in trust anchor,
eng/api-baselines/net9.0-tizen7.0-package-trust.json: every one of the 7 packages
generate-api-baseline.ps1 downloads must already have a pinned SHA-256 there (established
once, under review, by downloading each package fresh from nuget.org and independently
verifying isSigned=true/isValid=true via PackageVerify before recording its hash) or the
script refuses to run at all. A downloaded or cached file whose SHA-256 does not match its
pin is now a hard failure regardless of whether its signature still verifies -- the local
.nupkg cache is purely a performance shortcut, never itself a trust source. Verified all three
scenarios directly: cold cache (fresh download, hash matches pin), warm cache (reused file,
hash re-verified against pin before reuse), and a tampered cached file (hash mismatch,
generation refuses to proceed).

Added tests/Migration.Tooling.Tests/PackageVerifyTests.cs, which runs the REAL built
PackageVerify tool (not a reimplementation of its logic) against an already-restored, real
signed NuGet package (NuGet.Packaging itself, resolved dynamically from Directory.Packages.props
so the version never drifts -- no new network dependency, since restore already fetched it)
and a byte-tampered copy created with the exact original-signature-intact technique described
above, asserting the original verifies and the tampered copy fails with NU3008.
tests/Migration.Tooling.Tests/PackageTrustAnchorTests.cs checks the trust anchor's structure
(exactly the 7 expected packages, valid-looking hashes, no duplicates). Updated
PublishedApiBaselineTests.cs for the renamed/added manifest.json fields and to cross-check
every recorded hash against the trust anchor.

Regenerated eng/api-baselines/net9.0-tizen7.0 end to end (cold cache, warm cache, and a
simulated tamper case) -- every per-assembly API dump JSON file is byte-identical to what was
already committed; only manifest.json changed, to add the new pinnedNupkgSha256/signatureType/
signatureIntegrityAndTrustValid fields the strengthened verification now records. Ran
./eng/build-workload-free.sh end to end: PackageVerify and the two new offline test files are
wired into the CI lane, all local checks pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth
Redth force-pushed the redth-migration-inventory-tooling branch from e28428b to 89833f6 Compare August 27, 2026 00:10
@Redth
Redth changed the base branch from redth-tizen-foundation-import to main August 27, 2026 00:10
Redth and others added 2 commits August 26, 2026 20:20
…d of newline delimiter

Independent security review of the two hardening fixes came back clean, but flagged one
low-priority defense-in-depth note: Get-SnapshotTreeHash's prior encoding concatenated each
file as "relativePath\n<content>" with no length prefix. Since POSIX filesystems legally
permit a literal newline byte inside a filename, this delimiter-based scheme was not strictly
injective -- in principle, two different (path, content) partitions of the same underlying
byte stream could produce identical concatenations and therefore identical hashes.

Replaced the encoding with explicit 8-byte length prefixes for both the path and the content
before their bytes, making the full file-list encoding unambiguous regardless of what bytes
appear in any path or file content, with no assumption about delimiter-safety.

Added a regression test to eng/tests/test-snapshot-verification.ps1 constructing the exact
adversarial case a delimiter-based scheme would be vulnerable to (a single file named 'a<LF>b'
with content 'c', versus two files 'a' with content 'b' and 'b' with content 'c' -- both of
which concatenate to the same 'a\nb\nc' byte stream under the old scheme) and asserting the
two trees now hash differently.

This changes the tree-hash algorithm, but since .mt-snapshot.json markers are generated at
runtime into temporary cache directories and never committed to source control, no checked-in
artifact is affected -- verified via a full re-run of ./eng/build-workload-free.sh (all
checks pass, including all 8 snapshot verification regression tests).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth
Redth merged commit 39afe7f into main Aug 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant