odrefresh: check artifacts using cache and filesystem metadata

Renables checking of artifacts when invoked with "--check".

Replaces the former "--check" code with a lightweight method based on
artifact cache metadata and filesystem metadata. The worst case checks
take 60ms on aosp_blueline-userdebug.

The old "--check code" is now invoked with "--verify". This remains
for testing purposes and as an option for post-compilation checking.

Bug: 160683548
Bug: 181689036
Test: manual module installs, reboots, deleting artifacts.

Change-Id: I2a75e0fde032a8e3a29d105d8b52e1ede6cb764d
diff --git a/odrefresh/Android.bp b/odrefresh/Android.bp
index 26dfacd..a3fa07c 100644
--- a/odrefresh/Android.bp
+++ b/odrefresh/Android.bp
@@ -32,10 +32,14 @@
     ],
     local_include_dirs: ["include"],
     header_libs: ["dexoptanalyzer_headers"],
-    generated_sources: ["apex-info-list"],
+    generated_sources: [
+        "apex-info-list",
+        "art-apex-cache-info",
+    ],
     shared_libs: [
         "libartpalette",
         "libbase",
+        "libdexfile",
         "liblog",
     ],
     static_libs: ["libxml2"],
@@ -126,3 +130,11 @@
         "libbase",
     ],
 }
+
+xsd_config {
+    name: "art-apex-cache-info",
+    srcs: ["CacheInfo.xsd"],
+    package_name: "com.android.art",
+    api_dir: "schema",
+    gen_writer: true,
+}
diff --git a/odrefresh/CacheInfo.xsd b/odrefresh/CacheInfo.xsd
new file mode 100644
index 0000000..a0c00af
--- /dev/null
+++ b/odrefresh/CacheInfo.xsd
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+           version="2.0"
+           elementFormDefault="qualified"
+           attributeFormDefault="unqualified"
+           targetNamespace="http://schemas.android.com/art/apex-cache-info/v1_0"
+           xmlns:t="http://schemas.android.com/art/apex-cache-info/v1_0" >
+  <!-- Data type holding information on the AOT artifact cache in
+    `/data/misc/apexdata/com.android.art/dalvik-cache` -->
+  <xs:element name="cacheInfo">
+    <xs:complexType>
+    <xs:sequence>
+      <xs:element name="artModuleInfo" minOccurs="1" maxOccurs="1" type="t:artModuleInfo" />
+      <xs:element name="dex2oatBootClasspath" type="t:dex2oatBootClasspath" />
+      <xs:element name="systemServerClasspath" type="t:systemServerClasspath" />
+    </xs:sequence>
+    </xs:complexType>
+  </xs:element>
+
+  <!-- Data type representing the provenance of the AOT artifacts in the cache. -->
+  <xs:complexType name="artModuleInfo">
+    <!-- Module versionCode for the active ART APEX from `/apex/apex-info-list.xml`. -->
+    <xs:attribute name="versionCode" type="xs:long" use="required" />
+    <!-- Module versionName for the active ART APEX from `/apex/apex-info-list.xml`. -->
+    <xs:attribute name="versionName" type="xs:string" use="required" />
+  </xs:complexType>
+
+  <!-- Components of the `DEX2OATBOOTCLASSPATH`. -->
+  <xs:complexType name="dex2oatBootClasspath">
+    <xs:sequence>
+      <xs:element name="component" type="t:component" />
+    </xs:sequence>
+  </xs:complexType>
+
+  <!-- Components of the `SYSTEMSERVERCLASSPATH`. -->
+  <xs:complexType name="systemServerClasspath">
+    <xs:sequence>
+      <xs:element name="component" type="t:component" />
+    </xs:sequence>
+  </xs:complexType>
+
+  <xs:complexType name="component">
+    <!-- File path of component. -->
+    <xs:attribute name="file" type="xs:string" use="required" />
+    <!-- Size of component when cache information is generated. -->
+    <xs:attribute name="size" type="xs:unsignedLong" use="required" />
+    <!-- DEX file checksums within the component. Multidex files have multiple checksums. -->
+    <xs:attribute name="checksums" type="xs:string" use="required" />
+  </xs:complexType>
+
+</xs:schema>
diff --git a/odrefresh/odrefresh.cc b/odrefresh/odrefresh.cc
index 2199dad..ea65d4c 100644
--- a/odrefresh/odrefresh.cc
+++ b/odrefresh/odrefresh.cc
@@ -17,27 +17,36 @@
 #include "odrefresh/odrefresh.h"
 
 #include <dirent.h>
