1#!/usr/bin/env python 2# 3# Copyright 2018 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16"""Common code used by acloud create methods/classes.""" 17 18import collections 19import logging 20import os 21import re 22import shutil 23 24from acloud import errors 25from acloud.internal import constants 26from acloud.internal.lib import android_build_client 27from acloud.internal.lib import auth 28from acloud.internal.lib import utils 29 30 31logger = logging.getLogger(__name__) 32 33# The boot image name pattern supports the following cases: 34# - Cuttlefish ANDROID_PRODUCT_OUT directory conatins boot.img. 35# - In Android 12, the officially released GKI (Generic Kernel Image) name is 36# boot-<kernel version>.img. 37# - In Android 13, the name is boot.img. 38_BOOT_IMAGE_NAME_PATTERN = r"boot(-[\d.]+)?\.img" 39_SYSTEM_IMAGE_NAME_PATTERN = r"system\.img" 40 41_ANDROID_BOOT_IMAGE_MAGIC = b"ANDROID!" 42 43# Store the file path to upload to the remote instance. 44ExtraFile = collections.namedtuple("ExtraFile", ["source", "target"]) 45 46 47def ParseExtraFilesArgs(files_info, path_separator=","): 48 """Parse extra-files argument. 49 50 e.g. 51 ["local_path,gce_path"] 52 -> ExtraFile(source='local_path', target='gce_path') 53 54 Args: 55 files_info: List of strings to be converted to namedtuple ExtraFile. 56 item_separator: String character to separate file info. 57 58 Returns: 59 A list of namedtuple ExtraFile. 60 61 Raises: 62 error.MalformedDictStringError: If files_info is malformed. 63 """ 64 extra_files = [] 65 if files_info: 66 for file_info in files_info: 67 if path_separator not in file_info: 68 raise errors.MalformedDictStringError( 69 "Expecting '%s' in '%s'." % (path_separator, file_info)) 70 source, target = file_info.split(path_separator) 71 extra_files.append(ExtraFile(source, target)) 72 return extra_files 73 74 75def ParseKeyValuePairArgs(dict_str, item_separator=",", key_value_separator=":"): 76 """Helper function to initialize a dict object from string. 77 78 e.g. 79 cpu:2,dpi:240,resolution:1280x800 80 -> {"cpu":"2", "dpi":"240", "resolution":"1280x800"} 81 82 Args: 83 dict_str: A String to be converted to dict object. 84 item_separator: String character to separate items. 85 key_value_separator: String character to separate key and value. 86 87 Returns: 88 Dict created from key:val pairs in dict_str. 89 90 Raises: 91 error.MalformedDictStringError: If dict_str is malformed. 92 """ 93 args_dict = {} 94 if not dict_str: 95 return args_dict 96 97 for item in dict_str.split(item_separator): 98 if key_value_separator not in item: 99 raise errors.MalformedDictStringError( 100 "Expecting ':' in '%s' to make a key-val pair" % item) 101 key, value = item.split(key_value_separator) 102 if not value or not key: 103 raise errors.MalformedDictStringError( 104 "Missing key or value in %s, expecting form of 'a:b'" % item) 105 args_dict[key.strip()] = value.strip() 106 107 return args_dict 108 109 110def GetNonEmptyEnvVars(*variable_names): 111 """Get non-empty environment variables. 112 113 Args: 114 variable_names: Strings, the variable names. 115 116 Returns: 117 List of strings, the variable values that are defined and not empty. 118 """ 119 return list(filter(None, (os.environ.get(v) for v in variable_names))) 120 121 122def GetCvdHostPackage(package_path=None): 123 """Get cvd host package path. 124 125 Look for the host package in specified path or $ANDROID_HOST_OUT and dist 126 dir then verify existence and get cvd host package path. 127 128 Args: 129 package_path: String of cvd host package path. 130 131 Return: 132 A string, the path to the host package. 133 134 Raises: 135 errors.GetCvdLocalHostPackageError: Can't find cvd host package. 136 """ 137 if package_path: 138 if os.path.exists(package_path): 139 return package_path 140 raise errors.GetCvdLocalHostPackageError( 141 "The cvd host package path (%s) doesn't exist." % package_path) 142 dirs_to_check = GetNonEmptyEnvVars(constants.ENV_ANDROID_SOONG_HOST_OUT, 143 constants.ENV_ANDROID_HOST_OUT) 144 dist_dir = utils.GetDistDir() 145 if dist_dir: 146 dirs_to_check.append(dist_dir) 147 148 for path in dirs_to_check: 149 for name in [constants.CVD_HOST_TARBALL, constants.CVD_HOST_PACKAGE]: 150 cvd_host_package = os.path.join(path, name) 151 if os.path.exists(cvd_host_package): 152 logger.debug("cvd host package: %s", cvd_host_package) 153 return cvd_host_package 154 raise errors.GetCvdLocalHostPackageError( 155 "Can't find the cvd host package (Try lunching a cuttlefish target" 156 " like aosp_cf_x86_64_phone-userdebug and running 'm'): \n%s" % 157 '\n'.join(dirs_to_check)) 158 159 160def FindLocalImage(path, default_name_pattern, raise_error=True): 161 """Find an image file in the given path. 162 163 Args: 164 path: The path to the file or the parent directory. 165 default_name_pattern: A regex string, the file to look for if the path 166 is a directory. 167 168 Returns: 169 The absolute path to the image file. 170 171 Raises: 172 errors.GetLocalImageError if this method cannot find exactly one image. 173 """ 174 path = os.path.abspath(path) 175 if os.path.isdir(path): 176 names = [name for name in os.listdir(path) if 177 re.fullmatch(default_name_pattern, name)] 178 if not names: 179 if raise_error: 180 raise errors.GetLocalImageError(f"No image in {path}.") 181 return None 182 if len(names) != 1: 183 raise errors.GetLocalImageError( 184 f"More than one image in {path}: {' '.join(names)}") 185 path = os.path.join(path, names[0]) 186 if os.path.isfile(path): 187 return path 188 raise errors.GetLocalImageError(f"{path} is not a file.") 189 190 191def _IsBootImage(image_path): 192 """Check if a file is an Android boot image by reading the magic bytes. 193 194 Args: 195 image_path: The file path. 196 197 Returns: 198 A boolean, whether the file is a boot image. 199 """ 200 if not os.path.isfile(image_path): 201 return False 202 with open(image_path, "rb") as image_file: 203 return image_file.read(8) == _ANDROID_BOOT_IMAGE_MAGIC 204 205 206def FindBootImage(path, raise_error=True): 207 """Find a boot image file in the given path.""" 208 boot_image_path = FindLocalImage(path, _BOOT_IMAGE_NAME_PATTERN, 209 raise_error) 210 if boot_image_path and not _IsBootImage(boot_image_path): 211 raise errors.GetLocalImageError( 212 f"{boot_image_path} is not a boot image.") 213 return boot_image_path 214 215 216def FindSystemImage(path): 217 """Find a system image file in a given path.""" 218 return FindLocalImage(path, _SYSTEM_IMAGE_NAME_PATTERN, raise_error=True) 219 220 221def DownloadRemoteArtifact(cfg, build_target, build_id, artifact, extract_path, 222 decompress=False): 223 """Download remote artifact. 224 225 Args: 226 cfg: An AcloudConfig instance. 227 build_target: String, the build target, e.g. cf_x86_phone-userdebug. 228 build_id: String, Build id, e.g. "2263051", "P2804227" 229 artifact: String, zip image or cvd host package artifact. 230 extract_path: String, a path include extracted files. 231 decompress: Boolean, if true decompress the artifact. 232 """ 233 build_client = android_build_client.AndroidBuildClient( 234 auth.CreateCredentials(cfg)) 235 temp_file = os.path.join(extract_path, artifact) 236 build_client.DownloadArtifact( 237 build_target, 238 build_id, 239 artifact, 240 temp_file) 241 if decompress: 242 utils.Decompress(temp_file, extract_path) 243 try: 244 os.remove(temp_file) 245 logger.debug("Deleted temporary file %s", temp_file) 246 except OSError as e: 247 logger.error("Failed to delete temporary file: %s", str(e)) 248 249 250def PrepareLocalInstanceDir(instance_dir, avd_spec): 251 """Create a directory for a local cuttlefish or goldfish instance. 252 253 If avd_spec has the local instance directory, this method creates a 254 symbolic link from instance_dir to the directory. Otherwise, it creates an 255 empty directory at instance_dir. 256 257 Args: 258 instance_dir: The absolute path to the default instance directory. 259 avd_spec: AVDSpec object that provides the instance directory. 260 """ 261 if os.path.islink(instance_dir): 262 os.remove(instance_dir) 263 else: 264 shutil.rmtree(instance_dir, ignore_errors=True) 265 266 if avd_spec.local_instance_dir: 267 abs_instance_dir = os.path.abspath(avd_spec.local_instance_dir) 268 if instance_dir != abs_instance_dir: 269 os.symlink(abs_instance_dir, instance_dir) 270 return 271 if not os.path.exists(instance_dir): 272 os.makedirs(instance_dir) 273