Add --local-system_dlkm-image

system_dlkm has been a new requirement since Android 13. Generally it is
built from the kernel branches and in different formats. Cuttlefish
requires the flattened ext4 image. system_dlkm image has to be mixed
with cuttlefish images into a super image. The user has to provide OTA
tools and cuttlefish target_files zip as parameters to acloud.

Bug: 298075407
Test: acloud-dev create -vv --host 192.168.9.2 \
      --host-user vsoc-01 --host-ssh-private-key-path ~/id_rsa \
      --local-system_dlkm-image ~/system_dlkm.flatten.ext4.img \
      --local-boot-image ~/boot.img \
      --local-image ~/target_files.zip \
      --cvd-host-package ~/cvd-host_package.tar.gz \
      --local-tool ~/otatools
Change-Id: I17b9d7a1318f3357a4384d939a25d6ecf2e734b5
diff --git a/create/avd_spec.py b/create/avd_spec.py
index 386d96a..7f53ec8 100644
--- a/create/avd_spec.py
+++ b/create/avd_spec.py
@@ -123,6 +123,7 @@
         self._local_instance_dir = None
         self._local_kernel_image = None
         self._local_system_image = None
+        self._local_system_dlkm_image = None
         self._local_vendor_image = None
         self._local_tool_dirs = None
         self._image_download_dir = None
@@ -259,6 +260,10 @@
             self._local_system_image = self._GetLocalImagePath(
                 args.local_system_image)
 
+        if args.local_system_dlkm_image is not None:
+            self._local_system_dlkm_image = self._GetLocalImagePath(
+                args.local_system_dlkm_image)
+
         if args.local_vendor_image is not None:
             self._local_vendor_image = self._GetLocalImagePath(
                 args.local_vendor_image)
@@ -837,6 +842,11 @@
         return self._local_system_image
 
     @property
+    def local_system_dlkm_image(self):
+        """Return local system_dlkm image path."""
+        return self._local_system_dlkm_image
+
+    @property
     def local_vendor_image(self):
         """Return local vendor image path."""
         return self._local_vendor_image
diff --git a/create/avd_spec_test.py b/create/avd_spec_test.py
index 5f20e40..1b3e34c 100644
--- a/create/avd_spec_test.py
+++ b/create/avd_spec_test.py
@@ -121,22 +121,27 @@
         # Specified --local-*-image with dirs.
         self.args.local_kernel_image = expected_image_dir
         self.args.local_system_image = expected_image_dir
+        self.args.local_system_dlkm_image = expected_image_dir
         self.args.local_vendor_image = expected_image_dir
         self.AvdSpec._ProcessImageArgs(self.args)
         self.assertEqual(self.AvdSpec.local_kernel_image, expected_image_dir)
         self.assertEqual(self.AvdSpec.local_system_image, expected_image_dir)
+        self.assertEqual(self.AvdSpec.local_system_dlkm_image, expected_image_dir)
         self.assertEqual(self.AvdSpec.local_vendor_image, expected_image_dir)
 
         # Specified --local-*-image with files.
         self.args.local_kernel_image = expected_image_file
         self.args.local_system_image = expected_image_file
+        self.args.local_system_dlkm_image = expected_image_file
         self.AvdSpec._ProcessImageArgs(self.args)
         self.assertEqual(self.AvdSpec.local_kernel_image, expected_image_file)
         self.assertEqual(self.AvdSpec.local_system_image, expected_image_file)
+        self.assertEqual(self.AvdSpec.local_system_dlkm_image, expected_image_file)
 
         # Specified --local-*-image without args.
         self.args.local_kernel_image = constants.FIND_IN_BUILD_ENV
         self.args.local_system_image = constants.FIND_IN_BUILD_ENV
