Skip to content

Commit 72cef36

Browse files
Improve dependency resolver for lockfiles
Handle various lockfile cases where: * Same package/dependencies are present in different lockfiles * Independent lockfiles without a manifest and root package * Ecosystems which have only a single version of package in their environment * Dependency graphs where a resolved package can have many parent packages. Signed-off-by: Ayan Sinha Mahapatra <ayansmahapatra@gmail.com>
1 parent 7c9e0e8 commit 72cef36

5 files changed

Lines changed: 76 additions & 11 deletions

File tree

scanpipe/models.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3477,10 +3477,28 @@ class DiscoveredDependency(
34773477
"The identifier for the datafile handler used to obtain this dependency."
34783478
),
34793479
)
3480-
is_runtime = models.BooleanField(default=False)
3481-
is_optional = models.BooleanField(default=False)
3482-
is_resolved = models.BooleanField(default=False)
3483-
is_direct = models.BooleanField(default=False)
3480+
is_runtime = models.BooleanField(
3481+
default=False,
3482+
help_text=_("True if this dependency is a runtime dependency."),
3483+
)
3484+
is_optional = models.BooleanField(
3485+
default=False,
3486+
help_text=_("True if this dependency is an optional dependency"),
3487+
)
3488+
is_resolved = models.BooleanField(
3489+
default=False,
3490+
help_text=_(
3491+
"True if this dependency version requirement has been pinned "
3492+
"and this dependency points to an exact version."
3493+
),
3494+
)
3495+
is_direct = models.BooleanField(
3496+
default=False,
3497+
help_text=_(
3498+
"True if this is a direct, first-level dependency relationship "
3499+
"for a package."
3500+
),
3501+
)
34843502

34853503
objects = DiscoveredDependencyQuerySet.as_manager()
34863504

scanpipe/pipes/__init__.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,10 @@ def update_or_create_dependency(
270270
if ignore_dependency_scope(project, dependency_data):
271271
return # Do not create the DiscoveredDependency record.
272272

273+
dependencies = []
273274
if not dependency_uid:
274275
purl_data = DiscoveredDependency.extract_purl_data(dependency_data)
275-
dependency = DiscoveredDependency.objects.get_or_none(
276+
dependencies = DiscoveredDependency.objects.filter(
276277
project=project,
277278
extracted_requirement=extracted_requirement,
278279
**purl_data,
@@ -282,12 +283,36 @@ def update_or_create_dependency(
282283
project=project,
283284
dependency_uid=dependency_uid,
284285
)
286+
if dependency:
287+
dependencies.append(dependency)
288+
289+
for dependency in dependencies:
290+
# This dependency relationship is for a new package
291+
if (
292+
for_package
293+
and dependency.for_package
294+
and dependency.for_package != for_package
295+
):
296+
DiscoveredDependency.populate_dependency_uuid(dependency_data)
297+
dependency = DiscoveredDependency.create_from_data(
298+
project,
299+
dependency_data,
300+
for_package=for_package,
301+
resolved_to_package=resolved_to_package,
302+
datafile_resource=datafile_resource,
303+
datasource_id=datasource_id,
304+
strip_datafile_path_root=strip_datafile_path_root,
305+
)
306+
break
285307

286-
if dependency:
287-
dependency.update_from_data(dependency_data)
288-
if resolved_to_package and not dependency.resolved_to_package:
289-
dependency.update(resolved_to_package=resolved_to_package)
290-
else:
308+
elif dependency:
309+
dependency.update_from_data(dependency_data)
310+
if resolved_to_package and not dependency.resolved_to_package:
311+
dependency.update(resolved_to_package=resolved_to_package)
312+
if for_package and not dependency.for_package:
313+
dependency.update(for_package=for_package)
314+
315+
if not dependencies:
291316
DiscoveredDependency.populate_dependency_uuid(dependency_data)
292317
dependency = DiscoveredDependency.create_from_data(
293318
project,

scanpipe/pipes/scancode.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,11 +528,15 @@ def resolve_dependencies(project):
528528
package = None
529529
if pd.purl:
530530
purl_data = DiscoveredPackage.extract_purl_data(package_mapping)
531-
package = DiscoveredPackage.objects.get_or_none(
531+
packages = DiscoveredPackage.objects.filter(
532532
project=project,
533533
**purl_data,
534534
)
535535

536+
for package in packages:
537+
if resource.location in package.datafile_paths:
538+
break
539+
536540
dependencies = package_mapping.get("dependencies") or []
537541
update_packages_and_dependencies(
538542
project=project,
@@ -583,6 +587,8 @@ def update_packages_and_dependencies(
583587
dependency_data=dep,
584588
for_package=package,
585589
resolved_to_package=resolved_to_package,
590+
datafile_resource=resource,
591+
datasource_id=datasource_id,
586592
)
587593

588594

@@ -628,6 +634,15 @@ def match_and_resolve_dependencies(project):
628634
)
629635
]
630636

637+
# This should be done only in the case of lockfiles where only one version
638+
# of a package is present for an environment
639+
if not other_dependencies:
640+
other_dependencies = [
641+
dep
642+
for dep in matched_dependencies
643+
if (dep.purl == dependency.purl and dep.resolved_to_package)
644+
]
645+
631646
if other_dependencies:
632647
resolved_dependency = other_dependencies.pop()
633648
dependency.update(

scanpipe/templates/scanpipe/dependency_list.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
<td>
4949
<a href="?is_resolved={{ dependency.is_resolved }}" class="is-black-link">{{ dependency.is_resolved }}</a>
5050
</td>
51+
<td>
52+
<a href="?is_resolved={{ dependency.is_direct }}" class="is-black-link">{{ dependency.is_direct }}</a>
53+
</td>
5154
<td>
5255
{% if dependency.for_package %}
5356
{# CAUTION: Avoid relying on get_absolute_url to prevent unnecessary query triggers #}

scanpipe/views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,6 +1546,10 @@ class DiscoveredDependencyListView(
15461546
"field_name": "is_resolved",
15471547
"filter_fieldname": "is_resolved",
15481548
},
1549+
{
1550+
"field_name": "is_direct",
1551+
"filter_fieldname": "is_direct",
1552+
},
15491553
"for_package",
15501554
"resolved_to_package",
15511555
"datafile_resource",

0 commit comments

Comments
 (0)