1*6dbdd20aSAndroid Build Coastguard Worker#!/usr/bin/env python3 2*6dbdd20aSAndroid Build Coastguard Worker""" 3*6dbdd20aSAndroid Build Coastguard WorkerScript to synchronize (local>remote and viceversa) test data files from/to GCS. 4*6dbdd20aSAndroid Build Coastguard Worker 5*6dbdd20aSAndroid Build Coastguard Worker//test/data files are not checked in the codebase because they are large binary 6*6dbdd20aSAndroid Build Coastguard Workerfile and change frequently. Instead we check-in only xxx.sha256 files, which 7*6dbdd20aSAndroid Build Coastguard Workercontain the SHA-256 of the actual binary file, and sync them from a GCS bucket. 8*6dbdd20aSAndroid Build Coastguard Worker 9*6dbdd20aSAndroid Build Coastguard WorkerFile in the GCS bucket are content-indexed as gs://bucket/file_name-a1b2c3f4 . 10*6dbdd20aSAndroid Build Coastguard Worker 11*6dbdd20aSAndroid Build Coastguard WorkerUsage: 12*6dbdd20aSAndroid Build Coastguard Worker./test_data status # Prints the status of new & modified files. 13*6dbdd20aSAndroid Build Coastguard Worker./test_data download # To sync remote>local (used by install-build-deps). 14*6dbdd20aSAndroid Build Coastguard Worker./test_data upload # To upload newly created and modified files. 15*6dbdd20aSAndroid Build Coastguard Worker""" 16*6dbdd20aSAndroid Build Coastguard Worker 17*6dbdd20aSAndroid Build Coastguard Workerimport argparse 18*6dbdd20aSAndroid Build Coastguard Workerimport logging 19*6dbdd20aSAndroid Build Coastguard Workerimport os 20*6dbdd20aSAndroid Build Coastguard Workerimport sys 21*6dbdd20aSAndroid Build Coastguard Workerimport hashlib 22*6dbdd20aSAndroid Build Coastguard Workerimport subprocess 23*6dbdd20aSAndroid Build Coastguard Worker 24*6dbdd20aSAndroid Build Coastguard Workerfrom multiprocessing.pool import ThreadPool 25*6dbdd20aSAndroid Build Coastguard Workerfrom collections import namedtuple, defaultdict 26*6dbdd20aSAndroid Build Coastguard Worker 27*6dbdd20aSAndroid Build Coastguard WorkerROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 28*6dbdd20aSAndroid Build Coastguard WorkerBUCKET = 'gs://perfetto/test_data' 29*6dbdd20aSAndroid Build Coastguard WorkerSUFFIX = '.sha256' 30*6dbdd20aSAndroid Build Coastguard Worker 31*6dbdd20aSAndroid Build Coastguard WorkerFS_MATCH = 'matches' 32*6dbdd20aSAndroid Build Coastguard WorkerFS_NEW_FILE = 'needs upload' 33*6dbdd20aSAndroid Build Coastguard WorkerFS_MODIFIED = 'modified' 34*6dbdd20aSAndroid Build Coastguard WorkerFS_MISSING = 'needs download' 35*6dbdd20aSAndroid Build Coastguard Worker 36*6dbdd20aSAndroid Build Coastguard WorkerFileStat = namedtuple('FileStat', 37*6dbdd20aSAndroid Build Coastguard Worker ['path', 'status', 'actual_digest', 'expected_digest']) 38*6dbdd20aSAndroid Build Coastguard Workerargs = None 39*6dbdd20aSAndroid Build Coastguard Worker 40*6dbdd20aSAndroid Build Coastguard Worker 41*6dbdd20aSAndroid Build Coastguard Workerdef relpath(path): 42*6dbdd20aSAndroid Build Coastguard Worker return os.path.relpath(path, ROOT_DIR) 43*6dbdd20aSAndroid Build Coastguard Worker 44*6dbdd20aSAndroid Build Coastguard Worker 45*6dbdd20aSAndroid Build Coastguard Workerdef download(url, out_file): 46*6dbdd20aSAndroid Build Coastguard Worker subprocess.check_call(['curl', '-L', '-s', '-o', out_file, url]) 47*6dbdd20aSAndroid Build Coastguard Worker 48*6dbdd20aSAndroid Build Coastguard Worker 49*6dbdd20aSAndroid Build Coastguard Workerdef list_files(path, scan_new_files=False): 50*6dbdd20aSAndroid Build Coastguard Worker """ List files recursively in path. 51*6dbdd20aSAndroid Build Coastguard Worker 52*6dbdd20aSAndroid Build Coastguard Worker If scan_new_files=False, returns only files with a maching xxx.sha256 tracker. 53*6dbdd20aSAndroid Build Coastguard Worker If scan_new_files=True returns all files including untracked ones. 54*6dbdd20aSAndroid Build Coastguard Worker """ 55*6dbdd20aSAndroid Build Coastguard Worker seen = set() 56*6dbdd20aSAndroid Build Coastguard Worker for root, _, files in os.walk(path): 57*6dbdd20aSAndroid Build Coastguard Worker for fname in files: 58*6dbdd20aSAndroid Build Coastguard Worker if fname.endswith('.swp'): 59*6dbdd20aSAndroid Build Coastguard Worker continue # Temporary files left around if CTRL-C-ing while downloading. 60*6dbdd20aSAndroid Build Coastguard Worker if fname in ["OWNERS", "README.md"]: 61*6dbdd20aSAndroid Build Coastguard Worker continue # OWNERS or README.md file should not be uploaded. 62*6dbdd20aSAndroid Build Coastguard Worker fpath = os.path.join(root, fname) 63*6dbdd20aSAndroid Build Coastguard Worker if not os.path.isfile(fpath) or fname.startswith('.'): 64*6dbdd20aSAndroid Build Coastguard Worker continue 65*6dbdd20aSAndroid Build Coastguard Worker if fpath.endswith(SUFFIX): 66*6dbdd20aSAndroid Build Coastguard Worker fpath = fpath[:-len(SUFFIX)] 67*6dbdd20aSAndroid Build Coastguard Worker elif not scan_new_files: 68*6dbdd20aSAndroid Build Coastguard Worker continue 69*6dbdd20aSAndroid Build Coastguard Worker if fpath not in seen: 70*6dbdd20aSAndroid Build Coastguard Worker seen.add(fpath) 71*6dbdd20aSAndroid Build Coastguard Worker yield fpath 72*6dbdd20aSAndroid Build Coastguard Worker 73*6dbdd20aSAndroid Build Coastguard Worker 74*6dbdd20aSAndroid Build Coastguard Workerdef hash_file(fpath): 75*6dbdd20aSAndroid Build Coastguard Worker hasher = hashlib.sha256() 76*6dbdd20aSAndroid Build Coastguard Worker with open(fpath, 'rb') as f: 77*6dbdd20aSAndroid Build Coastguard Worker for chunk in iter(lambda: f.read(32768), b''): 78*6dbdd20aSAndroid Build Coastguard Worker hasher.update(chunk) 79*6dbdd20aSAndroid Build Coastguard Worker return hasher.hexdigest() 80*6dbdd20aSAndroid Build Coastguard Worker 81*6dbdd20aSAndroid Build Coastguard Worker 82*6dbdd20aSAndroid Build Coastguard Workerdef map_concurrently(fn, files): 83*6dbdd20aSAndroid Build Coastguard Worker done = 0 84*6dbdd20aSAndroid Build Coastguard Worker for fs in ThreadPool(args.jobs).imap_unordered(fn, files): 85*6dbdd20aSAndroid Build Coastguard Worker assert (isinstance(fs, FileStat)) 86*6dbdd20aSAndroid Build Coastguard Worker done += 1 87*6dbdd20aSAndroid Build Coastguard Worker if not args.quiet: 88*6dbdd20aSAndroid Build Coastguard Worker print( 89*6dbdd20aSAndroid Build Coastguard Worker '[%d/%d] %-60s' % (done, len(files), relpath(fs.path)[-60:]), 90*6dbdd20aSAndroid Build Coastguard Worker end='\r') 91*6dbdd20aSAndroid Build Coastguard Worker if not args.quiet: 92*6dbdd20aSAndroid Build Coastguard Worker print('') 93*6dbdd20aSAndroid Build Coastguard Worker 94*6dbdd20aSAndroid Build Coastguard Worker 95*6dbdd20aSAndroid Build Coastguard Workerdef get_file_status(fpath): 96*6dbdd20aSAndroid Build Coastguard Worker sha_file = fpath + SUFFIX 97*6dbdd20aSAndroid Build Coastguard Worker sha_exists = os.path.exists(sha_file) 98*6dbdd20aSAndroid Build Coastguard Worker file_exists = os.path.exists(fpath) 99*6dbdd20aSAndroid Build Coastguard Worker actual_digest = None 100*6dbdd20aSAndroid Build Coastguard Worker expected_digest = None 101*6dbdd20aSAndroid Build Coastguard Worker if sha_exists: 102*6dbdd20aSAndroid Build Coastguard Worker with open(sha_file, 'r') as f: 103*6dbdd20aSAndroid Build Coastguard Worker expected_digest = f.readline().strip() 104*6dbdd20aSAndroid Build Coastguard Worker if file_exists: 105*6dbdd20aSAndroid Build Coastguard Worker actual_digest = hash_file(fpath) 106*6dbdd20aSAndroid Build Coastguard Worker if sha_exists and not file_exists: 107*6dbdd20aSAndroid Build Coastguard Worker status = FS_MISSING 108*6dbdd20aSAndroid Build Coastguard Worker elif not sha_exists and file_exists: 109*6dbdd20aSAndroid Build Coastguard Worker status = FS_NEW_FILE 110*6dbdd20aSAndroid Build Coastguard Worker elif not sha_exists and not file_exists: 111*6dbdd20aSAndroid Build Coastguard Worker raise Exception(fpath) 112*6dbdd20aSAndroid Build Coastguard Worker elif expected_digest == actual_digest: 113*6dbdd20aSAndroid Build Coastguard Worker status = FS_MATCH 114*6dbdd20aSAndroid Build Coastguard Worker else: 115*6dbdd20aSAndroid Build Coastguard Worker status = FS_MODIFIED 116*6dbdd20aSAndroid Build Coastguard Worker return FileStat(fpath, status, actual_digest, expected_digest) 117*6dbdd20aSAndroid Build Coastguard Worker 118*6dbdd20aSAndroid Build Coastguard Worker 119*6dbdd20aSAndroid Build Coastguard Workerdef cmd_upload(dir): 120*6dbdd20aSAndroid Build Coastguard Worker all_files = list_files(dir, scan_new_files=True) 121*6dbdd20aSAndroid Build Coastguard Worker files_to_upload = [] 122*6dbdd20aSAndroid Build Coastguard Worker for fs in ThreadPool(args.jobs).imap_unordered(get_file_status, all_files): 123*6dbdd20aSAndroid Build Coastguard Worker if fs.status in (FS_NEW_FILE, FS_MODIFIED): 124*6dbdd20aSAndroid Build Coastguard Worker files_to_upload.append(fs) 125*6dbdd20aSAndroid Build Coastguard Worker if len(files_to_upload) == 0: 126*6dbdd20aSAndroid Build Coastguard Worker if not args.quiet: 127*6dbdd20aSAndroid Build Coastguard Worker print('No modified or new files require uploading') 128*6dbdd20aSAndroid Build Coastguard Worker return 0 129*6dbdd20aSAndroid Build Coastguard Worker if args.dry_run: 130*6dbdd20aSAndroid Build Coastguard Worker return 0 131*6dbdd20aSAndroid Build Coastguard Worker if not args.quiet: 132*6dbdd20aSAndroid Build Coastguard Worker print('\n'.join(relpath(f.path) for f in files_to_upload)) 133*6dbdd20aSAndroid Build Coastguard Worker print('') 134*6dbdd20aSAndroid Build Coastguard Worker print('About to upload %d files' % len(files_to_upload)) 135*6dbdd20aSAndroid Build Coastguard Worker input('Press a key to continue or CTRL-C to abort') 136*6dbdd20aSAndroid Build Coastguard Worker 137*6dbdd20aSAndroid Build Coastguard Worker def upload_one_file(fs): 138*6dbdd20aSAndroid Build Coastguard Worker assert (fs.actual_digest is not None) 139*6dbdd20aSAndroid Build Coastguard Worker dst_name = '%s/%s-%s' % (args.bucket, os.path.basename( 140*6dbdd20aSAndroid Build Coastguard Worker fs.path), fs.actual_digest) 141*6dbdd20aSAndroid Build Coastguard Worker cmd = ['gsutil', '-q', 'cp', '-n', '-a', 'public-read', fs.path, dst_name] 142*6dbdd20aSAndroid Build Coastguard Worker logging.debug(' '.join(cmd)) 143*6dbdd20aSAndroid Build Coastguard Worker subprocess.check_call(cmd) 144*6dbdd20aSAndroid Build Coastguard Worker with open(fs.path + SUFFIX + '.swp', 'w') as f: 145*6dbdd20aSAndroid Build Coastguard Worker f.write(fs.actual_digest) 146*6dbdd20aSAndroid Build Coastguard Worker os.replace(fs.path + SUFFIX + '.swp', fs.path + SUFFIX) 147*6dbdd20aSAndroid Build Coastguard Worker return fs 148*6dbdd20aSAndroid Build Coastguard Worker 149*6dbdd20aSAndroid Build Coastguard Worker map_concurrently(upload_one_file, files_to_upload) 150*6dbdd20aSAndroid Build Coastguard Worker return 0 151*6dbdd20aSAndroid Build Coastguard Worker 152*6dbdd20aSAndroid Build Coastguard Worker 153*6dbdd20aSAndroid Build Coastguard Workerdef cmd_clean(dir): 154*6dbdd20aSAndroid Build Coastguard Worker all_files = list_files(dir, scan_new_files=True) 155*6dbdd20aSAndroid Build Coastguard Worker files_to_clean = [] 156*6dbdd20aSAndroid Build Coastguard Worker for fs in ThreadPool(args.jobs).imap_unordered(get_file_status, all_files): 157*6dbdd20aSAndroid Build Coastguard Worker if fs.status in (FS_NEW_FILE, FS_MODIFIED): 158*6dbdd20aSAndroid Build Coastguard Worker files_to_clean.append(fs.path) 159*6dbdd20aSAndroid Build Coastguard Worker if len(files_to_clean) == 0: 160*6dbdd20aSAndroid Build Coastguard Worker if not args.quiet: 161*6dbdd20aSAndroid Build Coastguard Worker print('No modified or new files require cleaning') 162*6dbdd20aSAndroid Build Coastguard Worker return 0 163*6dbdd20aSAndroid Build Coastguard Worker if args.dry_run: 164*6dbdd20aSAndroid Build Coastguard Worker return 0 165*6dbdd20aSAndroid Build Coastguard Worker if not args.quiet: 166*6dbdd20aSAndroid Build Coastguard Worker print('\n'.join(relpath(f) for f in files_to_clean)) 167*6dbdd20aSAndroid Build Coastguard Worker print('') 168*6dbdd20aSAndroid Build Coastguard Worker print('About to remove %d files' % len(files_to_clean)) 169*6dbdd20aSAndroid Build Coastguard Worker input('Press a key to continue or CTRL-C to abort') 170*6dbdd20aSAndroid Build Coastguard Worker list(map(os.remove, files_to_clean)) 171*6dbdd20aSAndroid Build Coastguard Worker return 0 172*6dbdd20aSAndroid Build Coastguard Worker 173*6dbdd20aSAndroid Build Coastguard Worker 174*6dbdd20aSAndroid Build Coastguard Workerdef cmd_download(dir, overwrite_locally_modified=False): 175*6dbdd20aSAndroid Build Coastguard Worker files_to_download = [] 176*6dbdd20aSAndroid Build Coastguard Worker modified = [] 177*6dbdd20aSAndroid Build Coastguard Worker all_files = list_files(dir, scan_new_files=False) 178*6dbdd20aSAndroid Build Coastguard Worker for fs in ThreadPool(args.jobs).imap_unordered(get_file_status, all_files): 179*6dbdd20aSAndroid Build Coastguard Worker if fs.status == FS_MISSING: 180*6dbdd20aSAndroid Build Coastguard Worker files_to_download.append(fs) 181*6dbdd20aSAndroid Build Coastguard Worker elif fs.status == FS_MODIFIED: 182*6dbdd20aSAndroid Build Coastguard Worker modified.append(fs) 183*6dbdd20aSAndroid Build Coastguard Worker 184*6dbdd20aSAndroid Build Coastguard Worker if len(modified) > 0 and not overwrite_locally_modified: 185*6dbdd20aSAndroid Build Coastguard Worker print('WARNING: The following files diverged locally and will NOT be ' + 186*6dbdd20aSAndroid Build Coastguard Worker 'overwritten if you continue') 187*6dbdd20aSAndroid Build Coastguard Worker print('\n'.join(relpath(f.path) for f in modified)) 188*6dbdd20aSAndroid Build Coastguard Worker print('') 189*6dbdd20aSAndroid Build Coastguard Worker print('Re run `download --overwrite` to overwrite locally modified files') 190*6dbdd20aSAndroid Build Coastguard Worker print('or `upload` to sync them on the GCS bucket') 191*6dbdd20aSAndroid Build Coastguard Worker print('') 192*6dbdd20aSAndroid Build Coastguard Worker input('Press a key to continue or CTRL-C to abort') 193*6dbdd20aSAndroid Build Coastguard Worker elif overwrite_locally_modified: 194*6dbdd20aSAndroid Build Coastguard Worker files_to_download += modified 195*6dbdd20aSAndroid Build Coastguard Worker 196*6dbdd20aSAndroid Build Coastguard Worker if len(files_to_download) == 0: 197*6dbdd20aSAndroid Build Coastguard Worker if not args.quiet: 198*6dbdd20aSAndroid Build Coastguard Worker print('Nothing to do, all files are synced') 199*6dbdd20aSAndroid Build Coastguard Worker return 0 200*6dbdd20aSAndroid Build Coastguard Worker 201*6dbdd20aSAndroid Build Coastguard Worker if not args.quiet: 202*6dbdd20aSAndroid Build Coastguard Worker print('Downloading %d files in //%s' % 203*6dbdd20aSAndroid Build Coastguard Worker (len(files_to_download), relpath(args.dir))) 204*6dbdd20aSAndroid Build Coastguard Worker if args.dry_run: 205*6dbdd20aSAndroid Build Coastguard Worker print('\n'.join(files_to_download)) 206*6dbdd20aSAndroid Build Coastguard Worker return 207*6dbdd20aSAndroid Build Coastguard Worker 208*6dbdd20aSAndroid Build Coastguard Worker def download_one_file(fs): 209*6dbdd20aSAndroid Build Coastguard Worker assert (fs.expected_digest is not None) 210*6dbdd20aSAndroid Build Coastguard Worker uri = '%s/%s-%s' % (args.bucket, os.path.basename( 211*6dbdd20aSAndroid Build Coastguard Worker fs.path), fs.expected_digest) 212*6dbdd20aSAndroid Build Coastguard Worker uri = uri.replace('gs://', 'https://storage.googleapis.com/') 213*6dbdd20aSAndroid Build Coastguard Worker logging.debug(uri) 214*6dbdd20aSAndroid Build Coastguard Worker tmp_path = fs.path + '.swp' 215*6dbdd20aSAndroid Build Coastguard Worker download(uri, tmp_path) 216*6dbdd20aSAndroid Build Coastguard Worker digest = hash_file(tmp_path) 217*6dbdd20aSAndroid Build Coastguard Worker if digest != fs.expected_digest: 218*6dbdd20aSAndroid Build Coastguard Worker raise Exception('Mismatching digest for %s. expected=%s, actual=%s' % 219*6dbdd20aSAndroid Build Coastguard Worker (uri, fs.expected_digest, digest)) 220*6dbdd20aSAndroid Build Coastguard Worker os.replace(tmp_path, fs.path) 221*6dbdd20aSAndroid Build Coastguard Worker return fs 222*6dbdd20aSAndroid Build Coastguard Worker 223*6dbdd20aSAndroid Build Coastguard Worker map_concurrently(download_one_file, files_to_download) 224*6dbdd20aSAndroid Build Coastguard Worker return 0 225*6dbdd20aSAndroid Build Coastguard Worker 226*6dbdd20aSAndroid Build Coastguard Worker 227*6dbdd20aSAndroid Build Coastguard Workerdef cmd_status(dir): 228*6dbdd20aSAndroid Build Coastguard Worker files = list_files(dir, scan_new_files=True) 229*6dbdd20aSAndroid Build Coastguard Worker file_by_status = defaultdict(list) 230*6dbdd20aSAndroid Build Coastguard Worker num_files = 0 231*6dbdd20aSAndroid Build Coastguard Worker num_out_of_sync = 0 232*6dbdd20aSAndroid Build Coastguard Worker for fs in ThreadPool(args.jobs).imap_unordered(get_file_status, files): 233*6dbdd20aSAndroid Build Coastguard Worker file_by_status[fs.status].append(relpath(fs.path)) 234*6dbdd20aSAndroid Build Coastguard Worker num_files += 1 235*6dbdd20aSAndroid Build Coastguard Worker for status, rpaths in sorted(file_by_status.items()): 236*6dbdd20aSAndroid Build Coastguard Worker if status == FS_NEW_FILE and args.ignore_new: 237*6dbdd20aSAndroid Build Coastguard Worker continue 238*6dbdd20aSAndroid Build Coastguard Worker if status != FS_MATCH: 239*6dbdd20aSAndroid Build Coastguard Worker for rpath in rpaths: 240*6dbdd20aSAndroid Build Coastguard Worker num_out_of_sync += 1 241*6dbdd20aSAndroid Build Coastguard Worker if not args.quiet: 242*6dbdd20aSAndroid Build Coastguard Worker print('%-15s: %s' % (status, rpath)) 243*6dbdd20aSAndroid Build Coastguard Worker if num_out_of_sync == 0: 244*6dbdd20aSAndroid Build Coastguard Worker if not args.quiet: 245*6dbdd20aSAndroid Build Coastguard Worker print('Scanned %d files in //%s, everything in sync.' % 246*6dbdd20aSAndroid Build Coastguard Worker (num_files, relpath(dir))) 247*6dbdd20aSAndroid Build Coastguard Worker return 0 248*6dbdd20aSAndroid Build Coastguard Worker return 1 249*6dbdd20aSAndroid Build Coastguard Worker 250*6dbdd20aSAndroid Build Coastguard Worker 251*6dbdd20aSAndroid Build Coastguard Workerdef main(): 252*6dbdd20aSAndroid Build Coastguard Worker parser = argparse.ArgumentParser() 253*6dbdd20aSAndroid Build Coastguard Worker parser.add_argument('--dir', default=os.path.join(ROOT_DIR, 'test/data')) 254*6dbdd20aSAndroid Build Coastguard Worker parser.add_argument('--overwrite', action='store_true') 255*6dbdd20aSAndroid Build Coastguard Worker parser.add_argument('--bucket', default=BUCKET) 256*6dbdd20aSAndroid Build Coastguard Worker parser.add_argument('--jobs', '-j', default=10, type=int) 257*6dbdd20aSAndroid Build Coastguard Worker parser.add_argument('--dry-run', '-n', action='store_true') 258*6dbdd20aSAndroid Build Coastguard Worker parser.add_argument('--quiet', '-q', action='store_true') 259*6dbdd20aSAndroid Build Coastguard Worker parser.add_argument('--verbose', '-v', action='store_true') 260*6dbdd20aSAndroid Build Coastguard Worker parser.add_argument('--ignore-new', action='store_true') 261*6dbdd20aSAndroid Build Coastguard Worker parser.add_argument('cmd', choices=['status', 'download', 'upload', 'clean']) 262*6dbdd20aSAndroid Build Coastguard Worker global args 263*6dbdd20aSAndroid Build Coastguard Worker args = parser.parse_args() 264*6dbdd20aSAndroid Build Coastguard Worker logging.basicConfig( 265*6dbdd20aSAndroid Build Coastguard Worker format='%(asctime)s %(levelname).1s %(message)s', 266*6dbdd20aSAndroid Build Coastguard Worker level=logging.DEBUG if args.verbose else logging.INFO, 267*6dbdd20aSAndroid Build Coastguard Worker datefmt=r'%H:%M:%S') 268*6dbdd20aSAndroid Build Coastguard Worker if args.cmd == 'status': 269*6dbdd20aSAndroid Build Coastguard Worker return cmd_status(args.dir) 270*6dbdd20aSAndroid Build Coastguard Worker if args.cmd == 'download': 271*6dbdd20aSAndroid Build Coastguard Worker return cmd_download(args.dir, overwrite_locally_modified=args.overwrite) 272*6dbdd20aSAndroid Build Coastguard Worker if args.cmd == 'upload': 273*6dbdd20aSAndroid Build Coastguard Worker return cmd_upload(args.dir) 274*6dbdd20aSAndroid Build Coastguard Worker if args.cmd == 'clean': 275*6dbdd20aSAndroid Build Coastguard Worker return cmd_clean(args.dir) 276*6dbdd20aSAndroid Build Coastguard Worker print('Unknown command: %s' % args.cmd) 277*6dbdd20aSAndroid Build Coastguard Worker 278*6dbdd20aSAndroid Build Coastguard Worker 279*6dbdd20aSAndroid Build Coastguard Workerif __name__ == '__main__': 280*6dbdd20aSAndroid Build Coastguard Worker sys.exit(main()) 281