Skip to content

Commit 4fc3af2

Browse files
Add initial multiregex implementation
Use multiregex to use a cached regex path patterns and datafile handlers mapping to detect package datafiles faster. Reference: #4064 Reference: #4061 Signed-off-by: Ayan Sinha Mahapatra <asmahapatra@aboutcode.org>
1 parent 8d6fa73 commit 4fc3af2

4 files changed

Lines changed: 269 additions & 15 deletions

File tree

src/packagedcode/__init__.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,29 @@
246246
win_reg.InstalledProgramFromDockerUtilityvmSoftwareHandler,
247247
]
248248

249+
250+
# These handlers are special as they use filetype to
251+
# detect these binaries instead of datafile path patterns
252+
# as these are optionally installed, we can skip checking
253+
# for filetype if these are not available
254+
BINARY_HANDLERS_PRESENT = False
255+
BINARY_PACKAGE_DATAFILE_HANDLERS = []
256+
249257
try:
250258
from go_inspector.binary import get_go_binary_handler
251-
APPLICATION_PACKAGE_DATAFILE_HANDLERS.append(get_go_binary_handler())
259+
handler = get_go_binary_handler()
260+
APPLICATION_PACKAGE_DATAFILE_HANDLERS.append(handler)
261+
BINARY_PACKAGE_DATAFILE_HANDLERS.append(handler)
262+
BINARY_HANDLERS_PRESENT = True
252263
except ImportError:
253264
pass
254265

255266
try:
256267
from rust_inspector.packages import get_rust_binary_handler
257-
APPLICATION_PACKAGE_DATAFILE_HANDLERS.append(get_rust_binary_handler())
268+
handler = get_rust_binary_handler()
269+
APPLICATION_PACKAGE_DATAFILE_HANDLERS.append(handler)
270+
BINARY_PACKAGE_DATAFILE_HANDLERS.append(handler)
271+
BINARY_HANDLERS_PRESENT = True
258272
except ImportError:
259273
pass
260274

