fix(dotnet): identify deps.json root project from dependency graph#10954
fix(dotnet): identify deps.json root project from dependency graph#10954jetersen wants to merge 6 commits into
Conversation
collectPackages ranged over the libraries map and kept the first library with type "project" as the root, so Go map iteration order decided which assembly was reported when multiple project libraries exist. Iterate sorted library keys so the selection is stable across runs.
1cc5415 to
c6cb3e1
Compare
DmitriyLewen
left a comment
There was a problem hiding this comment.
Hello @jetersen
Thanks for digging into this — the non-determinism you describe is real, and making the output reproducible is a goal worth having.
Before we merge, though, I'd like us to fix the underlying cause rather than only the symptom, because sorting the keys makes the choice stable but not correct.
The key thing to realize is that type: project does not mean "root".
A .deps.json legitimately contains several type: project libraries: the application itself plus every <ProjectReference> that is built from source in the same solution.
So "multiple project libraries" is a normal, common case for any multi-project solution — not a corner case — and the existing Warn("Multiple root projects found") is itself misleading.
type: project really means "built from source here" (as opposed to type: package, i.e. pulled from NuGet), and exactly one of those projects is the real application entry point.
That is why picking the lexicographically first project isn't enough.
It is deterministic, but the first project alphabetically has no relationship to which one is the entry point, so we can now stably mark the wrong library as root.
On top of that, the current continue silently drops every other type: project library from the inventory — but those are first-party assemblies that are ordinary direct/indirect dependencies of the app, and they should stay in the SBOM.
The direction I'd suggest: the real root is the type: project library that nothing else depends on in the targets graph (the top of the graph), and the other projects should stay in the inventory as ordinary dependencies rather than being dropped.
A deterministic fallback (e.g. the first project) still makes sense when there's no graph to look at.
The exact implementation is up to you.
As a test case, a small multi-project fixture makes the difference obvious.
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"Web/1.0.0": {
"dependencies": {
"Api": "1.0.0",
"Newtonsoft.Json": "13.0.3"
},
"runtime": {
"Web.dll": {}
}
},
"Api/1.0.0": {
"dependencies": {
"Data": "1.0.0"
},
"runtime": {
"Api.dll": {}
}
},
"Data/1.0.0": {
"runtime": {
"Data.dll": {}
}
},
"Newtonsoft.Json/13.0.3": {
"runtime": {
"lib/netstandard2.0/Newtonsoft.Json.dll": {
"assemblyVersion": "13.0.0.0",
"fileVersion": "13.0.3.27908"
}
}
}
}
},
"libraries": {
"Web/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Api/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Data/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Newtonsoft.Json/13.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
"path": "newtonsoft.json/13.0.3",
"hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
}
}
}
Consider a solution Web → Api → Data (plus a NuGet package), where all three of Web, Api, Data are type: project:
- with sorted keys,
Apiwins (first alphabetically) andWeb/Dataare dropped; - with the graph rule,
Webis correctly the root (nothing depends on it),Apibecomes direct,Dataindirect, and nothing is dropped.
But it's synthetic, though — it would be better to build a real minimal example (a tiny multi-project solution, dotnet publish, take its .deps.json) and use that in the tests, asserting the result through Parse (end to end), not only through collectPackages.
|
Ya, I was considering that approach but went with minimal implementation first. |
|
@DmitriyLewen let me know if this is what you were looking for or should I remove the sample csproj or the .gitattributes changes |
|
Description
A
.deps.jsoncan legitimately contain multipletype: projectlibraries: the application itself and each<ProjectReference>built from source. The parser previously treated the first project library as the root and dropped every other project library. Because Go map iteration is unordered, this could select a different root between scans, omit first-party dependencies from the inventory, and produce an incorrect dependency graph.This PR now:
targetsgraph as the project that no other target depends on;For a published application with the graph
Web -> Api -> Data, plus a directNewtonsoft.Jsondependency fromWeb, the parser now reports:Webas root;ApiandNewtonsoft.Jsonas direct dependencies; andDataas an indirect dependency.The regression test exercises
Parseend to end using a real.deps.jsonproduced bydotnet publish. The minimal project sources and regeneration instructions are retained with the fixture.Related issues
.deps.jsonwith multipletype: projectlibraries #10960Checklist