diff --git a/fix_android_dependencies.py b/fix_android_dependencies.py index f3feff9..3a26d9b 100644 --- a/fix_android_dependencies.py +++ b/fix_android_dependencies.py @@ -8,40 +8,50 @@ TARGET_SDK_VERSION = 34 BUILD_TOOLS_VERSION = '34.0.0' +# build.gradle(.kts) files COMPILE_SDK_RE = r'compileSdk(?:Version)?\s*=?\s*[\w]+' TARGET_SDK_RE = r'targetSdk(?:Version)?\s*=?\s*[\w]+' BUILD_TOOLS_RE = r'buildTools(?:Version)?\s*=?\s*[\'\"\w\.]+' +# *.versions.toml files +VERSION_RE = r'(.*)\s*=\s*"(.*)"' +DEPENDENCY_RE = r'(group|module|name|version|version.ref|id)\s*=' + # Depends on https://github.com/ben-manes/gradle-versions-plugin # # Must run this command: # $ ./gradlew dependencyUpdates -Drevision=release -DoutputFormatter=json RELATIVE_PATH_TO_JSON_REPORT = 'build/dependencyUpdates/report.json' +# Default Gradle Version Catalog location +RELATIVE_PATH_TO_TOML = 'gradle/libs.versions.toml' + -def find_gradle_files(): - """Finds all build.gradle(.kts) files, recursively.""" +def find_configuration_files(): + """Finds all build configuration files, recursively.""" gradle_files = [] for root, dirs, files in os.walk('.'): for filename in files: - if filename.endswith(('build.gradle', 'build.gradle.kts')): + if filename.endswith(('build.gradle', 'build.gradle.kts', 'versions.toml')): gradle_files.append(os.path.join(root, filename)) return gradle_files + def get_android_replacements(): """Gets a dictionary of all android-specific replacements to be made.""" replacements = {} - compileSdk = f"compileSdk = {COMPILE_SDK_VERSION}" - targetSdk = f"targetSdk = {TARGET_SDK_VERSION}" - buildToolsVersion = f"buildToolsVersion = \"{BUILD_TOOLS_VERSION}\"" + compile_sdk = f"compileSdk = {COMPILE_SDK_VERSION}" + target_sdk = f"targetSdk = {TARGET_SDK_VERSION}" + build_tools_version = f"buildToolsVersion = \"{BUILD_TOOLS_VERSION}\"" - replacements[COMPILE_SDK_RE] = compileSdk - replacements[TARGET_SDK_RE] = targetSdk - replacements[BUILD_TOOLS_RE] = buildToolsVersion + replacements[COMPILE_SDK_RE] = compile_sdk + replacements[TARGET_SDK_RE] = target_sdk + replacements[BUILD_TOOLS_RE] = build_tools_version return replacements + def is_major_update(old_version, new_version): """Compares version strings to see if it's a major update.""" old_major = old_version.split('.')[0] @@ -49,7 +59,8 @@ def is_major_update(old_version, new_version): return old_major != new_major -def get_dep_replacements(json_file): + +def get_dep_replacements(json_file, toml_deps): """Gets a dictionary of all dependency replacements to be made.""" replacements = {} with open(json_file, 'r') as f: @@ -68,68 +79,154 @@ def get_dep_replacements(json_file): new_dep = f"{group}:{name}:{new_version}" replacements[curr_dep] = new_dep - # For the plugins block - curr_plugin = f'\("{group}"\) version "{curr_version}"' + # For the plugins block in .kts files + curr_plugin = f'("{group}") version "{curr_version}"' new_plugin = f'("{group}") version "{new_version}"' replacements[curr_plugin] = new_plugin + # For the TOML dependencies + module = group + ':' + name + if module not in toml_deps: + continue + curr_dep = toml_deps[module] + if 'original_line' in curr_dep: + original_line = curr_dep['original_line'] + new_line = original_line.replace(curr_version, new_version) + replacements[original_line] = new_line + return replacements -def update_project(project_path): - """Runs through all build.gradle(.kts) files and performs replacements for individual android project.""" + +def update_project(project_path, toml_path): + """Runs through all build configuration files and performs replacements for individual android project.""" replacements = {} replacements.update(get_android_replacements()) - replacements.update(get_dep_replacements(project_path)) + # Open the Gradle Version Catalog file and fetch its dependencies + toml_dependencies = get_toml_dependencies(toml_path) + replacements.update(get_dep_replacements(project_path, toml_dependencies)) # Print all updates found - print ("Dependency updates:") + print("Dependency updates:") for (k, v) in iter(replacements.items()): - print (f"{k} --> {v}") + print(f"{k} --> {v}") # Iterate through each file and replace it - for gradle_file in find_gradle_files(): - print (f"Updating dependencies for: {gradle_file}") + for config_file in find_configuration_files(): + print(f"Updating dependencies for: {config_file}") new_data = '' - with open(gradle_file, 'r') as f: + with open(config_file, 'r') as f: # Perform each replacement new_data = f.read() for (k, v) in iter(replacements.items()): new_data = re.sub(k, v, new_data) # Write the file - with open(gradle_file, 'w') as f: + with open(config_file, 'w') as f: f.write(new_data) + def update_all(): - """Runs through all build.gradle files and performs replacements.""" + """Runs through all build configuration files and performs replacements.""" project_root = os.getcwd() - print (f"Repo root: {project_root}") + print(f"Repo root: {project_root}") top_level_report = os.path.join(project_root, RELATIVE_PATH_TO_JSON_REPORT) + toml_path = os.path.join(project_root, RELATIVE_PATH_TO_TOML) if os.path.exists(top_level_report): - print ("Update dependencies via top-level report") - update_project(top_level_report) + print("Update dependencies via top-level report") + update_project(top_level_report, toml_path) else: - print ("Update dependencies via child-level report(s)") + print("Update dependencies via child-level report(s)") first_level_subdirectories = get_immediate_subdirectories(project_root) - print (f"List of subdirectories: {first_level_subdirectories}") + print(f"List of subdirectories: {first_level_subdirectories}") for subdirectory in first_level_subdirectories: - print (f"subdirectory: {subdirectory}") + print(f"subdirectory: {subdirectory}") subdirectory_report = os.path.join(project_root, subdirectory, RELATIVE_PATH_TO_JSON_REPORT) + toml_path = os.path.join(project_root, subdirectory, RELATIVE_PATH_TO_TOML) if os.path.exists(subdirectory_report): - print ("\tUpdate dependencies in subdirectory") - update_project(subdirectory_report) + print("\tUpdate dependencies in subdirectory") + update_project(subdirectory_report, toml_path) else: - print ("\tNo report in subdirectory") + print("\tNo report in subdirectory") + + +def get_toml_dependency(line, versions): + original_line = line + # skip dependencies that don't specify a version + if 'version' not in line: + return {} + # Turn it into a valid JSON + line = re.sub(DEPENDENCY_RE, r'"\1" =', line) + value = line.split("=", 1)[1].replace("=", ":") + dep_json = json.loads(value) + # unspecified version means it's an internal dependency + # we can skip version bumps for those + if 'version' in dep_json and dep_json['version'] == 'unspecified': + return {} + # Fill in the group and name + if 'module' in dep_json: + module = dep_json.pop('module') + [group, name] = module.split(':') + dep_json.update({'group': group, 'name': name}) + if 'id' in dep_json: + # 'id' indicates this is a plugin + dep_json['group'] = dep_json.pop('id') + dep_json['name'] = dep_json['group'] + '.gradle.plugin' + + # Fill in the current version + if 'version.ref' in dep_json: + dep_json['original_line'] = versions[dep_json.pop('version.ref')]['original_line'] + # if the version is inlined + if 'version' in dep_json: + dep_json['original_line'] = original_line + key = dep_json.pop('group') + ':' + dep_json.pop('name') + return {key: dep_json} + + +def get_toml_dependencies(toml_file): + """Gets a dictionary of all TOML dependencies.""" + # This code assumes the [versions] block will always be first + # True = read [versions] block; False = read [libraries] or [plugins] block + reading_versions = True + versions = {} + deps = {} + try: + with open(toml_file, 'r') as f: + lines = f.readlines() + for line in lines: + # skip empty lines or comments + if line.strip() == '' or line.startswith("#"): + continue + if '[versions]' in line: + reading_versions = True + continue + if '[libraries]' in line or '[plugins]' in line: + reading_versions = False + continue + # Versions + if reading_versions: + version_match = re.search(VERSION_RE, line) + if version_match: + key = version_match.group(1).strip() + value = version_match.group(2) + versions[key] = {'curr_version': value, 'original_line': line} + # Libraries and Plugins + else: + deps.update(get_toml_dependency(line, versions)) + except FileNotFoundError: + print('This project does not contain a ' + RELATIVE_PATH_TO_TOML + ' file.') + return deps + def get_immediate_subdirectories(directory): return [name for name in os.listdir(directory) if os.path.isdir(os.path.join(directory, name)) and not name.startswith('.')] + if __name__ == '__main__': update_all()