diff --git a/src/fosslight_source/_scan_item.py b/src/fosslight_source/_scan_item.py index 0e96a8c..fd71c04 100644 --- a/src/fosslight_source/_scan_item.py +++ b/src/fosslight_source/_scan_item.py @@ -25,6 +25,7 @@ r'.*\.podspec$', r'Cargo\.toml$', r'huggingface_hub_metadata\.json$', + r'Android\.bp$', ] MAX_LICENSE_LENGTH = 200 MAX_LICENSE_TOTAL_LENGTH = 600 diff --git a/src/fosslight_source/cli.py b/src/fosslight_source/cli.py index 459ade3..e3d148b 100755 --- a/src/fosslight_source/cli.py +++ b/src/fosslight_source/cli.py @@ -453,11 +453,11 @@ def merge_results( if manifest_licenses: for file_name, licenses in manifest_licenses.items(): valid_licenses = [lic.strip() for lic in licenses if isinstance(lic, str) and lic.strip()] - # Non-UI: skip manifests with no extracted licenses. - # UI: keep/create the row and mark is_manifest_file even without licenses. - if not valid_licenses and not ui_mode: + item = _get_or_append_source_item( + scancode_result, file_name, append=bool(valid_licenses) or ui_mode + ) + if item is None: continue - item = _get_or_append_source_item(scancode_result, file_name) item.is_manifest_file = True if valid_licenses: # overwrite existing detected licenses with manifest-provided licenses @@ -500,9 +500,13 @@ def merge_results( return scancode_result, kb_status_message, kb_requested_count, kb_returned_count -def _get_or_append_source_item(scancode_result: list, file_name: str) -> SourceItem: +def _get_or_append_source_item( + scancode_result: list, file_name: str, append: bool = True +) -> Optional[SourceItem]: if file_name in scancode_result: return scancode_result[scancode_result.index(file_name)] + if not append: + return None item = SourceItem(file_name) scancode_result.append(item) return item @@ -705,7 +709,7 @@ def run_scanners( return success, result_log.get(RESULT_KEY, ""), scan_item, license_list, scanoss_result -def metadata_collector(path_to_scan: str, excluded_files: set) -> dict: +def metadata_collector(path_to_scan: str, excluded_files: set) -> tuple[dict, dict]: """ Collect metadata for merging. diff --git a/src/fosslight_source/run_manifest_extractor.py b/src/fosslight_source/run_manifest_extractor.py index 27d70f9..360c5d8 100644 --- a/src/fosslight_source/run_manifest_extractor.py +++ b/src/fosslight_source/run_manifest_extractor.py @@ -345,6 +345,9 @@ def append_license(value): def get_manifest_licenses(file_path: str) -> list[str]: + # Android.bp licenses come from ScanCode; manifest merge only sets is_manifest_file. + if os.path.basename(file_path).lower() == 'android.bp': + return [] if file_path.endswith('.pom'): try: pom_licenses = get_license_from_pom(group_id='', artifact_id='', version='', pom_path=file_path, check_parent=True) diff --git a/tests/test_manifest_android_bp.py b/tests/test_manifest_android_bp.py new file mode 100644 index 0000000..278aa57 --- /dev/null +++ b/tests/test_manifest_android_bp.py @@ -0,0 +1,61 @@ +# Copyright (c) 2026 LG Electronics Inc. +# SPDX-License-Identifier: Apache-2.0 +"""Tests for Android.bp manifest handling.""" + +from unittest.mock import patch + +from fosslight_source._scan_item import ( + SourceItem, + is_manifest_file, +) +from fosslight_source.cli import merge_results, metadata_collector +from fosslight_source.run_manifest_extractor import get_manifest_licenses + + +def test_is_manifest_file_recognizes_android_bp(): + assert is_manifest_file("/tmp/module/Android.bp") is True + assert is_manifest_file("/tmp/module/android.bp") is True + assert is_manifest_file("/tmp/module/package.json") is True + + +def test_get_manifest_licenses_returns_empty_for_android_bp(tmp_path): + android_bp = tmp_path / "Android.bp" + android_bp.write_text('license { name: "test_license" }', encoding="utf-8") + assert get_manifest_licenses(str(android_bp)) == [] + + +def test_metadata_collector_adds_android_bp_without_license_extraction(tmp_path): + android_bp = tmp_path / "Android.bp" + android_bp.write_text('license { name: "test_license" }', encoding="utf-8") + package_json = tmp_path / "package.json" + package_json.write_text('{"license": "MIT"}', encoding="utf-8") + + with patch("fosslight_source.cli.get_manifest_licenses", wraps=get_manifest_licenses) as mock_get: + spdx_downloads, manifest_licenses = metadata_collector(str(tmp_path), set()) + + assert spdx_downloads == {} + assert manifest_licenses == {"Android.bp": [], "package.json": ["MIT"]} + assert mock_get.call_count == 2 + + +def test_merge_results_sets_manifest_flag_without_overwriting_scancode_licenses(): + scancode_item = SourceItem("carrois-gothic-sc/Android.bp") + scancode_item.licenses = ["Apache-2.0", "MIT", "BSD"] + + merged, _, _, _ = merge_results( + scancode_result=[scancode_item], + manifest_licenses={"carrois-gothic-sc/Android.bp": []}, + ) + + assert len(merged) == 1 + assert merged[0].is_manifest_file is True + assert merged[0].licenses == ["Apache-2.0", "MIT", "BSD"] + + +def test_merge_results_skips_android_bp_not_in_scancode_result(): + merged, _, _, _ = merge_results( + scancode_result=[], + manifest_licenses={"module/Android.bp": []}, + ) + + assert merged == [] diff --git a/tests/test_manifest_recommended_scenarios.py b/tests/test_manifest_recommended_scenarios.py new file mode 100644 index 0000000..e115cb1 --- /dev/null +++ b/tests/test_manifest_recommended_scenarios.py @@ -0,0 +1,76 @@ +# Copyright (c) 2026 LG Electronics Inc. +# SPDX-License-Identifier: Apache-2.0 +"""Recommended verification scenarios for Android.bp manifest handling.""" + +from fosslight_source._scan_item import SourceItem +from fosslight_source.cli import merge_results + + +def test_scenario1_android_bp_keeps_scancode_licenses(): + """Android.bp with ScanCode licenses: is_manifest_file=True, licenses preserved.""" + scancode_item = SourceItem("Android.bp") + scancode_item.licenses = ["Apache-2.0", "unknown-license-reference", "BSD", "MIT", "OFL"] + + merged, _, _, _ = merge_results( + scancode_result=[scancode_item], + manifest_licenses={"Android.bp": []}, + ) + + assert len(merged) == 1 + assert merged[0].is_manifest_file is True + assert merged[0].licenses == ["Apache-2.0", "unknown-license-reference", "BSD", "MIT", "OFL"] + + +def test_scenario2_package_json_manifest_fail_keeps_scancode_licenses(): + """package.json manifest extraction fails but ScanCode row exists: manifest flag only.""" + scancode_item = SourceItem("app/package.json") + scancode_item.licenses = ["Apache-2.0"] + + merged, _, _, _ = merge_results( + scancode_result=[scancode_item], + manifest_licenses={"app/package.json": []}, + ) + + assert len(merged) == 1 + assert merged[0].is_manifest_file is True + assert merged[0].licenses == ["Apache-2.0"] + + +def test_scenario3_android_bp_from_spdx_marks_manifest_without_new_row(): + """Android.bp added by spdx merge: manifest flag on existing item, no duplicate row.""" + spdx_item = SourceItem("module/Android.bp") + spdx_item.download_location = ["https://example.com/repo"] + + merged, _, _, _ = merge_results( + scancode_result=[spdx_item], + manifest_licenses={"module/Android.bp": []}, + ) + + assert len(merged) == 1 + assert merged[0].is_manifest_file is True + assert merged[0].download_location == ["https://example.com/repo"] + assert merged[0].licenses == [] + + +def test_scenario3_android_bp_not_in_result_no_row_non_ui(): + """Android.bp absent from merge result: no row in non-UI mode.""" + merged_non_ui, _, _, _ = merge_results( + scancode_result=[], + manifest_licenses={"module/Android.bp": []}, + ui_mode=False, + ) + assert merged_non_ui == [] + + +def test_scenario3_android_bp_not_in_result_ui_keeps_empty_row(): + """Android.bp absent from merge result: UI mode still creates manifest row.""" + merged_ui, _, _, _ = merge_results( + scancode_result=[], + manifest_licenses={"module/Android.bp": []}, + ui_mode=True, + ) + + assert len(merged_ui) == 1 + assert merged_ui[0].source_name_or_path == "module/Android.bp" + assert merged_ui[0].is_manifest_file is True + assert merged_ui[0].licenses == []