• 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_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