Support mixing images in local-image-local-instance mode

Acloud supports mixing local cuttlefish images with the system image
given by --local-system-image. Mixing the images requires OTA tools and
misc info of cuttlefish build. Acloud gets them from either environment
variables or command line arguments. The developers can use this feature
to test locally built images and the artifacts downloaded from build
server.

Test: acloud-dev create --local-instance \
      --local-image --local-system-image
Test: acloud-dev create --local-instance \
      --local-image ~/aosp_cf_x86_phone-target_files \
      --local-system-image ~/aosp_x86-img \
      --local-tool ~/cvd-host_package \
      --local-tool ~/otatools
Bug: 176270532
Change-Id: I516848ce847178041b976cc65dea4aa342202a11
diff --git a/create/avd_spec.py b/create/avd_spec.py
index f0ac5ad..794446e 100644
--- a/create/avd_spec.py
+++ b/create/avd_spec.py
@@ -467,6 +467,13 @@
             self._CheckCFBuildTarget(self._instance_type)
             local_image_path = utils.GetBuildEnvironmentVariable(
                 _ENV_ANDROID_PRODUCT_OUT)
+            # Since dir is provided, check that any images exist to ensure user
+            # didn't forget to 'make' before launch AVD.
+            image_list = glob.glob(os.path.join(local_image_path, "*.img"))
+            if not image_list:
+                raise errors.GetLocalImageError(
+                    "No image found(Did you choose a lunch target and run `m`?)"
+                    ": %s.\n " % local_image_path)
         else:
             local_image_path = local_image_arg
 
@@ -480,14 +487,6 @@
                                    utils.TextColors.WARNING)
         else:
             self._local_image_dir = local_image_path
-            # Since dir is provided, so checking that any images exist to ensure
-            # user didn't forget to 'make' before launch AVD.
-            image_list = glob.glob(os.path.join(self.local_image_dir, "*.img"))
-            if not image_list:
-                raise errors.GetLocalImageError(
-                    "No image found(Did you choose a lunch target and run `m`?)"
-                    ": %s.\n " % self.local_image_dir)
-
             try:
                 flavor_from_build_string = self._GetFlavorFromString(
                     utils.GetBuildEnvironmentVariable(constants.ENV_BUILD_TARGET))
diff --git a/create/avd_spec_test.py b/create/avd_spec_test.py
index 836c3ae..c608344 100644
--- a/create/avd_spec_test.py
+++ b/create/avd_spec_test.py
@@ -92,15 +92,37 @@
         self.AvdSpec._ProcessLocalImageArgs(self.args)
         self.assertEqual(self.AvdSpec._local_image_dir, expected_image_dir)
 
-        # Specified --avd-type=goldfish --local_image without arg
-        self.Patch(utils, "GetBuildEnvironmentVariable",
-                   return_value="test_environ")
-        self.Patch(os.path, "isdir", return_value=True)
+        # Specified --local-image and --local-system-image with dirs
+        self.args.local_image = expected_image_dir
+        self.args.local_system_image = expected_image_dir
+        self.AvdSpec._avd_type = constants.TYPE_CF
+        self.AvdSpec._instance_type = constants.INSTANCE_TYPE_LOCAL
+        with mock.patch("os.path.isfile", return_value=False), \
+             mock.patch("os.path.isdir",
+                        side_effect=lambda path: path == expected_image_dir), \
+             mock.patch("acloud.create.avd_spec.utils."
+                        "GetBuildEnvironmentVariable",
+                        return_value="cf_x86_phone"):
+            self.AvdSpec._ProcessLocalImageArgs(self.args)
+        self.assertEqual(self.AvdSpec._local_image_dir, expected_image_dir)
+        self.assertEqual(self.AvdSpec._local_system_image_dir,
+                         expected_image_dir)
+
+        # Specified --avd-type=goldfish, --local_image, and
+        # --local-system-image without args
         self.args.local_image = constants.FIND_IN_BUILD_ENV
+        self.args.local_system_image = constants.FIND_IN_BUILD_ENV
         self.AvdSpec._avd_type = constants.TYPE_GF
         self.AvdSpec._instance_type = constants.INSTANCE_TYPE_LOCAL
