fosslight-source 2.3.2__tar.gz → 2.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {fosslight_source-2.3.2/src/fosslight_source.egg-info → fosslight_source-2.3.4}/PKG-INFO +3 -2
  2. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/pyproject.toml +3 -2
  3. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source/_help.py +1 -0
  4. fosslight_source-2.3.4/src/fosslight_source/_merge.py +269 -0
  5. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source/_scan_item.py +1 -3
  6. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source/cli.py +62 -37
  7. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source/run_scancode.py +196 -69
  8. {fosslight_source-2.3.2 → fosslight_source-2.3.4/src/fosslight_source.egg-info}/PKG-INFO +3 -2
  9. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source.egg-info/SOURCES.txt +1 -0
  10. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source.egg-info/requires.txt +2 -1
  11. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/LICENSE +0 -0
  12. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/MANIFEST.in +0 -0
  13. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/README.md +0 -0
  14. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/setup.cfg +0 -0
  15. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source/__init__.py +0 -0
  16. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source/_kb_client.py +0 -0
  17. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source/_license_matched.py +0 -0
  18. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source/_parsing_scancode_file_item.py +0 -0
  19. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source/_parsing_scanoss_file.py +0 -0
  20. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source/run_manifest_extractor.py +0 -0
  21. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source/run_scanoss.py +0 -0
  22. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source/run_spdx_extractor.py +0 -0
  23. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source.egg-info/dependency_links.txt +0 -0
  24. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source.egg-info/entry_points.txt +0 -0
  25. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/src/fosslight_source.egg-info/top_level.txt +0 -0
  26. {fosslight_source-2.3.2 → fosslight_source-2.3.4}/tests/test_tox.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fosslight_source
3
- Version: 2.3.2
3
+ Version: 2.3.4
4
4
  Summary: FOSSLight Source Scanner
5
5
  Author: LG Electronics
6
6
  License-Expression: Apache-2.0
@@ -20,12 +20,13 @@ Requires-Dist: setuptools<=80.10.2
20
20
  Requires-Dist: pyparsing
21
21
  Requires-Dist: scanoss>=1.19.0
22
22
  Requires-Dist: XlsxWriter
23
- Requires-Dist: fosslight_util>=2.1.37
23
+ Requires-Dist: fosslight_util>=2.2.2
24
24
  Requires-Dist: PyYAML
25
25
  Requires-Dist: wheel>=0.38.1
26
26
  Requires-Dist: intbitset
27
27
  Requires-Dist: fosslight_binary>=5.1.22
28
28
  Requires-Dist: scancode-toolkit>=32.0.2
29
+ Requires-Dist: lxml>=6.0.1
29
30
  Requires-Dist: cryptography<49; platform_system == "Darwin" and platform_machine == "x86_64"
30
31
  Requires-Dist: fingerprints==1.2.3
31
32
  Requires-Dist: normality==2.6.1
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "fosslight_source"
10
- version = "2.3.2"
10
+ version = "2.3.4"
11
11
  description = "FOSSLight Source Scanner"
12
12
  readme = "README.md"
13
13
  license = "Apache-2.0"
