Skip to content

Commit 2607a5c

Browse files
committed
Implement package assembly in scancode.io #447
Signed-off-by: Jono Yang <jyang@nexb.com>
1 parent 15b3b45 commit 2607a5c

20 files changed

Lines changed: 4957 additions & 2026 deletions

CHANGELOG.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ v31.0.0 (next)
8282
- Export current results as XLSX for resource, packages, and errors list views.
8383
https://github.com/nexB/scancode.io/issues/48
8484

85+
- Update application Package scanning step to reflect the updates in
86+
scancode-toolkit package scanning.
87+
88+
- Package data detected from a file are now stored on the
89+
CodebaseResource.package_data field.
90+
- A second processing step is now done after scanning for Package data, where
91+
Package Resources are determined and DiscoveredPackages are created.
92+
93+
https://github.com/nexB/scancode.io/issues/444
94+
8595
v30.2.0 (2021-12-17)
8696
--------------------
8797

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.0.6 on 2022-08-04 18:38
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('scanpipe', '0019_auto_20220804_1836'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='codebaseresource',
15+
name='package_data',
16+
field=models.JSONField(blank=True, default=list, help_text='List of Package data detected from this CodebaseResource'),
17+
),
18+
migrations.AlterField(
19+
model_name='codebaseresource',
20+
name='name',
21+
field=models.CharField(blank=True, help_text='File or directory name of this resource with its extension.', max_length=255),
22+
),
23+
]

scanpipe/models.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import django_rq
5555
import redis
5656
import requests
57+
from commoncode.fileutils import parent_directory
5758
from commoncode.hash import multi_checksums
5859
from packageurl import PackageURL
5960
from packageurl import normalize_qualifiers
@@ -852,6 +853,14 @@ def error_count(self):
852853
"""
853854
return self.projecterrors.count()
854855

856+
@cached_property
857+
def has_single_resource(self):
858+
"""
859+
Return True if we only have a single CodebaseResource associated to this
860+
project, False otherwise.
861+
"""
862+
return self.codebaseresources.count() == 1
863+
855864

856865
class ProjectRelatedQuerySet(models.QuerySet):
857866
def project(self, project):
@@ -1248,6 +1257,9 @@ def has_licenses(self):
12481257
def has_no_licenses(self):
12491258
return self.filter(licenses=[])
12501259

1260+
def has_package_data(self):
1261+
return self.filter(package_data__isnull=False)
1262+
12511263
def licenses_categories(self, categories):
12521264
return self.json_list_contains(
12531265
field_name="licenses",
@@ -1466,6 +1478,12 @@ class Compliance(models.TextChoices):
14661478
),
14671479
)
14681480

1481+
package_data = models.JSONField(
1482+
default=list,
1483+
blank=True,
1484+
help_text=_("List of Package data detected from this CodebaseResource"),
1485+
)
1486+
14691487
objects = CodebaseResourceQuerySet.as_manager()
14701488

14711489
class Meta:
@@ -1488,11 +1506,14 @@ def from_db(cls, db, field_names, values):
14881506

14891507
return new
14901508

1491-
def save(self, *args, **kwargs):
1509+
def save(self, codebase=None, *args, **kwargs):
14921510
"""
14931511
Saves the current resource instance.
14941512
Injects policies—if the feature is enabled—when the `licenses` field value is
14951513
changed.
1514+
1515+
`codebase` is not used in this context but required for compatibility
1516+
with the commoncode.resource.Codebase class API.
14961517
"""
14971518
if scanpipe_app.policies_enabled:
14981519
loaded_licenses = getattr(self, "loaded_licenses", [])
@@ -1582,6 +1603,47 @@ def unique_license_expressions(self):
15821603
"""
15831604
return sorted(set(self.license_expressions))
15841605

