Skip to content

Commit e544fea

Browse files
authored
Merge pull request #5289 from aboutcode-org/system-resource-cache
System resource cache
2 parents 384b62a + 4b57628 commit e544fea

23 files changed

Lines changed: 1069 additions & 16 deletions

File tree

‎CHANGELOG.rst‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ Next release
1010

1111
- Improve copyright detection for statements with parens or trailing "authors"
1212

13+
- Add experimental option for using cached results during scan time. When the
14+
``--use-cached-results`` option is enabled in the ScanCode CLI, during scan
15+
time for a given Resource, we iterate through the active scanners and see if we
16+
have cached results for those already. If we do, we update our results with the
17+
cached data. If not, we add those scanners to a list of scanners to be run.
18+
After scanning, the cache is updated.
1319

1420
v33.0.0rc1 - 2026-05-14
1521
------------------------

‎commoncode-CHANGELOG.rst‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Release notes
22
=============
33

4+
Version 33.0.0rc1 - (2026-xx-xx)
5+
--------------------------------
6+
7+
- Split logic from `commoncode.hash.checksum_from_chunks` into new function
8+
`commoncode.hash.hasher_from_chunks`. This returns a hasher of a specified
9+
checksum that contains the contents of an iterator `chunks`.
10+
411
Version 32.5.2 - (2026-06-11)
512
-----------------------------
613

‎configure‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,8 @@ install_packages_with_local() {
261261
# we need to install commoncode from local source first so this
262262
# is tested and not the released commoncode
263263
"$CFG_BIN_DIR/flot" --pyproject pyproject-commoncode.toml
264-
"$CFG_BIN_DIR/pip" install ./dist/commoncode*.whl
265264
install_packages "$CFG_REQUIREMENTS"
265+
"$CFG_BIN_DIR/pip" install --force-reinstall ./dist/commoncode*.whl
266266
}
267267

268268

‎docs/source/reference/scancode-cli/cli-core-options.rst‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,32 @@ Comparing progress message options
196196

197197
----
198198

199+
.. _cli-use-cached-results-option:
200+
201+
``--use-cached-results``
202+
------------------------
203+
204+
When enabled, during scan time for a given Resource, we iterate through the
205+
active scanners and see if we have cached results for those already. If we
206+
do, we update our results with the cached data. If not, we add those
207+
scanners to a list of scanners to be run. After scanning, the cache is
208+
updated.
209+
210+
**Example**
211+
212+
.. code-block:: shell
213+
214+
scancode -clipeu --use-cached-results samples samples.json
215+
216+
On the first run, the cache will be created for the scanned Resources in
217+
`samples`. Subsequent runs on `samples` will return cached results.
218+
219+
When run as a stand-alone release, the results cache is tied to specific
220+
ScanCode toolkit versions. When run in development mode, the cache is stored
221+
in the `scancode-toolkit/.cache` directory.
222+
223+
----
224+
199225
.. _glob-pattern-matching:
200226

201227
Glob Pattern Matching

‎pyproject.toml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ files = [
164164
{ filename = "pyproject.toml" },
165165
{ filename = "pyproject-scancode-toolkit-mini.toml" },
166166
{ filename = "pyproject-packagedcode.toml" },
167-
168167
]
169168

170169

‎requirements.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ babel==2.17.0
33
banal==1.0.6
44
beartype==0.22.9
55
beautifulsoup4==4.14.3
6-
binaryornot==0.4.4
6+
binaryornot==0.6.0
77
boolean.py==5.0
88
certifi==2026.1.4
99
cffi==2.0.0

‎src/commoncode/hash.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,15 +229,24 @@ def checksum(location, name, base64=False):
229229
return checksum_from_chunks(chunks=chunks, total_length=total_length, name=name, base64=base64)
230230

231231