-        self.AvdSpec._ProcessLocalImageArgs(self.args)
-        self.assertEqual(self.AvdSpec._local_image_dir, "test_environ")
+        with mock.patch("os.path.isdir",
+                        side_effect=lambda path: path == expected_image_dir), \
+             mock.patch("acloud.create.avd_spec.utils."
+                        "GetBuildEnvironmentVariable",
+                        return_value=expected_image_dir):
+            self.AvdSpec._ProcessLocalImageArgs(self.args)
+        self.assertEqual(self.AvdSpec._local_image_dir, expected_image_dir)
+        self.assertEqual(self.AvdSpec._local_system_image_dir,
+                         expected_image_dir)
 
     def testProcessImageArgs(self):
         """Test process image source."""
diff --git a/create/create_args.py b/create/create_args.py
index 63eaeea..40cf44e 100644
--- a/create/create_args.py
+++ b/create/create_args.py
@@ -555,9 +555,8 @@
         raise errors.CheckPathError(
             "Specified path doesn't exist: %s" % args.local_instance_dir)
 
-    # TODO(b/133211308): Support TYPE_CF.
     if not (args.local_system_image is None or
-            args.avd_type == constants.TYPE_GF):
+            args.avd_type in (constants.TYPE_CF, constants.TYPE_GF)):
         raise errors.UnsupportedCreateArgs("%s instance does not support "
                                            "--local-system-image" %
                                            args.avd_type)
diff --git a/create/local_image_local_instance.py b/create/local_image_local_instance.py
index 322284d..e40d4b6 100644
--- a/create/local_image_local_instance.py
+++ b/create/local_image_local_instance.py
@@ -33,9 +33,23 @@
 
 To delete the local instance, we will call stop_cvd with the environment variable
 [CUTTLEFISH_CONFIG_FILE] which is pointing to the runtime cuttlefish json.
