Fall back to compiling the primary boot image on device.

After this change, when odrefresh fails to compile the full boot image,
it will fall back to compile a minimal boot image named
"boot_minimal.art", which is generated from BCP jars in the ART module.

Bug: 214954206
Test: atest art_standalone_odrefresh_tests
Test: atest odsign_e2e_tests
Change-Id: I7d1ccdee4990210a96c0acbcaff0c492dc6019c5
diff --git a/libartbase/base/file_utils.cc b/libartbase/base/file_utils.cc
index a62d6de..139d904 100644
--- a/libartbase/base/file_utils.cc
+++ b/libartbase/base/file_utils.cc
@@ -290,14 +290,18 @@
 std::string GetDefaultBootImageLocation(const std::string& android_root,
                                         bool deny_art_apex_data_files) {
   constexpr static const char* kEtcBootImageProf = "etc/boot-image.prof";
+  constexpr static const char* kBootImageStem = "boot";
+  constexpr static const char* kMinimalBootImageStem = "boot_minimal";
 
   // If an update for the ART module has been been installed, a single boot image for the entire
   // bootclasspath is in the ART APEX data directory.
   if (kIsTargetBuild && !deny_art_apex_data_files) {
     const std::string boot_image =
-        GetApexDataDalvikCacheDirectory(InstructionSet::kNone) + "/boot.art";
+        GetApexDataDalvikCacheDirectory(InstructionSet::kNone) + "/" + kBootImageStem + ".art";
     const std::string boot_image_filename = GetSystemImageFilename(boot_image.c_str(), kRuntimeISA);
     if (OS::FileExists(boot_image_filename.c_str(), /*check_file_type=*/true)) {
+      // Typically "/data/misc/apexdata/com.android.art/dalvik-cache/boot.art!/apex/com.android.art
+      // /etc/boot-image.prof!/system/etc/boot-image.prof".
       return StringPrintf("%s!%s/%s!%s/%s",
                           boot_image.c_str(),
                           kAndroidArtApexDefaultPath,
@@ -308,15 +312,42 @@
       // Additional warning for potential SELinux misconfiguration.
       PLOG(ERROR) << "Default boot image check failed, could not stat: " << boot_image_filename;
     }
+
+    // odrefresh can generate a minimal boot image, which only includes code from BCP jars in the
+    // ART module, when it fails to generate a single boot image for the entire bootclasspath (i.e.,
+    // full boot image). Use it if it exists.
+    const std::string minimal_boot_image = GetApexDataDalvikCacheDirectory(InstructionSet::kNone) +
+                                           "/" + kMinimalBootImageStem + ".art";
+    const std::string minimal_boot_image_filename =
+        GetSystemImageFilename(minimal_boot_image.c_str(), kRuntimeISA);
+    if (OS::FileExists(minimal_boot_image_filename.c_str(), /*check_file_type=*/true)) {
+      // Typically "/data/misc/apexdata/com.android.art/dalvik-cache/boot_minimal.art!/apex
+      // /com.android.art/etc/boot-image.prof:/nonx/boot_minimal-framework.art!/system/etc
+      // /boot-image.prof".
+      return StringPrintf("%s!%s/%s:/nonx/%s-framework.art!%s/%s",
+                          minimal_boot_image.c_str(),
+                          kAndroidArtApexDefaultPath,
+                          kEtcBootImageProf,
+                          kMinimalBootImageStem,
+                          android_root.c_str(),
+                          kEtcBootImageProf);
+    } else if (errno == EACCES) {
+      // Additional warning for potential SELinux misconfiguration.
+      PLOG(ERROR) << "Minimal boot image check failed, could not stat: " << boot_image_filename;
+    }
   }
   // Boot image consists of two parts:
   //  - the primary boot image (contains the Core Libraries)
   //  - the boot image extensions (contains framework libraries)
-  return StringPrintf("%s/boot.art!%s/%s:%s/framework/boot-framework.art!%s/%s",
+  // Typically "/apex/com.android.art/javalib/boot.art!/apex/com.android.art/etc/boot-image.prof:
+  // /system/framework/boot-framework.art!/system/etc/boot-image.prof".
+  return StringPrintf("%s/%s.art!%s/%s:%s/framework/%s-framework.art!%s/%s",
                       GetPrebuiltPrimaryBootImageDir().c_str(),
+                      kBootImageStem,
                       kAndroidArtApexDefaultPath,
                       kEtcBootImageProf,
                       android_root.c_str(),
+                      kBootImageStem,
                       android_root.c_str(),
                       kEtcBootImageProf);
 }
diff --git a/odrefresh/odr_config.h b/odrefresh/odr_config.h
index b44e4e2..d95ab96 100644
--- a/odrefresh/odr_config.h
+++ b/odrefresh/odr_config.h
@@ -64,6 +64,7 @@
   std::string artifact_dir_;
   std::string standalone_system_server_jars_;
   bool compilation_os_mode_ = false;
+  bool minimal_ = false;
 
   // Staging directory for artifacts. The directory must exist and will be automatically removed
   // after compilation. If empty, use the default directory.
@@ -143,6 +144,7 @@
     return staging_dir_;
   }
   bool GetCompilationOsMode() const { return compilation_os_mode_; }
+  bool GetMinimal() const { return minimal_; }
 
   void SetApexInfoListFile(const std::string& file_path) { apex_info_list_file_ = file_path; }
   void SetArtBinDir(const std::string& art_bin_dir) { art_bin_dir_ = art_bin_dir; }
@@ -192,6 +194,8 @@
 
   void SetCompilationOsMode(bool value) { compilation_os_mode_ = value; }
 
+  void SetMinimal(bool value) { minimal_ = value; }
+
  private:
   // Returns a pair for the possible instruction sets for the configured instruction set
   // architecture. The first item is the 32-bit architecture and the second item is the 64-bit
diff --git a/odrefresh/odrefresh.cc b/odrefresh/odrefresh.cc
index f0d3f19..2455772 100644
--- a/odrefresh/odrefresh.cc
+++ b/odrefresh/odrefresh.cc
@@ -109,6 +109,7 @@
 constexpr mode_t kFileMode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
 
 constexpr const char* kFirstBootImageBasename = "boot.art";
+constexpr const char* kMinimalBootImageBasename = "boot_minimal.art";
 
 void EraseFiles(const std::vector<std::unique_ptr<File>>& files) {
   for (auto& file : files) {
@@ -728,19 +729,23 @@
       });
 }
 
