Single-image boot image extensions.

Allow boot image extensions to be compiled into a single
art/oat/vdex triplet for any number of input dex files.
Support argument --image-fd so that this can be done with
file descriptors. This is intended for creating a boot
image extension in memory when starting zygote.

Remove the possibility to specify multiple oat or image
files. It was unused and untested.

Test: New test in dex2oat_image_test
Test: m test-art-host-gtest
Test: testrunner.py --host --optimizing
Change-Id: I78e5410642a5df14c0891d87f27eefcb02156764
diff --git a/dex2oat/dex2oat.cc b/dex2oat/dex2oat.cc
index e23bedf..345a8a0 100644
--- a/dex2oat/dex2oat.cc
+++ b/dex2oat/dex2oat.cc
@@ -251,6 +251,9 @@
   UsageError("  --oat-file=<file.oat>: specifies an oat output destination via a filename.");
   UsageError("      Example: --oat-file=/system/framework/boot.oat");
   UsageError("");
+  UsageError("  --oat-symbols=<file.oat>: specifies a symbolized oat output destination.");
+  UsageError("      Example: --oat-file=symbols/system/framework/boot.oat");
+  UsageError("");
   UsageError("  --oat-fd=<number>: specifies the oat output destination via a file descriptor.");
   UsageError("      Example: --oat-fd=6");
   UsageError("");
@@ -278,6 +281,9 @@
   UsageError("  --image=<file.art>: specifies an output image filename.");
   UsageError("      Example: --image=/system/framework/boot.art");
   UsageError("");
+  UsageError("  --image-fd=<number>: same as --image but accepts a file descriptor instead.");
+  UsageError("      Cannot be used together with --image.");
+  UsageError("");
   UsageError("  --image-format=(uncompressed|lz4|lz4hc):");
   UsageError("      Which format to store the image.");
   UsageError("      Example: --image-format=lz4");
@@ -430,7 +436,11 @@
   UsageError("  --app-image-file=<file-name>: specify a file name for app image.");
   UsageError("      Example: --app-image-file=/data/dalvik-cache/system@app@Calculator.apk.art");
   UsageError("");
-  UsageError("  --multi-image: obsolete, ignored");
+  UsageError("  --multi-image: specify that separate oat and image files be generated for ");
+  UsageError("      each input dex file; the default for boot image and boot image extension.");
+  UsageError("");
+  UsageError("  --single-image: specify that a single oat and image file be generated for ");
+  UsageError("      all input dex files; the default for app image.");
   UsageError("");
   UsageError("  --force-determinism: force the compiler to emit a deterministic output.");
   UsageError("");
@@ -706,6 +716,9 @@
       input_vdex_file_(nullptr),
       dm_fd_(-1),
       zip_fd_(-1),
+      image_fd_(-1),
+      have_multi_image_arg_(false),
+      multi_image_(false),
       image_base_(0U),
       image_storage_mode_(ImageHeader::kStorageModeUncompressed),
       passes_to_run_filename_(nullptr),
@@ -814,7 +827,7 @@
     }
 
     DCHECK(compiler_options_->image_type_ == CompilerOptions::ImageType::kNone);
-    if (!image_filenames_.empty()) {
+    if (!image_filenames_.empty() || image_fd_ != -1) {
       if (!boot_image_filename_.empty()) {
         compiler_options_->image_type_ = CompilerOptions::ImageType::kBootImageExtension;
       } else if (android::base::EndsWith(image_filenames_[0], "apex.art")) {
@@ -825,11 +838,15 @@
     }
     if (app_image_fd_ != -1 || !app_image_file_name_.empty()) {
       if (compiler_options_->IsBootImage() || compiler_options_->IsBootImageExtension()) {
-        Usage("Can't have both --image and (--app-image-fd or --app-image-file)");
+        Usage("Can't have both (--image or --image-fd) and (--app-image-fd or --app-image-file)");
       }
       compiler_options_->image_type_ = CompilerOptions::ImageType::kAppImage;
     }
 
+    if (!image_filenames_.empty() && image_fd_ != -1) {
+      Usage("Can't have both --image and --image-fd");
+    }
+
     if (oat_filenames_.empty() && oat_fd_ == -1) {
       Usage("Output must be supplied with either --oat-file or --oat-fd");
     }
@@ -851,6 +868,10 @@
             "or with --oat-fd and --output-vdex-fd file descriptors");
     }
 
+    if ((image_fd_ != -1) && (oat_fd_ == -1)) {
+      Usage("--image-fd must be used with --oat_fd and --output_vdex_fd");
+    }
+
     if (!parser_options->oat_symbols.empty() && oat_fd_ != -1) {
       Usage("--oat-symbols should not be used with --oat-fd");
     }
@@ -924,6 +945,25 @@
       }
     }
 
+    if (have_multi_image_arg_) {
+      if (!IsImage()) {
+        Usage("--multi-image or --single-image specified for non-image compilation");
+      }
+    } else {
+      // Use the default, i.e. multi-image for boot image and boot image extension.
+      multi_image_ = IsBootImage() || IsBootImageExtension();  // Shall pass checks below.
+    }
+    if (IsBootImage() && !multi_image_) {
+      Usage("--single-image specified for primary boot image");
+    }
+    if (IsAppImage() && multi_image_) {
+      Usage("--multi-image specified for app image");
+    }
+
+    if (image_fd_ != -1 && multi_image_) {
+      Usage("--single-image not specified for --image-fd");
+    }
+
     const bool have_profile_file = !profile_file_.empty();
     const bool have_profile_fd = profile_file_fd_ != kInvalidFd;
     if (have_profile_file && have_profile_fd) {
@@ -1015,24 +1055,36 @@
   }
 
   void ExpandOatAndImageFilenames() {
-    if (image_filenames_[0].rfind('/') == std::string::npos) {
-      Usage("Unusable boot image filename %s", image_filenames_[0].c_str());
+    ArrayRef<const std::string> locations(dex_locations_);
+    if (!multi_image_) {
+      locations = locations.SubArray(/*pos=*/ 0u, /*length=*/ 1u);
     }
-    image_filenames_ = ImageSpace::ExpandMultiImageLocations(
-        ArrayRef<const std::string>(dex_locations_), image_filenames_[0], IsBootImageExtension());
+    if (image_fd_ == -1) {
+      if (image_filenames_[0].rfind('/') == std::string::npos) {
+        Usage("Unusable boot image filename %s", image_filenames_[0].c_str());
+      }
+      image_filenames_ = ImageSpace::ExpandMultiImageLocations(
+          locations, image_filenames_[0], IsBootImageExtension());
 
-    if (oat_filenames_[0].rfind('/') == std::string::npos) {
-      Usage("Unusable boot image oat filename %s", oat_filenames_[0].c_str());
+      if (oat_filenames_[0].rfind('/') == std::string::npos) {
+        Usage("Unusable boot image oat filename %s", oat_filenames_[0].c_str());
+      }
+      oat_filenames_ = ImageSpace::ExpandMultiImageLocations(
+          locations, oat_filenames_[0], IsBootImageExtension());
+    } else {
+      DCHECK(!multi_image_);
+      std::vector<std::string> oat_locations = ImageSpace::ExpandMultiImageLocations(
+          locations, oat_location_, IsBootImageExtension());
+      DCHECK_EQ(1u, oat_locations.size());
+      oat_location_ = oat_locations[0];
     }
-    oat_filenames_ = ImageSpace::ExpandMultiImageLocations(
-        ArrayRef<const std::string>(dex_locations_), oat_filenames_[0], IsBootImageExtension());
 
     if (!oat_unstripped_.empty()) {
       if (oat_unstripped_[0].rfind('/') == std::string::npos) {
         Usage("Unusable boot image symbol filename %s", oat_unstripped_[0].c_str());
       }
       oat_unstripped_ = ImageSpace::ExpandMultiImageLocations(
-           ArrayRef<const std::string>(dex_locations_), oat_unstripped_[0], IsBootImageExtension());
+           locations, oat_unstripped_[0], IsBootImageExtension());
     }
   }
 
@@ -1111,6 +1163,15 @@
     }
   }
 