src/packagedcode/cache.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# ScanCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/nexB/scancode-toolkit for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import os
11+
import json
12+
import attr
13+
import fnmatch
14+
15+
from commoncode.fileutils import create_dir
16+
17+
from packagedcode import APPLICATION_PACKAGE_DATAFILE_HANDLERS
18+
from packagedcode import SYSTEM_PACKAGE_DATAFILE_HANDLERS
19+
20+
from scancode_config import packagedcode_cache_dir
21+
from scancode_config import scancode_cache_dir
22+
23+
"""
24+
An on-disk persistent cache of package manifest patterns and related package
25+
manifest handlers mapping. Loading and dumping the cached package manifest
26+
patterns is safe to use across multiple processes using lock files.
27+
"""
28+
29+
# global in-memory cache of the PkgManifestPatternsCache
30+
_PACKAGE_CACHE = None
31+
32+
PACKAGE_INDEX_LOCK_TIMEOUT = 60 * 6
33+
PACKAGE_INDEX_DIR = 'package_patterns_index'
34+
PACKAGE_INDEX_FILENAME = 'index_cache'
35+
PACKAGE_LOCKFILE_NAME = 'scancode_package_index_lockfile'
36+
PACKAGE_CHECKSUM_FILE = 'scancode_package_index_tree_checksums'
37+
38+
39+
@attr.s
40+
class PkgManifestPatternsCache:
41+
"""
42+
Represent cachable package manifest regex patterns, prematchers
43+
and mappings from regex patterns to datasource IDs for all datafile
44+
handlers.
45+
"""
46+
47+
handler_by_regex = attr.ib(default=attr.Factory(dict))
48+
system_multiregex_patterns = attr.ib(default=attr.Factory(list))
49+
application_multiregex_patterns = attr.ib(default=attr.Factory(list))
50+
51+
@staticmethod
52+
def all_multiregex_patterns(self):
53+
return self.application_multiregex_patterns + [
54+
multiregex_pattern
55+
for multiregex_pattern in self.system_multiregex_patterns
56+
if multiregex_pattern not in self.application_multiregex_patterns
57+
]
58+
59+
@classmethod
60+
def from_mapping(cls, cache_mapping):
61+
return cls(**cache_mapping)
62+
63+
@staticmethod
64+
def load_or_build(
65+
packagedcode_cache_dir=packagedcode_cache_dir,
66+
scancode_cache_dir=scancode_cache_dir,
67+
force=False,
68+
timeout=PACKAGE_INDEX_LOCK_TIMEOUT,
69+
):
70+
"""
71+
Load or build and save and return a PkgManifestPatternsCache object.
72+
73+
We either load a cached PkgManifestPatternsCache or build and cache the patterns.
74+
75+
- If the cache exists, it is returned unless corrupted.
76+
- If ``force`` is True, or if the cache does not exist a new index is built
77+
and cached.
78+
"""
79+
idx_cache_dir = os.path.join(packagedcode_cache_dir, PACKAGE_INDEX_DIR)
80+
create_dir(idx_cache_dir)
81+
cache_file = os.path.join(idx_cache_dir, PACKAGE_INDEX_FILENAME)
82+
has_cache = os.path.exists(cache_file) and os.path.getsize(cache_file)
83+
84+
# bypass build if cache exists
85+
if has_cache and not force:
86+
try:
87+
return load_cache_file(cache_file)
88+
except Exception as e:
89+
# work around some rare Windows quirks
90+
import traceback
91+
print('Inconsistent License cache: rebuilding index.')
92+
print(str(e))
93+
print(traceback.format_exc())
94+
95+
96+
from scancode import lockfile
97+
lock_file = os.path.join(scancode_cache_dir, PACKAGE_LOCKFILE_NAME)
98+
99+
# here, we have no cache: lock, check and rebuild
100+
try:
101+
# acquire lock and wait until timeout to get a lock or die
102+
with lockfile.FileLock(lock_file).locked(timeout=timeout):
103+
104+
system_multiregex_patterns, system_handlers_by_regex = build_mappings_and_multiregex_patterns(
105+
datafile_handlers=SYSTEM_PACKAGE_DATAFILE_HANDLERS,
106+
)
107+
application_multiregex_patterns, application_handlers_by_regex = build_mappings_and_multiregex_patterns(
108+
datafile_handlers=APPLICATION_PACKAGE_DATAFILE_HANDLERS,
109+
)
110+
package_cache = PkgManifestPatternsCache(
111+
handler_by_regex=system_handlers_by_regex + application_handlers_by_regex,
112+
system_multiregex_patterns=system_multiregex_patterns,
113+
application_multiregex_patterns=application_multiregex_patterns,
114+
)
115+
package_cache.dump(cache_file)
116+
return package_cache
117+
118+
except lockfile.LockTimeout:
119+
# TODO: handle unable to lock in a nicer way
120+
raise
121+
122+
def dump(self, cache_file):
123+
"""
124+
Dump this package cache on disk at ``cache_file``.
125+
"""
126+
package_cache = {}
127+
with open(cache_file, 'w') as f:
128+
json.dump(package_cache, f)
129+
130+
131+
def get_prematchers_from_glob_pattern(pattern):
132+
return [
133+
prematcher.lower().lstrip("/")
134+
for prematcher in pattern.split("*")
135+
if prematcher
136+
]
137+
138+
139+
def build_mappings_and_multiregex_patterns(
140+
datafile_handlers,
141+
):
142+
"""
143+
Return an index built from rules and licenses directories
144+
"""
145+
with_patterns = []
146+
147+
for handler in datafile_handlers:
148+
if handler.path_patterns:
149+
with_patterns.append(handler)
150+
151+
handler_by_regex = {}
152+
prematchers_by_regex = {}
153+
154+
for handler in with_patterns:
155+
for pattern in handler.path_patterns:
156+
regex_pattern = fnmatch.translate(pattern)
157+
regex_pattern = fr"{regex_pattern}"
158+
159+
prematchers_by_regex[regex_pattern] = get_prematchers_from_glob_pattern(pattern)
160+
161+
if regex_pattern in handler_by_regex:
162+
handler_by_regex[regex_pattern].append(handler.datasource_id)
163+
else:
164+
handler_by_regex[regex_pattern]= [handler.datasource_id]
165+
166+
multiregex_patterns = []
167+
for regex in handler_by_regex.keys():
168+
regex_and_prematcher = (regex, prematchers_by_regex.get(regex, []))
169+
multiregex_patterns.append(regex_and_prematcher)
170+
171+
return handler_by_regex, multiregex_patterns
172+
173+
174+
def get_cache(
175+
force=False,
176+
):
177+
"""
178+
Return a PkgManifestPatternsCache either rebuilt, cached or loaded from disk.
179+
"""
180+
global _PACKAGE_CACHE
181+
182+
if force or not _PACKAGE_CACHE:
183+
_PACKAGE_CACHE = PkgManifestPatternsCache.load_or_build(
184+
packagedcode_cache_dir=packagedcode_cache_dir,
185+
scancode_cache_dir=scancode_cache_dir,
186+
force=force,
187+
# used for testing only
188+
timeout=PACKAGE_INDEX_LOCK_TIMEOUT,
189+
)
190+
return _PACKAGE_CACHE
191+
192+
193+
def load_cache_file(cache_file):
194+
"""
195+
Return a PkgManifestPatternsCache loaded from JSON ``cache_file``.
196+
"""
197+
with open(cache_file) as f:
198+
cache = json.load(f)
199+
200+
return PkgManifestPatternsCache.from_mapping(cache)

