blob: 66e5c091761c79a6f6b8fbe8befbae4bb7e190e8 [file] [log] [blame]
/*
* 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.
*/
#include "host/commands/cvd/acloud/converter.h"
#include <sys/stat.h>
#include <cstdio>
#include <fstream>
#include <optional>
#include <vector>
#include <android-base/file.h>
#include <android-base/parseint.h>
#include <android-base/strings.h>
#include <google/protobuf/text_format.h>
#include "common/libs/fs/shared_fd.h"
#include "common/libs/utils/files.h"
#include "common/libs/utils/flag_parser.h"
#include "common/libs/utils/result.h"
#include "common/libs/utils/subprocess.h"
#include "common/libs/utils/users.h"
#include "cvd_server.pb.h"
#include "host/commands/cvd/acloud/config.h"
#include "host/commands/cvd/command_sequence.h"
#include "host/commands/cvd/common_utils.h"
#include "host/commands/cvd/instance_lock.h" // TempDir()
#include "host/commands/cvd/selector/instance_database_utils.h"
#include "host/commands/cvd/selector/selector_constants.h"
#include "host/commands/cvd/server_client.h"
#include "host/commands/cvd/server_command/utils.h"
#include "host/commands/cvd/types.h"
#include "host/libs/config/cuttlefish_config.h"
namespace cuttlefish {
namespace {
// Image names to search
const std::vector<std::string> _KERNEL_IMAGE_NAMES = {"kernel", "bzImage",
"Image"};
const std::vector<std::string> _INITRAMFS_IMAGE_NAME = {"initramfs.img"};
const std::vector<std::string> _BOOT_IMAGE_NAME = {"boot.img"};
const std::vector<std::string> _VENDOR_BOOT_IMAGE_NAME = {"vendor_boot.img"};
/**
* Find a image file through the input path and pattern.
*
* If it finds the file, return the path string.
* If it can't find the file, return empty string.
*/
std::string FindImage(const std::string& search_path,
const std::vector<std::string>& pattern) {
const std::string& search_path_extend = search_path + "/";
for (const auto& name : pattern) {
const std::string image = search_path_extend + name;
if (FileExists(image)) {
return image;
}
}
return "";
}
/**
* Split a string into arguments based on shell tokenization rules.
*
* This behaves like `shlex.split` from python where arguments are separated
* based on whitespace, but quoting and quote escaping is respected. This
* function effectively removes one level of quoting from its inputs while
* making the split.
*/
Result<std::vector<std::string>> BashTokenize(const std::string& str) {
Command command("bash");
command.AddParameter("-c");
command.AddParameter("printf '%s\n' ", str);
std::string stdout_str;
std::string stderr_str;
auto ret = RunWithManagedStdio(std::move(command), nullptr, &stdout_str,
&stderr_str);
CF_EXPECT(ret == 0,
"printf fail \"" << stdout_str << "\", \"" << stderr_str << "\"");
return android::base::Split(stdout_str, "\n");
}
} // namespace
// body of pure virtual destructor required by C++
ConvertAcloudCreateCommand::~ConvertAcloudCreateCommand() {}
class ConvertAcloudCreateCommandImpl : public ConvertAcloudCreateCommand {
public:
INJECT(ConvertAcloudCreateCommandImpl()) {}
~ConvertAcloudCreateCommandImpl() override = default;
Result<ConvertedAcloudCreateCommand> Convert(
const RequestWithStdio& request) {
auto arguments = ParseInvocation(request.Message()).arguments;
CF_EXPECT(arguments.size() > 0);
CF_EXPECT(arguments[0] == "create");
arguments.erase(arguments.begin());
const auto& request_command = request.Message().command_request();
std::vector<Flag> flags;
bool local_instance_set;
std::optional<int> local_instance;
auto local_instance_flag = Flag();
local_instance_flag.Alias(
{FlagAliasMode::kFlagConsumesArbitrary, "--local-instance"});
local_instance_flag.Setter([&local_instance_set,
&local_instance](const FlagMatch& m) {
local_instance_set = true;
if (m.value != "" && local_instance) {
LOG(ERROR) << "Instance number already set, was \"" << *local_instance
<< "\", now set to \"" << m.value << "\"";
return false;
} else if (m.value != "" && !local_instance) {
local_instance = std::stoi(m.value);
}
return true;
});
flags.emplace_back(local_instance_flag);
std::optional<std::string> flavor;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--config"})
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--flavor"})
.Setter([&flavor](const FlagMatch& m) {
flavor = m.value;
return true;
}));
std::optional<std::string> local_kernel_image;
flags.emplace_back(Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--local-kernel-image"})
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--local-boot-image"})
.Setter([&local_kernel_image](const FlagMatch& m) {
local_kernel_image = m.value;
return true;
}));
std::optional<std::string> image_download_dir;
flags.emplace_back(Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--image-download-dir"})
.Setter([&image_download_dir](const FlagMatch& m) {
image_download_dir = m.value;
return true;
}));
verbose_ = false;
flags.emplace_back(Flag()
.Alias({FlagAliasMode::kFlagExact, "-v"})
.Alias({FlagAliasMode::kFlagExact, "-vv"})
.Alias({FlagAliasMode::kFlagExact, "--verbose"})
.Setter([this](const FlagMatch&) {
verbose_ = true;
return true;
}));
std::optional<std::string> branch;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--branch"})
.Setter([&branch](const FlagMatch& m) {
branch = m.value;
return true;
}));
bool local_image;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesArbitrary, "--local-image"})
.Setter([&local_image](const FlagMatch& m) {
local_image = true;
return m.value == "";
}));
std::optional<std::string> build_id;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--build-id"})
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--build_id"})
.Setter([&build_id](const FlagMatch& m) {
build_id = m.value;
return true;
}));
std::optional<std::string> build_target;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--build-target"})
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--build_target"})
.Setter([&build_target](const FlagMatch& m) {
build_target = m.value;
return true;
}));
std::optional<std::string> config_file;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--config-file"})
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--config_file"})
.Setter([&config_file](const FlagMatch& m) {
config_file = m.value;
return true;
}));
std::optional<std::string> bootloader_build_id;
flags.emplace_back(Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--bootloader-build-id"})
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--bootloader_build_id"})
.Setter([&bootloader_build_id](const FlagMatch& m) {
bootloader_build_id = m.value;
return true;
}));
std::optional<std::string> bootloader_build_target;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--bootloader-build-target"})
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--bootloader_build_target"})
.Setter([&bootloader_build_target](const FlagMatch& m) {
bootloader_build_target = m.value;
return true;
}));
std::optional<std::string> bootloader_branch;
flags.emplace_back(Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--bootloader-branch"})
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--bootloader_branch"})
.Setter([&bootloader_branch](const FlagMatch& m) {
bootloader_branch = m.value;
return true;
}));
std::optional<std::string> boot_build_id;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--boot-build-id"})
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--boot_build_id"})
.Setter([&boot_build_id](const FlagMatch& m) {
boot_build_id = m.value;
return true;
}));
std::optional<std::string> boot_build_target;
flags.emplace_back(Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--boot-build-target"})
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--boot_build_target"})
.Setter([&boot_build_target](const FlagMatch& m) {
boot_build_target = m.value;
return true;
}));
std::optional<std::string> boot_branch;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--boot-branch"})
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--boot_branch"})
.Setter([&boot_branch](const FlagMatch& m) {
boot_branch = m.value;
return true;
}));
std::optional<std::string> boot_artifact;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--boot-artifact"})
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--boot_artifact"})
.Setter([&boot_artifact](const FlagMatch& m) {
boot_artifact = m.value;
return true;
}));
std::optional<std::string> ota_build_id;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--ota-build-id"})
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--ota_build_id"})
.Setter([&ota_build_id](const FlagMatch& m) {
ota_build_id = m.value;
return true;
}));
std::optional<std::string> ota_build_target;
flags.emplace_back(Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--ota-build-target"})
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--ota_build_target"})
.Setter([&ota_build_target](const FlagMatch& m) {
ota_build_target = m.value;
return true;
}));
std::optional<std::string> ota_branch;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--ota-branch"})
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--ota_branch"})
.Setter([&ota_branch](const FlagMatch& m) {
ota_branch = m.value;
return true;
}));
std::optional<std::string> launch_args;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--launch-args"})
.Setter([&launch_args](const FlagMatch& m) {
launch_args = m.value;
return true;
}));
std::optional<std::string> system_branch;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--system-branch"})
.Setter([&system_branch](const FlagMatch& m) {
system_branch = m.value;
return true;
}));
std::optional<std::string> system_build_target;
flags.emplace_back(Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--system-build-target"})
.Setter([&system_build_target](const FlagMatch& m) {
system_build_target = m.value;
return true;
}));
std::optional<std::string> system_build_id;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--system-build-id"})
.Setter([&system_build_id](const FlagMatch& m) {
system_build_id = m.value;
return true;
}));
std::optional<std::string> kernel_branch;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--kernel-branch"})
.Setter([&kernel_branch](const FlagMatch& m) {
kernel_branch = m.value;
return true;
}));
std::optional<std::string> kernel_build_target;
flags.emplace_back(Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing,
"--kernel-build-target"})
.Setter([&kernel_build_target](const FlagMatch& m) {
kernel_build_target = m.value;
return true;
}));
std::optional<std::string> kernel_build_id;
flags.emplace_back(
Flag()
.Alias({FlagAliasMode::kFlagConsumesFollowing, "--kernel-build-id"})
.Setter([&kernel_build_id](const FlagMatch& m) {
kernel_build_id = m.value;
return true;
}));
std::optional<std::string> pet_name;
Flag pet_name_gflag = GflagsCompatFlag("pet-name");
flags.emplace_back(
GflagsCompatFlag("pet-name")
.Getter([&pet_name]() { return (pet_name ? *pet_name : ""); })
.Setter([&pet_name](const FlagMatch& match) {
pet_name = match.value;
return true;
}));
CF_EXPECT(ParseFlags(flags, arguments));
CF_EXPECT(arguments.size() == 0,
"Unrecognized arguments:'"
<< android::base::Join(arguments, "', '") << "'");
CF_EXPECT(local_instance_set == true,
"Only '--local-instance' is supported");
auto host_dir = TempDir() + "/acloud_image_artifacts/";
if (image_download_dir) {
host_dir = image_download_dir.value() + "/acloud_image_artifacts/";
}
auto host_artifacts_path = request_command.env().find(kAndroidHostOut);
CF_EXPECT(host_artifacts_path != request_command.env().end(),
"Missing " << kAndroidHostOut);
std::vector<cvd::Request> request_protos;
const uid_t uid = request.Credentials()->uid;
// default user config path
std::string user_config_path = CF_EXPECT(GetDefaultConfigFile(uid));
if (config_file) {
user_config_path = config_file.value();
}
AcloudConfig acloud_config =
CF_EXPECT(LoadAcloudConfig(user_config_path, uid));
if (local_image) {
CF_EXPECT(!(system_branch || system_build_target || system_build_id),
"--local-image incompatible with --system-* flags");
CF_EXPECT(!(bootloader_branch || bootloader_build_target ||
bootloader_build_id),
"--local-image incompatible with --bootloader-* flags");
CF_EXPECT(
!(boot_branch || boot_build_target || boot_build_id || boot_artifact),
"--local-image incompatible with --boot-* flags");
CF_EXPECT(!(ota_branch || ota_build_target || ota_build_id),
"--local-image incompatible with --ota-* flags");
} else {
if (!DirectoryExists(host_dir)) {
// fetch/download directory doesn't exist, create directory
cvd::Request& mkdir_request = request_protos.emplace_back();
auto& mkdir_command = *mkdir_request.mutable_command_request();
mkdir_command.add_args("cvd");
mkdir_command.add_args("mkdir");
mkdir_command.add_args("-p");
mkdir_command.add_args(host_dir);
auto& mkdir_env = *mkdir_command.mutable_env();
mkdir_env[kAndroidHostOut] = host_artifacts_path->second;
}
if (branch || build_id || build_target) {
auto target = build_target ? *build_target : "";
auto build = build_id.value_or(branch.value_or("aosp-master"));
host_dir += (build + target);
} else {
host_dir += "aosp-master";
}
// TODO(weihsu): if we fetch default ID such as aosp-master,
// cvd fetch will fetch the latest release. There is a potential
// issue that two different fetch with same default ID may
// download different releases.
// Eventually, we should match python acloud behavior to translate
// default ID (aosp-master) to real ID to solve this issue.
cvd::Request& fetch_request = request_protos.emplace_back();
auto& fetch_command = *fetch_request.mutable_command_request();
fetch_command.add_args("cvd");
fetch_command.add_args("fetch");
fetch_command.add_args("--directory");
fetch_command.add_args(host_dir);
fetch_command_str_ = "";
if (branch || build_id || build_target) {
fetch_command.add_args("--default_build");
fetch_command_str_ += "--default_build=";
auto target = build_target ? "/" + *build_target : "";
auto build = build_id.value_or(branch.value_or("aosp-master"));
fetch_command.add_args(build + target);
fetch_command_str_ += (build + target);
}
if (system_branch || system_build_id || system_build_target) {
fetch_command.add_args("--system_build");
fetch_command_str_ += " --system_build=";
auto target = system_build_target.value_or(build_target.value_or(""));
if (target != "") {
target = "/" + target;
}
auto build =
system_build_id.value_or(system_branch.value_or("aosp-master"));
fetch_command.add_args(build + target);
fetch_command_str_ += (build + target);
}
if (bootloader_branch || bootloader_build_id || bootloader_build_target) {
fetch_command.add_args("--bootloader_build");
fetch_command_str_ += " --bootloader_build=";
auto target = bootloader_build_target.value_or("");
if (target != "") {
target = "/" + target;
}
auto build = bootloader_build_id.value_or(
bootloader_branch.value_or("aosp_u-boot-mainline"));
fetch_command.add_args(build + target);
fetch_command_str_ += (build + target);
}
if (boot_branch || boot_build_id || boot_build_target) {
fetch_command.add_args("--boot_build");
fetch_command_str_ += " --boot_build=";
auto target = boot_build_target.value_or("");
if (target != "") {
target = "/" + target;
}
auto build =
boot_build_id.value_or(boot_branch.value_or("aosp-master"));
fetch_command.add_args(build + target);
fetch_command_str_ += (build + target);
}
if (boot_artifact) {
CF_EXPECT(boot_branch || boot_build_target || boot_build_id,
"--boot-artifact must combine with other --boot-* flags");
fetch_command.add_args("--boot_artifact");
fetch_command_str_ += " --boot_artifact=";
auto target = boot_artifact.value_or("");
fetch_command.add_args(target);
fetch_command_str_ += (target);
}
if (ota_branch || ota_build_id || ota_build_target) {
fetch_command.add_args("--otatools_build");
fetch_command_str_ += " --otatools_build=";
auto target = ota_build_target.value_or("");
if (target != "") {
target = "/" + target;
}
auto build = ota_build_id.value_or(ota_branch.value_or(""));
fetch_command.add_args(build + target);
fetch_command_str_ += (build + target);
}
if (kernel_branch || kernel_build_id || kernel_build_target) {
fetch_command.add_args("--kernel_build");
fetch_command_str_ += " --kernel_build=";
auto target = kernel_build_target.value_or("kernel_virt_x86_64");
auto build = kernel_build_id.value_or(
branch.value_or("aosp_kernel-common-android-mainline"));
fetch_command.add_args(build + "/" + target);
fetch_command_str_ += (build + "/" + target);
}
auto& fetch_env = *fetch_command.mutable_env();
fetch_env[kAndroidHostOut] = host_artifacts_path->second;
fetch_cvd_args_file_ = host_dir + "/fetch-cvd-args.txt";
if (FileExists(fetch_cvd_args_file_)) {
// file exists
std::string read_str;
using android::base::ReadFileToString;
CF_EXPECT(ReadFileToString(fetch_cvd_args_file_.c_str(), &read_str,
/* follow_symlinks */ true));
if (read_str == fetch_command_str_) {
// same fetch cvd command, reuse original dir
fetch_command_str_ = "";
request_protos.pop_back();
}
}
}
cvd::Request start_request;
auto& start_command = *start_request.mutable_command_request();
start_command.add_args("cvd");
start_command.add_args("start");
start_command.add_args("--daemon");
start_command.add_args("--undefok");
start_command.add_args("report_anonymous_usage_stats");
start_command.add_args("--report_anonymous_usage_stats");
start_command.add_args("y");
if (flavor) {
start_command.add_args("-config");
start_command.add_args(flavor.value());
}
if (local_kernel_image) {
// kernel image has 1st priority than boot image
struct stat statbuf;
std::string local_boot_image = "";
std::string vendor_boot_image = "";
std::string kernel_image = "";
std::string initramfs_image = "";
if (stat(local_kernel_image.value().c_str(), &statbuf) == 0) {
if (statbuf.st_mode & S_IFDIR) {
// it's a directory, deal with kernel image case first
kernel_image =
FindImage(local_kernel_image.value(), _KERNEL_IMAGE_NAMES);
initramfs_image =
FindImage(local_kernel_image.value(), _INITRAMFS_IMAGE_NAME);
// This is the original python acloud behavior, it
// expects both kernel and initramfs files, however,
// there are some very old kernels that are built without
// an initramfs.img file,
// e.g. aosp_kernel-common-android-4.14-stable
if (kernel_image != "" && initramfs_image != "") {
start_command.add_args("-kernel_path");
start_command.add_args(kernel_image);
start_command.add_args("-initramfs_path");
start_command.add_args(initramfs_image);
} else {
// boot.img case
// adding boot.img and vendor_boot.img to the path
local_boot_image =
FindImage(local_kernel_image.value(), _BOOT_IMAGE_NAME);
vendor_boot_image =
FindImage(local_kernel_image.value(), _VENDOR_BOOT_IMAGE_NAME);
start_command.add_args("-boot_image");
start_command.add_args(local_boot_image);
// vendor boot image may not exist
if (vendor_boot_image != "") {
start_command.add_args("-vendor_boot_image");
start_command.add_args(vendor_boot_image);
}
}
} else if (statbuf.st_mode & S_IFREG) {
// it's a file which directly points to boot.img
local_boot_image = local_kernel_image.value();
start_command.add_args("-boot_image");
start_command.add_args(local_boot_image);
}
}
}
if (launch_args) {
for (const auto& arg : CF_EXPECT(BashTokenize(*launch_args))) {
start_command.add_args(arg);
}
}
if (acloud_config.launch_args != "") {
for (const auto& arg :
CF_EXPECT(BashTokenize(acloud_config.launch_args))) {
start_command.add_args(arg);
}
}
start_command.mutable_selector_opts()->add_args(
std::string("--") + selector::SelectorFlags::kDisableDefaultGroup +
"=true");
if (pet_name) {
const auto [group_name, instance_name] =
CF_EXPECT(selector::BreakDeviceName(*pet_name),
*pet_name << " must be a group name followed by - "
<< "followed by an instance name.");
std::string group_name_arg = "--";
group_name_arg.append(selector::SelectorFlags::kGroupName)
.append("=")
.append(group_name);
std::string instance_name_arg = "--";
instance_name_arg.append(selector::SelectorFlags::kInstanceName)
.append("=")
.append(instance_name);
start_command.mutable_selector_opts()->add_args(group_name_arg);
start_command.mutable_selector_opts()->add_args(instance_name_arg);
}
auto& start_env = *start_command.mutable_env();
if (local_image) {
start_env[kAndroidHostOut] = host_artifacts_path->second;
auto product_out = request_command.env().find(kAndroidProductOut);
CF_EXPECT(product_out != request_command.env().end(),
"Missing " << kAndroidProductOut);
start_env[kAndroidProductOut] = product_out->second;
} else {
start_env[kAndroidHostOut] = host_dir;
start_env[kAndroidProductOut] = host_dir;
}
if (Contains(start_env, kCuttlefishInstanceEnvVarName)) {
// Python acloud does not use this variable.
// this variable will confuse cvd start, though
start_env.erase(kCuttlefishInstanceEnvVarName);
}
if (local_instance) {
start_env[kCuttlefishInstanceEnvVarName] =
std::to_string(*local_instance);
}
// we don't know which HOME is assigned by cvd start.
// cvd server does not rely on the working directory for cvd start
*start_command.mutable_working_directory() =
request_command.working_directory();
std::vector<SharedFD> fds;
if (verbose_) {
fds = request.FileDescriptors();
} else {
auto dev_null = SharedFD::Open("/dev/null", O_RDWR);
CF_EXPECT(dev_null->IsOpen(), dev_null->StrError());
fds = {dev_null, dev_null, dev_null};
}
ConvertedAcloudCreateCommand ret{
.start_request = RequestWithStdio(request.Client(), start_request, fds,
request.Credentials())};
for (auto& request_proto : request_protos) {
ret.prep_requests.emplace_back(request.Client(), request_proto, fds,
request.Credentials());
}
return ret;
}
const std::string& FetchCvdArgsFile() const override {
return fetch_cvd_args_file_;
}
const std::string& FetchCommandString() const override {
return fetch_command_str_;
}
bool Verbose() const { return verbose_; }
private:
std::string fetch_cvd_args_file_;
std::string fetch_command_str_;
bool verbose_;
};
fruit::Component<ConvertAcloudCreateCommand>
AcloudCreateConvertCommandComponent() {
return fruit::createComponent()
.bind<ConvertAcloudCreateCommand, ConvertAcloudCreateCommandImpl>();
}
} // namespace cuttlefish