|  | # Copyright 2020 The Fuchsia Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | import attr | 
|  |  | 
|  | from recipe_engine import recipe_api | 
|  | from recipe_engine.config_types import Path | 
|  |  | 
|  | SDK_GCS_BUCKET = 'fuchsia' | 
|  |  | 
|  |  | 
|  | @attr.s | 
|  | class ImageFilePaths(object): | 
|  | """Required files from fuchsia image to start FEMU.""" | 
|  |  | 
|  | # Recipe API, required | 
|  | _api = attr.ib(type=recipe_api.RecipeApi) | 
|  |  | 
|  | # Files from fuchsia image | 
|  | build_args = attr.ib(type=Path, default=None) | 
|  | kernel_file = attr.ib(type=Path, default=None) | 
|  | system_fvm = attr.ib(type=Path, default=None) | 
|  | zircona = attr.ib(type=Path, default=None) | 
|  |  | 
|  | def _exists(self, p): | 
|  | return p and self._api.path.exists(p) | 
|  |  | 
|  | def _exist(self): | 
|  | return all([ | 
|  | self._exists(self.build_args), | 
|  | self._exists(self.kernel_file), | 
|  | self._exists(self.system_fvm), | 
|  | self._exists(self.zircona), | 
|  | ]) | 
|  |  | 
|  | def _report_missing(self): | 
|  | result = [] | 
|  | if not self._exists(self.build_args): | 
|  | result.append(self.build_args) | 
|  | if not self._exists(self.kernel_file): | 
|  | result.append(self.kernel_file) | 
|  | if not self._exists(self.system_fvm): | 
|  | result.append(self.system_fvm) | 
|  | if not self._exists(self.zircona): | 
|  | result.append(self.zircona) | 
|  | return result | 
|  |  | 
|  |  | 
|  | @attr.s | 
|  | class PackageFilePaths(object): | 
|  | # Recipe API, required | 
|  | _api = attr.ib(type=recipe_api.RecipeApi, init=True) | 
|  |  | 
|  | # Files from fuchsia packages | 
|  | tar_file = attr.ib(type=Path, default=None) | 
|  | amber_files = attr.ib(type=Path, default=None) | 
|  | pm = attr.ib(type=Path, default=None) | 
|  |  | 
|  | def _exists(self, p): | 
|  | return p and self._api.path.exists(p) | 
|  |  | 
|  | def _exist(self): | 
|  | return all([ | 
|  | self._exists(self.tar_file), | 
|  | self._exists(self.amber_files), | 
|  | self._exists(self.pm), | 
|  | ]) | 
|  |  | 
|  | def _report_missing(self): | 
|  | result = [] | 
|  | if not self._exists(self.tar_file): | 
|  | result.append(self.tar_file) | 
|  | if not self._exists(self.amber_files): | 
|  | result.append(self.amber_files) | 
|  | if not self._exists(self.pm): | 
|  | result.append(self.pm) | 
|  | return result | 
|  |  | 
|  |  | 
|  | class SDKApi(recipe_api.RecipeApi): | 
|  | """Downloads Fuchsia SDK files required to start FEMU.""" | 
|  |  | 
|  | def __init__(self, *args, **kwargs): | 
|  | super(SDKApi, self).__init__(*args, **kwargs) | 
|  | self._image_paths = ImageFilePaths(api=self.m) | 
|  | self._package_paths = PackageFilePaths(api=self.m) | 
|  | self._sdk_path = None | 
|  | self._version = None | 
|  |  | 
|  | def _fetch_sdk(self): | 
|  | """Downloads Fuchsia SDK from GCS and untar.""" | 
|  | with self.m.step.nest('ensure sdk'): | 
|  | with self.m.context(infra_steps=True): | 
|  | # Ensure cache path has the correct permission | 
|  | cache_path = self.m.buildbucket.builder_cache_path.join( | 
|  | self.version, 'fuchsia_sdk', self.platform_name | 
|  | ) | 
|  | self.m.file.ensure_directory('init fuchsia_sdk cache', cache_path) | 
|  | if not self.m.file.listdir(name='check sdk cache content', | 
|  | source=cache_path, test_data=()): | 
|  | # Copy GN image to temp directory | 
|  | sdk_file = 'gn.tar.gz' | 
|  | local_tmp_root = self.m.path.mkdtemp('fuchsia_sdk_tmp') | 
|  | self.m.gsutil.download( | 
|  | src_bucket=SDK_GCS_BUCKET, | 
|  | src=self.m.path.join( | 
|  | 'development', | 
|  | self.version, | 
|  | 'sdk', | 
|  | self.sdk_platform_name, | 
|  | sdk_file, | 
|  | ), | 
|  | dest=local_tmp_root, | 
|  | ) | 
|  | # Extract sdk | 
|  | self.m.tar.extract( | 
|  | step_name='extract sdk gz', | 
|  | path=self.m.path.join(local_tmp_root, sdk_file), | 
|  | directory=cache_path, | 
|  | ) | 
|  |  | 
|  | # Save cache path | 
|  | self._sdk_path = cache_path | 
|  |  | 
|  | def _fetch_image(self): | 
|  | """Downloads Fuchsia image from GCS. Untar and store the required paths for FEMU.""" | 
|  | with self.m.step.nest('ensure image'): | 
|  | with self.m.context(infra_steps=True): | 
|  | image_to_download = self._select_image_to_download() | 
|  | # Ensure cache path has the correct permission | 
|  | cache_path = self.m.buildbucket.builder_cache_path.join( | 
|  | self.version, 'fuchsia_image', self.platform_name | 
|  | ) | 
|  | self.m.file.ensure_directory('init fuchsia_image cache', cache_path) | 
|  |  | 
|  | if not self.m.file.listdir(name='check image cache content', | 
|  | source=cache_path, test_data=()): | 
|  | # Copy image to temp directory | 
|  | local_tmp_path = self.m.path.mkdtemp('fuchsia_image_tmp') | 
|  | self.m.gsutil.download( | 
|  | src_bucket=SDK_GCS_BUCKET, | 
|  | src=self.m.path.join( | 
|  | 'development', self.version, 'images', image_to_download | 
|  | ), | 
|  | dest=local_tmp_path, | 
|  | ) | 
|  |  | 
|  | # Extract image | 
|  | self.m.tar.extract( | 
|  | step_name='extract image tgz', | 
|  | path=self.m.path.join(local_tmp_path, image_to_download), | 
|  | directory=cache_path, | 
|  | ) | 
|  | # Assemble files required for FEMU | 
|  | for p in self.m.file.listdir( | 
|  | name='set image files', | 
|  | source=cache_path, | 
|  | test_data=( | 
|  | 'buildargs.gn', | 
|  | 'qemu-kernel.kernel', | 
|  | 'zircon-a.zbi', | 
|  | 'zircon-r.zbi', | 
|  | 'storage-sparse.blk', | 
|  | 'storage-full.blk', | 
|  | ), | 
|  | ): | 
|  | base = self.m.path.basename(p) | 
|  | if base == 'buildargs.gn': | 
|  | self._image_paths.build_args = p | 
|  | elif base == 'qemu-kernel.kernel': | 
|  | self._image_paths.kernel_file = p | 
|  | elif base == 'storage-full.blk': | 
|  | self._image_paths.system_fvm = p | 
|  | elif base == 'zircon-a.zbi': | 
|  | self._image_paths.zircona = p | 
|  |  | 
|  | def _fetch_packages(self): | 
|  | with self.m.step.nest('ensure packages'): | 
|  | with self.m.context(infra_steps=True): | 
|  | package_to_download = self._select_package_to_download() | 
|  | # Ensure cache path has the correct permission | 
|  | cache_path = self.m.buildbucket.builder_cache_path.join( | 
|  | self.version, 'fuchsia_packages', self.platform_name | 
|  | ) | 
|  | self.m.file.ensure_directory('init fuchsia_packages cache', cache_path) | 
|  |  | 
|  | if not self.m.file.listdir(name='check packages cache content', | 
|  | source=cache_path, test_data=()): | 
|  | # Copy packages to cache directory. | 
|  | # TODO(yuanzhi) Copy to tmp. We need to keep this in cache because fuchsia_ctl | 
|  | # used by the flutter team expects packages as .tar.gz input. However, we should | 
|  | # use fuchsia device controller (FDC) that comes with VDL for FEMU based testing | 
|  | # eventually. | 
|  | self.m.gsutil.download( | 
|  | src_bucket=SDK_GCS_BUCKET, | 
|  | src=self.m.path.join( | 
|  | 'development', self.version, 'packages', package_to_download | 
|  | ), | 
|  | dest=cache_path, | 
|  | ) | 
|  |  | 
|  | # Extract package, this will produce the following subdirectories: | 
|  | # |cache_path| | 
|  | #   |__ amber-files | 
|  | #      |__ keys | 
|  | #      |__ repository | 
|  | #   |__ pm | 
|  | self.m.tar.extract( | 
|  | step_name='extract package tar.gz', | 
|  | path=self.m.path.join(cache_path, package_to_download), | 
|  | directory=cache_path, | 
|  | ) | 
|  | self._package_paths.tar_file = cache_path.join(package_to_download) | 
|  | self._package_paths.amber_files = cache_path.join('amber-files') | 
|  | self._package_paths.pm = cache_path.join('pm') | 
|  |  | 
|  | def authorize_zbi( | 
|  | self, | 
|  | ssh_key_path, | 
|  | zbi_input_path, | 
|  | zbi_output_path=None, | 
|  | zbi_tool_path=None | 
|  | ): | 
|  | """Use zbi tool to extend BootFS with SSH authorization key. | 
|  |  | 
|  | Arguments: | 
|  | ssh_key_path: path to public ssh key file. | 
|  | zbi_input_path: path to zircon-a.zbi file. | 
|  | zbi_output_path: (optional) output path to store extended zbi image file. | 
|  | if None, we will replace zircon file specified in zbi_input_path | 
|  | zbi_tool_path: (optional) path to the zbi binary tool. | 
|  | if None, we will fetch the zbi tool from fuchsia sdk in GCS. | 
|  | """ | 
|  | zbi_path = None | 
|  | if zbi_tool_path: | 
|  | zbi_path = zbi_tool_path | 
|  | if not zbi_path: | 
|  | zbi_path = self.m.path.join(self.sdk_path, 'tools', 'zbi') | 
|  | if not zbi_output_path: | 
|  | zbi_output_path = zbi_input_path | 
|  |  | 
|  | self.m.step( | 
|  | "authorize zbi", | 
|  | [ | 
|  | zbi_path, | 
|  | "--output", | 
|  | self.m.raw_io.output_text(leak_to=zbi_output_path), | 
|  | zbi_input_path, | 
|  | "--entry", | 
|  | "%s=%s" % ('data/ssh/authorized_keys', ssh_key_path), | 
|  | ], | 
|  | ) | 
|  |  | 
|  | def _select_package_to_download(self): | 
|  | """Maps platform parameters to Package names.""" | 
|  | return 'qemu-{arch}.tar.gz'.format(arch=self._select_arch()) | 
|  |  | 
|  | def _select_image_to_download(self): | 
|  | """Maps platform parameters to Image names.""" | 
|  | return 'qemu-{arch}.tgz'.format(arch=self._select_arch()) | 
|  |  | 
|  | def _select_arch(self): | 
|  | """Maps platform parameters to SDK names.""" | 
|  | if self.m.platform.arch == 'arm' and self.m.platform.bits == 64: | 
|  | return 'arm64' | 
|  | elif self.m.platform.arch == 'intel' and self.m.platform.bits == 64: | 
|  | return 'x64' | 
|  | raise self.m.step.StepFailure( | 
|  | 'Cannot find supported tools. arch %s, bit %s' % | 
|  | (self.m.platform.arch, self.m.platform.bits) | 
|  | ) | 
|  |  | 
|  | @property | 
|  | def sdk_platform_name(self): | 
|  | """Derives the sdk package platform name, resembles the CIPD platform name.""" | 
|  | name = '' | 
|  | if self.m.platform.is_linux: | 
|  | name = 'linux-amd64' | 
|  | elif self.m.platform.is_mac: | 
|  | name = 'mac-amd64' | 
|  | return name | 
|  |  | 
|  | @property | 
|  | def platform_name(self): | 
|  | return '%s_%s_%s' % ( | 
|  | self.m.platform.name, | 
|  | self.m.platform.arch, | 
|  | self.m.platform.bits, | 
|  | ) | 
|  |  | 
|  | @property | 
|  | def version(self): | 
|  | return self._version | 
|  |  | 
|  | @version.setter | 
|  | def version(self, value): | 
|  | self._version = value | 
|  |  | 
|  | @property | 
|  | def image_paths(self): | 
|  | """Downloads and unpacks Fuchsia image files from GCS. | 
|  |  | 
|  | Raises: | 
|  | StepFailure: When cannot find image files matching host architecture. | 
|  | StepFailure: When image files do not exist after download and unpack from GCS. | 
|  | """ | 
|  | assert self.version | 
|  | self._fetch_image() | 
|  | if not self._image_paths._exist(): | 
|  | missing = self._image_paths._report_missing() | 
|  | ex = self.m.step.StepFailure( | 
|  | 'Image paths do not exist. {missing_paths}'.format( | 
|  | missing_paths=missing | 
|  | ) | 
|  | ) | 
|  | ex.missing_paths = missing | 
|  | raise ex | 
|  | return self._image_paths | 
|  |  | 
|  | @property | 
|  | def package_paths(self): | 
|  | """Downloads and unpacks Fuchsia package files from GCS. | 
|  |  | 
|  | Raises: | 
|  | StepFailure: When cannot find package files matching host architecture. | 
|  | StepFailure: When package files do not exist after download and unpack from GCS. | 
|  | """ | 
|  | assert self.version | 
|  | self._fetch_packages() | 
|  | if not self._package_paths._exist(): | 
|  | missing = self._package_paths._report_missing() | 
|  | ex = self.m.step.StepFailure( | 
|  | 'Package paths do not exist. {missing_paths}'.format( | 
|  | missing_paths=missing | 
|  | ) | 
|  | ) | 
|  | ex.missing_paths = missing | 
|  | raise ex | 
|  | return self._package_paths | 
|  |  | 
|  | @property | 
|  | def sdk_path(self): | 
|  | """Downloads and unpacks Fuchsia sdk files from GCS. | 
|  |  | 
|  | Raises: | 
|  | StepFailure: When cannot find sdk files matching host architecture. | 
|  | """ | 
|  | assert self.version | 
|  | self._fetch_sdk() | 
|  | return self._sdk_path |