232+
def hasher_from_chunks(chunks, name, total_length=0):
233+
"""
234+
Return a hasher of ``name`` checksum algorithm of contains the contents of
235+
the iterator of byte strings ``chunks`` of length ``total_length``.
236+
"""
237+
hasher = get_hasher_instance_by_name(name=name, total_length=total_length)
238+
for chunk in chunks:
239+
hasher.update(chunk)
240+
return hasher
241+
242+
232243
def checksum_from_chunks(chunks, name, total_length=0, base64=False):
233244
"""
234245
Return a checksum from the content of the iterator of byte strings ``chunks`` with a
235246
``total_length`` combined length using the ``name`` checksum algorithm. The returned checksum is
236247
a string as a hexdigest or is base64-encoded is ``base64`` is True.
237248
"""
238-
hasher = get_hasher_instance_by_name(name=name, total_length=total_length)
239-
for chunk in chunks:
240-
hasher.update(chunk)
249+
hasher = hasher_from_chunks(chunks=chunks, name=name, total_length=total_length)
241250
if base64:
242251
return hasher.b64digest()
243252
return hasher.hexdigest()

‎src/scancode/cli.py‎

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class WindowsError(Exception):
6767
from scancode import notice
6868
from scancode import print_about
6969
from scancode import Scanner
70+
from scancode import results_cache
7071
from scancode.help import epilog_text
7172
from scancode.help import examples_text
7273
from scancode.interrupt import DEFAULT_TIMEOUT
@@ -407,12 +408,18 @@ def default_processes():
407408
help_group=cliutils.MISC_GROUP, sort_order=1000, cls=PluggableCommandLineOption)
408409

