Tiancong Wang | 6cfc163 | 2019-07-25 21:32:37 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | # Copyright 2018 The Chromium Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | """This script is used to update our local profiles (AFDO or orderfiles) |
| 7 | |
| 8 | This uses profiles of Chrome, or orderfiles for linking, provided by our |
| 9 | friends from Chrome OS. Though the profiles are available externally, |
| 10 | the bucket they sit in is otherwise unreadable by non-Googlers. Gsutil |
| 11 | usage with this bucket is therefore quite awkward: you can't do anything |
| 12 | but `cp` certain files with an external account, and you can't even do |
| 13 | that if you're not yet authenticated. |
| 14 | |
| 15 | No authentication is necessary if you pull these profiles directly over |
| 16 | https. |
| 17 | """ |
| 18 | |
Raul Tambre | f3d9412e | 2019-09-24 05:31:44 | [diff] [blame] | 19 | from __future__ import print_function |
| 20 | |
Tiancong Wang | 6cfc163 | 2019-07-25 21:32:37 | [diff] [blame] | 21 | import argparse |
| 22 | import contextlib |
| 23 | import os |
| 24 | import subprocess |
| 25 | import sys |
| 26 | import urllib2 |
| 27 | |
| 28 | GS_HTTP_URL = 'https://storage.googleapis.com' |
| 29 | |
| 30 | |
| 31 | def ReadUpToDateProfileName(newest_profile_name_path): |
| 32 | with open(newest_profile_name_path) as f: |
| 33 | return f.read().strip() |
| 34 | |
| 35 | |
| 36 | def ReadLocalProfileName(local_profile_name_path): |
| 37 | try: |
| 38 | with open(local_profile_name_path) as f: |
| 39 | return f.read().strip() |
| 40 | except IOError: |
| 41 | # Assume it either didn't exist, or we couldn't read it. In either case, we |
| 42 | # should probably grab a new profile (and, in doing so, make this file sane |
| 43 | # again) |
| 44 | return None |
| 45 | |
| 46 | |
| 47 | def WriteLocalProfileName(name, local_profile_name_path): |
| 48 | with open(local_profile_name_path, 'w') as f: |
| 49 | f.write(name) |
| 50 | |
| 51 | |
| 52 | def CheckCallOrExit(cmd): |
| 53 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 54 | stdout, stderr = proc.communicate() |
| 55 | exit_code = proc.wait() |
| 56 | if not exit_code: |
| 57 | return |
| 58 | |
| 59 | complaint_lines = [ |
| 60 | '## %s failed with exit code %d' % (cmd[0], exit_code), |
| 61 | '## Full command: %s' % cmd, |
| 62 | '## Stdout:\n' + stdout, |
| 63 | '## Stderr:\n' + stderr, |
| 64 | ] |
Raul Tambre | f3d9412e | 2019-09-24 05:31:44 | [diff] [blame] | 65 | print('\n'.join(complaint_lines), file=sys.stderr) |
Tiancong Wang | 6cfc163 | 2019-07-25 21:32:37 | [diff] [blame] | 66 | sys.exit(1) |
| 67 | |
| 68 | |
| 69 | def RetrieveProfile(desired_profile_name, out_path, gs_url_base): |
| 70 | # vpython is > python 2.7.9, so we can expect urllib to validate HTTPS certs |
| 71 | # properly. |
| 72 | ext = os.path.splitext(desired_profile_name)[1] |
| 73 | compressed_path = out_path + ext |
| 74 | gs_prefix = 'gs://' |
| 75 | if not desired_profile_name.startswith(gs_prefix): |
| 76 | gs_url = os.path.join(GS_HTTP_URL, gs_url_base, desired_profile_name) |
| 77 | else: |
| 78 | gs_url = os.path.join(GS_HTTP_URL, desired_profile_name[len(gs_prefix):]) |
| 79 | |
| 80 | with contextlib.closing(urllib2.urlopen(gs_url)) as u: |
| 81 | with open(compressed_path, 'wb') as f: |
| 82 | while True: |
| 83 | buf = u.read(4096) |
| 84 | if not buf: |
| 85 | break |
| 86 | f.write(buf) |
| 87 | |
| 88 | if ext == '.bz2': |
| 89 | # NOTE: we can't use Python's bzip module, since it doesn't support |
| 90 | # multi-stream bzip files. It will silently succeed and give us a garbage |
| 91 | # profile. |
| 92 | # bzip2 removes the compressed file on success. |
| 93 | CheckCallOrExit(['bzip2', '-d', compressed_path]) |
| 94 | elif ext == '.xz': |
| 95 | # ...And we can't use the `lzma` module, since it was introduced in python3. |
| 96 | # xz removes the compressed file on success. |
| 97 | CheckCallOrExit(['xz', '-d', compressed_path]) |
| 98 | else: |
| 99 | # Wait until after downloading the file to check the file extension, so the |
| 100 | # user has something usable locally if the file extension is unrecognized. |
| 101 | raise ValueError( |
| 102 | 'Only bz2 and xz extensions are supported; "%s" is not' % ext) |
| 103 | |
| 104 | |
| 105 | def main(): |
| 106 | parser = argparse.ArgumentParser( |
| 107 | 'Downloads profile/orderfile provided by Chrome OS') |
| 108 | |
| 109 | parser.add_argument( |
| 110 | '--newest_state', |
| 111 | required=True, |
| 112 | help='Path to the file with name of the newest profile. ' |
| 113 | 'We use this file to track the name of the newest profile ' |
| 114 | 'we should pull' |
| 115 | ) |
| 116 | parser.add_argument( |
| 117 | '--local_state', |
| 118 | required=True, |
| 119 | help='Path of the file storing name of the local profile. ' |
| 120 | 'We use this file to track the most recent profile we\'ve ' |
| 121 | 'successfully pulled.' |
| 122 | ) |
| 123 | parser.add_argument( |
| 124 | '--gs_url_base', |
| 125 | required=True, |
| 126 | help='The base GS URL to search for the profile.' |
| 127 | ) |
| 128 | parser.add_argument( |
| 129 | '--output_name', |
| 130 | required=True, |
| 131 | help='Output name of the downloaded and uncompressed profile.' |
| 132 | ) |
| 133 | parser.add_argument( |
| 134 | '-f', '--force', |
| 135 | action='store_true', |
| 136 | help='Fetch a profile even if the local one is current' |
| 137 | ) |
| 138 | args = parser.parse_args() |
| 139 | |
| 140 | up_to_date_profile = ReadUpToDateProfileName(args.newest_state) |
| 141 | if not args.force: |
| 142 | local_profile_name = ReadLocalProfileName(args.local_state) |
| 143 | # In a perfect world, the local profile should always exist if we |
| 144 | # successfully read local_profile_name. If it's gone, though, the user |
| 145 | # probably removed it as a way to get us to download it again. |
| 146 | if local_profile_name == up_to_date_profile \ |
| 147 | and os.path.exists(args.output_name): |
| 148 | return 0 |
| 149 | |
| 150 | new_tmpfile = args.output_name + '.new' |
| 151 | RetrieveProfile(up_to_date_profile, new_tmpfile, args.gs_url_base) |
| 152 | os.rename(new_tmpfile, args.output_name) |
| 153 | WriteLocalProfileName(up_to_date_profile, args.local_state) |
| 154 | |
| 155 | |
| 156 | if __name__ == '__main__': |
| 157 | sys.exit(main()) |