Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions emu/android_release_zip.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ def is_emulator(self) -> bool:
"""True if this zip file contains the android emulator."""
return "Android Emulator" in self.description()

def is_preview_emulator(self) -> bool:
"""True if this zip contains the Emulator Preview rather than the regular emulator.

The preview requires features on the system images that are right now
only available in the API 37+ system images. Its Pkg.Desc is
"Android Emulator (Preview)".
"""
return self.is_emulator() and "(Preview)" in (self.description() or "")

def copy(self, destination: str) -> str:
"""Copy the zipfile to the given destination.

Expand Down
9 changes: 8 additions & 1 deletion emu/containers/emulator_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,14 @@ def write(self, dest):
)

writer.write_template(
"launch-emulator.sh", {"extra": self.extra, "version": emu.__version__}
"launch-emulator.sh",
{
"extra": self.extra,
"version": emu.__version__,
# The Emulator Preview runs on a much newer qemu that rejects
# some of the passthrough arguments the regular emulator takes.
"preview_emulator": self.emulator_zip.is_preview_emulator(),
},
)
writer.write_template("default.pa", {})

Expand Down
90 changes: 70 additions & 20 deletions emu/emu_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"""Minimal dependency script to create a Dockerfile for a particular combination of emulator and system image."""

import argparse
import itertools
import logging
import os
import re
Expand All @@ -25,6 +24,7 @@
import click
import colorlog
import emu.emu_downloads_menu as emu_downloads_menu
from emu.android_release_zip import AndroidReleaseZip
from emu.cloud_build import cloud_build
from emu.containers.emulator_container import EmulatorContainer
from emu.containers.system_image_container import SystemImageContainer
Expand Down Expand Up @@ -63,6 +63,34 @@ def metrics_config(args):
return cfg


def _api_major(sys_docker):
"""The integer api level of the system image in the given container."""
if sys_docker.system_image_info is not None:
return sys_docker.system_image_info.api_major
match = re.match(r"\d+", sys_docker.system_image_zip.api() or "")
return int(match.group()) if match else 0


def _check_emulator_supports_image(emulator, sys_docker):
"""Refuse an emulator/system-image pairing that cannot work.

The Emulator Preview requires features on the system images that are right
now only available in the API 37+ system images, so it is rejected below
MIN_API_PREVIEW_EMULATOR. Checked from the emulator zip itself so it also
covers locally supplied files, not just menu selections.
"""
if not AndroidReleaseZip(emulator).is_preview_emulator():
return
api_major = _api_major(sys_docker)
if api_major < emu_downloads_menu.MIN_API_PREVIEW_EMULATOR:
raise Exception(
f"The Emulator Preview requires an API "
f"{emu_downloads_menu.MIN_API_PREVIEW_EMULATOR}+ system image, "
f"but {os.path.basename(str(emulator))} was paired with API {api_major}. "
f"Use the regular emulator for this image."
)


def create_docker_image(args):
"""Create a directory containing all the necessary ingredients to construct a docker image.

Expand All @@ -73,18 +101,32 @@ def create_docker_image(args):
if not os.path.exists(imgzip[0]):
imgzip = emu_downloads_menu.find_image(imgzip[0])

emuzip = [args.emuzip]
if emuzip[0] in ["stable", "canary", "all"]:
emuzip = [x.download() for x in emu_downloads_menu.find_emulator(emuzip[0])]
elif re.match(r"\d+", emuzip[0]):
# We must be looking for a build id
logging.info("Treating %s as a build id", emuzip[0])
emuzip = [emu_downloads_menu.download_build(emuzip[0])]
# A channel resolves to a different emulator depending on the system image
# (API 37+ images get the Emulator Preview), so it is looked up per image.
emuzip = {}

def emulators_for(sys_docker):
if args.emuzip in ["stable", "beta", "dev", "canary", "all"]:
api_major = _api_major(sys_docker)
preview = api_major >= emu_downloads_menu.MIN_API_PREVIEW_EMULATOR
if preview not in emuzip:
emuzip[preview] = [
x.download()
for x in emu_downloads_menu.find_emulator(args.emuzip, api_major)
]
return emuzip[preview]
if None not in emuzip:
emuzip[None] = [args.emuzip]
if re.match(r"\d+", args.emuzip):
# We must be looking for a build id
logging.info("Treating %s as a build id", args.emuzip)
emuzip[None] = [emu_downloads_menu.download_build(args.emuzip)]
return emuzip[None]

devices = []
logging.info("Using repo %s", args.repo)
for img, emulator in itertools.product(imgzip, emuzip):
logging.info("Processing %s, %s", img, emulator)
for img in imgzip:
logging.info("Processing %s", img)
sys_docker = SystemImageContainer(img, args.repo)
if not sys_docker.available() and not sys_docker.can_pull():
sys_docker.build(Path(args.dest) / "sys_img")
Expand All @@ -102,25 +144,29 @@ def create_docker_image(args):
if args.sys:
continue

emu_docker = EmulatorContainer(
emulator, sys_docker, args.repo, cfg.collect_metrics(), args.extra, args.name
)
emu_docker.build(Path(args.dest) / "emulator")
for emulator in emulators_for(sys_docker):
logging.info("Processing %s, %s", img, emulator)
_check_emulator_supports_image(emulator, sys_docker)

