1#!/usr/bin/env python3
2#
3# Copyright 2024 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the',  help="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',  help="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
17import glob
18import logging
19import os
20from pathlib import Path
21import platform
22import re
23import shutil
24import zipfile
25
26from environment import get_default_environment
27from tasks.task import Task
28from utils import (
29    AOSP_ROOT,
30    EMULATOR_ARTIFACT_PATH,
31    binary_extension,
32    run,
33)
34
35OBJS_DIR = AOSP_ROOT / "tools" / "netsim" / "objs"
36PLATFORM_SYSTEM = platform.system()
37PLATFORM_MACHINE = platform.machine()
38
39
40class InstallEmulatorTask(Task):
41
42  def __init__(self, args):
43    super().__init__("InstallEmulator")
44    self.buildbot = args.buildbot
45    self.out_dir = args.out_dir
46    # Local fetching use only - default to emulator-linux_x64
47    self.target = args.emulator_target
48    # Local Emulator directory
49    self.local_emulator_dir = args.local_emulator_dir
50
51  def do_run(self):
52    install_emulator_manager = InstallEmulatorManager(
53        self.buildbot, self.out_dir, self.target, self.local_emulator_dir
54    )
55    return install_emulator_manager.process()
56
57
58class InstallEmulatorManager:
59  """Manager for installing emulator artifacts into netsim build
60
61  The InstallEmulatorManager checks if the conditions are met
62  to fetch the emulator artifact zip file and installs it with the
63  newly built netsim. The manager contains processing logic for
64  both local and pre/post submit.
65
66  Attributes:
67    buildbot: A boolean indicating if it's being invoked with Android Build Bots
68    out_dir: A str or None representing the directory of out/. This is priamrily
69      used for Android Build Bots.
70  """
71
72  def __init__(self, buildbot, out_dir, target, local_emulator_dir):
73    """Initializes the instances based on environment
74
75    Args:
76      buildbot: Defines if it's being invoked with Build Bots
77      out_dir: Defines the out directory of the build environment
78      target: The emulator build target to install
79    """
80    self.buildbot = buildbot
81    self.out_dir = out_dir
82    self.target = target
83    self.local_emulator_dir = local_emulator_dir
84
85  def __os_name_fetch(self):
86    """Obtains the os substring of the emulator artifact"""
87    if PLATFORM_SYSTEM == "Linux" and PLATFORM_MACHINE == "x86_64":
88      return "linux"
89    elif PLATFORM_SYSTEM == "Darwin":
90      if PLATFORM_MACHINE == "x86_64":
91        return "darwin"
92      elif PLATFORM_MACHINE == "arm64":
93        return "darwin_aarch64"
94    elif PLATFORM_SYSTEM == "Windows":
95      return "windows"
96    else:
97      logging.info("Unsupported OS:", PLATFORM_SYSTEM, ",", PLATFORM_MACHINE)
98      return None
99
100  def __prerequisites(self) -> bool:
101    """Prerequisite checks for invalid cases"""
102    if self.buildbot:
103      # out_dir is not provided
104      if not self.out_dir:
105        logging.info("Error: please specify '--out_dir' when using buildbots")
106        return False
107      # If out_dir does not exist
108      elif not Path(self.out_dir).exists():
109        logging.info(f"Error: {self.out_dir} does not exist")
110        return False
111    else:
112      # Without buildbots, this scripts is only runnable on Linux
113      # TODO: support local builds for Mac and Windows
114      if PLATFORM_SYSTEM != "Linux" and not self.local_emulator_dir:
115        logging.info(
116            "The local case only works for Linux if you don't have"
117            " --local_emulator_dir specified"
118        )
119        return False
120      # Check if the netsim has been built prior to install_emulator
121      if not (
122          OBJS_DIR.exists()
123          and (OBJS_DIR / binary_extension("netsim")).exists()
124          and (OBJS_DIR / binary_extension("netsimd")).exists()
125      ):
126        logging.info(
127            "Please run 'scripts/build_tools.sh --Compile' "
128            "before running InstallEmulator"
129        )
130        return False
131    return True
132
133  def __unzip_emulator_artifacts(self, os_name_artifact) -> bool:
134    """unzips the emulator artifacts inside EMULATOR_ARTIFACT_PATH"""
135    # Unzipping emulator artifacts
136    zip_file_exists = False
137    for filename in os.listdir(EMULATOR_ARTIFACT_PATH):
138      # Check if the filename matches the pattern
139      if re.match(
140          rf"^sdk-repo-{os_name_artifact}-emulator-\d+\.zip$", filename
141      ):
142        zip_file_exists = True
143        logging.info(f"Unzipping {filename}...")
144        with zipfile.ZipFile(EMULATOR_ARTIFACT_PATH / filename, "r") as zip_ref:
145          zip_ref.extractall(EMULATOR_ARTIFACT_PATH)
146          # Preserve permission bits
147          for info in zip_ref.infolist():
148            filename = EMULATOR_ARTIFACT_PATH / info.filename
149            original_permissions = info.external_attr >> 16
150            if original_permissions:
151              os.chmod(filename, original_permissions)
152    # Log and return False if the artifact does not exist
153    if not zip_file_exists:
154      logging.info("Emulator artifact prebuilt is not found!")
155      return False
156    # Remove all zip files
157    files = glob.glob(
158        str(
159            EMULATOR_ARTIFACT_PATH
160            / f"sdk-repo-{os_name_artifact}-emulator-*.zip"
161        )
162    )
163    for file in files:
164      os.remove(EMULATOR_ARTIFACT_PATH / file)
165    return True
166
167  def __copy_artifacts(self, emulator_filepath):
168    """Copy artifacts into desired location
169
170    In the local case, the emulator artifacts get copied into objs/
171    In the buildbot case, the netsim artifacts get copied into
172      EMULATOR_ARTIFACT_PATH
173
174    Note that the downloaded netsim artifacts are removed before copying.
175    """
176    # Remove all downloaded netsim artifacts
177    files = glob.glob(str(emulator_filepath / "netsim*"))
178    for fname in files:
179      file = emulator_filepath / fname
180      if os.path.isdir(file):
181        shutil.rmtree(file)
182      else:
183        os.remove(file)
184    # Copy artifacts
185    if self.buildbot:
186      shutil.copytree(
187          Path(self.out_dir) / "distribution" / "emulator",
188          emulator_filepath,
189          symlinks=True,
190          dirs_exist_ok=True,
191      )
192    else:
193      shutil.copytree(
194          emulator_filepath,
195          OBJS_DIR,
196          symlinks=True,
197          dirs_exist_ok=True,
198      )
199      shutil.copytree(
200          emulator_filepath,
201          OBJS_DIR / "distribution" / "emulator",
202          symlinks=True,
203          dirs_exist_ok=True,
204      )
205
206  def process(self) -> bool:
207    """Process the emulator installation
208
209    The process will terminate if sub-function calls returns
210    a None or False
211    """
212    # Obtain OS name of the artifact
213    os_name_artifact = self.__os_name_fetch()
214    if not os_name_artifact:
215      return False
216
217    # Invalid Case checks
218    if not self.__prerequisites():
219      return False
220
221    if self.local_emulator_dir:
222      # If local_emulator_dir is provided, copy the artifacts from this directory.
223      self.__copy_artifacts(Path(self.local_emulator_dir))
224    else:
225      # Artifact fetching for local case
226      if not self.buildbot:
227        # Simulating the shell command
228        run(
229            [
230                "/google/data/ro/projects/android/fetch_artifact",
231                "--latest",
232                "--target",
233                self.target,
234                "--branch",
235                "aosp-emu-master-dev",
236                "sdk-repo-linux-emulator-*.zip",
237            ],
238            get_default_environment(AOSP_ROOT),
239            "install_emulator",
240            cwd=EMULATOR_ARTIFACT_PATH,
241        )
242
243      # Unzipping emulator artifacts and remove zip files
244      if not self.__unzip_emulator_artifacts(os_name_artifact):
245        return False
246
247      # Copy artifacts after removing downloaded netsim artifacts
248      self.__copy_artifacts(EMULATOR_ARTIFACT_PATH / "emulator")
249
250    # Remove the EMULATOR_ARTIFACT_PATH in local case
251    if not self.buildbot:
252      shutil.rmtree(EMULATOR_ARTIFACT_PATH, ignore_errors=True)
253
254    logging.info("Emulator installation completed!")
255    return True
256