ART services: optimize package - Add artd methods.

This change adds two artd methods: `getDexoptNeeded` and `dexopt`, which
are used for app compilation.

Also, this CL replaces all `StringPrintf` with `_format`.

Bug: 229268202
Test: m test-art-host-gtest-art_artd_tests
Ignore-AOSP-First: ART Services.
Change-Id: I51a42816750ff39c768658f739c7e6337cfe3e1c
diff --git a/artd/Android.bp b/artd/Android.bp
index 58680a5..2170289 100644
--- a/artd/Android.bp
+++ b/artd/Android.bp
@@ -34,6 +34,7 @@
         "libarttools",
         "libbase",
         "libbinder_ndk",
+        "libselinux",
     ],
     static_libs: [
         "artd-aidl-ndk",
@@ -75,6 +76,10 @@
         "file_utils_test.cc",
         "path_utils_test.cc",
     ],
+    data: [
+        ":art-gtest-jars-Main",
+        ":art-gtest-jars-Nested",
+    ],
 }
 
 // Version of ART gtest `art_artd_tests` bundled with the ART APEX on target.
@@ -99,4 +104,5 @@
         "art_standalone_gtest_defaults",
         "art_artd_tests_defaults",
     ],
+    test_config_template: "art_standalone_artd_tests.xml",
 }
diff --git a/artd/art_standalone_artd_tests.xml b/artd/art_standalone_artd_tests.xml
new file mode 100644
index 0000000..9125046
--- /dev/null
+++ b/artd/art_standalone_artd_tests.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+<!-- Note: This test config file for {MODULE} is generated from a template. -->
+<configuration description="Runs {MODULE}.">
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="{MODULE}->/data/local/tmp/{MODULE}/{MODULE}" />
+        <option name="append-bitness" value="true" />
+    </target_preparer>
+
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="art-gtest-jars-Main.jar->/data/local/tmp/{MODULE}/art-gtest-jars-Main.jar" />
+        <option name="push" value="art-gtest-jars-Nested.jar->/data/local/tmp/{MODULE}/art-gtest-jars-Nested.jar" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp/{MODULE}" />
+        <option name="module-name" value="{MODULE}" />
+        <option name="ld-library-path-32" value="/apex/com.android.art/lib" />
+        <option name="ld-library-path-64" value="/apex/com.android.art/lib64" />
+    </test>
+
+    <!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if
+         one of the Mainline modules below is present on the device used for testing. -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <!-- ART Mainline Module (internal version). -->
+        <option name="mainline-module-package-name" value="com.google.android.art" />
+        <!-- ART Mainline Module (external (AOSP) version). -->
+        <option name="mainline-module-package-name" value="com.android.art" />
+    </object>
+
+    <!-- Only run tests if the device under test is SDK version 31 (Android 12) or above. -->
+    <!-- TODO(jiakaiz): Change this to U once `ro.build.version.sdk` is bumped. -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk31ModuleController" />
+</configuration>
diff --git a/artd/artd.cc b/artd/artd.cc
index fc99b2a..2cdca3c 100644
--- a/artd/artd.cc
+++ b/artd/artd.cc
@@ -17,29 +17,47 @@
 #include "artd.h"
 
 #include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
 #include <unistd.h>
 
+#include <climits>
 #include <cstdint>
 #include <filesystem>
+#include <functional>
+#include <map>
 #include <memory>
+#include <optional>
+#include <ostream>
 #include <string>
+#include <string_view>
 #include <system_error>
+#include <type_traits>
 #include <utility>
 #include <vector>
 
 #include "aidl/com/android/server/art/BnArtd.h"
+#include "aidl/com/android/server/art/DexoptTrigger.h"
 #include "android-base/errors.h"
+#include "android-base/file.h"
 #include "android-base/logging.h"
 #include "android-base/result.h"
-#include "android-base/stringprintf.h"
+#include "android-base/scopeguard.h"
 #include "android-base/strings.h"
 #include "android/binder_auto_utils.h"
 #include "android/binder_manager.h"
 #include "android/binder_process.h"
+#include "base/compiler_filter.h"
 #include "base/file_utils.h"
+#include "base/globals.h"
+#include "base/os.h"
+#include "file_utils.h"
+#include "fmt/format.h"
 #include "oat_file_assistant.h"
 #include "oat_file_assistant_context.h"
 #include "path_utils.h"
+#include "selinux/android.h"
+#include "tools/cmdline_builder.h"
 #include "tools/tools.h"
 
 namespace art {
@@ -48,16 +66,37 @@
 namespace {
 
 using ::aidl::com::android::server::art::ArtifactsPath;
+using ::aidl::com::android::server::art::DexoptOptions;
+using ::aidl::com::android::server::art::DexoptTrigger;
+using ::aidl::com::android::server::art::FsPermission;
+using ::aidl::com::android::server::art::GetDexoptNeededResult;
 using ::aidl::com::android::server::art::GetOptimizationStatusResult;
+using ::aidl::com::android::server::art::OutputArtifacts;
+using ::aidl::com::android::server::art::PriorityClass;
+using ::aidl::com::android::server::art::ProfilePath;
+using ::aidl::com::android::server::art::VdexPath;
+using ::android::base::Dirname;
 using ::android::base::Error;
+using ::android::base::Join;
+using ::android::base::make_scope_guard;
 using ::android::base::Result;
 using ::android::base::Split;
-using ::android::base::StringPrintf;
 using ::android::base::StringReplace;
+using ::art::tools::CmdlineBuilder;
 using ::ndk::ScopedAStatus;
 
+using ::fmt::literals::operator""_format;  // NOLINT
+
+using ArtifactsLocation = GetDexoptNeededResult::ArtifactsLocation;
+
 constexpr const char* kServiceName = "artd";
 
+// Timeout for long operations, such as compilation. We set it to be smaller than the Package
+// Manager watchdog (PackageManagerService.WATCHDOG_TIMEOUT, 10 minutes), so that if the operation
+// is called from the Package Manager's thread handler, it will be aborted before that watchdog
+// would take down the system server.
+constexpr int kLongTimeoutSec = 570;  // 9.5 minutes.
+
 // Deletes a file. Returns the size of the deleted file, or 0 if the deleted file is empty or an
 // error occurs.
 int64_t GetSizeAndDeleteFile(const std::string& path) {
@@ -66,14 +105,13 @@
   if (ec) {
     // It is okay if the file does not exist. We don't have to log it.
     if (ec.value() != ENOENT) {
-      LOG(ERROR) << StringPrintf(
-          "Failed to get the file size of '%s': %s", path.c_str(), ec.message().c_str());
+      LOG(ERROR) << "Failed to get the file size of '{}': {}"_format(path, ec.message());
     }
     return 0;
   }
 
   if (!std::filesystem::remove(path, ec)) {
-    LOG(ERROR) << StringPrintf("Failed to remove '%s': %s", path.c_str(), ec.message().c_str());
+    LOG(ERROR) << "Failed to remove '{}': {}"_format(path, ec.message());
     return 0;
   }
 
@@ -103,6 +141,123 @@
                                                             EscapeErrorMessage(message).c_str());
 }
 
+Result<CompilerFilter::Filter> ParseCompilerFilter(const std::string& compiler_filter_str) {
+  CompilerFilter::Filter compiler_filter;
+  if (!CompilerFilter::ParseCompilerFilter(compiler_filter_str.c_str(), &compiler_filter)) {
+    return Errorf("Failed to parse compiler filter '{}'", compiler_filter_str);
+  }
+  return compiler_filter;
+}
+
+OatFileAssistant::DexOptTrigger DexOptTriggerFromAidl(int8_t aidl_value) {
+  OatFileAssistant::DexOptTrigger trigger{};
+  if ((aidl_value & static_cast<int8_t>(DexoptTrigger::COMPILER_FILTER_IS_BETTER)) != 0) {
+    trigger.targetFilterIsBetter = true;
+  }
+  if ((aidl_value & static_cast<int8_t>(DexoptTrigger::COMPILER_FILTER_IS_SAME)) != 0) {
+    trigger.targetFilterIsSame = true;
+  }
+  if ((aidl_value & static_cast<int8_t>(DexoptTrigger::COMPILER_FILTER_IS_WORSE)) != 0) {
+    trigger.targetFilterIsWorse = true;
+  }
+  if ((aidl_value & static_cast<int8_t>(DexoptTrigger::PRIMARY_BOOT_IMAGE_BECOMES_USABLE)) != 0) {
+    trigger.primaryBootImageBecomesUsable = true;
+  }
+  return trigger;
+}
+
+ArtifactsLocation ArtifactsLocationToAidl(OatFileAssistant::Location location) {
+  switch (location) {
+    case OatFileAssistant::Location::kLocationNoneOrError:
+      return ArtifactsLocation::NONE_OR_ERROR;
+    case OatFileAssistant::Location::kLocationOat:
+      return ArtifactsLocation::DALVIK_CACHE;
+    case OatFileAssistant::Location::kLocationOdex:
+      return ArtifactsLocation::NEXT_TO_DEX;
+    case OatFileAssistant::Location::kLocationDm:
+      return ArtifactsLocation::DM;
+      // No default. All cases should be explicitly handled, or the compilation will fail.
+  }
+  // This should never happen. Just in case we get a non-enumerator value.
+  LOG(FATAL) << "Unexpected Location " << location;
+}
+
+Result<void> PrepareArtifactsDir(
+    const std::string& path,
+    const FsPermission& fs_permission,
+    const std::optional<OutputArtifacts::PermissionSettings::SeContext>& se_context =
+        std::nullopt) {
+  std::error_code ec;
+  bool created = std::filesystem::create_directory(path, ec);
+  if (ec) {
+    return Errorf("Failed to create directory '{}': {}", path, ec.message());
+  }
+
+  auto cleanup = make_scope_guard([&] {
+    if (created) {
+      std::filesystem::remove(path, ec);
+    }
+  });
+
+  if (chmod(path.c_str(), DirFsPermissionToMode(fs_permission)) != 0) {
+    return ErrnoErrorf("Failed to chmod directory '{}'", path);
+  }
+  OR_RETURN(Chown(path, fs_permission));
+
+  if (kIsTargetAndroid) {
+    int res = 0;
+    if (se_context.has_value()) {
+      res = selinux_android_restorecon_pkgdir(path.c_str(),
+                                              se_context->seInfo.c_str(),
+                                              se_context->packageUid,
+                                              SELINUX_ANDROID_RESTORECON_RECURSE);
+    } else {
+      res = selinux_android_restorecon(path.c_str(), SELINUX_ANDROID_RESTORECON_RECURSE);
+    }
+    if (res != 0) {
+      return ErrnoErrorf("Failed to restorecon directory '{}'", path);
+    }
+  }
+
+  cleanup.Disable();
+  return {};
+}
+
+Result<void> PrepareArtifactsDirs(const OutputArtifacts& output_artifacts) {
+  if (output_artifacts.artifactsPath.isInDalvikCache) {
+    return {};
+  }
+
+  std::filesystem::path oat_path(OR_RETURN(BuildOatPath(output_artifacts.artifactsPath)));
+  std::filesystem::path isa_dir = oat_path.parent_path();
+  std::filesystem::path oat_dir = isa_dir.parent_path();
+  DCHECK_EQ(oat_dir.filename(), "oat");
+
+  OR_RETURN(PrepareArtifactsDir(oat_dir,
+                                output_artifacts.permissionSettings.dirFsPermission,
+                                output_artifacts.permissionSettings.seContext));
+  OR_RETURN(PrepareArtifactsDir(isa_dir, output_artifacts.permissionSettings.dirFsPermission));
+  return {};
+}
+
+class FdLogger {
+ public:
+  void Add(const NewFile& file) { fd_mapping_.emplace_back(file.Fd(), file.TempPath()); }
+  void Add(const File& file) { fd_mapping_.emplace_back(file.Fd(), file.GetPath()); }
+
+ private:
+  std::vector<std::pair<int, std::string>> fd_mapping_;
+
+  friend std::ostream& operator<<(std::ostream& os, const FdLogger& fd_logger);
+};
+
+std::ostream& operator<<(std::ostream& os, const FdLogger& fd_logger) {
+  for (const auto& [fd, path] : fd_logger.fd_mapping_) {
+    os << fd << ":" << path << ' ';
+  }
+  return os;
+}
+
 }  // namespace
 
 #define OR_RETURN_ERROR(func, expr)         \
