blob: 79267c532dd61735bcf2e23d648de06b41589d97 [file] [log] [blame]
#!/usr/bin/env python3
import argparse, os, sys
# List of directories we want to exclude when traversing the project files. This list should
# contain directories related to tests, documentation/samples, API, resources, etc.
EXCLUDED_DIRS = ['androidTest', 'androidAndroidTest', 'api', 'docs', 'res', 'samples', 'test',
'androidInstrumentedTest']
# List of packages that should be excluded when traversing the project files. These packages might
# include Java/Kotlin source files that contain Composable, but we're not interested in including
# them on the output list because it's unlikely that developers will use them in their code.
# For example, this list should contain (test) utility and tooling-related packages.
EXCLUDED_PACKAGES = ['benchmark-utils', 'compiler', 'integration-tests', 'test-utils',
'ui-android-stubs', 'ui-tooling', 'ui-tooling-data', 'ui-tooling-preview']
# Set of directories that will be excluded when traversing the project files. Excluding a directory
# means our search won't look into its subdirectories, so this list should be populated
# with caution.
EXCLUDED_FROM_FILE_SEARCH = set(EXCLUDED_DIRS + EXCLUDED_PACKAGES)
# The directory containing this script, relative to androidx-main root.
SCRIPT_DIR_PATH = 'frameworks/support/compose/ui/ui-inspection/generate-packages/'
# The file name of this script.
SCRIPT_NAME = 'generate_compose_packages.py'
# File containing an ordered list of packages that contain at least one Composable.
# The file is formatted as one package per line.
COMPOSE_PACKAGES_LIST_FILE = 'compose_packages_list.txt'
# `frameworks/support/compose/`, `frameworks/support/navigation/navigation-compose`, and
# `frameworks/support/wear/compose`, relative to this script directory, should be the root
# directories where we search for composables.
TARGET_DIRECTORIES = [
'../../..',
'../../../../navigation/navigation-compose',
'../../../../wear/compose',
]
# Reads a source file with the given file_path and adds its package to the current set of packages
# if the file contains at least one Composable.
def add_package_if_composable(file_path, packages):
with open(file_path, 'r') as file:
lines = file.readlines()
for line in lines:
if line.startswith('package '):
package = line.lstrip('package').strip().strip(';')
# Early return to prevent reading the rest of the file.
if package in packages: return
if line.lstrip().startswith('@Composable') and package:
packages.add(package)
return
# Iterates on a directory recursively, looking for Java/Kotlin source files that contain Composable
# functions, and add their corresponding packages to a set that will be returned when the traversal
# is complete.
def extract_packages_from_directory(directory):
packages = set()
for root, dirs, files in os.walk(directory, topdown=True):
dirs[:] = [d for d in dirs if d not in EXCLUDED_FROM_FILE_SEARCH]
for filename in files:
if filename.endswith('.java') or filename.endswith('.kt'):
add_package_if_composable(os.path.join(root, filename), packages)
return packages
# Searches the given directories and returns a sorted list of all the packages that contain
# Composable functions.
def sorted_packages_from_directories(directories):
packages = []
for directory in directories:
packages.extend(extract_packages_from_directory(directory))
return sorted(packages)
# Verifies that the given the list of packages match the ones currently listed on the
# compose_packages_list.txt file
def verify_packages(packages):
with open(COMPOSE_PACKAGES_LIST_FILE, 'r') as file:
file_packages = file.readlines()
if len(file_packages) != len(packages): report_failure_and_exit()
for i in range(len(file_packages)):
if packages[i] != file_packages[i].strip('\n'): report_failure_and_exit()
def report_failure_and_exit():
print(
'Compose packages mismatch\n The current list of Compose packages does not match the list '
'stored in %s%s. If the current list of packages have changed, please regenerate the list '
'by running the following command:\n\t%s%s --regenerate' % (
SCRIPT_DIR_PATH,
COMPOSE_PACKAGES_LIST_FILE,
SCRIPT_DIR_PATH,
SCRIPT_NAME
),
file=sys.stderr
)
sys.exit(1)
# Regenerates the compose_packages_list.txt file, given the list of packages.
def regenerate_packages_file(packages):
with open(COMPOSE_PACKAGES_LIST_FILE, 'w') as file:
file.write('\n'.join(packages))
# Regenerates the PackageHashes.kt, given the list of packages. The file format is:
# 1) Header indicating the file should not be edited manually
# 2) Package definition
# 3) Required imports
# 4) packageNameHash function
# 5) systemPackages val, which is a list containing the result of the packageNameHash
# function applied to each package name of the given packages list.
def regenerate_packages_kt_file(packages):
kt_file = '../src/main/java/androidx/compose/ui/inspection/inspector/PackageHashes.kt'
header = (
'// WARNING: DO NOT EDIT THIS FILE MANUALLY. It\'s automatically generated by running:\n'
'// %s%s -r\n' % (SCRIPT_DIR_PATH, SCRIPT_NAME)
)
package = 'package androidx.compose.ui.inspection.inspector\n\n'
imports = (
'import androidx.annotation.VisibleForTesting\n'
'import androidx.collection.intSetOf\n'
'import kotlin.math.absoluteValue\n\n'
)
package_name_hash_function = (
'@VisibleForTesting\n'
'fun packageNameHash(packageName: String) =\n'
' packageName.fold(0) { hash, char -> hash * 31 + char.code }.absoluteValue\n\n'
)
system_packages_val = (
'val systemPackages =\n'
' intSetOf(\n'
' -1,\n'
'%s\n'
' )\n' % (
'\n'.join([' packageNameHash("' + package + '"),' for package in packages])
)
)
with open(kt_file, 'w') as file:
file.write(
header + package + imports + package_name_hash_function + system_packages_val
)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='This script is invoked to check whether the current list of packages '
'containing Composables is up-to-date. This list is used by Layout Inspector '
'and Compose Preview to filter out framework Composables.'
)
parser.add_argument(
'-r',
'--regenerate',
action='store_true',
help='this argument should be used to regenerate the list of packages'
)
args = parser.parse_args()
# cd into directory of script
os.chdir(os.path.dirname(os.path.abspath(__file__)))
current_packages = sorted_packages_from_directories(TARGET_DIRECTORIES)
if args.regenerate:
regenerate_packages_file(current_packages)
regenerate_packages_kt_file(current_packages)
else:
verify_packages(current_packages)