1*3c875a21SAndroid Build Coastguard Worker# Copyright (C) 2018 The Android Open Source Project 2*3c875a21SAndroid Build Coastguard Worker# 3*3c875a21SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 4*3c875a21SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 5*3c875a21SAndroid Build Coastguard Worker# You may obtain a copy of the License at 6*3c875a21SAndroid Build Coastguard Worker# 7*3c875a21SAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 8*3c875a21SAndroid Build Coastguard Worker# 9*3c875a21SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 10*3c875a21SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 11*3c875a21SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*3c875a21SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 13*3c875a21SAndroid Build Coastguard Worker# limitations under the License. 14*3c875a21SAndroid Build Coastguard Worker"""Send notification email if new version is found. 15*3c875a21SAndroid Build Coastguard Worker 16*3c875a21SAndroid Build Coastguard WorkerExample usage: 17*3c875a21SAndroid Build Coastguard Workerexternal_updater_notifier \ 18*3c875a21SAndroid Build Coastguard Worker --history ~/updater/history \ 19*3c875a21SAndroid Build Coastguard Worker --generate_change \ 20*3c875a21SAndroid Build Coastguard Worker --recipients [email protected] \ 21*3c875a21SAndroid Build Coastguard Worker googletest 22*3c875a21SAndroid Build Coastguard Worker""" 23*3c875a21SAndroid Build Coastguard Worker 24*3c875a21SAndroid Build Coastguard Workerfrom datetime import timedelta, datetime 25*3c875a21SAndroid Build Coastguard Workerimport argparse 26*3c875a21SAndroid Build Coastguard Workerimport json 27*3c875a21SAndroid Build Coastguard Workerimport os 28*3c875a21SAndroid Build Coastguard Workerimport re 29*3c875a21SAndroid Build Coastguard Workerimport subprocess 30*3c875a21SAndroid Build Coastguard Workerimport time 31*3c875a21SAndroid Build Coastguard Worker 32*3c875a21SAndroid Build Coastguard Worker# pylint: disable=invalid-name 33*3c875a21SAndroid Build Coastguard Worker 34*3c875a21SAndroid Build Coastguard Workerdef parse_args(): 35*3c875a21SAndroid Build Coastguard Worker """Parses commandline arguments.""" 36*3c875a21SAndroid Build Coastguard Worker 37*3c875a21SAndroid Build Coastguard Worker parser = argparse.ArgumentParser( 38*3c875a21SAndroid Build Coastguard Worker description='Check updates for third party projects in external/.') 39*3c875a21SAndroid Build Coastguard Worker parser.add_argument('--history', 40*3c875a21SAndroid Build Coastguard Worker help='Path of history file. If doesn' 41*3c875a21SAndroid Build Coastguard Worker 't exist, a new one will be created.') 42*3c875a21SAndroid Build Coastguard Worker parser.add_argument( 43*3c875a21SAndroid Build Coastguard Worker '--recipients', 44*3c875a21SAndroid Build Coastguard Worker help='Comma separated recipients of notification email.') 45*3c875a21SAndroid Build Coastguard Worker parser.add_argument( 46*3c875a21SAndroid Build Coastguard Worker '--generate_change', 47*3c875a21SAndroid Build Coastguard Worker help='If set, an upgrade change will be uploaded to Gerrit.', 48*3c875a21SAndroid Build Coastguard Worker action='store_true', 49*3c875a21SAndroid Build Coastguard Worker required=False) 50*3c875a21SAndroid Build Coastguard Worker parser.add_argument('paths', nargs='*', help='Paths of the project.') 51*3c875a21SAndroid Build Coastguard Worker parser.add_argument('--all', 52*3c875a21SAndroid Build Coastguard Worker action='store_true', 53*3c875a21SAndroid Build Coastguard Worker help='Checks all projects.') 54*3c875a21SAndroid Build Coastguard Worker 55*3c875a21SAndroid Build Coastguard Worker return parser.parse_args() 56*3c875a21SAndroid Build Coastguard Worker 57*3c875a21SAndroid Build Coastguard Worker 58*3c875a21SAndroid Build Coastguard Workerdef _get_android_top(): 59*3c875a21SAndroid Build Coastguard Worker return os.environ['ANDROID_BUILD_TOP'] 60*3c875a21SAndroid Build Coastguard Worker 61*3c875a21SAndroid Build Coastguard Worker 62*3c875a21SAndroid Build Coastguard WorkerCHANGE_URL_PATTERN = r'(https:\/\/[^\s]*android-review[^\s]*) Upgrade' 63*3c875a21SAndroid Build Coastguard WorkerCHANGE_URL_RE = re.compile(CHANGE_URL_PATTERN) 64*3c875a21SAndroid Build Coastguard Worker 65*3c875a21SAndroid Build Coastguard Worker 66*3c875a21SAndroid Build Coastguard Workerdef _read_owner_file(proj): 67*3c875a21SAndroid Build Coastguard Worker owner_file = os.path.join(_get_android_top(), 'external', proj, 'OWNERS') 68*3c875a21SAndroid Build Coastguard Worker if not os.path.isfile(owner_file): 69*3c875a21SAndroid Build Coastguard Worker return None 70*3c875a21SAndroid Build Coastguard Worker with open(owner_file, 'r', encoding='utf-8') as f: 71*3c875a21SAndroid Build Coastguard Worker return f.read().strip() 72*3c875a21SAndroid Build Coastguard Worker 73*3c875a21SAndroid Build Coastguard Worker 74*3c875a21SAndroid Build Coastguard Workerdef _send_email(proj, latest_ver, recipient, upgrade_log): 75*3c875a21SAndroid Build Coastguard Worker print(f'Sending email for {proj}: {latest_ver}') 76*3c875a21SAndroid Build Coastguard Worker msg = "" 77*3c875a21SAndroid Build Coastguard Worker match = CHANGE_URL_RE.search(upgrade_log) 78*3c875a21SAndroid Build Coastguard Worker if match is not None: 79*3c875a21SAndroid Build Coastguard Worker subject = "[Succeeded]" 80*3c875a21SAndroid Build Coastguard Worker msg = f'An upgrade change is generated at:\n{match.group(1)}' 81*3c875a21SAndroid Build Coastguard Worker else: 82*3c875a21SAndroid Build Coastguard Worker subject = "[Failed]" 83*3c875a21SAndroid Build Coastguard Worker msg = 'Failed to generate upgrade change. See logs below for details.' 84*3c875a21SAndroid Build Coastguard Worker 85*3c875a21SAndroid Build Coastguard Worker subject += f" {proj} {latest_ver}" 86*3c875a21SAndroid Build Coastguard Worker owners = _read_owner_file(proj) 87*3c875a21SAndroid Build Coastguard Worker if owners: 88*3c875a21SAndroid Build Coastguard Worker msg += '\n\nOWNERS file: \n' 89*3c875a21SAndroid Build Coastguard Worker msg += owners 90*3c875a21SAndroid Build Coastguard Worker 91*3c875a21SAndroid Build Coastguard Worker msg += '\n\n' 92*3c875a21SAndroid Build Coastguard Worker msg += upgrade_log 93*3c875a21SAndroid Build Coastguard Worker 94*3c875a21SAndroid Build Coastguard Worker cc_recipient = '' 95*3c875a21SAndroid Build Coastguard Worker for line in owners.splitlines(): 96*3c875a21SAndroid Build Coastguard Worker line = line.strip() 97*3c875a21SAndroid Build Coastguard Worker if line.endswith('@google.com'): 98*3c875a21SAndroid Build Coastguard Worker cc_recipient += line 99*3c875a21SAndroid Build Coastguard Worker cc_recipient += ',' 100*3c875a21SAndroid Build Coastguard Worker 101*3c875a21SAndroid Build Coastguard Worker subprocess.run(['sendgmr', 102*3c875a21SAndroid Build Coastguard Worker f'--to={recipient}', 103*3c875a21SAndroid Build Coastguard Worker f'--cc={cc_recipient}', 104*3c875a21SAndroid Build Coastguard Worker f'--subject={subject}'], 105*3c875a21SAndroid Build Coastguard Worker check=True, 106*3c875a21SAndroid Build Coastguard Worker stdout=subprocess.PIPE, 107*3c875a21SAndroid Build Coastguard Worker stderr=subprocess.PIPE, 108*3c875a21SAndroid Build Coastguard Worker input=msg, 109*3c875a21SAndroid Build Coastguard Worker encoding='ascii') 110*3c875a21SAndroid Build Coastguard Worker 111*3c875a21SAndroid Build Coastguard Worker 112*3c875a21SAndroid Build Coastguard WorkerCOMMIT_PATTERN = r'^[a-f0-9]{40}$' 113*3c875a21SAndroid Build Coastguard WorkerCOMMIT_RE = re.compile(COMMIT_PATTERN) 114*3c875a21SAndroid Build Coastguard Worker 115*3c875a21SAndroid Build Coastguard Worker 116*3c875a21SAndroid Build Coastguard Workerdef is_commit(commit: str) -> bool: 117*3c875a21SAndroid Build Coastguard Worker """Whether a string looks like a SHA1 hash.""" 118*3c875a21SAndroid Build Coastguard Worker return bool(COMMIT_RE.match(commit)) 119*3c875a21SAndroid Build Coastguard Worker 120*3c875a21SAndroid Build Coastguard Worker 121*3c875a21SAndroid Build Coastguard WorkerNOTIFIED_TIME_KEY_NAME = 'latest_notified_time' 122*3c875a21SAndroid Build Coastguard Worker 123*3c875a21SAndroid Build Coastguard Worker 124*3c875a21SAndroid Build Coastguard Workerdef _should_notify(latest_ver, proj_history): 125*3c875a21SAndroid Build Coastguard Worker if latest_ver in proj_history: 126*3c875a21SAndroid Build Coastguard Worker # Processed this version before. 127*3c875a21SAndroid Build Coastguard Worker return False 128*3c875a21SAndroid Build Coastguard Worker 129*3c875a21SAndroid Build Coastguard Worker timestamp = proj_history.get(NOTIFIED_TIME_KEY_NAME, 0) 130*3c875a21SAndroid Build Coastguard Worker time_diff = datetime.today() - datetime.fromtimestamp(timestamp) 131*3c875a21SAndroid Build Coastguard Worker if is_commit(latest_ver) and time_diff <= timedelta(days=30): 132*3c875a21SAndroid Build Coastguard Worker return False 133*3c875a21SAndroid Build Coastguard Worker 134*3c875a21SAndroid Build Coastguard Worker return True 135*3c875a21SAndroid Build Coastguard Worker 136*3c875a21SAndroid Build Coastguard Worker 137*3c875a21SAndroid Build Coastguard Workerdef _process_results(args, history, results): 138*3c875a21SAndroid Build Coastguard Worker for proj, res in results.items(): 139*3c875a21SAndroid Build Coastguard Worker if 'latest' not in res: 140*3c875a21SAndroid Build Coastguard Worker continue 141*3c875a21SAndroid Build Coastguard Worker latest_ver = res['latest'] 142*3c875a21SAndroid Build Coastguard Worker current_ver = res['current'] 143*3c875a21SAndroid Build Coastguard Worker if latest_ver == current_ver: 144*3c875a21SAndroid Build Coastguard Worker continue 145*3c875a21SAndroid Build Coastguard Worker proj_history = history.setdefault(proj, {}) 146*3c875a21SAndroid Build Coastguard Worker if _should_notify(latest_ver, proj_history): 147*3c875a21SAndroid Build Coastguard Worker upgrade_log = _upgrade(proj) if args.generate_change else "" 148*3c875a21SAndroid Build Coastguard Worker try: 149*3c875a21SAndroid Build Coastguard Worker _send_email(proj, latest_ver, args.recipients, upgrade_log) 150*3c875a21SAndroid Build Coastguard Worker proj_history[latest_ver] = int(time.time()) 151*3c875a21SAndroid Build Coastguard Worker proj_history[NOTIFIED_TIME_KEY_NAME] = int(time.time()) 152*3c875a21SAndroid Build Coastguard Worker except subprocess.CalledProcessError as err: 153*3c875a21SAndroid Build Coastguard Worker msg = f"""Failed to send email for {proj} ({latest_ver}). 154*3c875a21SAndroid Build Coastguard Workerstdout: {err.stdout} 155*3c875a21SAndroid Build Coastguard Workerstderr: {err.stderr}""" 156*3c875a21SAndroid Build Coastguard Worker print(msg) 157*3c875a21SAndroid Build Coastguard Worker 158*3c875a21SAndroid Build Coastguard Worker 159*3c875a21SAndroid Build Coastguard WorkerRESULT_FILE_PATH = '/tmp/update_check_result.json' 160*3c875a21SAndroid Build Coastguard Worker 161*3c875a21SAndroid Build Coastguard Worker 162*3c875a21SAndroid Build Coastguard Workerdef send_notification(args): 163*3c875a21SAndroid Build Coastguard Worker """Compare results and send notification.""" 164*3c875a21SAndroid Build Coastguard Worker results = {} 165*3c875a21SAndroid Build Coastguard Worker with open(RESULT_FILE_PATH, 'r', encoding='utf-8') as f: 166*3c875a21SAndroid Build Coastguard Worker results = json.load(f) 167*3c875a21SAndroid Build Coastguard Worker history = {} 168*3c875a21SAndroid Build Coastguard Worker try: 169*3c875a21SAndroid Build Coastguard Worker with open(args.history, 'r', encoding='utf-8') as f: 170*3c875a21SAndroid Build Coastguard Worker history = json.load(f) 171*3c875a21SAndroid Build Coastguard Worker except (FileNotFoundError, json.decoder.JSONDecodeError): 172*3c875a21SAndroid Build Coastguard Worker pass 173*3c875a21SAndroid Build Coastguard Worker 174*3c875a21SAndroid Build Coastguard Worker _process_results(args, history, results) 175*3c875a21SAndroid Build Coastguard Worker 176*3c875a21SAndroid Build Coastguard Worker with open(args.history, 'w', encoding='utf-8') as f: 177*3c875a21SAndroid Build Coastguard Worker json.dump(history, f, sort_keys=True, indent=4) 178*3c875a21SAndroid Build Coastguard Worker 179*3c875a21SAndroid Build Coastguard Worker 180*3c875a21SAndroid Build Coastguard Workerdef _upgrade(proj): 181*3c875a21SAndroid Build Coastguard Worker # pylint: disable=subprocess-run-check 182*3c875a21SAndroid Build Coastguard Worker out = subprocess.run([ 183*3c875a21SAndroid Build Coastguard Worker 'out/soong/host/linux-x86/bin/external_updater', 'update', proj 184*3c875a21SAndroid Build Coastguard Worker ], 185*3c875a21SAndroid Build Coastguard Worker stdout=subprocess.PIPE, 186*3c875a21SAndroid Build Coastguard Worker stderr=subprocess.PIPE, 187*3c875a21SAndroid Build Coastguard Worker cwd=_get_android_top()) 188*3c875a21SAndroid Build Coastguard Worker stdout = out.stdout.decode('utf-8') 189*3c875a21SAndroid Build Coastguard Worker stderr = out.stderr.decode('utf-8') 190*3c875a21SAndroid Build Coastguard Worker return f""" 191*3c875a21SAndroid Build Coastguard Worker==================== 192*3c875a21SAndroid Build Coastguard Worker| Debug Info | 193*3c875a21SAndroid Build Coastguard Worker==================== 194*3c875a21SAndroid Build Coastguard Worker-=-=-=-=stdout=-=-=-=- 195*3c875a21SAndroid Build Coastguard Worker{stdout} 196*3c875a21SAndroid Build Coastguard Worker 197*3c875a21SAndroid Build Coastguard Worker-=-=-=-=stderr=-=-=-=- 198*3c875a21SAndroid Build Coastguard Worker{stderr} 199*3c875a21SAndroid Build Coastguard Worker""" 200*3c875a21SAndroid Build Coastguard Worker 201*3c875a21SAndroid Build Coastguard Worker 202*3c875a21SAndroid Build Coastguard Workerdef _check_updates(args): 203*3c875a21SAndroid Build Coastguard Worker params = [ 204*3c875a21SAndroid Build Coastguard Worker 'out/soong/host/linux-x86/bin/external_updater', 'check', 205*3c875a21SAndroid Build Coastguard Worker '--json_output', RESULT_FILE_PATH, '--delay', '30' 206*3c875a21SAndroid Build Coastguard Worker ] 207*3c875a21SAndroid Build Coastguard Worker if args.all: 208*3c875a21SAndroid Build Coastguard Worker params.append('--all') 209*3c875a21SAndroid Build Coastguard Worker else: 210*3c875a21SAndroid Build Coastguard Worker params += args.paths 211*3c875a21SAndroid Build Coastguard Worker 212*3c875a21SAndroid Build Coastguard Worker print(_get_android_top()) 213*3c875a21SAndroid Build Coastguard Worker # pylint: disable=subprocess-run-check 214*3c875a21SAndroid Build Coastguard Worker subprocess.run(params, cwd=_get_android_top()) 215*3c875a21SAndroid Build Coastguard Worker 216*3c875a21SAndroid Build Coastguard Worker 217*3c875a21SAndroid Build Coastguard Workerdef main(): 218*3c875a21SAndroid Build Coastguard Worker """The main entry.""" 219*3c875a21SAndroid Build Coastguard Worker 220*3c875a21SAndroid Build Coastguard Worker args = parse_args() 221*3c875a21SAndroid Build Coastguard Worker _check_updates(args) 222*3c875a21SAndroid Build Coastguard Worker send_notification(args) 223*3c875a21SAndroid Build Coastguard Worker 224*3c875a21SAndroid Build Coastguard Worker 225*3c875a21SAndroid Build Coastguard Workerif __name__ == '__main__': 226*3c875a21SAndroid Build Coastguard Worker main() 227