@@ -172,6 +327,173 @@
   return ScopedAStatus::ok();
 }
 
+ndk::ScopedAStatus Artd::getDexoptNeeded(const std::string& in_dexFile,
+                                         const std::string& in_instructionSet,
+                                         const std::string& in_classLoaderContext,
+                                         const std::string& in_compilerFilter,
+                                         int8_t in_dexoptTrigger,
+                                         GetDexoptNeededResult* _aidl_return) {
+  Result<OatFileAssistantContext*> ofa_context = GetOatFileAssistantContext();
+  if (!ofa_context.ok()) {
+    return NonFatal("Failed to get runtime options: " + ofa_context.error().message());
+  }
+
+  std::unique_ptr<ClassLoaderContext> context;
+  std::string error_msg;
+  auto oat_file_assistant = OatFileAssistant::Create(in_dexFile.c_str(),
+                                                     in_instructionSet.c_str(),
+                                                     in_classLoaderContext.c_str(),
+                                                     /*load_executable=*/false,
+                                                     /*only_load_trusted_executable=*/true,
+                                                     ofa_context.value(),
+                                                     &context,
+                                                     &error_msg);
+  if (oat_file_assistant == nullptr) {
+    return NonFatal("Failed to create OatFileAssistant: " + error_msg);
+  }
+
+  OatFileAssistant::DexOptStatus status;
+  _aidl_return->isDexoptNeeded =
+      oat_file_assistant->GetDexOptNeeded(OR_RETURN_FATAL(ParseCompilerFilter(in_compilerFilter)),
+                                          DexOptTriggerFromAidl(in_dexoptTrigger),
+                                          &status);
+  _aidl_return->isVdexUsable = status.IsVdexUsable();
+  _aidl_return->artifactsLocation = ArtifactsLocationToAidl(status.GetLocation());
+
+  return ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus Artd::dexopt(const OutputArtifacts& in_outputArtifacts,
+                                const std::string& in_dexFile,
+                                const std::string& in_instructionSet,
+                                const std::string& in_classLoaderContext,
+                                const std::string& in_compilerFilter,
+                                const std::optional<ProfilePath>& in_profile,
+                                const std::optional<VdexPath>& in_inputVdex,
+                                PriorityClass in_priorityClass,
+                                const DexoptOptions& in_dexoptOptions,
+                                bool* _aidl_return) {
+  std::string oat_path = OR_RETURN_FATAL(BuildOatPath(in_outputArtifacts.artifactsPath));
+  std::string vdex_path = OatPathToVdexPath(oat_path);
+  std::string art_path = OatPathToArtPath(oat_path);
+  OR_RETURN_FATAL(ValidateDexPath(in_dexFile));
+  if (in_profile.has_value()) {
+    return Fatal("Profile-guided compilation not implemented");
+  }
+  std::optional<std::string> profile_path = std::nullopt;
+
+  std::unique_ptr<ClassLoaderContext> context =
+      ClassLoaderContext::Create(in_classLoaderContext.c_str());
+  if (context == nullptr) {
+    return Fatal("Class loader context '{}' is invalid"_format(in_classLoaderContext));
+  }
+
+  OR_RETURN_NON_FATAL(PrepareArtifactsDirs(in_outputArtifacts));
+
+  CmdlineBuilder args;
+  args.Add(OR_RETURN_FATAL(GetArtExec())).Add("--drop-capabilities");
+
+  if (in_priorityClass < PriorityClass::BOOT) {
+    args.Add("--set-task-profile=Dex2OatBootComplete").Add("--set-priority=background");
+  }
+
+  args.Add("--").Add(OR_RETURN_FATAL(GetDex2Oat()));
+  FdLogger fd_logger;
+
+  const FsPermission& fs_permission = in_outputArtifacts.permissionSettings.fileFsPermission;
+  std::unique_ptr<NewFile> oat_file = OR_RETURN_NON_FATAL(NewFile::Create(oat_path, fs_permission));
+  args.Add("--oat-fd=%d", oat_file->Fd()).Add("--oat-location=%s", oat_path);
+  fd_logger.Add(*oat_file);
+
+  std::unique_ptr<NewFile> vdex_file =
+      OR_RETURN_NON_FATAL(NewFile::Create(vdex_path, fs_permission));
+  args.Add("--output-vdex-fd=%d", vdex_file->Fd());
+  fd_logger.Add(*vdex_file);
+
+  std::vector<NewFile*> files_to_commit{oat_file.get(), vdex_file.get()};
+  std::vector<std::string_view> files_to_delete;
+
+  std::unique_ptr<NewFile> art_file = nullptr;
+  if (in_dexoptOptions.generateAppImage) {
+    art_file = OR_RETURN_NON_FATAL(NewFile::Create(art_path, fs_permission));
+    args.Add("--app-image-fd=%d", art_file->Fd());
+    fd_logger.Add(*art_file);
+    files_to_commit.push_back(art_file.get());
+  } else {
+    files_to_delete.push_back(art_path);
+  }
+
+  std::unique_ptr<NewFile> swap_file = nullptr;
+  if (ShouldCreateSwapFileForDexopt()) {
+    swap_file = OR_RETURN_NON_FATAL(
+        NewFile::Create("{}.swap"_format(oat_path), FsPermission{.uid = -1, .gid = -1}));
+    args.Add("--swap-fd=%d", swap_file->Fd());
+    fd_logger.Add(*swap_file);
+  }
+
+  std::unique_ptr<File> dex_file = OR_RETURN_NON_FATAL(OpenFileForReading(in_dexFile));
+  args.Add("--zip-fd=%d", dex_file->Fd()).Add("--zip-location=%s", in_dexFile);
+  fd_logger.Add(*dex_file);
+
+  std::vector<std::string> flattened_context = context->FlattenDexPaths();
+  std::string dex_dir = Dirname(in_dexFile.c_str());
+  std::vector<std::unique_ptr<File>> context_files;
+  std::vector<int> context_fds;
+  for (const std::string& context_element : flattened_context) {
+    std::string context_path = std::filesystem::path(dex_dir).append(context_element);
+    OR_RETURN_FATAL(ValidateDexPath(context_path));
+    std::unique_ptr<File> context_file = OR_RETURN_NON_FATAL(OpenFileForReading(context_path));
+    context_fds.push_back(context_file->Fd());
+    fd_logger.Add(*context_file);
+    context_files.push_back(std::move(context_file));
+  }
+  args.Add("--class-loader-context-fds=%s", Join(context_fds, /*separator=*/':'))
+      .Add("--class-loader-context=%s", in_classLoaderContext)
+      .Add("--classpath-dir=%s", dex_dir);
+
+  std::unique_ptr<File> input_vdex_file = nullptr;
+  if (in_inputVdex.has_value()) {
+    if (in_inputVdex->getTag() == VdexPath::dexMetadataPath) {
+      std::string input_vdex_path = OR_RETURN_FATAL(BuildDexMetadataPath(in_inputVdex.value()));
+      input_vdex_file = OR_RETURN_NON_FATAL(OpenFileForReading(input_vdex_path));
+      args.Add("--dm-fd=%d", input_vdex_file->Fd());
+    } else {
+      std::string input_vdex_path = OR_RETURN_FATAL(BuildVdexPath(in_inputVdex.value()));
+      input_vdex_file = OR_RETURN_NON_FATAL(OpenFileForReading(input_vdex_path));
+      args.Add("--input-vdex-fd=%d", input_vdex_file->Fd());
+    }
+    fd_logger.Add(*input_vdex_file);
+  }
+
+  std::unique_ptr<File> profile_file = nullptr;
+  if (profile_path.has_value()) {
+    profile_file = OR_RETURN_NON_FATAL(OpenFileForReading(profile_path.value()));
+    args.Add("--profile-file-fd=%d", profile_file->Fd());
+    fd_logger.Add(*profile_file);
+  }
+
+  AddCompilerConfigFlags(
+      in_instructionSet, in_compilerFilter, in_priorityClass, in_dexoptOptions, args);
+  AddPerfConfigFlags(in_priorityClass, args);
+
+  LOG(INFO) << "Running dex2oat: " << Join(args.Get(), /*separator=*/" ")
+            << "\nOpened FDs: " << fd_logger;
+
+  Result<int> result = ExecAndReturnCode(args.Get(), kLongTimeoutSec);
+  if (!result.ok()) {
+    // TODO(b/244412198): Return false if dexopt is cancelled upon request.
+    return NonFatal("Failed to run dex2oat: " + result.error().message());
+  }
+  if (result.value() != 0) {
+    return NonFatal("dex2oat returned an unexpected code: %d"_format(result.value()));
+  }
+
+  NewFile::CommitAllOrAbandon(files_to_commit, files_to_delete);
+
+  *_aidl_return = true;
+  return ScopedAStatus::ok();
+}
+
 Result<void> Artd::Start() {
   ScopedAStatus status = ScopedAStatus::fromStatus(
       AServiceManager_registerLazyService(this->asBinder().get(), kServiceName));
@@ -254,5 +576,100 @@
   return cached_deny_art_apex_data_files_.value();
 }
 
+Result<std::string> Artd::GetArtExec() { return BuildArtBinPath("art_exec"); }
+
+bool Artd::ShouldUseDex2Oat64() {
+  return !props_->GetOrEmpty("ro.product.cpu.abilist64").empty() &&
+         props_->GetBool("dalvik.vm.dex2oat64.enabled", /*default_value=*/false);
+}
+
+Result<std::string> Artd::GetDex2Oat() {
+  std::string binary_name = ShouldUseDex2Oat64() ? "dex2oat64" : "dex2oat32";
+  // TODO(b/234351700): Should we use the "d" variant?
+  return BuildArtBinPath(binary_name);
+}
+
+bool Artd::ShouldCreateSwapFileForDexopt() {
+  // Create a swap file by default. Dex2oat will decide whether to use it or not.
+  return props_->GetBool("dalvik.vm.dex2oat-swap", /*default_value=*/true);
+}
+
+void Artd::AddCompilerConfigFlags(const std::string& instruction_set,
+                                  const std::string& compiler_filter,
+                                  PriorityClass priority_class,
+                                  const DexoptOptions& dexopt_options,
+                                  /*out*/ CmdlineBuilder& args) {
+  args.Add("--instruction-set=%s", instruction_set);
+  std::string features_prop = "dalvik.vm.isa.{}.features"_format(instruction_set);
+  args.AddIfNonEmpty("--instruction-set-features=%s", props_->GetOrEmpty(features_prop));
+  std::string variant_prop = "dalvik.vm.isa.{}.variant"_format(instruction_set);
+  args.AddIfNonEmpty("--instruction-set-variant=%s", props_->GetOrEmpty(variant_prop));
+
+  args.Add("--compiler-filter=%s", compiler_filter)
+      .Add("--compilation-reason=%s", dexopt_options.compilationReason);
+
+  args.AddIf(priority_class >= PriorityClass::INTERACTIVE, "--compact-dex-level=none");
+
+  args.AddIfNonEmpty("--max-image-block-size=%s",
+                     props_->GetOrEmpty("dalvik.vm.dex2oat-max-image-block-size"))
+      .AddIfNonEmpty("--very-large-app-threshold=%s",
+                     props_->GetOrEmpty("dalvik.vm.dex2oat-very-large"))
+      .AddIfNonEmpty(
+          "--resolve-startup-const-strings=%s",
+          props_->GetOrEmpty("persist.device_config.runtime.dex2oat_resolve_startup_strings",
+                             "dalvik.vm.dex2oat-resolve-startup-strings"));
+
+  args.AddIf(dexopt_options.debuggable, "--debuggable")
+      .AddIf(props_->GetBool("debug.generate-debug-info", /*default_value=*/false),
+             "--generate-debug-info")
+      .AddIf(props_->GetBool("dalvik.vm.dex2oat-minidebuginfo", /*default_value=*/false),
+             "--generate-mini-debug-info");
+
+  args.AddRuntimeIf(DenyArtApexDataFiles(), "-Xdeny-art-apex-data-files")
+      .AddRuntime("-Xtarget-sdk-version:%d", dexopt_options.targetSdkVersion)
+      .AddRuntimeIf(dexopt_options.hiddenApiPolicyEnabled, "-Xhidden-api-policy:enabled");
+}
+
+void Artd::AddPerfConfigFlags(PriorityClass priority_class, /*out*/ CmdlineBuilder& args) {
+  // CPU set and number of threads.
+  std::string default_cpu_set_prop = "dalvik.vm.dex2oat-cpu-set";
+  std::string default_threads_prop = "dalvik.vm.dex2oat-threads";
+  std::string cpu_set;
+  std::string threads;
+  if (priority_class >= PriorityClass::BOOT) {
+    cpu_set = props_->GetOrEmpty("dalvik.vm.boot-dex2oat-cpu-set");
+    threads = props_->GetOrEmpty("dalvik.vm.boot-dex2oat-threads");
+  } else if (priority_class >= PriorityClass::INTERACTIVE_FAST) {
+    cpu_set = props_->GetOrEmpty("dalvik.vm.restore-dex2oat-cpu-set", default_cpu_set_prop);
+    threads = props_->GetOrEmpty("dalvik.vm.restore-dex2oat-threads", default_threads_prop);
+  } else if (priority_class <= PriorityClass::BACKGROUND) {
+    cpu_set = props_->GetOrEmpty("dalvik.vm.background-dex2oat-cpu-set", default_cpu_set_prop);
+    threads = props_->GetOrEmpty("dalvik.vm.background-dex2oat-threads", default_threads_prop);
+  } else {
+    cpu_set = props_->GetOrEmpty(default_cpu_set_prop);
+    threads = props_->GetOrEmpty(default_threads_prop);
+  }
+  args.AddIfNonEmpty("--cpu-set=%s", cpu_set).AddIfNonEmpty("-j%s", threads);
+
+  args.AddRuntimeIfNonEmpty("-Xms%s", props_->GetOrEmpty("dalvik.vm.dex2oat-Xms"))
+      .AddRuntimeIfNonEmpty("-Xmx%s", props_->GetOrEmpty("dalvik.vm.dex2oat-Xmx"));
+
+  // Enable compiling dex files in isolation on low ram devices.
+  // It takes longer but reduces the memory footprint.
+  args.AddIf(props_->GetBool("ro.config.low_ram", /*default_value=*/false),
+             "--compile-individually");
+}
+
+android::base::Result<int> Artd::ExecAndReturnCode(const std::vector<std::string>& args,
+                                                   int timeout_sec) const {
+  bool ignored_timed_out = false;  // This information is encoded in the error message.
+  std::string error_msg;
+  int exit_code = exec_utils_->ExecAndReturnCode(args, timeout_sec, &ignored_timed_out, &error_msg);
+  if (exit_code < 0) {
+    return Error() << error_msg;
+  }
+  return exit_code;
+}
+
 }  // namespace artd
 }  // namespace art
