fosslight-source 2.3.8__tar.gz → 2.3.9__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 (32) hide show
  1. {fosslight_source-2.3.8/src/fosslight_source.egg-info → fosslight_source-2.3.9}/PKG-INFO +2 -2
  2. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/pyproject.toml +2 -2
  3. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/_parsing_scancode_file_item.py +29 -3
  4. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/_scan_item.py +1 -0
  5. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/cli.py +11 -8
  6. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/run_manifest_extractor.py +59 -28
  7. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/run_scancode.py +2 -3
  8. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/run_spdx_extractor.py +1 -1
  9. {fosslight_source-2.3.8 → fosslight_source-2.3.9/src/fosslight_source.egg-info}/PKG-INFO +2 -2
  10. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source.egg-info/SOURCES.txt +3 -0
  11. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source.egg-info/requires.txt +1 -1
  12. fosslight_source-2.3.9/tests/test_manifest_android_bp.py +61 -0
  13. fosslight_source-2.3.9/tests/test_manifest_pyproject.py +51 -0
  14. fosslight_source-2.3.9/tests/test_manifest_recommended_scenarios.py +76 -0
  15. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/tests/test_parsing_unknown_spdx.py +63 -0
  16. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/LICENSE +0 -0
  17. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/MANIFEST.in +0 -0
  18. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/README.md +0 -0
  19. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/setup.cfg +0 -0
  20. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/__init__.py +0 -0
  21. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/_help.py +0 -0
  22. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/_kb_client.py +0 -0
  23. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/_license_matched.py +0 -0
  24. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/_merge.py +0 -0
  25. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/_parsing_scanoss_file.py +0 -0
  26. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/_scancode_ignore_binaries.py +0 -0
  27. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source/run_scanoss.py +0 -0
  28. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source.egg-info/dependency_links.txt +0 -0
  29. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source.egg-info/entry_points.txt +0 -0
  30. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/src/fosslight_source.egg-info/top_level.txt +0 -0
  31. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/tests/test_manifest_composer.py +0 -0
  32. {fosslight_source-2.3.8 → fosslight_source-2.3.9}/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.8
3
+ Version: 2.3.9
4
4
  Summary: FOSSLight Source Scanner
5
5
  Author: LG Electronics
6
6
  License-Expression: Apache-2.0
@@ -20,7 +20,7 @@ Requires-Dist: setuptools<=80.10.2
20
20
  Requires-Dist: pyparsing
21
21
  Requires-Dist: scanoss>=1.45.0
22
22
  Requires-Dist: XlsxWriter
23
- Requires-Dist: fosslight_util>=2.2.7
23
+ Requires-Dist: fosslight_util>=2.2.8
24
24
  Requires-Dist: PyYAML
25
25
  Requires-Dist: wheel>=0.38.1
26
26
  Requires-Dist: intbitset
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "fosslight_source"
10
- version = "2.3.8"
10
+ version = "2.3.9"
11
11
  description = "FOSSLight Source Scanner"
12
12
  readme = "README.md"
13
13
  license = "Apache-2.0"