+  void AssignIfExists(Dex2oatArgumentMap& map,
+                      const Dex2oatArgumentMap::Key<std::string>& key,
+                      std::vector<std::string>* out) {
+    DCHECK(out->empty());
+    if (map.Exists(key)) {
+      out->push_back(*map.Get(key));
+    }
+  }
+
   // Parse the arguments from the command line. In case of an unrecognized option or impossible
   // values/combinations, a usage error will be displayed and exit() is called. Thus, if the method
   // returns, arguments have been successfully parsed.
@@ -1138,10 +1199,11 @@
     AssignIfExists(args, M::CompactDexLevel, &compact_dex_level_);
     AssignIfExists(args, M::DexFiles, &dex_filenames_);
     AssignIfExists(args, M::DexLocations, &dex_locations_);
-    AssignIfExists(args, M::OatFiles, &oat_filenames_);
+    AssignIfExists(args, M::OatFile, &oat_filenames_);
     AssignIfExists(args, M::OatSymbols, &parser_options->oat_symbols);
     AssignTrueIfExists(args, M::Strip, &strip_);
-    AssignIfExists(args, M::ImageFilenames, &image_filenames_);
+    AssignIfExists(args, M::ImageFilename, &image_filenames_);
+    AssignIfExists(args, M::ImageFd, &image_fd_);
     AssignIfExists(args, M::ZipFd, &zip_fd_);
     AssignIfExists(args, M::ZipLocation, &zip_location_);
     AssignIfExists(args, M::InputVdexFd, &input_vdex_fd_);
@@ -1198,6 +1260,9 @@
     }
     AssignIfExists(args, M::CopyDexFiles, &copy_dex_files_);
 
+    AssignTrueIfExists(args, M::MultiImage, &have_multi_image_arg_);
+    AssignIfExists(args, M::MultiImage, &multi_image_);
+
     if (args.Exists(M::ForceDeterminism)) {
       force_determinism_ = true;
     }
@@ -1266,8 +1331,9 @@
     // Prune non-existent dex files now so that we don't create empty oat files for multi-image.
     PruneNonExistentDexFiles();
 