diff --git a/artd/artd.h b/artd/artd.h
index 25a4c86..54d6776 100644
--- a/artd/artd.h
+++ b/artd/artd.h
@@ -17,14 +17,18 @@
 #ifndef ART_ARTD_ARTD_H_
 #define ART_ARTD_ARTD_H_
 
+#include <cstdint>
 #include <memory>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "aidl/com/android/server/art/BnArtd.h"
 #include "android-base/result.h"
 #include "android/binder_auto_utils.h"
+#include "exec_utils.h"
 #include "oat_file_assistant_context.h"
+#include "tools/cmdline_builder.h"
 #include "tools/system_properties.h"
 
 namespace art {
@@ -33,8 +37,9 @@
 class Artd : public aidl::com::android::server::art::BnArtd {
  public:
   explicit Artd(std::unique_ptr<art::tools::SystemProperties> props =
-                    std::make_unique<art::tools::SystemProperties>())
-      : props_(std::move(props)) {}
+                    std::make_unique<art::tools::SystemProperties>(),
+                std::unique_ptr<ExecUtils> exec_utils = std::make_unique<ExecUtils>())
+      : props_(std::move(props)), exec_utils_(std::move(exec_utils)) {}
 
   ndk::ScopedAStatus isAlive(bool* _aidl_return) override;
 
@@ -48,6 +53,26 @@
       const std::string& in_classLoaderContext,
       aidl::com::android::server::art::GetOptimizationStatusResult* _aidl_return) override;
 
+  ndk::ScopedAStatus getDexoptNeeded(
+      const std::string& in_dexFile,
+      const std::string& in_instructionSet,
+      const std::string& in_classLoaderContext,
+      const std::string& in_compilerFilter,
+      int8_t in_dexoptTrigger,
+      aidl::com::android::server::art::GetDexoptNeededResult* _aidl_return) override;
+
+  ndk::ScopedAStatus dexopt(
+      const aidl::com::android::server::art::OutputArtifacts& in_outputArtifacts,
+      const std::string& in_dexFile,
+      const std::string& in_instructionSet,
+      const std::string& in_classLoaderContext,
+      const std::string& in_compilerFilter,
+      const std::optional<aidl::com::android::server::art::ProfilePath>& in_profile,
+      const std::optional<aidl::com::android::server::art::VdexPath>& in_inputVdex,
+      aidl::com::android::server::art::PriorityClass in_priorityClass,
+      const aidl::com::android::server::art::DexoptOptions& in_dexoptOptions,
+      bool* _aidl_return) override;
+
   android::base::Result<void> Start();
 
  private:
@@ -61,6 +86,26 @@
 
   bool DenyArtApexDataFiles();
 
+  android::base::Result<int> ExecAndReturnCode(const std::vector<std::string>& arg_vector,
+                                               int timeout_sec) const;
+
+  android::base::Result<std::string> GetArtExec();
+
+  bool ShouldUseDex2Oat64();
+
+  android::base::Result<std::string> GetDex2Oat();
+
+  bool ShouldCreateSwapFileForDexopt();
+
+  void AddCompilerConfigFlags(const std::string& instruction_set,
+                              const std::string& compiler_filter,
+                              aidl::com::android::server::art::PriorityClass priority_class,
+                              const aidl::com::android::server::art::DexoptOptions& dexopt_options,
+                              /*out*/ art::tools::CmdlineBuilder& args);
+
+  void AddPerfConfigFlags(aidl::com::android::server::art::PriorityClass priority_class,
+                          /*out*/ art::tools::CmdlineBuilder& args);
+
   std::optional<std::vector<std::string>> cached_boot_image_locations_;
   std::optional<std::vector<std::string>> cached_boot_class_path_;
   std::optional<std::string> cached_apex_versions_;
@@ -70,6 +115,7 @@
   std::unique_ptr<OatFileAssistantContext> ofa_context_;
 
   std::unique_ptr<art::tools::SystemProperties> props_;
+  std::unique_ptr<ExecUtils> exec_utils_;
 };
 
 }  // namespace artd
diff --git a/artd/artd_test.cc b/artd/artd_test.cc
index 129e31c..8fd9e96 100644
--- a/artd/artd_test.cc
+++ b/artd/artd_test.cc
@@ -16,29 +16,67 @@
 
 #include "artd.h"
 
+#include <algorithm>
 #include <filesystem>
 #include <functional>
 #include <memory>
+#include <optional>
+#include <string>
+#include <type_traits>
+#include <vector>
 
+#include "aidl/com/android/server/art/BnArtd.h"
+#include "android-base/errors.h"
 #include "android-base/file.h"
 #include "android-base/logging.h"
+#include "android-base/parseint.h"
+#include "android-base/result.h"
 #include "android-base/scopeguard.h"
-#include "android/binder_interface_utils.h"
+#include "android-base/strings.h"
+#include "android/binder_auto_utils.h"
+#include "android/binder_status.h"
 #include "base/common_art_test.h"
