Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ Next release

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

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

v33.0.0rc1 - 2026-05-14
------------------------
Expand Down
26 changes: 26 additions & 0 deletions docs/source/reference/scancode-cli/cli-core-options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,32 @@ Comparing progress message options

----

.. _cli-use-cached-results-option:

``--use-cached-results``
------------------------

When enabled, during scan time for a given Resource, we iterate through the
active scanners and see if we have cached results for those already. If we
do, we update our results with the cached data. If not, we add those
scanners to a list of scanners to be run. After scanning, the cache is
updated.

**Example**

.. code-block:: shell

scancode -clipeu --use-cached-results samples samples.json

On the first run, the cache will be created for the scanned Resources in
`samples`. Subsequent runs on `samples` will return cached results.

When run as a stand-alone release, the results cache is tied to specific
ScanCode toolkit versions. When run in development mode, the cache is stored
in the `scancode-toolkit/.cache` directory.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include some functionality to re-compute the cache for a file based on data structure changes caused by different versions of scancode, or different CLI options used with different output data formats? There is some discussion on this at #3941

We need to check that we atleast handle the errors resulting out of this. Additionally it would be nice to somehow include the data format versions in the index cache too, smartly avoiding cache recompute whenever possible.

Output data formats are not always changes based on scancode versions, and an update in the output data format version also doesn't correspond to updates in all the scanner output data. So we need to probably compute a hash based on all the data fields present for a particular scanner data based on the keys present in the mapping and include this in the index file path? This would cover both variations caused by scancode version changes and CLI options used.

This is an enhancement though, so feel free to mark this out of scope.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm avoiding the issue of the cache being different between scancode versions because of scanner output by having a separate cache for each version of scancode. I feel like a cache migration tool can get complex quickly, but I also feel that we may want to bring over an existing large cache and something like this would be useful for that.

----

.. _glob-pattern-matching:

Glob Pattern Matching
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ files = [
{ filename = "pyproject.toml" },
{ filename = "pyproject-scancode-toolkit-mini.toml" },
{ filename = "pyproject-packagedcode.toml" },

]


Expand Down
15 changes: 12 additions & 3 deletions src/commoncode/hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,24 @@ def checksum(location, name, base64=False):
return checksum_from_chunks(chunks=chunks, total_length=total_length, name=name, base64=base64)


def hasher_from_chunks(chunks, name, total_length=0):
"""
Return a hasher of ``name`` checksum algorithm of contains the contents of
the iterator of byte strings ``chunks`` of length ``total_length``.
"""
hasher = get_hasher_instance_by_name(name=name, total_length=total_length)
for chunk in chunks:
hasher.update(chunk)
return hasher


def checksum_from_chunks(chunks, name, total_length=0, base64=False):
"""
Return a checksum from the content of the iterator of byte strings ``chunks`` with a
``total_length`` combined length using the ``name`` checksum algorithm. The returned checksum is
a string as a hexdigest or is base64-encoded is ``base64`` is True.
"""
hasher = get_hasher_instance_by_name(name=name, total_length=total_length)
for chunk in chunks:
hasher.update(chunk)
hasher = hasher_from_chunks(chunks=chunks, name=name, total_length=total_length)
if base64:
return hasher.b64digest()
return hasher.hexdigest()
Expand Down
63 changes: 53 additions & 10 deletions src/scancode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class WindowsError(Exception):
from scancode import notice
from scancode import print_about
from scancode import Scanner
from scancode import results_cache
from scancode.help import epilog_text
from scancode.help import examples_text
from scancode.interrupt import DEFAULT_TIMEOUT
Expand Down Expand Up @@ -407,12 +408,18 @@ def default_processes():
help_group=cliutils.MISC_GROUP, sort_order=1000, cls=PluggableCommandLineOption)

@click.option(
"--check-version/--no-check-version",
help="Whether to check for new versions. Defaults to true.",
'--check-version/--no-check-version',
help='Whether to check for new versions. Defaults to true.',
default=True,
# not yet supported in Click 6.7 but added in PluggableCommandLineOption
hidden=True,
help_group=cliutils.MISC_GROUP, sort_order=1000, cls=PluggableCommandLineOption)