+#include <errno.h>
 #include <fcntl.h>
 #include <ftw.h>
+#include <limits.h>
+#include <inttypes.h>
 #include <stdint.h>
 #include <stdio.h>
+#include <string.h>
 #include <sys/stat.h>
 #include <sys/statvfs.h>
 #include <sysexits.h>
 #include <time.h>
 #include <unistd.h>
 
+#include <algorithm>
 #include <cstdarg>
 #include <cstdlib>
+#include <fstream>
 #include <initializer_list>
 #include <iosfwd>
 #include <iostream>
 #include <memory>
+#include <optional>
 #include <ostream>
 #include <queue>
 #include <sstream>
 #include <string>
 #include <string_view>
+#include <type_traits>
+#include <utility>
 #include <vector>
 
 #include "android-base/file.h"
@@ -49,12 +58,15 @@
 #include "android/log.h"
 #include "arch/instruction_set.h"
 #include "base/bit_utils.h"
+#include "base/file_utils.h"
 #include "base/globals.h"
 #include "base/macros.h"
 #include "base/os.h"
 #include "base/string_view_cpp20.h"
 #include "base/unix_file/fd_file.h"
 #include "com_android_apex.h"
+#include "com_android_art.h"
+#include "dex/art_dex_file_loader.h"
 #include "dexoptanalyzer.h"
 #include "exec_utils.h"
 #include "log/log_main.h"