+#include "exec_utils.h"
+#include "fmt/format.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "path_utils.h"
+#include "tools/system_properties.h"
 
 namespace art {
 namespace artd {
 namespace {
 
 using ::aidl::com::android::server::art::ArtifactsPath;
+using ::aidl::com::android::server::art::DexMetadataPath;
+using ::aidl::com::android::server::art::DexoptOptions;
+using ::aidl::com::android::server::art::FsPermission;
+using ::aidl::com::android::server::art::OutputArtifacts;
+using ::aidl::com::android::server::art::PriorityClass;
+using ::aidl::com::android::server::art::VdexPath;
 using ::android::base::make_scope_guard;
+using ::android::base::ParseInt;
+using ::android::base::ReadFileToString;
+using ::android::base::Result;
 using ::android::base::ScopeGuard;
+using ::android::base::Split;
+using ::android::base::WriteStringToFd;
+using ::android::base::WriteStringToFile;
 using ::testing::_;
+using ::testing::AllOf;
+using ::testing::AnyNumber;
+using ::testing::Contains;
 using ::testing::ContainsRegex;
+using ::testing::DoAll;
+using ::testing::ElementsAre;
 using ::testing::HasSubstr;
 using ::testing::MockFunction;
+using ::testing::Not;
+using ::testing::ResultOf;
+using ::testing::Return;
+using ::testing::WithArg;
+
+using ::fmt::literals::operator""_format;  // NOLINT
 
 ScopeGuard<std::function<void()>> ScopedSetLogger(android::base::LogFunction&& logger) {
   android::base::LogFunction old_logger = android::base::SetLogger(std::move(logger));
@@ -47,12 +85,143 @@
   });
 }
 
+void CheckContent(const std::string& path, const std::string& expected_content) {
+  std::string actual_content;
+  ASSERT_TRUE(ReadFileToString(path, &actual_content));
+  EXPECT_EQ(actual_content, expected_content);
+}
+
+// Writes `content` to the FD specified by the `flag`.
+ACTION_P(WriteToFdFlag, flag, content) {
+  for (const std::string& arg : arg0) {
+    std::string_view value(arg);
+    if (android::base::ConsumePrefix(&value, flag)) {
+      int fd;
+      ASSERT_TRUE(ParseInt(std::string(value), &fd));
+      ASSERT_TRUE(WriteStringToFd(content, fd));
+      return;
+    }
+  }
+  FAIL() << "Flag '{}' not found"_format(flag);
+}
+
+// Matches a flag that starts with `flag` and whose value matches `matcher`.
+MATCHER_P2(Flag, flag, matcher, "") {
+  std::string_view value(arg);
+  if (!android::base::ConsumePrefix(&value, flag)) {
+    return false;
+  }
+  return ExplainMatchResult(matcher, std::string(value), result_listener);
+}
+
+// Matches a flag that starts with `flag` and whose value is a colon-separated list that matches
+// `matcher`. The matcher acts on an `std::vector<std::string>` of the split list argument.
+MATCHER_P2(ListFlag, flag, matcher, "") {
+  return ExplainMatchResult(
+      Flag(flag, ResultOf(std::bind(Split, std::placeholders::_1, ":"), matcher)),
+      arg,
+      result_listener);
+}
+
+// Matches an FD of a file whose path matches `matcher`.
+MATCHER_P(FdOf, matcher, "") {
+  int fd;
+  if (!ParseInt(arg, &fd)) {
+    return false;
+  }
+  std::string proc_path = "/proc/self/fd/{}"_format(fd);
+  char path[PATH_MAX];
+  ssize_t len = readlink(proc_path.c_str(), path, sizeof(path));
+  if (len < 0) {
+    return false;
+  }
+  return ExplainMatchResult(matcher, std::string(path, static_cast<size_t>(len)), result_listener);
+}
+
+// Matches a container that, when split by `separator`, the first part matches `head_matcher`, and
+// the second part matches `tail_matcher`.
+MATCHER_P3(WhenSplitBy, separator, head_matcher, tail_matcher, "") {
+  using Value = const typename std::remove_reference<decltype(arg)>::type::value_type;
+  auto it = std::find(arg.begin(), arg.end(), separator);
+  if (it == arg.end()) {
+    return false;
+  }
+  size_t pos = it - arg.begin();
+  return ExplainMatchResult(head_matcher, ArrayRef<Value>(arg).SubArray(0, pos), result_listener) &&
+         ExplainMatchResult(tail_matcher, ArrayRef<Value>(arg).SubArray(pos + 1), result_listener);
+}
+
+class MockSystemProperties : public tools::SystemProperties {
+ public:
+  MOCK_METHOD(std::string, GetProperty, (const std::string& key), (const, override));
+};
+
+class MockExecUtils : public ExecUtils {
+ public:
+  // A workaround to avoid MOCK_METHOD on a method with an `std::string*` parameter, which will lead
+  // to a conflict between gmock and android-base/logging.h (b/132668253).
+  int ExecAndReturnCode(const std::vector<std::string>& arg_vector,
+                        int,
+                        bool*,
+                        std::string*) const override {
+    return DoExecAndReturnCode(arg_vector);
+  }
+
+  MOCK_METHOD(int, DoExecAndReturnCode, (const std::vector<std::string>& arg_vector), (const));
+};
+
 class ArtdTest : public CommonArtTest {
  protected:
   void SetUp() override {
     CommonArtTest::SetUp();
-    artd_ = ndk::SharedRefBase::make<Artd>();
+    auto mock_props = std::make_unique<MockSystemProperties>();
+    mock_props_ = mock_props.get();
+    EXPECT_CALL(*mock_props_, GetProperty).Times(AnyNumber()).WillRepeatedly(Return(""));
+    auto mock_exec_utils = std::make_unique<MockExecUtils>();
+    mock_exec_utils_ = mock_exec_utils.get();
+    artd_ = ndk::SharedRefBase::make<Artd>(std::move(mock_props), std::move(mock_exec_utils));
     scratch_dir_ = std::make_unique<ScratchDir>();
+    scratch_path_ = scratch_dir_->GetPath();
+    // Remove the trailing '/';
+    scratch_path_.resize(scratch_path_.length() - 1);
+
+    // Use an arbitrary existing directory as ART root.
+    art_root_ = scratch_path_ + "/com.android.art";
+    std::filesystem::create_directories(art_root_);
+    setenv("ANDROID_ART_ROOT", art_root_.c_str(), /*overwrite=*/1);
+
+    dex_file_ = scratch_path_ + "/a/b.apk";
+    isa_ = "arm64";
+    artifacts_path_ = ArtifactsPath{
+        .dexPath = dex_file_,
+        .isa = isa_,
+        .isInDalvikCache = false,
+    };
+    struct stat st;
+    ASSERT_EQ(stat(scratch_path_.c_str(), &st), 0);
+    output_artifacts_ = OutputArtifacts{
+        .artifactsPath = artifacts_path_,
+        .permissionSettings =
+            OutputArtifacts::PermissionSettings{
+                .dirFsPermission =
+                    FsPermission{
+                        .uid = static_cast<int32_t>(st.st_uid),
+                        .gid = static_cast<int32_t>(st.st_gid),
+                        .isOtherReadable = true,
+                        .isOtherExecutable = true,
+                    },
+                .fileFsPermission =
+                    FsPermission{
+                        .uid = static_cast<int32_t>(st.st_uid),
+                        .gid = static_cast<int32_t>(st.st_gid),
+                        .isOtherReadable = true,
+                    },
+            },
+    };
+    clc_1_ = GetTestDexFileName("Main");
+    clc_2_ = GetTestDexFileName("Nested");
+    class_loader_context_ = "PCL[{}:{}]"_format(clc_1_, clc_2_);
+    compiler_filter_ = "speed";
   }
 
   void TearDown() override {
@@ -60,9 +229,63 @@
     CommonArtTest::TearDown();
   }
 
+  void RunDexopt(binder_exception_t expected_status = EX_NONE, bool expected_aidl_return = true) {
+    InitDexoptInputFiles();
+    bool aidl_return;
+    ndk::ScopedAStatus status = artd_->dexopt(output_artifacts_,
+                                              dex_file_,
+                                              isa_,
+                                              class_loader_context_,
+                                              compiler_filter_,
+                                              /*in_profile=*/std::nullopt,
+                                              vdex_path_,
+                                              priority_class_,
+                                              dexopt_options_,
+                                              &aidl_return);
+    ASSERT_EQ(status.getExceptionCode(), expected_status) << status.getMessage();
+    if (status.isOk()) {
+      ASSERT_EQ(aidl_return, expected_aidl_return);
+    }
+  }
+
   std::shared_ptr<Artd> artd_;
   std::unique_ptr<ScratchDir> scratch_dir_;
+  std::string scratch_path_;
+  std::string art_root_;
   MockFunction<android::base::LogFunction> mock_logger_;
+  ScopedUnsetEnvironmentVariable art_root_env_ = ScopedUnsetEnvironmentVariable("ANDROID_ART_ROOT");
+  MockSystemProperties* mock_props_;
+  MockExecUtils* mock_exec_utils_;
+
+  std::string dex_file_;
+  std::string isa_;
+  ArtifactsPath artifacts_path_;
+  OutputArtifacts output_artifacts_;
+  std::string clc_1_;
+  std::string clc_2_;
+  std::string class_loader_context_;
+  std::string compiler_filter_;
+  std::optional<VdexPath> vdex_path_;
+  PriorityClass priority_class_ = PriorityClass::BACKGROUND;
+  DexoptOptions dexopt_options_;
+
+ private:
+  void CreateFile(const std::string& filename) {
+    std::filesystem::path path(filename);
+    std::filesystem::create_directories(path.parent_path());
+    WriteStringToFile("", filename);
+  }
+
+  void InitDexoptInputFiles() {
+    CreateFile(dex_file_);
+    if (vdex_path_.has_value()) {
+      if (vdex_path_->getTag() == VdexPath::dexMetadataPath) {
+        CreateFile(OR_FATAL(BuildDexMetadataPath(vdex_path_.value())));
+      } else {
+        CreateFile(OR_FATAL(BuildVdexPath(vdex_path_.value())));
+      }
+    }
+  }
 };
 
 TEST_F(ArtdTest, isAlive) {
@@ -72,22 +295,14 @@
 }
 
 TEST_F(ArtdTest, deleteArtifacts) {
-  std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64";
+  std::string oat_dir = scratch_path_ + "/a/oat/arm64";
   std::filesystem::create_directories(oat_dir);
-  android::base::WriteStringToFile("abcd", oat_dir + "/b.odex");  // 4 bytes.
-  android::base::WriteStringToFile("ab", oat_dir + "/b.vdex");    // 2 bytes.
-  android::base::WriteStringToFile("a", oat_dir + "/b.art");      // 1 byte.
+  WriteStringToFile("abcd", oat_dir + "/b.odex");  // 4 bytes.
+  WriteStringToFile("ab", oat_dir + "/b.vdex");    // 2 bytes.
+  WriteStringToFile("a", oat_dir + "/b.art");      // 1 byte.
 
   int64_t result = -1;
-  EXPECT_TRUE(artd_
-                  ->deleteArtifacts(
-                      ArtifactsPath{
-                          .dexPath = scratch_dir_->GetPath() + "/a/b.apk",
-                          .isa = "arm64",
-                          .isInDalvikCache = false,
-                      },
-                      &result)
-                  .isOk());
+  EXPECT_TRUE(artd_->deleteArtifacts(artifacts_path_, &result).isOk());
   EXPECT_EQ(result, 4 + 2 + 1);
 
   EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.odex"));
@@ -99,8 +314,8 @@
   // Missing VDEX file.
   std::string oat_dir = dalvik_cache_ + "/arm64";
   std::filesystem::create_directories(oat_dir);
-  android::base::WriteStringToFile("abcd", oat_dir + "/a@b.apk@classes.dex");  // 4 bytes.
-  android::base::WriteStringToFile("a", oat_dir + "/a@b.apk@classes.art");     // 1 byte.
+  WriteStringToFile("abcd", oat_dir + "/a@b.apk@classes.dex");  // 4 bytes.
+  WriteStringToFile("a", oat_dir + "/a@b.apk@classes.art");     // 1 byte.
 
   auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
   EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0);
@@ -126,24 +341,16 @@
   EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0);
 
   int64_t result = -1;
