1#!/usr/bin/env python3 2 3import argparse, os, sys 4 5# List of directories we want to exclude when traversing the project files. This list should 6# contain directories related to tests, documentation/samples, API, resources, etc. 7EXCLUDED_DIRS = ['androidTest', 'androidAndroidTest', 'api', 'docs', 'res', 'samples', 'test', 8 'androidInstrumentedTest'] 9# List of packages that should be excluded when traversing the project files. These packages might 10# include Java/Kotlin source files that contain Composable, but we're not interested in including 11# them on the output list because it's unlikely that developers will use them in their code. 12# For example, this list should contain (test) utility and tooling-related packages. 13EXCLUDED_PACKAGES = ['benchmark-utils', 'compiler', 'integration-tests', 'test-utils', 14 'ui-android-stubs', 'ui-tooling', 'ui-tooling-data', 'ui-tooling-preview'] 15# Set of directories that will be excluded when traversing the project files. Excluding a directory 16# means our search won't look into its subdirectories, so this list should be populated 17# with caution. 18EXCLUDED_FROM_FILE_SEARCH = set(EXCLUDED_DIRS + EXCLUDED_PACKAGES) 19 20# The directory containing this script, relative to androidx-main root. 21SCRIPT_DIR_PATH = 'frameworks/support/compose/ui/ui-inspection/generate-packages/' 22# The file name of this script. 23SCRIPT_NAME = 'generate_compose_packages.py' 24# File containing an ordered list of packages that contain at least one Composable. 25# The file is formatted as one package per line. 26COMPOSE_PACKAGES_LIST_FILE = 'compose_packages_list.txt' 27 28# `frameworks/support/compose/`, `frameworks/support/navigation/navigation-compose`, and 29# `frameworks/support/wear/compose`, relative to this script directory, should be the root 30# directories where we search for composables. 31TARGET_DIRECTORIES = [ 32 '../../..', 33 '../../../../navigation/navigation-compose', 34 '../../../../wear/compose', 35 '../../../../lifecycle/lifecycle-runtime-compose', 36] 37 38# Reads a source file with the given file_path and adds its package to the current set of packages 39# if the file contains at least one Composable. 40def add_package_if_composable(file_path, packages): 41 with open(file_path, 'r') as file: 42 lines = file.readlines() 43 for line in lines: 44 if line.startswith('package '): 45 package = line.lstrip('package').strip().strip(';') 46 # Early return to prevent reading the rest of the file. 47 if package in packages: return 48 if line.lstrip().startswith('@Composable') and package: 49 packages.add(package) 50 return 51 52# Iterates on a directory recursively, looking for Java/Kotlin source files that contain Composable 53# functions, and add their corresponding packages to a set that will be returned when the traversal 54# is complete. 55def extract_packages_from_directory(directory): 56 packages = set() 57 for root, dirs, files in os.walk(directory, topdown=True): 58 dirs[:] = [d for d in dirs if d not in EXCLUDED_FROM_FILE_SEARCH] 59 for filename in files: 60 if filename.endswith('.java') or filename.endswith('.kt'): 61 add_package_if_composable(os.path.join(root, filename), packages) 62 return packages 63 64# Searches the given directories and returns a sorted list of all the packages that contain 65# Composable functions. 66def sorted_packages_from_directories(directories): 67 packages = [] 68 for directory in directories: 69 packages.extend(extract_packages_from_directory(directory)) 70 return sorted(packages) 71 72# Verifies that the given the list of packages match the ones currently listed on the 73# compose_packages_list.txt file 74def verify_packages(packages): 75 with open(COMPOSE_PACKAGES_LIST_FILE, 'r') as file: 76 file_packages = file.readlines() 77 if len(file_packages) != len(packages): report_failure_and_exit() 78 for i in range(len(file_packages)): 79 if packages[i] != file_packages[i].strip('\n'): report_failure_and_exit() 80 81def report_failure_and_exit(): 82 print( 83 'Compose packages mismatch\n The current list of Compose packages does not match the list ' 84 'stored in %s%s. If the current list of packages have changed, please regenerate the list ' 85 'by running the following command:\n\t%s%s --regenerate' % ( 86 SCRIPT_DIR_PATH, 87 COMPOSE_PACKAGES_LIST_FILE, 88 SCRIPT_DIR_PATH, 89 SCRIPT_NAME 90 ), 91 file=sys.stderr 92 ) 93 sys.exit(1) 94 95# Regenerates the compose_packages_list.txt file, given the list of packages. 96def regenerate_packages_file(packages): 97 with open(COMPOSE_PACKAGES_LIST_FILE, 'w') as file: 98 file.write('\n'.join(packages)) 99 100# Regenerates the PackageHashes.kt, given the list of packages. The file format is: 101# 1) Header indicating the file should not be edited manually 102# 2) Package definition 103# 3) Required imports 104# 4) packageNameHash function 105# 5) systemPackages val, which is a list containing the result of the packageNameHash 106# function applied to each package name of the given packages list. 107def regenerate_packages_kt_file(packages): 108 kt_file = '../src/main/java/androidx/compose/ui/inspection/inspector/PackageHashes.kt' 109 header = ( 110 '// WARNING: DO NOT EDIT THIS FILE MANUALLY. It\'s automatically generated by running:\n' 111 '// %s%s -r\n' % (SCRIPT_DIR_PATH, SCRIPT_NAME) 112 ) 113 package = 'package androidx.compose.ui.inspection.inspector\n\n' 114 imports = ( 115 'import androidx.annotation.VisibleForTesting\n' 116 'import androidx.collection.intSetOf\n' 117 'import kotlin.math.absoluteValue\n\n' 118 ) 119 package_name_hash_function = ( 120 '@VisibleForTesting\n' 121 'fun packageNameHash(packageName: String) =\n' 122 ' packageName.fold(0) { hash, char -> hash * 31 + char.code }.absoluteValue\n\n' 123 ) 124 system_packages_val = ( 125 'val systemPackages =\n' 126 ' intSetOf(\n' 127 ' -1,\n' 128 '%s\n' 129 ' )\n' % ( 130 '\n'.join([' packageNameHash("' + package + '"),' for package in packages]) 131 ) 132 ) 133 with open(kt_file, 'w') as file: 134 file.write( 135 header + package + imports + package_name_hash_function + system_packages_val 136 ) 137 138if __name__ == '__main__': 139 parser = argparse.ArgumentParser( 140 description='This script is invoked to check whether the current list of packages ' 141 'containing Composables is up-to-date. This list is used by Layout Inspector ' 142 'and Compose Preview to filter out framework Composables.' 143 ) 144 parser.add_argument( 145 '-r', 146 '--regenerate', 147 action='store_true', 148 help='this argument should be used to regenerate the list of packages' 149 ) 150 args = parser.parse_args() 151 152 # cd into directory of script 153 os.chdir(os.path.dirname(os.path.abspath(__file__))) 154 current_packages = sorted_packages_from_directories(TARGET_DIRECTORIES) 155 156 if args.regenerate: 157 regenerate_packages_file(current_packages) 158 regenerate_packages_kt_file(current_packages) 159 else: 160 verify_packages(current_packages) 161