if args.start:
emu_docker.launch({"5555/tcp": 5555, "8554/tcp": 8554})
if args.push:
emu_docker.push()
emu_docker = EmulatorContainer(
emulator, sys_docker, args.repo, cfg.collect_metrics(), args.extra, args.name
)
emu_docker.build(Path(args.dest) / "emulator")

if args.start:
emu_docker.launch({"5555/tcp": 5555, "8554/tcp": 8554})
if args.push:
emu_docker.push()

devices.append(emu_docker)
devices.append(emu_docker)

return devices


def create_docker_image_interactive(args):
"""Interactively create a docker image by selecting the desired combination from a menu."""
img = emu_downloads_menu.select_image(args.arm) or sys.exit(1)
emulator = emu_downloads_menu.select_emulator() or sys.exit(1)
emulator = emu_downloads_menu.select_emulator(img) or sys.exit(1)
cfg = DockerConfig()
metrics = False

Expand Down Expand Up @@ -197,6 +243,8 @@ def main():
create_parser.add_argument(
"emuzip",
help="Zipfile containing the a publicly released emulator, or (canary|stable|[0-9]+) to use the latest canary, stable, or build id of the emulator to use. "
"For API 37+ system images, canary and stable select the latest Emulator Preview; pass a zipfile to use another emulator. "
"The Emulator Preview requires features on the system images that are right now only available in the API 37+ system images. "
"Keep in mind that using a build id can result in downloading an untested pre-release emulator build from the android ci server.",
)
create_parser.add_argument(
Expand Down Expand Up @@ -326,6 +374,8 @@ def main():
dist_parser.add_argument(
"emuzip",
help="Zipfile containing the a publicly released emulator, or (canary|stable|[0-9]+) to use the latest canary, stable, or build id of the emulator to use. "
"For API 37+ system images, canary and stable select the latest Emulator Preview; pass a zipfile to use another emulator. "
"The Emulator Preview requires features on the system images that are right now only available in the API 37+ system images. "
"Keep in mind that using a build id can result in downloading an untested pre-release emulator build from the android ci server.",
)
dist_parser.add_argument(
Expand Down
111 changes: 99 additions & 12 deletions emu/emu_downloads_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@

EMU_REPOS = ["%s/android/repository/repository2-1.xml" % ANDROID_REPOSITORY]

# The regular emulator is published under the "emulator" path; the Emulator
# Preview under "emulators;latest" and "emulators;<build-id>".
EMU_PATH = "emulator"
EMU_PREVIEW_PATH_PREFIX = "emulators;"
EMU_PREVIEW_LATEST_PATH = "emulators;latest"

CHANNEL_MAPPING = {
"channel-0": "stable",
"channel-1": "beta",
Expand Down Expand Up @@ -77,6 +83,12 @@
MIN_API_I386 = 19 # K
MIN_API_X64 = 26 # O

# The Emulator Preview ("emulators;latest" and the pinned "emulators;<build-id>"
# packages) requires features on the system images that are right now only
# available in the API 37+ system images. It is what those images get by
# default, and it is not offered below that.
MIN_API_PREVIEW_EMULATOR = 37


class License(object):
"""Represents a license."""
Expand Down Expand Up @@ -243,13 +255,29 @@ def __init__(self, pkg, licenses):
rev_major = rev.find("major").text
rev_minor = rev.find("minor").text
rev_micro = rev.find("micro").text
# The Emulator Preview packages carry a <preview> build id and share the
# same 0.0.1 major/minor/micro, so without it every preview release
# would present itself as plain "0.0.1".
rev_preview = rev.find("preview")

archives = pkg.find("archives")
channel = pkg.find("channelRef")

self.channel = CHANNEL_MAPPING[channel.attrib["ref"]]

# "emulator" is the regular emulator; "emulators;latest" and
# "emulators;<build-id>" are the Emulator Preview.
self.path = pkg.attrib.get("path", "")
self.is_preview = self.path.startswith(EMU_PREVIEW_PATH_PREFIX)

display_name = pkg.find("display-name")
self.display_name = (
display_name.text if display_name is not None else "Android Emulator"
)

self.version = "%s.%s.%s" % (rev_major, rev_minor, rev_micro)
if rev_preview is not None and rev_preview.text:
self.version = "%s-%s" % (self.version, rev_preview.text)
self.urls = {}

for archive in archives:
Expand All @@ -258,6 +286,10 @@ def __init__(self, pkg, licenses):
self.urls[hostos] = "%s/android/repository/%s" % (ANDROID_REPOSITORY, url)

def download_name(self):
# The two emulators are versioned independently, and an existing file
# is not downloaded again, so they must not share a name.
if self.is_preview:
return "emulator-preview-{}.zip".format(self.version)
return "emulator-{}.zip".format(self.version)

def download(self, hostos="linux", dest=None):
Expand All @@ -268,6 +300,8 @@ def download(self, hostos="linux", dest=None):
return super(EmuInfo, self).download(self.urls[hostos], dest)

def __str__(self):
if self.is_preview:
return "preview {}".format(self.version)
return "{} {}".format(self.channel, self.version)


Expand Down Expand Up @@ -334,15 +368,37 @@ def find_image(regexpr):
return matches


def find_emulator(channel):
"""Finds the released emulator binaries for the given channel.
def find_emulator(channel, api_major=None):
"""Finds the released emulator binaries to use for the given channel.

As with sdkmanager, a channel includes the more stable channels below it:
"canary" gives the newest emulator published on any channel, "stable" the
newest stable one. "all" gives every published emulator.

If the api level of the system image is given and it is
MIN_API_PREVIEW_EMULATOR or higher, the latest Emulator Preview is returned
whatever the channel, as that is the emulator those images should run on.

Returns a list of EmuInfo objects."""
emu_infos = [x for x in get_emus_info() if "linux" in x.urls]
if api_major is not None and api_major >= MIN_API_PREVIEW_EMULATOR:
latest = [x for x in emu_infos if x.path == EMU_PREVIEW_LATEST_PATH]
if latest:
logging.info(
"Using the Emulator Preview %s for API %s (%s+ system images use it instead of %s)",
latest[0].version,
api_major,
MIN_API_PREVIEW_EMULATOR,
channel,
)
return latest[:1]
logging.warning(
"No Emulator Preview found for API %s, using the %s emulator",
api_major,
channel,
)

emu_infos = [x for x in emu_infos if not x.is_preview]
if channel != "all":
channels = list(CHANNEL_MAPPING.values())
included = channels[: channels.index(channel) + 1] if channel in channels else []
Expand Down Expand Up @@ -373,7 +429,8 @@ def get_emus_info():
[
p
for p in ET.fromstring(x).findall("remotePackage")
if "emulator" == p.attrib["path"]
if p.attrib["path"] == EMU_PATH
or p.attrib["path"].startswith(EMU_PREVIEW_PATH_PREFIX)
]
for x in xml
]
Expand All @@ -398,12 +455,45 @@ def select_image(arm):
return img_infos[selection] if selection < len(img_infos) else None


def select_emulator():
def emulator_supports_image(emu_info, sys_img_info):
"""True if this emulator can be paired with the given system image.

The Emulator Preview requires features on the system images that are right
now only available in the API 37+ system images, so it is not offered below
MIN_API_PREVIEW_EMULATOR.
"""
if not emu_info.is_preview or sys_img_info is None:
return True
return sys_img_info.api_major >= MIN_API_PREVIEW_EMULATOR


def select_emulator(sys_img_info=None):
"""Displayes an interactive menu to select a released emulator binary.

Returns a ImuInfo object with the choice or None if the user aborts."""
emu_infos = [x for x in get_emus_info() if "linux" in x.urls]
display = [f"EMU {emu_info.channel} {emu_info.version}" for emu_info in emu_infos]
If a system image is given, emulators that cannot run it are left out of
the menu, so an unsupported pairing cannot be picked. When the Emulator
Preview is offered it is listed first, latest on top.

Returns a EmuInfo object with the choice or None if the user aborts."""
emu_infos = [
x
for x in get_emus_info()
if "linux" in x.urls and emulator_supports_image(x, sys_img_info)
]
if sys_img_info is not None:
emu_infos.sort(
key=lambda x: (not x.is_preview, x.path != EMU_PREVIEW_LATEST_PATH)
)
display = [
f"EMU {'preview' if emu_info.is_preview else emu_info.channel} "
f"{emu_info.version} ({emu_info.display_name})"
for emu_info in emu_infos
]
if sys_img_info is not None and not any(x.is_preview for x in emu_infos):
print(
f"Note: the Emulator Preview is not offered for API {sys_img_info.api}; "
f"it requires API {MIN_API_PREVIEW_EMULATOR} or higher."
)
selection = SelectionMenu.get_selection(
display, title="Select the emulator you wish to use:"
)
Expand All @@ -429,12 +519,9 @@ def list_all_downloads(arm):
)

for emu_info in emu_infos:
channel = "preview" if emu_info.is_preview else emu_info.channel
for hostos, url in list(emu_info.urls.items()):
print(
"EMU {} {} {} {}".format(
emu_info.channel, emu_info.version, hostos, url
)
)
print("EMU {} {} {} {}".format(channel, emu_info.version, hostos, url))


def download_build(build_id, dest=None):
Expand Down
7 changes: 7 additions & 0 deletions emu/templates/launch-emulator.sh
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,14 @@ if [ ! -z "${TURN}" ]; then
fi

# Add qemu specific parameters
{% if preview_emulator %}
# The Emulator Preview builds its own qemu command line, which already carries
# an -append with the guest kernel arguments. Passing another one through
# -qemu makes its (much newer) qemu fail with "requires an argument", so the
# panic=1 kernel argument is not set for preview builds.
{% else %}
LAUNCH_CMD+=("-qemu" "-append" "panic=1")
{% endif %}

if [ ! -z "${ANDROID_AVD_HOME}" ]; then
export ANDROID_AVD_HOME=/android-home
Expand Down
Loading
Loading