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_TARGET_FILES_IMAGES_DIR_NAME = "IMAGES" 40_SYSTEM_IMAGE_NAME = "system.img" 41_SYSTEM_EXT_IMAGE_NAME = "system_ext.img" 42_PRODUCT_IMAGE_NAME = "product.img" 43_VENDOR_BOOT_IMAGE_NAME_PATTERN = r"vendor_boot\.img" 44 45_ANDROID_BOOT_IMAGE_MAGIC = b"ANDROID!" 46 47# Store the file path to upload to the remote instance. 48ExtraFile = collections.namedtuple("ExtraFile", ["source", "target"]) 49 50SystemImagePaths = collections.namedtuple( 51 "SystemImagePaths", 52 ["system", "system_ext", "product"]) 53 54 55def ParseExtraFilesArgs(files_info, path_separator=","): 56 """Parse extra-files argument. 57 58 e.g. 59 ["local_path,gce_path"] 60 -> ExtraFile(source='local_path', target='gce_path') 61 62 Args: 63 files_info: List of strings to be converted to namedtuple ExtraFile. 64 item_separator: String character to separate file info. 65 66 Returns: 67 A list of namedtuple ExtraFile. 68 69 Raises: 70 error.MalformedDictStringError: If files_info is malformed. 71 """ 72 extra_files = [] 73 if files_info: 74 for file_info in files_info: 75 if path_separator not in file_info: 76 raise errors.MalformedDictStringError( 77 "Expecting '%s' in '%s'." % (path_separator, file_info)) 78 source, target = file_info.split(path_separator) 79 extra_files.append(ExtraFile(source, target)) 80 return extra_files 81 82 83def ParseKeyValuePairArgs(dict_str, item_separator=",", key_value_separator=":"): 84 """Helper function to initialize a dict object from string. 85 86 e.g. 87 cpu:2,dpi:240,resolution:1280x800 88 -> {"cpu":"2", "dpi":"240", "resolution":"1280x800"} 89 90 Args: 91 dict_str: A String to be converted to dict object. 92 item_separator: String character to separate items. 93 key_value_separator: String character to separate key and value. 94 95 Returns: 96 Dict created from key:val pairs in dict_str. 97 98 Raises: 99 error.MalformedDictStringError: If dict_str is malformed. 100 """ 101 args_dict = {} 102 if not dict_str: 103 return args_dict 104 105 for item in dict_str.split(item_separator): 106 if key_value_separator not in item: 107 raise errors.MalformedDictStringError( 108 "Expecting ':' in '%s' to make a key-val pair" % item) 109 key, value = item.split(key_value_separator) 110 if not value or not key: 111 raise errors.MalformedDictStringError( 112 "Missing key or value in %s, expecting form of 'a:b'" % item) 113 args_dict[key.strip()] = value.strip() 114 115 return args_dict 116 117 118def GetNonEmptyEnvVars(*variable_names): 119 """Get non-empty environment variables. 120 121 Args: 122 variable_names: Strings, the variable names. 123 124 Returns: 125 List of strings, the variable values that are defined and not empty. 126 """ 127 return list(filter(None, (os.environ.get(v) for v in variable_names))) 128 129 130def GetCvdHostPackage(package_path=None): 131 """Get cvd host package path. 132 133 Look for the host package in specified path or $ANDROID_HOST_OUT and dist 134 dir then verify existence and get cvd host package path. 135 136 Args: 137 package_path: String of cvd host package path. 138 139 Return: 140 A string, the path to the host package. 141 142 Raises: 143 errors.GetCvdLocalHostPackageError: Can't find cvd host package. 144 """ 145 if package_path: 146 if os.path.exists(package_path): 147 return package_path 148 raise errors.GetCvdLocalHostPackageError( 149 "The cvd host package path (%s) doesn't exist." % package_path) 150 dirs_to_check = GetNonEmptyEnvVars(constants.ENV_ANDROID_SOONG_HOST_OUT, 151 constants.ENV_ANDROID_HOST_OUT) 152 dist_dir = utils.GetDistDir() 153 if dist_dir: 154 dirs_to_check.append(dist_dir) 155 156 for path in dirs_to_check: 157 for name in [constants.CVD_HOST_TARBALL, constants.CVD_HOST_PACKAGE]: 158 cvd_host_package = os.path.join(path, name) 159 if os.path.exists(cvd_host_package): 160 logger.debug("cvd host package: %s", cvd_host_package) 161 return cvd_host_package 162 raise errors.GetCvdLocalHostPackageError( 163 "Can't find the cvd host package (Try lunching a cuttlefish target" 164 " like aosp_cf_x86_64_phone-userdebug and running 'm'): \n%s" % 165 '\n'.join(dirs_to_check)) 166 167 168def FindLocalImage(path, default_name_pattern, raise_error=True): 169 """Find an image file in the given path. 170 171 Args: 172 path: The path to the file or the parent directory. 173 default_name_pattern: A regex string, the file to look for if the path 174 is a directory. 175 176 Returns: 177 The absolute path to the image file. 178 179 Raises: 180 errors.GetLocalImageError if this method cannot find exactly one image. 181 """ 182 path = os.path.abspath(path) 183 if os.path.isdir(path): 184 names = [name for name in os.listdir(path) if 185 re.fullmatch(default_name_pattern, name)] 186 if not names: 187 if raise_error: 188 raise errors.GetLocalImageError(f"No image in {path}.") 189 return None 190 if len(names) != 1: 191 raise errors.GetLocalImageError( 192 f"More than one image in {path}: {' '.join(names)}") 193 path = os.path.join(path, names[0]) 194 if os.path.isfile(path): 195 return path 196 raise errors.GetLocalImageError(f"{path} is not a file.") 197 198 199def _IsBootImage(image_path): 200 """Check if a file is an Android boot image by reading the magic bytes. 201 202 Args: 203 image_path: The file path. 204 205 Returns: 206 A boolean, whether the file is a boot image. 207 """ 208 if not os.path.isfile(image_path): 209 return False 210 with open(image_path, "rb") as image_file: 211 return image_file.read(8) == _ANDROID_BOOT_IMAGE_MAGIC 212 213 214def FindBootImage(path, raise_error=True): 215 """Find a boot image file in the given path.""" 216 boot_image_path = FindLocalImage(path, _BOOT_IMAGE_NAME_PATTERN, 217 raise_error) 218 if boot_image_path and not _IsBootImage(boot_image_path): 219 raise errors.GetLocalImageError( 220 f"{boot_image_path} is not a boot image.") 221 return boot_image_path 222 223 224def FindVendorBootImage(path, raise_error=True): 225 """Find a vendor boot image file in the given path.""" 226 return FindLocalImage(path, _VENDOR_BOOT_IMAGE_NAME_PATTERN, raise_error) 227 228 229def FindSystemImages(path): 230 """Find system, system_ext, and product image files in a given path. 231 232 Args: 233 path: A string, the search path. 234 235 Returns: 236 The absolute paths to system, system_ext and product images. 237 The paths to system_ext and product can be None. 238 239 Raises: 240 GetLocalImageError if this method cannot find the system image. 241 """ 242 path = os.path.abspath(path) 243 if os.path.isfile(path): 244 return SystemImagePaths(path, None, None) 245 246 image_dir = path 247 system_image_path = os.path.join(image_dir, _SYSTEM_IMAGE_NAME) 248 if not os.path.isfile(system_image_path): 249 image_dir = os.path.join(path, _TARGET_FILES_IMAGES_DIR_NAME) 250 system_image_path = os.path.join(image_dir, _SYSTEM_IMAGE_NAME) 251 if not os.path.isfile(system_image_path): 252 raise errors.GetLocalImageError( 253 f"No {_SYSTEM_IMAGE_NAME} in {path}.") 254 255 system_ext_image_path = os.path.join(image_dir, _SYSTEM_EXT_IMAGE_NAME) 256 product_image_path = os.path.join(image_dir, _PRODUCT_IMAGE_NAME) 257 return SystemImagePaths( 258 system_image_path, 259 (system_ext_image_path if os.path.isfile(system_ext_image_path) else 260 None), 261 (product_image_path if os.path.isfile(product_image_path) else None)) 262 263 264def DownloadRemoteArtifact(cfg, build_target, build_id, artifact, extract_path, 265 decompress=False): 266 """Download remote artifact. 267 268 Args: 269 cfg: An AcloudConfig instance. 270 build_target: String, the build target, e.g. cf_x86_phone-userdebug. 271 build_id: String, Build id, e.g. "2263051", "P2804227" 272 artifact: String, zip image or cvd host package artifact. 273 extract_path: String, a path include extracted files. 274 decompress: Boolean, if true decompress the artifact. 275 """ 276 build_client = android_build_client.AndroidBuildClient( 277 auth.CreateCredentials(cfg)) 278 temp_file = os.path.join(extract_path, artifact) 279 build_client.DownloadArtifact( 280 build_target, 281 build_id, 282 artifact, 283 temp_file) 284 if decompress: 285 utils.Decompress(temp_file, extract_path) 286 try: 287 os.remove(temp_file) 288 logger.debug("Deleted temporary file %s", temp_file) 289 except OSError as e: 290 logger.error("Failed to delete temporary file: %s", str(e)) 291 292 293def PrepareLocalInstanceDir(instance_dir, avd_spec): 294 """Create a directory for a local cuttlefish or goldfish instance. 295 296 If avd_spec has the local instance directory, this method creates a 297 symbolic link from instance_dir to the directory. Otherwise, it creates an 298 empty directory at instance_dir. 299 300 Args: 301 instance_dir: The absolute path to the default instance directory. 302 avd_spec: AVDSpec object that provides the instance directory. 303 """ 304 if os.path.islink(instance_dir): 305 os.remove(instance_dir) 306 else: 307 shutil.rmtree(instance_dir, ignore_errors=True) 308 309 if avd_spec.local_instance_dir: 310 abs_instance_dir = os.path.abspath(avd_spec.local_instance_dir) 311 if instance_dir != abs_instance_dir: 312 os.symlink(abs_instance_dir, instance_dir) 313 return 314 if not os.path.exists(instance_dir): 315 os.makedirs(instance_dir) 316