blob: 24aa192b6ea5538d966c8b28e0049ca7d8da2f6a [file] [log] [blame]
Tiancong Wang6cfc1632019-07-25 21:32:371#!/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
8This uses profiles of Chrome, or orderfiles for linking, provided by our
9friends from Chrome OS. Though the profiles are available externally,
10the bucket they sit in is otherwise unreadable by non-Googlers. Gsutil
11usage with this bucket is therefore quite awkward: you can't do anything
12but `cp` certain files with an external account, and you can't even do
13that if you're not yet authenticated.
14
15No authentication is necessary if you pull these profiles directly over
16https.
17"""
18
Raul Tambref3d9412e2019-09-24 05:31:4419from __future__ import print_function
20
Tiancong Wang6cfc1632019-07-25 21:32:3721import argparse
22import contextlib
23import os
24import subprocess
25import sys
26import urllib2
27
28GS_HTTP_URL = 'https://storage.googleapis.com'
29
30
31def ReadUpToDateProfileName(newest_profile_name_path):
32 with open(newest_profile_name_path) as f:
33 return f.read().strip()
34
35
36def 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
47def WriteLocalProfileName(name, local_profile_name_path):
48 with open(local_profile_name_path, 'w') as f:
49 f.write(name)
50
51
52def 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 Tambref3d9412e2019-09-24 05:31:4465 print('\n'.join(complaint_lines), file=sys.stderr)
Tiancong Wang6cfc1632019-07-25 21:32:3766 sys.exit(1)
67
68
69def 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
105def 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
156if __name__ == '__main__':
157 sys.exit(main())