-  EXPECT_TRUE(artd_
-                  ->deleteArtifacts(
-                      ArtifactsPath{
-                          .dexPath = android_data_ + "/a/b.apk",
-                          .isa = "arm64",
-                          .isInDalvikCache = false,
-                      },
-                      &result)
-                  .isOk());
+  EXPECT_TRUE(artd_->deleteArtifacts(artifacts_path_, &result).isOk());
   EXPECT_EQ(result, 0);
 }
 
 TEST_F(ArtdTest, deleteArtifactsPermissionDenied) {
-  std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64";
+  std::string oat_dir = scratch_path_ + "/a/oat/arm64";
   std::filesystem::create_directories(oat_dir);
-  android::base::WriteStringToFile("abcd", oat_dir + "/b.odex");  // 4 bytes.
-  android::base::WriteStringToFile("ab", oat_dir + "/b.vdex");    // 2 bytes.
-  android::base::WriteStringToFile("a", oat_dir + "/b.art");      // 1 byte.
+  WriteStringToFile("abcd", oat_dir + "/b.odex");  // 4 bytes.
+  WriteStringToFile("ab", oat_dir + "/b.vdex");    // 2 bytes.
+  WriteStringToFile("a", oat_dir + "/b.art");      // 1 byte.
 
   auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
   EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(3);
@@ -152,25 +359,17 @@
   auto scoped_unroot = ScopedUnroot();
 
   int64_t result = -1;
-  EXPECT_TRUE(artd_
-                  ->deleteArtifacts(
-                      ArtifactsPath{
-                          .dexPath = scratch_dir_->GetPath() + "/a/b.apk",
-                          .isa = "arm64",
-                          .isInDalvikCache = false,
-                      },
-                      &result)
-                  .isOk());
+  EXPECT_TRUE(artd_->deleteArtifacts(artifacts_path_, &result).isOk());
   EXPECT_EQ(result, 0);
 }
 
 TEST_F(ArtdTest, deleteArtifactsFileIsDir) {
   // VDEX file is a directory.
-  std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64";
+  std::string oat_dir = scratch_path_ + "/a/oat/arm64";
   std::filesystem::create_directories(oat_dir);
   std::filesystem::create_directories(oat_dir + "/b.vdex");
-  android::base::WriteStringToFile("abcd", oat_dir + "/b.odex");  // 4 bytes.
-  android::base::WriteStringToFile("a", oat_dir + "/b.art");      // 1 byte.
+  WriteStringToFile("abcd", oat_dir + "/b.odex");  // 4 bytes.
+  WriteStringToFile("a", oat_dir + "/b.art");      // 1 byte.
 
   auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
   EXPECT_CALL(mock_logger_,
@@ -178,15 +377,7 @@
       .Times(1);
 
   int64_t result = -1;
-  EXPECT_TRUE(artd_
-                  ->deleteArtifacts(
-                      ArtifactsPath{
-                          .dexPath = scratch_dir_->GetPath() + "/a/b.apk",
-                          .isa = "arm64",
-                          .isInDalvikCache = false,
-                      },
-                      &result)
-                  .isOk());
+  EXPECT_TRUE(artd_->deleteArtifacts(artifacts_path_, &result).isOk());
   EXPECT_EQ(result, 4 + 1);
 
   // The directory is kept because getting the file size failed.
@@ -195,6 +386,339 @@
   EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.art"));
 }
 
+TEST_F(ArtdTest, dexopt) {
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(WhenSplitBy(
+                  "--",
+                  AllOf(Contains(art_root_ + "/bin/art_exec"), Contains("--drop-capabilities")),
+                  AllOf(Contains(art_root_ + "/bin/dex2oat32"),
+                        Contains(Flag("--zip-fd=", FdOf(dex_file_))),
+                        Contains(Flag("--zip-location=", dex_file_)),
+                        Contains(Flag("--oat-location=", scratch_path_ + "/a/oat/arm64/b.odex")),
+                        Contains(Flag("--instruction-set=", "arm64")),
+                        Contains(Flag("--compiler-filter=", "speed"))))))
+      .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--oat-fd=", "oat")),
+                      WithArg<0>(WriteToFdFlag("--output-vdex-fd=", "vdex")),
+                      Return(0)));
+  RunDexopt();
+
+  CheckContent(scratch_path_ + "/a/oat/arm64/b.odex", "oat");
+  CheckContent(scratch_path_ + "/a/oat/arm64/b.vdex", "vdex");
+}
+
+TEST_F(ArtdTest, dexoptClassLoaderContext) {
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(WhenSplitBy(
+                  "--",
+                  _,
+                  AllOf(Contains(ListFlag("--class-loader-context-fds=",
+                                          ElementsAre(FdOf(clc_1_), FdOf(clc_2_)))),
+                        Contains(Flag("--class-loader-context=", class_loader_context_)),
+                        Contains(Flag("--classpath-dir=", scratch_path_ + "/a"))))))
+      .WillOnce(Return(0));
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptNoInputVdex) {
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(WhenSplitBy("--",
+                                              _,
+                                              AllOf(Not(Contains(Flag("--dm-fd=", _))),
+                                                    Not(Contains(Flag("--input-vdex-fd=", _)))))))
+      .WillOnce(Return(0));
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptInputVdex) {
+  vdex_path_ = artifacts_path_;
+  EXPECT_CALL(
+      *mock_exec_utils_,
+      DoExecAndReturnCode(WhenSplitBy(
+          "--",
+          _,
+          AllOf(Not(Contains(Flag("--dm-fd=", _))),
+                Contains(Flag("--input-vdex-fd=", FdOf(scratch_path_ + "/a/oat/arm64/b.vdex")))))))
+      .WillOnce(Return(0));
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptInputVdexDm) {
+  vdex_path_ = DexMetadataPath{.dexPath = dex_file_};
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(
+                  WhenSplitBy("--",
+                              _,
+                              AllOf(Contains(Flag("--dm-fd=", FdOf(scratch_path_ + "/a/b.dm"))),
+                                    Not(Contains(Flag("--input-vdex-fd=", _)))))))
+      .WillOnce(Return(0));
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptPriorityClassBoot) {
+  priority_class_ = PriorityClass::BOOT;
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(WhenSplitBy("--",
+                                              AllOf(Not(Contains(Flag("--set-task-profile=", _))),
+                                                    Not(Contains(Flag("--set-priority=", _)))),
+                                              Contains(Flag("--compact-dex-level=", "none")))))
+      .WillOnce(Return(0));
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptPriorityClassInteractive) {
+  priority_class_ = PriorityClass::INTERACTIVE;
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(
+                  WhenSplitBy("--",
+                              AllOf(Contains(Flag("--set-task-profile=", "Dex2OatBootComplete")),
+                                    Contains(Flag("--set-priority=", "background"))),
+                              Contains(Flag("--compact-dex-level=", "none")))))
+      .WillOnce(Return(0));
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptPriorityClassInteractiveFast) {
+  priority_class_ = PriorityClass::INTERACTIVE_FAST;
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(
+                  WhenSplitBy("--",
+                              AllOf(Contains(Flag("--set-task-profile=", "Dex2OatBootComplete")),
+                                    Contains(Flag("--set-priority=", "background"))),
+                              Contains(Flag("--compact-dex-level=", "none")))))
+      .WillOnce(Return(0));
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptPriorityClassBackground) {
+  priority_class_ = PriorityClass::BACKGROUND;
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(
+                  WhenSplitBy("--",
+                              AllOf(Contains(Flag("--set-task-profile=", "Dex2OatBootComplete")),
+                                    Contains(Flag("--set-priority=", "background"))),
+                              Not(Contains(Flag("--compact-dex-level=", _))))))
+      .WillOnce(Return(0));
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptDexoptOptions) {
+  dexopt_options_ = DexoptOptions{
+      .compilationReason = "install",
+      .targetSdkVersion = 123,
+      .debuggable = false,
+      .generateAppImage = false,
+      .hiddenApiPolicyEnabled = false,
+  };
+
+  EXPECT_CALL(
+      *mock_exec_utils_,
+      DoExecAndReturnCode(WhenSplitBy("--",
+                                      _,
+                                      AllOf(Contains(Flag("--compilation-reason=", "install")),
+                                            Contains(Flag("-Xtarget-sdk-version:", "123")),
+                                            Not(Contains("--debuggable")),
+                                            Not(Contains(Flag("--app-image-fd=", _))),
+                                            Not(Contains(Flag("-Xhidden-api-policy:", _)))))))
+      .WillOnce(Return(0));
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptDexoptOptions2) {
+  dexopt_options_ = DexoptOptions{
+      .compilationReason = "bg-dexopt",
+      .targetSdkVersion = 456,
+      .debuggable = true,
+      .generateAppImage = true,
+      .hiddenApiPolicyEnabled = true,
+  };
+
+  EXPECT_CALL(
+      *mock_exec_utils_,
+      DoExecAndReturnCode(WhenSplitBy("--",
+                                      _,
+                                      AllOf(Contains(Flag("--compilation-reason=", "bg-dexopt")),
+                                            Contains(Flag("-Xtarget-sdk-version:", "456")),
+                                            Contains("--debuggable"),
+                                            Contains(Flag("-Xhidden-api-policy:", "enabled"))))))
+      .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--app-image-fd=", "art")), Return(0)));
+  RunDexopt();
+
+  CheckContent(scratch_path_ + "/a/oat/arm64/b.art", "art");
+}
+
+TEST_F(ArtdTest, dexoptDefaultFlagsWhenNoSystemProps) {
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(
+                  WhenSplitBy("--",
+                              _,
+                              AllOf(Contains(Flag("--swap-fd=", FdOf(_))),
+                                    Not(Contains(Flag("--instruction-set-features=", _))),
+                                    Not(Contains(Flag("--instruction-set-variant=", _))),
+                                    Not(Contains(Flag("--max-image-block-size=", _))),
+                                    Not(Contains(Flag("--very-large-app-threshold=", _))),
+                                    Not(Contains(Flag("--resolve-startup-const-strings=", _))),
+                                    Not(Contains("--generate-debug-info")),
+                                    Not(Contains("--generate-mini-debug-info")),
+                                    Contains("-Xdeny-art-apex-data-files"),
+                                    Not(Contains(Flag("--cpu-set=", _))),
+                                    Not(Contains(Flag("-j", _))),
+                                    Not(Contains(Flag("-Xms", _))),
+                                    Not(Contains(Flag("-Xmx", _))),
+                                    Not(Contains("--compile-individually"))))))
+      .WillOnce(Return(0));
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptFlagsFromSystemProps) {
+  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-swap")).WillOnce(Return("0"));
+  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.isa.arm64.features"))
+      .WillOnce(Return("features"));
+  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.isa.arm64.variant")).WillOnce(Return("variant"));
+  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-max-image-block-size"))
+      .WillOnce(Return("size"));
+  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-very-large"))
+      .WillOnce(Return("threshold"));
+  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-resolve-startup-strings"))
+      .WillOnce(Return("strings"));
+  EXPECT_CALL(*mock_props_, GetProperty("debug.generate-debug-info")).WillOnce(Return("1"));
+  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-minidebuginfo")).WillOnce(Return("1"));
+  EXPECT_CALL(*mock_props_, GetProperty("odsign.verification.success")).WillOnce(Return("1"));
+  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-Xms")).WillOnce(Return("xms"));
+  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-Xmx")).WillOnce(Return("xmx"));
+  EXPECT_CALL(*mock_props_, GetProperty("ro.config.low_ram")).WillOnce(Return("1"));
+
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(
+                  WhenSplitBy("--",
+                              _,
+                              AllOf(Not(Contains(Flag("--swap-fd=", _))),
+                                    Contains(Flag("--instruction-set-features=", "features")),
+                                    Contains(Flag("--instruction-set-variant=", "variant")),
+                                    Contains(Flag("--max-image-block-size=", "size")),
+                                    Contains(Flag("--very-large-app-threshold=", "threshold")),
+                                    Contains(Flag("--resolve-startup-const-strings=", "strings")),
+                                    Contains("--generate-debug-info"),
+                                    Contains("--generate-mini-debug-info"),
+                                    Not(Contains("-Xdeny-art-apex-data-files")),
+                                    Contains(Flag("-Xms", "xms")),
+                                    Contains(Flag("-Xmx", "xmx")),
+                                    Contains("--compile-individually")))))
+      .WillOnce(Return(0));
+  RunDexopt();
+}
+
+static void SetDefaultResourceControlProps(MockSystemProperties* mock_props) {
+  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.dex2oat-cpu-set")).WillRepeatedly(Return("0,2"));
+  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.dex2oat-threads")).WillRepeatedly(Return("4"));
+}
+
+TEST_F(ArtdTest, dexoptDefaultResourceControlBoot) {
+  SetDefaultResourceControlProps(mock_props_);
+
+  // The default resource control properties don't apply to BOOT.
+  EXPECT_CALL(
+      *mock_exec_utils_,
+      DoExecAndReturnCode(WhenSplitBy(
+          "--", _, AllOf(Not(Contains(Flag("--cpu-set=", _))), Contains(Not(Flag("-j", _)))))))
+      .WillOnce(Return(0));
+  priority_class_ = PriorityClass::BOOT;
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptDefaultResourceControlOther) {
+  SetDefaultResourceControlProps(mock_props_);
+
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(WhenSplitBy(
+                  "--", _, AllOf(Contains(Flag("--cpu-set=", "0,2")), Contains(Flag("-j", "4"))))))
+      .Times(3)
+      .WillRepeatedly(Return(0));
+  priority_class_ = PriorityClass::INTERACTIVE_FAST;
+  RunDexopt();
+  priority_class_ = PriorityClass::INTERACTIVE;
+  RunDexopt();
+  priority_class_ = PriorityClass::BACKGROUND;
+  RunDexopt();
+}
+
+static void SetAllResourceControlProps(MockSystemProperties* mock_props) {
+  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.dex2oat-cpu-set")).WillRepeatedly(Return("0,2"));
+  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.dex2oat-threads")).WillRepeatedly(Return("4"));
+  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.boot-dex2oat-cpu-set"))
+      .WillRepeatedly(Return("0,1,2,3"));
+  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.boot-dex2oat-threads"))
+      .WillRepeatedly(Return("8"));
+  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.restore-dex2oat-cpu-set"))
+      .WillRepeatedly(Return("0,2,3"));
+  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.restore-dex2oat-threads"))
+      .WillRepeatedly(Return("6"));
+  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.background-dex2oat-cpu-set"))
+      .WillRepeatedly(Return("0"));
+  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.background-dex2oat-threads"))
+      .WillRepeatedly(Return("2"));
+}
+
+TEST_F(ArtdTest, dexoptAllResourceControlBoot) {
+  SetAllResourceControlProps(mock_props_);
+
+  EXPECT_CALL(
+      *mock_exec_utils_,
+      DoExecAndReturnCode(WhenSplitBy(
+          "--", _, AllOf(Contains(Flag("--cpu-set=", "0,1,2,3")), Contains(Flag("-j", "8"))))))
+      .WillOnce(Return(0));
+  priority_class_ = PriorityClass::BOOT;
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptAllResourceControlInteractiveFast) {
+  SetAllResourceControlProps(mock_props_);
+
+  EXPECT_CALL(
+      *mock_exec_utils_,
+      DoExecAndReturnCode(WhenSplitBy(
+          "--", _, AllOf(Contains(Flag("--cpu-set=", "0,2,3")), Contains(Flag("-j", "6"))))))
+      .WillOnce(Return(0));
+  priority_class_ = PriorityClass::INTERACTIVE_FAST;
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptAllResourceControlInteractive) {
+  SetAllResourceControlProps(mock_props_);
+
+  // INTERACTIVE always uses the default resource control properties.
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(WhenSplitBy(
+                  "--", _, AllOf(Contains(Flag("--cpu-set=", "0,2")), Contains(Flag("-j", "4"))))))
+      .WillOnce(Return(0));
+  priority_class_ = PriorityClass::INTERACTIVE;
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptAllResourceControlBackground) {
+  SetAllResourceControlProps(mock_props_);
+
+  EXPECT_CALL(*mock_exec_utils_,
+              DoExecAndReturnCode(WhenSplitBy(
+                  "--", _, AllOf(Contains(Flag("--cpu-set=", "0")), Contains(Flag("-j", "2"))))))
+      .WillOnce(Return(0));
+  priority_class_ = PriorityClass::BACKGROUND;
+  RunDexopt();
+}
+
+TEST_F(ArtdTest, dexoptFailed) {
+  dexopt_options_.generateAppImage = true;
+  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_))
+      .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--oat-fd=", "oat")),
+                      WithArg<0>(WriteToFdFlag("--output-vdex-fd=", "vdex")),
+                      WithArg<0>(WriteToFdFlag("--app-image-fd=", "art")),
+                      Return(1)));
+  RunDexopt(EX_SERVICE_SPECIFIC);
+
+  EXPECT_FALSE(std::filesystem::exists(scratch_path_ + "/a/oat/arm64/b.odex"));
+  EXPECT_FALSE(std::filesystem::exists(scratch_path_ + "/a/oat/arm64/b.vdex"));
+  EXPECT_FALSE(std::filesystem::exists(scratch_path_ + "/a/oat/arm64/b.art"));
+}
+
 }  // namespace
 }  // namespace artd
 }  // namespace art