-    // Expand oat and image filenames for multi image.
-    if ((IsBootImage() || IsBootImageExtension()) && image_filenames_.size() == 1) {
+    // Expand oat and image filenames for boot image and boot image extension.
+    // This is mostly for multi-image but single-image also needs some processing.
+    if (IsBootImage() || IsBootImageExtension()) {
       ExpandOatAndImageFilenames();
     }
 
@@ -2612,13 +2678,21 @@
   bool CreateImageFile()
       REQUIRES(!Locks::mutator_lock_) {
     CHECK(image_writer_ != nullptr);
-    if (!IsBootImage() && !IsBootImageExtension()) {
-      CHECK(image_filenames_.empty());
-      image_filenames_.push_back(app_image_file_name_);
+    if (IsAppImage()) {
+      DCHECK(image_filenames_.empty());
+      if (app_image_fd_ != -1) {
+        image_filenames_.push_back(StringPrintf("FileDescriptor[%d]", app_image_fd_));
+      } else {
+        image_filenames_.push_back(app_image_file_name_);
+      }
     }
-    if (!image_writer_->Write(app_image_fd_,
+    if (image_fd_ != -1) {
+      DCHECK(image_filenames_.empty());
+      image_filenames_.push_back(StringPrintf("FileDescriptor[%d]", image_fd_));
+    }
+    if (!image_writer_->Write(IsAppImage() ? app_image_fd_ : image_fd_,
                               image_filenames_,
-                              oat_filenames_)) {
+                              IsAppImage() ? 1u : dex_locations_.size())) {
       LOG(ERROR) << "Failure during image file creation";
       return false;
     }
@@ -2781,6 +2855,9 @@
   std::string boot_image_filename_;
   std::vector<const char*> runtime_args_;
   std::vector<std::string> image_filenames_;
+  int image_fd_;
+  bool have_multi_image_arg_;
+  bool multi_image_;
   uintptr_t image_base_;
   ImageHeader::StorageMode image_storage_mode_;
   const char* passes_to_run_filename_;
diff --git a/dex2oat/dex2oat_image_test.cc b/dex2oat/dex2oat_image_test.cc
index f03f02c..a80db55 100644
--- a/dex2oat/dex2oat_image_test.cc
+++ b/dex2oat/dex2oat_image_test.cc
@@ -198,7 +198,8 @@
   bool CompileBootImage(const std::vector<std::string>& extra_args,
                         const std::string& image_file_name_prefix,
                         ArrayRef<const std::string> dex_files,
-                        std::string* error_msg) {
+                        std::string* error_msg,
+                        const std::string& use_fd_prefix = "") {
     Runtime* const runtime = Runtime::Current();
     std::vector<std::string> argv;
     argv.push_back(runtime->GetCompilerExecutable());
@@ -219,9 +220,22 @@
       argv.push_back("--host");
     }
 
-    argv.push_back("--image=" + image_file_name_prefix + ".art");
-    argv.push_back("--oat-file=" + image_file_name_prefix + ".oat");
-    argv.push_back("--oat-location=" + image_file_name_prefix + ".oat");
+    std::unique_ptr<File> art_file;
+    std::unique_ptr<File> vdex_file;
+    std::unique_ptr<File> oat_file;
+    if (!use_fd_prefix.empty()) {
+      art_file.reset(OS::CreateEmptyFile((use_fd_prefix + ".art").c_str()));
+      vdex_file.reset(OS::CreateEmptyFile((use_fd_prefix + ".vdex").c_str()));
+      oat_file.reset(OS::CreateEmptyFile((use_fd_prefix + ".oat").c_str()));
+      argv.push_back("--image-fd=" + std::to_string(art_file->Fd()));
+      argv.push_back("--output-vdex-fd=" + std::to_string(vdex_file->Fd()));
+      argv.push_back("--oat-fd=" + std::to_string(oat_file->Fd()));
+      argv.push_back("--oat-location=" + image_file_name_prefix + ".oat");
+    } else {
+      argv.push_back("--image=" + image_file_name_prefix + ".art");
+      argv.push_back("--oat-file=" + image_file_name_prefix + ".oat");
+      argv.push_back("--oat-location=" + image_file_name_prefix + ".oat");
+    }
 
     std::vector<std::string> compiler_options = runtime->GetCompilerOptions();
     argv.insert(argv.end(), compiler_options.begin(), compiler_options.end());
@@ -232,7 +246,17 @@
     argv.push_back("--android-root=" + std::string(android_root));
     argv.insert(argv.end(), extra_args.begin(), extra_args.end());
 
-    return RunDex2Oat(argv, error_msg);
+    bool result = RunDex2Oat(argv, error_msg);
+    if (art_file != nullptr) {
+      CHECK_EQ(0, art_file->FlushClose());
+    }
+    if (vdex_file != nullptr) {
+      CHECK_EQ(0, vdex_file->FlushClose());
+    }
+    if (oat_file != nullptr) {
+      CHECK_EQ(0, oat_file->FlushClose());
+    }
+    return result;
   }
 
   bool RunDex2Oat(const std::vector<std::string>& args, std::string* error_msg) {
@@ -277,6 +301,20 @@
       dex_file = new_location;
     }
   }
+
+  bool CompareFiles(const std::string& filename1, const std::string& filename2) {
+    std::unique_ptr<File> file1(OS::OpenFileForReading(filename1.c_str()));
+    std::unique_ptr<File> file2(OS::OpenFileForReading(filename2.c_str()));
+    // Did we open the files?
+    if (file1 == nullptr || file2 == nullptr) {
+      return false;
+    }
+    // Are they non-empty and the same length?
+    if (file1->GetLength() <= 0 || file2->GetLength() != file1->GetLength()) {
+      return false;
+    }
+    return file1->Compare(file2.get()) == 0;
+  }
 };
 
 TEST_F(Dex2oatImageTest, TestModesAndFilters) {
@@ -601,4 +639,142 @@
   ASSERT_EQ(0, rmdir_result);
 }
 
+TEST_F(Dex2oatImageTest, TestExtensionSingleImage) {
+  std::string error_msg;
+  MemMap reservation = ReserveCoreImageAddressSpace(&error_msg);
+  ASSERT_TRUE(reservation.IsValid()) << error_msg;
+
+  ScratchFile scratch;
+  std::string scratch_dir = scratch.GetFilename() + "-d";
+  int mkdir_result = mkdir(scratch_dir.c_str(), 0700);
+  ASSERT_EQ(0, mkdir_result);
+  scratch_dir += '/';
+  std::string image_dir = scratch_dir + GetInstructionSetString(kRuntimeISA);
+  mkdir_result = mkdir(image_dir.c_str(), 0700);
+  ASSERT_EQ(0, mkdir_result);
+  std::string filename_prefix = image_dir + "/core";
+
+  // Copy the libcore dex files to a custom dir inside `scratch_dir` so that we do not
+  // accidentally load pre-compiled core images from their original directory based on BCP paths.
+  std::string jar_dir = scratch_dir + "jars";
+  mkdir_result = mkdir(jar_dir.c_str(), 0700);
+  ASSERT_EQ(0, mkdir_result);
+  jar_dir += '/';
+  std::vector<std::string> libcore_dex_files = GetLibCoreDexFileNames();
+  CopyDexFiles(jar_dir, &libcore_dex_files);
+
+  // Create a smaller profile compared to the TestExtension test.
+  ScratchFile profile_file;
+  GenerateProfile(libcore_dex_files,
+                  profile_file.GetFile(),
+                  /*method_frequency=*/ 5u,
+                  /*type_frequency=*/ 4u);
+  std::vector<std::string> extra_args;
+  extra_args.push_back("--profile-file=" + profile_file.GetFilename());
+
+  ArrayRef<const std::string> full_bcp(libcore_dex_files);
+  size_t total_dex_files = full_bcp.size();
+  ASSERT_GE(total_dex_files, 5u);  // 3 for "head", at least 2 for "tail".
+
+  // The primary image must contain at least core-oj and core-libart to initialize the runtime
+  // and we also need the core-icu4j if we want to compile these with full profile.
+  ASSERT_NE(std::string::npos, full_bcp[0].find("core-oj"));
+  ASSERT_NE(std::string::npos, full_bcp[1].find("core-libart"));
+  ASSERT_NE(std::string::npos, full_bcp[2].find("core-icu4j"));
+  ArrayRef<const std::string> head_dex_files = full_bcp.SubArray(/*pos=*/ 0u, /*length=*/ 3u);
+  ArrayRef<const std::string> tail_dex_files = full_bcp.SubArray(/*pos=*/ 3u);
+
+  // Prepare the "head" and "tail" names and locations.
+  std::string base_name = "core.art";
+  std::string base_location = scratch_dir + base_name;
+  CHECK_GE(tail_dex_files.size(), 2u);
+  std::vector<std::string> expanded_tail = gc::space::ImageSpace::ExpandMultiImageLocations(
+      tail_dex_files.SubArray(/*pos=*/ 0u, /*length=*/ 1u),
+      base_location,
+      /*boot_image_extension=*/ true);
+  CHECK_EQ(1u, expanded_tail.size());
+  std::string tail_location = expanded_tail[0];
+  size_t tail_slash_pos = tail_location.rfind('/');
+  ASSERT_NE(std::string::npos, tail_slash_pos);
+  std::string tail_name = tail_location.substr(tail_slash_pos + 1u);
+
+  // Compile the "head", i.e. the primary boot image.
+  extra_args.push_back(android::base::StringPrintf("--base=0x%08x", kBaseAddress));
+  bool head_ok = CompileBootImage(extra_args, filename_prefix, head_dex_files, &error_msg);
+  ASSERT_TRUE(head_ok) << error_msg;
+  extra_args.pop_back();
+
+  // Compile the "tail" against the primary boot image.
+  std::string full_bcp_string = android::base::Join(full_bcp, ':');
+  AddRuntimeArg(extra_args, "-Xbootclasspath:" + full_bcp_string);
+  AddRuntimeArg(extra_args, "-Xbootclasspath-locations:" + full_bcp_string);
+  extra_args.push_back("--boot-image=" + base_location);
+  extra_args.push_back("--single-image");
+  extra_args.push_back("--avoid-storing-invocation");  // For comparison below.
+  error_msg.clear();
+  bool tail_ok = CompileBootImage(extra_args, filename_prefix, tail_dex_files, &error_msg);
+  ASSERT_TRUE(tail_ok) << error_msg;
+
+  reservation = MemMap::Invalid();  // Free the reserved memory for loading images.
+
+  // Try to load the boot image with different image locations.
+  std::vector<std::string> boot_class_path = libcore_dex_files;
+  std::vector<std::unique_ptr<gc::space::ImageSpace>> boot_image_spaces;
+  bool relocate = false;
+  MemMap extra_reservation;
+  auto load = [&](const std::string& image_location) {
+    boot_image_spaces.clear();
+    extra_reservation = MemMap::Invalid();
+    ScopedObjectAccess soa(Thread::Current());
+    return gc::space::ImageSpace::LoadBootImage(/*boot_class_path=*/ boot_class_path,
+                                                /*boot_class_path_locations=*/ libcore_dex_files,
+                                                image_location,
+                                                kRuntimeISA,
+                                                gc::space::ImageSpaceLoadingOrder::kSystemFirst,
+                                                relocate,
+                                                /*executable=*/ true,
+                                                /*is_zygote=*/ false,
+                                                /*extra_reservation_size=*/ 0u,
+                                                &boot_image_spaces,
+                                                &extra_reservation);
+  };
+
+  for (bool r : { false, true }) {
+    relocate = r;
+
+    // Load the primary and first extension with full path.
+    bool load_ok = load(base_location + ':' + tail_location);
+    ASSERT_TRUE(load_ok) << error_msg;
+    ASSERT_EQ(head_dex_files.size() + 1u, boot_image_spaces.size());
+
+    // Load the primary with full path and extension with a specified search path.
+    load_ok = load(base_location + ':' + scratch_dir + '*');
+    ASSERT_TRUE(load_ok) << error_msg;
+    ASSERT_EQ(head_dex_files.size() + 1u, boot_image_spaces.size());
+  }
+
+  // Recompile using file descriptors and compare contents.
+  std::vector<std::string> expanded_oat_filename = gc::space::ImageSpace::ExpandMultiImageLocations(
+      tail_dex_files.SubArray(/*pos=*/ 0u, /*length=*/ 1u),
+      filename_prefix,
+      /*boot_image_extension=*/ true);
+  CHECK_EQ(1u, expanded_oat_filename.size());
+  std::string extension_filename_prefix = expanded_oat_filename[0];
+  std::string filename_prefix2 = extension_filename_prefix + "2";
+  error_msg.clear();
+  tail_ok = CompileBootImage(extra_args,
+                             filename_prefix,
+                             tail_dex_files,
+                             &error_msg,
+                             /*use_fd_prefix=*/ filename_prefix2);
+  ASSERT_TRUE(tail_ok) << error_msg;
+  EXPECT_TRUE(CompareFiles(extension_filename_prefix + ".art", filename_prefix2 + ".art"));
+  EXPECT_TRUE(CompareFiles(extension_filename_prefix + ".vdex", filename_prefix2 + ".vdex"));
+  EXPECT_TRUE(CompareFiles(extension_filename_prefix + ".oat", filename_prefix2 + ".oat"));
+
+  ClearDirectory(scratch_dir.c_str());
+  int rmdir_result = rmdir(scratch_dir.c_str());
+  ASSERT_EQ(0, rmdir_result);
+}
+
 }  // namespace art