@@ -29,12 +29,13 @@ dependencies = [
29
29
  "pyparsing",
30
30
  "scanoss>=1.19.0",
31
31
  "XlsxWriter",
32
- "fosslight_util>=2.1.37",
32
+ "fosslight_util>=2.2.2",
33
33
  "PyYAML",
34
34
  "wheel>=0.38.1",
35
35
  "intbitset",
36
36
  "fosslight_binary>=5.1.22",
37
37
  "scancode-toolkit>=32.0.2",
38
+ "lxml>=6.0.1",
38
39
  # cryptography 49.x does not provide macOS x86_64 wheels, causing source builds to require OpenSSL/pkg-config.
39
40
  "cryptography<49; platform_system == 'Darwin' and platform_machine == 'x86_64'",
40
41
  "fingerprints==1.2.3",
@@ -39,6 +39,7 @@ _HELP_MESSAGE_SOURCE_SCANNER = f"""
39
39
  -c <number> Number of CPU cores/threads to use for scanning
40
40
  -t <seconds> Timeout in seconds for ScanCode scanning
41
41
  -j Generate raw scanner results in JSON format
42
+ --no_merge Keep source paths file-based without folder merge
42
43
  --no_correction Skip OSS information correction with sbom-info.yaml
43
44
  --correct_fpath <path> Path to custom sbom-info.yaml file
44
45
  --hide_progress Hide the progress bar during scanning
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # Copyright (c) 2020 LG Electronics Inc.
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ import os
7
+ from collections import Counter
8
+
9
+ from fosslight_util.constant import COMMENT_DELIMITER
10
+
11
+ from ._scan_item import SourceItem
12
+
13
+
14
+ def _get_source_rows_to_print(source_items: list) -> list:
15
+ source_rows = []
16
+ for source_item in source_items:
17
+ source_rows.extend(source_item.get_print_array())
18
+ return source_rows
19
+
20
+
21
+ def _add_pre_merge_sheet(scan_item, pre_merge_sheet_name: str, header_row: list, pkg_name: str) -> None:
22
+ external_sheets = getattr(scan_item, "external_sheets", {}) or {}
23
+ external_sheets[pre_merge_sheet_name] = [
24
+ header_row,
25
+ *_get_source_rows_to_print(scan_item.file_items.get(pkg_name, [])),
26
+ ]
27
+ scan_item.external_sheets = external_sheets
28
+
29
+
30
+ def _normalize_merge_text(value: str) -> str:
31
+ return value.strip() if value else ""
32
+
33
+
34
+ def _iter_merge_values(values) -> list:
35
+ if not values:
36
+ return []
37
+ if isinstance(values, str):
38
+ return [values]
39
+ return values
40
+
41
+
42
+ def _normalize_merge_licenses(licenses) -> tuple:
43
+ return tuple(sorted([lic.strip() for lic in _iter_merge_values(licenses) if lic and lic.strip()]))
44
+
45
+
46
+ def _normalize_merge_downloads(downloads) -> tuple:
47
+ if not downloads:
48
+ return ()
49
+ if isinstance(downloads, str):
50
+ downloads = [downloads]
51
+ return tuple(sorted([dl.strip() for dl in downloads if dl and dl.strip()]))
52
+
53
+
54
+ def _iter_merge_rows(item: SourceItem):
55
+ if item.oss_items:
56
+ for oss_item in item.oss_items:
57
+ name = _normalize_merge_text(oss_item.name) or _normalize_merge_text(item.oss_name)
58
+ version = _normalize_merge_text(oss_item.version) or _normalize_merge_text(item.oss_version)
59
+ licenses = _normalize_merge_licenses(oss_item.license or item.licenses)
60
+ downloads = _normalize_merge_downloads(oss_item.download_location or item.download_location)
61
+ yield name, version, licenses, downloads
62
+ else:
63
+ yield (
64
+ _normalize_merge_text(item.oss_name),
65
+ _normalize_merge_text(item.oss_version),
66
+ _normalize_merge_licenses(item.licenses),
67
+ _normalize_merge_downloads(item.download_location),
68
+ )
69
+
70
+
71
+ def _get_item_oss_name(item: SourceItem) -> str:
72
+ for name, _, _, _ in _iter_merge_rows(item):
73
+ if name:
74
+ return name
75
+ return ""
76
+
77
+
78
+ def _get_item_oss_version(item: SourceItem) -> str:
79
+ for _, version, _, _ in _iter_merge_rows(item):
80
+ if version:
81
+ return version
82
+ return ""
83
+
84
+
85
+ def _get_merge_licenses(scan_item: SourceItem) -> tuple:
86
+ for _, _, licenses, _ in _iter_merge_rows(scan_item):
87
+ if licenses:
88
+ return licenses
89
+ return ()
90
+
91
+
92
+ def _get_merge_download_locations(scan_item: SourceItem) -> tuple:
93
+ for _, _, _, downloads in _iter_merge_rows(scan_item):
94
+ if downloads:
95
+ return downloads
96
+ return ()
97
+
98
+
99
+ def _get_merge_field_value(scan_items: list, value_getter):
100
+ for scan_item in scan_items:
101
+ value = value_getter(scan_item)
102
+ if value:
103
+ return value
104
+ return ""
105
+
106
+
107
+ def _get_top_merge_values(scan_items: list, value_getter) -> list:
108
+ values = []
109
+ for scan_item in scan_items:
110
+ for value in _iter_merge_values(value_getter(scan_item)):
111
+ normalized_value = _normalize_merge_text(value)
112
+ if normalized_value:
113
+ values.append(normalized_value)
114
+ return [value for value, _ in Counter(values).most_common(3)]
115
+
116
+
117
+ def _can_merge_folder(scan_items: list) -> bool:
118
+ if len(scan_items) <= 1:
119
+ return False
120
+
121
+ ref_name = None
122
+ ref_version = None
123
+ ref_licenses = None
124
+ ref_downloads = None
125
+
126
+ for item in scan_items:
127
+ for name, version, licenses, downloads in _iter_merge_rows(item):
128
+ if name:
129
+ if ref_name is None:
130
+ ref_name = name
131
+ elif name != ref_name:
132
+ return False
133
+
134
+ if version:
135
+ if ref_version is None:
136
+ ref_version = version
137
+ elif version != ref_version:
138
+ return False
139
+
140
+ if licenses:
141
+ if ref_licenses is None:
142
+ ref_licenses = licenses
143
+ elif licenses != ref_licenses:
144
+ return False
145
+
146
+ if downloads:
147
+ if ref_downloads is None:
148
+ ref_downloads = downloads
149
+ elif downloads != ref_downloads:
150
+ return False
151
+
152
+ return True
153
+
154
+
155
+ def _get_merged_comments(scan_items: list) -> str:
156
+ comments = []
157
+ for item in scan_items:
158
+ val = item.comment
159
+ if val:
160
+ parts = [p.strip() for p in val.split(COMMENT_DELIMITER) if p.strip()]
161
+ for p in parts:
162
+ if p not in comments:
163
+ comments.append(p)
164
+ if not comments:
165
+ return ""
166
+
167
+ return COMMENT_DELIMITER.join(comments)
168
+
169
+
170
+ def _create_merged_item(scan_items: list, merge_path: str) -> SourceItem:
171
+ merged_path = f"{merge_path} ({len(scan_items)})"
172
+ merged_item = SourceItem(merged_path)
173
+ merged_item.oss_name = _get_merge_field_value(scan_items, lambda item: _normalize_merge_text(_get_item_oss_name(item)))
174
+ merged_item.oss_version = _get_merge_field_value(scan_items, lambda item: _normalize_merge_text(_get_item_oss_version(item)))
175
+ merged_licenses = _get_merge_field_value(scan_items, _get_merge_licenses)
176
+ merged_item.licenses = list(merged_licenses) if merged_licenses else []
177
+ merged_downloads = _get_merge_field_value(scan_items, _get_merge_download_locations)
178
+ merged_item.download_location = list(merged_downloads) if merged_downloads else []
179
+ merged_copyrights = _get_top_merge_values(scan_items, lambda item: item.copyright)
180
+ merged_item.copyright = merged_copyrights if merged_copyrights else []
181
+ merged_comments = _get_merged_comments(scan_items)
182
+ if merged_comments:
183
+ merged_item.comment = merged_comments
184
+ merged_item.set_oss_item()
185
+ return merged_item
186
+
187
+
188
+ def merge_results_by_folder(scan_result: list) -> list:
189
+ """
190
+ Merge output rows within the same folder when OSS name, OSS version, license,
191
+ and download location are compatible.
192
+
193
+ A field is compatible when all non-empty values across rows are identical
194
+ (empty values are ignored). Each oss_item is treated as its own row. All eligible
195
+ rows in the folder must be compatible together; rows are not grouped by key subsets.
196
+ """
197
+ # Build a folder tree first so merge never jumps straight to root ".".
198
+ merge_tree = {"items": [], "children": {}}
199
+
200
+ for scan_item in scan_result:
201
+ normalized_path = os.path.normpath(scan_item.source_name_or_path).replace("\\", "/")
202
+ path_parts = [part for part in normalized_path.split("/") if part and part != "."]
203
+ current_node = merge_tree
204
+
205
+ for folder_name in path_parts[:-1]:
206
+ current_node = current_node["children"].setdefault(folder_name, {"items": [], "children": {}})
207
+ current_node["items"].append(scan_item)
208
+
209
+ def merge_node(merge_node_item: dict, merge_path: str = "", depth: int = 0) -> tuple:
210
+ child_finalized = []
211
+ child_unfinalized = []
212
+ for folder_name, child_node in merge_node_item["children"].items():
213
+ child_path = f"{merge_path}/{folder_name}" if merge_path else folder_name
214
+ fin, unfin = merge_node(child_node, child_path, depth + 1)
215
+ child_finalized.extend(fin)
216
+ child_unfinalized.extend(unfin)
217
+
218
+ local_excluded = [item for item in merge_node_item["items"] if item.exclude]
219
+ local_eligible = [item for item in merge_node_item["items"] if not item.exclude]
220
+
221
+ all_eligible_candidates = list(local_eligible)
222
+ for _, g_items in child_unfinalized:
223
+ all_eligible_candidates.extend(g_items)
224
+
225
+ # We can merge under the current node if depth > 0 and we are combining multiple sources:
226
+ # e.g., local files + child groups, or multiple child groups, or multiple local files.
227
+ can_merge_here = (depth > 0) and (len(local_eligible) + len(child_unfinalized) > 1)
228
+
229
+ if can_merge_here:
230
+ if _can_merge_folder(all_eligible_candidates):
231
+ new_finalized = child_finalized + local_excluded
232
+ new_unfinalized = [(merge_path, all_eligible_candidates)]
233
+ return new_finalized, new_unfinalized
234
+ else:
235
+ # Compatibility broke at this level. We must finalize the subtrees.
236
+ finalized_child_groups = []
237
+ for c_path, c_items in child_unfinalized:
238
+ if len(c_items) > 1 and _can_merge_folder(c_items):
239
+ finalized_child_groups.append(_create_merged_item(c_items, c_path))
240
+ else:
241
+ finalized_child_groups.extend(c_items)
242
+
243
+ finalized_local = []
244
+ if len(local_eligible) > 1 and _can_merge_folder(local_eligible):
245
+ finalized_local.append(_create_merged_item(local_eligible, merge_path))
246
+ else:
247
+ finalized_local.extend(local_eligible)
248
+
249
+ new_finalized = child_finalized + local_excluded + finalized_child_groups + finalized_local
250
+ new_unfinalized = []
251
+ return new_finalized, new_unfinalized
252
+ else:
253
+ # We cannot merge or don't need to merge at this level (e.g. depth == 0 or only 1 source).
254
+ # We propagate everything up as unfinalized.
255
+ new_finalized = child_finalized + local_excluded
256
+ new_unfinalized = list(child_unfinalized)
257
+ if local_eligible:
258
+ new_unfinalized.append((merge_path, local_eligible))
259
+ return new_finalized, new_unfinalized
260
+
261
+ fin, unfin = merge_node(merge_tree)
262
+ finalized_results = list(fin)
263
+ for path, items in unfin:
264
+ if path and len(items) > 1 and _can_merge_folder(items):
265
+ finalized_results.append(_create_merged_item(items, path))
266
+ else:
267
+ finalized_results.extend(items)
268
+
269
+ return finalized_results
@@ -48,7 +48,6 @@ class SourceItem(FileItem):
48
48
  self.source_name_or_path = value
49
49
  self.is_license_text = False
50
50
  self.is_manifest_file = False
51
- self.license_reference = ""
52
51
  self.scanoss_reference = {}
53
52
  self.matched_lines = "" # Only for SCANOSS results
54
53
  self.fileURL = "" # Only for SCANOSS results
@@ -206,8 +205,7 @@ class SourceItem(FileItem):
206
205
  for item in self.oss_items:
207
206
  print_rows.append([self.source_name_or_path, item.name, item.version, ",".join(item.license),
208
207
  item.download_location, "",
209
- item.copyright, "Exclude" if self.exclude else "", item.comment,
210
- self.license_reference])
208
+ item.copyright, "Exclude" if self.exclude else "", item.comment])
211
209
  return print_rows
212
210
 
213
211
  def __eq__(self, other: object) -> bool:
@@ -12,12 +12,12 @@ import logging
12
12
  import re
13
13
  import urllib.request
14
14
  import urllib.error
15
- from datetime import datetime
16
15
  import fosslight_util.constant as constant
17
16
  from fosslight_util.set_log import init_log
18
17
  from ._help import print_version, print_help_msg_source_scanner
19
18
  from ._license_matched import get_license_list_to_print
20
19
  from fosslight_util.output_format import check_output_formats_v2, write_output_file
20
+ from fosslight_util.write_excel import get_header_row
21
21
  from fosslight_util.correct import correct_with_yaml
22
22
  from fosslight_util.parsing_yaml import SUPPORT_OSS_INFO_FILES
23
23
  from .run_scancode import run_scan
@@ -31,19 +31,20 @@ from .run_spdx_extractor import get_spdx_downloads
31
31
  from .run_manifest_extractor import get_manifest_licenses
32
32
  from ._scan_item import SourceItem, resolve_kb_config, is_notice_file
33
33
  from ._kb_client import fetch_origin_urls_via_scan_job
34
+ from fosslight_util.cover import dump_result_log
35
+ from fosslight_util.time import current_timestamp_utc, format_running_time, timestamp_for_filename
34
36
  from fosslight_util.oss_item import ScannerItem
35
37
  from typing import Optional, Tuple
36
38
  from ._scan_item import is_manifest_file
37
39
  import shutil
40
+ from ._merge import (
41
+ _add_pre_merge_sheet,
42
+ merge_results_by_folder
43
+ )
38
44
 
39
45
 
40
46
  SRC_SHEET_NAME = 'SRC_FL_Source'
41
- SCANOSS_HEADER = {SRC_SHEET_NAME: ['ID', 'Source Path', 'OSS Name',
42
- 'OSS Version', 'License', 'Download Location',
43
- 'Homepage', 'Copyright Text', 'Exclude', 'Comment']}
44
- MERGED_HEADER = {SRC_SHEET_NAME: ['ID', 'Source Path', 'OSS Name',
45
- 'OSS Version', 'License', 'Download Location',
46
- 'Homepage', 'Copyright Text', 'Exclude', 'Comment', 'license_reference']}
47
+ PRE_MERGE_SHEET_NAME = '.SRC_FL_Source_no_merge'
47
48
  KB_REFERENCE_HEADER = ['ID', 'Source Path', 'KB Origin URL', 'Evidence']
48
49
  ALL_MODE = 'all'
49
50
  SCANNER_TYPE = ['kb', 'scancode', 'scanoss', ALL_MODE]
@@ -88,6 +89,7 @@ def main() -> None:
88
89
  parser.add_argument('--hide_progress', action='store_true', required=False)
89
90
  parser.add_argument('--kb_url', type=str, required=False, default="")
90
91
  parser.add_argument('--kb_token', type=str, required=False, default="")
92
+ parser.add_argument('--no_merge', action='store_true', required=False)
91
93
 
92
94
  args = parser.parse_args()
93
95
 
@@ -118,19 +120,19 @@ def main() -> None:
118
120
  hide_progress = args.hide_progress
119
121
  kb_url = args.kb_url
120
122
  kb_token = args.kb_token
123
+ merge_by_folder = not args.no_merge
121
124
 
122
125
  time_out = args.timeout
123
126
  core = args.cores
124
127
 
125
128
  if os.path.isdir(path_to_scan):
126
- result = []
127
129
  result = run_scanners(path_to_scan, output_file_name, write_json_file, core, True,
128
130
  print_matched_text, formats, time_out, correct_mode, correct_filepath,
129
131
  selected_scanner, path_to_exclude, hide_progress=hide_progress,
130
- kb_url=kb_url, kb_token=kb_token)
132
+ kb_url=kb_url, kb_token=kb_token,
133
+ merge_by_folder=merge_by_folder)
131
134
 
132
135
  _result_log["Scan Result"] = result[1]
133
-
134
136
  try:
135
137
  logger.info(yaml.safe_dump(_result_log, allow_unicode=True, sort_keys=True))
136
138
  except Exception as ex:
@@ -148,7 +150,7 @@ def create_report_file(
148
150
  output_extensions: list = [], correct_mode: bool = True,
149
151
  correct_filepath: str = "", path_to_scan: str = "", path_to_exclude: list = [],
150
152
  formats: list = [], api_limit_exceed: bool = False, files_count: int = 0, final_output_path: str = "",
151
- run_kb_msg: str = ""
153
+ run_kb_msg: str = "", merge_by_folder: bool = True
152
154
  ) -> 'ScannerItem':
153
155
  """
154
156
  Create report files for given scanned result.
@@ -158,9 +160,9 @@ def create_report_file(
158
160
  :param license_list: matched text (only for scancode).
159
161
  :param need_license: if requested, output matched text (only for scancode).
160
162
  """
161
- extended_header = {}
162
163
  sheet_list = {}
163
164
  _json_ext = ".json"
165
+ name_time = timestamp_for_filename(_start_time)
164
166
 
165
167
  output_path = os.path.abspath(output_path)
166
168
 
@@ -178,19 +180,19 @@ def create_report_file(
178
180
  to_remove.append(i)
179
181
  else:
180
182
  if formats[i].startswith('spdx'):
181
- output_files[i] = f"fosslight_spdx_src_{_start_time}"
183
+ output_files[i] = f"fosslight_spdx_src_{name_time}"
182
184
  elif formats[i].startswith('cyclonedx'):
183
- output_files[i] = f'fosslight_cyclonedx_src_{_start_time}'
185
+ output_files[i] = f'fosslight_cyclonedx_src_{name_time}'
184
186
  else:
185
187
  if output_extension == _json_ext:
186
- output_files[i] = f"fosslight_opossum_src_{_start_time}"
188
+ output_files[i] = f"fosslight_opossum_src_{name_time}"
187
189
  else:
188
- output_files[i] = f"fosslight_report_src_{_start_time}"
190
+ output_files[i] = f"fosslight_report_src_{name_time}"
189
191
  else:
190
192
  if output_extension == _json_ext:
191
- output_files[i] = f"fosslight_opossum_src_{_start_time}"
193
+ output_files[i] = f"fosslight_opossum_src_{name_time}"
192
194
  else:
193
- output_files[i] = f"fosslight_report_src_{_start_time}"
195
+ output_files[i] = f"fosslight_report_src_{name_time}"
194
196
  for index in sorted(to_remove, reverse=True):
195
197
  # remove elements of spdx format on windows
196
198
  del output_files[index]
@@ -228,11 +230,6 @@ def create_report_file(
228
230
  sheet_list = {}
229
231
  scan_item.append_file_items(merged_result, PKG_NAME)
230
232
 
231
- if selected_scanner == 'scanoss':
232
- extended_header = SCANOSS_HEADER
233
- else:
234
- extended_header = MERGED_HEADER
235
-
236
233
  if need_license:
237
234
  if selected_scanner == 'scancode':
238
235
  sheet_list["scancode_reference"] = get_license_list_to_print(license_list)
@@ -258,12 +255,24 @@ def create_report_file(
258
255
  scan_item = correct_item
259
256
  logger.info("Success to correct with yaml.")
260
257
 
258
+ finish_time = current_timestamp_utc()
259
+ scan_item.set_cover_finish_time(finish_time)
260
+
261
+ if merged_result and merge_by_folder:
262
+ _add_pre_merge_sheet(scan_item, PRE_MERGE_SHEET_NAME, get_header_row(SRC_SHEET_NAME), PKG_NAME)
263
+ _merge_start = time.time()
264
+ scan_item.file_items[PKG_NAME] = merge_results_by_folder(scan_item.file_items[PKG_NAME])
265
+ logger.debug(f"[TIMING] merge_results_by_folder: {time.time() - _merge_start:.4f}s")
266
+
261
267
  combined_paths_and_files = [os.path.join(output_path, file) for file in output_files]
262
268
  results = []
263
269
  for combined_path_and_file, output_extension, output_format in zip(combined_paths_and_files, output_extensions, formats):
264
270
  # if need_license and output_extension == _json_ext and "scanoss_reference" in sheet_list:
265
271
  # del sheet_list["scanoss_reference"]
266
- results.append(write_output_file(combined_path_and_file, output_extension, scan_item, extended_header, "", output_format))
272
+ result = write_output_file(
273
+ combined_path_and_file, output_extension, scan_item, hide_header="", format=output_format
274
+ )
275
+ results.append(result)
267
276
  for success, msg, result_file in results:
268
277
  final_result_file = result_file.replace(output_path, final_output_path)
269
278
  if success:
@@ -492,7 +501,8 @@ def run_scanners(
492
501
  formats: list = [], time_out: int = 120,
493
502
  correct_mode: bool = True, correct_filepath: str = "",
494
503
  selected_scanner: str = ALL_MODE, path_to_exclude: list = [],
495
- all_exclude_mode: tuple = (), hide_progress: bool = False, kb_url: str = "", kb_token: str = ""
504
+ all_exclude_mode: tuple = (), hide_progress: bool = False,
505
+ kb_url: str = "", kb_token: str = "", merge_by_folder: bool = True
496
506
  ) -> Tuple[bool, str, 'ScannerItem', list, list]:
497
507
  """
498
508
  Run Scancode and scanoss.py for the given path.
@@ -513,7 +523,8 @@ def run_scanners(
513
523
  """
514
524
  global logger
515
525
 
516
- start_time = datetime.now().strftime('%y%m%d_%H%M')
526
+ start_time = current_timestamp_utc()
527
+ file_time = timestamp_for_filename(start_time)
517
528
  scancode_result = []
518
529
  scanoss_result = []
519
530
  merged_result = []
@@ -529,13 +540,13 @@ def run_scanners(
529
540
  if output_path == "":
530
541
  output_path = os.getcwd()
531
542
  final_output_path = output_path
532
- output_path = os.path.join(os.path.dirname(output_path), f'.fosslight_temp_{start_time}')
543
+ output_path = os.path.join(os.path.dirname(output_path), f'.fosslight_temp_{file_time}')
533
544
  publish_temp_output = False
534
545
  logger = None
535
546
  publish_ok = True
536
547
 
537
548
  try:
538
- logger, result_log = init_log(os.path.join(output_path, f"fosslight_log_src_{start_time}.txt"),
549
+ logger, result_log = init_log(os.path.join(output_path, f"fosslight_log_src_{file_time}.txt"),
539
550
  True, logging.INFO, logging.DEBUG, PKG_NAME, path_to_scan, path_to_exclude)
540
551
 
541
552
  logger.info(f"Tool Info : {result_log['Tool Info']}")
@@ -545,6 +556,11 @@ def run_scanners(
545
556
  print_matched_text = False
546
557
 
547
558
  if success:
559
+ has_sbom_format = any(f.startswith('spdx') or f.startswith('cyclonedx') for f in formats) if formats else False
560
+ if has_sbom_format and merge_by_folder:
561
+ logger.info("SPDX/CycloneDX format does not support merge. Merging is not performed.")
562
+ merge_by_folder = False
563
+
548
564
  if all_exclude_mode and len(all_exclude_mode) == 4:
549
565
  (excluded_path_with_default_exclusion,
550
566
  excluded_path_without_dot,
@@ -556,7 +572,7 @@ def run_scanners(
556
572
  excluded_path_without_dot,
557
573
  excluded_files,
558
574
  cnt_file_except_skipped) = get_excluded_paths(path_to_scan, path_to_exclude_with_filename)
559
- logger.debug(f"Skipped paths: {excluded_path_with_default_exclusion}")
575
+ logger.debug(f"Skipped paths count: {len(excluded_path_with_default_exclusion)}")
560
576
 
561
577
  if not selected_scanner:
562
578
  selected_scanner = ALL_MODE
@@ -564,8 +580,8 @@ def run_scanners(
564
580
  success, result_log[RESULT_KEY], scancode_result, license_list = run_scan(
565
581
  path_to_scan, output_file_name, write_json_file, num_cores, True,
566
582
  print_matched_text, formats, called_by_cli, time_out, correct_mode,
567
- correct_filepath, excluded_path_with_default_exclusion,
568
- excluded_files, hide_progress,
583
+ correct_filepath, path_to_exclude,
584
+ hide_progress=hide_progress,
569
585
  )
570
586
  excluded_files = set(excluded_files) if excluded_files else set()
571
587
  if selected_scanner in ['scanoss', ALL_MODE]:
@@ -582,7 +598,7 @@ def run_scanners(
582
598
  run_kb_msg = f"KB({kb_url}) Unreachable"
583
599
 
584
600
  spdx_downloads, manifest_licenses = metadata_collector(path_to_scan, excluded_files)
585
- merged_result, kb_status_message, kb_requested_count, kb_returned_count = merge_results(
601
+ merged_result, kb_status_message, kb_requested_count, _ = merge_results(
586
602
  scancode_result, scanoss_result, spdx_downloads,
587
603
  path_to_scan, run_kb, manifest_licenses, excluded_files,
588
604
  hide_progress, kb_url, kb_token,
@@ -590,15 +606,13 @@ def run_scanners(
590
606
  if kb_status_message:
591
607
  run_kb_msg = f"KB({kb_url}) {kb_status_message}"
592
608
  elif run_kb and kb_requested_count > 0:
593
- run_kb_msg = (
594
- f"KB({kb_url}) response : {kb_returned_count}/"
595
- f" requested: {kb_requested_count}"
596
- )
609
+ run_kb_msg = f"KB({kb_url}) : Completed"
597
610
  mark_oss_info_correction_files_as_excluded(merged_result)
598
611
  scan_item = create_report_file(start_time, merged_result, license_list, scanoss_result, selected_scanner,
599
612
  print_matched_text, output_path, output_files, output_extensions, correct_mode,
600
613
  correct_filepath, path_to_scan, excluded_path_without_dot, formats,
601
- api_limit_exceed, cnt_file_except_skipped, final_output_path, run_kb_msg)
614
+ api_limit_exceed, cnt_file_except_skipped, final_output_path, run_kb_msg,
615
+ merge_by_folder)
602
616
  else:
603
617
  print_help_msg_source_scanner()
604
618
  result_log[RESULT_KEY] = "Unsupported scanner"
@@ -619,6 +633,17 @@ def run_scanners(
619
633
  else "Failed to publish scan artifacts"
620
634
  )
621
635
 
636
+ if logger:
637
+ if scan_item and not isinstance(scan_item, list):
638
+ result_log["Running time"] = scan_item.cover.running_time
639
+ else:
640
+ finish_time = current_timestamp_utc()
641
+ result_log["Running time"] = format_running_time(start_time, finish_time)
642
+ try:
643
+ logger.info(dump_result_log(result_log))
644
+ except Exception as ex:
645
+ logger.warning(f"Failed to print result log. {ex}")
646
+
622
647
  return success, result_log.get(RESULT_KEY, ""), scan_item, license_list, scanoss_result
623
648
 
624
649
 
@@ -7,11 +7,10 @@ import os
7
7
  import multiprocessing
8
8
  import warnings
9
9
  import logging
10
- import yaml
11
10
  from scancode import cli
12
- from datetime import datetime
13
11
  import fosslight_util.constant as constant
14
12
  from fosslight_util.set_log import init_log
13
+ from fosslight_util.time import current_timestamp_utc, timestamp_for_filename
15
14
  from ._parsing_scancode_file_item import parsing_file_item
16
15
  from ._parsing_scancode_file_item import get_error_from_header
17
16
  from fosslight_util.output_format import check_output_formats_v2
@@ -23,7 +22,7 @@ from fosslight_util.exclude import (
23
22
  PACKAGE_DIRECTORY,
24
23
  )
25
24
  from commoncode.fileset import is_included
26
- from typing import Tuple, Iterable
25
+ from typing import Callable, Tuple
27
26
 
28
27
  logger = logging.getLogger(constant.LOGGER_NAME)
29
28
  warnings.filterwarnings("ignore", category=FutureWarning)
@@ -63,6 +62,96 @@ def _apply_scancode_unset_workaround(kwargs: dict) -> None:
63
62
  logger.debug("scancode UNSET workaround skipped: %s", ex)
64
63
 
65
64
 
65
+ _WILDCARD_EXTENSIONS = {
66
+ "png", "mp3", "wav", "comp", "bin", "o", "db", "tflite",
67
+ "ttf", "exe", "dll", "jpg", "jpeg", "gif",
68
+ "zip", "tar", "tgz", "gz",
69
+ "bmp", "webp", "ico",
70
+ } | {ext.lower() for ext in EXCLUDE_FILE_EXTENSION}
71
+ _SKIP_DIR_NAMES = frozenset(name.lower() for name in PACKAGE_DIRECTORY + EXCLUDE_DIRECTORY)
72
+ _SKIP_EXTS = frozenset(ext.lower() for ext in EXCLUDE_FILE_EXTENSION)
73
+
74
+
75
+ def _normalize_custom_pattern(pattern: str, abs_path_to_scan: str) -> set:
76
+ pat = pattern.replace('\\', '/').strip()
77
+ if not pat:
78
+ return set()
79
+
80
+ patterns_to_add = {pat}
81
+
82
+ if pat.endswith("/**"):
83
+ base = pat[:-3].rstrip("/")
84
+ if base:
85
+ patterns_to_add.add(base)
86
+ elif pat.endswith("/*"):
87
+ base = pat[:-2].rstrip("/")
88
+ if base:
89
+ patterns_to_add.add(base)
90
+ patterns_to_add.add(f"{base}/**")
91
+ elif pat.endswith("/"):
92
+ base = pat.rstrip("/")
93
+ if base:
94
+ patterns_to_add.add(base)
95
+ patterns_to_add.add(f"{base}/**")
96
+ patterns_to_add.add(f"{base}/*")
97
+ else:
98
+ full_path = os.path.join(abs_path_to_scan, pat)
99
+ if os.path.isdir(full_path):
100
+ patterns_to_add.add(f"{pat}/**")
101
+ patterns_to_add.add(f"{pat}/*")
102
+
103
+ return patterns_to_add
104
+
105
+
106
+ def _expand_custom_exclude_pattern(pattern: str, abs_path_to_scan: str) -> set:
107
+ exclude_path_normalized = os.path.normpath(
108
+ pattern.replace('\\', '/').strip()
109
+ ).replace("\\", "/")
110
+ if not exclude_path_normalized:
111
+ return set()
112
+
113
+ patterns = set(_normalize_custom_pattern(exclude_path_normalized, abs_path_to_scan))
114
+
115
+ if exclude_path_normalized.endswith("/**"):
116
+ base_dir = exclude_path_normalized[:-3].rstrip("/")
117
+ if base_dir:
118
+ full_exclude_path = os.path.join(abs_path_to_scan, base_dir)
119
+ if os.path.isdir(full_exclude_path):
120
+ patterns.add(base_dir)
121
+ patterns.add(exclude_path_normalized)
122
+ else:
123
+ patterns.add(exclude_path_normalized)
124
+ return patterns
125
+
126
+ has_glob_chars = any(char in exclude_path_normalized for char in ['*', '?', '['])
127
+ if has_glob_chars:
128
+ patterns.add(exclude_path_normalized)
129
+ return patterns
130
+
131
+ full_exclude_path = os.path.join(abs_path_to_scan, exclude_path_normalized)
132
+ if os.path.isdir(full_exclude_path):
133
+ base_path = exclude_path_normalized.rstrip("/")
134
+ if base_path:
135
+ patterns.add(base_path)
136
+ patterns.add(f"{base_path}/**")
137
+ else:
138
+ patterns.add(exclude_path_normalized)
139
+ elif os.path.isfile(full_exclude_path):
140
+ ext = os.path.splitext(exclude_path_normalized)[1].lstrip('.').lower()
141
+ if ext in _WILDCARD_EXTENSIONS:
142
+ patterns.add(f"*.{ext}")
143
+ else:
144
+ patterns.add(f"**/{exclude_path_normalized}")
145
+ else:
146
+ ext = os.path.splitext(exclude_path_normalized)[1].lstrip('.').lower()
147
+ if ext in _WILDCARD_EXTENSIONS:
148
+ patterns.add(f"*.{ext}")
149
+ else:
150
+ patterns.add(exclude_path_normalized)
151
+
152
+ return patterns
153
+
154
+
66
155
  def _directory_ignore_pattern(dir_name: str) -> str:
67
156
  """Path-based glob for a directory name (avoids matching the scan root itself)."""
68
157
  normalized = dir_name.strip().strip("/").replace("\\", "/")
@@ -71,7 +160,10 @@ def _directory_ignore_pattern(dir_name: str) -> str:
71
160
  return f"**/{normalized}/**"
72
161
 
73
162
 
74
- def _default_scancode_coarse_ignore_patterns() -> frozenset:
163
+ def _default_scancode_coarse_ignore_patterns(
164
+ path_to_exclude: list = None,
165
+ abs_path_to_scan: str = ""
166
+ ) -> frozenset:
75
167
  """
76
168
  Coarse ignore patterns aligned with fosslight_util.get_excluded_paths() rules.
77
169
  Directory names use path-based globs (e.g. **/tests/**) so they do not match
@@ -84,73 +176,93 @@ def _default_scancode_coarse_ignore_patterns() -> frozenset:
84
176
  patterns.add(f"*.{ext}")
85
177
  for name in EXCLUDE_FILENAME:
86
178
  patterns.add(name)
179
+
180
+ for pattern in path_to_exclude or []:
181
+ if os.path.isabs(pattern):
182
+ pattern = os.path.relpath(pattern, abs_path_to_scan)
183
+ patterns.update(_expand_custom_exclude_pattern(pattern, abs_path_to_scan))
184
+
87
185
  return frozenset(patterns)
88
186
 
89
187
 
90
- def _is_covered_by_coarse_ignore(rel_path: str, coarse_patterns: Iterable[str]) -> bool:
91
- excludes = {pattern: "" for pattern in coarse_patterns}
188
+ def _to_excludes_dict(patterns) -> dict:
189
+ return {pattern: "exclude" for pattern in patterns}
190
+
191
+
192
+ def _is_path_covered(rel_path: str, excludes: dict) -> bool:
92
193
  return not is_included(rel_path, includes={}, excludes=excludes)
93
194
 
94
195
 
95
- def _add_path_to_exclude_pattern(
196
+ def _add_ignore_pattern(
96
197
  patterns: set,
97
- exclude_path: str,
98
- abs_path_to_scan: str,
198
+ excludes: dict,
199
+ pattern: str,
200
+ *,
201
+ sample_path: str = None,
202
+ ) -> bool:
203
+ if pattern in patterns:
204
+ return False
205
+ if sample_path and _is_path_covered(sample_path, excludes):
206
+ return False
207
+ patterns.add(pattern)
208
+ excludes[pattern] = "exclude"
209
+ return True
210
+
211
+
212
+ def _make_pre_scan_skip_filter(
99
213
  coarse_patterns: frozenset,
214
+ ) -> Tuple[dict, Callable[[str, str], bool], Callable[[str, str], bool]]:
215
+ excludes = _to_excludes_dict(coarse_patterns)
216
+
217
+ def should_skip_dir(dir_name: str, rel_dir: str) -> bool:
218
+ if dir_name.startswith('.'):
219
+ return True
220
+ if dir_name.lower() in _SKIP_DIR_NAMES:
221
+ return True
222
+ return _is_path_covered(f"{rel_dir}/_", excludes)
223
+
224
+ def should_skip_file(file_name: str, rel_path: str) -> bool:
225
+ if file_name.startswith('.'):
226
+ return True
227
+ ext = os.path.splitext(file_name)[1].lstrip('.').lower()
228
+ if ext in _SKIP_EXTS:
229
+ return True
230
+ return _is_path_covered(rel_path, excludes)
231
+
232
+ return excludes, should_skip_dir, should_skip_file
233
+
234
+
235
+ def _add_binary_ignore_patterns(
236
+ patterns: set,
237
+ excludes: dict,
238
+ binary_paths: list,
100
239
  ) -> None:
101
- exclude_path_normalized = os.path.normpath(exclude_path).replace("\\", "/")
240
+ extensions = set()
241
+ no_ext_paths = []
102
242
 
103
- if exclude_path_normalized.endswith("/**"):
104
- base_dir = exclude_path_normalized[:-3].rstrip("/")
105
- if base_dir:
106
- full_exclude_path = os.path.join(abs_path_to_scan, base_dir)
107
- if os.path.isdir(full_exclude_path):
108
- patterns.add(base_dir)
109
- patterns.add(exclude_path_normalized)
110
- else:
111
- patterns.add(exclude_path_normalized)
243
+ for rel_path in binary_paths:
244
+ ext = os.path.splitext(rel_path)[1].lstrip('.').lower()
245
+ if ext:
246
+ extensions.add(ext)
112
247
  else:
113
- patterns.add(exclude_path_normalized)
114
- return
248
+ no_ext_paths.append(rel_path)
115
249
 
116
- has_glob_chars = any(char in exclude_path_normalized for char in ['*', '?', '['])
117
- if has_glob_chars:
118
- patterns.add(exclude_path_normalized)
119
- return
250
+ for ext in extensions:
251
+ _add_ignore_pattern(patterns, excludes, f"*.{ext}")
120
252
 
121
- full_exclude_path = os.path.join(abs_path_to_scan, exclude_path_normalized)
122
- if os.path.isdir(full_exclude_path):
123
- base_path = exclude_path_normalized.rstrip("/")
124
- if base_path:
125
- patterns.add(base_path)
126
- patterns.add(f"{base_path}/**")
127
- else:
128
- patterns.add(exclude_path_normalized)
129
- elif os.path.isfile(full_exclude_path):
130
- if not _is_covered_by_coarse_ignore(exclude_path_normalized, coarse_patterns):
131
- patterns.add(f"**/{exclude_path_normalized}")
132
- else:
133
- patterns.add(exclude_path_normalized)
253
+ for rel_path in no_ext_paths:
254
+ _add_ignore_pattern(
255
+ patterns, excludes, f"**/{rel_path}", sample_path=rel_path
256
+ )
134
257
 
135
258
 
136
259
  def _build_scancode_ignore_patterns(
137
- path_to_exclude: list,
138
- abs_path_to_scan: str,
260
+ coarse_patterns: frozenset,
139
261
  binary_paths: list,
140
262
  ) -> tuple:
141
- coarse_patterns = _default_scancode_coarse_ignore_patterns()
142
263
  patterns = set(coarse_patterns)
143
-
144
- for path in path_to_exclude or []:
145
- if os.path.isabs(path):
146
- exclude_path = os.path.relpath(path, abs_path_to_scan)
147
- else:
148
- exclude_path = path
149
- _add_path_to_exclude_pattern(patterns, exclude_path, abs_path_to_scan, coarse_patterns)
150
-
151
- for rel_path in binary_paths:
152
- patterns.add(f"**/{rel_path}")
153
-
264
+ excludes = _to_excludes_dict(coarse_patterns)
265
+ _add_binary_ignore_patterns(patterns, excludes, binary_paths)
154
266
  return tuple(sorted(patterns))
155
267
 
156
268
 
@@ -168,12 +280,12 @@ def run_scan(
168
280
 
169
281
  success = True
170
282
  msg = ""
171
- _str_final_result_log = ""
172
283
  _result_log = {}
173
284
  result_list = []
174
285
  license_list = []
175
286
  _json_ext = ".json"
176
- _start_time = datetime.now().strftime('%y%m%d_%H%M')
287
+ _start_time = current_timestamp_utc()
288
+ _file_time = timestamp_for_filename(_start_time)
177
289
 
178
290
  if not correct_filepath:
179
291
  correct_filepath = path_to_scan
@@ -189,9 +301,9 @@ def run_scan(
189
301
  for i, output_extension in enumerate(output_extensions):
190
302
  if output_files[i] is None or output_files[i] == "":
191
303
  if output_extension == _json_ext:
192
- output_files[i] = f"fosslight_opossum_src_{_start_time}"
304
+ output_files[i] = f"fosslight_opossum_src_{_file_time}"
193
305
  else:
194
- output_files[i] = f"fosslight_report_src_{_start_time}"
306
+ output_files[i] = f"fosslight_report_src_{_file_time}"
195
307
 
196
308
  if _write_json_file:
197
309
  output_json_file = os.path.join(output_path, "scancode_raw_result.json")
@@ -199,8 +311,13 @@ def run_scan(
199
311
  output_json_file = ""
200
312
 
201
313
  if not called_by_cli:
202
- logger, _result_log = init_log(os.path.join(output_path, f"fosslight_log_src_{_start_time}.txt"),
203
- True, logging.INFO, logging.DEBUG, _PKG_NAME, path_to_scan, path_to_exclude)
314
+ log_file_path = os.path.join(
315
+ output_path, f"fosslight_log_src_{_file_time}.txt"
316
+ )
317
+ logger, _result_log = init_log(
318
+ log_file_path, True, logging.INFO, logging.DEBUG,
319
+ _PKG_NAME, path_to_scan, path_to_exclude
320
+ )
204
321
 
205
322
  logger.info(f"Tool Info : {_result_log['Tool Info']}")
206
323
 
@@ -215,21 +332,36 @@ def run_scan(
215
332
  pretty_params["output_file"] = output_file_name
216
333
  abs_path_to_scan = os.path.abspath(path_to_scan)
217
334
  binary_paths = []
218
- for root, _, files in os.walk(path_to_scan):
335
+ coarse_patterns = _default_scancode_coarse_ignore_patterns(
336
+ path_to_exclude, abs_path_to_scan
337
+ )
338
+ _, should_skip_dir, should_skip_file = _make_pre_scan_skip_filter(
339
+ coarse_patterns
340
+ )
341
+
342
+ for root, dirs, files in os.walk(path_to_scan):
343
+ rel_root = os.path.relpath(root, abs_path_to_scan).replace("\\", "/")
344
+ dirs[:] = [
345
+ d for d in dirs
346
+ if not should_skip_dir(
347
+ d, d if rel_root == "." else f"{rel_root}/{d}"
348
+ )
349
+ ]
219
350
  for name in files:
351
+ rel_path = name if rel_root == "." else f"{rel_root}/{name}"
352
+ if should_skip_file(name, rel_path):
353
+ continue
220
354
  full_path = os.path.join(root, name)
221
355
  try:
222
356
  if not check_binary(full_path, True):
223
357
  continue
224
358
  except Exception:
225
359
  continue
226
- rel_path = os.path.relpath(full_path, abs_path_to_scan)
227
- rel_norm = os.path.normpath(rel_path).replace("\\", "/")
228
- binary_paths.append(rel_norm)
229
- logger.debug(f"Excluded binary from scancode: {rel_norm}")
360
+ binary_paths.append(rel_path)
361
+ logger.debug(f"Excluded binary from scancode: {rel_path}")
230
362
 
231
363
  ignore_tuple = _build_scancode_ignore_patterns(
232
- path_to_exclude, abs_path_to_scan, binary_paths
364
+ coarse_patterns, binary_paths
233
365
  )
234
366
  logger.debug(f"Scancode ignore patterns: {len(ignore_tuple)}")
235
367
 
@@ -292,11 +424,6 @@ def run_scan(
292
424
  scan_result_msg = str(success) if msg == "" else f"{success}, {msg}"
293
425
  _result_log["Scan Result"] = scan_result_msg
294
426
  _result_log["Output Directory"] = output_path
295
- try:
296
- _str_final_result_log = yaml.safe_dump(_result_log, allow_unicode=True, sort_keys=True)
297
- logger.info(_str_final_result_log)
298
- except Exception as ex:
299
- logger.warning(f"Failed to print result log. {ex}")
300
427
 
301
428
  if not success:
302
429
  logger.error(f"Failed to run: {scan_result_msg}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fosslight_source
3
- Version: 2.3.2
3
+ Version: 2.3.4
4
4
  Summary: FOSSLight Source Scanner
5
5
  Author: LG Electronics
6
6
  License-Expression: Apache-2.0
@@ -20,12 +20,13 @@ Requires-Dist: setuptools<=80.10.2
20
20
  Requires-Dist: pyparsing
21
21
  Requires-Dist: scanoss>=1.19.0
22
22
  Requires-Dist: XlsxWriter
23
- Requires-Dist: fosslight_util>=2.1.37
23
+ Requires-Dist: fosslight_util>=2.2.2
24
24
  Requires-Dist: PyYAML
25
25
  Requires-Dist: wheel>=0.38.1
26
26
  Requires-Dist: intbitset
27
27
  Requires-Dist: fosslight_binary>=5.1.22
28
28
  Requires-Dist: scancode-toolkit>=32.0.2
29
+ Requires-Dist: lxml>=6.0.1
29
30
  Requires-Dist: cryptography<49; platform_system == "Darwin" and platform_machine == "x86_64"
30
31
  Requires-Dist: fingerprints==1.2.3
31
32
  Requires-Dist: normality==2.6.1
@@ -6,6 +6,7 @@ src/fosslight_source/__init__.py
6
6
  src/fosslight_source/_help.py
7
7
  src/fosslight_source/_kb_client.py
8
8
  src/fosslight_source/_license_matched.py
9
+ src/fosslight_source/_merge.py
9
10
  src/fosslight_source/_parsing_scancode_file_item.py
10
11
  src/fosslight_source/_parsing_scanoss_file.py
11
12
  src/fosslight_source/_scan_item.py
@@ -2,12 +2,13 @@ setuptools<=80.10.2
2
2
  pyparsing
3
3
  scanoss>=1.19.0
4
4
  XlsxWriter
5
- fosslight_util>=2.1.37
5
+ fosslight_util>=2.2.2
6
6
  PyYAML
7
7
  wheel>=0.38.1
8
8
  intbitset
9
9
  fosslight_binary>=5.1.22
10
10
  scancode-toolkit>=32.0.2
11
+ lxml>=6.0.1
11
12
  fingerprints==1.2.3
12
13
  normality==2.6.1
13
14
  tqdm