blob: 876ecac8d227c25bd39cf4f7c6623be72a3e174d [file] [log] [blame]
Josip Sokcevicfb12b3f2021-04-19 18:09:50 +00001#!/usr/bin/env python3
hinoka@chromium.org7a790542014-12-10 02:04:39 +00002# Copyright 2014 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"""Run a pinned gsutil."""
7
8
9import argparse
hinoka@chromium.org7a790542014-12-10 02:04:39 +000010import base64
dnj@chromium.org605d81d2015-09-18 22:33:53 +000011import contextlib
primiano@chromium.orgdf351762014-12-18 11:12:34 +000012import hashlib
hinoka@chromium.org7a790542014-12-10 02:04:39 +000013import json
primiano@chromium.orgdf351762014-12-18 11:12:34 +000014import os
15import shutil
hinoka@chromium.org7a790542014-12-10 02:04:39 +000016import subprocess
primiano@chromium.orgdf351762014-12-18 11:12:34 +000017import sys
dnj@chromium.org605d81d2015-09-18 22:33:53 +000018import tempfile
19import time
Raul Tambreb946b232019-03-26 14:48:46 +000020
21try:
22 import urllib2 as urllib
23except ImportError: # For Py3 compatibility
24 import urllib.request as urllib
25
primiano@chromium.orgdf351762014-12-18 11:12:34 +000026import zipfile
hinoka@chromium.org7a790542014-12-10 02:04:39 +000027
28
29GSUTIL_URL = 'https://storage.googleapis.com/pub/'
30API_URL = 'https://www.googleapis.com/storage/v1/b/pub/o/'
31
32THIS_DIR = os.path.dirname(os.path.abspath(__file__))
33DEFAULT_BIN_DIR = os.path.join(THIS_DIR, 'external_bin', 'gsutil')
hinoka@chromium.org7a790542014-12-10 02:04:39 +000034
Dan Jacques509776e2017-09-07 18:01:08 -070035IS_WINDOWS = os.name == 'nt'
36
Josip Sokcevic19096962022-03-10 17:56:09 +000037VERSION = '4.68'
Josip Sokcevicfa474e82021-09-17 16:59:49 +000038
Dan Jacques509776e2017-09-07 18:01:08 -070039
hinoka@chromium.org7a790542014-12-10 02:04:39 +000040class InvalidGsutilError(Exception):
41 pass
42
43
hinoka@chromium.org7a790542014-12-10 02:04:39 +000044def download_gsutil(version, target_dir):
45 """Downloads gsutil into the target_dir."""
46 filename = 'gsutil_%s.zip' % version
47 target_filename = os.path.join(target_dir, filename)
48
49 # Check if the target exists already.
50 if os.path.exists(target_filename):
51 md5_calc = hashlib.md5()
52 with open(target_filename, 'rb') as f:
53 while True:
54 buf = f.read(4096)
55 if not buf:
56 break
57 md5_calc.update(buf)
58 local_md5 = md5_calc.hexdigest()
59
60 metadata_url = '%s%s' % (API_URL, filename)
Raul Tambreb946b232019-03-26 14:48:46 +000061 metadata = json.load(urllib.urlopen(metadata_url))
Edward Lemur83aafc92019-11-25 23:25:05 +000062 remote_md5 = base64.b64decode(metadata['md5Hash']).decode('utf-8')
hinoka@chromium.org7a790542014-12-10 02:04:39 +000063
64 if local_md5 == remote_md5:
65 return target_filename
66 os.remove(target_filename)
67
68 # Do the download.
69 url = '%s%s' % (GSUTIL_URL, filename)
Raul Tambreb946b232019-03-26 14:48:46 +000070 u = urllib.urlopen(url)
hinoka@chromium.org7a790542014-12-10 02:04:39 +000071 with open(target_filename, 'wb') as f:
72 while True:
73 buf = u.read(4096)
74 if not buf:
75 break
76 f.write(buf)
77 return target_filename
78
79
dnj@chromium.org605d81d2015-09-18 22:33:53 +000080@contextlib.contextmanager
81def temporary_directory(base):
Takuto Ikuta8daf2442021-10-28 05:51:18 +000082 tmpdir = tempfile.mkdtemp(prefix='t', dir=base)
dnj@chromium.org605d81d2015-09-18 22:33:53 +000083 try:
84 yield tmpdir
85 finally:
86 if os.path.isdir(tmpdir):
87 shutil.rmtree(tmpdir)
88
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000089
dnj@chromium.org605d81d2015-09-18 22:33:53 +000090def ensure_gsutil(version, target, clean):
hinoka@chromium.org7a790542014-12-10 02:04:39 +000091 bin_dir = os.path.join(target, 'gsutil_%s' % version)
92 gsutil_bin = os.path.join(bin_dir, 'gsutil', 'gsutil')
Ryan Tseng83fd81f2017-10-23 11:13:48 -070093 gsutil_flag = os.path.join(bin_dir, 'gsutil', 'install.flag')
94 # We assume that if gsutil_flag exists, then we have a good version
95 # of the gsutil package.
96 if not clean and os.path.isfile(gsutil_flag):
hinoka@chromium.org7a790542014-12-10 02:04:39 +000097 # Everything is awesome! we're all done here.
98 return gsutil_bin
99
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000100 if not os.path.exists(target):
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000101 try:
102 os.makedirs(target)
103 except FileExistsError:
104 # Another process is prepping workspace, so let's check if gsutil_bin is
105 # present. If after several checks it's still not, continue with
106 # downloading gsutil.
107 delay = 2 # base delay, in seconds
108 for _ in range(3): # make N attempts
109 # sleep first as it's not expected to have file ready just yet.
110 time.sleep(delay)
111 delay *= 1.5 # next delay increased by that factor
112 if os.path.isfile(gsutil_bin):
113 return gsutil_bin
114
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000115 with temporary_directory(target) as instance_dir:
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000116 # Clean up if we're redownloading a corrupted gsutil.
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000117 cleanup_path = os.path.join(instance_dir, 'clean')
118 try:
119 os.rename(bin_dir, cleanup_path)
120 except (OSError, IOError):
121 cleanup_path = None
122 if cleanup_path:
123 shutil.rmtree(cleanup_path)
124
Takuto Ikuta8daf2442021-10-28 05:51:18 +0000125 download_dir = os.path.join(instance_dir, 'd')
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000126 target_zip_filename = download_gsutil(version, instance_dir)
127 with zipfile.ZipFile(target_zip_filename, 'r') as target_zip:
128 target_zip.extractall(download_dir)
129
130 try:
131 os.rename(download_dir, bin_dir)
132 except (OSError, IOError):
133 # Something else did this in parallel.
134 pass
Ryan Tseng83fd81f2017-10-23 11:13:48 -0700135 # Final check that the gsutil bin exists. This should never fail.
136 if not os.path.isfile(gsutil_bin):
137 raise InvalidGsutilError()
138 # Drop a flag file.
139 with open(gsutil_flag, 'w') as f:
140 f.write('This flag file is dropped by gsutil.py')
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000141
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000142 return gsutil_bin
143
144
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000145def run_gsutil(target, args, clean=False):
146 gsutil_bin = ensure_gsutil(VERSION, target, clean)
147 args_opt = ['-o', 'GSUtil:software_update_check_period=0']
Dan Jacques509776e2017-09-07 18:01:08 -0700148
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000149 if sys.platform == 'darwin':
150 # We are experiencing problems with multiprocessing on MacOS where gsutil.py
151 # may hang.
152 # This behavior is documented in gsutil codebase, and recommendation is to
153 # set GSUtil:parallel_process_count=1.
154 # https://github.com/GoogleCloudPlatform/gsutil/blob/06efc9dc23719fab4fd5fadb506d252bbd3fe0dd/gslib/command.py#L1331
155 # https://github.com/GoogleCloudPlatform/gsutil/issues/1100
156 args_opt.extend(['-o', 'GSUtil:parallel_process_count=1'])
Chris Nardiab816ce2017-10-31 15:45:05 -0400157 if sys.platform == 'cygwin':
158 # This script requires Windows Python, so invoke with depot_tools'
159 # Python.
160 def winpath(path):
Edward Lesmes94d6f482019-11-04 20:55:09 +0000161 stdout = subprocess.check_output(['cygpath', '-w', path])
162 return stdout.strip().decode('utf-8', 'replace')
Chris Nardiab816ce2017-10-31 15:45:05 -0400163 cmd = ['python.bat', winpath(__file__)]
164 cmd.extend(args)
165 sys.exit(subprocess.call(cmd))
166 assert sys.platform != 'cygwin'
167
Dan Jacques509776e2017-09-07 18:01:08 -0700168 cmd = [
Josip Sokcevic19096962022-03-10 17:56:09 +0000169 'vpython3',
170 '-vpython-spec', os.path.join(THIS_DIR, 'gsutil.vpython3'),
Gavin Mak37db69d2022-03-10 00:54:39 +0000171 '--',
Dan Jacques509776e2017-09-07 18:01:08 -0700172 gsutil_bin
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000173 ] + args_opt + args
Dan Jacques509776e2017-09-07 18:01:08 -0700174 return subprocess.call(cmd, shell=IS_WINDOWS)
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000175
176
177def parse_args():
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000178 bin_dir = os.environ.get('DEPOT_TOOLS_GSUTIL_BIN_DIR', DEFAULT_BIN_DIR)
179
Josip Sokcevicc1fd44b2021-09-20 22:31:37 +0000180 # Help is disabled as it conflicts with gsutil -h, which controls headers.
181 parser = argparse.ArgumentParser(add_help=False)
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000182
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000183 parser.add_argument('--clean', action='store_true',
184 help='Clear any existing gsutil package, forcing a new download.')
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000185 parser.add_argument('--target', default=bin_dir,
186 help='The target directory to download/store a gsutil version in. '
187 '(default is %(default)s).')
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000188
189 # These two args exist for backwards-compatibility but are no-ops.
190 parser.add_argument('--force-version', default=VERSION,
191 help='(deprecated, this flag has no effect)')
192 parser.add_argument('--fallback',
193 help='(deprecated, this flag has no effect)')
194
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000195 parser.add_argument('args', nargs=argparse.REMAINDER)
196
hinoka@chromium.orgc13b0542014-12-18 01:06:20 +0000197 args, extras = parser.parse_known_args()
198 if args.args and args.args[0] == '--':
199 args.args.pop(0)
200 if extras:
201 args.args = extras + args.args
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000202 return args
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000203
204
205def main():
dnj@chromium.org605d81d2015-09-18 22:33:53 +0000206 args = parse_args()
Josip Sokcevicfa474e82021-09-17 16:59:49 +0000207 return run_gsutil(args.target, args.args, clean=args.clean)
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000208
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000209
hinoka@chromium.org7a790542014-12-10 02:04:39 +0000210if __name__ == '__main__':
211 sys.exit(main())