@@ -66,8 +78,15 @@
 
 namespace art {
 namespace odrefresh {
+
+namespace apex = com::android::apex;
+namespace art_apex = com::android::art;
+
 namespace {
 
+// Name of cache info file in the ART Apex artifact cache.
+static constexpr const char* kCacheInfoFile = "cache-info.xml";
+
 static void UsageErrorV(const char* fmt, va_list ap) {
   std::string error;
   android::base::StringAppendV(&error, fmt, ap);
@@ -102,11 +121,13 @@
   UsageError("");
   UsageError("Valid ACTION choices are:");
   UsageError("");
-  UsageError("--check          Check compilation artifacts are up to date.");
+  UsageError(
+      "--check          Check compilation artifacts are up-to-date based on metadata (fast).");
   UsageError("--compile        Compile boot class path extensions and system_server jars");
   UsageError("                 when necessary).");
   UsageError("--force-compile  Unconditionally compile the boot class path extensions and");
   UsageError("                 system_server jars.");
+  UsageError("--verify         Verify artifacts are up-to-date with dexoptanalyzer (slow).");
   UsageError("--help           Display this help information.");
   exit(EX_USAGE);
 }
@@ -229,19 +250,25 @@
   // Maximum execution time for any child process spawned.
   static constexpr time_t kMaxChildProcessSeconds = 90;
 
+  // Configuration to use.
   const OdrConfig& config_;
 
+  // Path to cache information file that is used to speed up artifact checking.
+  const std::string cache_info_filename_;
+
+  // List of boot extension components that should be compiled.
   std::vector<std::string> boot_extension_compilable_jars_;
 
-  std::string systemserver_output_dir_;
+  // List of system_server components that should be compiled.
   std::vector<std::string> systemserver_compilable_jars_;
 
   const time_t start_time_;
 
  public:
-  explicit OnDeviceRefresh(const OdrConfig& config) : config_(config), start_time_(time(nullptr)) {
-    const std::string art_apex_data = GetArtApexData();
-
+  explicit OnDeviceRefresh(const OdrConfig& config)
+      : config_{config},
+        cache_info_filename_{Concatenate({kOdrefreshArtifactDirectory, "/", kCacheInfoFile})},
+        start_time_{time(nullptr)} {
     for (const std::string& jar : android::base::Split(config_.GetDex2oatBootClasspath(), ":")) {
       // Boot class path extensions are those not in the ART APEX. Updatable APEXes should not
       // have DEX files in the DEX2OATBOOTCLASSPATH. At the time of writing i18n is a non-updatable
@@ -270,22 +297,322 @@
     return std::max(GetExecutionTimeRemaining(), kMaxChildProcessSeconds);
   }
 
-  // Read apex_info_list.xml from input stream and determine if the ART APEX
-  // listed is the factory installed version.
-  static bool IsFactoryApex(const std::string& apex_info_list_xml_path) {
-    auto info_list = com::android::apex::readApexInfoList(apex_info_list_xml_path.c_str());
+  // Gets the `ApexInfo` associated with the currently active ART APEX.
+  std::optional<apex::ApexInfo> GetArtApexInfo() const {
+    auto info_list = apex::readApexInfoList(config_.GetApexInfoListFile().c_str());
     if (!info_list.has_value()) {
-      LOG(FATAL) << "Failed to process " << QuotePath(apex_info_list_xml_path);
+      return {};
     }
 
-    for (const com::android::apex::ApexInfo& info : info_list->getApexInfo()) {
+    for (const apex::ApexInfo& info : info_list->getApexInfo()) {
       if (info.getIsActive() && info.getModuleName() == "com.android.art") {
-        return info.getIsFactory();
+        return info;
+      }
+    }
+    return {};
+  }
+
+  // Reads the ART APEX cache information (if any) found in `kOdrefreshArtifactDirectory`.
+  std::optional<art_apex::CacheInfo> ReadCacheInfo() {
+    return art_apex::read(cache_info_filename_.c_str());
+  }
+
+  // Write ART APEX cache information to `kOdrefreshArtifactDirectory`.
+  void WriteCacheInfo() const {
+    if (OS::FileExists(cache_info_filename_.c_str())) {
+      if (unlink(cache_info_filename_.c_str()) != 0) {
+        PLOG(ERROR) << "Failed to unlink() file " << QuotePath(cache_info_filename_);
       }
     }
 
-    LOG(FATAL) << "Failed to find active com.android.art in " << QuotePath(apex_info_list_xml_path);
-    return false;
+    std::optional<art_apex::ArtModuleInfo> art_module_info = GenerateArtModuleInfo();
+    if (!art_module_info.has_value()) {
+      LOG(ERROR) << "Unable to generate cache provenance";
+      return;
+    }
+
+    // There can be only one CacheProvence in the XML file, but `xsdc` does not have
+    // minOccurs/maxOccurs in the xsd schema.
+    const std::vector<art_apex::ArtModuleInfo> art_module_infos { art_module_info.value() };
+
+    std::optional<std::vector<art_apex::Component>> bcp_components =
+        GenerateBootExtensionComponents();
+    if (!bcp_components.has_value()) {
+      return;
+    }
+
+    std::optional<std::vector<art_apex::Component>> system_server_components =
+        GenerateSystemServerComponents();
+    if (!system_server_components.has_value()) {
+      return;
+    }
+
+    std::ofstream out(cache_info_filename_.c_str());
+    art_apex::CacheInfo info{
+      art_module_infos,
+      {{ art_apex::Dex2oatBootClasspath {bcp_components.value()}}},
+      {{ art_apex::SystemServerClasspath {system_server_components.value()}}}
+    };
+
+    art_apex::write(out, info);
+  }
+
+  // Returns cache provenance information based on the current ART APEX version and filesystem
+  // information.
+  std::optional<art_apex::ArtModuleInfo> GenerateArtModuleInfo() const {
+    auto info = GetArtApexInfo();
+    if (!info.has_value()) {
+      LOG(ERROR) << "Could not update " << QuotePath(cache_info_filename_) << " : no ART Apex info";
+      return {};
+    }
+    return art_apex::ArtModuleInfo{info->getVersionCode(), info->getVersionName()};
+  }
+
+  bool CheckComponents(const std::vector<art_apex::Component>& expected_components,
+                       const std::vector<art_apex::Component>& actual_components,
+                       std::string* error_msg) const {
+    if (expected_components.size() != actual_components.size()) {
+      return false;
+    }
+
+    for (size_t i = 0; i < expected_components.size(); ++i) {
+      const art_apex::Component& expected = expected_components[i];
+      const art_apex::Component& actual = actual_components[i];
+
+      if (expected.getFile() != actual.getFile()) {
+        *error_msg = android::base::StringPrintf("Component %zu file differs ('%s' != '%s')",
+                                                 i,
+                                                 expected.getFile().c_str(),
+                                                 actual.getFile().c_str());
+        return false;
+      }
+      if (expected.getSize() != actual.getSize()) {
+        *error_msg = android::base::StringPrintf("Component %zu size differs (%" PRIu64
+                                                 " != %" PRIu64 ")",
+                                                 i,
+                                                 expected.getSize(),
+                                                 actual.getSize());
+        return false;
+      }
+      if (expected.getChecksums() != actual.getChecksums()) {
+        *error_msg = android::base::StringPrintf("Component %zu checksums differ ('%s' != '%s')",
+                                                 i,
+                                                 expected.getChecksums().c_str(),
+                                                 actual.getChecksums().c_str());
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  std::vector<art_apex::Component> GenerateComponents(const std::vector<std::string>& jars) const {
+    std::vector<art_apex::Component> components;
+
+    ArtDexFileLoader loader;
+    for (const std::string& path : jars) {
+      struct stat sb;
+      if (stat(path.c_str(), &sb) == -1) {
+        PLOG(ERROR) << "Failed to get component: " << QuotePath(path);
+        return {};
+      }
+
+      std::vector<uint32_t> checksums;
+      std::vector<std::string> dex_locations;
+      std::string error_msg;
+      if (!loader.GetMultiDexChecksums(path.c_str(), &checksums, &dex_locations, &error_msg)) {
+        LOG(ERROR) << "Failed to get components: " << error_msg;
+        return {};
+      }
+
+      std::ostringstream oss;
+      for (size_t i = 0; i < checksums.size(); ++i) {
+        if (i != 0) {
+          oss << ';';
+        }
+        oss << android::base::StringPrintf("%08x", checksums[i]);
+      }
+      const std::string checksum = oss.str();
+
+      components.emplace_back(
+          art_apex::Component{path, static_cast<uint64_t>(sb.st_size), checksum});
+    }
+
+    return components;
+  }
+
+  std::vector<art_apex::Component> GenerateBootExtensionComponents() const {
+    return GenerateComponents(boot_extension_compilable_jars_);
+  }
+
+  std::vector<art_apex::Component> GenerateSystemServerComponents() const {
+    return GenerateComponents(systemserver_compilable_jars_);
+  }
+
+  // Checks whether a group of artifacts exists. Returns true if all are present, false otherwise.
+  static bool ArtifactsExist(const OdrArtifacts& artifacts, /*out*/ std::string* error_msg) {
+    const auto paths = {
+        artifacts.ImagePath().c_str(), artifacts.OatPath().c_str(), artifacts.VdexPath().c_str()};
+    for (const char* path : paths) {
+      if (!OS::FileExists(path)) {
+        if (errno == EACCES) {
+          PLOG(ERROR) << "Failed to stat() " << path;
+        }
+        *error_msg = "Missing file: " + QuotePath(path);
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // Checks whether all boot extension artifacts are present on /data. Returns true if all are
+  // present, false otherwise.
+  bool BootExtensionArtifactsExistOnData(const InstructionSet isa,
+                                         /*out*/ std::string* error_msg) const {
+    const std::string apexdata_image_location = GetBootImageExtensionImagePath(isa);
+    const OdrArtifacts artifacts = OdrArtifacts::ForBootImageExtension(apexdata_image_location);
+    return ArtifactsExist(artifacts, error_msg);
+  }
+
+  // Checks whether all system_server artifacts are present on /data. Returns true if all are
+  // present, false otherwise.
+  bool SystemServerArtifactsExistOnData(/*out*/ std::string* error_msg) const {
+    for (const std::string& jar_path : systemserver_compilable_jars_) {
+      const std::string image_location = GetSystemServerImagePath(/*on_system=*/false, jar_path);
+      const OdrArtifacts artifacts = OdrArtifacts::ForSystemServer(image_location);
+      if (!ArtifactsExist(artifacts, error_msg)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  ExitCode CheckArtifactsAreUpToDate() {
+    const auto apex_info = GetArtApexInfo();
+    if (!apex_info.has_value()) {
+      // This should never happen, but do not proceed if it does.
+      LOG(ERROR) << "Could not get ART APEX info.";
+      RemoveArtifactsOrDie();
+      return ExitCode::kCompilationRequired;
+    }
+
+    if (apex_info->getIsFactory()) {
+      // Remove any artifacts on /data as they are not necessary and no compilation is necessary.
+      RemoveArtifactsOrDie();
+      return ExitCode::kOkay;
+    }
+
+    if (!OS::FileExists(cache_info_filename_.c_str())) {
+      PLOG(ERROR) << "No prior cache-info file: " << QuotePath(cache_info_filename_);
+      RemoveArtifactsOrDie();
+      return ExitCode::kCompilationRequired;
+    }
+
+    // Get and parse the ART APEX cache info file.
+    std::optional<art_apex::CacheInfo> cache_info = ReadCacheInfo();
+    if (!cache_info.has_value()) {
+      PLOG(ERROR) << "Failed to read cache-info file: " << QuotePath(cache_info_filename_);
+      RemoveArtifactsOrDie();
+      return ExitCode::kCompilationRequired;
+    }
+
+    // Generate current module info for the current ART APEX.
+    const auto current_info = GenerateArtModuleInfo();
+    if (!current_info.has_value()) {
+      LOG(ERROR) << "Failed to generate cache provenance.";
+      RemoveArtifactsOrDie();
+      return ExitCode::kCompilationRequired;
+    }
+
+    // Check whether the current cache ART module info differs from the current ART module info.
+    // Always check APEX version.
+    const auto cached_info = cache_info->getFirstArtModuleInfo();
+    if (cached_info->getVersionCode() != current_info->getVersionCode()) {
+      LOG(INFO) << "ART APEX version code mismatch ("
+                << cached_info->getVersionCode()
+                << " != " << current_info->getVersionCode() << ").";
+      RemoveArtifactsOrDie();
+      return ExitCode::kCompilationRequired;
+    }
+
+    if (cached_info->getVersionName() != current_info->getVersionName()) {
+      LOG(INFO) << "ART APEX version code mismatch ("
+                << cached_info->getVersionName()
+                << " != " << current_info->getVersionName() << ").";
+      RemoveArtifactsOrDie();
+      return ExitCode::kCompilationRequired;
+    }
+
+    // Check boot class components.
+    //
+    // This checks the size and checksums of odrefresh compilable files on the DEX2OATBOOTCLASSPATH
+    // (the Odrefresh constructor determines which files are compilable). If the number of files
+    // there changes, or their size or checksums change then compilation will be triggered.
+    //
+    // The boot class components may change unexpectedly, for example an OTA could update
+    // framework.jar.
+    const std::vector<art_apex::Component> expected_bcp_components =
+        GenerateBootExtensionComponents();
+    if (expected_bcp_components.size() != 0 &&
+        (!cache_info->hasDex2oatBootClasspath() ||
+         !cache_info->getFirstDex2oatBootClasspath()->hasComponent())) {
+      LOG(INFO) << "Missing Dex2oatBootClasspath components.";
+      RemoveArtifactsOrDie();
+      return ExitCode::kCompilationRequired;
+    }
+
+    std::string error_msg;
+    const std::vector<art_apex::Component>& bcp_components =
+        cache_info->getFirstDex2oatBootClasspath()->getComponent();
+    if (!CheckComponents(expected_bcp_components, bcp_components, &error_msg)) {
+      LOG(INFO) << "Dex2OatClasspath components mismatch: " << error_msg;
+      RemoveArtifactsOrDie();
+      return ExitCode::kCompilationRequired;
+    }
+
+    // Check system server components.
+    //
+    // This checks the size and checksums of odrefresh compilable files on the
+    // SYSTEMSERVERCLASSPATH (the Odrefresh constructor determines which files are compilable). If
+    // the number of files there changes, or their size or checksums change then compilation will be
+    // triggered.
+    //
+    // The system_server components may change unexpectedly, for example an OTA could update
+    // services.jar.
+    const std::vector<art_apex::Component> expected_system_server_components =
+        GenerateSystemServerComponents();
+    if (expected_system_server_components.size() != 0 &&
+        (!cache_info->hasSystemServerClasspath() ||
+         !cache_info->getFirstSystemServerClasspath()->hasComponent())) {
+      LOG(INFO) << "Missing SystemServerClasspath components.";
+      RemoveSystemServerArtifactsFromData();
+      return ExitCode::kCompilationRequired;
+    }
+
+    const std::vector<art_apex::Component>& system_server_components =
+        cache_info->getFirstSystemServerClasspath()->getComponent();
+    if (!CheckComponents(expected_system_server_components, system_server_components, &error_msg)) {
+      LOG(INFO) << "SystemServerClasspath components mismatch: " << error_msg;
+      RemoveSystemServerArtifactsFromData();
+      return ExitCode::kCompilationRequired;
+    }
+
+    // Cache info looks  good, check all compilation artifacts exist.
+    for (const InstructionSet isa : config_.GetBootExtensionIsas()) {
+      if (!BootExtensionArtifactsExistOnData(isa, &error_msg)) {
+        LOG(INFO) << "Incomplete boot extension artifacts. " << error_msg;
+        RemoveBootExtensionArtifactsFromData(isa);
+        return ExitCode::kCompilationRequired;
+      }
+    }
+
+    if (!SystemServerArtifactsExistOnData(&error_msg)) {
+        LOG(INFO) << "Incomplete system_server artifacts. " << error_msg;
+        RemoveSystemServerArtifactsFromData();
+        return ExitCode::kCompilationRequired;
+    }
+
+    return ExitCode::kOkay;
   }
 
   static void AddDex2OatCommonOptions(/*inout*/ std::vector<std::string>& args) {
@@ -332,7 +659,7 @@
     }
   }
 
-  bool CheckSystemServerArtifactsAreUpToDate(bool on_system) const {
+  bool VerifySystemServerArtifactsAreUpToDate(bool on_system) const {
     std::vector<std::string> classloader_context;
     for (const std::string& jar_path : systemserver_compilable_jars_) {
       std::vector<std::string> args;
@@ -452,6 +779,7 @@
       LOG(INFO) << "Removal of system_server artifacts on /data skipped (dry-run).";
       return;
     }
+
     for (const std::string& jar_path : systemserver_compilable_jars_) {
       const std::string image_location =
           GetSystemServerImagePath(/*on_system=*/false, jar_path);
@@ -461,14 +789,14 @@
     }
   }
 
-  // Check the validity of system server artifacts on both /system and /data.
+  // Verify the validity of system server artifacts on both /system and /data.
   // This method has the side-effect of removing system server artifacts on /data, if there are
   // valid artifacts on /system, or if the artifacts on /data are not valid.
   // Returns true if valid artifacts are found.
-  bool CheckSystemServerArtifactsAreUpToDate() const {
-    bool system_ok = CheckSystemServerArtifactsAreUpToDate(/*on_system=*/true);
+  bool VerifySystemServerArtifactsAreUpToDate() const {
+    bool system_ok = VerifySystemServerArtifactsAreUpToDate(/*on_system=*/true);
     LOG(INFO) << "system_server artifacts on /system are " << (system_ok ? "ok" : "stale");
-    bool data_ok = CheckSystemServerArtifactsAreUpToDate(/*on_system=*/false);
+    bool data_ok = VerifySystemServerArtifactsAreUpToDate(/*on_system=*/false);
     LOG(INFO) << "system_server artifacts on /data are " << (data_ok ? "ok" : "stale");
     if (system_ok || !data_ok) {
       // Artifacts on /system are usable or the ones on /data are not usable. Either way, remove
@@ -481,7 +809,7 @@
   // Check the validity of boot class path extension artifacts.
   //
   // Returns true if artifacts exist and are valid according to dexoptanalyzer.
-  bool CheckBootExtensionArtifactsAreUpToDate(const InstructionSet isa, bool on_system) const {
+  bool VerifyBootExtensionArtifactsAreUpToDate(const InstructionSet isa, bool on_system) const {
     const std::string dex_file = boot_extension_compilable_jars_.front();
     const std::string image_location = GetBootImageExtensionImage(on_system);
 
@@ -515,6 +843,11 @@
 
   // Remove boot extension artifacts from /data.
   void RemoveBootExtensionArtifactsFromData(InstructionSet isa) const {
+    if (isa == config_.GetSystemServerIsa()) {
+      // system_server artifacts are invalid without boot extension artifacts.
+      RemoveSystemServerArtifactsFromData();
+    }
+
     if (config_.GetDryRun()) {
       LOG(INFO) << "Removal of bcp extension artifacts on /data skipped (dry-run).";
       return;
@@ -525,14 +858,14 @@
     RemoveArtifacts(OdrArtifacts::ForBootImageExtension(apexdata_image_location));
   }
 
-  // Check whether boot extension artifacts for `isa` are valid on system partition or in apexdata.
+  // Verify whether boot extension artifacts for `isa` are valid on system partition or in apexdata.
   // This method has the side-effect of removing boot classpath extension artifacts on /data,
   // if there are valid artifacts on /system, or if the artifacts on /data are not valid.
   // Returns true if valid boot externsion artifacts are valid.
-  bool CheckBootExtensionArtifactsAreUpToDate(InstructionSet isa) const {
-    bool system_ok = CheckBootExtensionArtifactsAreUpToDate(isa, /*on_system=*/true);
+  bool VerifyBootExtensionArtifactsAreUpToDate(InstructionSet isa) const {
+    bool system_ok = VerifyBootExtensionArtifactsAreUpToDate(isa, /*on_system=*/true);
     LOG(INFO) << "Boot extension artifacts on /system are " << (system_ok ? "ok" : "stale");
-    bool data_ok = CheckBootExtensionArtifactsAreUpToDate(isa, /*on_system=*/false);
+    bool data_ok = VerifyBootExtensionArtifactsAreUpToDate(isa, /*on_system=*/false);
     LOG(INFO) << "Boot extension artifacts on /data are " << (data_ok ? "ok" : "stale");
     if (system_ok || !data_ok) {
       // Artifacts on /system are usable or the ones on /data are not usable. Either way, remove
@@ -542,6 +875,27 @@
     return system_ok || data_ok;
   }
 
+  // Verify all artifacts are up-to-date.
+  //
+  // This method checks artifacts can be loaded by the runtime.
+  //
+  // Returns ExitCode::kOkay if artifacts are up-to-date, ExitCode::kCompilationRequired otherwise.
+  //
+  // NB This is the main function used by the --check command-line option. When invoked with
+  // --compile, we only recompile the out-of-date artifacts, not all (see `Odrefresh::Compile`).
+  ExitCode VerifyArtifactsAreUpToDate() {
+    ExitCode exit_code = ExitCode::kOkay;
+    for (const InstructionSet isa : config_.GetBootExtensionIsas()) {
+      if (!VerifyBootExtensionArtifactsAreUpToDate(isa)) {
+        exit_code = ExitCode::kCompilationRequired;
+      }
+    }
+    if (!VerifySystemServerArtifactsAreUpToDate()) {
+      exit_code = ExitCode::kCompilationRequired;
+    }
+    return exit_code;
+  }
+
   static bool GetFreeSpace(const char* path, uint64_t* bytes) {
     struct statvfs sv;
     if (statvfs(path, &sv) != 0) {
@@ -590,25 +944,6 @@
     }
   }
 
-  // Checks all artifacts are up-to-date.
-  //
-  // Returns ExitCode::kOkay if artifacts are up-to-date, ExitCode::kCompilationRequired otherwise.
-  //
-  // NB This is the main function used by the --check command-line option. When invoked with
-  // --compile, we only recompile the out-of-date artifacts, not all (see `Odrefresh::Compile`).
-  ExitCode CheckArtifactsAreUpToDate() {
-    ExitCode exit_code = ExitCode::kOkay;
-    for (const InstructionSet isa : config_.GetBootExtensionIsas()) {
-      if (!CheckBootExtensionArtifactsAreUpToDate(isa)) {
-        exit_code = ExitCode::kCompilationRequired;
-      }
-    }
-    if (!CheckSystemServerArtifactsAreUpToDate()) {
-      exit_code = ExitCode::kCompilationRequired;
-    }
-    return exit_code;
-  }
-
   // Callback for use with nftw(3) to assist with clearing files and sub-directories.
   // This method removes files and directories below the top-level directory passed to nftw().
   static int NftwUnlinkRemoveCallback(const char* fpath,
@@ -651,6 +986,11 @@
     // Remove everything under ArtApexDataDir
     std::string data_dir = GetArtApexData();
 
+    if (config_.GetDryRun()) {
+      LOG(INFO) << "Artifacts under " << QuotePath(data_dir) << " would be removed (dry-run).";
+      return;
+    }
+
     // Perform depth first traversal removing artifacts.
     nftw(data_dir.c_str(), NftwUnlinkRemoveCallback, 1, FTW_DEPTH | FTW_MOUNT);
   }
@@ -658,6 +998,11 @@
   void RemoveArtifacts(const OdrArtifacts& artifacts) const {
     for (const auto& location :
          {artifacts.ImagePath(), artifacts.OatPath(), artifacts.VdexPath()}) {
+      if (config_.GetDryRun()) {
+        LOG(INFO) << "Removing " << QuotePath(location) << " (dry-run).";
+        continue;
+      }
+
       if (OS::FileExists(location.c_str()) && TEMP_FAILURE_RETRY(unlink(location.c_str())) != 0) {
         PLOG(ERROR) << "Failed to remove: " << QuotePath(location);
       }
@@ -906,29 +1251,33 @@
     // Create staging area and assign label for generating compilation artifacts.
     const char* staging_dir;
     if (PaletteCreateOdrefreshStagingDirectory(&staging_dir) != PALETTE_STATUS_OK) {
+      WriteCacheInfo();
       return ExitCode::kCompilationFailed;
     }
 
     std::string error_msg;
 
     for (const InstructionSet isa : config_.GetBootExtensionIsas()) {
-      if (force_compile || !CheckBootExtensionArtifactsAreUpToDate(isa)) {
+      if (force_compile || !BootExtensionArtifactsExistOnData(isa, &error_msg)) {
         if (!CompileBootExtensionArtifacts(isa, staging_dir, &error_msg)) {
-          LOG(ERROR) << "BCP compilation failed: " << error_msg;
+          LOG(ERROR) << "Compilation of BCP failed: " << error_msg;
           RemoveStagingFilesOrDie(staging_dir);
+          WriteCacheInfo();
           return ExitCode::kCompilationFailed;
         }
       }
     }
 
-    if (force_compile || !CheckSystemServerArtifactsAreUpToDate()) {
+    if (force_compile || !SystemServerArtifactsExistOnData(&error_msg)) {
       if (!CompileSystemServerArtifacts(staging_dir, &error_msg)) {
-        LOG(ERROR) << "system_server compilation failed: " << error_msg;
+        LOG(ERROR) << "Compilation of system_server failed: " << error_msg;
         RemoveStagingFilesOrDie(staging_dir);
+        WriteCacheInfo();
         return ExitCode::kCompilationFailed;
       }
     }
 
+    WriteCacheInfo();
     return ExitCode::kOkay;
   }
 
@@ -1053,15 +1402,16 @@
     for (int i = 0; i < argc; ++i) {
       std::string_view action(argv[i]);
       if (action == "--check") {
-        static constexpr bool kDisableArtifactChecks = true;  // Boot regression b/181689036.
-        if (kDisableArtifactChecks) {
-          return ExitCode::kOkay;
-        }
+        // Fast determination of whether artifacts are up to date.
         return odr.CheckArtifactsAreUpToDate();
       } else if (action == "--compile") {
         return odr.Compile(/*force_compile=*/false);
       } else if (action == "--force-compile") {
         return odr.Compile(/*force_compile=*/true);
+      } else if (action == "--verify") {
+        // Slow determination of whether artifacts are up to date. These are too slow for checking
+        // during boot (b/181689036).
+        return odr.VerifyArtifactsAreUpToDate();
       } else if (action == "--help") {
         UsageHelp(argv[0]);
       } else {
diff --git a/odrefresh/schema/current.txt b/odrefresh/schema/current.txt
new file mode 100644
index 0000000..ae10712
--- /dev/null
+++ b/odrefresh/schema/current.txt
@@ -0,0 +1,58 @@
+// Signature format: 2.0
+package com.android.art {
+
+  public class ArtModuleInfo {
+    ctor public ArtModuleInfo();
+    method public long getVersionCode();
+    method public String getVersionName();
+    method public void setVersionCode(long);
+    method public void setVersionName(String);
+  }
+
+  public class CacheInfo {
+    ctor public CacheInfo();
+    method public com.android.art.ArtModuleInfo getArtModuleInfo();
+    method public com.android.art.Dex2oatBootClasspath getDex2oatBootClasspath();
+    method public com.android.art.SystemServerClasspath getSystemServerClasspath();
+    method public void setArtModuleInfo(com.android.art.ArtModuleInfo);
+    method public void setDex2oatBootClasspath(com.android.art.Dex2oatBootClasspath);
+    method public void setSystemServerClasspath(com.android.art.SystemServerClasspath);
+  }
+
+  public class Component {
+    ctor public Component();
+    method public String getChecksums();
+    method public String getFile();
+    method public java.math.BigInteger getSize();
+    method public void setChecksums(String);
+    method public void setFile(String);
+    method public void setSize(java.math.BigInteger);
+  }
+
+  public class Dex2oatBootClasspath {
+    ctor public Dex2oatBootClasspath();
+    method public com.android.art.Component getComponent();
+    method public void setComponent(com.android.art.Component);
+  }
+
+  public class SystemServerClasspath {
+    ctor public SystemServerClasspath();
+    method public com.android.art.Component getComponent();
+    method public void setComponent(com.android.art.Component);
+  }
+
+  public class XmlParser {
+    ctor public XmlParser();
+    method public static com.android.art.CacheInfo read(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+    method public static String readText(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+    method public static void skip(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+  }
+
+  public class XmlWriter implements java.io.Closeable {
+    ctor public XmlWriter(java.io.PrintWriter);
+    method public void close();
+    method public static void write(com.android.art.XmlWriter, com.android.art.CacheInfo) throws java.io.IOException;
+  }
+
+}
+
diff --git a/odrefresh/schema/last_current.txt b/odrefresh/schema/last_current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/odrefresh/schema/last_current.txt
diff --git a/odrefresh/schema/last_removed.txt b/odrefresh/schema/last_removed.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/odrefresh/schema/last_removed.txt
diff --git a/odrefresh/schema/removed.txt b/odrefresh/schema/removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/odrefresh/schema/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0