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