merge multiple targets for the same dotnet package (#3869)

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-05-08 11:28:08 -04:00 committed by GitHub
parent 00c4a4e72a
commit 1574fb20ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 201 additions and 27 deletions

View File

@ -149,7 +149,12 @@ func isRuntimePackageLocation(loc file.Location) (string, bool) {
func partitionPEs(depJsons []logicalDepsJSON, peFiles []logicalPE) ([]logicalDepsJSON, []logicalPE, []logicalDepsJSON) { func partitionPEs(depJsons []logicalDepsJSON, peFiles []logicalPE) ([]logicalDepsJSON, []logicalPE, []logicalDepsJSON) {
// sort deps.json paths from longest to shortest. This is so we are processing the most specific match first. // sort deps.json paths from longest to shortest. This is so we are processing the most specific match first.
sort.Slice(depJsons, func(i, j int) bool { sort.Slice(depJsons, func(i, j int) bool {
return len(depJsons[i].Location.RealPath) > len(depJsons[j].Location.RealPath) return depJsons[i].Location.RealPath > depJsons[j].Location.RealPath
})
// we should be processing PE files in a stable order
sort.Slice(peFiles, func(i, j int) bool {
return peFiles[i].Location.RealPath > peFiles[j].Location.RealPath
}) })
peFilesByPath := make(map[file.Coordinates][]logicalPE) peFilesByPath := make(map[file.Coordinates][]logicalPE)
@ -321,8 +326,7 @@ func relationshipsFromLogicalDepsJSON(doc logicalDepsJSON, pkgMap map[string]pkg
if lp.Targets == nil { if lp.Targets == nil {
continue continue
} }
for depName, depVersion := range lp.Targets.Dependencies { for _, depNameVersion := range lp.dependencyNameVersions() {
depNameVersion := createNameAndVersion(depName, depVersion)
thisPkg, ok := pkgMap[lp.NameVersion] thisPkg, ok := pkgMap[lp.NameVersion]
if !ok { if !ok {
continue continue
@ -371,8 +375,7 @@ func findNearestDependencyPackages(skippedDep logicalDepsJSONPackage, pkgMap map
processed.Add(skippedDep.NameVersion) processed.Add(skippedDep.NameVersion)
for depName, depVersion := range skippedDep.Targets.Dependencies { for _, depNameVersion := range skippedDep.dependencyNameVersions() {
depNameVersion := createNameAndVersion(depName, depVersion)
depPkg, ok := pkgMap[depNameVersion] depPkg, ok := pkgMap[depNameVersion]
if !ok { if !ok {
skippedDepPkg, ok := skipped[depNameVersion] skippedDepPkg, ok := skipped[depNameVersion]

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"regexp" "regexp"
"sort"
"strings" "strings"
"github.com/scylladb/go-set/strset" "github.com/scylladb/go-set/strset"
@ -66,7 +67,11 @@ func (t depsTarget) resourcePaths() map[string]string {
func (t depsTarget) runtimePaths() map[string]string { func (t depsTarget) runtimePaths() map[string]string {
result := make(map[string]string) result := make(map[string]string)
for path := range t.Runtime { for path := range t.Runtime {
result[trimLibPrefix(path)] = path trimmedPath := trimLibPrefix(path)
if _, exists := result[trimmedPath]; exists {
continue
}
result[trimmedPath] = path
} }
return result return result
} }
@ -82,14 +87,14 @@ type depsLibrary struct {
// Note: this is not a real construct of the deps.json, just a useful reorganization of the data for downstream processing. // Note: this is not a real construct of the deps.json, just a useful reorganization of the data for downstream processing.
type logicalDepsJSONPackage struct { type logicalDepsJSONPackage struct {
NameVersion string NameVersion string
Targets *depsTarget Targets []depsTarget
Library *depsLibrary Library *depsLibrary
// anyChildClaimsDLLs is a flag that indicates if any of the children of this package claim a DLL associated with them in the deps.json. // AnyChildClaimsDLLs is a flag that indicates if any of the children of this package claim a DLL associated with them in the deps.json.
anyChildClaimsDLLs bool AnyChildClaimsDLLs bool
// anyChildHasDLLs is a flag that indicates if any of the children of this package have a DLL associated with them (found on disk). // AnyChildHasDLLs is a flag that indicates if any of the children of this package have a DLL associated with them (found on disk).
anyChildHasDLLs bool AnyChildHasDLLs bool
// RuntimePathsByRelativeDLLPath is a map of the relative path to the DLL relative to the deps.json file // RuntimePathsByRelativeDLLPath is a map of the relative path to the DLL relative to the deps.json file
// to the target path as described in the deps.json target entry under "runtime". // to the target path as described in the deps.json target entry under "runtime".
@ -103,7 +108,7 @@ type logicalDepsJSONPackage struct {
// to the target path as described in the deps.json target entry under "compile". // to the target path as described in the deps.json target entry under "compile".
CompilePathsByRelativeDLLPath map[string]string CompilePathsByRelativeDLLPath map[string]string
// NativePathsByRelativeDLLPath is a map of the relative path to the DLL relative to the deps.json file // NativePaths is a map of the relative path to the DLL relative to the deps.json file
// to the target path as described in the deps.json target entry under "native". These should not have // to the target path as described in the deps.json target entry under "native". These should not have
// any runtime references to trim from the front of the path. // any runtime references to trim from the front of the path.
NativePaths *strset.Set NativePaths *strset.Set
@ -118,11 +123,15 @@ func (l *logicalDepsJSONPackage) dependencyNameVersions() []string {
if l.Targets == nil { if l.Targets == nil {
return nil return nil
} }
var results []string results := strset.New()
for name, version := range l.Targets.Dependencies { for _, t := range l.Targets {
results = append(results, createNameAndVersion(name, version)) for name, version := range t.Dependencies {
results.Add(createNameAndVersion(name, version))
}
} }
return results r := results.List()
sort.Strings(r)
return r
} }
// ClaimsDLLs indicates if this package has any DLLs associated with it (directly or indirectly with a dependency). // ClaimsDLLs indicates if this package has any DLLs associated with it (directly or indirectly with a dependency).
@ -131,7 +140,7 @@ func (l *logicalDepsJSONPackage) ClaimsDLLs(includeChildren bool) bool {
if !includeChildren { if !includeChildren {
return selfClaim return selfClaim
} }
return selfClaim || l.anyChildClaimsDLLs return selfClaim || l.AnyChildClaimsDLLs
} }
func (l *logicalDepsJSONPackage) FoundDLLs(includeChildren bool) bool { func (l *logicalDepsJSONPackage) FoundDLLs(includeChildren bool) bool {
@ -139,7 +148,7 @@ func (l *logicalDepsJSONPackage) FoundDLLs(includeChildren bool) bool {
if !includeChildren { if !includeChildren {
return selfClaim return selfClaim
} }
return selfClaim || l.anyChildHasDLLs return selfClaim || l.AnyChildHasDLLs
} }
type logicalDepsJSON struct { type logicalDepsJSON struct {
@ -206,6 +215,14 @@ func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON {
for libName, target := range targets { for libName, target := range targets {
_, exists := packageMap[libName] _, exists := packageMap[libName]
if exists { if exists {
// merge this with existing targets (multiple targets can exist for the same library)
p := packageMap[libName]
p.Targets = append(p.Targets, target)
p.RuntimePathsByRelativeDLLPath = mergeMaps(p.RuntimePathsByRelativeDLLPath, target.runtimePaths())
p.ResourcePathsByRelativeDLLPath = mergeMaps(p.ResourcePathsByRelativeDLLPath, target.resourcePaths())
p.CompilePathsByRelativeDLLPath = mergeMaps(p.CompilePathsByRelativeDLLPath, target.compilePaths())
p.NativePaths = mergeSets(p.NativePaths, target.nativePaths())
continue continue
} }
@ -218,7 +235,7 @@ func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON {
p := &logicalDepsJSONPackage{ p := &logicalDepsJSONPackage{
NameVersion: libName, NameVersion: libName,
Library: lib, Library: lib,
Targets: &target, Targets: []depsTarget{target},
RuntimePathsByRelativeDLLPath: target.runtimePaths(), RuntimePathsByRelativeDLLPath: target.runtimePaths(),
ResourcePathsByRelativeDLLPath: target.resourcePaths(), ResourcePathsByRelativeDLLPath: target.resourcePaths(),
CompilePathsByRelativeDLLPath: target.compilePaths(), CompilePathsByRelativeDLLPath: target.compilePaths(),
@ -235,8 +252,8 @@ func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON {
if !bundlingDetected && knownBundlers.Has(name) { if !bundlingDetected && knownBundlers.Has(name) {
bundlingDetected = true bundlingDetected = true
} }
p.anyChildClaimsDLLs = searchForDLLClaims(packageMap, p.dependencyNameVersions()...) p.AnyChildClaimsDLLs = searchForDLLClaims(packageMap, p.dependencyNameVersions()...)
p.anyChildHasDLLs = searchForDLLEvidence(packageMap, p.dependencyNameVersions()...) p.AnyChildHasDLLs = searchForDLLEvidence(packageMap, p.dependencyNameVersions()...)
packages[p.NameVersion] = *p packages[p.NameVersion] = *p
} }
@ -250,6 +267,22 @@ func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON {
} }
} }
func mergeMaps(m1, m2 map[string]string) map[string]string {
if m1 == nil {
m1 = make(map[string]string)
}
for k, v := range m2 {
if _, exists := m1[k]; !exists {
m1[k] = v
}
}
return m1
}
func mergeSets(s1, s2 *strset.Set) *strset.Set {
return strset.Union(s1, s2)
}
type visitorFunc func(p *logicalDepsJSONPackage) bool type visitorFunc func(p *logicalDepsJSONPackage) bool
// searchForDLLEvidence recursively searches for executables found for any of the given nameVersions and children recursively. // searchForDLLEvidence recursively searches for executables found for any of the given nameVersions and children recursively.
@ -277,12 +310,7 @@ func traverseDependencies(packageMap map[string]*logicalDepsJSONPackage, visitor
return true return true
} }
var children []string if traverseDependencies(packageMap, visitor, p.dependencyNameVersions()...) {
for name, version := range p.Targets.Dependencies {
children = append(children, createNameAndVersion(name, version))
}
if traverseDependencies(packageMap, visitor, children...) {
return true return true
} }
} }

View File

@ -2,6 +2,12 @@ package dotnet
import ( import (
"testing" "testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
) )
func TestTrimLibPrefix(t *testing.T) { func TestTrimLibPrefix(t *testing.T) {
@ -71,3 +77,140 @@ func TestTrimLibPrefix(t *testing.T) {
}) })
} }
} }
func TestGetLogicalDepsJSON_MergeTargets(t *testing.T) {
deps := depsJSON{
Location: file.NewLocation("/path/to/deps.json"),
RuntimeTarget: runtimeTarget{
Name: ".NETCoreApp,Version=v6.0",
},
// note: for this test we have two targets with the same name, which will be merged when creating a logical deps
Targets: map[string]map[string]depsTarget{
".NETCoreApp,Version=v6.0": {
"Microsoft.CodeAnalysis.CSharp/4.0.0": {
Dependencies: map[string]string{
"Microsoft.CodeAnalysis.Common": "4.0.0",
},
Runtime: map[string]map[string]string{
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll": {
"assemblyVersion": "4.0.0.0",
"fileVersion": "4.0.21.51404",
},
},
Resources: map[string]map[string]string{
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "cs",
},
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "de",
},
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "es",
},
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "fr",
},
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "it",
},
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ja",
},
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ko",
},
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "pl",
},
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "pt-BR",
},
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "ru",
},
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "tr",
},
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "zh-Hans",
},
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": {
"locale": "zh-Hant",
},
},
},
},
"net6.0": {
"Microsoft.CodeAnalysis.CSharp/4.0.0": {
Dependencies: map[string]string{
"Microsoft.CodeAnalysis.Common": "4.0.0",
},
Compile: map[string]map[string]string{
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll": {},
},
},
},
},
Libraries: map[string]depsLibrary{
"Microsoft.CodeAnalysis.CSharp/4.0.0": {
Type: "package",
Path: "microsoft.codeanalysis.csharp/4.0.0",
Sha512: "sha512-example-hash",
HashPath: "microsoft.codeanalysis.csharp.4.0.0.nupkg.sha512",
},
},
}
result := getLogicalDepsJSON(deps, &libmanJSON{})
assert.Equal(t, "/path/to/deps.json", result.Location.RealPath)
assert.Equal(t, ".NETCoreApp,Version=v6.0", result.RuntimeTarget.Name)
libPackage, exists := result.PackagesByNameVersion["Microsoft.CodeAnalysis.CSharp/4.0.0"]
require.True(t, exists, "Expected to find the merged package")
assert.NotNil(t, libPackage.Library)
assert.Equal(t, "package", libPackage.Library.Type)
assert.Equal(t, "microsoft.codeanalysis.csharp/4.0.0", libPackage.Library.Path)
assert.Equal(t, "sha512-example-hash", libPackage.Library.Sha512)
assert.Equal(t, "microsoft.codeanalysis.csharp.4.0.0.nupkg.sha512", libPackage.Library.HashPath)
require.Equal(t, 2, len(libPackage.Targets), "Expected 2 targets to be merged")
expectedRuntimePaths := map[string]string{
"Microsoft.CodeAnalysis.CSharp.dll": "lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll",
}
if diff := cmp.Diff(expectedRuntimePaths, libPackage.RuntimePathsByRelativeDLLPath); diff != "" {
t.Errorf("RuntimePathsByRelativeDLLPath mismatch (-expected +actual):\n%s", diff)
}
expectedResourcePaths := map[string]string{
"cs/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.resources.dll",
"de/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.resources.dll",
"es/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.resources.dll",
"fr/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.resources.dll",
"it/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.resources.dll",
"ja/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.resources.dll",
"ko/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.resources.dll",
"pl/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.resources.dll",
"pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll",
"ru/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.resources.dll",
"tr/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.resources.dll",
"zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll",
"zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": "lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll",
}
if diff := cmp.Diff(expectedResourcePaths, libPackage.ResourcePathsByRelativeDLLPath); diff != "" {
t.Errorf("ResourcePathsByRelativeDLLPath mismatch (-expected +actual):\n%s", diff)
}
expectedCompilePaths := map[string]string{
"Microsoft.CodeAnalysis.CSharp.dll": "lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll",
}
if diff := cmp.Diff(expectedCompilePaths, libPackage.CompilePathsByRelativeDLLPath); diff != "" {
t.Errorf("CompilePathsByRelativeDLLPath mismatch (-expected +actual):\n%s", diff)
}
assert.Equal(t, 0, libPackage.NativePaths.Size(), "Expected no native paths")
assert.True(t, result.PackageNameVersions.Has("Microsoft.CodeAnalysis.CSharp/4.0.0"))
}