1#!/usr/bin/env python3
2
3import os, sys, zipfile
4import argparse
5import subprocess
6
7#### ####
8# This scripts updates LibraryVersions.k (see $LIBRARYVERSIONS_REL_PATH) based on the artifacts
9# in Google Maven (see $GMAVEN_BASE_URL).  It will only numerically increment alpha or beta versions.
10# It will NOT change stability suffixes and it will NOT increment the version of a RC
11# or stable library.  These changes should be done manually and purposefully.
12#### ####
13
14LIBRARYVERSIONS_REL_PATH = 'buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt'
15FRAMEWORKS_SUPPORT_FULL_PATH = os.path.abspath(os.path.join(os.getcwd(), '..'))
16LIBRARYVERSIONS_FULL_PATH = os.path.join(FRAMEWORKS_SUPPORT_FULL_PATH, LIBRARYVERSIONS_REL_PATH)
17GMAVEN_BASE_URL = 'https://dl.google.com/dl/android/maven2/androidx/'
18summary_log = []
19exclude_dirs = []
20
21# Defines an artifact in terms of its Maven Coorindates: artifactId, groupId, version
22class MavenCoordinates:
23	def __init__(self, artifactId, version):
24		self.artifactId = artifactId
25		self.version = version
26		self.groupId = self.get_groupId_from_artifactId(artifactId)
27	def get_groupId_from_artifactId(self, artifactId):
28		# By convention, androidx namespace is declared as:
29		# androidx.${groupId}:${groupId}-${optionalArtifactIdSuffix}:${version}
30		# So, artifactId == "${groupId}-${optionalArtifactIdSuffix}"
31		return artifactId.split('-')[0]
32
33def print_e(*args, **kwargs):
34	print(*args, file=sys.stderr, **kwargs)
35
36def should_update_artifact(commlineArgs, groupId, artifactId):
37	# Tells whether to update the given artifact based on the command-line arguments
38	should_update = False
39	if (commlineArgs.groups) or (commlineArgs.artifacts):
40		if (commlineArgs.groups) and (groupId in commlineArgs.groups):
41			should_update = True
42		if (commlineArgs.artifacts) and (artifactId in commlineArgs.artifacts):
43			should_update = True
44	else:
45		should_update = True
46	return should_update
47
48def print_change_summary():
49	print("\n ---  SUMMARY --- ")
50	for change in summary_log:
51		print(change)
52
53def read_in_lines_from_file(file_path):
54	if not os.path.exists(file_path):
55		print_e("File path does not exist: %s" % file_path)
56		exit(1)
57	else:
58		with open(file_path, 'r') as f:
59			lv_lines = f.readlines()
60		return lv_lines
61
62def write_lines_to_file(file_path, lines):
63	if not os.path.exists(file_path):
64		print_e("File path does not exist: %s" % file_path)
65		exit(1)
66	# Open file for writing and update all lines
67	with open(file_path, 'w') as f:
68		f.writelines(lines)
69
70def get_artifactId_from_LibraryVersions_line(line):
71	artifactId = line.split('val')[1]
72	artifactId = artifactId.split('=')[0]
73	artifactId = artifactId.strip(' ')
74	artifactId = artifactId.lower()
75	artifactId = artifactId.replace('_', '-')
76	return artifactId
77
78def get_version_or_macro_from_LibraryVersions_line(line):
79	## Sample input:	'val ACTIVITY = Version("1.0.0-alpha04")'
80	## Sample output: 	'1.0.0-alpha04', True
81	## Sample input:	'val ACTIVITY = FRAGMENT'
82	## Sample output: 	'fragment', False
83	is_resolved = False
84	version = ""
85	if 'Version(' in line and '\"' in line:
86		is_resolved = True
87		version = line.split('\"')[1]
88	else:
89		is_resolved = False
90		version = line.split('=')[-1].strip(' \n').lower().replace('_','-')
91	return version, is_resolved
92
93def get_tot_artifact_list(lv_lines):
94	resolved_artifact_list = []
95	unresolved_versions_list = []
96	for cur_line in lv_lines:
97		# Skip any line that doesn't declare a version
98		if 'val' not in cur_line: continue
99		artifactId = get_artifactId_from_LibraryVersions_line(cur_line)
100		version, is_resolved = get_version_or_macro_from_LibraryVersions_line(cur_line)
101		artifact = MavenCoordinates(artifactId, version)
102		if is_resolved:
103			resolved_artifact_list.append(artifact)
104		else:
105			# Artifact version specification is a reference to another artifact's
106			# version -> it needs to be resolved
107			unresolved_versions_list.append(artifact)
108	# Resolve variable references in unresolved_versions_list to complete resolved_artifact_list.
109	# This is needed because version MACROs can be a copy another version MACRO.  For example:
110	#    val ARCH_CORE = Version("2.0.0")
111	#    val ARCH_CORE_TESTING = ARCH_CORE
112	for unresolved_artifact in unresolved_versions_list:
113		artifactId_to_copy = unresolved_artifact.version
114		for resolved_artifact in resolved_artifact_list:
115			if resolved_artifact.artifactId == artifactId_to_copy:
116				unresolved_artifact.version = resolved_artifact.version
117				break
118		resolved_artifact_list.append(unresolved_artifact)
119	return resolved_artifact_list
120
121def does_exist_on_gmaven(groupId, artifactId, version):
122	print("Checking GMaven for %s-%s..." % (artifactId, version), end = '')
123	# URLS are of the format:
124	# https://dl.google.com/dl/android/maven2/androidx/${groupId}/${artifactId}/${version}/${artifactId}-${version}.pom
125	artifactUrl = GMAVEN_BASE_URL + groupId + '/' + artifactId + '/' + version + '/' + artifactId + '-' + version + '.pom'
126	try:
127		# Curl the url to see if artifact pom exists
128		curl_output = subprocess.check_output('curl -s %s' % artifactUrl, shell=True)
129	except subprocess.CalledProcessError:
130		print_e('FAIL: Failed to curl url: ' %  artifactUrl)
131		return None
132	if '404' in curl_output.decode():
133		print("version is good")
134		return False
135	else:
136		print("version is OUT OF DATE")
137		return True
138
139def increment_alpha_beta_version(version):
140	# Only increment alpha and beta versions.
141	# rc and stable should never need to be incremented in the androidx-main branch
142	# Suffix changes should be done manually.
143	changed = False
144	if 'alpha' in version or 'beta' in version:
145		changed = True
146		# Assure we don't violate a version naming policy
147		if not version[-1:].isdigit():
148			print_e("--- --- \n Version %s has violated version naming policy!  Please fix." % version)
149			exit(1)
150		if version[-2:].isdigit():
151			new_version = int(version[-2:]) + 1
152			formatted_version = "%02d" % (new_version,)
153			return version[:-2] + formatted_version, changed
154		else:
155			# Account for single digit versions with no preceding 0 (the leading 0 will be added)
156			new_version = int(version[-1:]) + 1
157			formatted_version = "%02d" % (new_version,)
158			return version[:-1] + formatted_version, changed
159	else:
160		return version, changed
161
162def artifactId_to_kotlin_macro(artifactId):
163	return artifactId.replace('-','_').upper()
164
165def update_artifact_version(lv_lines, artifact):
166	num_lines = len(lv_lines)
167	for i in range(num_lines):
168		cur_line = lv_lines[i]
169		# Skip any line that doesn't declare a version
170		if 'val' not in cur_line: continue
171		artifactId = get_artifactId_from_LibraryVersions_line(cur_line)
172		if artifactId == artifact.artifactId:
173			new_version, ver_was_updated = increment_alpha_beta_version(artifact.version)
174			if ver_was_updated:
175				# Only modify line if the version was actually changed
176				lv_lines[i] ="    val " + artifactId_to_kotlin_macro(artifactId) + " = Version(\"" + new_version + "\")\n"
177				summary_log.append("Updated %s to FROM %s TO %s" % (artifactId.upper(), artifact.version, new_version))
178				# Assert incremented version doesn't exist
179				if does_exist_on_gmaven(artifact.groupId, artifact.artifactId, new_version):
180					print_e("--- --- \n Incremented version of %s from %s to %s, but %s has already been published!\
181					  This needs to be fixed manually." % (artifact.artifactId, artifact.version, new_version, new_version))
182					exit(1)
183
184def update_api():
185	try:
186		os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
187		curl_output = subprocess.check_output('./gradlew updateApi', shell=True)
188		os.chdir(os.path.dirname(os.path.abspath(__file__)))
189	except subprocess.CalledProcessError:
190		print_e('FAIL: Failed gradle task updateApi!')
191		return None
192
193def update_library_versions(args):
194	# Open LibraryVersions.kt file for reading and get all lines
195	libraryversions_lines = read_in_lines_from_file(LIBRARYVERSIONS_FULL_PATH)
196	tot_artifact_list = get_tot_artifact_list(libraryversions_lines)
197	# Loop through every library version and update the version, if necessary
198	versions_changed = False
199	for artifact in tot_artifact_list:
200		if should_update_artifact(args, artifact.groupId, artifact.artifactId):
201			print("Updating %s " % artifact.artifactId)
202			if does_exist_on_gmaven(artifact.groupId, artifact.artifactId, artifact.version):
203				update_artifact_version(libraryversions_lines, artifact)
204				versions_changed = True
205	if versions_changed:
206		write_lines_to_file(LIBRARYVERSIONS_FULL_PATH, libraryversions_lines)
207		update_api()
208		summary_log.append("These changes have not been committed.  Please double check before uploading.")
209	else:
210		summary_log.append("No changes needed.  All versions are update to date :)")
211
212
213if __name__ == '__main__':
214	# cd into directory of script
215	os.chdir(os.path.dirname(os.path.abspath(__file__)))
216
217	# Set up input arguments
218	parser = argparse.ArgumentParser(
219		description=('This script increments versions in LibraryVersions.kt based on artifacts released to Google Maven.'))
220	parser.add_argument(
221		'--groups', metavar='groupId', nargs='+',
222		help="""If specified, only increments the version for libraries whose groupId contains the listed text.
223		For example, if you specify \"--groups paging slice lifecycle\", then this
224		script will increment the version of each library with groupId beginning with \"androidx.paging\", \"androidx.slice\",
225		or \"androidx.lifecycle\"""")
226	parser.add_argument(
227		'--artifacts', metavar='artifactId', nargs='+',
228		help="""If specified, only increments the version for libraries whose artifactId contains the listed text.
229		For example, if you specify \"--artifacts core slice-view lifecycle-common\", then this
230		script will increment the version for specific artifacts \"androidx.core:core\", \"androidx.slice:slice-view\",
231		and \"androidx.lifecycle:lifecycle-common\"""")
232	args = parser.parse_args()
233	update_library_versions(args)
234	print_change_summary()
235
236