src/packagedcode/recognize.py

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@
1010
import os
1111
import sys
1212

13+
import multiregex
14+
1315
from commoncode import filetype
14-
from packagedcode import APPLICATION_PACKAGE_DATAFILE_HANDLERS
15-
from packagedcode import SYSTEM_PACKAGE_DATAFILE_HANDLERS
16-
from packagedcode import ALL_DATAFILE_HANDLERS
16+
from commoncode.fileutils import as_posixpath
17+
18+
from packagedcode import HANDLER_BY_DATASOURCE_ID
19+
from packagedcode import BINARY_HANDLERS_PRESENT
20+
from packagedcode import BINARY_PACKAGE_DATAFILE_HANDLERS
1721
from packagedcode import models
22+
from packagedcode.cache import get_cache
1823

1924
TRACE = os.environ.get('SCANCODE_DEBUG_PACKAGE_API', False)
2025

@@ -56,25 +61,19 @@ def recognize_package_data(
5661
if not filetype.is_file(location):
5762
return []
5863

59-
assert application or system or package_only
60-
if package_only or (application and system):
61-
datafile_handlers = ALL_DATAFILE_HANDLERS
62-
elif application:
63-
datafile_handlers = APPLICATION_PACKAGE_DATAFILE_HANDLERS
64-
elif system:
65-
datafile_handlers = SYSTEM_PACKAGE_DATAFILE_HANDLERS
66-
6764
return list(_parse(
6865
location=location,
6966
package_only=package_only,
70-
datafile_handlers=datafile_handlers,
67+
application=application,
68+
system=system,
7169
))
7270

7371

7472
def _parse(
7573
location,
74+
application=True,
75+
system=False,
7676
package_only=False,
77-
datafile_handlers=APPLICATION_PACKAGE_DATAFILE_HANDLERS,
7877
):
7978
"""
8079
Yield parsed PackageData objects from ``location``. Raises Exceptions on errors.
@@ -83,6 +82,41 @@ def _parse(
8382
Default to use application packages
8483
"""
8584

85+
package_path = as_posixpath(location)
86+
package_patterns = get_cache()
87+
88+
assert application or system or package_only
89+
if package_only or (application and system):
90+
multiregex_patterns = package_patterns.all_multiregex_patterns
91+
elif application:
92+
multiregex_patterns = package_patterns.application_multiregex_patterns
93+
elif system:
94+
multiregex_patterns = package_patterns.system_multiregex_patterns
95+
96+
package_matcher = multiregex.RegexMatcher(multiregex_patterns)
97+
matched_patterns = package_matcher.match(package_path)
98+
99+
datafile_handlers = []
100+
for matched_pattern in matched_patterns:
101+
regex, _match = matched_pattern
102+
handler_ids = package_patterns.handler_by_regex.get(regex.pattern)
103+
if TRACE:
104+
logger_debug(f'_parse:.handler_ids: {handler_ids}')
105+
106+
datafile_handlers = [
107+
HANDLER_BY_DATASOURCE_ID.get(handler_id)
108+
for handler_id in handler_ids
109+
]
110+
111+
if not datafile_handlers:
112+
if BINARY_HANDLERS_PRESENT:
113+
datafile_handlers = BINARY_PACKAGE_DATAFILE_HANDLERS
114+
else:
115+
if TRACE:
116+
logger_debug(f'_parse: no package datafile detected at {package_path}')
117+
118+
return
119+
86120
for handler in datafile_handlers:
87121
if TRACE:
88122
logger_debug(f'_parse:.is_datafile: {handler}')

src/scancode_config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,13 @@ def _create_dir(location):
185185
__env_license_cache_dir = os.getenv('SCANCODE_LICENSE_INDEX_CACHE')
186186
licensedcode_cache_dir = (__env_license_cache_dir or std_license_cache_dir)
187187

188+
189+
std_package_cache_dir = join(scancode_src_dir, 'packagedcode', 'data', 'cache')
190+
__env_package_cache_dir = os.getenv('SCANCODE_PACKAGE_INDEX_CACHE')
191+
packagedcode_cache_dir = (__env_package_cache_dir or std_package_cache_dir)
192+
188193
_create_dir(licensedcode_cache_dir)
194+
_create_dir(packagedcode_cache_dir)
189195
_create_dir(scancode_cache_dir)
190196

191197
# - scancode_temp_dir: for short-lived temporary files which are import- or run-

0 commit comments

Comments
 (0)