Novolis Docs
novolis-governance / completed-plans/libraryreference_msbuild_repo_bbcf86b8.plan.md

LibraryReference MSBuild package repo

dotnetgovernancenovolis

name: LibraryReference MSBuild repo overview: Create a new novolis-msbuild repo that publishes Novolis.MSBuild.LibraryReference — an MSBuild props/targets package providing a single LibraryReference item that expands to ProjectReference when a mapped/explicit .csproj exists, otherwise to PackageReference with an explicit or default floating version. todos:

  • id: scaffold-repo

content: Scaffold d:\novolis\novolis-msbuild from template-dotnet (workflows, version.json, nuget.config, strip local-feed leftovers) status: completed

  • id: pack-props-targets

content: Implement Novolis.MSBuild.LibraryReference packable project + ExpandLibraryReferences props/targets (ProjectPath, LibraryProjectMap, CPM, default Version=*) status: completed

  • id: tests-smoke

content: Add fixture provider/consumer tests asserting ProjectReference vs PackageReference via dotnet msbuild -getItem status: completed

  • id: docs-publish

content: Package README + repo docs; wire merge publish; register in PackageToProject map status: completed

  • id: governance-bridge

content: Optional governance adapter NovolisPackageProject→LibraryProjectMap + verify-nuget-only allowance for LibraryReference status: completed isProject: false


Context (today’s ProjectRef mode)

Novolis already rewrites committed PackageReference → sibling ProjectReference when enabled, via pure props/targets (no custom task):

That model keeps .csproj PackageReference-only and only substitutes when PackageReference ∩ map ∩ Exists. LibraryReference inverts the declaration: the committed item is the dependency; expansion invents Project vs Package at evaluate time.

flowchart LR
  LR[LibraryReference PackageId]
  Map{ProjectPath set or map hit and Exists}
  PR[ProjectReference]
  PK[PackageReference plus Version]
  LR --> Map
  Map -->|yes| PR
  Map -->|no| PK

Out of scope for this plan: mass-migrating Novolis repos off PackageReference / retiring ProjectRef mode. This plan ships the package + tests + a thin optional governance bridge so the existing map can feed LibraryProjectMap.

Repo: `novolis-msbuild`

Scaffold from novolis-template-dotnet under Novolis-Platform (standard CalVer + thin workflows → novolis-workflows).

PieceChoice
Repo`d:\novolis\novolis-msbuild`
Package`Novolis.MSBuild.LibraryReference`
LayoutProps/targets-only nupkg (`IncludeBuildOutput=false`, `DevelopmentDependency=true`) under `build/` + `buildTransitive/`
VersioningPlatform CalVer via `build/version.json` → `2026.1.1.{run}` on merge publish to GPR
Sourcesnuget.org + GitHub Packages only (no local feed; drop template `pack-local` / `NovolisLocalFeed` leftovers)

Packable project pattern:

<!-- src/Novolis.MSBuild.LibraryReference/Novolis.MSBuild.LibraryReference.csproj -->
IncludeBuildOutput=false, SuppressDependenciesWhenPacking=true,
DevelopmentDependency=true, IsPackable=true
None Include="build/**" Pack → PackagePath=build/
None Include="buildTransitive/**" Pack → PackagePath=buildTransitive/

Consumer import: PackageReference with PrivateAssets=all (typically in Directory.Build.props / CPM), so restore auto-imports targets without shipping the tooling package as a library dependency.

`LibraryReference` contract

Committed usage:

<ItemGroup>
  <LibraryReference Include="Novolis.Math.Geometry"
                    Version="2026.1.*"
                    ProjectPath="$(NovolisWorkspaceRoot)novolis-math\src\Novolis.Math.Geometry\Novolis.Math.Geometry.csproj" />
  <!-- Version omitted → $(LibraryReferenceDefaultVersion), default "*" -->
  <LibraryReference Include="Some.Other.Lib" />
</ItemGroup>

Resolution (single target, same BeforeTargets list as ProjectRef mode):

  1. Resolve effective path: item ProjectPath metadata, else %(LibraryProjectMap.ProjectPath) where map Include equals PackageId.
  2. If path non-empty, Exists, and not self → emit ProjectReference to that path; forward safe metadata (PrivateAssets, IncludeAssets, ExcludeAssets, ReferenceOutputAssembly, OutputItemType, Aliases, SetConfiguration/SetPlatform if present).
  3. Else → emit PackageReference for the PackageId:
  • Version = item Version if set, else $(LibraryReferenceDefaultVersion) (default *).
  • If ManagePackageVersionsCentrally == true: emit bare PackageReference + PackageVersion (only when no existing PackageVersion for that id, so CPM tables still win when present).
  • If not CPM: put Version on the PackageReference.
  1. Clear processed LibraryReference items (or mark expanded) so they are not double-applied.

