Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(android): add support for Gradle Version Catalogs #190

Merged
merged 2 commits into from
May 10, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 128 additions & 31 deletions fix_android_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,59 @@
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]
new_major = new_version.split('.')[0]

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:
Expand All @@ -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()