| # 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', 'x64', '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 |