+
+To run this program outside of a build environment, the following setup is
+required.
+- One of the local tool directories is a decompressed cvd host package,
+  i.e., cvd-host_package.tar.gz.
+- If the instance doesn't require mixed images, the local image directory
+  should be an unzipped update package, i.e., <target>-img-<build>.zip,
+  which contains a super image.
+- If the instance requires mixed images, the local image directory should
+  be an unzipped target files package, i.e., <target>-target_files-<build>.zip,
+  which contains misc info and images not packed into a super image.
+- If the instance requires mixed images, one of the local tool directories
+  should be an unzipped OTA tools package, i.e., otatools.zip.
 """
 
 import collections
+import glob
 import logging
 import os
 import subprocess
@@ -46,6 +60,7 @@
 from acloud.create import base_avd_create
 from acloud.create import create_common
 from acloud.internal import constants
+from acloud.internal.lib import ota_tools
 from acloud.internal.lib import utils
 from acloud.internal.lib.adb_tools import AdbTools
 from acloud.list import list as list_instance
@@ -55,6 +70,11 @@
 
 logger = logging.getLogger(__name__)
 
+_SYSTEM_IMAGE_NAME = "system.img"
+_MISC_INFO_FILE_NAME = "misc_info.txt"
+_TARGET_FILES_IMAGES_DIR_NAME = "IMAGES"
+_TARGET_FILES_META_DIR_NAME = "META"
+_MIXED_SUPER_IMAGE_NAME = "mixed_super.img"
 _CMD_LAUNCH_CVD_ARGS = (" -daemon -cpus %s -x_res %s -y_res %s -dpi %s "
                         "-memory_mb %s -run_adb_connector=%s "
                         "-system_image_dir %s -instance_dir %s "
@@ -69,6 +89,7 @@
                                "-start_webrtc=true "
                                "-webrtc_public_ip=%s" % constants.LOCALHOST)
 _CMD_LAUNCH_CVD_VNC_ARG = " -start_vnc_server=true"
+_CMD_LAUNCH_CVD_SUPER_IMAGE_ARG = " -super_image=%s"
 
 # In accordance with the number of network interfaces in
 # /etc/init.d/cuttlefish-common
@@ -81,10 +102,13 @@
                      "Enter 'y' to terminate current instance and launch a new "
                      "instance, enter anything else to exit out[y/N]: ")
 
-# The collection of image folder and CVD host package folder for local
-# instances.
+# The first two fields of this named tuple are image folder and CVD host
+# package folder which are essential for local instances. The following fields
+# are optional. They are set when the AVD spec requires to mix images.
 ArtifactPaths = collections.namedtuple(
-    "ArtifactPaths", ["image_dir", "host_bins"])
+    "ArtifactPaths",
+    ["image_dir", "host_bins", "misc_info", "ota_tools_dir", "system_image"],
+    defaults=[None, None, None])
 
 
 class LocalImageLocalInstance(base_avd_create.BaseAVDCreate):
@@ -181,6 +205,10 @@
 
         cvd_home_dir = instance.GetLocalInstanceHomeDir(local_instance_id)
         create_common.PrepareLocalInstanceDir(cvd_home_dir, avd_spec)
+        super_image_path = None
+        if artifact_paths.system_image:
+            super_image_path = self._MixSuperImage(cvd_home_dir,
+                                                   artifact_paths)
         runtime_dir = instance.GetLocalInstanceRuntimeDir(local_instance_id)
         # TODO(b/168171781): cvd_status of list/delete via the symbolic.
         self.PrepareLocalCvdToolsLink(cvd_home_dir, artifact_paths.host_bins)
@@ -194,6 +222,7 @@
                                        avd_spec.connect_webrtc,
                                        avd_spec.connect_vnc,
                                        avd_spec.gpu,
+                                       super_image_path,
                                        avd_spec.cfg.launch_args)
 
         result_report = report.Report(command="create")
@@ -251,6 +280,54 @@
             "CVD host binaries are not found. Please run `make hosttar`, or "
             "set --local-tool to an extracted CVD host package.")
 
+    @staticmethod
+    def _FindMiscInfo(image_dir):
+        """Find misc info in build output dir or extracted target files.
+
+        Args:
+            image_dir: The directory to search for misc info.
+
+        Returns:
+            image_dir if the directory structure looks like an output directory
+            in build environment.
+            image_dir/META if it looks like extracted target files.
+
+        Raises:
+            errors.CheckPathError if this method cannot find misc info.
+        """
+        misc_info_path = os.path.join(image_dir, _MISC_INFO_FILE_NAME)
+        if os.path.isfile(misc_info_path):
+            return misc_info_path
+        misc_info_path = os.path.join(image_dir, _TARGET_FILES_META_DIR_NAME,
+                                      _MISC_INFO_FILE_NAME)
+        if os.path.isfile(misc_info_path):
+            return misc_info_path
+        raise errors.CheckPathError(
+            "Cannot find %s in %s." % (_MISC_INFO_FILE_NAME, image_dir))
+
+    @staticmethod
+    def _FindImageDir(image_dir):
+        """Find images in build output dir or extracted target files.
+
+        Args:
+            image_dir: The directory to search for images.
+
+        Returns:
+            image_dir if the directory structure looks like an output directory
+            in build environment.
+            image_dir/IMAGES if it looks like extracted target files.
+
+        Raises:
+            errors.GetLocalImageError if this method cannot find images.
+        """
+        if glob.glob(os.path.join(image_dir, "*.img")):
+            return image_dir
+        subdir = os.path.join(image_dir, _TARGET_FILES_IMAGES_DIR_NAME)
+        if glob.glob(os.path.join(subdir, "*.img")):
+            return subdir
+        raise errors.GetLocalImageError(
+            "Cannot find images in %s." % image_dir)
+
     def GetImageArtifactsPath(self, avd_spec):
         """Get image artifacts path.
 
@@ -265,15 +342,57 @@
         Returns:
             ArtifactPaths object consisting of image directory and host bins
             package.