409410
@click.option(
410-
"--check-version/--no-check-version",
411-
help="Whether to check for new versions. Defaults to true.",
411+
'--check-version/--no-check-version',
412+
help='Whether to check for new versions. Defaults to true.',
412413
default=True,
413414
# not yet supported in Click 6.7 but added in PluggableCommandLineOption
414415
hidden=True,
415416
help_group=cliutils.MISC_GROUP, sort_order=1000, cls=PluggableCommandLineOption)
417+
418+
@click.option('--use-cached-results',
419+
is_flag=True,
420+
default=False,
421+
help='(EXPERIMENTAL) Enables the creation, update, and usage of cached results during scan time.',
422+
help_group=cliutils.CORE_GROUP, sort_order=250, cls=PluggableCommandLineOption)
416423
def scancode(
417424
ctx,
418425
input, # NOQA
@@ -433,6 +440,7 @@ def scancode(
433440
test_error_mode,
434441
keep_temp_files,
435442
check_version,
443+
use_cached_results,
436444
echo_func=echo_stderr,
437445
*args,
438446
**kwargs,
@@ -549,6 +557,7 @@ def scancode(
549557
return_results=False,
550558
echo_func=echo_func,
551559
outdated=outdated,
560+
use_cached_results=use_cached_results,
552561
*args,
553562
**kwargs
554563
)
@@ -570,7 +579,7 @@ def scancode(
570579

571580

572581
def run_scan(
573-
input, #
582+
input, #
574583
config_file=None,
575584
ignore=[],
576585
from_json=False,
@@ -594,6 +603,7 @@ def run_scan(
594603
pretty_params=None,
595604
plugin_options=plugin_options,
596605
outdated=None,
606+
use_cached_results=False,
597607
*args,
598608
**kwargs
599609
):
@@ -1006,6 +1016,7 @@ def echo_func(*_args, **_kwargs):
10061016
verbose=verbose,
10071017
kwargs=requested_options,
10081018
echo_func=echo_func,
1019+
use_cached_results=use_cached_results,
10091020
)
10101021
success = success and scan_success
10111022

@@ -1141,7 +1152,7 @@ def load_configuration_file(path):
11411152

11421153
click.echo(f"Loading env from {path}")
11431154
try:
1144-
1155+
11451156
config_values = saneyaml.load(path.read())
11461157
ignores = config_values.get("ignored_patterns", [])
11471158
except (saneyaml.YAMLError, Exception):
@@ -1226,6 +1237,7 @@ def run_scanners(
12261237
verbose=False,
12271238
kwargs=None,
12281239
echo_func=echo_stderr,
1240+
use_cached_results=False,
12291241
):
12301242
"""
12311243
Run the list of `stage` ScanPlugin `plugins` on `codebase`.
@@ -1267,7 +1279,8 @@ def run_scanners(
12671279
# TODO: add CLI option to bypass cache entirely?
12681280
scan_success = scan_codebase(
12691281
codebase, scanners, processes, timeout,
1270-
with_timing=timing, progress_manager=progress_manager)
1282+
with_timing=timing, progress_manager=progress_manager,
1283+
use_cached_results=use_cached_results)
12711284

12721285
# TODO: add progress indicator
12731286
# run the process codebase of each scan plugin (most often a no-op)
@@ -1305,6 +1318,7 @@ def scan_codebase(
13051318
with_timing=False,
13061319
progress_manager=None,
13071320
echo_func=echo_stderr,
1321+
use_cached_results=False,
13081322
):
13091323
"""
13101324
Run the `scanners` Scanner objects on the `codebase` Codebase. Return True
@@ -1326,15 +1340,16 @@ def scan_codebase(
13261340
"""
13271341

13281342
# NOTE: we never scan directories
1329-
resources = ((r.location, r.path) for r in codebase.walk() if r.is_file)
1343+
resources = ((r.location, r.path, r.name) for r in codebase.walk() if r.is_file)
13301344

13311345
use_threading = processes >= 0
13321346
runner = partial(
13331347
scan_resource,
13341348
scanners=scanners,
13351349
timeout=timeout,
13361350
with_timing=with_timing,
1337-
with_threading=use_threading
1351+
with_threading=use_threading,
1352+
use_cached_results=use_cached_results,
13381353
)
13391354

13401355
if TRACE:
@@ -1462,11 +1477,12 @@ def terminate_pool_with_backoff(pool, number_of_trials=3):
14621477

14631478

14641479
def scan_resource(
1465-
location_path,
1480+
location_path_name,
14661481
scanners,
14671482
timeout=DEFAULT_TIMEOUT,
14681483
with_timing=False,
14691484
with_threading=True,
1485+
use_cached_results=False,
14701486
):
14711487
"""
14721488
Given a ``location_path`` tuple pf (location, path), return a tuple of:
@@ -1489,10 +1505,11 @@ def scan_resource(
14891505
processing and threading works.
14901506
"""
14911507
scan_time = time()
1492-
location, path = location_path
1508+
location, path, name = location_path_name
14931509
results = {}
14941510
scan_errors = []
14951511
timings = {} if with_timing else None
1512+
scanners_to_run = []
14961513

14971514
if not with_threading:
14981515
interruptor = fake_interruptible
@@ -1503,8 +1520,28 @@ def scan_resource(
15031520
# and start returning values. The kill timeout is otherwise there
15041521
# as a gatekeeper for runaway processes.
15051522

1523+
results_cache_index = ''
1524+
if use_cached_results:
1525+
# compute results_cache_index
1526+
results_cache_index = results_cache.compute_results_cache_index(location=location, filename=name)
1527+
1528+
# update `results` with cached data or add scanner to scanners_to_run if no
1529+
# cache data is available
1530+
for scanner in scanners:
1531+
# get resource_cache_data
1532+
resource_cache_data = results_cache.get_results_cache_data(
1533+
results_cache_index=results_cache_index,
1534+
plugin_name=scanner.name
1535+
)
1536+
if resource_cache_data:
1537+
results.update(resource_cache_data)
1538+
else:
1539+
scanners_to_run.append(scanner)
1540+
else:
1541+
scanners_to_run = scanners
1542+
15061543
# run each scanner in sequence in its own interruptible
1507-
for scanner in scanners:
1544+
for scanner in scanners_to_run:
15081545
if with_timing:
15091546
start = time()
15101547

@@ -1523,6 +1560,12 @@ def scan_resource(
15231560
# the return value of a scanner fun MUST be a mapping
15241561
if values_mapping:
15251562
results.update(values_mapping)
1563+
if use_cached_results:
1564+
results_cache.update_results_cache_data(
1565+
results_cache_index=results_cache_index,
1566+
plugin_name=scanner.name,
1567+
results=values_mapping,
1568+
)
15261569

15271570
except Exception:
15281571
msg = 'ERROR: for scanner: ' + scanner.name + ':\n' + traceback.format_exc()

0 commit comments

Comments
 (0)