|
| 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