• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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