Skip to content
1 change: 1 addition & 0 deletions src/fosslight_source/_scan_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 10 additions & 6 deletions src/fosslight_source/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions src/fosslight_source/run_manifest_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
61 changes: 61 additions & 0 deletions tests/test_manifest_android_bp.py
Original file line number Diff line number Diff line change
@@ -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 == []
76 changes: 76 additions & 0 deletions tests/test_manifest_recommended_scenarios.py
Original file line number Diff line number Diff line change
@@ -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 == []
Loading