diff --git a/artd/binder/com/android/server/art/DexMetadataPath.aidl b/artd/binder/com/android/server/art/DexMetadataPath.aidl
new file mode 100644
index 0000000..5f9ab81
--- /dev/null
+++ b/artd/binder/com/android/server/art/DexMetadataPath.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+package com.android.server.art;
+
+/**
+ * Represents the path to a dex metadata file.
+ *
+ * @hide
+ */
+parcelable DexMetadataPath {
+    /**
+     * The absolute path starting with '/' to the dex file that the dex metadata file is next to.
+     */
+    @utf8InCpp String dexPath;
+}
diff --git a/artd/binder/com/android/server/art/DexoptOptions.aidl b/artd/binder/com/android/server/art/DexoptOptions.aidl
new file mode 100644
index 0000000..351c079
--- /dev/null
+++ b/artd/binder/com/android/server/art/DexoptOptions.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+package com.android.server.art;
+
+/**
+ * Miscellaneous options for performing dexopt. Every field corresponds to a dex2oat command line
+ * flag.
+ *
+ * DO NOT add fields for flags that artd can determine directly with trivial logic. That includes
+ * static flags, and flags that only depend on system properties or other passed parameters, such as
+ * the priority class.
+ *
+ * All fields are required.
+ *
+ * @hide
+ */
+parcelable DexoptOptions {
+    /** --compilation-reason */
+    @utf8InCpp String compilationReason;
+    /** -Xtarget-sdk-version */
+    int targetSdkVersion;
+    /** --debuggable */
+    boolean debuggable;
+    /** --app-image-fd */
+    boolean generateAppImage;
+    /** -Xhidden-api-policy:enabled */
+    boolean hiddenApiPolicyEnabled;
+}
diff --git a/artd/binder/com/android/server/art/DexoptTrigger.aidl b/artd/binder/com/android/server/art/DexoptTrigger.aidl
new file mode 100644
index 0000000..a160f22
--- /dev/null
+++ b/artd/binder/com/android/server/art/DexoptTrigger.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+package com.android.server.art;
+
+/**
+ * Represents the conditions where dexopt should be performed.
+ * See `OatFileAssistant::DexOptTrigger`.
+ *
+ * This is actually used as a bit field, but is declared as an enum because AIDL doesn't support bit
+ * fields.
+ *
+ * @hide
+ */
+enum DexoptTrigger {
+    COMPILER_FILTER_IS_BETTER = 1 << 0,
+    COMPILER_FILTER_IS_SAME = 1 << 1,
+    COMPILER_FILTER_IS_WORSE = 1 << 2,
+    PRIMARY_BOOT_IMAGE_BECOMES_USABLE = 1 << 3,
+}
diff --git a/artd/binder/com/android/server/art/FsPermission.aidl b/artd/binder/com/android/server/art/FsPermission.aidl
index 229f182..9c2ddb9 100644
--- a/artd/binder/com/android/server/art/FsPermission.aidl
+++ b/artd/binder/com/android/server/art/FsPermission.aidl
@@ -19,6 +19,8 @@
 /**
  * Represents the Linux filesystem permission of a file or a directory.
  *
+ * If both `uid` and `gid` are negative, no `chown` will be performed.
+ *
  * If none of the booleans are set, the default permission bits are `rw-r-----` for a file, and
  * `rwxr-x---` for a directory.
  *
diff --git a/artd/binder/com/android/server/art/GetDexoptNeededResult.aidl b/artd/binder/com/android/server/art/GetDexoptNeededResult.aidl
new file mode 100644
index 0000000..99c4951
--- /dev/null
+++ b/artd/binder/com/android/server/art/GetDexoptNeededResult.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+package com.android.server.art;
+
+/**
+ * The result of {@code IArtd.getDexoptNeeded}.
+ *
+ * @hide
+ */
+parcelable GetDexoptNeededResult {
+    /** Whether dexopt is needed. */
+    boolean isDexoptNeeded;
+    /** Whether there is a usable VDEX file. Note that this can be true even if dexopt is needed. */
+    boolean isVdexUsable;
+    /** The location of the best usable artifacts. */
+    ArtifactsLocation artifactsLocation = ArtifactsLocation.NONE_OR_ERROR;
+
+    enum ArtifactsLocation {
+        /** No usable artifacts. */
+        NONE_OR_ERROR = 0,
+        /** In the global "dalvik-cache" folder. */
+        DALVIK_CACHE = 1,
+        /** In the "oat" folder next to the dex file. */
+        NEXT_TO_DEX = 2,
+        /** In the dex metadata file. This means the only usable artifact is the VDEX file. */
+        DM = 3,
+    }
+}
diff --git a/artd/binder/com/android/server/art/IArtd.aidl b/artd/binder/com/android/server/art/IArtd.aidl
index 5cf23b9..6750d9c 100644
--- a/artd/binder/com/android/server/art/IArtd.aidl
+++ b/artd/binder/com/android/server/art/IArtd.aidl
@@ -36,4 +36,29 @@
     com.android.server.art.GetOptimizationStatusResult getOptimizationStatus(
             @utf8InCpp String dexFile, @utf8InCpp String instructionSet,
             @utf8InCpp String classLoaderContext);