@@ -29,7 +29,7 @@ dependencies = [
29
29
  "pyparsing",
30
30
  "scanoss>=1.45.0",
31
31
  "XlsxWriter",
32
- "fosslight_util>=2.2.7",
32
+ "fosslight_util>=2.2.8",
33
33
  "PyYAML",
34
34
  "wheel>=0.38.1",
35
35
  "intbitset",
@@ -7,6 +7,7 @@ import os
7
7
  import logging
8
8
  import re
9
9
  import fosslight_util.constant as constant
10
+ from fosslight_util.exclude import is_excluded_filename
10
11
  from ._license_matched import MatchedLicense
11
12
  from ._scan_item import SourceItem
12
13
  from ._scan_item import replace_word
@@ -20,9 +21,15 @@ SPDX_LICENSE_IDENTIFIER_PATTERN = re.compile(
20
21
  r'SPDX[-\s]+License[-\s]+Identifier(?:\s*[:,-]\s*|\s+)([^\r\n]+)',
21
22
  re.IGNORECASE,
22
23
  )
24
+ # Android Soong license_kinds string, e.g. "SPDX-license-identifier-BSD"
25
+ SOONG_SPDX_LICENSE_KIND_PATTERN = re.compile(
26
+ r'SPDX-license-identifier-([A-Za-z0-9.-]+)',
27
+ re.IGNORECASE,
28
+ )
23
29
  LICENSE_REF_PREFIX_PATTERN = re.compile(r'^LicenseRef-', re.IGNORECASE)
24
30
  # Trailing closers from comments and quoted lists, e.g. "MIT */", "MIT -->", or '"MIT",'.
25
31
  SPDX_DECLARATION_TRAILER_PATTERN = re.compile(r'\s*(?:\*/|-->|["\']\s*,?)\s*$')
32
+ SPDX_DECLARATION_LINE_COMMENT_PATTERN = re.compile(r'\s//.*$')
26
33
  KEYWORD_SPDX_ID = r'SPDX-License-Identifier\s*[\S]+'
27
34
  KEYWORD_DOWNLOAD_LOC = r'DownloadLocation\s*[\S]+'
28
35
  KEYWORD_SCANCODE_UNKNOWN = "unknown-spdx"
@@ -294,11 +301,22 @@ def _omit_parens_for_two_license_expression(expression: str) -> str:
294
301
 
295
302
 
296
303
  def _clean_spdx_declaration(raw: str) -> str:
297
- """Strip whitespace and trailing comment or quoted-list terminators."""
304
+ """Strip whitespace, line comments, and trailing comment or quoted-list terminators."""
298
305
  cleaned = (raw or "").strip()
299
306
  if not cleaned:
300
307
  return ""
301
- return SPDX_DECLARATION_TRAILER_PATTERN.sub('', cleaned).strip()
308
+ cleaned = SPDX_DECLARATION_LINE_COMMENT_PATTERN.sub('', cleaned).strip()
309
+ cleaned = SPDX_DECLARATION_TRAILER_PATTERN.sub('', cleaned).strip()
310
+ cleaned = cleaned.strip('"\'')
311
+ return cleaned.strip()
312
+
313
+
314
+ def _extract_soong_license_kind(matched_txt: str) -> str:
315
+ """Extract license id from Android Soong SPDX-license-identifier-* strings."""
316
+ matched = SOONG_SPDX_LICENSE_KIND_PATTERN.search(matched_txt or "")
317
+ if not matched:
318
+ return ""
319
+ return matched.group(1)
302
320
 
303
321
 
304
322
  def _strip_license_ref_prefix(token: str) -> str:
@@ -309,9 +327,14 @@ def _extract_spdx_declared_expression(matched_txt: str) -> str:
309
327
  """
310
328
  Extract SPDX-License-Identifier value from matched_text.
311
329
 
312
- - Removes trailing comment closers (*/ , -->)
330
+ - Android Soong license_kinds (SPDX-license-identifier-*) first
331
+ - Removes trailing line comments and comment closers (*/, -->)
313
332
  - Strips LicenseRef- from each AND/OR token
314
333
  """
334
+ soong_license = _extract_soong_license_kind(matched_txt)
335
+ if soong_license:
336
+ return soong_license
337
+
315
338
  matched = SPDX_LICENSE_IDENTIFIER_PATTERN.search(matched_txt or "")
316
339
  if not matched:
317
340
  return ""
@@ -493,6 +516,9 @@ def parsing_scancode(
493
516
  if (not file_path) or is_binary or is_dir:
494
517
  logger.info(f"Skipping {file_path} because it is binary or directory")
495
518
  continue
519
+ if is_excluded_filename(file_path):
520
+ logger.debug(f"Skipping {file_path} because it is an excluded filename")
521
+ continue
496
522
  result_item = SourceItem(file_path)
497
523
  if has_error:
498
524
  error_msg = file.get("scan_errors", [])
@@ -25,6 +25,7 @@ _manifest_filename = [
25
25
  r'.*\.podspec$',
26
26
  r'Cargo\.toml$',
27
27
  r'huggingface_hub_metadata\.json$',
28
+ r'Android\.bp$',
28
29
  ]
29
30
  MAX_LICENSE_LENGTH = 200
30
31
  MAX_LICENSE_TOTAL_LENGTH = 600
@@ -29,13 +29,12 @@ import tqdm
29
29
  import argparse
30
30
  from .run_spdx_extractor import get_spdx_downloads
31
31
  from .run_manifest_extractor import get_manifest_licenses
32
- from ._scan_item import SourceItem, resolve_kb_config, is_notice_file
32
+ from ._scan_item import SourceItem, resolve_kb_config, is_notice_file, is_manifest_file
33
33
  from ._kb_client import fetch_origin_urls_via_scan_job
34
34
  from fosslight_util.cover import dump_result_log
35
35
  from fosslight_util.time import current_timestamp_utc, format_running_time, timestamp_for_filename
36
36
  from fosslight_util.oss_item import ScannerItem
37
37
  from typing import Optional, Tuple
38
- from ._scan_item import is_manifest_file
39
38
  import shutil
40
39
  from ._merge import (
41
40
  _add_pre_merge_sheet,
@@ -454,11 +453,11 @@ def merge_results(
454
453
  if manifest_licenses:
455
454
  for file_name, licenses in manifest_licenses.items():
456
455
  valid_licenses = [lic.strip() for lic in licenses if isinstance(lic, str) and lic.strip()]
457
- # Non-UI: skip manifests with no extracted licenses.
458
- # UI: keep/create the row and mark is_manifest_file even without licenses.
459
- if not valid_licenses and not ui_mode:
456
+ item = _get_or_append_source_item(
457
+ scancode_result, file_name, append=bool(valid_licenses) or ui_mode
458
+ )
459
+ if item is None:
460
460
  continue
461
- item = _get_or_append_source_item(scancode_result, file_name)
462
461
  item.is_manifest_file = True
463
462
  if valid_licenses:
464
463
  # overwrite existing detected licenses with manifest-provided licenses
@@ -501,9 +500,13 @@ def merge_results(
501
500
  return scancode_result, kb_status_message, kb_requested_count, kb_returned_count
502
501
 
503
502
 
504
- def _get_or_append_source_item(scancode_result: list, file_name: str) -> SourceItem:
503
+ def _get_or_append_source_item(
504
+ scancode_result: list, file_name: str, append: bool = True
505
+ ) -> Optional[SourceItem]:
505
506
  if file_name in scancode_result:
506
507
  return scancode_result[scancode_result.index(file_name)]
508
+ if not append:
509
+ return None
507
510
  item = SourceItem(file_name)
508
511
  scancode_result.append(item)
509
512
  return item
@@ -706,7 +709,7 @@ def run_scanners(
706
709
  return success, result_log.get(RESULT_KEY, ""), scan_item, license_list, scanoss_result
707
710
 
708
711
 
709
- def metadata_collector(path_to_scan: str, excluded_files: set) -> dict:
712
+ def metadata_collector(path_to_scan: str, excluded_files: set) -> tuple[dict, dict]:
710
713
  """
711
714
  Collect metadata for merging.
712
715
 
@@ -178,16 +178,28 @@ def get_licenses_from_pyproject_toml(file_path: str) -> list[str]:
178
178
  data = None
179
179
 
180
180
  if isinstance(data, dict):
181
- project_tbl = data.get('project') or {}
182
- license_value = project_tbl.get('license')
183
- if isinstance(license_value, str) and license_value.strip():
184
- return [license_value.strip()]
185
- if isinstance(license_value, dict):
186
- text_value = license_value.get('text')
187
- if isinstance(text_value, str) and text_value.strip():
188
- return [text_value.strip()]
189
- if license_value.get('file'):
190
- return []
181
+ project_tbl = data.get('project')
182
+ if isinstance(project_tbl, dict) and 'license' in project_tbl:
183
+ license_value = project_tbl.get('license')
184
+ if isinstance(license_value, str) and license_value.strip():
185
+ return [license_value.strip()]
186
+ if isinstance(license_value, dict):
187
+ text_value = license_value.get('text')
188
+ if isinstance(text_value, str) and text_value.strip():
189
+ return [text_value.strip()]
190
+ if license_value.get('file'):
191
+ return []
192
+ return []
193
+
194
+ tool_tbl = data.get('tool')
195
+ if isinstance(tool_tbl, dict):
196
+ poetry_tbl = tool_tbl.get('poetry')
197
+ if isinstance(poetry_tbl, dict):
198
+ poetry_license = poetry_tbl.get('license')
199
+ if isinstance(poetry_license, str) and poetry_license.strip():
200
+ return [poetry_license.strip()]
201
+ # Structured parse succeeded; do not re-read/regex the same file.
202
+ return []
191
203
  except Exception as ex:
192
204
  logger.info(f"Failed to parse pyproject.toml via toml parser for {file_path}: {ex}")
193
205
 
@@ -195,24 +207,40 @@ def get_licenses_from_pyproject_toml(file_path: str) -> list[str]:
195
207
  with open(file_path, 'r', encoding='utf-8') as f:
196
208
  content = f.read()
197
209
  project_match = re.search(r'^\s*\[project\]\s*(.*?)(?=^\s*\[|\Z)', content, flags=re.MULTILINE | re.DOTALL)
198
- if not project_match:
199
- return []
200
- block = project_match.group(1)
201
- m = re.search(r'^\s*license\s*=\s*(?P<q>"""|\'\'\'|"|\')(?P<val>.*?)(?P=q)', block,
202
- flags=re.MULTILINE | re.DOTALL)
203
- if m:
204
- val = m.group('val').strip()
205
- if val:
206
- return [val]
207
- m2 = re.search(r'^\s*license\s*=\s*\{[^}]*?\btext\s*=\s*(?P<q>"""|\'\'\'|"|\')(?P<val>.*?)(?P=q)',
208
- block, flags=re.MULTILINE | re.DOTALL)
209
- if m2:
210
- val = m2.group('val').strip()
211
- if val:
212
- return [val]
213
- m3 = re.search(r'^\s*license\s*=\s*\{[^}]*?\bfile\s*=', block, flags=re.MULTILINE | re.DOTALL)
214
- if m3:
215
- return []
210
+ if project_match:
211
+ block = project_match.group(1)
212
+ project_license_declared = re.search(r'^\s*license\s*=', block, flags=re.MULTILINE)
213
+ if project_license_declared:
214
+ m = re.search(r'^\s*license\s*=\s*(?P<q>"""|\'\'\'|"|\')(?P<val>.*?)(?P=q)', block,
215
+ flags=re.MULTILINE | re.DOTALL)
216
+ if m:
217
+ val = m.group('val').strip()
218
+ if val:
219
+ return [val]
220
+ m2 = re.search(r'^\s*license\s*=\s*\{[^}]*?\btext\s*=\s*(?P<q>"""|\'\'\'|"|\')(?P<val>.*?)(?P=q)',
221
+ block, flags=re.MULTILINE | re.DOTALL)
222
+ if m2:
223
+ val = m2.group('val').strip()
224
+ if val:
225
+ return [val]
226
+ return []
227
+
228
+ poetry_match = re.search(
229
+ r'^\s*\[tool\.poetry\]\s*(.*?)(?=^\s*\[|\Z)',
230
+ content,
231
+ flags=re.MULTILINE | re.DOTALL,
232
+ )
233
+ if poetry_match:
234
+ poetry_block = poetry_match.group(1)
235
+ poetry_license = re.search(
236
+ r'^\s*license\s*=\s*(?P<q>"""|\'\'\'|"|\')(?P<val>.*?)(?P=q)',
237
+ poetry_block,
238
+ flags=re.MULTILINE | re.DOTALL,
239
+ )
240
+ if poetry_license:
241
+ val = poetry_license.group('val').strip()
242
+ if val:
243
+ return [val]
216
244
  except Exception as ex:
217
245
  logger.info(f"Failed to parse pyproject.toml {file_path}: {ex}")
218
246
  return []
@@ -345,6 +373,9 @@ def get_licenses_from_huggingface_metadata(file_path: str) -> list[str]:
345
373
 
346
374
 
347
375
  def get_manifest_licenses(file_path: str) -> list[str]:
376
+ # Android.bp licenses come from ScanCode; manifest merge only sets is_manifest_file.
377
+ if os.path.basename(file_path).lower() == 'android.bp':
378
+ return []
348
379
  if file_path.endswith('.pom'):
349
380
  try:
350
381
  pom_licenses = get_license_from_pom(group_id='', artifact_id='', version='', pom_path=file_path, check_parent=True)
@@ -19,7 +19,6 @@ from fosslight_util.output_format import check_output_formats_v2
19
19
  from fosslight_util.exclude import (
20
20
  EXCLUDE_DIRECTORY,
21
21
  EXCLUDE_FILE_EXTENSION,
22
- EXCLUDE_FILENAME,
23
22
  PACKAGE_DIRECTORY,
24
23
  )
25
24
 
@@ -166,14 +165,14 @@ def _default_scancode_ignore_patterns(
166
165
  Directory names use path-based globs (e.g. **/tests/**) so they do not match
167
166
  the scan root directory name itself.
168
167
  Binary files are excluded separately via scancode --ignore-binaries.
168
+ EXCLUDE_FILENAME is not passed to ScanCode --ignore: matching many exact
169
+ names on a large tree is slow. Those files are dropped after parsing.
169
170
  """
170
171
  patterns = {".*"}
171
172
  for name in PACKAGE_DIRECTORY + EXCLUDE_DIRECTORY:
172
173
  patterns.add(_directory_ignore_pattern(name))
173
174
  for ext in EXCLUDE_FILE_EXTENSION:
174
175
  patterns.add(f"*.{ext}")
175
- for name in EXCLUDE_FILENAME:
176
- patterns.add(name)
177
176
 
178
177
  for pattern in path_to_exclude or []:
179
178
  if os.path.isabs(pattern):
@@ -22,5 +22,5 @@ def get_spdx_downloads(file_path: str) -> list[str]:
22
22
  for word in find_word.findall(mmap_obj):
23
23
  results.append(word.decode('utf-8'))
24
24
  except Exception as ex:
25
- logger.warning(f"Failed to extract SPDX download location. {file_path}, {ex}")
25
+ logger.debug(f"Failed to extract SPDX download location. {file_path}, {ex}")
26
26
  return results
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fosslight_source
3
- Version: 2.3.8
3
+ Version: 2.3.9
4
4
  Summary: FOSSLight Source Scanner
5
5
  Author: LG Electronics
6
6
  License-Expression: Apache-2.0
@@ -20,7 +20,7 @@ Requires-Dist: setuptools<=80.10.2
20
20
  Requires-Dist: pyparsing
21
21
  Requires-Dist: scanoss>=1.45.0
22
22
  Requires-Dist: XlsxWriter
23
- Requires-Dist: fosslight_util>=2.2.7
23
+ Requires-Dist: fosslight_util>=2.2.8
24
24
  Requires-Dist: PyYAML
25
25
  Requires-Dist: wheel>=0.38.1
26
26
  Requires-Dist: intbitset
@@ -22,6 +22,9 @@ src/fosslight_source.egg-info/dependency_links.txt
22
22
  src/fosslight_source.egg-info/entry_points.txt
23
23
  src/fosslight_source.egg-info/requires.txt
24
24
  src/fosslight_source.egg-info/top_level.txt
25
+ tests/test_manifest_android_bp.py
25
26
  tests/test_manifest_composer.py
27
+ tests/test_manifest_pyproject.py
28
+ tests/test_manifest_recommended_scenarios.py
26
29
  tests/test_parsing_unknown_spdx.py
27
30
  tests/test_tox.py
@@ -2,7 +2,7 @@ setuptools<=80.10.2
2
2
  pyparsing
3
3
  scanoss>=1.45.0
4
4
  XlsxWriter
5
- fosslight_util>=2.2.7
5
+ fosslight_util>=2.2.8
6
6
  PyYAML
7
7
  wheel>=0.38.1
8
8
  intbitset
@@ -0,0 +1,61 @@
1
+ # Copyright (c) 2026 LG Electronics Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Tests for Android.bp manifest handling."""
4
+
5
+ from unittest.mock import patch
6
+
7
+ from fosslight_source._scan_item import (
8
+ SourceItem,
9
+ is_manifest_file,
10
+ )
11
+ from fosslight_source.cli import merge_results, metadata_collector
12
+ from fosslight_source.run_manifest_extractor import get_manifest_licenses
13
+
14
+
15
+ def test_is_manifest_file_recognizes_android_bp():
16
+ assert is_manifest_file("/tmp/module/Android.bp") is True
17
+ assert is_manifest_file("/tmp/module/android.bp") is True
18
+ assert is_manifest_file("/tmp/module/package.json") is True
19
+
20
+
21
+ def test_get_manifest_licenses_returns_empty_for_android_bp(tmp_path):
22
+ android_bp = tmp_path / "Android.bp"
23
+ android_bp.write_text('license { name: "test_license" }', encoding="utf-8")
24
+ assert get_manifest_licenses(str(android_bp)) == []
25
+
26
+
27
+ def test_metadata_collector_adds_android_bp_without_license_extraction(tmp_path):
28
+ android_bp = tmp_path / "Android.bp"
29
+ android_bp.write_text('license { name: "test_license" }', encoding="utf-8")
30
+ package_json = tmp_path / "package.json"
31
+ package_json.write_text('{"license": "MIT"}', encoding="utf-8")
32
+
33
+ with patch("fosslight_source.cli.get_manifest_licenses", wraps=get_manifest_licenses) as mock_get:
34
+ spdx_downloads, manifest_licenses = metadata_collector(str(tmp_path), set())
35
+
36
+ assert spdx_downloads == {}
37
+ assert manifest_licenses == {"Android.bp": [], "package.json": ["MIT"]}
38
+ assert mock_get.call_count == 2
39
+
40
+
41
+ def test_merge_results_sets_manifest_flag_without_overwriting_scancode_licenses():
42
+ scancode_item = SourceItem("carrois-gothic-sc/Android.bp")
43
+ scancode_item.licenses = ["Apache-2.0", "MIT", "BSD"]
44
+
45
+ merged, _, _, _ = merge_results(
46
+ scancode_result=[scancode_item],
47
+ manifest_licenses={"carrois-gothic-sc/Android.bp": []},
48
+ )
49
+
50
+ assert len(merged) == 1
51
+ assert merged[0].is_manifest_file is True
52
+ assert merged[0].licenses == ["Apache-2.0", "MIT", "BSD"]
53
+
54
+
55
+ def test_merge_results_skips_android_bp_not_in_scancode_result():
56
+ merged, _, _, _ = merge_results(
57
+ scancode_result=[],
58
+ manifest_licenses={"module/Android.bp": []},
59
+ )
60
+
61
+ assert merged == []
@@ -0,0 +1,51 @@
1
+ # Copyright (c) 2026 LG Electronics Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Tests for pyproject.toml manifest license extraction."""
4
+
5
+ from fosslight_source.run_manifest_extractor import get_licenses_from_pyproject_toml
6
+
7
+
8
+ def test_pep621_license_string(tmp_path):
9
+ path = tmp_path / "pyproject.toml"
10
+ path.write_text(
11
+ '[project]\nname = "demo"\nlicense = "MIT"\n',
12
+ encoding="utf-8",
13
+ )
14
+ assert get_licenses_from_pyproject_toml(str(path)) == ["MIT"]
15
+
16
+
17
+ def test_pep621_license_text_table(tmp_path):
18
+ path = tmp_path / "pyproject.toml"
19
+ path.write_text(
20
+ '[project]\nname = "demo"\nlicense = {text = "MIT"}\n',
21
+ encoding="utf-8",
22
+ )
23
+ assert get_licenses_from_pyproject_toml(str(path)) == ["MIT"]
24
+
25
+
26
+ def test_pep621_license_file_returns_empty(tmp_path):
27
+ path = tmp_path / "pyproject.toml"
28
+ path.write_text(
29
+ '[project]\nname = "demo"\nlicense = {file = "LICENSE"}\n',
30
+ encoding="utf-8",
31
+ )
32
+ assert get_licenses_from_pyproject_toml(str(path)) == []
33
+
34
+
35
+ def test_legacy_poetry_license(tmp_path):
36
+ path = tmp_path / "pyproject.toml"
37
+ path.write_text(
38
+ '[tool.poetry]\nname = "demo"\nlicense = "Apache-2.0"\n',
39
+ encoding="utf-8",
40
+ )
41
+ assert get_licenses_from_pyproject_toml(str(path)) == ["Apache-2.0"]
42
+
43
+
44
+ def test_project_license_file_does_not_fall_back_to_poetry(tmp_path):
45
+ path = tmp_path / "pyproject.toml"
46
+ path.write_text(
47
+ '[project]\nname = "demo"\nlicense = {file = "LICENSE"}\n\n'
48
+ '[tool.poetry]\nname = "demo"\nlicense = "MIT"\n',
49
+ encoding="utf-8",
50
+ )
51
+ assert get_licenses_from_pyproject_toml(str(path)) == []
@@ -0,0 +1,76 @@
1
+ # Copyright (c) 2026 LG Electronics Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Recommended verification scenarios for Android.bp manifest handling."""
4
+
5
+ from fosslight_source._scan_item import SourceItem
6
+ from fosslight_source.cli import merge_results
7
+
8
+
9
+ def test_scenario1_android_bp_keeps_scancode_licenses():
10
+ """Android.bp with ScanCode licenses: is_manifest_file=True, licenses preserved."""
11
+ scancode_item = SourceItem("Android.bp")
12
+ scancode_item.licenses = ["Apache-2.0", "unknown-license-reference", "BSD", "MIT", "OFL"]
13
+
14
+ merged, _, _, _ = merge_results(
15
+ scancode_result=[scancode_item],
16
+ manifest_licenses={"Android.bp": []},
17
+ )
18
+
19
+ assert len(merged) == 1
20
+ assert merged[0].is_manifest_file is True
21
+ assert merged[0].licenses == ["Apache-2.0", "unknown-license-reference", "BSD", "MIT", "OFL"]
22
+
23
+
24
+ def test_scenario2_package_json_manifest_fail_keeps_scancode_licenses():
25
+ """package.json manifest extraction fails but ScanCode row exists: manifest flag only."""
26
+ scancode_item = SourceItem("app/package.json")
27
+ scancode_item.licenses = ["Apache-2.0"]
28
+
29
+ merged, _, _, _ = merge_results(
30
+ scancode_result=[scancode_item],
31
+ manifest_licenses={"app/package.json": []},
32
+ )
33
+
34
+ assert len(merged) == 1
35
+ assert merged[0].is_manifest_file is True
36
+ assert merged[0].licenses == ["Apache-2.0"]
37
+
38
+
39
+ def test_scenario3_android_bp_from_spdx_marks_manifest_without_new_row():
40
+ """Android.bp added by spdx merge: manifest flag on existing item, no duplicate row."""
41
+ spdx_item = SourceItem("module/Android.bp")
42
+ spdx_item.download_location = ["https://example.com/repo"]
43
+
44
+ merged, _, _, _ = merge_results(
45
+ scancode_result=[spdx_item],
46
+ manifest_licenses={"module/Android.bp": []},
47
+ )
48
+
49
+ assert len(merged) == 1
50
+ assert merged[0].is_manifest_file is True
51
+ assert merged[0].download_location == ["https://example.com/repo"]
52
+ assert merged[0].licenses == []
53
+
54
+
55
+ def test_scenario3_android_bp_not_in_result_no_row_non_ui():
56
+ """Android.bp absent from merge result: no row in non-UI mode."""
57
+ merged_non_ui, _, _, _ = merge_results(
58
+ scancode_result=[],
59
+ manifest_licenses={"module/Android.bp": []},
60
+ ui_mode=False,
61
+ )
62
+ assert merged_non_ui == []
63
+
64
+
65
+ def test_scenario3_android_bp_not_in_result_ui_keeps_empty_row():
66
+ """Android.bp absent from merge result: UI mode still creates manifest row."""
67
+ merged_ui, _, _, _ = merge_results(
68
+ scancode_result=[],
69
+ manifest_licenses={"module/Android.bp": []},
70
+ ui_mode=True,
71
+ )
72
+
73
+ assert len(merged_ui) == 1
74
+ assert merged_ui[0].source_name_or_path == "module/Android.bp"
75
+ assert merged_ui[0].is_manifest_file is True
76
+ assert merged_ui[0].licenses == []
@@ -21,6 +21,7 @@ from fosslight_source._parsing_scancode_file_item import (
21
21
  ("// SPDX-License-Identifier, MIT", "MIT"),
22
22
  ("// SPDX-License-Identifier MIT", "MIT"),
23
23
  (' "SPDX-license-identifier-BSD",', "BSD"),
24
+ (' "SPDX-license-identifier-OFL", // by exception only', "OFL"),
24
25
  ("/* SPDX-License-Identifier: MIT */", "MIT"),
25
26
  ("<!-- SPDX-License-Identifier: MIT -->", "MIT"),
26
27
  ("# SPDX-License-Identifier: LicenseRef-MIT-like", "MIT-like"),
@@ -281,3 +282,65 @@ def test_two_license_comment_omits_outer_parentheses():
281
282
  assert success is True
282
283
  assert results[0].licenses == ["Apache-2.0", "MIT"]
283
284
  assert results[0].comment == "Apache-2.0 OR MIT"
285
+
286
+
287
+ def test_android_bp_soong_license_kinds_without_line_comment_in_license():
288
+ scancode_file_list = [{
289
+ "path": "Android.bp",
290
+ "type": "file",
291
+ "detected_license_expression": (
292
+ "(apache-2.0 AND unknown-license-reference) AND (unknown-spdx AND mit)"
293
+ ),
294
+ "license_detections": [
295
+ {
296
+ "license_expression": "apache-2.0 AND unknown-license-reference",
297
+ "matches": [
298
+ {
299
+ "license_expression": "apache-2.0",
300
+ "matched_text": (
301
+ "// Licensed under the Apache License, Version 2.0 (the \"License\");\n"
302
+ "// limitations under the License."
303
+ ),
304
+ },
305
+ {
306
+ "license_expression": "unknown-license-reference",
307
+ "matched_text": "// *** THIS PACKAGE HAS SPECIAL LICENSING CONDITIONS. PLEASE",
308
+ },
309
+ ],
310
+ },
311
+ {
312
+ "license_expression": "unknown-spdx AND mit",
313
+ "matches": [
314
+ {
315
+ "license_expression": "unknown-spdx",
316
+ "matched_text": ' "SPDX-license-identifier-BSD",',
317
+ },
318
+ {
319
+ "license_expression": "mit",
320
+ "matched_text": ' "SPDX-license-identifier-MIT",',
321
+ },
322
+ {
323
+ "license_expression": "unknown-spdx",
324
+ "matched_text": (
325
+ ' "SPDX-license-identifier-OFL", // by exception only'
326
+ ),
327
+ },
328
+ ],
329
+ },
330
+ ],
331
+ "copyrights": [],
332
+ }]
333
+
334
+ success, results, _messages, _ = parsing_scancode(scancode_file_list)
335
+
336
+ assert success is True
337
+ licenses = results[0].licenses
338
+ assert licenses == [
339
+ "Apache-2.0",
340
+ "unknown-license-reference",
341
+ "BSD",
342
+ "MIT",
343
+ "OFL",
344
+ ]
345
+ assert all("//" not in lic for lic in results[0].licenses)
346
+ assert all('"' not in lic for lic in results[0].licenses)