|
7 | 7 | "os" |
8 | 8 | "path/filepath" |
9 | 9 | "regexp" |
| 10 | + "sort" |
10 | 11 | "strings" |
11 | 12 |
|
12 | 13 | "github.com/jfrog/build-info-go/build/utils/dotnet/dependencies" |
@@ -72,20 +73,35 @@ func (solution *solution) BuildInfo(moduleName string, log utils.Log) (*buildinf |
72 | 73 | module := buildinfo.Module{Id: getModuleId(moduleName, currProject.Name()), Type: buildinfo.Nuget} |
73 | 74 |
|
74 | 75 | // Populate requestedBy field |
| 76 | + // Seed all direct dependencies with the module path |
| 77 | + for _, directDepName := range directDeps { |
| 78 | + if directDep, exist := projectDependencies[directDepName]; exist { |
| 79 | + // Add the direct path (don't overwrite - merge with existing) |
| 80 | + directDep.RequestedBy = append(directDep.RequestedBy, []string{module.Id}) |
| 81 | + } |
| 82 | + } |
| 83 | + // Propagate paths to transitive dependencies |
75 | 84 | for _, directDepName := range directDeps { |
76 | | - // Populate the direct dependency requested by only if the dependency exist in the cache |
77 | 85 | if directDep, exist := projectDependencies[directDepName]; exist { |
78 | | - directDep.RequestedBy = [][]string{{module.Id}} |
79 | 86 | populateRequestedBy(*directDep, projectDependencies, childrenMap) |
80 | 87 | } |
81 | 88 | } |
82 | 89 |
|
83 | 90 | // Populate module dependencies |
84 | | - for _, dep := range projectDependencies { |
| 91 | + // Sort dependency keys for deterministic output |
| 92 | + depKeys := make([]string, 0, len(projectDependencies)) |
| 93 | + for key := range projectDependencies { |
| 94 | + depKeys = append(depKeys, key) |
| 95 | + } |
| 96 | + sort.Strings(depKeys) |
| 97 | + |
| 98 | + for _, key := range depKeys { |
| 99 | + dep := projectDependencies[key] |
85 | 100 | // If dependency has no RequestedBy field, it means that the dependency not accessible in the current project. |
86 | 101 | // In that case, the dependency is assumed to be under a project which is referenced by this project. |
87 | 102 | // We therefore don't include the dependency in the build-info. |
88 | 103 | if len(dep.RequestedBy) > 0 { |
| 104 | + sortRequestedByPaths(dep.RequestedBy) |
89 | 105 | module.Dependencies = append(module.Dependencies, *dep) |
90 | 106 | } |
91 | 107 | } |
@@ -128,6 +144,40 @@ func getDependencyName(dependencyKey string) string { |
128 | 144 | return strings.ToLower(dependencyName) |
129 | 145 | } |
130 | 146 |
|
| 147 | +// sortRequestedByPaths sorts RequestedBy paths for deterministic output. |
| 148 | +// Shorter paths come first (direct deps before transitive), then lexicographic order. |
| 149 | +func sortRequestedByPaths(paths [][]string) { |
| 150 | + sort.Slice(paths, func(i, j int) bool { |
| 151 | + // Shorter paths come first |
| 152 | + if len(paths[i]) != len(paths[j]) { |
| 153 | + return len(paths[i]) < len(paths[j]) |
| 154 | + } |
| 155 | + // Same length: compare lexicographically |
| 156 | + for k := 0; k < len(paths[i]); k++ { |
| 157 | + if paths[i][k] != paths[j][k] { |
| 158 | + return paths[i][k] < paths[j][k] |
| 159 | + } |
| 160 | + } |
| 161 | + return false |
| 162 | + }) |
| 163 | +} |
| 164 | + |
| 165 | +// isMatchingDependencySource checks if a dependency source file belongs to a project. |
| 166 | +// It matches if the source is: |
| 167 | +// - directly in the project root directory |
| 168 | +// - under the project's obj directory (for project.assets.json) |
| 169 | +// - in a subdirectory named after the project |
| 170 | +func isMatchingDependencySource(source, projectRootPath, projectObjPattern, projectNamePattern string) bool { |
| 171 | + sourceLower := strings.ToLower(source) |
| 172 | + // Check if source is directly in project root |
| 173 | + isInRoot := projectRootPath == strings.ToLower(filepath.Dir(source)) |
| 174 | + // Check if source is under the project's obj directory |
| 175 | + isUnderObjDir := strings.Contains(sourceLower, projectObjPattern) |
| 176 | + // Check if source path contains the project name directory (handles subdirs like /projectname/obj/) |
| 177 | + isUnderSubDirWithName := strings.Contains(sourceLower, projectNamePattern) |
| 178 | + return isInRoot || isUnderObjDir || isUnderSubDirWithName |
| 179 | +} |
| 180 | + |
131 | 181 | func (solution *solution) Marshal() ([]byte, error) { |
132 | 182 | return json.Marshal(&struct { |
133 | 183 | Projects []project.Project `json:"projects,omitempty"` |
@@ -233,14 +283,12 @@ func (solution *solution) loadSingleProject(project project.Project, log utils.L |
233 | 283 | // It can be located directly in the project's root directory or in a directory with the project name under the solution root |
234 | 284 | // or under obj directory (in case of assets.json file) |
235 | 285 | projectRootPath := strings.ToLower(project.RootPath()) |
236 | | - projectPathPattern := strings.ToLower(filepath.Join(projectRootPath, dependencies.AssetDirName) + string(filepath.Separator)) |
237 | | - projectNamePattern := strings.ToLower(string(filepath.Separator) + project.Name()) |
| 286 | + projectObjPattern := strings.ToLower(filepath.Join(projectRootPath, dependencies.AssetDirName) + string(filepath.Separator)) |
| 287 | + // Pattern includes trailing separator to avoid partial matches (e.g., "project" matching "projectname") |
| 288 | + projectNamePattern := strings.ToLower(string(filepath.Separator) + project.Name() + string(filepath.Separator)) |
238 | 289 | var dependenciesSource string |
239 | 290 | for _, source := range solution.dependenciesSources { |
240 | | - isInRoot := projectRootPath == strings.ToLower(filepath.Dir(source)) |
241 | | - isUnderObjDir := strings.Contains(strings.ToLower(source), projectPathPattern) |
242 | | - isUnderSubDirWithName := strings.HasSuffix(strings.ToLower(filepath.Dir(source)), projectNamePattern) |
243 | | - if isInRoot || isUnderObjDir || isUnderSubDirWithName { |
| 291 | + if isMatchingDependencySource(source, projectRootPath, projectObjPattern, projectNamePattern) { |
244 | 292 | dependenciesSource = source |
245 | 293 | break |
246 | 294 | } |
|
0 commit comments