+
+        Raises:
+            errors.GetCvdLocalHostPackageError, errors.GetLocalImageError, or
+            errors.CheckPathError if any artifact is not found.
         """
-        return ArtifactPaths(
-            avd_spec.local_image_dir,
-            self._FindCvdHostBinaries(avd_spec.local_tool_dirs))
+        image_dir = os.path.abspath(avd_spec.local_image_dir)
+        host_bins_path = self._FindCvdHostBinaries(avd_spec.local_tool_dirs)
+
+        if not avd_spec.local_system_image_dir:
+            return ArtifactPaths(image_dir, host_bins_path)
+
+        misc_info_path = self._FindMiscInfo(image_dir)
+        image_dir = self._FindImageDir(image_dir)
+        ota_tools_dir = os.path.abspath(
+            ota_tools.FindOtaTools(avd_spec.local_tool_dirs))
+        system_image_path = os.path.abspath(
+            os.path.join(avd_spec.local_system_image_dir, _SYSTEM_IMAGE_NAME))
+        if not os.path.isfile(system_image_path):
+            raise errors.GetLocalImageError(
+                "Cannot find %s." % system_image_path)
+
+        return ArtifactPaths(image_dir, host_bins_path,
+                             misc_info=misc_info_path,
+                             ota_tools_dir=ota_tools_dir,
+                             system_image=system_image_path)
+
+    @staticmethod
+    def _MixSuperImage(output_dir, artifact_paths):
+        """Mix cuttlefish images and a system image into a super image.
+
+        Args:
+            output_dir: The path to the output directory.
+            artifact_paths: ArtifactPaths object.
+
+        Returns:
+            The path to the super image in output_dir.
+        """
+        ota = ota_tools.OtaTools(artifact_paths.ota_tools_dir)
+        super_image_path = os.path.join(output_dir, _MIXED_SUPER_IMAGE_NAME)
+        ota.BuildSuperImage(
+            super_image_path, artifact_paths.misc_info,
+            lambda partition: ota_tools.GetImageForPartition(
+                partition, artifact_paths.image_dir,
+                system=artifact_paths.system_image))
+        return super_image_path
 
     @staticmethod
     def PrepareLaunchCVDCmd(launch_cvd_path, hw_property, connect_adb,
-                            system_image_dir, runtime_dir, connect_webrtc,
-                            connect_vnc, gpu, launch_args):
+                            image_dir, runtime_dir, connect_webrtc,
+                            connect_vnc, gpu, super_image_path,
+                            launch_args):
         """Prepare launch_cvd command.
 
         Create the launch_cvd commands with all the required args and add
@@ -282,13 +401,14 @@
         Args:
             launch_cvd_path: String of launch_cvd path.
             hw_property: dict object of hw property.
-            system_image_dir: String of local images path.
+            image_dir: String of local images path.
             connect_adb: Boolean flag that enables adb_connector.
             runtime_dir: String of runtime directory path.
             connect_webrtc: Boolean of connect_webrtc.
             connect_vnc: Boolean of connect_vnc.
             gpu: String of gpu name, the gpu name of local instance should be
                  "default" if gpu is enabled.
+            super_image_path: String of non-default super image path.
             launch_args: String of launch args.
 
         Returns:
@@ -297,8 +417,7 @@
         launch_cvd_w_args = launch_cvd_path + _CMD_LAUNCH_CVD_ARGS % (
             hw_property["cpu"], hw_property["x_res"], hw_property["y_res"],
             hw_property["dpi"], hw_property["memory"],
-            ("true" if connect_adb else "false"), system_image_dir,
-            runtime_dir)
+            ("true" if connect_adb else "false"), image_dir, runtime_dir)
         if constants.HW_ALIAS_DISK in hw_property:
             launch_cvd_w_args = (launch_cvd_w_args + _CMD_LAUNCH_CVD_DISK_ARGS %
                                  hw_property[constants.HW_ALIAS_DISK])
@@ -311,6 +430,11 @@
         if gpu:
             launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_GPU_ARG
 
+        if super_image_path:
+            launch_cvd_w_args = (launch_cvd_w_args +
+                                 _CMD_LAUNCH_CVD_SUPER_IMAGE_ARG %
+                                 super_image_path)
+
         if launch_args:
             launch_cvd_w_args = launch_cvd_w_args + " " + launch_args
 