+
+    /**
+     * Returns true if dexopt is needed. `dexoptTrigger` is a bit field that consists of values
+     * defined in `com.android.server.art.DexoptTrigger`.
+     *
+     * Throws fatal and non-fatal errors.
+     */
+    com.android.server.art.GetDexoptNeededResult getDexoptNeeded(
+            @utf8InCpp String dexFile, @utf8InCpp String instructionSet,
+            @utf8InCpp String classLoaderContext, @utf8InCpp String compilerFilter,
+            byte dexoptTrigger);
+
+    /**
+     * Dexopts a dex file for the given instruction set. Returns true on success, or false if
+     * cancelled.
+     *
+     * Throws fatal and non-fatal errors.
+     */
+    boolean dexopt(in com.android.server.art.OutputArtifacts outputArtifacts,
+            @utf8InCpp String dexFile, @utf8InCpp String instructionSet,
+            @utf8InCpp String classLoaderContext, @utf8InCpp String compilerFilter,
+            in @nullable com.android.server.art.ProfilePath profile,
+            in @nullable com.android.server.art.VdexPath inputVdex,
+            com.android.server.art.PriorityClass priorityClass,
+            in com.android.server.art.DexoptOptions dexoptOptions);
 }
diff --git a/artd/binder/com/android/server/art/OutputArtifacts.aidl b/artd/binder/com/android/server/art/OutputArtifacts.aidl
new file mode 100644
index 0000000..20ed475
--- /dev/null
+++ b/artd/binder/com/android/server/art/OutputArtifacts.aidl
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+package com.android.server.art;
+
+/**
+ * Represents output optimized artifacts of a dex file (i.e., ART, OAT, and VDEX files).
+ *
+ * @hide
+ */
+parcelable OutputArtifacts {
+    /** The path to the output. */
+    com.android.server.art.ArtifactsPath artifactsPath;
+
+    parcelable PermissionSettings {
+        /**
+         * The permission of the directories that contain the artifacts. Has no effect if
+         * `artifactsPath.isInDalvikCache` is true.
+         */
+        com.android.server.art.FsPermission dirFsPermission;
+
+        /** The permission of the files. */
+        com.android.server.art.FsPermission fileFsPermission;
+
+        /** The tuple used for looking up for the SELinux context. */
+        parcelable SeContext {
+            /** The seinfo tag in SELinux policy. */
+            @utf8InCpp String seInfo;
+
+            /** The package uid. */
+            int packageUid;
+        }
+
+        /**
+         * Determines the SELinux context of the directories and the files. If empty, the default
+         * context based on the file path will be used. Has no effect if
+         * `artifactsPath.isInDalvikCache` is true.
+         */
+        @nullable SeContext seContext;
+    }
+
+    PermissionSettings permissionSettings;
+}
diff --git a/artd/binder/com/android/server/art/ProfilePath.aidl b/artd/binder/com/android/server/art/ProfilePath.aidl
new file mode 100644
index 0000000..d9e1208
--- /dev/null
+++ b/artd/binder/com/android/server/art/ProfilePath.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+package com.android.server.art;
+
+/**
+ * Represents the path to a profile file.
+ *
+ * @hide
+ */
+parcelable ProfilePath {
+}
diff --git a/artd/binder/com/android/server/art/VdexPath.aidl b/artd/binder/com/android/server/art/VdexPath.aidl
new file mode 100644
index 0000000..ec23a0e
--- /dev/null
+++ b/artd/binder/com/android/server/art/VdexPath.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+package com.android.server.art;
+
+/**
+ * Represents the path to a VDEX file.
+ *
+ * @hide
+ */
+union VdexPath {
+    /** Represents a VDEX file as part of the artifacts. */
+    com.android.server.art.ArtifactsPath artifactsPath;
+    /** Represents a VDEX file in a dex metadata file. */
+    com.android.server.art.DexMetadataPath dexMetadataPath;
+}
diff --git a/artd/file_utils.cc b/artd/file_utils.cc
index 67185b8..45344e1 100644
--- a/artd/file_utils.cc
+++ b/artd/file_utils.cc
@@ -33,9 +33,9 @@
 #include "android-base/logging.h"
 #include "android-base/result.h"
 #include "android-base/scopeguard.h"
-#include "android-base/stringprintf.h"
 #include "base/os.h"
 #include "base/unix_file/fd_file.h"
+#include "fmt/format.h"
 
 namespace art {
 namespace artd {
@@ -45,14 +45,14 @@
 using ::aidl::com::android::server::art::FsPermission;
 using ::android::base::make_scope_guard;
 using ::android::base::Result;
-using ::android::base::StringPrintf;
+
+using ::fmt::literals::operator""_format;  // NOLINT
 
 void UnlinkIfExists(const std::string& path) {
   std::error_code ec;
   if (!std::filesystem::remove(path, ec)) {
     if (ec.value() != ENOENT) {
-      LOG(WARNING) << StringPrintf(
-          "Failed to remove file '%s': %s", path.c_str(), ec.message().c_str());
+      LOG(WARNING) << "Failed to remove file '{}': {}"_format(path, ec.message());
     }
   }
 }
@@ -100,7 +100,7 @@
 }
 
 Result<void> NewFile::Init() {
-  mode_t mode = FsPermissionToMode(fs_permission_);
+  mode_t mode = FileFsPermissionToMode(fs_permission_);
   // "<path_>.XXXXXX.tmp".
   temp_path_ = BuildTempPath(final_path_, "XXXXXX");
   fd_ = mkstemps(temp_path_.data(), /*suffixlen=*/4);
@@ -111,9 +111,7 @@
   if (fchmod(fd_, mode) != 0) {
     return ErrnoErrorf("Failed to chmod file '{}'", temp_path_);
   }
-  if (fchown(fd_, fs_permission_.uid, fs_permission_.gid) != 0) {
-    return ErrnoErrorf("Failed to chown file '{}'", temp_path_);
-  }
+  OR_RETURN(Chown(temp_path_, fs_permission_));
   return {};
 }
 
@@ -143,11 +141,8 @@
       if (ec) {
         // This should never happen. We were able to move the file from `original_path` to
         // `temp_path`. We should be able to move it back.
-        LOG(WARNING) << StringPrintf(
-            "Failed to move old file '%s' back from temporary path '%s': %s",
-            std::string(original_path).c_str(),
-            std::string(temp_path).c_str(),
-            ec.message().c_str());
+        LOG(WARNING) << "Failed to move old file '{}' back from temporary path '{}': {}"_format(
+            original_path, temp_path, ec.message());
       }
     }
   });
@@ -208,7 +203,7 @@
 }
 
 std::string NewFile::BuildTempPath(std::string_view final_path, const std::string& id) {
-  return StringPrintf("%s.%s.tmp", std::string(final_path).c_str(), id.c_str());
+  return "{}.{}.tmp"_format(final_path, id);
 }
 
 Result<std::unique_ptr<File>> OpenFileForReading(const std::string& path) {
@@ -219,10 +214,28 @@
   return file;
 }
 
-mode_t FsPermissionToMode(const FsPermission& fs_permission) {
+mode_t FileFsPermissionToMode(const FsPermission& fs_permission) {
   return S_IRUSR | S_IWUSR | S_IRGRP | (fs_permission.isOtherReadable ? S_IROTH : 0) |
          (fs_permission.isOtherExecutable ? S_IXOTH : 0);
 }
 
+mode_t DirFsPermissionToMode(const FsPermission& fs_permission) {
+  return FileFsPermissionToMode(fs_permission) | S_IXUSR | S_IXGRP;
+}
+
+Result<void> Chown(const std::string& path, const FsPermission& fs_permission) {
+  if (fs_permission.uid < 0 && fs_permission.gid < 0) {
+    // Keep the default owner.
+  } else if (fs_permission.uid < 0 || fs_permission.gid < 0) {
+    return Errorf("uid and gid must be both non-negative or both negative, got {} and {}.",
+                  fs_permission.uid,
+                  fs_permission.gid);
+  }
+  if (chown(path.c_str(), fs_permission.uid, fs_permission.gid) != 0) {
+    return ErrnoErrorf("Failed to chown '{}'", path);
+  }
+  return {};
+}
+
 }  // namespace artd
 }  // namespace art
diff --git a/artd/file_utils.h b/artd/file_utils.h
index 9dc41f4..b5fd170 100644
--- a/artd/file_utils.h
+++ b/artd/file_utils.h
@@ -119,8 +119,15 @@
 // Opens a file for reading.
 android::base::Result<std::unique_ptr<File>> OpenFileForReading(const std::string& path);
 
-// Converts FsPermission to Linux access mode.
-mode_t FsPermissionToMode(const aidl::com::android::server::art::FsPermission& fs_permission);
+// Converts FsPermission to Linux access mode for a file.
+mode_t FileFsPermissionToMode(const aidl::com::android::server::art::FsPermission& fs_permission);
+
+// Converts FsPermission to Linux access mode for a directory.
+mode_t DirFsPermissionToMode(const aidl::com::android::server::art::FsPermission& fs_permission);
+
+// Changes the owner based on FsPermission.
+android::base::Result<void> Chown(
+    const std::string& path, const aidl::com::android::server::art::FsPermission& fs_permission);
 
 }  // namespace artd
 }  // namespace art
diff --git a/artd/file_utils_test.cc b/artd/file_utils_test.cc
index e6219a4..8f79d5d 100644
--- a/artd/file_utils_test.cc
+++ b/artd/file_utils_test.cc
@@ -325,14 +325,25 @@
               HasError(WithMessage(ContainsRegex("Failed to open file .*/foo"))));
 }
 
