| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2023 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Holders for license-related GN metadata dictionaries.""" |
| |
| import json |
| import dataclasses |
| import logging |
| import os |
| from typing import Any, Dict, List, Tuple, TypeAlias |
| from gn_label import GnLabel |
| |
| AnyDict: TypeAlias = Dict[Any, Any] |
| AnyList: TypeAlias = List[Any] |
| OptionalPath: TypeAlias = str | None |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class GnLicenseMetadata: |
| """Metadata produced by //build/licenses/license.gni template""" |
| |
| target_label: GnLabel |
| public_package_name: str |
| license_files: Tuple[GnLabel] |
| |
| @staticmethod |
| def is_license_metadata_dict(dict: AnyDict) -> bool: |
| return "license_files" in dict |
| |
| @staticmethod |
| def from_json_dict( |
| dict: AnyDict, absolute_fuchsia_source_path: OptionalPath = None |
| ) -> "GnLicenseMetadata": |
| target_label = GnLabel.from_str(dict["target_label"]) |
| logging.debug("Loading GnLicenseMetadata for %s", target_label) |
| assert ( |
| target_label.toolchain |
| ), f"Label must have a toolchain part: {target_label}" |
| assert ( |
| target_label.is_local_name |
| ), f"Label must have a `:name` part: {target_label}" |
| |
| public_package_name = dict["public_package_name"] |
| license_files = [ |
| target_label.create_child_from_str( |
| _rebase_absolute_path(s, absolute_fuchsia_source_path) |
| ) |
| for s in dict["license_files"] |
| ] |
| |
| return GnLicenseMetadata( |
| target_label=target_label, |
| public_package_name=public_package_name, |
| license_files=tuple(license_files), |
| ) |
| |
| |
| @dataclasses.dataclass |
| class GnApplicableLicensesMetadata: |
| """Metadata produced by the GN `applicable_licenses` template parameter""" |
| |
| target_label: GnLabel |
| target_type: str |
| license_labels: Tuple[GnLabel] |
| third_party_resources: Tuple[GnLabel] |
| |
| @staticmethod |
| def is_applicable_licenses_metadata_dict(dict: AnyDict) -> bool: |
| return "license_labels" in dict |
| |
| def is_group(self) -> bool: |
| return self.target_type == "group" |
| |
| @staticmethod |
| def from_json_dict( |
| data: AnyDict, absolute_fuchsia_source_path: OptionalPath = None |
| ) -> "GnApplicableLicensesMetadata": |
| assert isinstance(data, dict) |
| target_label = GnLabel.from_str(data["target_label"]) |
| logging.debug( |
| "Loading GnApplicableLicensesMetadata for %s", target_label |
| ) |
| target_type = data["target_type"] if "target_type" in data else None |
| assert ( |
| target_label.toolchain |
| ), f"Label must have a toolchain part: {target_label}" |
| assert ( |
| target_label.is_local_name |
| ), f"Label must have a `:name` part: {target_label}" |
| |
| license_labels = [GnLabel.from_str(s) for s in data["license_labels"]] |
| |
| third_party_resources = [ |
| target_label.create_child_from_str( |
| _rebase_absolute_path(s, absolute_fuchsia_source_path) |
| ) |
| for s in data.get("third_party_resources", []) |
| ] |
| |
| return GnApplicableLicensesMetadata( |
| target_label=target_label, |
| target_type=target_type, |
| license_labels=tuple(license_labels), |
| third_party_resources=tuple(third_party_resources), |
| ) |
| |
| |
| @dataclasses.dataclass |
| class GnLicenseMetadataDB: |
| """An in-memory DB of licensing GN metadata""" |
| |
| """GnLicenseMetadata by the license's GN label""" |
| licenses_by_label: Dict[GnLabel, GnLicenseMetadata] = dataclasses.field( |
| default_factory=dict |
| ) |
| """GnApplicableLicensesMetadata by the target label they apply to""" |
| applicable_licenses_by_target: Dict[ |
| GnLabel, GnApplicableLicensesMetadata |
| ] = dataclasses.field(default_factory=dict) |
| |
| @staticmethod |
| def from_file( |
| file_path: str, fuchsia_source_path: str |
| ) -> "GnLicenseMetadataDB": |
| """Loads from a json file generated by the build/licenses/license_collection.gni template""" |
| logging.debug("Loading metadata from %s", file_path) |
| |
| # To resolve absolute paths, turn the source path into an absolute one |
| absolute_fuchsia_source_path = os.path.abspath( |
| os.path.normpath(fuchsia_source_path) |
| ) |
| |
| with open(file_path, "r") as f: |
| output = GnLicenseMetadataDB.from_json_list( |
| json.load(f), fuchsia_source_path=absolute_fuchsia_source_path |
| ) |
| logging.debug( |
| f"Loaded {len(output.licenses_by_label)} licenses and {len(output.applicable_licenses_by_target)} from {file_path}" |
| ) |
| return output |
| |
| @staticmethod |
| def from_json_list( |
| json_list: AnyList, fuchsia_source_path: OptionalPath = None |
| ) -> "GnLicenseMetadataDB": |
| """Loads from a json list generated by the build/licenses/license_collection.gni template""" |
| |
| db = GnLicenseMetadataDB() |
| |
| assert type(json_list) is list |
| for d in json_list: |
| if GnLicenseMetadata.is_license_metadata_dict(d): |
| db.add_license_metadata( |
| GnLicenseMetadata.from_json_dict(d, fuchsia_source_path) |
| ) |
| elif GnApplicableLicensesMetadata.is_applicable_licenses_metadata_dict( |
| d |
| ): |
| db.add_applicable_licenses_metadata( |
| GnApplicableLicensesMetadata.from_json_dict( |
| d, fuchsia_source_path |
| ) |
| ) |
| else: |
| raise RuntimeError(f"Unexpected json element: {d}") |
| |
| # Remove applicable_licenses for targets that are license targets. |
| # Those are meaningless. |
| for label in db.licenses_by_label.keys(): |
| db.applicable_licenses_by_target.pop(label, None) |
| |
| return db |
| |
| def add_license_metadata(self, license_metadata: GnLicenseMetadata) -> None: |
| current = self.licenses_by_label.setdefault( |
| license_metadata.target_label, license_metadata |
| ) |
| assert current == license_metadata |
| |
| def add_applicable_licenses_metadata( |
| self, application: GnApplicableLicensesMetadata |
| ) -> None: |
| current = self.applicable_licenses_by_target.setdefault( |
| application.target_label, application |
| ) |
| assert ( |
| current == application |
| ), f"Multiple applicable_licenses metadata entries for {application.target_label} ({application.target_type}), probably due to https://fxbug.dev/42083609)." |
| |
| |
| def _rebase_absolute_path( |
| path_str: str, absolute_fuchsia_source_path: OptionalPath |
| ) -> str: |
| if absolute_fuchsia_source_path and os.path.isabs(path_str): |
| rel_path = os.path.relpath(path_str, absolute_fuchsia_source_path) |
| if not rel_path.startswith("../"): |
| return "//" + rel_path |
| return path_str |