Properties:

PropertyDefaultRole
`LibraryReferenceDefaultVersion``*`“latest” float when Version omitted
`LibraryReferenceEnabled``true`Kill switch
`EnableFloatingVersions` / CPM floatconsumer responsibility`*` / `2026.1.*` need floating versions enabled (Novolis already sets `CentralPackageFloatingVersionsEnabled`)

Map item (optional):

<LibraryProjectMap Include="Novolis.Math.Geometry">
  <ProjectPath>$(NovolisWorkspaceRoot)novolis-math\...\Novolis.Math.Geometry.csproj</ProjectPath>
</LibraryProjectMap>

No filesystem crawl. Discovery is explicit path and/or map (same reliability model as today’s generated map).

Implementation files (inside the package)

  • build/Novolis.MSBuild.LibraryReference.props — defaults + empty item definitions
  • build/Novolis.MSBuild.LibraryReference.targetsExpandLibraryReferences target before CollectPackageReferences;ResolvePackageAssets;Restore;ResolvePackageFileConflicts
  • buildTransitive/* — same imports so transitive PrivateAssets patterns still load when appropriate
  • Package README documenting CPM vs non-CPM, non-transitive ProjectReference caveat (same as ProjectRef mode), and metadata forwarding

Keep logic pure MSBuild ItemGroup surgery (match existing Novolis style); no C# task DLL unless metadata join proves impossible (map join by PackageId is doable with Remove / batching like Novolis.ProjectReferenceMode.targets).

Tests

Under tests/:

  1. Fixture provider + consumer csprojs (same-repo) proving: with ProjectPath present → ProjectReference; with path missing/deleted → PackageReference (mock with a fake package id / skip restore assert via -getItem).
  2. Map-only path: LibraryProjectMap without item ProjectPath → project when Exists.
  3. CPM path: ManagePackageVersionsCentrally=true → emitted PackageVersion when falling back to package.
  4. Default version: omitted Version → *.

Drive with dotnet msbuild -getItem:ProjectReference / -getItem:PackageReference (same smoke style as verify-project-ref-mode.ps1).

Publish / repo infra checklist

Reuse template + governance scripts:

  1. Create repo from template; set NovolisGitHubRepository = novolis-msbuild.
  2. build/version.json on platform line; sync via d:\novolis\novolis-governance\scripts\sync-version-props.ps1.
  3. Thin workflows: PR / merge / release calling novolis-workflows @main.
  4. nuget.config: nuget.org + GPR Novolis.* only.
  5. Register PackageId in platform map after first packable project exists (Generate-Platform-Slnx.ps1) so meta-solution consumers can ProjectRef the MSBuild package itself while iterating.
  6. Docs required by policy: README, docs/getting-started.md, docs/design.md, docs/release.md.
  7. Validate: verify-nuget-only.ps1, merge → GPR 2026.1.1.{run}.

Local iteration before first GPR publish: build consumer fixtures with -p:NovolisUseProjectReferences=true against sibling source, or ProjectReference the packable project from test fixtures only (same-repo).

Thin Novolis bridge (governance, small follow-on in same effort)

After the package exists (or via ProjectRef to the new repo in meta mode):

  • Add optional import/adapter in governance that copies @(NovolisPackageProject)@(LibraryProjectMap) when the LibraryReference package targets are present.
  • Document that committed LibraryReference is allowed by policy (expands at build time; still no committed cross-repo ProjectReference).
  • Extend verify-nuget-only.ps1 to ignore/allow LibraryReference (and still forbid raw sibling ProjectReference in .csproj).

Do not rewrite existing Novolis PackageReference trees in this plan.

Relation to current ProjectRef mode

ProjectRef modeLibraryReference
Committed item`PackageReference``LibraryReference`
When project usedMode on + map ∩ PackageReference ∩ ExistsPath/map Exists (always, if enabled)
When package usedDefault / mode offPath missing
VersionCPM `2026.1.*` on PackageReferenceItem Version or default `*`
MapGenerated Novolis-onlyOptional `LibraryProjectMap` (can be fed by Novolis map)

ProjectRef mode remains the supported Novolis multi-repo workflow until a later migration adopts LibraryReference in csprojs.