Skip to content

Commit 2aca363

Browse files
committed
ios part is working
1 parent 90f9549 commit 2aca363

File tree

1 file changed

+322
-0
lines changed

1 file changed

+322
-0
lines changed
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2021 Google
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
"""
18+
This script fetches the latest cocoapod and android package versions from their
19+
respective public repositories and updates these versions in various files
20+
across the C++ repository.
21+
22+
There are 3 types of files being updated by this script,
23+
- Podfile : Files containins lists of cocoapods along with their versions.
24+
Eg: `ios_pods/Podfile` and any integration tests podfiles.
25+
26+
- Android dependencies gradle file: Gradle files containing list of android
27+
libraries and their versions that is
28+
referenced by gradle files from sub projects
29+
Eg: `Android/firebase_dependencies.gradle`
30+
31+
- Readme file: Readme file containing all dependencies (ios and android) and
32+
and their versions. Eg: 'release_build_files/readme.md`
33+
34+
Usage:
35+
# Update versions in default set of files in the repository.
36+
python3 scripts/update_ios_android_dependencies.py
37+
38+
# Update specific pod files (or directories containing pod files)
39+
python3 scripts/update_ios_android_dependencies.py --podfiles foo/Podfile
40+
dir_with_podfiles
41+
42+
Other similar flags:
43+
--depfiles
44+
--readmefiles
45+
46+
These "files" flags can take a list of paths (files and directories).
47+
If directories are provided, they are scanned for known file types.
48+
"""
49+
50+
import argparse
51+
import logging
52+
import os
53+
import pprint
54+
import re
55+
import requests
56+
import shutil
57+
import subprocess
58+
import sys
59+
import tempfile
60+
61+
from collections import defaultdict
62+
from pkg_resources import packaging
63+
from xml.etree import ElementTree
64+
65+
66+
def get_files_from_directory(dirpath, file_extension, file_name=None,
67+
absolute_paths=True):
68+
""" Helper function to filter files in directories.
69+
70+
Args:
71+
dirpath (str): Root directory to search in.
72+
file_extension (str): File extension to search for.
73+
Eg: '.gradle'
74+
file_name (str, optional): Exact file name to search for.
75+
Defaults to None.
76+
absolute_paths (bool, optional): Return absolute paths to files.
77+
Defaults to True.
78+
If False, just filenames are returned.
79+
80+
Returns:
81+
list(str): List of files matching the specified criteria.
82+
List of filenames (if absolute_paths=False), or
83+
a list of absolute paths (if absolute_paths=True)
84+
"""
85+
files = []
86+
for dirpath, _, filenames in os.walk(dirpath):
87+
for filename in filenames:
88+
if not filename.endswith(file_extension):
89+
continue
90+
if file_name and not file_name == filename:
91+
continue
92+
if absolute_paths:
93+
files.append(os.path.join(dirpath, filename))
94+
else:
95+
files.append(filename)
96+
return files
97+
98+
99+
# Cocoapods github repo from where we scan available pods and their versions.
100+
PODSPEC_REPOSITORY = 'https://github.com/CocoaPods/Specs.git'
101+
102+
# Android gMaven repostiory from where we scan available android packages
103+
# and their versions
104+
GMAVEN_MASTER_INDEX = "https://dl.google.com/dl/android/maven2/master-index.xml"
105+
GMAVEN_GROUP_INDEX = "https://dl.google.com/dl/android/maven2/{0}/group-index.xml"
106+
107+
# List of Pods that we are interested in.
108+
PODS = (
109+
'FirebaseCore',
110+
'FirebaseAdMob',
111+
'FirebaseAnalytics',
112+
'FirebaseAuth',
113+
'FirebaseCrashlytics',
114+
'FirebaseDatabase',
115+
'FirebaseDynamicLinks',
116+
'FirebaseFirestore',
117+
'FirebaseFunctions',
118+
'FirebaseInstallations',
119+
'FirebaseInstanceID',
120+
'FirebaseMessaging',
121+
'FirebaseRemoteConfig',
122+
'FirebaseStorage',
123+
)
124+
125+
126+
def get_pod_versions(specs_repo, pods=PODS):
127+
""" Get available pods and their versions from the specs repo
128+
129+
Args:
130+
local_repo_dir (str): Directory mirroring Cocoapods specs repo
131+
pods (iterable(str), optional): List of pods whose versions we need.
132+
Defaults to PODS.
133+
134+
Returns:
135+
dict: Map of the form {<str>:list(str)}
136+
Containing a mapping of podnames to available versions.
137+
"""
138+
all_versions = defaultdict(list)
139+
logging.info('Fetching pod versions from Specs repo...')
140+
podspec_files = get_files_from_directory(specs_repo,
141+
file_extension='.podspec.json')
142+
for podspec_file in podspec_files:
143+
filename = os.path.basename(podspec_file)
144+
# Example: FirebaseAuth.podspec.json
145+
podname = filename.split('.')[0]
146+
if not podname in pods:
147+
continue
148+
parent_dir = os.path.dirname(podspec_file)
149+
version = os.path.basename(parent_dir)
150+
all_versions[podname].append(version)
151+
152+
return all_versions
153+
154+
155+
def get_latest_pod_versions(specs_repo=None, pods=PODS):
156+
"""Get latest versions for specified pods.
157+
158+
Args:
159+
pods (iterable(str) optional): Pods for which we need latest version.
160+
Defaults to PODS.
161+
specs_repo (str optional): Local checkout of Cocoapods specs repo.
162+
163+
Returns:
164+
dict: Map of the form {<str>:<str>} containing a mapping of podnames to
165+
latest version.
166+
"""
167+
cleanup_required = False
168+
if specs_repo is None:
169+
specs_repo = tempfile.mkdtemp(suffix='pods')
170+
logging.info('Cloning podspecs git repo...')
171+
git_clone_cmd = ['git', 'clone', '-q', '--depth', '1',
172+
PODSPEC_REPOSITORY, specs_repo]
173+
subprocess.run(git_clone_cmd)
174+
# Temporary directory should be cleaned up after use.
175+
cleanup_required = True
176+
177+
all_versions = get_pod_versions(specs_repo, pods)
178+
if cleanup_required:
179+
shutil.rmtree(specs_repo)
180+
181+
latest_versions = {}
182+
for pod in all_versions:
183+
# all_versions map is in the following format:
184+
# { 'PodnameA' : ['1.0.1', '2.0.4'], 'PodnameB': ['3.0.4', '1.0.2'] }
185+
# Convert string version numbers to semantic version objects
186+
# for easier comparison and get the latest version.
187+
latest_version = max([packaging.version.parse(v)
188+
for v in all_versions[pod]])
189+
# Replace the list of versions with just the latest version
190+
latest_versions[pod] = latest_version.base_version
191+
print("Latest pod versions retreived from cocoapods specs repo: \n")
192+
pprint.pprint(latest_versions)
193+
print()
194+
return latest_versions
195+
196+
197+
def get_pod_files(dirs_and_files):
198+
""" Get final list of podfiles to update.
199+
If a directory is passed, it is searched recursively.
200+
201+
Args:
202+
dirs_and_files (iterable(str)): List of paths which could be files or
203+
directories.
204+
205+
Returns:
206+
iterable(str): Final list of podfiles after recursively searching dirs.
207+
"""
208+
pod_files = []
209+
for entry in dirs_and_files:
210+
abspath = os.path.abspath(entry)
211+
if not os.path.exists(abspath):
212+
continue
213+
if os.path.isdir(abspath):
214+
pod_files = pod_files + get_files_from_directory(abspath,
215+
file_extension='',
216+
file_name='Podfile')
217+
elif os.path.isfile(abspath):
218+
pod_files.append(abspath)
219+
220+
return pod_files
221+
222+
# Look for lines like, pod 'Firebase/Core', '7.11.0'
223+
RE_PODFILE_VERSION = re.compile("\s+pod '(?P<pod_name>.+)', '(?P<version>.+)'\n")
224+
225+
def modify_pod_file(pod_file, pod_version_map, dryrun=True):
226+
""" Update pod versions in specified podfile.
227+
228+
Args:
229+
pod_file (str): Absolute path to a podfile.
230+
pod_version_map (dict): Map of podnames to their respective version.
231+
dryrun (bool, optional): Just print the substitutions.
232+
Do not write to file. Defaults to True.
233+
"""
234+
to_update = False
235+
existing_lines = []
236+
with open(pod_file, "r") as podfile:
237+
existing_lines = podfile.readlines()
238+
if not existing_lines:
239+
logging.debug('Update failed. ' +
240+
'Could not read contents from pod file {0}.'.format(podfile))
241+
return
242+
logging.debug('Checking if update is required for {0}'.format(pod_file))
243+
244+
substituted_pairs = []
245+
for idx, line in enumerate(existing_lines):
246+
match = re.match(RE_PODFILE_VERSION, line)
247+
if match:
248+
pod_name = match['pod_name']
249+
# Firebase/Auth -> FirebaseAuth
250+
pod_name_key = pod_name.replace('/', '')
251+
if pod_name_key in pod_version_map:
252+
latest_version = pod_version_map[pod_name_key]
253+
substituted_line = line.replace(match['version'], latest_version)
254+
if substituted_line != line:
255+
substituted_pairs.append((line, substituted_line))
256+
existing_lines[idx] = substituted_line
257+
to_update = True
258+
259+
if to_update:
260+
print('Updating contents of {0}'.format(pod_file))
261+
for original, substituted in substituted_pairs:
262+
print('(-) ' + original + '(+) ' + substituted)
263+
264+
if not dryrun:
265+
with open(pod_file, "w") as podfile:
266+
podfile.writelines(existing_lines)
267+
print()
268+
269+
270+
def main():
271+
args = parse_cmdline_args()
272+
latest_versions_map = get_latest_pod_versions(args.specs_repo, PODS)
273+
#latest_versions_map = {'FirebaseAuth': '8.0.0', 'FirebaseRemoteConfig':'9.9.9'}
274+
pod_files = get_pod_files(args.podfiles)
275+
for pod_file in pod_files:
276+
modify_pod_file(pod_file, latest_versions_map, args.dryrun)
277+
278+
def parse_cmdline_args():
279+
parser = argparse.ArgumentParser(description='Update pod files with '
280+
'latest pod versions')
281+
parser.add_argument('--dryrun', action='store_true',
282+
help='Just print the replaced lines, DO NOT overwrite any files')
283+
parser.add_argument( "--log_level", default="info",
284+
help="Logging level (debug, warning, info)")
285+
# iOS options
286+
parser.add_argument('--podfiles', nargs='+', default=(os.getcwd(),),
287+
help= 'List of pod files or directories containing podfiles')
288+
parser.add_argument('--specs_repo',
289+
help= 'Local checkout of github Cocoapods Specs repository')
290+
# Android options
291+
parser.add_argument('--depfiles', nargs='+',
292+
default=('Android/firebase_dependencies.gradle',),
293+
help= 'List of android dependency files or directories'
294+
'containing them.')
295+
parser.add_argument('--readmefiles', nargs='+',
296+
default=('release_build_files/readme.md',),
297+
help= 'List of release readme markdown files or directories'
298+
'containing them.')
299+
300+
args = parser.parse_args()
301+
302+
# Special handling for log level argument
303+
log_levels = {
304+
'critical': logging.CRITICAL,
305+
'error': logging.ERROR,
306+
'warning': logging.WARNING,
307+
'info': logging.INFO,
308+
'debug': logging.DEBUG
309+
}
310+
311+
level = log_levels.get(args.log_level.lower())
312+
if level is None:
313+
raise ValueError('Please use one of the following as'
314+
'log levels:\n{0}'.format(','.join(log_levels.keys())))
315+
logging.basicConfig(level=level)
316+
logger = logging.getLogger(__name__)
317+
return args
318+
319+
if __name__ == '__main__':
320+
main()
321+
# from IPython import embed
322+
# embed()

0 commit comments

Comments
 (0)