xref: /aosp_15_r20/external/cronet/android/tools/license/create_android_metadata_license.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1import os
2import sys
3import glob
4import constants
5from typing import Dict, Callable, List
6from pathlib import Path
7
8from license_type import LicenseType
9import license_utils
10
11METADATA_HEADER = """# This was automatically generated by {}
12# This directory was imported from Chromium.""".format(
13    os.path.basename(__file__))
14
15_ROOT_CRONET = os.path.abspath(
16    os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir,
17                 os.path.pardir))
18
19
20def _create_metadata_file(repo_path: str, directory_path: str, content: str,
21    verify_only: bool):
22  """Creates a METADATA file with a header to ensure that this was generated
23  through the script. If the header is not found then it is assumed that the
24  METADATA file is created manually and will not be touched."""
25  metadata = Path(os.path.join(directory_path, "METADATA"))
26  if metadata.is_file() and METADATA_HEADER not in metadata.read_text():
27    # This is a manually created file! Don't overwrite.
28    return
29
30  metadata_content = "\n".join([
31      METADATA_HEADER,
32      content
33  ])
34  if verify_only:
35    if not metadata.exists():
36      raise Exception(
37          f"Failed to find metadata file {metadata.relative_to(repo_path)}")
38    if not metadata.read_text() == metadata_content:
39      raise Exception(
40          f"Metadata content of {metadata.relative_to(repo_path)} does not match the expected."
41          f"Please re-run create_android_metadata_license.py")
42  else:
43    metadata.write_text(metadata_content)
44
45
46def _create_module_license_file(repo_path: str, directory_path: str,
47    licenses: List[str],
48    verify_only: bool):
49  """Creates a MODULE_LICENSE_XYZ files."""
50  for license in licenses:
51    license_file = Path(os.path.join(directory_path,
52                                     f"MODULE_LICENSE_{license_utils.get_license_file_format(license)}"))
53    if verify_only:
54      if not license_file.exists():
55        raise Exception(
56            f"Failed to find module file {license_file.relative_to(repo_path)}")
57    else:
58      license_file.touch()
59
60
61def _maybe_create_license_file_symlink(directory_path: str,
62    original_license_file: str,
63    verify_only: bool):
64  """Creates a LICENSE symbolic link only if it doesn't exist."""
65  license_symlink_path = Path(os.path.join(directory_path, "LICENSE"))
66  if license_symlink_path.exists():
67    # The symlink is already there, skip.
68    return
69
70  if verify_only:
71    if not license_symlink_path.exists():
72      raise Exception(
73          f"License symlink does not exist for {license_symlink_path}")
74  else:
75    # license_symlink_path.relative_to(.., walk_up=True) does not exist in
76    # Python 3.10, this is the reason why os.path.relpath is used.
77    os.symlink(
78        os.path.relpath(original_license_file, license_symlink_path.parent),
79        license_symlink_path)
80
81
82def _map_rust_license_path_to_directory(license_file_path: str) -> str:
83  """ Returns the canonical path of the parent directory that includes
84  the LICENSE file for rust crates.
85
86  :param license_file_path: This is the filepath found in the README.chromium
87  and the expected format is //some/path/license_file
88  """
89  if not license_file_path.startswith("//"):
90    raise ValueError(
91        f"Rust crate's `License File` is expected to be absolute path "
92        f"(Absolute GN labels are expected to start with //), "
93        f"but found {license_file_path}")
94  return license_file_path[2:license_file_path.rfind("/")]
95
96
97def get_all_readme(repo_path: str):
98  """Fetches all README.chromium files under |repo_path|."""
99  return glob.glob("**/README.chromium", root_dir=repo_path, recursive=True)
100
101
102def update_license(repo_path: str = _ROOT_CRONET,
103    post_process_dict: Dict[str, Callable] = constants.POST_PROCESS_OPERATION,
104    verify_only: bool = False):
105  """
106  Updates the licensing files for the entire repository of external/cronet.
107
108  Running this will generate the following files for each README.chromium
109
110  * LICENSE, this is a symbolic link and only created if there is no LICENSE
111  file already.
112  * METADATA
113  * MODULE_LICENSE_XYZ, XYZ represents the license found in README.chromium.
114
115  Running in verify-only mode will ensure that everything is up to date, an
116  exception will be thrown if there needs to be any changes.
117  :param repo_path: Absolute path to Cronet's AOSP repository
118  :param post_process_dict: A dictionary that includes post-processing, this
119  post processing is not done on the README.chromium file but on the Metadata
120  structure that is extracted from them.
121  :param verify_only: Ensures that everything is up to date or throws.
122  """
123  readme_files = get_all_readme(repo_path)
124  if readme_files == 0:
125    raise Exception(
126        f"Failed to find any README.chromium files under {repo_path}")
127
128  for readme_file in readme_files:
129    if readme_file in constants.IGNORED_README:
130      continue
131    readme_directory = os.path.dirname(
132        os.path.abspath(os.path.join(repo_path, readme_file)))
133
134    metadata = license_utils.parse_chromium_readme_file(
135        os.path.abspath(os.path.join(repo_path, readme_file)),
136        post_process_dict.get(
137            readme_file,
138            lambda
139                _metadata: _metadata))
140
141    license_directory = readme_directory
142    if (os.path.relpath(readme_directory, repo_path)
143        .startswith("third_party/rust/")):
144      # We must do a mapping as Chromium stores the README.chromium
145      # in a different directory than where the src/LICENSE is stored.
146      license_directory = os.path.join(repo_path,
147                                       _map_rust_license_path_to_directory(
148                                           metadata.get_license_file_path()))
149
150    if metadata.get_license_type() != LicenseType.UNENCUMBERED:
151      # Unencumbered license are public domains or don't have a license.
152      _maybe_create_license_file_symlink(license_directory,
153                                         license_utils.resolve_license_path(
154                                             readme_directory,
155                                             metadata.get_license_file_path()),
156                                         verify_only)
157    _create_module_license_file(repo_path, license_directory,
158                                metadata.get_licenses(), verify_only)
159    _create_metadata_file(repo_path, license_directory,
160                          metadata.to_android_metadata(), verify_only)
161
162
163if __name__ == '__main__':
164  sys.exit(update_license(post_process_dict=constants.POST_PROCESS_OPERATION))
165