+        self.args.local_system_dlkm_image = constants.FIND_IN_BUILD_ENV
         self.args.local_vendor_image = constants.FIND_IN_BUILD_ENV
         with mock.patch("acloud.create.avd_spec.utils."
                         "GetBuildEnvironmentVariable",
@@ -144,6 +149,7 @@
             self.AvdSpec._ProcessImageArgs(self.args)
         self.assertEqual(self.AvdSpec.local_kernel_image, expected_image_dir)
         self.assertEqual(self.AvdSpec.local_system_image, expected_image_dir)
+        self.assertEqual(self.AvdSpec.local_system_dlkm_image, expected_image_dir)
         self.assertEqual(self.AvdSpec.local_vendor_image, expected_image_dir)
 
     def testProcessAutoconnect(self):
diff --git a/create/create_args.py b/create/create_args.py
index ba6e2a8..6ad1e96 100644
--- a/create/create_args.py
+++ b/create/create_args.py
@@ -591,6 +591,16 @@
         "e.g., --local-system-image, --local-system-image /path/to/dir, or "
         "--local-system-image /path/to/img")
     create_parser.add_argument(
+        "--local-system_dlkm-image",
+        const=constants.FIND_IN_BUILD_ENV,
+        type=str,
+        dest="local_system_dlkm_image",
+        nargs="?",
+        required=False,
+        help="`cuttlefish only` Use the locally built system_dlkm image for "
+        "the AVD. Look for the image in $ANDROID_PRODUCT_OUT if no args value "
+        "is provided.")
+    create_parser.add_argument(
         "--local-vendor-image",
         const=constants.FIND_IN_BUILD_ENV,
         type=str,
diff --git a/internal/lib/cvd_utils.py b/internal/lib/cvd_utils.py
index fe42685..7b71896 100644
--- a/internal/lib/cvd_utils.py
+++ b/internal/lib/cvd_utils.py
@@ -38,13 +38,10 @@
 
 # Local build artifacts to be uploaded.
 _ARTIFACT_FILES = ["*.img", "bootloader", "kernel"]
-# The boot image name pattern corresponds to the use cases:
-# - In a cuttlefish build environment, ANDROID_PRODUCT_OUT conatins boot.img
-#   and boot-debug.img. The former is the default boot image. The latter is not
-#   useful for cuttlefish.
-# - In an officially released GKI (Generic Kernel Image) package, the image
-#   name is boot-<kernel version>.img.
-_BOOT_IMAGE_NAME_PATTERN = r"boot(-[\d.]+)?\.img"
+_SYSTEM_DLKM_IMAGE_NAMES = (
+    "system_dlkm.flatten.ext4.img",  # GKI artifact
+    "system_dlkm.img",  # cuttlefish artifact
+)
 _VENDOR_BOOT_IMAGE_NAME = "vendor_boot.img"
 _KERNEL_IMAGE_NAMES = ("kernel", "bzImage", "Image")
 _INITRAMFS_IMAGE_NAME = "initramfs.img"
@@ -362,6 +359,32 @@
         f"{search_path} is not a boot image or a directory containing images.")
 
 