diff --git a/create/local_image_local_instance_test.py b/create/local_image_local_instance_test.py
index 8a2ac9e..ab7d103 100644
--- a/create/local_image_local_instance_test.py
+++ b/create/local_image_local_instance_test.py
@@ -17,6 +17,7 @@
 
 import os
 import subprocess
+import tempfile
 import unittest
 import mock
 
@@ -51,6 +52,12 @@
 sg group2
 launch_cvd -daemon -cpus fake -x_res fake -y_res fake -dpi fake -memory_mb fake -run_adb_connector=true -system_image_dir fake_image_dir -instance_dir fake_cvd_dir -undefok=report_anonymous_usage_stats,enable_sandbox -report_anonymous_usage_stats=y -enable_sandbox=false -guest_enforce_security=false -vm_manager=crosvm -start_webrtc=true -webrtc_public_ip=0.0.0.0
 EOF"""
+
+    LAUNCH_CVD_CMD_WITH_SUPER_IMAGE = """sg group1 <<EOF
+sg group2
+launch_cvd -daemon -cpus fake -x_res fake -y_res fake -dpi fake -memory_mb fake -run_adb_connector=true -system_image_dir fake_image_dir -instance_dir fake_cvd_dir -undefok=report_anonymous_usage_stats,enable_sandbox -report_anonymous_usage_stats=y -enable_sandbox=false -start_vnc_server=true -super_image=fake_super_image
+EOF"""
+
     LAUNCH_CVD_CMD_WITH_ARGS = """sg group1 <<EOF
 sg group2
 launch_cvd -daemon -cpus fake -x_res fake -y_res fake -dpi fake -memory_mb fake -run_adb_connector=true -system_image_dir fake_image_dir -instance_dir fake_cvd_dir -undefok=report_anonymous_usage_stats,enable_sandbox -report_anonymous_usage_stats=y -enable_sandbox=false -start_vnc_server=true -setupwizard_mode=REQUIRED
@@ -151,6 +158,7 @@
         mock_lock.LockIfNotInUse.assert_not_called()
 
     @mock.patch("acloud.create.local_image_local_instance.utils")
+    @mock.patch("acloud.create.local_image_local_instance.ota_tools")
     @mock.patch("acloud.create.local_image_local_instance.create_common")
     @mock.patch.object(local_image_local_instance.LocalImageLocalInstance,
                        "_LaunchCvd")
@@ -158,14 +166,18 @@
                        "PrepareLaunchCVDCmd")
     @mock.patch.object(instance, "GetLocalInstanceRuntimeDir")
     @mock.patch.object(instance, "GetLocalInstanceHomeDir")
-    def testCreateInstance(self, _mock_home_dir, _mock_runtime_dir,
+    def testCreateInstance(self, mock_home_dir, _mock_runtime_dir,
                            _mock_prepare_cmd, mock_launch_cvd,
-                           _mock_create_common, _mock_utils):
+                           _mock_create_common, mock_ota_tools, _mock_utils):
         """Test the report returned by _CreateInstance."""
         self.Patch(instance, "GetLocalInstanceName",
                    return_value="local-instance-1")
+        mock_home_dir.return_value = "/local-instance-1"
         artifact_paths = local_image_local_instance.ArtifactPaths(
-            "/image/path", "/host/bin/path")
+            "/image/path", "/host/bin/path", "/misc/info/path",
+            "/ota/tools/dir", "/system/image/path")
+        mock_ota_tools_object = mock.Mock()
+        mock_ota_tools.OtaTools.return_value = mock_ota_tools_object
         mock_avd_spec = mock.Mock(unlock_screen=False)
         local_ins = mock.Mock(
             adb_port=6520,
@@ -184,6 +196,9 @@
 
         self.assertEqual(report.data.get("devices"),
                          self._EXPECTED_DEVICES_IN_REPORT)
+        mock_ota_tools.OtaTools.assert_called_with("/ota/tools/dir")
+        mock_ota_tools_object.BuildSuperImage.assert_called_with(
+            "/local-instance-1/mixed_super.img", "/misc/info/path", mock.ANY)
 
         # Failure
         mock_launch_cvd.side_effect = errors.LaunchCVDFail("unit test")
@@ -224,6 +239,94 @@
                 [cvd_host_dir])
             self.assertEqual(path, cvd_host_dir)
 