-std::string OnDeviceRefresh::GetBootImage(bool on_system) const {
+std::string OnDeviceRefresh::GetBootImage(bool on_system, bool minimal) const {
+  DCHECK(!on_system || !minimal);
+  const char* basename = minimal ? kMinimalBootImageBasename : kFirstBootImageBasename;
   if (on_system) {
     // Typically "/system/framework/boot.art".
-    return GetPrebuiltPrimaryBootImageDir() + "/" + kFirstBootImageBasename;
+    return GetPrebuiltPrimaryBootImageDir() + "/" + basename;
   } else {
     // Typically "/data/misc/apexdata/com.android.art/dalvik-cache/boot.art".
-    return config_.GetArtifactDirectory() + "/" + kFirstBootImageBasename;
+    return config_.GetArtifactDirectory() + "/" + basename;
   }
 }
 
-std::string OnDeviceRefresh::GetBootImagePath(bool on_system, const InstructionSet isa) const {
+std::string OnDeviceRefresh::GetBootImagePath(bool on_system,
+                                              bool minimal,
+                                              const InstructionSet isa) const {
   // Typically "/data/misc/apexdata/com.android.art/dalvik-cache/<isa>/boot.art".
-  return GetSystemImageFilename(GetBootImage(on_system).c_str(), isa);
+  return GetSystemImageFilename(GetBootImage(on_system, minimal).c_str(), isa);
 }
 
 std::string OnDeviceRefresh::GetSystemBootImageExtension() const {
@@ -790,10 +795,11 @@
 
 WARN_UNUSED bool OnDeviceRefresh::BootClasspathArtifactsExist(
     bool on_system,
+    bool minimal,
     const InstructionSet isa,
     /*out*/ std::string* error_msg,
     /*out*/ std::vector<std::string>* checked_artifacts) const {
-  std::string path = GetBootImagePath(on_system, isa);
+  std::string path = GetBootImagePath(on_system, minimal, isa);
   OdrArtifacts artifacts = OdrArtifacts::ForBootImage(path);
   if (!ArtifactsExist(artifacts, /*check_art_file=*/true, error_msg, checked_artifacts)) {
     return false;
@@ -841,7 +847,7 @@
 
     // ART is not updated, so we can use the artifacts on /system. Check if they exist.
     std::string error_msg;
-    if (BootClasspathArtifactsExist(/*on_system=*/true, isa, &error_msg)) {
+    if (BootClasspathArtifactsExist(/*on_system=*/true, /*minimal=*/false, isa, &error_msg)) {
       return true;
     }
 
@@ -924,9 +930,17 @@
 
   // Cache info looks good, check all compilation artifacts exist.
   std::string error_msg;
-  if (!BootClasspathArtifactsExist(/*on_system=*/false, isa, &error_msg, checked_artifacts)) {
+  if (!BootClasspathArtifactsExist(
+          /*on_system=*/false, /*minimal=*/false, isa, &error_msg, checked_artifacts)) {
     LOG(INFO) << "Incomplete boot classpath artifacts. " << error_msg;
     metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts);
+    // Add the minimal boot image to `checked_artifacts` if exists. This is to prevent the minimal
+    // boot image from being deleted. It does not affect the return value because we should still
+    // attempt to generate a full boot image even if the minimal one exists.
+    if (BootClasspathArtifactsExist(
+            /*on_system=*/false, /*minimal=*/true, isa, &error_msg, checked_artifacts)) {
+      LOG(INFO) << "Found minimal boot classpath artifacts.";
+    }
     return false;
   }
 
@@ -1301,6 +1315,7 @@
     const std::string& staging_dir,
     OdrMetrics& metrics,
     const std::function<void()>& on_dex2oat_success,
+    bool minimal,
     std::string* error_msg) const {
   ScopedOdrCompilationTimer compilation_timer(metrics);
   std::vector<std::string> args;
@@ -1334,7 +1349,16 @@
   }
 
   // Add boot classpath jars to compile.
-  for (const std::string& component : boot_classpath_compilable_jars_) {
+  std::vector<std::string> jars_to_compile = boot_classpath_compilable_jars_;
+  if (minimal) {
+    auto end =
+        std::remove_if(jars_to_compile.begin(), jars_to_compile.end(), [](const std::string& jar) {
+          return !android::base::StartsWith(jar, GetArtRoot());
+        });
+    jars_to_compile.erase(end, jars_to_compile.end());
+  }
+
+  for (const std::string& component : jars_to_compile) {
     std::string actual_path = AndroidRootRewrite(component);
     args.emplace_back("--dex-file=" + component);
     std::unique_ptr<File> file(OS::OpenFileForReading(actual_path.c_str()));
@@ -1343,13 +1367,12 @@
   }
 
   args.emplace_back("--runtime-arg");
-  args.emplace_back(Concatenate({"-Xbootclasspath:", config_.GetDex2oatBootClasspath()}));
-  auto bcp_jars = android::base::Split(config_.GetDex2oatBootClasspath(), ":");
-  if (!AddBootClasspathFds(args, readonly_files_raii, bcp_jars)) {
+  args.emplace_back(Concatenate({"-Xbootclasspath:", android::base::Join(jars_to_compile, ":")}));
+  if (!AddBootClasspathFds(args, readonly_files_raii, jars_to_compile)) {
     return false;
   }
 
-  const std::string image_location = GetBootImagePath(/*on_system=*/false, isa);
+  const std::string image_location = GetBootImagePath(/*on_system=*/false, minimal, isa);
   const OdrArtifacts artifacts = OdrArtifacts::ForBootImage(image_location);
 
   args.emplace_back("--oat-location=" + artifacts.OatPath());
@@ -1388,8 +1411,11 @@
 
   const time_t timeout = GetSubprocessTimeout();
   const std::string cmd_line = android::base::Join(args, ' ');
-  LOG(INFO) << "Compiling boot classpath (" << isa << "): " << cmd_line << " [timeout " << timeout
-            << "s]";
+  LOG(INFO) << android::base::StringPrintf("Compiling boot classpath (%s%s): %s [timeout %lds]",
+                                           GetInstructionSetString(isa),
+                                           minimal ? ", minimal" : "",
+                                           cmd_line.c_str(),
+                                           timeout);
   if (config_.GetDryRun()) {
     LOG(INFO) << "Compilation skipped (dry-run).";
     return true;
@@ -1506,17 +1532,19 @@
     std::string unused_error_msg;
     // If the boot classpath artifacts are not on /data, then the boot classpath are not re-compiled
     // and the artifacts must exist on /system.
-    bool boot_image_on_system =
-        !BootClasspathArtifactsExist(/*on_system=*/false, isa, &unused_error_msg);
+    bool boot_image_on_system = !BootClasspathArtifactsExist(
+        /*on_system=*/false, /*minimal=*/false, isa, &unused_error_msg);
     AddCompiledBootClasspathFdsIfAny(
         args,
         readonly_files_raii,
         bcp_jars,
         isa,
         boot_image_on_system ? GetSystemBootImageDir() : config_.GetArtifactDirectory());
-    args.emplace_back(Concatenate({"--boot-image=", boot_image_on_system
-        ? GetBootImage(/*on_system=*/true) + ":" + GetSystemBootImageExtension()
-        : GetBootImage(/*on_system=*/false)}));
+    args.emplace_back(
+        Concatenate({"--boot-image=",
+                     boot_image_on_system ? GetBootImage(/*on_system=*/true, /*minimal=*/false) +
+                                                ":" + GetSystemBootImageExtension() :
+                                            GetBootImage(/*on_system=*/false, /*minimal=*/false)}));
 
     const std::string context_path = android::base::Join(classloader_context, ':');
     if (art::ContainsElement(systemserver_classpath_jars_, jar)) {
@@ -1616,25 +1644,63 @@
 
   const auto& bcp_instruction_sets = config_.GetBootClasspathIsas();
   DCHECK(!bcp_instruction_sets.empty() && bcp_instruction_sets.size() <= 2);
+  bool full_compilation_failed = false;
   for (const InstructionSet isa : compilation_options.compile_boot_classpath_for_isas) {
     auto stage = (isa == bcp_instruction_sets.front()) ? OdrMetrics::Stage::kPrimaryBootClasspath :
                                                          OdrMetrics::Stage::kSecondaryBootClasspath;
     metrics.SetStage(stage);
-    if (!CheckCompilationSpace()) {
-      metrics.SetStatus(OdrMetrics::Status::kNoSpace);
-      // Return kCompilationFailed so odsign will keep and sign whatever we have been able to
-      // compile.
-      return ExitCode::kCompilationFailed;
+    if (!config_.GetMinimal()) {
+      if (CheckCompilationSpace()) {
+        if (CompileBootClasspathArtifacts(isa,
+                                          staging_dir,
+                                          metrics,
+                                          advance_animation_progress,
+                                          /*minimal=*/false,
+                                          &error_msg)) {
+          // Remove the minimal boot image only if the full boot image is successfully generated.
+          std::string path = GetBootImagePath(/*on_system=*/false, /*minimal=*/true, isa);
+          OdrArtifacts artifacts = OdrArtifacts::ForBootImage(path);
+          unlink(artifacts.ImagePath().c_str());
+          unlink(artifacts.OatPath().c_str());
+          unlink(artifacts.VdexPath().c_str());
+          continue;
+        }
+        LOG(ERROR) << "Compilation of BCP failed: " << error_msg;
+      } else {
+        metrics.SetStatus(OdrMetrics::Status::kNoSpace);
+      }
     }
 
-    if (!CompileBootClasspathArtifacts(
-            isa, staging_dir, metrics, advance_animation_progress, &error_msg)) {
-      LOG(ERROR) << "Compilation of BCP failed: " << error_msg;
-      if (!config_.GetDryRun() && !RemoveDirectory(staging_dir)) {
-        return ExitCode::kCleanupFailed;
-      }
-      return ExitCode::kCompilationFailed;
+    // Fall back to generating a minimal boot image.
+    // The compilation of the full boot image will be retried on later reboots with a backoff time,
+    // and the minimal boot image will be removed once the compilation of the full boot image
+    // succeeds.
+    full_compilation_failed = true;
+    std::string ignored_error_msg;
+    if (BootClasspathArtifactsExist(
+            /*on_system=*/false, /*minimal=*/true, isa, &ignored_error_msg)) {
+      continue;
     }
+    if (CompileBootClasspathArtifacts(isa,
+                                      staging_dir,
+                                      metrics,
+                                      advance_animation_progress,
+                                      /*minimal=*/true,
+                                      &error_msg)) {
+      continue;
+    }
+    LOG(ERROR) << "Compilation of minimal BCP failed: " << error_msg;
+    if (!config_.GetDryRun() && !RemoveDirectory(staging_dir)) {
+      return ExitCode::kCleanupFailed;
+    }
+    return ExitCode::kCompilationFailed;
+  }
+
+  if (full_compilation_failed) {
+    if (!config_.GetDryRun() && !RemoveDirectory(staging_dir)) {
+      return ExitCode::kCleanupFailed;
+    }
+    return ExitCode::kCompilationFailed;
   }
 
   if (!compilation_options.system_server_jars_to_compile.empty()) {
diff --git a/odrefresh/odrefresh.h b/odrefresh/odrefresh.h
index f8bfefb..4558344 100644
--- a/odrefresh/odrefresh.h
+++ b/odrefresh/odrefresh.h
@@ -92,11 +92,13 @@
 
   std::vector<com::android::art::SystemServerComponent> GenerateSystemServerComponents() const;
 
-  // Returns the symbolic boot image location (without ISA).
-  std::string GetBootImage(bool on_system) const;
+  // Returns the symbolic boot image location (without ISA). If `minimal` is true, returns the
+  // symbolic location of the minimal boot image.
+  std::string GetBootImage(bool on_system, bool minimal) const;
 
-  // Returns the real boot image location (with ISA).
-  std::string GetBootImagePath(bool on_system, const InstructionSet isa) const;
+  // Returns the real boot image location (with ISA).  If `minimal` is true, returns the
+  // symbolic location of the minimal boot image.
+  std::string GetBootImagePath(bool on_system, bool minimal, const InstructionSet isa) const;
 
   // Returns the symbolic boot image extension location (without ISA). Note that this only applies
   // to boot images on /system.
@@ -119,9 +121,11 @@
 
   // Checks whether all boot classpath artifacts are present. Returns true if all are present, false
   // otherwise.
+  // If `minimal` is true, checks the minimal boot image.
   // If `checked_artifacts` is present, adds checked artifacts to `checked_artifacts`.
   WARN_UNUSED bool BootClasspathArtifactsExist(
       bool on_system,
+      bool minimal,
       const InstructionSet isa,
       /*out*/ std::string* error_msg,
       /*out*/ std::vector<std::string>* checked_artifacts = nullptr) const;
@@ -157,10 +161,12 @@
       /*out*/ std::set<std::string>* jars_to_compile,
       /*out*/ std::vector<std::string>* checked_artifacts) const;
 
+  // Compiles boot classpath. If `minimal` is true, only compiles the jars in the ART module.
   WARN_UNUSED bool CompileBootClasspathArtifacts(const InstructionSet isa,
                                                  const std::string& staging_dir,
                                                  OdrMetrics& metrics,
                                                  const std::function<void()>& on_dex2oat_success,
+                                                 bool minimal,
                                                  std::string* error_msg) const;
 
   WARN_UNUSED bool CompileSystemServerArtifacts(
diff --git a/odrefresh/odrefresh_main.cc b/odrefresh/odrefresh_main.cc
index efcae2c..6dbe372 100644
--- a/odrefresh/odrefresh_main.cc
+++ b/odrefresh/odrefresh_main.cc
@@ -145,6 +145,8 @@
       config->SetPartialCompilation(true);
     } else if (ArgumentEquals(arg, "--no-refresh")) {
       config->SetRefresh(false);
+    } else if (ArgumentEquals(arg, "--minimal")) {
+      config->SetMinimal(true);
     } else {
       ArgumentError("Unrecognized argument: '%s'", arg);
     }
@@ -198,6 +200,7 @@
   UsageMsg("--system-server-compiler-filter=<STRING>");
   UsageMsg("                                 Compiler filter that overrides");
   UsageMsg("                                 dalvik.vm.systemservercompilerfilter");
+  UsageMsg("--minimal                        Generate a minimal boot image only.");
 
   exit(EX_USAGE);
 }
diff --git a/odrefresh/odrefresh_test.cc b/odrefresh/odrefresh_test.cc
index 35c4c2d..ae7cc78 100644
--- a/odrefresh/odrefresh_test.cc
+++ b/odrefresh/odrefresh_test.cc
@@ -50,6 +50,7 @@
 using ::testing::AllOf;
 using ::testing::Contains;
 using ::testing::HasSubstr;
+using ::testing::Not;
 using ::testing::Return;
 
 constexpr int kReplace = 1;
@@ -268,6 +269,32 @@
             ExitCode::kCompilationSuccess);
 }
 
+TEST_F(OdRefreshTest, BootClasspathJarsFallback) {
+  // Simulate the case where dex2oat fails when generating the full boot image.
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(AllOf(Contains(Concatenate({"--dex-file=", core_oj_jar_})),
+                                        Contains(Concatenate({"--dex-file=", framework_jar_})))))
+      .Times(2)
+      .WillRepeatedly(Return(1));
+
+  // It should fall back to generating a minimal boot image.
+  EXPECT_CALL(
+      *mock_exec_utils_,
+      DoExecAndReturnCode(AllOf(Contains(Concatenate({"--dex-file=", core_oj_jar_})),
+                                Not(Contains(Concatenate({"--dex-file=", framework_jar_}))))))
+      .Times(2)
+      .WillOnce(Return(0));
+
+  EXPECT_EQ(
+      odrefresh_->Compile(
+          *metrics_,
+          CompilationOptions{
+              .compile_boot_classpath_for_isas = {InstructionSet::kX86, InstructionSet::kX86_64},
+              .system_server_jars_to_compile = odrefresh_->AllSystemServerJars(),
+          }),
+      ExitCode::kCompilationFailed);
+}
+
 TEST_F(OdRefreshTest, AllSystemServerJars) {
   EXPECT_CALL(*mock_exec_utils_,
               DoExecAndReturnCode(AllOf(
diff --git a/runtime/gc/space/image_space.cc b/runtime/gc/space/image_space.cc
index fc2aa00..9302ca2 100644
--- a/runtime/gc/space/image_space.cc
+++ b/runtime/gc/space/image_space.cc
@@ -52,6 +52,7 @@
 #include "gc/accounting/space_bitmap-inl.h"
 #include "gc/task_processor.h"
 #include "image-inl.h"
+#include "image.h"
 #include "intern_table-inl.h"
 #include "mirror/class-inl.h"
 #include "mirror/executable-inl.h"
@@ -2890,11 +2891,12 @@
     size_t num_spaces = spaces.size();
     const ImageHeader& primary_header = spaces.front()->GetImageHeader();
     size_t primary_image_count = primary_header.GetImageSpaceCount();
+    size_t primary_image_component_count = primary_header.GetComponentCount();
     DCHECK_LE(primary_image_count, num_spaces);
     // The primary boot image can be generated with `--single-image` on device, when generated
     // in-memory or with odrefresh.
-    DCHECK(primary_image_count == primary_header.GetComponentCount() || primary_image_count == 1);
-    size_t component_count = primary_image_count;
+    DCHECK(primary_image_count == primary_image_component_count || primary_image_count == 1);
+    size_t component_count = primary_image_component_count;
     size_t space_pos = primary_image_count;
     while (space_pos != num_spaces) {
       const ImageHeader& current_header = spaces[space_pos]->GetImageHeader();
@@ -2905,7 +2907,7 @@
       if (dependency_component_count < component_count) {
         // There shall be no duplicate strings with the components that this space depends on.
         // Find the end of the dependencies, i.e. start of non-dependency images.
-        size_t start_component_count = primary_image_count;
+        size_t start_component_count = primary_image_component_count;
         size_t start_pos = primary_image_count;
         while (start_component_count != dependency_component_count) {
           const ImageHeader& dependency_header = spaces[start_pos]->GetImageHeader();
@@ -3172,6 +3174,7 @@
     std::vector<std::string> filenames =
         ExpandMultiImageLocations(requested_bcp_locations, chunk.base_filename, is_extension);
     DCHECK_EQ(locations.size(), filenames.size());
+    size_t max_dependency_count = spaces->size();
     for (size_t i = 0u, size = locations.size(); i != size; ++i) {
       spaces->push_back(Load(locations[i],
                              filenames[i],
@@ -3217,9 +3220,23 @@
       }
     }
     DCHECK_GE(max_image_space_dependencies, chunk.boot_image_component_count);
+    size_t dependency_count = 0;
+    size_t dependency_component_count = 0;
+    while (dependency_component_count < chunk.boot_image_component_count &&
+           dependency_count < max_dependency_count) {
+      const ImageHeader& current_header = (*spaces)[dependency_count]->GetImageHeader();
+      dependency_component_count += current_header.GetComponentCount();
+      dependency_count += current_header.GetImageSpaceCount();
+    }
+    if (dependency_component_count != chunk.boot_image_component_count) {
+      *error_msg = StringPrintf(
+          "Unable to find dependencies from image spaces; boot_image_component_count: %u",
+          chunk.boot_image_component_count);
+      return false;
+    }
     ArrayRef<const std::unique_ptr<ImageSpace>> dependencies =
         ArrayRef<const std::unique_ptr<ImageSpace>>(*spaces).SubArray(
-            /*pos=*/ 0u, chunk.boot_image_component_count);
+            /*pos=*/ 0u, dependency_count);
     for (size_t i = 0u, size = locations.size(); i != size; ++i) {
       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;
@@ -3809,9 +3826,14 @@
     return false;
   }
   const size_t num_image_spaces = image_spaces.size();
-  if (num_image_spaces != oat_bcp_size) {
-    *error_msg = StringPrintf("Image header records more dependencies (%zu) than BCP (%zu)",
-                              num_image_spaces,
+  size_t dependency_component_count = 0;
+  for (const std::unique_ptr<ImageSpace>& space : image_spaces) {
+    dependency_component_count += space->GetComponentCount();
+  }
+  if (dependency_component_count != oat_bcp_size) {
+    *error_msg = StringPrintf("Image header records %s dependencies (%zu) than BCP (%zu)",
+                              dependency_component_count < oat_bcp_size ? "less" : "more",
+                              dependency_component_count,
                               oat_bcp_size);
     return false;
   }
@@ -3842,7 +3864,7 @@
         CHECK(!DexFileLoader::IsMultiDexLocation(main_location.c_str()));
         size_t num_base_locations = 1u;
         for (size_t i = 1u; i != num_dex_files; ++i) {
-          if (DexFileLoader::IsMultiDexLocation(
+          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;
diff --git a/test/odsign/test-src/com/android/tests/odsign/ActivationTest.java b/test/odsign/test-src/com/android/tests/odsign/ActivationTest.java
index 374e28b..ffcbc41 100644
--- a/test/odsign/test-src/com/android/tests/odsign/ActivationTest.java
+++ b/test/odsign/test-src/com/android/tests/odsign/ActivationTest.java
@@ -27,11 +27,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.Arrays;
 import java.util.Optional;
 import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 /**
  * Test to check if odrefresh, odsign, fs-verity, and ART runtime work together properly.
@@ -83,8 +80,8 @@
         // artifacts compiled and signed by odrefresh and odsign. We check both here rather than
         // having a separate test because the device reboots between each @Test method and
         // that is an expensive use of time.
-        verifyZygotesLoadedArtifacts();
-        verifySystemServerLoadedArtifacts();
+        mTestUtils.verifyZygotesLoadedArtifacts("boot");
+        mTestUtils.verifySystemServerLoadedArtifacts();
     }
 
     @Test
@@ -106,85 +103,4 @@
         mTestUtils.reboot();
         verifyGeneratedArtifactsLoaded();
     }
-
-    private String[] getListFromEnvironmentVariable(String name) throws Exception {
-        String systemServerClasspath = getDevice().executeShellCommand("echo $" + name).trim();
-        if (!systemServerClasspath.isEmpty()) {
-            return systemServerClasspath.split(":");
-        }
-        return new String[0];
-    }
-
-    private String getSystemServerIsa(String mappedArtifact) {
-        // Artifact path for system server artifacts has the form:
-        //    ART_APEX_DALVIK_CACHE_DIRNAME + "/<arch>/system@framework@some.jar@classes.odex"
-        String[] pathComponents = mappedArtifact.split("/");
-        return pathComponents[pathComponents.length - 2];
-    }
-
-    private void verifySystemServerLoadedArtifacts() throws Exception {
-        String[] classpathElements = getListFromEnvironmentVariable("SYSTEMSERVERCLASSPATH");
-        assertTrue("SYSTEMSERVERCLASSPATH is empty", classpathElements.length > 0);
-        String[] standaloneJars = getListFromEnvironmentVariable("STANDALONE_SYSTEMSERVER_JARS");
-        String[] allSystemServerJars = Stream
-                .concat(Arrays.stream(classpathElements), Arrays.stream(standaloneJars))
-                .toArray(String[]::new);
-
-        final Set<String> mappedArtifacts = mTestUtils.getSystemServerLoadedArtifacts();
-        assertTrue(
-                "No mapped artifacts under " + OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME,
-                mappedArtifacts.size() > 0);
-        final String isa = getSystemServerIsa(mappedArtifacts.iterator().next());
-        final String isaCacheDirectory =
-                String.format("%s/%s", OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME, isa);
-
-        // Check components in the system_server classpath have mapped artifacts.
-        for (String element : allSystemServerJars) {
-          String escapedPath = element.substring(1).replace('/', '@');
-          for (String extension : OdsignTestUtils.APP_ARTIFACT_EXTENSIONS) {
-            final String fullArtifactPath =
-                    String.format("%s/%s@classes%s", isaCacheDirectory, escapedPath, extension);
-            assertTrue("Missing " + fullArtifactPath, mappedArtifacts.contains(fullArtifactPath));
-          }
-        }
-
-        for (String mappedArtifact : mappedArtifacts) {
-          // Check the mapped artifact has a .art, .odex or .vdex extension.
-          final boolean knownArtifactKind =
-                    OdsignTestUtils.APP_ARTIFACT_EXTENSIONS.stream().anyMatch(
-                            e -> mappedArtifact.endsWith(e));
-          assertTrue("Unknown artifact kind: " + mappedArtifact, knownArtifactKind);
-        }
-    }
-
-    private void verifyZygoteLoadedArtifacts(String zygoteName, Set<String> mappedArtifacts)
-            throws Exception {
-        final String bootImageStem = "boot";
-
-        assertTrue("Expect 3 bootclasspath artifacts", mappedArtifacts.size() == 3);
-
-        String allArtifacts = mappedArtifacts.stream().collect(Collectors.joining(","));
-        for (String extension : OdsignTestUtils.BCP_ARTIFACT_EXTENSIONS) {
-            final String artifact = bootImageStem + extension;
-            final boolean found = mappedArtifacts.stream().anyMatch(a -> a.endsWith(artifact));
-            assertTrue(zygoteName + " " + artifact + " not found: '" + allArtifacts + "'", found);
-        }
-    }
-
-    private void verifyZygotesLoadedArtifacts() throws Exception {
-        // There are potentially two zygote processes "zygote" and "zygote64". These are
-        // instances 32-bit and 64-bit unspecialized app_process processes.
-        // (frameworks/base/cmds/app_process).
-        int zygoteCount = 0;
-        for (String zygoteName : OdsignTestUtils.ZYGOTE_NAMES) {
-            final Optional<Set<String>> mappedArtifacts =
-                    mTestUtils.getZygoteLoadedArtifacts(zygoteName);
-            if (!mappedArtifacts.isPresent()) {
-                continue;
-            }
-            verifyZygoteLoadedArtifacts(zygoteName, mappedArtifacts.get());
-            zygoteCount += 1;
-        }
-        assertTrue("No zygote processes found", zygoteCount > 0);
-    }
 }
diff --git a/test/odsign/test-src/com/android/tests/odsign/OdrefreshHostTest.java b/test/odsign/test-src/com/android/tests/odsign/OdrefreshHostTest.java
index c53cead..7708aaa 100644
--- a/test/odsign/test-src/com/android/tests/odsign/OdrefreshHostTest.java
+++ b/test/odsign/test-src/com/android/tests/odsign/OdrefreshHostTest.java
@@ -50,6 +50,8 @@
     private static final String ODREFRESH_BIN = "odrefresh";
     private static final String ODREFRESH_COMMAND =
             ODREFRESH_BIN + " --partial-compilation --no-refresh --compile";
+    private static final String ODREFRESH_MINIMAL_COMMAND =
+            ODREFRESH_BIN + " --partial-compilation --no-refresh --minimal --compile";
 
     private static final String TAG = "OdrefreshHostTest";
     private static final String ZYGOTE_ARTIFACTS_KEY = TAG + ":ZYGOTE_ARTIFACTS";
@@ -205,6 +207,48 @@
         assertArtifactsNotModifiedAfter(getSystemServerArtifacts(), timeMs);
     }
 
+    @Test
+    public void verifyMinimalCompilation() throws Exception {
+        mTestUtils.removeCompilationLogToAvoidBackoff();
+        getDevice().executeShellV2Command(
+            "rm -rf " + OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME);
+        getDevice().executeShellV2Command(ODREFRESH_MINIMAL_COMMAND);
+
+        mTestUtils.restartZygote();
+
+        // The minimal boot image should be loaded.
+        Set<String> minimalZygoteArtifacts =
+                mTestUtils.verifyZygotesLoadedArtifacts("boot_minimal");
+
+        // Running the command again should not overwrite the minimal boot image.
+        mTestUtils.removeCompilationLogToAvoidBackoff();
+        long timeMs = getCurrentTimeMs();
+        getDevice().executeShellV2Command(ODREFRESH_MINIMAL_COMMAND);
+
+        assertArtifactsNotModifiedAfter(minimalZygoteArtifacts, timeMs);
+
+        // `odrefresh --check` should keep the minimal boot image.
+        mTestUtils.removeCompilationLogToAvoidBackoff();
+        timeMs = getCurrentTimeMs();
+        getDevice().executeShellV2Command(ODREFRESH_BIN + " --check");
+
+        assertArtifactsNotModifiedAfter(minimalZygoteArtifacts, timeMs);
+
+        // A normal odrefresh invocation should replace the minimal boot image with a full one.
+        mTestUtils.removeCompilationLogToAvoidBackoff();
+        timeMs = getCurrentTimeMs();
+        getDevice().executeShellV2Command(ODREFRESH_COMMAND);
+
+        for (String artifact : minimalZygoteArtifacts) {
+            assertFalse(
+                    String.format(
+                            "Artifact %s should be cleaned up while it still exists", artifact),
+                    getDevice().doesFileExist(artifact));
+        }
+
+        assertArtifactsModifiedAfter(getZygoteArtifacts(), timeMs);
+    }
+
     /**
      * Checks the input line by line and replaces all lines that match the regex with the given
      * replacement.
diff --git a/test/odsign/test-src/com/android/tests/odsign/OdsignTestUtils.java b/test/odsign/test-src/com/android/tests/odsign/OdsignTestUtils.java
index edb9fa4..48644e7 100644
--- a/test/odsign/test-src/com/android/tests/odsign/OdsignTestUtils.java
+++ b/test/odsign/test-src/com/android/tests/odsign/OdsignTestUtils.java
@@ -29,10 +29,13 @@
 import com.android.tradefed.util.CommandResult;
 
 import java.time.Duration;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 public class OdsignTestUtils {
     public static final String ART_APEX_DALVIK_CACHE_DIRNAME =
@@ -49,6 +52,7 @@
             "/data/misc/odrefresh/compilation-log.txt";
 
     private static final Duration BOOT_COMPLETE_TIMEOUT = Duration.ofMinutes(2);
+    private static final Duration RESTART_ZYGOTE_COMPLETE_TIMEOUT = Duration.ofMinutes(1);
 
     private static final String TAG = "OdsignTestUtils";
     private static final String WAS_ADB_ROOT_KEY = TAG + ":WAS_ADB_ROOT";
@@ -129,6 +133,72 @@
         return getMappedArtifacts(systemServerPid, grepPattern);
     }
 
+    public void verifyZygoteLoadedArtifacts(String zygoteName, Set<String> mappedArtifacts,
+            String bootImageStem) throws Exception {
+        assertTrue("Expect 3 bootclasspath artifacts", mappedArtifacts.size() == 3);
+
+        String allArtifacts = mappedArtifacts.stream().collect(Collectors.joining(","));
+        for (String extension : BCP_ARTIFACT_EXTENSIONS) {
+            final String artifact = bootImageStem + extension;
+            final boolean found = mappedArtifacts.stream().anyMatch(a -> a.endsWith(artifact));
+            assertTrue(zygoteName + " " + artifact + " not found: '" + allArtifacts + "'", found);
+        }
+    }
+
+    // Verifies that boot image files with the given stem are loaded by Zygote for each instruction
+    // set. Returns the verified files.
+    public HashSet<String> verifyZygotesLoadedArtifacts(String bootImageStem) throws Exception {
+        // There are potentially two zygote processes "zygote" and "zygote64". These are
+        // instances 32-bit and 64-bit unspecialized app_process processes.
+        // (frameworks/base/cmds/app_process).
+        int zygoteCount = 0;
+        HashSet<String> verifiedArtifacts = new HashSet<>();
+        for (String zygoteName : ZYGOTE_NAMES) {
+            final Optional<Set<String>> mappedArtifacts = getZygoteLoadedArtifacts(zygoteName);
+            if (!mappedArtifacts.isPresent()) {
+                continue;
+            }
+            verifyZygoteLoadedArtifacts(zygoteName, mappedArtifacts.get(), bootImageStem);
+            zygoteCount += 1;
+            verifiedArtifacts.addAll(mappedArtifacts.get());
+        }
+        assertTrue("No zygote processes found", zygoteCount > 0);
+        return verifiedArtifacts;
+    }
+
+    public void verifySystemServerLoadedArtifacts() throws Exception {
+        String[] classpathElements = getListFromEnvironmentVariable("SYSTEMSERVERCLASSPATH");
+        assertTrue("SYSTEMSERVERCLASSPATH is empty", classpathElements.length > 0);
+        String[] standaloneJars = getListFromEnvironmentVariable("STANDALONE_SYSTEMSERVER_JARS");
+        String[] allSystemServerJars = Stream
+                .concat(Arrays.stream(classpathElements), Arrays.stream(standaloneJars))
+                .toArray(String[]::new);
+
+        final Set<String> mappedArtifacts = getSystemServerLoadedArtifacts();
+        assertTrue(
+                "No mapped artifacts under " + ART_APEX_DALVIK_CACHE_DIRNAME,
+                mappedArtifacts.size() > 0);
+        final String isa = getSystemServerIsa(mappedArtifacts.iterator().next());
+        final String isaCacheDirectory = String.format("%s/%s", ART_APEX_DALVIK_CACHE_DIRNAME, isa);
+
+        // Check components in the system_server classpath have mapped artifacts.
+        for (String element : allSystemServerJars) {
+          String escapedPath = element.substring(1).replace('/', '@');
+          for (String extension : APP_ARTIFACT_EXTENSIONS) {
+            final String fullArtifactPath =
+                    String.format("%s/%s@classes%s", isaCacheDirectory, escapedPath, extension);
+            assertTrue("Missing " + fullArtifactPath, mappedArtifacts.contains(fullArtifactPath));
+          }
+        }
+
+        for (String mappedArtifact : mappedArtifacts) {
+          // Check the mapped artifact has a .art, .odex or .vdex extension.
+          final boolean knownArtifactKind =
+                    APP_ARTIFACT_EXTENSIONS.stream().anyMatch(e -> mappedArtifact.endsWith(e));
+          assertTrue("Unknown artifact kind: " + mappedArtifact, knownArtifactKind);
+        }
+    }
+
     public boolean haveCompilationLog() throws Exception {
         CommandResult result =
                 mTestInfo.getDevice().executeShellV2Command("stat " + ODREFRESH_COMPILATION_LOG);
@@ -146,6 +216,16 @@
         assertWithMessage("Device didn't boot in %s", BOOT_COMPLETE_TIMEOUT).that(success).isTrue();
     }
 
+    public void restartZygote() throws Exception {
+        // `waitForBootComplete` relies on `dev.bootcomplete`.
+        mTestInfo.getDevice().executeShellCommand("setprop dev.bootcomplete 0");
+        mTestInfo.getDevice().executeShellCommand("setprop ctl.restart zygote");
+        boolean success = mTestInfo.getDevice()
+                .waitForBootComplete(RESTART_ZYGOTE_COMPLETE_TIMEOUT.toMillis());
+        assertWithMessage("Zygote didn't start in %s", BOOT_COMPLETE_TIMEOUT).that(success)
+                .isTrue();
+    }
+
     /**
      * Enables adb root or skips the test if adb root is not supported.
      */
@@ -179,4 +259,20 @@
     private void setBoolean(String key, boolean value) {
         mTestInfo.properties().put(key, Boolean.toString(value));
     }
+
+    private String[] getListFromEnvironmentVariable(String name) throws Exception {
+        String systemServerClasspath =
+                mTestInfo.getDevice().executeShellCommand("echo $" + name).trim();
+        if (!systemServerClasspath.isEmpty()) {
+            return systemServerClasspath.split(":");
+        }
+        return new String[0];
+    }
+
+    private String getSystemServerIsa(String mappedArtifact) {
+        // Artifact path for system server artifacts has the form:
+        //    ART_APEX_DALVIK_CACHE_DIRNAME + "/<arch>/system@framework@some.jar@classes.odex"
+        String[] pathComponents = mappedArtifact.split("/");
+        return pathComponents[pathComponents.length - 2];
+    }
 }