diff --git a/dex2oat/dex2oat_options.cc b/dex2oat/dex2oat_options.cc
index 29a288d..58944ff 100644
--- a/dex2oat/dex2oat_options.cc
+++ b/dex2oat/dex2oat_options.cc
@@ -93,10 +93,10 @@
           .WithType<std::string>()
           .IntoKey(M::DmFile)
       .Define("--oat-file=_")
-          .WithType<std::vector<std::string>>().AppendValues()
-          .IntoKey(M::OatFiles)
+          .WithType<std::string>()
+          .IntoKey(M::OatFile)
       .Define("--oat-symbols=_")
-          .WithType<std::vector<std::string>>().AppendValues()
+          .WithType<std::string>()
           .IntoKey(M::OatSymbols)
       .Define("--strip")
           .IntoKey(M::Strip)
@@ -111,8 +111,11 @@
 static void AddImageMappings(Builder& builder) {
   builder.
       Define("--image=_")
-          .WithType<std::vector<std::string>>().AppendValues()
-          .IntoKey(M::ImageFilenames)
+          .WithType<std::string>()
+          .IntoKey(M::ImageFilename)
+      .Define("--image-fd=_")
+          .WithType<int>()
+          .IntoKey(M::ImageFd)
       .Define("--base=_")
           .WithType<std::string>()
           .IntoKey(M::Base)
@@ -122,7 +125,8 @@
       .Define("--app-image-fd=_")
           .WithType<int>()
           .IntoKey(M::AppImageFileFd)
-      .Define("--multi-image")
+      .Define({"--multi-image", "--single-image"})
+          .WithValues({true, false})
           .IntoKey(M::MultiImage)
       .Define("--dirty-image-objects=_")
           .WithType<std::string>()
diff --git a/dex2oat/dex2oat_options.def b/dex2oat/dex2oat_options.def
index ad28f3a..f48806c 100644
--- a/dex2oat/dex2oat_options.def
+++ b/dex2oat/dex2oat_options.def
@@ -45,8 +45,8 @@
 DEX2OAT_OPTIONS_KEY (std::string,                    OutputVdex)
 DEX2OAT_OPTIONS_KEY (int,                            DmFd)
 DEX2OAT_OPTIONS_KEY (std::string,                    DmFile)
-DEX2OAT_OPTIONS_KEY (std::vector<std::string>,       OatFiles)
-DEX2OAT_OPTIONS_KEY (std::vector<std::string>,       OatSymbols)
+DEX2OAT_OPTIONS_KEY (std::string,                    OatFile)
+DEX2OAT_OPTIONS_KEY (std::string,                    OatSymbols)
 DEX2OAT_OPTIONS_KEY (Unit,                           Strip)
 DEX2OAT_OPTIONS_KEY (int,                            OatFd)
 DEX2OAT_OPTIONS_KEY (std::string,                    OatLocation)
@@ -54,7 +54,8 @@
 DEX2OAT_OPTIONS_KEY (int,                            WatchdogTimeout)
 DEX2OAT_OPTIONS_KEY (unsigned int,                   Threads)
 DEX2OAT_OPTIONS_KEY (std::vector<std::int32_t>,      CpuSet)
-DEX2OAT_OPTIONS_KEY (std::vector<std::string>,       ImageFilenames)
+DEX2OAT_OPTIONS_KEY (std::string,                    ImageFilename)
+DEX2OAT_OPTIONS_KEY (int,                            ImageFd)
 DEX2OAT_OPTIONS_KEY (ImageHeader::StorageMode,       ImageFormat)
 DEX2OAT_OPTIONS_KEY (std::string,                    Passes)
 DEX2OAT_OPTIONS_KEY (std::string,                    Base)  // TODO: Hex string parsing.
@@ -79,7 +80,7 @@
 DEX2OAT_OPTIONS_KEY (unsigned int,                   VeryLargeAppThreshold)
 DEX2OAT_OPTIONS_KEY (std::string,                    AppImageFile)
 DEX2OAT_OPTIONS_KEY (int,                            AppImageFileFd)
-DEX2OAT_OPTIONS_KEY (Unit,                           MultiImage)
+DEX2OAT_OPTIONS_KEY (bool,                           MultiImage)
 DEX2OAT_OPTIONS_KEY (std::string,                    NoInlineFrom)
 DEX2OAT_OPTIONS_KEY (Unit,                           ForceDeterminism)
 DEX2OAT_OPTIONS_KEY (std::string,                    ClasspathDir)
diff --git a/dex2oat/linker/image_test.h b/dex2oat/linker/image_test.h
index 69e144c..ef296fc 100644
--- a/dex2oat/linker/image_test.h
+++ b/dex2oat/linker/image_test.h
@@ -345,7 +345,7 @@
 
     bool success_image = writer->Write(kInvalidFd,
                                        image_filenames,
-                                       oat_filenames);
+                                       image_filenames.size());
     ASSERT_TRUE(success_image);
 
     for (size_t i = 0, size = oat_filenames.size(); i != size; ++i) {
diff --git a/dex2oat/linker/image_writer.cc b/dex2oat/linker/image_writer.cc
index 3d998c9..0facc85 100644
--- a/dex2oat/linker/image_writer.cc
+++ b/dex2oat/linker/image_writer.cc
@@ -401,21 +401,21 @@
 
 bool ImageWriter::Write(int image_fd,
                         const std::vector<std::string>& image_filenames,
-                        const std::vector<std::string>& oat_filenames) {
+                        size_t component_count) {
   // If image_fd or oat_fd are not kInvalidFd then we may have empty strings in image_filenames or
   // oat_filenames.
   CHECK(!image_filenames.empty());
   if (image_fd != kInvalidFd) {
     CHECK_EQ(image_filenames.size(), 1u);
   }
-  CHECK(!oat_filenames.empty());
-  CHECK_EQ(image_filenames.size(), oat_filenames.size());
+  DCHECK(!oat_filenames_.empty());
+  CHECK_EQ(image_filenames.size(), oat_filenames_.size());
 
   Thread* const self = Thread::Current();
   {
     ScopedObjectAccess soa(self);
-    for (size_t i = 0; i < oat_filenames.size(); ++i) {
-      CreateHeader(i);
+    for (size_t i = 0; i < oat_filenames_.size(); ++i) {
+      CreateHeader(i, component_count);
       CopyAndFixupNativeData(i);
     }
   }
@@ -444,15 +444,12 @@
     ImageInfo& image_info = GetImageInfo(i);
     ImageFileGuard image_file;
     if (image_fd != kInvalidFd) {
-      if (image_filename.empty()) {
-        image_file.reset(new File(image_fd, unix_file::kCheckSafeUsage));
-        // Empty the file in case it already exists.
-        if (image_file != nullptr) {
-          TEMP_FAILURE_RETRY(image_file->SetLength(0));
-          TEMP_FAILURE_RETRY(image_file->Flush());
-        }
-      } else {
-        LOG(ERROR) << "image fd " << image_fd << " name " << image_filename;
+      // Ignore image_filename, it is supplied only for better diagnostic.
+      image_file.reset(new File(image_fd, unix_file::kCheckSafeUsage));
+      // Empty the file in case it already exists.
+      if (image_file != nullptr) {
+        TEMP_FAILURE_RETRY(image_file->SetLength(0));
+        TEMP_FAILURE_RETRY(image_file->Flush());
       }
     } else {
       image_file.reset(OS::CreateEmptyFile(image_filename.c_str()));
@@ -2640,7 +2637,7 @@
   return make_pair(metadata_section.End(), std::move(sections));
 }
 
-void ImageWriter::CreateHeader(size_t oat_index) {
+void ImageWriter::CreateHeader(size_t oat_index, size_t component_count) {
   ImageInfo& image_info = GetImageInfo(oat_index);
   const uint8_t* oat_file_begin = image_info.oat_file_begin_;
   const uint8_t* oat_file_end = oat_file_begin + image_info.oat_loaded_size_;
@@ -2648,18 +2645,23 @@
 
   uint32_t image_reservation_size = image_info.image_size_;
   DCHECK_ALIGNED(image_reservation_size, kPageSize);
-  uint32_t component_count = 1u;
-  if (!compiler_options_.IsAppImage()) {
+  uint32_t current_component_count = 1u;
+  if (compiler_options_.IsAppImage()) {
+    DCHECK_EQ(oat_index, 0u);
+    DCHECK_EQ(component_count, current_component_count);
+  } else {
+    DCHECK(image_infos_.size() == 1u || image_infos_.size() == component_count)
+        << image_infos_.size() << " " << component_count;
     if (oat_index == 0u) {
       const ImageInfo& last_info = image_infos_.back();
       const uint8_t* end = last_info.oat_file_begin_ + last_info.oat_loaded_size_;
       DCHECK_ALIGNED(image_info.image_begin_, kPageSize);
       image_reservation_size =
           dchecked_integral_cast<uint32_t>(RoundUp(end - image_info.image_begin_, kPageSize));
-      component_count = image_infos_.size();
+      current_component_count = component_count;
     } else {
       image_reservation_size = 0u;
-      component_count = 0u;
+      current_component_count = 0u;
     }
   }
 
@@ -2669,13 +2671,13 @@
   if (oat_index == 0u) {
     const std::vector<gc::space::ImageSpace*>& image_spaces =
         Runtime::Current()->GetHeap()->GetBootImageSpaces();
-    boot_image_components = dchecked_integral_cast<uint32_t>(image_spaces.size());
-    DCHECK_EQ(boot_image_components == 0u, compiler_options_.IsBootImage());
-    for (uint32_t i = 0; i != boot_image_components; ) {
+    DCHECK_EQ(image_spaces.empty(), compiler_options_.IsBootImage());
+    for (size_t i = 0u, size = image_spaces.size(); i != size; ) {
       const ImageHeader& header = image_spaces[i]->GetImageHeader();
+      boot_image_components += header.GetComponentCount();
       boot_image_checksums ^= header.GetImageChecksum();
-      DCHECK_LE(header.GetComponentCount(), boot_image_components - i);
-      i += header.GetComponentCount();
+      DCHECK_LE(header.GetImageSpaceCount(), size - i);
+      i += header.GetImageSpaceCount();
     }
   }
 
@@ -2709,7 +2711,7 @@
   // image.
   new (image_info.image_.Begin()) ImageHeader(
       image_reservation_size,
-      component_count,
+      current_component_count,
       PointerToLowMemUInt32(image_info.image_begin_),
       image_end,
       sections.data(),
diff --git a/dex2oat/linker/image_writer.h b/dex2oat/linker/image_writer.h
index 22b7739..811b5c3 100644
--- a/dex2oat/linker/image_writer.h
+++ b/dex2oat/linker/image_writer.h
@@ -144,7 +144,7 @@
   // the names in oat_filenames.
   bool Write(int image_fd,
              const std::vector<std::string>& image_filenames,
-             const std::vector<std::string>& oat_filenames)
+             size_t component_count)
       REQUIRES(!Locks::mutator_lock_);
 
   uintptr_t GetOatDataBegin(size_t oat_index) {
@@ -470,7 +470,7 @@
   // Lays out where the image objects will be at runtime.
   void CalculateNewObjectOffsets()
       REQUIRES_SHARED(Locks::mutator_lock_);
-  void CreateHeader(size_t oat_index)
+  void CreateHeader(size_t oat_index, size_t component_count)
       REQUIRES_SHARED(Locks::mutator_lock_);
   ObjPtr<mirror::ObjectArray<mirror::Object>> CollectDexCaches(Thread* self, size_t oat_index) const
       REQUIRES_SHARED(Locks::mutator_lock_);
diff --git a/runtime/gc/heap.cc b/runtime/gc/heap.cc
index 8d94bea..eea3084 100644
--- a/runtime/gc/heap.cc
+++ b/runtime/gc/heap.cc
@@ -178,12 +178,12 @@
   for (size_t i = 0u, num_spaces = image_spaces.size(); i != num_spaces; ) {
     const ImageHeader& image_header = image_spaces[i]->GetImageHeader();
     uint32_t reservation_size = image_header.GetImageReservationSize();
-    uint32_t component_count = image_header.GetComponentCount();
+    uint32_t image_count = image_header.GetImageSpaceCount();
 
-    CHECK_NE(component_count, 0u);
-    CHECK_LE(component_count, num_spaces - i);
+    CHECK_NE(image_count, 0u);
+    CHECK_LE(image_count, num_spaces - i);
     CHECK_NE(reservation_size, 0u);
-    for (size_t j = 1u; j != image_header.GetComponentCount(); ++j) {
+    for (size_t j = 1u; j != image_count; ++j) {
       CHECK_EQ(image_spaces[i + j]->GetImageHeader().GetComponentCount(), 0u);
       CHECK_EQ(image_spaces[i + j]->GetImageHeader().GetImageReservationSize(), 0u);
     }
@@ -193,7 +193,7 @@
     // Check contiguous layout of images and oat files.
     const uint8_t* current_heap = image_spaces[i]->Begin();
     const uint8_t* current_oat = image_spaces[i]->GetImageHeader().GetOatFileBegin();
-    for (size_t j = 0u; j != image_header.GetComponentCount(); ++j) {
+    for (size_t j = 0u; j != image_count; ++j) {
       const ImageHeader& current_header = image_spaces[i + j]->GetImageHeader();
       CHECK_EQ(current_heap, image_spaces[i + j]->Begin());
       CHECK_EQ(current_oat, current_header.GetOatFileBegin());
@@ -207,7 +207,7 @@
     CHECK_EQ(reservation_size, static_cast<size_t>(current_oat - image_spaces[i]->Begin()));
 
     boot_image_size += reservation_size;
-    i += component_count;
+    i += image_count;
   }
 }
 
diff --git a/runtime/gc/space/image_space.cc b/runtime/gc/space/image_space.cc
index c4f1c2e..de4f93f 100644
--- a/runtime/gc/space/image_space.cc
+++ b/runtime/gc/space/image_space.cc
@@ -966,8 +966,9 @@
     }
     uint32_t checksum = 0u;
     size_t chunk_count = 0u;
+    size_t space_pos = 0u;
     for (size_t component_count = 0u; component_count != boot_image_component_count; ) {
-      const ImageHeader& current_header = image_spaces[component_count]->GetImageHeader();
+      const ImageHeader& current_header = image_spaces[space_pos]->GetImageHeader();
       if (current_header.GetComponentCount() > boot_image_component_count - component_count) {
         *error_msg = StringPrintf("Boot image component count in %s ends in the middle of a chunk, "
                                       "%u is between %zu and %zu",
@@ -980,6 +981,7 @@
       component_count += current_header.GetComponentCount();
       checksum ^= current_header.GetImageChecksum();
       chunk_count += 1u;
+      space_pos += current_header.GetImageSpaceCount();
     }
     if (image_header.GetBootImageChecksum() != checksum) {
       *error_msg = StringPrintf("Boot image checksum mismatch (0x%x != 0x%x) in image %s",
@@ -1481,10 +1483,12 @@
     std::string base_location;
     std::string base_filename;
     size_t start_index;
-    size_t component_count;
-    size_t reservation_size;
+    uint32_t component_count;
+    uint32_t image_space_count;
+    uint32_t reservation_size;
     uint32_t checksum;
     uint32_t boot_image_component_count;
+    uint32_t boot_image_space_count;
     uint32_t boot_image_checksum;
   };
 
@@ -1568,6 +1572,7 @@
 
   bool ValidateBootImageChecksum(const std::string& actual_filename,
                                  const ImageHeader& header,
+                                 /*out*/uint32_t* boot_image_space_count,
                                  /*out*/std::string* error_msg);
 
   bool ReadHeader(const std::string& base_location,
@@ -1750,6 +1755,7 @@
 
 bool ImageSpace::BootImageLayout::ValidateBootImageChecksum(const std::string& actual_filename,
                                                             const ImageHeader& header,
+                                                            /*out*/uint32_t* boot_image_space_count,
                                                             /*out*/std::string* error_msg) {
   uint32_t boot_image_component_count = header.GetBootImageComponentCount();
   if (chunks_.empty() != (boot_image_component_count == 0u)) {
@@ -1761,6 +1767,7 @@
   }
   uint32_t component_count = 0u;
   uint32_t composite_checksum = 0u;
+  *boot_image_space_count = 0u;
   for (const ImageChunk& chunk : chunks_) {
     if (component_count == boot_image_component_count) {
       break;  // Hit the component count.
@@ -1770,7 +1777,7 @@
     }
     if (chunk.component_count > boot_image_component_count - component_count) {
       *error_msg = StringPrintf("Boot image component count in %s ends in the middle of a chunk, "
-                                    "%u is between %u and %zu",
+                                    "%u is between %u and %u",
                                 actual_filename.c_str(),
                                 boot_image_component_count,
                                 component_count,
@@ -1779,6 +1786,7 @@
     }
     component_count += chunk.component_count;
     composite_checksum ^= chunk.checksum;
+    *boot_image_space_count += chunk.image_space_count;
   }
   DCHECK_LE(component_count, boot_image_component_count);
   if (component_count != boot_image_component_count) {
@@ -1830,7 +1838,8 @@
                               allowed_reservation_size);
     return false;
   }
-  if (!ValidateBootImageChecksum(actual_filename, header, error_msg)) {
+  uint32_t boot_image_space_count;
+  if (!ValidateBootImageChecksum(actual_filename, header, &boot_image_space_count, error_msg)) {
     return false;
   }
 
@@ -1842,9 +1851,11 @@
   chunk.base_filename = base_filename;
   chunk.start_index = bcp_index;
   chunk.component_count = header.GetComponentCount();
+  chunk.image_space_count = header.GetImageSpaceCount();
   chunk.reservation_size = header.GetImageReservationSize();
   chunk.checksum = header.GetImageChecksum();
   chunk.boot_image_component_count = header.GetBootImageComponentCount();
+  chunk.boot_image_space_count = boot_image_space_count;
   chunk.boot_image_checksum = header.GetBootImageChecksum();
   chunks_.push_back(std::move(chunk));
   next_bcp_index_ = bcp_index + header.GetComponentCount();
@@ -2308,22 +2319,22 @@
             spaces.front()->Begin(),
             spaces.back()->End() - spaces.front()->Begin()));
     const ImageHeader& base_header = spaces[0]->GetImageHeader();
-    size_t base_component_count = base_header.GetComponentCount();
-    DCHECK_LE(base_component_count, spaces.size());
+    size_t base_image_space_count = base_header.GetImageSpaceCount();
+    DCHECK_LE(base_image_space_count, spaces.size());
     DoRelocateSpaces<kPointerSize, /*kExtension=*/ false>(
-        spaces.SubArray(/*pos=*/ 0u, base_component_count),
+        spaces.SubArray(/*pos=*/ 0u, base_image_space_count),
         base_diff64,
         &patched_objects);
 
-    for (size_t i = base_component_count, size = spaces.size(); i != size; ) {
+    for (size_t i = base_image_space_count, size = spaces.size(); i != size; ) {
       const ImageHeader& ext_header = spaces[i]->GetImageHeader();
-      size_t ext_component_count = ext_header.GetComponentCount();
-      DCHECK_LE(ext_component_count, size - i);
+      size_t ext_image_space_count = ext_header.GetImageSpaceCount();
+      DCHECK_LE(ext_image_space_count, size - i);
       DoRelocateSpaces<kPointerSize, /*kExtension=*/ true>(
-          spaces.SubArray(/*pos=*/ i, ext_component_count),
+          spaces.SubArray(/*pos=*/ i, ext_image_space_count),
           base_diff64,
           &patched_objects);
-      i += ext_component_count;
+      i += ext_image_space_count;
     }
   }
 
@@ -2741,7 +2752,7 @@
     }
     ArrayRef<const std::string> requested_bcp_locations =
         ArrayRef<const std::string>(boot_class_path_locations_).SubArray(
-            chunk.start_index, chunk.component_count);
+            chunk.start_index, chunk.image_space_count);
     std::vector<std::string> locations =
         ExpandMultiImageLocations(requested_bcp_locations, chunk.base_location, is_extension);
     std::vector<std::string> filenames =
@@ -2761,14 +2772,18 @@
       }
       const ImageHeader& header = space->GetImageHeader();
       if (i == 0 && (chunk.checksum != header.GetImageChecksum() ||
+                     chunk.image_space_count != header.GetImageSpaceCount() ||
                      chunk.boot_image_component_count != header.GetBootImageComponentCount() ||
                      chunk.boot_image_checksum != header.GetBootImageChecksum())) {
         *error_msg = StringPrintf("Image header modified since previously read from %s; "
                                       "checksum: 0x%08x -> 0x%08x,"
+                                      "image_space_count: %u -> %u"
                                       "boot_image_component_count: %u -> %u, "
                                       "boot_image_checksum: 0x%08x -> 0x%08x",
                                   space->GetImageFilename().c_str(),
                                   chunk.checksum,
+                                  chunk.image_space_count,
+                                  header.GetImageSpaceCount(),
                                   header.GetImageChecksum(),
                                   chunk.boot_image_component_count,
                                   header.GetBootImageComponentCount(),
@@ -2782,9 +2797,10 @@
         ArrayRef<const std::unique_ptr<ImageSpace>>(*spaces).SubArray(
             /*pos=*/ 0u, chunk.boot_image_component_count);
     for (std::size_t i = 0u, size = locations.size(); i != size; ++i) {
-      ImageSpace* space = (*spaces)[spaces->size() - chunk.component_count + i].get();
+      ImageSpace* space = (*spaces)[spaces->size() - chunk.image_space_count + i].get();
+      size_t bcp_chunk_size = (chunk.image_space_count == 1u) ? chunk.component_count : 1u;
       if (!OpenOatFile(space,
-                       boot_class_path_.SubArray(/*pos=*/ chunk.start_index + i, /*length=*/ 1u),
+                       boot_class_path_.SubArray(/*pos=*/ chunk.start_index + i, bcp_chunk_size),
                        validate_oat_file,
                        dependencies,
                        logger,
@@ -3220,15 +3236,16 @@
     DCHECK_EQ(main_space->oat_file_non_owned_->GetOatDexFiles()[0]->GetDexFileLocation(),
               boot_class_path[bcp_pos]->GetLocation());
     const ImageHeader& current_header = main_space->GetImageHeader();
-    uint32_t component_count = current_header.GetComponentCount();
-    DCHECK_NE(component_count, 0u);
-    DCHECK_LE(component_count, image_spaces.size() - image_pos);
+    uint32_t image_space_count = current_header.GetImageSpaceCount();
+    DCHECK_NE(image_space_count, 0u);
+    DCHECK_LE(image_space_count, image_spaces.size() - image_pos);
     if (image_pos != 0u) {
       boot_image_checksum += ':';
     }
+    uint32_t component_count = current_header.GetComponentCount();
     AppendImageChecksum(component_count, current_header.GetImageChecksum(), &boot_image_checksum);
-    for (size_t component_index = 0; component_index != component_count; ++component_index) {
-      const ImageSpace* space = image_spaces[image_pos + component_index];
+    for (size_t space_index = 0; space_index != image_space_count; ++space_index) {
+      const ImageSpace* space = image_spaces[image_pos + space_index];
       const OatFile* oat_file = space->oat_file_non_owned_;
       size_t num_dex_files = oat_file->GetOatDexFiles().size();
       if (kIsDebugBuild) {
@@ -3241,7 +3258,7 @@
       }
       bcp_pos += num_dex_files;
     }
-    image_pos += component_count;
+    image_pos += image_space_count;
   }
 
   ArrayRef<const DexFile* const> boot_class_path_tail =
@@ -3426,13 +3443,15 @@
   }
 
   // Verify image checksums.
+  size_t bcp_pos = 0u;
   size_t image_pos = 0u;
   while (image_pos != num_image_spaces && StartsWith(oat_checksums, "i")) {
     // Verify the current image checksum.
     const ImageHeader& current_header = image_spaces[image_pos]->GetImageHeader();
+    uint32_t image_space_count = current_header.GetImageSpaceCount();
+    DCHECK_NE(image_space_count, 0u);
+    DCHECK_LE(image_space_count, image_spaces.size() - image_pos);
     uint32_t component_count = current_header.GetComponentCount();
-    DCHECK_NE(component_count, 0u);
-    DCHECK_LE(component_count, image_spaces.size() - image_pos);
     uint32_t checksum = current_header.GetImageChecksum();
     if (!CheckAndRemoveImageChecksum(component_count, checksum, &oat_checksums, error_msg)) {
       DCHECK(!error_msg->empty());
@@ -3440,21 +3459,29 @@
     }
 
     if (kIsDebugBuild) {
-      for (size_t component_index = 0; component_index != component_count; ++component_index) {
-        const OatFile* oat_file = image_spaces[image_pos + component_index]->oat_file_non_owned_;
+      for (size_t space_index = 0; space_index != image_space_count; ++space_index) {
+        const OatFile* oat_file = image_spaces[image_pos + space_index]->oat_file_non_owned_;
         size_t num_dex_files = oat_file->GetOatDexFiles().size();
         CHECK_NE(num_dex_files, 0u);
         const std::string main_location = oat_file->GetOatDexFiles()[0]->GetDexFileLocation();
-        CHECK_EQ(main_location, boot_class_path_locations[image_pos + component_index]);
+        CHECK_EQ(main_location, boot_class_path_locations[bcp_pos + space_index]);
         CHECK(!DexFileLoader::IsMultiDexLocation(main_location.c_str()));
+        size_t num_base_locations = 1u;
         for (size_t i = 1u; i != num_dex_files; ++i) {
-          CHECK(DexFileLoader::IsMultiDexLocation(
-                    oat_file->GetOatDexFiles()[i]->GetDexFileLocation().c_str()));
+          if (DexFileLoader::IsMultiDexLocation(
+                  oat_file->GetOatDexFiles()[i]->GetDexFileLocation().c_str())) {
+            CHECK_EQ(image_space_count, 1u);  // We can find base locations only for --single-image.
+            ++num_base_locations;
+          }
+        }
+        if (image_space_count == 1u) {
+          CHECK_EQ(num_base_locations, component_count);
         }
       }
     }
 
-    image_pos += component_count;
+    image_pos += image_space_count;
+    bcp_pos += component_count;
 
     if (!StartsWith(oat_checksums, ":")) {
       // Check that we've reached the end of checksums and BCP.
diff --git a/runtime/image.cc b/runtime/image.cc
index 07fcc8b..782c0c6 100644
--- a/runtime/image.cc
+++ b/runtime/image.cc
@@ -29,7 +29,7 @@
 namespace art {
 
 const uint8_t ImageHeader::kImageMagic[] = { 'a', 'r', 't', '\n' };
-const uint8_t ImageHeader::kImageVersion[] = { '0', '8', '4', '\0' };  // FP16 gt/ge/lt/le intrinsic
+const uint8_t ImageHeader::kImageVersion[] = { '0', '8', '5', '\0' };  // Single-image.
 
 ImageHeader::ImageHeader(uint32_t image_reservation_size,
                          uint32_t component_count,
@@ -104,6 +104,14 @@
   return image_reservation_size_ == RoundUp(image_size_, kPageSize);
 }
 
+uint32_t ImageHeader::GetImageSpaceCount() const {
+  DCHECK(!IsAppImage());
+  DCHECK_NE(component_count_, 0u);  // Must be the header for the first component.
+  // For images compiled with --single-image, there is only one oat file. To detect
+  // that, check whether the reservation ends at the end of the first oat file.
+  return (image_begin_ + image_reservation_size_ == oat_file_end_) ? 1u : component_count_;
+}
+
 bool ImageHeader::IsValid() const {
   if (memcmp(magic_, kImageMagic, sizeof(kImageMagic)) != 0) {
     return false;
diff --git a/runtime/image.h b/runtime/image.h
index 12950a3..896a83b 100644
--- a/runtime/image.h
+++ b/runtime/image.h
@@ -366,6 +366,8 @@
 
   bool IsAppImage() const;
 
+  uint32_t GetImageSpaceCount() const;
+
   // Visit mirror::Objects in the section starting at base.
   // TODO: Delete base parameter if it is always equal to GetImageBegin.
   void VisitObjects(ObjectVisitor* visitor,