+    @staticmethod
+    def _CreateEmptyFile(path):
+        os.makedirs(os.path.dirname(path), exist_ok=True)
+        with open(path, "w"):
+            pass
+
+    @mock.patch("acloud.create.local_image_local_instance.ota_tools")
+    def testGetImageArtifactsPath(self, mock_ota_tools):
+        """Test GetImageArtifactsPath without system image dir."""
+        with tempfile.TemporaryDirectory() as temp_dir:
+            image_dir = "/unit/test"
+            cvd_dir = os.path.join(temp_dir, "cvd-host_package")
+            self._CreateEmptyFile(os.path.join(cvd_dir, "bin", "launch_cvd"))
+
+            mock_avd_spec = mock.Mock(
+                local_image_dir=image_dir,
+                local_system_image_dir=None,
+                local_tool_dirs=[cvd_dir])
+
+            paths = self.local_image_local_instance.GetImageArtifactsPath(
+                mock_avd_spec)
+
+        mock_ota_tools.FindOtaTools.assert_not_called()
+        self.assertEqual(paths, (image_dir, cvd_dir, None, None, None))
+
+    @mock.patch("acloud.create.local_image_local_instance.ota_tools")
+    def testGetImageFromBuildEnvironment(self, mock_ota_tools):
+        """Test GetImageArtifactsPath with files in build environment."""
+        with tempfile.TemporaryDirectory() as temp_dir:
+            image_dir = os.path.join(temp_dir, "image")
+            cvd_dir = os.path.join(temp_dir, "cvd-host_package")
+            mock_ota_tools.FindOtaTools.return_value = cvd_dir
+            system_image_dir = os.path.join(temp_dir, "system_image")
+            system_image_path = os.path.join(system_image_dir, "system.img")
+            misc_info_path = os.path.join(image_dir, "misc_info.txt")
+            self._CreateEmptyFile(os.path.join(image_dir, "boot.img"))
+            self._CreateEmptyFile(os.path.join(cvd_dir, "bin", "launch_cvd"))
+            self._CreateEmptyFile(system_image_path)
+            self._CreateEmptyFile(misc_info_path)
+
+            mock_avd_spec = mock.Mock(
+                local_image_dir=image_dir,
+                local_system_image_dir=system_image_dir,
+                local_tool_dirs=[])
+
+            with mock.patch.dict("acloud.create.local_image_local_instance."
+                                 "os.environ",
+                                 {"ANDROID_SOONG_HOST_OUT": cvd_dir},
+                                 clear=True):
+                paths = self.local_image_local_instance.GetImageArtifactsPath(
+                    mock_avd_spec)
+
+        mock_ota_tools.FindOtaTools.assert_called_once()
+        self.assertEqual(paths,
+                         (image_dir, cvd_dir, misc_info_path, cvd_dir,
+                          system_image_path))
+
+    @mock.patch("acloud.create.local_image_local_instance.ota_tools")
+    def testGetImageFromTargetFiles(self, mock_ota_tools):
+        """Test GetImageArtifactsPath with extracted target files."""
+        ota_tools_dir = "/mock_ota_tools"
+        mock_ota_tools.FindOtaTools.return_value = ota_tools_dir
+
+        with tempfile.TemporaryDirectory() as temp_dir:
+            image_dir = os.path.join(temp_dir, "image")
+            cvd_dir = os.path.join(temp_dir, "cvd-host_package")
+            system_image_dir = os.path.join(temp_dir, "system_image")
+            system_image_path = os.path.join(system_image_dir, "system.img")
+            misc_info_path = os.path.join(image_dir, "META", "misc_info.txt")
+            self._CreateEmptyFile(os.path.join(image_dir, "IMAGES",
+                                               "boot.img"))
+            self._CreateEmptyFile(os.path.join(cvd_dir, "bin", "launch_cvd"))
+            self._CreateEmptyFile(system_image_path)
+            self._CreateEmptyFile(misc_info_path)
+
+            mock_avd_spec = mock.Mock(
+                local_image_dir=image_dir,
+                local_system_image_dir=system_image_dir,
+                local_tool_dirs=[ota_tools_dir, cvd_dir])
+
+            paths = self.local_image_local_instance.GetImageArtifactsPath(
+                mock_avd_spec)
+
+        mock_ota_tools.FindOtaTools.assert_called_once()
+        self.assertEqual(paths,
+                         (os.path.join(image_dir, "IMAGES"), cvd_dir,
+                          misc_info_path, ota_tools_dir, system_image_path))
+
     @mock.patch.object(utils, "CheckUserInGroups")
     def testPrepareLaunchCVDCmd(self, mock_usergroups):
         """test PrepareLaunchCVDCmd."""