-TEST_F(FileUtilsTest, FsPermissionToMode) {
-  EXPECT_EQ(FsPermissionToMode(FsPermission{}), S_IRUSR | S_IWUSR | S_IRGRP);
-  EXPECT_EQ(FsPermissionToMode(FsPermission{.isOtherReadable = true}),
+TEST_F(FileUtilsTest, FileFsPermissionToMode) {
+  EXPECT_EQ(FileFsPermissionToMode(FsPermission{}), S_IRUSR | S_IWUSR | S_IRGRP);
+  EXPECT_EQ(FileFsPermissionToMode(FsPermission{.isOtherReadable = true}),
             S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
-  EXPECT_EQ(FsPermissionToMode(FsPermission{.isOtherExecutable = true}),
+  EXPECT_EQ(FileFsPermissionToMode(FsPermission{.isOtherExecutable = true}),
             S_IRUSR | S_IWUSR | S_IRGRP | S_IXOTH);
-  EXPECT_EQ(FsPermissionToMode(FsPermission{.isOtherReadable = true, .isOtherExecutable = true}),
-            S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH | S_IXOTH);
+  EXPECT_EQ(
+      FileFsPermissionToMode(FsPermission{.isOtherReadable = true, .isOtherExecutable = true}),
+      S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH | S_IXOTH);
+}
+
+TEST_F(FileUtilsTest, DirFsPermissionToMode) {
+  EXPECT_EQ(DirFsPermissionToMode(FsPermission{}), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP);
+  EXPECT_EQ(DirFsPermissionToMode(FsPermission{.isOtherReadable = true}),
+            S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH);
+  EXPECT_EQ(DirFsPermissionToMode(FsPermission{.isOtherExecutable = true}),
+            S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IXOTH);
+  EXPECT_EQ(DirFsPermissionToMode(FsPermission{.isOtherReadable = true, .isOtherExecutable = true}),
+            S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
 }
 
 }  // namespace
diff --git a/artd/path_utils.cc b/artd/path_utils.cc
index c4d9031..802ced0 100644
--- a/artd/path_utils.cc
+++ b/artd/path_utils.cc
@@ -24,6 +24,7 @@
 #include "android-base/strings.h"
 #include "arch/instruction_set.h"
 #include "base/file_utils.h"
+#include "fmt/format.h"
 #include "oat_file_assistant.h"
 
 namespace art {
@@ -32,14 +33,21 @@
 namespace {
 
 using ::aidl::com::android::server::art::ArtifactsPath;
+using ::aidl::com::android::server::art::DexMetadataPath;
+using ::aidl::com::android::server::art::VdexPath;
 using ::android::base::EndsWith;
 using ::android::base::Error;
 using ::android::base::Result;
 
+using ::fmt::literals::operator""_format;  // NOLINT
+
 Result<void> ValidateAbsoluteNormalPath(const std::string& path_str) {
   if (path_str.empty()) {
     return Errorf("Path is empty");
   }
+  if (path_str.find('\0') != std::string::npos) {
+    return Errorf("Path '{}' has invalid character '\\0'", path_str);
+  }
   std::filesystem::path path(path_str);
   if (!path.is_absolute()) {
     return Errorf("Path '{}' is not an absolute path", path_str);
@@ -50,6 +58,17 @@
   return {};
 }
 
+Result<std::string> GetArtRootOrError() {
+  std::string error_msg;
+  std::string result = GetArtRootSafe(&error_msg);
+  if (!error_msg.empty()) {
+    return Error() << error_msg;
+  }
+  return result;
+}
+
+}  // namespace
+
 Result<void> ValidateDexPath(const std::string& dex_path) {
   OR_RETURN(ValidateAbsoluteNormalPath(dex_path));
   if (!EndsWith(dex_path, ".apk") && !EndsWith(dex_path, ".jar")) {
@@ -58,14 +77,16 @@
   return {};
 }
 
-}  // namespace
+Result<std::string> BuildArtBinPath(const std::string& binary_name) {
+  return "{}/bin/{}"_format(OR_RETURN(GetArtRootOrError()), binary_name);
+}
 
 Result<std::string> BuildOatPath(const ArtifactsPath& artifacts_path) {
   OR_RETURN(ValidateDexPath(artifacts_path.dexPath));
 
   InstructionSet isa = GetInstructionSetFromString(artifacts_path.isa.c_str());
   if (isa == InstructionSet::kNone) {
-    return Errorf("Instruction set '{}' is invalid", artifacts_path.isa.c_str());
+    return Errorf("Instruction set '{}' is invalid", artifacts_path.isa);
   }
 
   std::string error_msg;
@@ -86,12 +107,19 @@
   }
 }
 
-std::string OatPathToVdexPath(const std::string& oat_path) {
-  return ReplaceFileExtension(oat_path, "vdex");
+Result<std::string> BuildDexMetadataPath(const DexMetadataPath& dex_metadata_path) {
+  OR_RETURN(ValidateDexPath(dex_metadata_path.dexPath));
+  return ReplaceFileExtension(dex_metadata_path.dexPath, "dm");
 }
 
-std::string OatPathToArtPath(const std::string& oat_path) {
-  return ReplaceFileExtension(oat_path, "art");
+Result<std::string> BuildDexMetadataPath(const VdexPath& vdex_path) {
+  DCHECK(vdex_path.getTag() == VdexPath::dexMetadataPath);
+  return BuildDexMetadataPath(vdex_path.get<VdexPath::dexMetadataPath>());
+}
+
+Result<std::string> BuildVdexPath(const VdexPath& vdex_path) {
+  DCHECK(vdex_path.getTag() == VdexPath::artifactsPath);
+  return OatPathToVdexPath(OR_RETURN(BuildOatPath(vdex_path.get<VdexPath::artifactsPath>())));
 }
 
 }  // namespace artd
diff --git a/artd/path_utils.h b/artd/path_utils.h
index 970143a..4151387 100644
--- a/artd/path_utils.h
+++ b/artd/path_utils.h
@@ -19,19 +19,37 @@
 
 #include "aidl/com/android/server/art/BnArtd.h"
 #include "android-base/result.h"
+#include "base/file_utils.h"
 
 namespace art {
 namespace artd {
 
+android::base::Result<void> ValidateDexPath(const std::string& dex_path);
+
+android::base::Result<std::string> BuildArtBinPath(const std::string& binary_name);
+
 // Returns the absolute path to the OAT file built from the `ArtifactsPath`.
 android::base::Result<std::string> BuildOatPath(
     const aidl::com::android::server::art::ArtifactsPath& artifacts_path);
 
 // Returns the path to the VDEX file that corresponds to the OAT file.
-std::string OatPathToVdexPath(const std::string& oat_path);
+inline std::string OatPathToVdexPath(const std::string& oat_path) {
+  return ReplaceFileExtension(oat_path, "vdex");
+}
 
 // Returns the path to the ART file that corresponds to the OAT file.
-std::string OatPathToArtPath(const std::string& oat_path);
+inline std::string OatPathToArtPath(const std::string& oat_path) {
+  return ReplaceFileExtension(oat_path, "art");
+}
+
+android::base::Result<std::string> BuildDexMetadataPath(
+    const aidl::com::android::server::art::DexMetadataPath& dex_metadata_path);
+
+android::base::Result<std::string> BuildDexMetadataPath(
+    const aidl::com::android::server::art::VdexPath& vdex_path);
+
+android::base::Result<std::string> BuildVdexPath(
+    const aidl::com::android::server::art::VdexPath& vdex_path);
 
 }  // namespace artd
 }  // namespace art
diff --git a/artd/path_utils_test.cc b/artd/path_utils_test.cc
index 9ce40c5..4162da3 100644
--- a/artd/path_utils_test.cc
+++ b/artd/path_utils_test.cc
@@ -26,12 +26,23 @@
 namespace {
 
 using ::aidl::com::android::server::art::ArtifactsPath;
+using ::aidl::com::android::server::art::DexMetadataPath;
+using ::aidl::com::android::server::art::VdexPath;
 using ::android::base::testing::HasError;
 using ::android::base::testing::HasValue;
 using ::android::base::testing::WithMessage;
 
+using std::literals::operator""s;  // NOLINT
+
 class PathUtilsTest : public CommonArtTest {};
 
+TEST_F(PathUtilsTest, BuildArtBinPath) {
+  auto scratch_dir = std::make_unique<ScratchDir>();
+  auto art_root_env = ScopedUnsetEnvironmentVariable("ANDROID_ART_ROOT");
+  setenv("ANDROID_ART_ROOT", scratch_dir->GetPath().c_str(), /*overwrite=*/1);
+  EXPECT_THAT(BuildArtBinPath("foo"), HasValue(scratch_dir->GetPath() + "/bin/foo"));
+}
+
 TEST_F(PathUtilsTest, BuildOatPath) {
   EXPECT_THAT(
       BuildOatPath(ArtifactsPath{.dexPath = "/a/b.apk", .isa = "arm64", .isInDalvikCache = false}),
@@ -61,6 +72,12 @@
               HasError(WithMessage("Path '/a/c/../b.apk' is not in normal form")));
 }
 
+TEST_F(PathUtilsTest, BuildOatPathNul) {
+  EXPECT_THAT(BuildOatPath(ArtifactsPath{
+                  .dexPath = "/a/\0/b.apk"s, .isa = "arm64", .isInDalvikCache = false}),
+              HasError(WithMessage("Path '/a/\0/b.apk' has invalid character '\\0'"s)));
+}
+
 TEST_F(PathUtilsTest, BuildOatPathInvalidDexExtension) {
   EXPECT_THAT(BuildOatPath(ArtifactsPath{
                   .dexPath = "/a/b.invalid", .isa = "arm64", .isInDalvikCache = false}),
@@ -81,6 +98,21 @@
   EXPECT_EQ(OatPathToArtPath("/a/oat/arm64/b.odex"), "/a/oat/arm64/b.art");
 }
 
+TEST_F(PathUtilsTest, BuildDexMetadataPath) {
+  EXPECT_THAT(BuildDexMetadataPath(DexMetadataPath{.dexPath = "/a/b.apk"}), HasValue("/a/b.dm"));
+}
+
+TEST_F(PathUtilsTest, BuildDexMetadataPathForVdex) {
+  EXPECT_THAT(BuildDexMetadataPath(VdexPath(DexMetadataPath{.dexPath = "/a/b.apk"})),
+              HasValue("/a/b.dm"));
+}
+
+TEST_F(PathUtilsTest, BuildVdexPath) {
+  EXPECT_THAT(
+      BuildVdexPath(ArtifactsPath{.dexPath = "/a/b.apk", .isa = "arm64", .isInDalvikCache = false}),
+      HasValue("/a/oat/arm64/b.vdex"));
+}
+
 }  // namespace
 }  // namespace artd
 }  // namespace art