@click.option('--use-cached-results',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was just wondering if this is the best name since this option enables both the creation of the cache and the use of cached results and this name only indicates the latter.

Alternatives could be: --use-results-cache, --cache-results, --cache but I'm not sure what to choose too.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other option is to use two other options one to create the cache and the other to use the cached results, but using just one option is probably much better if there are no large performance implications.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure about this too. My original thought was to update the cache in all circumstances, even if we are not using it to get scan results, but I decided against it because this is still an experimental feature and maybe it wouldn't be a good idea to create a whole bunch of cache files that wouldn't normally be used.

is_flag=True,
default=False,
help='(EXPERIMENTAL) Enables the creation, update, and usage of cached results during scan time.',
help_group=cliutils.CORE_GROUP, sort_order=250, cls=PluggableCommandLineOption)
def scancode(
ctx,
input, # NOQA
Expand All @@ -433,6 +440,7 @@ def scancode(
test_error_mode,
keep_temp_files,
check_version,
use_cached_results,
echo_func=echo_stderr,
*args,
**kwargs,
Expand Down Expand Up @@ -549,6 +557,7 @@ def scancode(
return_results=False,
echo_func=echo_func,
outdated=outdated,
use_cached_results=use_cached_results,
*args,
**kwargs
)
Expand All @@ -570,7 +579,7 @@ def scancode(


def run_scan(
input, #
input, #
config_file=None,
ignore=[],
from_json=False,
Expand All @@ -594,6 +603,7 @@ def run_scan(
pretty_params=None,
plugin_options=plugin_options,
outdated=None,
use_cached_results=False,
*args,
**kwargs
):
Expand Down Expand Up @@ -1006,6 +1016,7 @@ def echo_func(*_args, **_kwargs):
verbose=verbose,
kwargs=requested_options,
echo_func=echo_func,
use_cached_results=use_cached_results,
)
success = success and scan_success

Expand Down Expand Up @@ -1141,7 +1152,7 @@ def load_configuration_file(path):

click.echo(f"Loading env from {path}")
try:

config_values = saneyaml.load(path.read())
ignores = config_values.get("ignored_patterns", [])
except (saneyaml.YAMLError, Exception):
Expand Down Expand Up @@ -1226,6 +1237,7 @@ def run_scanners(
verbose=False,
kwargs=None,
echo_func=echo_stderr,
use_cached_results=False,
):
"""
Run the list of `stage` ScanPlugin `plugins` on `codebase`.
Expand Down Expand Up @@ -1267,7 +1279,8 @@ def run_scanners(
# TODO: add CLI option to bypass cache entirely?
scan_success = scan_codebase(
codebase, scanners, processes, timeout,
with_timing=timing, progress_manager=progress_manager)
with_timing=timing, progress_manager=progress_manager,
use_cached_results=use_cached_results)

# TODO: add progress indicator
# run the process codebase of each scan plugin (most often a no-op)
Expand Down Expand Up @@ -1305,6 +1318,7 @@ def scan_codebase(
with_timing=False,
progress_manager=None,
echo_func=echo_stderr,
use_cached_results=False,
):
"""
Run the `scanners` Scanner objects on the `codebase` Codebase. Return True
Expand All @@ -1326,15 +1340,16 @@ def scan_codebase(
"""

# NOTE: we never scan directories
resources = ((r.location, r.path) for r in codebase.walk() if r.is_file)
resources = ((r.location, r.path, r.name) for r in codebase.walk() if r.is_file)

use_threading = processes >= 0
runner = partial(
scan_resource,
scanners=scanners,
timeout=timeout,
with_timing=with_timing,
with_threading=use_threading
with_threading=use_threading,
use_cached_results=use_cached_results,
)

if TRACE:
Expand Down Expand Up @@ -1462,11 +1477,12 @@ def terminate_pool_with_backoff(pool, number_of_trials=3):


def scan_resource(
location_path,
location_path_name,
scanners,
timeout=DEFAULT_TIMEOUT,
with_timing=False,
with_threading=True,
use_cached_results=False,
):
"""
Given a ``location_path`` tuple pf (location, path), return a tuple of:
Expand All @@ -1489,10 +1505,11 @@ def scan_resource(
processing and threading works.
"""
scan_time = time()
location, path = location_path
location, path, name = location_path_name
results = {}
scan_errors = []
timings = {} if with_timing else None
scanners_to_run = []

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

results_cache_index = ''
if use_cached_results:
# compute results_cache_index
results_cache_index = results_cache.compute_results_cache_index(location=location, filename=name)

# update `results` with cached data or add scanner to scanners_to_run if no
# cache data is available
for scanner in scanners:
# get resource_cache_data
resource_cache_data = results_cache.get_results_cache_data(
results_cache_index=results_cache_index,
plugin_name=scanner.name
)
if resource_cache_data:
results.update(resource_cache_data)
else:
scanners_to_run.append(scanner)
else:
scanners_to_run = scanners

# run each scanner in sequence in its own interruptible
for scanner in scanners:
for scanner in scanners_to_run:
if with_timing:
start = time()

Expand All @@ -1523,6 +1560,12 @@ def scan_resource(
# the return value of a scanner fun MUST be a mapping
if values_mapping:
results.update(values_mapping)
if use_cached_results:
results_cache.update_results_cache_data(
results_cache_index=results_cache_index,
plugin_name=scanner.name,
results=values_mapping,
)

except Exception:
msg = 'ERROR: for scanner: ' + scanner.name + ':\n' + traceback.format_exc()
Expand Down
Loading
Loading