@@ -234,7 +337,7 @@
 
         launch_cmd = self.local_image_local_instance.PrepareLaunchCVDCmd(
             constants.CMD_LAUNCH_CVD, hw_property, True, "fake_image_dir",
-            "fake_cvd_dir", False, True, None, None)
+            "fake_cvd_dir", False, True, None, None, None)
         self.assertEqual(launch_cmd, self.LAUNCH_CVD_CMD_WITH_DISK)
 
         # "disk" doesn't exist in hw_property.
@@ -242,24 +345,30 @@
                        "dpi": "fake", "memory": "fake"}
         launch_cmd = self.local_image_local_instance.PrepareLaunchCVDCmd(
             constants.CMD_LAUNCH_CVD, hw_property, True, "fake_image_dir",
-            "fake_cvd_dir", False, True, None, None)
+            "fake_cvd_dir", False, True, None, None, None)
         self.assertEqual(launch_cmd, self.LAUNCH_CVD_CMD_NO_DISK)
 
         # "gpu" is enabled with "default"
         launch_cmd = self.local_image_local_instance.PrepareLaunchCVDCmd(
             constants.CMD_LAUNCH_CVD, hw_property, True, "fake_image_dir",
-            "fake_cvd_dir", False, True, "default", None)
+            "fake_cvd_dir", False, True, "default", None, None)
         self.assertEqual(launch_cmd, self.LAUNCH_CVD_CMD_NO_DISK_WITH_GPU)
 
         launch_cmd = self.local_image_local_instance.PrepareLaunchCVDCmd(
             constants.CMD_LAUNCH_CVD, hw_property, True, "fake_image_dir",
-            "fake_cvd_dir", True, False, None, None)
+            "fake_cvd_dir", True, False, None, None, None)
         self.assertEqual(launch_cmd, self.LAUNCH_CVD_CMD_WITH_WEBRTC)
 
+        launch_cmd = self.local_image_local_instance.PrepareLaunchCVDCmd(
+            constants.CMD_LAUNCH_CVD, hw_property, True, "fake_image_dir",
+            "fake_cvd_dir", False, True, None, "fake_super_image", None)
+        self.assertEqual(launch_cmd, self.LAUNCH_CVD_CMD_WITH_SUPER_IMAGE)
+
         # Add args into launch command with "-setupwizard_mode=REQUIRED"
         launch_cmd = self.local_image_local_instance.PrepareLaunchCVDCmd(
             constants.CMD_LAUNCH_CVD, hw_property, True, "fake_image_dir",
-            "fake_cvd_dir", False, True, None, "-setupwizard_mode=REQUIRED")
+            "fake_cvd_dir", False, True, None, None,
+            "-setupwizard_mode=REQUIRED")
         self.assertEqual(launch_cmd, self.LAUNCH_CVD_CMD_WITH_ARGS)
 
     @mock.patch.object(utils, "GetUserAnswerYes")
diff --git a/create/remote_image_local_instance.py b/create/remote_image_local_instance.py
index caaf27b..5166efa 100644
--- a/create/remote_image_local_instance.py
+++ b/create/remote_image_local_instance.py
@@ -184,4 +184,6 @@
             raise errors.GetCvdLocalHostPackageError(
                 "No launch_cvd found. Please check downloaded artifacts dir: %s"
                 % image_dir)
+        # This method does not set the optional fields because launch_cvd loads
+        # the paths from the fetcher config in image_dir.
         return local_image_local_instance.ArtifactPaths(image_dir, image_dir)