+def _FindSystemDlkmImage(search_path):
+    """Find system_dlkm image in a path.
+
+    Args:
+        search_path: A path to an image file or an image directory.
+
+    Returns:
+        The system_dlkm image path.
+
+    Raises:
+        errors.GetLocalImageError if search_path does not contain a
+        system_dlkm image.
+    """
+    if os.path.isfile(search_path):
+        return search_path
+
+    for name in _SYSTEM_DLKM_IMAGE_NAMES:
+        path = os.path.join(search_path, name)
+        if os.path.isfile(path):
+            return path
+
+    raise errors.GetLocalImageError(
+        f"{search_path} is not a system_dlkm image or a directory containing "
+        "images.")
+
+
 def _MixSuperImage(super_image_path, avd_spec, target_files_dir, ota):
     """Mix super image from device images and extra images.
 
@@ -378,6 +401,7 @@
     system_image_path = None
     system_ext_image_path = None
     product_image_path = None
+    system_dlkm_image_path = None
     vendor_image_path = None
     vendor_dlkm_image_path = None
     odm_image_path = None
@@ -390,6 +414,10 @@
             product_image_path,
         ) = create_common.FindSystemImages(avd_spec.local_system_image)
 
+    if avd_spec.local_system_dlkm_image:
+        system_dlkm_image_path = _FindSystemDlkmImage(
+            avd_spec.local_system_dlkm_image)
+
     if avd_spec.local_vendor_image:
         (
             vendor_image_path,
@@ -402,6 +430,7 @@
                       system_image=system_image_path,
                       system_ext_image=system_ext_image_path,
                       product_image=product_image_path,
+                      system_dlkm_image=system_dlkm_image_path,
                       vendor_image=vendor_image_path,
                       vendor_dlkm_image=vendor_dlkm_image_path,
                       odm_image=odm_image_path,
@@ -428,7 +457,8 @@
 
 def AreTargetFilesRequired(avd_spec):
     """Return whether UploadExtraImages requires target_files_dir."""
-    return bool(avd_spec.local_system_image or avd_spec.local_vendor_image)
+    return bool(avd_spec.local_system_image or avd_spec.local_vendor_image or
+                avd_spec.local_system_dlkm_image)
 
 
 def UploadExtraImages(ssh_obj, remote_dir, avd_spec, target_files_dir):
@@ -461,7 +491,8 @@
     if AreTargetFilesRequired(avd_spec):
         if not target_files_dir:
             raise ValueError("target_files_dir is required when avd_spec has "
-                             "local system image or local vendor image.")
+                             "local system image, local system_dlkm image, or "
+                             "local vendor image.")
         ota = ota_tools.FindOtaTools(
             avd_spec.local_tool_dirs + create_common.GetNonEmptyEnvVars(
                 constants.ENV_ANDROID_SOONG_HOST_OUT,
diff --git a/internal/lib/cvd_utils_test.py b/internal/lib/cvd_utils_test.py
index dccfd47..505f144 100644
--- a/internal/lib/cvd_utils_test.py
+++ b/internal/lib/cvd_utils_test.py
@@ -161,6 +161,7 @@
 
             mock_avd_spec = mock.Mock(local_kernel_image="boot.img",
                                       local_system_image=None,
+                                      local_system_dlkm_image=None,
                                       local_vendor_image=None)
             args = cvd_utils.UploadExtraImages(mock_ssh, "dir", mock_avd_spec,
                                                None)
@@ -192,6 +193,7 @@
 
             mock_avd_spec = mock.Mock(local_kernel_image=kernel_image_path,
                                       local_system_image=None,
+                                      local_system_dlkm_image=None,
                                       local_vendor_image=None)
             with self.assertRaises(errors.GetLocalImageError):
                 cvd_utils.UploadExtraImages(mock_ssh, "dir", mock_avd_spec,
@@ -222,14 +224,15 @@
             extra_image_dir = os.path.join(temp_dir, "extra")
             mock_avd_spec = mock.Mock(local_kernel_image=None,
                                       local_system_image=extra_image_dir,
+                                      local_system_dlkm_image=extra_image_dir,
                                       local_vendor_image=extra_image_dir,
                                       local_tool_dirs=[])
             self.CreateFile(
                 os.path.join(target_files_dir, "IMAGES", "boot.img"))
             self.CreateFile(
                 os.path.join(target_files_dir, "META", "misc_info.txt"))
-            for image_name in ["system.img", "vendor.img", "vendor_dlkm.img",
-                               "odm.img", "odm_dlkm.img"]:
+            for image_name in ["system.img", "system_dlkm.img", "vendor.img",
+                               "vendor_dlkm.img", "odm.img", "odm_dlkm.img"]:
                 self.CreateFile(os.path.join(extra_image_dir, image_name))
             args = cvd_utils.UploadExtraImages(mock_ssh, "dir", mock_avd_spec,
                                                target_files_dir)
@@ -246,7 +249,16 @@
         self.assertEqual(1, len(upload_args))
         self.assertIn(" super.img", upload_args[0])
         self.assertIn("dir/acloud_image", upload_args[0])
-        mock_ota_tools_object.MixSuperImage.assert_called_once()
+        mock_ota_tools_object.MixSuperImage.assert_called_once_with(
+            mock.ANY, mock.ANY, os.path.join(target_files_dir, "IMAGES"),
+            system_image=os.path.join(extra_image_dir, "system.img"),
+            system_ext_image=None,
+            product_image=None,
+            system_dlkm_image=os.path.join(extra_image_dir, "system_dlkm.img"),
+            vendor_image=os.path.join(extra_image_dir, "vendor.img"),
+            vendor_dlkm_image=os.path.join(extra_image_dir, "vendor_dlkm.img"),
+            odm_image=os.path.join(extra_image_dir, "odm.img"),
+            odm_dlkm_image=os.path.join(extra_image_dir, "odm_dlkm.img"))
         # vbmeta image
         mock_ota_tools_object.MakeDisabledVbmetaImage.assert_called_once()
         mock_ssh.ScpPushFile.assert_called_once_with(
diff --git a/internal/lib/ota_tools.py b/internal/lib/ota_tools.py
index 8863623..5048770 100644
--- a/internal/lib/ota_tools.py
+++ b/internal/lib/ota_tools.py
@@ -301,9 +301,9 @@
 
     def MixSuperImage(self, super_image, misc_info, image_dir,
                       system_image=None, system_ext_image=None,
-                      product_image=None, vendor_image=None,
-                      vendor_dlkm_image=None, odm_image=None,
-                      odm_dlkm_image=None):
+                      product_image=None, system_dlkm_image=None,
+                      vendor_image=None, vendor_dlkm_image=None,
+                      odm_image=None, odm_dlkm_image=None):
         """Create mixed super image from device images and given partition
         images.
 
@@ -314,6 +314,7 @@
             system_image: Path to the system image.
             system_ext_image: Path to the system_ext image.
             product_image: Path to the product image.
+            system_dlkm_image: Path to the system_dlkm image.
             vendor_image: Path to the vendor image.
             vendor_dlkm_image: Path to the vendor_dlkm image.
             odm_image: Path to the odm image.
@@ -326,6 +327,7 @@
                 system=system_image,
                 system_ext=system_ext_image,
                 product=product_image,
+                system_dlkm=system_dlkm_image,
                 vendor=vendor_image,
                 vendor_dlkm=vendor_dlkm_image,
                 odm=odm_image,