2 # Copyright 2020 The Chromium Authors
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5 """Updates the Fuchsia images to the given revision. Should be used in a
6 'hooks_os' entry so that it only runs when .gclient's target_os includes
16 from typing import Dict, Optional
18 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),
21 from common import DIR_SRC_ROOT, IMAGES_ROOT, get_host_os, \
24 from gcs_download import DownloadAndUnpackFromCloudStorage
26 from update_sdk import GetSDKOverrideGCSPath
28 IMAGE_SIGNATURE_FILE = '.hash'
31 # TODO(crbug.com/1138433): Investigate whether we can deprecate
32 # use of sdk_bucket.txt.
33 def GetOverrideCloudStorageBucket():
34 """Read bucket entry from sdk_bucket.txt"""
35 return ReadFile('sdk-bucket.txt').strip()
38 def ReadFile(filename):
39 """Read a file in this directory."""
40 with open(os.path.join(os.path.dirname(__file__), filename), 'r') as f:
45 return lambda str_value: str_value
48 def VarLookup(local_scope):
49 return lambda var_name: local_scope['vars'][var_name]
52 def GetImageHashList(bucket):
53 """Read filename entries from sdk-hash-files.list (one per line), substitute
54 {platform} in each entry if present, and read from each filename."""
55 assert (get_host_os() == 'linux')
57 line.strip() for line in ReadFile('sdk-hash-files.list').replace(
58 '{platform}', 'linux_internal').splitlines()
60 image_hashes = [ReadFile(filename).strip() for filename in filenames]
64 def ParseDepsDict(deps_content):
67 'Str': StrExpansion(),
68 'Var': VarLookup(local_scope),
71 exec(deps_content, global_scope, local_scope)
75 def ParseDepsFile(filename):
76 with open(filename, 'rb') as f:
77 deps_content = f.read()
78 return ParseDepsDict(deps_content)
81 def GetImageHash(bucket):
82 """Gets the hash identifier of the newest generation of images."""
83 if bucket == 'fuchsia-sdk':
84 hashes = GetImageHashList(bucket)
86 deps_file = os.path.join(DIR_SRC_ROOT, 'DEPS')
87 return ParseDepsFile(deps_file)['vars']['fuchsia_version'].split(':')[1]
90 def GetImageSignature(image_hash, boot_images):
91 return 'gn:{image_hash}:{boot_images}:'.format(image_hash=image_hash,
92 boot_images=boot_images)
95 def GetAllImages(boot_image_names):
96 if not boot_image_names:
99 all_device_types = ['generic', 'qemu']
100 all_archs = ['x64', 'arm64']
102 images_to_download = set()
104 for boot_image in boot_image_names.split(','):
105 components = boot_image.split('.')
106 if len(components) != 2:
109 device_type, arch = components
110 device_images = all_device_types if device_type == '*' else [device_type]
111 arch_images = all_archs if arch == '*' else [arch]
112 images_to_download.update(itertools.product(device_images, arch_images))
113 return images_to_download
116 def DownloadBootImages(bucket, image_hash, boot_image_names, image_root_dir):
117 images_to_download = GetAllImages(boot_image_names)
118 for image_to_download in images_to_download:
119 device_type = image_to_download[0]
120 arch = image_to_download[1]
121 image_output_dir = os.path.join(image_root_dir, arch, device_type)
122 if os.path.exists(image_output_dir):
125 logging.info('Downloading Fuchsia boot images for %s.%s...', device_type,
128 # Legacy images use different naming conventions. See fxbug.dev/85552.
129 legacy_delimiter_device_types = ['qemu', 'generic']
130 if bucket == 'fuchsia-sdk' or \
131 device_type not in legacy_delimiter_device_types:
132 type_arch_connector = '.'
134 type_arch_connector = '-'
136 images_tarball_url = 'gs://{bucket}/development/{image_hash}/images/'\
137 '{device_type}{type_arch_connector}{arch}.tgz'.format(
138 bucket=bucket, image_hash=image_hash, device_type=device_type,
139 type_arch_connector=type_arch_connector, arch=arch)
141 DownloadAndUnpackFromCloudStorage(images_tarball_url, image_output_dir)
142 except subprocess.CalledProcessError as e:
143 logging.exception('Failed to download image %s from URL: %s',
144 image_to_download, images_tarball_url)
148 def _GetImageOverrideInfo() -> Optional[Dict[str, str]]:
149 """Get the bucket location from sdk_override.txt."""
150 location = GetSDKOverrideGCSPath()
154 m = re.match(r'gs://([^/]+)/development/([^/]+)/?(?:sdk)?', location)
156 raise ValueError('Badly formatted image override location %s' % location)
159 'bucket': m.group(1),
160 'image_hash': m.group(2),
164 def GetImageLocationInfo(default_bucket: str,
165 allow_override: bool = True) -> Dict[str, str]:
166 """Figures out where to pull the image from.
168 Defaults to the provided default bucket and generates the hash from defaults.
169 If sdk_override.txt exists (and is allowed) it uses that bucket instead.
172 default_bucket: a given default for what bucket to use
173 allow_override: allow SDK override to be used.
176 A dictionary containing the bucket and image_hash
178 # if sdk_override.txt exists (and is allowed) use the image from that bucket.
180 override = _GetImageOverrideInfo()
184 # Use the bucket in sdk-bucket.txt if an entry exists.
185 # Otherwise use the default bucket.
186 bucket = GetOverrideCloudStorageBucket() or default_bucket
189 'image_hash': GetImageHash(bucket),
194 parser = argparse.ArgumentParser()
195 parser.add_argument('--verbose',
198 help='Enable debug-level logging.')
203 help='List of boot images to download, represented as a comma separated '
204 'list. Wildcards are allowed. ')
209 help='The Google Cloud Storage bucket in which the Fuchsia images are '
210 'stored. Entry in sdk-bucket.txt will override this flag.')
214 help='Specify the root directory of the downloaded images. Optional')
218 help='Whether sdk_override.txt can be used for fetching the image, if '
220 args = parser.parse_args()
222 logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
224 # If no boot images need to be downloaded, exit.
225 if not args.boot_images:
228 # Check whether there's Fuchsia support for this platform.
230 image_info = GetImageLocationInfo(args.default_bucket, args.allow_override)
232 bucket = image_info['bucket']
233 image_hash = image_info['image_hash']
238 signature_filename = os.path.join(args.image_root_dir, IMAGE_SIGNATURE_FILE)
239 current_signature = (open(signature_filename, 'r').read().strip()
240 if os.path.exists(signature_filename) else '')
241 new_signature = GetImageSignature(image_hash, args.boot_images)
242 if current_signature != new_signature:
243 logging.info('Downloading Fuchsia images %s from bucket %s...', image_hash,
245 make_clean_directory(args.image_root_dir)
248 DownloadBootImages(bucket, image_hash, args.boot_images,
250 with open(signature_filename, 'w') as f:
251 f.write(new_signature)
252 except subprocess.CalledProcessError as e:
253 logging.exception("command '%s' failed with status %d.%s",
254 ' '.join(e.cmd), e.returncode,
255 ' Details: ' + e.output if e.output else '')
258 logging.info('Signatures matched! Got %s', new_signature)
263 if __name__ == '__main__':