208 lines
7.0 KiB
Python
208 lines
7.0 KiB
Python
# Copyright 2022 The Chromium Authors
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
"""Functions used in both v1 and v2 scripts."""
|
|
|
|
import os
|
|
import platform
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
|
|
from typing import Iterable, List, Optional, Tuple
|
|
|
|
|
|
# File indicating version of an image downloaded to the host
|
|
_BUILD_ARGS = "buildargs.gn"
|
|
_ARGS_FILE = 'args.gn'
|
|
|
|
_FILTER_DIR = 'testing/buildbot/filters'
|
|
_SSH_KEYS = os.path.expanduser('~/.ssh/fuchsia_authorized_keys')
|
|
|
|
|
|
class VersionNotFoundError(Exception):
|
|
"""Thrown when version info cannot be retrieved from device."""
|
|
|
|
|
|
def get_ssh_keys() -> str:
|
|
"""Returns path of Fuchsia ssh keys."""
|
|
|
|
return _SSH_KEYS
|
|
|
|
|
|
def running_unattended() -> bool:
|
|
"""Returns true if running non-interactively.
|
|
|
|
When running unattended, confirmation prompts and the like are suppressed.
|
|
"""
|
|
|
|
# TODO(crbug/1401387): Change to mixin based approach.
|
|
return 'SWARMING_SERVER' in os.environ
|
|
|
|
|
|
def get_host_arch() -> str:
|
|
"""Retrieve CPU architecture of the host machine. """
|
|
host_arch = platform.machine()
|
|
# platform.machine() returns AMD64 on 64-bit Windows.
|
|
if host_arch in ['x86_64', 'AMD64']:
|
|
return 'x64'
|
|
if host_arch in ['aarch64', 'arm64']:
|
|
return 'arm64'
|
|
raise NotImplementedError('Unsupported host architecture: %s' % host_arch)
|
|
|
|
|
|
def add_exec_to_file(file: str) -> None:
|
|
"""Add execution bits to a file.
|
|
|
|
Args:
|
|
file: path to the file.
|
|
"""
|
|
file_stat = os.stat(file)
|
|
os.chmod(file, file_stat.st_mode | stat.S_IXUSR)
|
|
|
|
|
|
def _add_exec_to_pave_binaries(system_image_dir: str):
|
|
"""Add exec to required pave files.
|
|
|
|
The pave files may vary depending if a product-bundle or a prebuilt images
|
|
directory is being used.
|
|
Args:
|
|
system_image_dir: string path to the directory containing the pave files.
|
|
"""
|
|
pb_files = [
|
|
'pave.sh',
|
|
os.path.join(f'host_{get_host_arch()}', 'bootserver')
|
|
]
|
|
image_files = [
|
|
'pave.sh',
|
|
os.path.join(f'bootserver.exe.linux-{get_host_arch()}')
|
|
]
|
|
use_pb_files = os.path.exists(os.path.join(system_image_dir, pb_files[1]))
|
|
for f in pb_files if use_pb_files else image_files:
|
|
add_exec_to_file(os.path.join(system_image_dir, f))
|
|
|
|
|
|
def pave(image_dir: str, target_id: Optional[str])\
|
|
-> subprocess.CompletedProcess:
|
|
""""Pave a device using the pave script inside |image_dir|."""
|
|
_add_exec_to_pave_binaries(image_dir)
|
|
pave_command = [
|
|
os.path.join(image_dir, 'pave.sh'), '--authorized-keys',
|
|
get_ssh_keys(), '-1'
|
|
]
|
|
if target_id:
|
|
pave_command.extend(['-n', target_id])
|
|
return subprocess.run(pave_command, check=True, text=True, timeout=300)
|
|
|
|
|
|
def parse_host_port(host_port_pair: str) -> Tuple[str, int]:
|
|
"""Parses a host name or IP address and a port number from a string of
|
|
any of the following forms:
|
|
- hostname:port
|
|
- IPv4addy:port
|
|
- [IPv6addy]:port
|
|
|
|
Returns:
|
|
A tuple of the string host name/address and integer port number.
|
|
|
|
Raises:
|
|
ValueError if `host_port_pair` does not contain a colon or if the
|
|
substring following the last colon cannot be converted to an int.
|
|
"""
|
|
|
|
host, port = host_port_pair.rsplit(':', 1)
|
|
|
|
# Strip the brackets if the host looks like an IPv6 address.
|
|
if len(host) >= 4 and host[0] == '[' and host[-1] == ']':
|
|
host = host[1:-1]
|
|
return (host, int(port))
|
|
|
|
|
|
def get_ssh_prefix(host_port_pair: str) -> List[str]:
|
|
"""Get the prefix of a barebone ssh command."""
|
|
|
|
ssh_addr, ssh_port = parse_host_port(host_port_pair)
|
|
return [
|
|
'ssh', '-F',
|
|
os.path.expanduser('~/.fuchsia/sshconfig'), ssh_addr, '-p',
|
|
str(ssh_port)
|
|
]
|
|
|
|
|
|
def install_symbols(package_paths: Iterable[str],
|
|
fuchsia_out_dir: str) -> None:
|
|
"""Installs debug symbols for a package into the GDB-standard symbol
|
|
directory located in fuchsia_out_dir."""
|
|
|
|
symbol_root = os.path.join(fuchsia_out_dir, '.build-id')
|
|
for path in package_paths:
|
|
package_dir = os.path.dirname(path)
|
|
ids_txt_path = os.path.join(package_dir, 'ids.txt')
|
|
with open(ids_txt_path, 'r') as f:
|
|
for entry in f:
|
|
build_id, binary_relpath = entry.strip().split(' ')
|
|
binary_abspath = os.path.abspath(
|
|
os.path.join(package_dir, binary_relpath))
|
|
symbol_dir = os.path.join(symbol_root, build_id[:2])
|
|
symbol_file = os.path.join(symbol_dir, build_id[2:] + '.debug')
|
|
if not os.path.exists(symbol_dir):
|
|
os.makedirs(symbol_dir)
|
|
|
|
if os.path.islink(symbol_file) or os.path.exists(symbol_file):
|
|
# Clobber the existing entry to ensure that the symlink's
|
|
# target is up to date.
|
|
os.unlink(symbol_file)
|
|
os.symlink(os.path.relpath(binary_abspath, symbol_dir),
|
|
symbol_file)
|
|
|
|
|
|
# TODO(crbug.com/1279803): Until one can send files to the device when running
|
|
# a test, filter files must be read from the test package.
|
|
def map_filter_file_to_package_file(filter_file: str) -> str:
|
|
"""Returns the path to |filter_file| within the test component's package."""
|
|
|
|
if not _FILTER_DIR in filter_file:
|
|
raise ValueError('CFv2 tests only support registered filter files '
|
|
'present in the test package')
|
|
return '/pkg/' + filter_file[filter_file.index(_FILTER_DIR):]
|
|
|
|
|
|
def get_sdk_hash(system_image_dir: str) -> Tuple[str, str]:
|
|
"""Read version of hash in pre-installed package directory.
|
|
Returns:
|
|
Tuple of (product, version) of image to be installed.
|
|
Raises:
|
|
VersionNotFoundError: if contents of buildargs.gn cannot be found or the
|
|
version number cannot be extracted.
|
|
"""
|
|
|
|
# TODO(crbug.com/1261961): Stop processing buildargs.gn directly.
|
|
args_file = os.path.join(system_image_dir, _BUILD_ARGS)
|
|
if not os.path.exists(args_file):
|
|
args_file = os.path.join(system_image_dir, _ARGS_FILE)
|
|
|
|
if not os.path.exists(args_file):
|
|
raise VersionNotFoundError(
|
|
f'Dir {system_image_dir} did not contain {_BUILD_ARGS} or '
|
|
f'{_ARGS_FILE}')
|
|
|
|
with open(args_file) as f:
|
|
contents = f.readlines()
|
|
if not contents:
|
|
raise VersionNotFoundError('Could not retrieve %s' % args_file)
|
|
version_key = 'build_info_version'
|
|
product_key = 'build_info_product'
|
|
info_keys = [product_key, version_key]
|
|
version_info = {}
|
|
for line in contents:
|
|
for key in info_keys:
|
|
match = re.match(r'%s = "(.*)"' % key, line)
|
|
if match:
|
|
version_info[key] = match.group(1)
|
|
if not (version_key in version_info and product_key in version_info):
|
|
raise VersionNotFoundError(
|
|
'Could not extract version info from %s. Contents: %s' %
|
|
(args_file, contents))
|
|
|
|
return (version_info[product_key], version_info[version_key])
|