1606+
def parent_path(self):
1607+
"""
1608+
Return the parent path for this CodebaseResource or None.
1609+
"""
1610+
return parent_directory(self.path, with_trail=False)
1611+
1612+
def has_parent(self):
1613+
"""
1614+
Return True if this CodebaseResource has a parent CodebaseResource or
1615+
False otherwise.
1616+
"""
1617+
parent_path = self.parent_path()
1618+
if not parent_path:
1619+
return False
1620+
if self.project.codebaseresources.filter(path=parent_path).exists():
1621+
return True
1622+
return False
1623+
1624+
def parent(self, codebase=None):
1625+
"""
1626+
Return the parent CodebaseResource object for this CodebaseResource or
1627+
None.
1628+
1629+
`codebase` is not used in this context but required for compatibility
1630+
with the commoncode.resource.Codebase class API.
1631+
"""
1632+
parent_path = self.parent_path()
1633+
return parent_path and self.project.codebaseresources.get(path=parent_path)
1634+
1635+
def siblings(self, codebase=None):
1636+
"""
1637+
Return a sequence of sibling Resource objects for this Resource
1638+
or an empty sequence.
1639+
1640+
`codebase` is not used in this context but required for compatibility
1641+
with the commoncode.resource.Codebase class API.
1642+
"""
1643+
if self.has_parent():
1644+
return self.parent(codebase).children(codebase)
1645+
return []
1646+
15851647
def descendants(self):
15861648
"""
15871649
Returns a QuerySet of descendant CodebaseResource objects using a

scanpipe/pipes/scancode.py

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
from commoncode import fileutils
3737
from commoncode.resource import VirtualCodebase
3838
from extractcode import api as extractcode_api
39+
from packagedcode import get_package_handler
40+
from packagedcode import models as packagedcode_models
3941
from scancode import ScancodeError
4042
from scancode import Scanner
4143
from scancode import api as scancode_api
@@ -230,10 +232,9 @@ def save_scan_package_results(codebase_resource, scan_results, scan_errors):
230232
Saves the resource scan package results in the database.
231233
Creates project errors if any occurred during the scan.
232234
"""
233-
packages = scan_results.get("package_data", [])
234-
if packages:
235-
for package_data in packages:
236-
codebase_resource.create_and_add_package(package_data)
235+
package_data = scan_results.get("package_data", [])
236+
if package_data:
237+
codebase_resource.package_data = package_data
237238
codebase_resource.status = "application-package"
238239
codebase_resource.save()
239240

@@ -310,18 +311,84 @@ def scan_for_files(project):
310311

311312
def scan_for_application_packages(project):
312313
"""
313-
Runs a package scan on files without a status for a `project`.
314+
Runs a package scan on files without a status for a `project`, then create
315+
DiscoveredPackage instances from the detected package data.
314316
315-
Multiprocessing is enabled by default on this pipe, the number of processes can be
316-
controlled through the SCANCODEIO_PROCESSES setting.
317+
Multiprocessing is enabled by default on this pipe, the number of processes
318+
can be controlled through the SCANCODEIO_PROCESSES setting.
317319
"""
318320
resource_qs = project.codebaseresources.no_status()
321+
322+
# Collect detected Package data and save it to the CodebaseResource it was
323+
# detected from
319324
_scan_and_save(
320325
resource_qs=resource_qs,
321326
scan_func=scan_for_package_data,
322327
save_func=save_scan_package_results,
323328
)
324329

330+
# Iterate through CodebaseResources with Package data and handle them using
331+
# the proper Package handler from packagedcode
332+
assemble_packages(project=project)
333+
334+
335+
def add_to_package(package_uid, resource, project):
336+
"""
337+
Relate a DiscoveredPackage to `resource` from `project` using `package_uid`
338+
"""
339+
if not package_uid:
340+
return
341+
package_associated_with_resource = resource.discovered_packages.filter(
342+
package_uid=package_uid
343+
).exists()
344+
if not package_associated_with_resource:
345+
package = project.discoveredpackages.get(package_uid=package_uid)
346+
resource.discovered_packages.add(package)
347+
348+
349+
def assemble_packages(project):
350+
"""
351+
Create instances of DiscoveredPackage and DiscoveredDependency for `project`
352+
from the parsed package data present in the CodebaseResources of `project`.
353+
"""
354+
logger.info(f"Project: {project}:\n" "Function: assemble_packages\n")
355+
seen_resource_paths = set()
356+
for resource in project.codebaseresources.has_package_data():
357+
if resource.path in seen_resource_paths:
358+
continue
359+
360+
logger.info(f"Processing: CodebaseResource {resource.path}\n")
361+
362+
for package_mapping in resource.package_data:
363+
pd = packagedcode_models.PackageData.from_dict(mapping=package_mapping)
364+
365+
logger.info(f"Processing: PackageData {pd.purl}\n")
366+
367+
handler = get_package_handler(pd)
368+
369+
logger.info(f"Selected: Package handler {handler}\n")
370+
371+
items = handler.assemble(
372+
package_data=pd,
373+
resource=resource,
374+
codebase=project,
375+
package_adder=add_to_package,
376+
)
377+
378+
for item in items:
379+
logger.info(f"Processing: item {item}\n")
380+
if isinstance(item, packagedcode_models.Package):
381+
package_data = item.to_dict()
382+
pipes.update_or_create_package(project, package_data)
383+
elif isinstance(item, packagedcode_models.Dependency):
384+
# We will handle Dependencies when we properly implement the
385+
# DiscoveredDependency model
386+
pass
387+
elif isinstance(item, CodebaseResource):
388+
seen_resource_paths.add(item.path)
389+
else:
390+
logger.info(f"Unknown Package assembly item type: {item!r}\n")
391+
325392

326393
def run_scancode(location, output_file, options, raise_on_error=False):
327394
"""

0 commit comments

Comments
 (0)