LibraryReference MSBuild package repo
Policies that keep the org coherent
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):
- Trigger / map import: Novolis.ProjectReferenceMode.props
- Intersect substitution: Novolis.ProjectReferenceMode.targets
- Generated map: Novolis.PackageToProject.props (
NovolisPackageProject= PackageId →ProjectPath)
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| PKOut 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).
| Piece | Choice |
|---|---|
| Repo | `d:\novolis\novolis-msbuild` |
| Package | `Novolis.MSBuild.LibraryReference` |
| Layout | Props/targets-only nupkg (`IncludeBuildOutput=false`, `DevelopmentDependency=true`) under `build/` + `buildTransitive/` |
| Versioning | Platform CalVer via `build/version.json` → `2026.1.1.{run}` on merge publish to GPR |
| Sources | nuget.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):
- Resolve effective path: item
ProjectPathmetadata, else%(LibraryProjectMap.ProjectPath)where mapIncludeequals PackageId. - If path non-empty,
Exists, and not self → emitProjectReferenceto that path; forward safe metadata (PrivateAssets,IncludeAssets,ExcludeAssets,ReferenceOutputAssembly,OutputItemType,Aliases,SetConfiguration/SetPlatformif present). - Else → emit
PackageReferencefor the PackageId:
- Version = item
Versionif set, else$(LibraryReferenceDefaultVersion)(default*). - If
ManagePackageVersionsCentrally == true: emit barePackageReference+PackageVersion(only when no existingPackageVersionfor that id, so CPM tables still win when present). - If not CPM: put
Versionon thePackageReference.
- Clear processed
LibraryReferenceitems (or mark expanded) so they are not double-applied.
Properties:
| Property | Default | Role |
|---|---|---|
| `LibraryReferenceDefaultVersion` | `*` | “latest” float when Version omitted |
| `LibraryReferenceEnabled` | `true` | Kill switch |
| `EnableFloatingVersions` / CPM float | consumer 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 definitionsbuild/Novolis.MSBuild.LibraryReference.targets—ExpandLibraryReferencestarget beforeCollectPackageReferences;ResolvePackageAssets;Restore;ResolvePackageFileConflictsbuildTransitive/*— 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/:
- Fixture provider + consumer csprojs (same-repo) proving: with
ProjectPathpresent →ProjectReference; with path missing/deleted →PackageReference(mock with a fake package id / skip restore assert via-getItem). - Map-only path:
LibraryProjectMapwithout itemProjectPath→ project when Exists. - CPM path:
ManagePackageVersionsCentrally=true→ emittedPackageVersionwhen falling back to package. - 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:
- Create repo from template; set
NovolisGitHubRepository=novolis-msbuild. build/version.jsonon platform line; sync viad:\novolis\novolis-governance\scripts\sync-version-props.ps1.- Thin workflows: PR / merge / release calling
novolis-workflows@main. nuget.config: nuget.org + GPRNovolis.*only.- 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. - Docs required by policy: README,
docs/getting-started.md,docs/design.md,docs/release.md. - Validate:
verify-nuget-only.ps1, merge → GPR2026.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
LibraryReferenceis allowed by policy (expands at build time; still no committed cross-repoProjectReference). - Extend verify-nuget-only.ps1 to ignore/allow
LibraryReference(and still forbid raw siblingProjectReferencein.csproj).
Do not rewrite existing Novolis PackageReference trees in this plan.
Relation to current ProjectRef mode
| ProjectRef mode | LibraryReference | |
|---|---|---|
| Committed item | `PackageReference` | `LibraryReference` |
| When project used | Mode on + map ∩ PackageReference ∩ Exists | Path/map Exists (always, if enabled) |
| When package used | Default / mode off | Path missing |
| Version | CPM `2026.1.*` on PackageReference | Item Version or default `*` |
| Map | Generated Novolis-only | Optional `LibraryProjectMap` (can be fed by Novolis map) |
ProjectRef mode remains the supported Novolis multi-repo workflow until a later migration adopts LibraryReference in csprojs.