From 48a54690008be23d2736aa9787f7ace5f4eb6397 Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Wed, 27 May 2026 11:09:02 -0700 Subject: [PATCH 01/10] Add operator< for Flag that compares flag names. --- base/cvd/cuttlefish/common/libs/utils/flag_parser.h | 1 + 1 file changed, 1 insertion(+) diff --git a/base/cvd/cuttlefish/common/libs/utils/flag_parser.h b/base/cvd/cuttlefish/common/libs/utils/flag_parser.h index 1baea558ae3..836dbe26861 100644 --- a/base/cvd/cuttlefish/common/libs/utils/flag_parser.h +++ b/base/cvd/cuttlefish/common/libs/utils/flag_parser.h @@ -95,6 +95,7 @@ class Flag { Flag AddValidator(std::function()>) &&; const std::string& Name() const { return name_; } + bool operator<(const Flag& other) const { return Name() < other.Name(); } /* Examines a list of arguments, removing any matches from the list and * invoking the `Setter` for every match. Returns `false` if the callback ever From a2db3788eb5ff6cf299bb578911e8a33f94ac415 Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Tue, 19 May 2026 13:26:33 -0700 Subject: [PATCH 02/10] cvd: DetailedHelp implementation in CvdCommandHandler New virtual functions for description and flags are added to the base command handler class, with default implementation since not all subcommands implement them yet. The default implementation of DetailedHelp will ensure all help messages will have the same style. --- .../common/libs/utils/flag_parser.cpp | 4 +- .../commands/cvd/cli/commands/BUILD.bazel | 4 +- .../cvd/cli/commands/command_handler.cpp | 119 ++++++++++++++++++ .../cvd/cli/commands/command_handler.h | 11 +- 4 files changed, 134 insertions(+), 4 deletions(-) diff --git a/base/cvd/cuttlefish/common/libs/utils/flag_parser.cpp b/base/cvd/cuttlefish/common/libs/utils/flag_parser.cpp index 4ec049e3ecd..b64e206dd5d 100644 --- a/base/cvd/cuttlefish/common/libs/utils/flag_parser.cpp +++ b/base/cvd/cuttlefish/common/libs/utils/flag_parser.cpp @@ -425,10 +425,10 @@ std::ostream& operator<<(std::ostream& out, const Flag& flag) { } out << "\n"; if (flag.help_) { - out << " " << *flag.help_ << "\n"; + out << *flag.help_ << "\n"; } if (flag.getter_) { - out << " Current value: \"" << (*flag.getter_)() << "\"\n"; + out << "Current value: \"" << (*flag.getter_)() << "\"\n"; } return out; } diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel index 1aa127cab42..548cd070083 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel @@ -83,9 +83,12 @@ cf_cc_library( hdrs = ["command_handler.h"], deps = [ "//cuttlefish/common/libs/utils:contains", + "//cuttlefish/common/libs/utils:flag_parser", "//cuttlefish/host/commands/cvd/cli:command_request", "//cuttlefish/host/commands/cvd/cli:types", + "//cuttlefish/host/commands/cvd/cli/selector:selector_common_parser", "//cuttlefish/result", + "@abseil-cpp//absl/strings", ], ) @@ -430,7 +433,6 @@ cf_cc_library( "//cuttlefish/host/commands/cvd/cli/commands:command_handler", "//cuttlefish/host/commands/cvd/cli/commands:host_tool_target", "//cuttlefish/host/commands/cvd/cli/selector", - "//cuttlefish/host/commands/cvd/cli/selector:selector_common_parser", "//cuttlefish/host/commands/cvd/fetch:substitute", "//cuttlefish/host/commands/cvd/instances", "//cuttlefish/host/commands/cvd/instances:cvd_persistent_data", diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.cpp index f50d2be2795..d7cd042f879 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.cpp @@ -16,8 +16,127 @@ #include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" +#include + +#include +#include +#include +#include +#include + +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" + +#include "cuttlefish/common/libs/utils/flag_parser.h" +#include "cuttlefish/host/commands/cvd/cli/command_request.h" +#include "cuttlefish/host/commands/cvd/cli/selector/selector_common_parser.h" +#include "cuttlefish/host/commands/cvd/cli/types.h" +#include "cuttlefish/result/result.h" + namespace cuttlefish { +namespace { + +constexpr size_t kMaxLineLength = 80; + +std::vector WrapAroundLine( + std::string_view str, size_t max_line_length = kMaxLineLength) { + std::vector ret; + std::vector words = absl::StrSplit(str, ' '); + size_t total_word_sizes = 0; + std::vector line; + for (std::string_view word : words) { + // line.size() accounts for the spaces added when joining the words. + if (total_word_sizes + word.size() + line.size() >= max_line_length) { + // If the line is empty at this point it means the current word is too + // long, it will be added to the line anyways and printed on its own in + // the next iteration. + if (!line.empty()) { + ret.push_back(absl::StrJoin(line, " ")); + } + total_word_sizes = 0; + line.clear(); + } + total_word_sizes += word.size(); + line.push_back(word); + } + if (!line.empty()) { + // Print the last line + ret.push_back(absl::StrJoin(line, " ")); + } + return ret; +} + +} // namespace bool CvdCommandHandler::RequiresDeviceExists() const { return false; } +std::vector CvdCommandHandler::Description() const { return {}; } + +Result> CvdCommandHandler::Flags(const CommandRequest&) { + return {}; +} + +Result CvdCommandHandler::DetailedHelp( + const CommandRequest& request) { + std::stringstream ss; + std::vector cmd_list = CmdList(); + CF_EXPECT(!cmd_list.empty(), "Command aliases list is empty"); + + ss << "cvd " << cmd_list[0] << " - " << SummaryHelp() << "\n"; + ss << "\n"; + for (const std::string& paragraph : Description()) { + for (std::string_view line : WrapAroundLine(paragraph)) { + ss << line << "\n"; + } + // Empty line after paragraphs + ss << "\n"; + } + + std::vector flags = CF_EXPECT(Flags(request)); + // Consume flags to ensure "current value" is accurate + cvd_common::Args args = request.SubcommandArguments(); + + CF_EXPECT(ConsumeFlags(flags, args)); + selector::SelectorOptions selector_options = request.Selectors(); + if (RequiresDeviceExists()) { + // Add the common selector flags if the command supports them. This doesn't + // need to hapen before consuming as the selector flags were consumed + // already. Using the selectors from the request ensures the flags's + // "current value" is correct. + std::vector selector_flags = + selector::BuildCommonSelectorFlags(selector_options); + flags.insert(flags.end(), selector_flags.begin(), selector_flags.end()); + } + + // Make sure the flags are in alphabetical order + std::sort(flags.begin(), flags.end()); + + for (const Flag& flag : flags) { + std::stringstream ss2; + ss2 << flag; + std::string flag_help = ss2.str(); + std::vector flag_help_lines = + absl::StrSplit(flag_help, '\n'); + bool first_line = true; + for (std::string_view& line : flag_help_lines) { + if (line.empty()) { + continue; + } + if (first_line) { + // The first line contains the --flag=value examples, don't indent or + // wrap those. + ss << line << "\n"; + first_line = false; + } else { + for (std::string_view l : WrapAroundLine(line, kMaxLineLength - 4)) { + ss << " " << l << "\n"; + } + } + } + ss << "\n"; + } + + return ss.str(); +} + } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.h b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.h index 53f86d0a986..c1067a8f7f8 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.h @@ -18,6 +18,7 @@ #include +#include "cuttlefish/common/libs/utils/flag_parser.h" #include "cuttlefish/host/commands/cvd/cli/command_request.h" #include "cuttlefish/host/commands/cvd/cli/types.h" #include "cuttlefish/result/result.h" @@ -34,8 +35,16 @@ class CvdCommandHandler { // used for command help text virtual std::string SummaryHelp() const = 0; virtual bool RequiresDeviceExists() const; + virtual Result DetailedHelp(const CommandRequest&); + // Each string returned by Description() is treated as a paragraph. The + // default implementation of DetailedHelp will visually separate each + // paragraph. That implementation will also split each paragraph in lines not + // longer than 80 characters, so implementations of Description() don't need + // to (and probably shouldn't) use line breaks for formatting. + virtual std::vector Description() const; + virtual Result> Flags(const CommandRequest&); + virtual bool RequiresHostConfiguration() const { return true; } - virtual Result DetailedHelp(const CommandRequest&) = 0; }; } // namespace cuttlefish From a82db9f9302690314a6aa5824ad90be3647ffe7e Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Tue, 19 May 2026 13:26:44 -0700 Subject: [PATCH 03/10] cvd: Start uses default DetailedHelp implementation --- .../host/commands/cvd/cli/commands/start.cpp | 52 +++++++------------ .../host/commands/cvd/cli/commands/start.h | 3 +- 2 files changed, 20 insertions(+), 35 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.cpp index 307e82b10e1..942a60e05ea 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.cpp @@ -30,7 +30,6 @@ #include #include #include -#include #include #include #include @@ -53,7 +52,6 @@ #include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" #include "cuttlefish/host/commands/cvd/cli/commands/host_tool_target.h" #include "cuttlefish/host/commands/cvd/cli/selector/selector.h" -#include "cuttlefish/host/commands/cvd/cli/selector/selector_common_parser.h" #include "cuttlefish/host/commands/cvd/cli/types.h" #include "cuttlefish/host/commands/cvd/cli/utils.h" #include "cuttlefish/host/commands/cvd/fetch/substitute.h" @@ -75,18 +73,6 @@ namespace cuttlefish { namespace { -constexpr char kCommandDescription[] = - R"(The `cvd start` command applies to the instance group, not specific instances -because Cuttlefish instances in the same group must all be started (and stopped) -in unisom. The group to be started is chosen using the standar selector flags. - -Flags that modify individual instances accept a comma separated list of values. -If the number of values in one of these flags is less than the number of -instances, then the last instances in the group will default to the first value -in the list (as a result a single value provided applies to all instances). The -empty string is interpreted as a valid value, to force a particular instance to -use its intended default value the special value "unset" must be given.)"; - std::optional GetConfigPath(cvd_common::Args& args) { size_t initial_size = args.size(); std::string config_file; @@ -521,34 +507,32 @@ Result CvdStartCommandHandler::LaunchDeviceInterruptible( return {}; } -Result CvdStartCommandHandler::DetailedHelp( +std::vector CvdStartCommandHandler::Description() const { + return { + "The `cvd start` command applies to the instance group, not specific " + "instances because Cuttlefish instances in the same group must all be " + "started (and stopped) in unisom. The group to be started is chosen " + "using the standard selector flags.", + "Flags that modify individual instances accept a comma separated list of " + "values. If the number of values in one of these flags is less than the " + "number of instances, then the last instances in the group will default " + "to the first value in the list (as a result a single value provided " + "applies to all instances). The empty string is interpreted as a valid " + "value, to force a particular instance to use its intended default value " + "the special value \"unset\" must be given."}; +} + +Result> CvdStartCommandHandler::Flags( const CommandRequest& request) { - cvd_common::Args args = request.SubcommandArguments(); std::vector own_flags = BuildOwnFlags(); - CF_EXPECT(ConsumeFlags(own_flags, args)); std::vector internal_flags = CF_EXPECT(GetCvdInternalStartFlags( request.SubcommandArguments(), cvd_common::Envs())); - selector::SelectorOptions selector_options = request.Selectors(); - std::vector selector_flags = - selector::BuildCommonSelectorFlags(selector_options); - - std::vector flags = std::move(selector_flags); - flags.insert(flags.end(), own_flags.begin(), own_flags.end()); + std::vector flags = std::move(own_flags); flags.insert(flags.end(), internal_flags.begin(), internal_flags.end()); - // Make sure the flags are in alphabetical order - std::sort(flags.begin(), flags.end(), - [](auto f1, auto f2) { return f1.Name() < f2.Name(); }); - - std::stringstream ss; - ss << SummaryHelp() << "\n\n"; - ss << kCommandDescription << "\n\n"; - for (const Flag& flag : flags) { - ss << flag << std::endl; - } - return ss.str(); + return flags; } std::vector CvdStartCommandHandler::BuildOwnFlags() { diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.h b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.h index 96f4b840351..01eaa5ac790 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.h @@ -37,9 +37,10 @@ class CvdStartCommandHandler : public CvdCommandHandler { std::string SummaryHelp() const override { return "Start all Cuttlefish Instances in a group"; } + std::vector Description() const override; + Result> Flags(const CommandRequest&) override; bool RequiresDeviceExists() const override { return true; } - Result DetailedHelp(const CommandRequest& request) override; private: Result LaunchDevice(Command command, LocalInstanceGroup& group, From ec2634915370f7cbd2e6506b3efbe11c886e0297 Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Tue, 19 May 2026 15:26:31 -0700 Subject: [PATCH 04/10] Extract help utilities from CvdCommandHandler Not all command handlers will be able to use the default help implementation. They can still reuse some of the lower level utitlities by exposing those in a library. --- .../host/commands/cvd/cli/BUILD.bazel | 12 ++ .../commands/cvd/cli/commands/BUILD.bazel | 3 +- .../cvd/cli/commands/command_handler.cpp | 73 +------------ .../host/commands/cvd/cli/help_format.cpp | 103 ++++++++++++++++++ .../host/commands/cvd/cli/help_format.h | 44 ++++++++ 5 files changed, 164 insertions(+), 71 deletions(-) create mode 100644 base/cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp create mode 100644 base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel index 4ce19f4e81f..4bd4886e856 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel @@ -40,6 +40,18 @@ cf_cc_test( ], ) +cf_cc_library( + name = "help_format", + srcs = ["help_format.cpp"], + hdrs = ["help_format.h"], + deps = [ + "//cuttlefish/common/libs/utils:flag_parser", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/strings", + ], +) + cf_cc_library( name = "interruptible_terminal", srcs = ["interruptible_terminal.cpp"], diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel index 548cd070083..06f8487837b 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel @@ -82,13 +82,12 @@ cf_cc_library( srcs = ["command_handler.cpp"], hdrs = ["command_handler.h"], deps = [ - "//cuttlefish/common/libs/utils:contains", "//cuttlefish/common/libs/utils:flag_parser", "//cuttlefish/host/commands/cvd/cli:command_request", + "//cuttlefish/host/commands/cvd/cli:help_format", "//cuttlefish/host/commands/cvd/cli:types", "//cuttlefish/host/commands/cvd/cli/selector:selector_common_parser", "//cuttlefish/result", - "@abseil-cpp//absl/strings", ], ) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.cpp index d7cd042f879..c6b35f220cd 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.cpp @@ -21,52 +21,16 @@ #include #include #include -#include #include -#include "absl/strings/str_join.h" -#include "absl/strings/str_split.h" - #include "cuttlefish/common/libs/utils/flag_parser.h" #include "cuttlefish/host/commands/cvd/cli/command_request.h" +#include "cuttlefish/host/commands/cvd/cli/help_format.h" #include "cuttlefish/host/commands/cvd/cli/selector/selector_common_parser.h" #include "cuttlefish/host/commands/cvd/cli/types.h" #include "cuttlefish/result/result.h" namespace cuttlefish { -namespace { - -constexpr size_t kMaxLineLength = 80; - -std::vector WrapAroundLine( - std::string_view str, size_t max_line_length = kMaxLineLength) { - std::vector ret; - std::vector words = absl::StrSplit(str, ' '); - size_t total_word_sizes = 0; - std::vector line; - for (std::string_view word : words) { - // line.size() accounts for the spaces added when joining the words. - if (total_word_sizes + word.size() + line.size() >= max_line_length) { - // If the line is empty at this point it means the current word is too - // long, it will be added to the line anyways and printed on its own in - // the next iteration. - if (!line.empty()) { - ret.push_back(absl::StrJoin(line, " ")); - } - total_word_sizes = 0; - line.clear(); - } - total_word_sizes += word.size(); - line.push_back(word); - } - if (!line.empty()) { - // Print the last line - ret.push_back(absl::StrJoin(line, " ")); - } - return ret; -} - -} // namespace bool CvdCommandHandler::RequiresDeviceExists() const { return false; } @@ -84,19 +48,13 @@ Result CvdCommandHandler::DetailedHelp( ss << "cvd " << cmd_list[0] << " - " << SummaryHelp() << "\n"; ss << "\n"; - for (const std::string& paragraph : Description()) { - for (std::string_view line : WrapAroundLine(paragraph)) { - ss << line << "\n"; - } - // Empty line after paragraphs - ss << "\n"; - } + ss << FormatHelpText(Description()); std::vector flags = CF_EXPECT(Flags(request)); // Consume flags to ensure "current value" is accurate cvd_common::Args args = request.SubcommandArguments(); - CF_EXPECT(ConsumeFlags(flags, args)); + selector::SelectorOptions selector_options = request.Selectors(); if (RequiresDeviceExists()) { // Add the common selector flags if the command supports them. This doesn't @@ -111,30 +69,7 @@ Result CvdCommandHandler::DetailedHelp( // Make sure the flags are in alphabetical order std::sort(flags.begin(), flags.end()); - for (const Flag& flag : flags) { - std::stringstream ss2; - ss2 << flag; - std::string flag_help = ss2.str(); - std::vector flag_help_lines = - absl::StrSplit(flag_help, '\n'); - bool first_line = true; - for (std::string_view& line : flag_help_lines) { - if (line.empty()) { - continue; - } - if (first_line) { - // The first line contains the --flag=value examples, don't indent or - // wrap those. - ss << line << "\n"; - first_line = false; - } else { - for (std::string_view l : WrapAroundLine(line, kMaxLineLength - 4)) { - ss << " " << l << "\n"; - } - } - } - ss << "\n"; - } + ss << FormatFlagsHelp(flags); return ss.str(); } diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp new file mode 100644 index 00000000000..83c3d12f650 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2026 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 "cuttlefish/host/commands/cvd/cli/help_format.h" + +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" + +#include "cuttlefish/common/libs/utils/flag_parser.h" + +namespace cuttlefish { +namespace { + +std::vector WrapAroundLine(std::string_view str, + size_t max_line_length) { + std::vector ret; + size_t total_word_sizes = 0; + std::vector line; + for (std::string_view word : absl::StrSplit(str, absl::ByAnyChar(" \t\r\n"))) { + // line.size() accounts for the spaces added when joining the words. + if (total_word_sizes + word.size() + line.size() >= max_line_length) { + // If the line is empty at this point it means the current word is too + // long, it will be added to the line anyways and printed on its own in + // the next iteration. + if (!line.empty()) { + ret.push_back(absl::StrJoin(line, " ")); + } + total_word_sizes = 0; + line.clear(); + } + total_word_sizes += word.size(); + line.push_back(word); + } + if (!line.empty()) { + // Print the last line + ret.push_back(absl::StrJoin(line, " ")); + } + return ret; +} + +std::vector GetFlagHelpMessage(const Flag& flag) { + std::stringstream ss; + ss << flag; + std::vector flag_help_lines = + absl::StrSplit(ss.str(), '\n', absl::SkipWhitespace()); + return flag_help_lines; +} + +} // namespace + +std::string FormatHelpText(const std::vector& text, + size_t max_line_width) { + std::stringstream ss; + for (const std::string& paragraph : text) { + for (std::string_view line : WrapAroundLine(paragraph, max_line_width)) { + ss << line << "\n"; + } + // Empty line after paragraphs + ss << "\n"; + } + return ss.str(); +} + +std::string FormatFlagsHelp(const std::vector& flags, + size_t max_line_width) { + std::stringstream ss; + for (const Flag& flag : flags) { + std::vector help_lines = GetFlagHelpMessage(flag); + CHECK(!help_lines.empty()) + << "Flag produced empty help message: " << flag.Name(); + // The first line contains the --flag=value examples, don't indent or wrap + // those. + ss << help_lines.front() << "\n"; + for (size_t i = 1; i < help_lines.size(); ++i) { + for (std::string_view l : + WrapAroundLine(help_lines[i], max_line_width - 4)) { + ss << " " << l << "\n"; + } + } + ss << "\n"; + } + return ss.str(); +} + +} // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h b/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h new file mode 100644 index 00000000000..43867fe495f --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2026 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. + */ + +#pragma once + +#include + +#include +#include + +#include "cuttlefish/common/libs/utils/flag_parser.h" + +namespace cuttlefish { + +constexpr size_t kDefaultMaxLineWidth = 80; + +// Formats text (typically a command description) to be displayed in the +// terminal. Each string in the input is considered to be a different paragraph +// and should not contain line changes. Each paragraph will be broken into lines +// of at most max_line_width columns without splitting individual words. An +// empty line will be added after each paragraph. +std::string FormatHelpText(const std::vector& text, + size_t max_line_width = kDefaultMaxLineWidth); + +// Formats the help messages of a list of flags to be displayed in the terminal. +// The return string will contain lines of at most max_line_width characters +// except when necessary to avoid splitting a word. +std::string FormatFlagsHelp(const std::vector& flags, + size_t max_line_width = kDefaultMaxLineWidth); + +} // namespace cuttlefish From a0581b0087441497cffc675a1bab7d9cd74fbbf8 Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Tue, 19 May 2026 17:53:10 -0700 Subject: [PATCH 05/10] cvd: help helpers support printing verbatim string By adding a marker to the string that the helper removes, but otherwise leaves the input string unmodified. --- .../cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp | 9 +++++++++ base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp index 83c3d12f650..f15b2df2ae2 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp @@ -29,8 +29,13 @@ namespace cuttlefish { namespace { +constexpr char kRawTextMark[] = "_RAW_TEXT:"; + std::vector WrapAroundLine(std::string_view str, size_t max_line_length) { + if (absl::ConsumePrefix(&str, kRawTextMark)) { + return {std::string(str)}; + } std::vector ret; size_t total_word_sizes = 0; std::vector line; @@ -100,4 +105,8 @@ std::string FormatFlagsHelp(const std::vector& flags, return ss.str(); } +std::string MarkAsRawText(std::string_view str) { + return absl::StrCat(kRawTextMark, str); +} + } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h b/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h index 43867fe495f..9e42dfd3e43 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h @@ -19,6 +19,7 @@ #include #include +#include #include #include "cuttlefish/common/libs/utils/flag_parser.h" @@ -41,4 +42,7 @@ std::string FormatHelpText(const std::vector& text, std::string FormatFlagsHelp(const std::vector& flags, size_t max_line_width = kDefaultMaxLineWidth); +// Marks a string as raw text so that FormatHelpText doesn't modify it. +std::string MarkAsRawText(std::string_view str); + } // namespace cuttlefish From f1e7326a6a2265ea80bd3803d04f9e31a41567a8 Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Tue, 19 May 2026 17:54:27 -0700 Subject: [PATCH 06/10] cvd: Overhaul help text for `cvd load` Use the default DetailedHelp implmentation for consistent formatting. Add more details to the help text. --- .../host/commands/cvd/cli/BUILD.bazel | 1 + .../commands/cvd/cli/commands/BUILD.bazel | 2 +- .../host/commands/cvd/cli/commands/lint.cpp | 20 ++-- .../host/commands/cvd/cli/commands/lint.h | 3 +- .../cvd/cli/commands/load_configs.cpp | 102 +++++++++++++----- .../commands/cvd/cli/commands/load_configs.h | 4 +- .../cvd/cli/parser/load_configs_parser.cpp | 82 ++++++-------- .../cvd/cli/parser/load_configs_parser.h | 8 +- 8 files changed, 136 insertions(+), 86 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel index 4bd4886e856..269d16489cd 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel @@ -116,6 +116,7 @@ cf_cc_library( "//cuttlefish/common/libs/utils:subprocess", "//cuttlefish/common/libs/utils:users", "//cuttlefish/host/commands/cvd/cli:command_request", + "//cuttlefish/host/commands/cvd/cli:help_format", "//cuttlefish/host/commands/cvd/cli:types", "//cuttlefish/host/commands/cvd/cli/commands:bugreport", "//cuttlefish/host/commands/cvd/cli/commands:cache", diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel index 06f8487837b..25b766ebd9e 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel @@ -198,7 +198,7 @@ cf_cc_library( srcs = ["lint.cpp"], hdrs = ["lint.h"], deps = [ - "//cuttlefish/common/libs/utils:files", + "//cuttlefish/common/libs/utils:flag_parser", "//cuttlefish/host/commands/cvd/cli:command_request", "//cuttlefish/host/commands/cvd/cli:types", "//cuttlefish/host/commands/cvd/cli/commands:command_handler", diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/lint.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/lint.cpp index c15d3440fae..ef0e9026ac3 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/lint.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/lint.cpp @@ -21,7 +21,7 @@ #include #include -#include "cuttlefish/common/libs/utils/files.h" +#include "cuttlefish/common/libs/utils/flag_parser.h" #include "cuttlefish/host/commands/cvd/cli/command_request.h" #include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" #include "cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h" @@ -47,8 +47,7 @@ Usage: cvd lint /path/to/input.json Result LintCommandHandler::Handle(const CommandRequest& request) { std::vector args = request.SubcommandArguments(); - auto working_directory = CurrentDirectory(); - const auto config_path = CF_EXPECT(ValidateConfig(args, working_directory)); + const auto config_path = CF_EXPECT(ValidateConfig(args)); std::cout << "Lint of flags and config \"" << config_path << "\" succeeded\n"; @@ -65,10 +64,17 @@ Result LintCommandHandler::DetailedHelp( } Result LintCommandHandler::ValidateConfig( - std::vector& args, const std::string& working_directory) { - const LoadFlags flags = CF_EXPECT(GetFlags(args, working_directory)); - CF_EXPECT(GetEnvironmentSpecification(flags)); - return flags.config_path; + std::vector& args) { + LoadFlags load_flags; + std::vector flags = BuildCvdLoadFlags(load_flags); + CF_EXPECT(ConsumeFlags(flags, args)); + CF_EXPECT( + !args.empty(), + "No arguments provided to cvd command, please provide path to json file"); + std::string& config_path = args.front(); + CF_EXPECT(ValidateCvdLoadFlags(load_flags)); + CF_EXPECT(GetEnvironmentSpecification(config_path, load_flags.overrides)); + return config_path; } std::unique_ptr NewLintCommand() { diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/lint.h b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/lint.h index 7d6e0c7c1e9..9fab5de2252 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/lint.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/lint.h @@ -34,8 +34,7 @@ class LintCommandHandler : public CvdCommandHandler { Result DetailedHelp(const CommandRequest& request) override; private: - Result ValidateConfig(std::vector& args, - const std::string& working_directory); + Result ValidateConfig(std::vector& args); }; std::unique_ptr NewLintCommand(); diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.cpp index e86f98c8c06..650c961dc6a 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.cpp @@ -27,10 +27,12 @@ #include "absl/log/log.h" #include "cuttlefish/common/libs/utils/files.h" +#include "cuttlefish/common/libs/utils/flag_parser.h" #include "cuttlefish/host/commands/cvd/cli/command_request.h" #include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" #include "cuttlefish/host/commands/cvd/cli/commands/fetch.h" #include "cuttlefish/host/commands/cvd/cli/commands/start.h" +#include "cuttlefish/host/commands/cvd/cli/help_format.h" #include "cuttlefish/host/commands/cvd/cli/parser/load_config.pb.h" #include "cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h" #include "cuttlefish/host/commands/cvd/cli/selector/selector_common_parser.h" @@ -52,26 +54,8 @@ namespace { constexpr char kLoadSubCmd[] = "load"; constexpr char kSummaryHelpText[] = - R"(Loads the given JSON configuration file and launches devices based on the options provided)"; + "Creates and starts an instance group from a JSON configuration file"; -constexpr char kDetailedHelpText[] = R"( -Warning: This command is deprecated, use cvd create --config_file instead. - -Usage: -cvd load [--override=:] - -Reads the fields in the JSON configuration file and translates them to corresponding start command and flags. - -Optionally fetches remote artifacts prior to launching the cuttlefish environment. - -The --override flag can be used to give new values for properties in the config file without needing to edit the file directly. Convenient for one-off invocations. -)"; - -Result GetLoadFlags(const CommandRequest& request) { - std::vector args = request.SubcommandArguments(); - auto working_directory = CurrentDirectory(); - return CF_EXPECT(GetFlags(args, working_directory)); -} Result BuildFetchCmd(const CommandRequest& request, const CvdFlags& cvd_flags) { @@ -133,9 +117,17 @@ LoadConfigsCommand::LoadConfigsCommand(InstanceManager& instance_manager) : instance_manager_(instance_manager) {} Result LoadConfigsCommand::Handle(const CommandRequest& request) { - LoadFlags load_flags = CF_EXPECT(GetLoadFlags(request)); + std::vector args = request.SubcommandArguments(); + std::vector flags = CF_EXPECT(Flags(request)); + CF_EXPECT(ConsumeFlags(flags, args)); + CF_EXPECT( + !args.empty(), + "No arguments provided to cvd command, please provide path to json file"); + std::string& config_path = args.front(); + CF_EXPECT(ValidateCvdLoadFlags(flags_)); + EnvironmentSpecification env_spec = - CF_EXPECT(GetEnvironmentSpecification(load_flags)); + CF_EXPECT(GetEnvironmentSpecification(config_path, flags_.overrides)); std::mutex group_creation_mtx; // Have to use the group name because LocalInstanceGroup can't be default @@ -181,7 +173,7 @@ Result LoadConfigsCommand::Handle(const CommandRequest& request) { group_creation_mtx.lock(); // Don't use CF_EXPECT here or the mutex will be left locked. auto group_res = - CreateGroup(instance_manager_, load_flags.base_dir, env_spec); + CreateGroup(instance_manager_, flags_.base_dir, env_spec); if (group_res.ok()) { // Have to initialize the group_name variable before releasing the mutex. group_name = (*group_res).GroupName(); @@ -250,9 +242,71 @@ cvd_common::Args LoadConfigsCommand::CmdList() const { return {kLoadSubCmd}; } std::string LoadConfigsCommand::SummaryHelp() const { return kSummaryHelpText; } -Result LoadConfigsCommand::DetailedHelp( +std::vector LoadConfigsCommand::Description() const { + return { + "This command is an alias of `cvd create --config_file= " + "[--override=:]...`, provided for convenience and backwards " + "compatibility.", + + "Usage:", + + " cvd load [--override=:]", + + "Creates and starts a new instance group from a specification file. An " + "example specification file looks like:", + + MarkAsRawText(R"( { + "instances": [ + { + "name": "ins-1", + "disk": { + "default_build": "@ab/aosp-android-latest-release/aosp_cf_x86_64_only_phone-userdebug" + }, + "vm": { + "cpus": 8, + "memory_mb": 2048 + } + }, + { + "name": "ins-2", + "disk": { + "default_build": "/path/to/android/build" + } + } + ] + })"), + + "A complete reference of the specification file format can be found in " + "https://github.com/google/android-cuttlefish/blob/main/base/cvd/" + "cuttlefish/host/commands/cvd/cli/parser/load_config.proto.", + + "While most config file properties are self explanatory, the build " + "properties (default_build, kernel.build, bootloader.build, etc) require " + "more explanation. These properties support two types of values:", + + MarkAsRawText( + R"( - "@ab/[/[{}]]" + - "")"), + + "If the build value starts with \"@ab\", cvd will fetch the specified " + "Android build target from the Android build servers. By default it will " + "download the cuttlefish host package archive or the images zip as " + "needed, but for more advanced use cases the file to download from the " + "server can be specified with the optional parameter in curly " + "braces. For more information on build fetching and caching operations " + "refer to `cvd help fetch`.", + + "Alternatively, the build value may point to an absolute path (starts " + "with '/') in the filesystem where the Android source code has been " + "checked out and a Cuttlefish target has been built. This is " + "particularly useful for rapid iteration during development in " + "combination with incremental builds and the `cvd stop` and `cvd start` " + "subcommands."}; +} + +Result> LoadConfigsCommand::Flags( const CommandRequest& request) { - return kDetailedHelpText; + return BuildCvdLoadFlags(flags_); } std::unique_ptr NewLoadConfigsCommand( diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.h b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.h index 99aed1213a1..8989b0f385e 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.h @@ -35,13 +35,15 @@ class LoadConfigsCommand : public CvdCommandHandler { Result Handle(const CommandRequest& request) override; cvd_common::Args CmdList() const override; std::string SummaryHelp() const override; - Result DetailedHelp(const CommandRequest& request) override; + std::vector Description() const override; + Result> Flags(const CommandRequest&) override; private: Result LoadGroup(const CommandRequest& request, LocalInstanceGroup& group, CvdFlags cvd_flags); InstanceManager& instance_manager_; + LoadFlags flags_; }; /* diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp index 13becd1989b..889c6607c32 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp @@ -172,29 +172,6 @@ Json::Value OverrideToJson(const std::string& key, return leaf; } -std::vector GetFlagsVector(LoadFlags& load_flags) { - std::vector flags; - flags.emplace_back( - GflagsCompatFlag("credential_source", load_flags.credential_source)); - flags.emplace_back(GflagsCompatFlag("project_id", load_flags.project_id)); - flags.emplace_back( - GflagsCompatFlag("base_directory", load_flags.base_dir) - .Help( - "Parent directory for artifacts and runtime files. Defaults to " + - CvdDir() + "/.")); - flags.emplace_back(GflagsCompatFlagOverride("override", load_flags.overrides) - .Help("Use --override=: " - "to override config values")); - return flags; -} - -void MakeAbsolute(std::string& path, const std::string& working_dir) { - if (!path.empty() && path[0] == '/') { - return; - } - path.insert(0, working_dir + "/"); -} - Result ParseJsonFile(const std::string& file_path) { CF_EXPECTF(FileExists(file_path), "Provided file \"{}\" to cvd command does not exist", file_path); @@ -266,6 +243,32 @@ EnvironmentSpecification FillEmptyInstanceNames( } // namespace +std::vector BuildCvdLoadFlags(LoadFlags& load_flags) { + std::vector flags; + flags.emplace_back( + GflagsCompatFlag("credential_source", load_flags.credential_source) + .Help("Source of credentials to access the Android Build Server API. " + "Can be left empty in most cases, see the help for `login` and " + "`fetch` for details.")); + flags.emplace_back(GflagsCompatFlag("project_id", load_flags.project_id) + .Help("Google Cloud Project ID for Android Build " + "Server API access and quotas.")); + flags.emplace_back( + GflagsCompatFlag("base_directory", load_flags.base_dir) + .Help(fmt::format( + "Parent directory for artifacts and runtime files. When not " + "provided a new directory under {}/{} will be created.", + CvdDir(), getuid()))); + flags.emplace_back( + GflagsCompatFlagOverride("override", load_flags.overrides) + .Help( + "Override or add new properties to the group specification. This " + "flag may appear multiple times to affect different properties. " + "The format is `--override=:`. " + "Forexample: `--override=instance.0.vm.cpus=16`.")); + return flags; +} + Result GetGroupCreationDirectories( const std::string& parent_directory, const EnvironmentSpecification& env_spec) { @@ -335,26 +338,12 @@ std::ostream& operator<<(std::ostream& out, const Override& override) { return out; } -Result GetFlags(std::vector& args, - const std::string& working_directory) { - LoadFlags load_flags; - auto flags = GetFlagsVector(load_flags); - CF_EXPECT(ConsumeFlags(flags, args)); - CF_EXPECT( - !args.empty(), - "No arguments provided to cvd command, please provide path to json file"); - - if (!load_flags.base_dir.empty()) { - MakeAbsolute(load_flags.base_dir, working_directory); - } - - load_flags.config_path = args.front(); - MakeAbsolute(load_flags.config_path, working_directory); +Result ValidateCvdLoadFlags(LoadFlags& load_flags) { + auto working_directory = CurrentDirectory(); if (!load_flags.credential_source.empty()) { for (const auto& flag : load_flags.overrides) { - CF_EXPECT(!absl::StartsWith(flag.config_path, - kCredentialSourceOverride), + CF_EXPECT(!absl::StartsWith(flag.config_path, kCredentialSourceOverride), "Specifying both --override=fetch.credential_source and the " "--credential_source flag is not allowed."); } @@ -364,22 +353,21 @@ Result GetFlags(std::vector& args, } if (!load_flags.project_id.empty()) { for (const auto& flag : load_flags.overrides) { - CF_EXPECT( - !absl::StartsWith(flag.config_path, kProjectIDOverride), - "Specifying both --override=fetch.project_id and the " - "--project_id flag is not allowed."); + CF_EXPECT(!absl::StartsWith(flag.config_path, kProjectIDOverride), + "Specifying both --override=fetch.project_id and the " + "--project_id flag is not allowed."); } load_flags.overrides.emplace_back( Override{.config_path = std::string(kProjectIDOverride), .new_value = load_flags.project_id}); } - return load_flags; + return {}; } Result GetEnvironmentSpecification( - const LoadFlags& flags) { + const std::string& config_path, const std::vector& overrides) { Json::Value json_configs = - CF_EXPECT(GetOverriddenConfig(flags.config_path, flags.overrides)); + CF_EXPECT(GetOverriddenConfig(config_path, overrides)); EnvironmentSpecification env_spec = CF_EXPECT(ValidateCfConfigs(json_configs)); diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h index faefdd12e25..21e0a794778 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h @@ -44,17 +44,17 @@ std::ostream& operator<<(std::ostream& out, const Override& override); struct LoadFlags { std::vector overrides; - std::string config_path; std::string credential_source; std::string project_id; std::string base_dir; }; -Result GetFlags(std::vector& args, - const std::string& working_directory); +std::vector BuildCvdLoadFlags(LoadFlags& load_flags); + +Result ValidateCvdLoadFlags(LoadFlags& load_flags); Result GetEnvironmentSpecification( - const LoadFlags& flags); + const std::string& config_path, const std::vector& overrides); Result GetGroupCreationDirectories( const std::string& parent_directory, From bd5dbe16745509311edd557a83a6e9c325e5d444 Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Wed, 20 May 2026 19:47:55 -0700 Subject: [PATCH 07/10] cvd: Skip the middleman when parsing load flags The credential_source and project_id flags are parsed into respective string fields, only to later add them to the overrides list. This change makes the setter and getter of those flags interact with the overrides list directly. Assisted-by: Gemini:Next --- .../host/commands/cvd/cli/commands/lint.cpp | 1 - .../cvd/cli/commands/load_configs.cpp | 1 - .../host/commands/cvd/cli/parser/BUILD.bazel | 2 + .../cvd/cli/parser/flags_parser_test.cc | 116 ++++++++++++++++++ .../cvd/cli/parser/load_configs_parser.cpp | 67 +++++----- .../cvd/cli/parser/load_configs_parser.h | 4 - 6 files changed, 154 insertions(+), 37 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/lint.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/lint.cpp index ef0e9026ac3..565a4d319f4 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/lint.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/lint.cpp @@ -72,7 +72,6 @@ Result LintCommandHandler::ValidateConfig( !args.empty(), "No arguments provided to cvd command, please provide path to json file"); std::string& config_path = args.front(); - CF_EXPECT(ValidateCvdLoadFlags(load_flags)); CF_EXPECT(GetEnvironmentSpecification(config_path, load_flags.overrides)); return config_path; } diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.cpp index 650c961dc6a..b95762d84f8 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.cpp @@ -124,7 +124,6 @@ Result LoadConfigsCommand::Handle(const CommandRequest& request) { !args.empty(), "No arguments provided to cvd command, please provide path to json file"); std::string& config_path = args.front(); - CF_EXPECT(ValidateCvdLoadFlags(flags_)); EnvironmentSpecification env_spec = CF_EXPECT(GetEnvironmentSpecification(config_path, flags_.overrides)); diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/BUILD.bazel index 9844bff6dec..4676f2c2034 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/BUILD.bazel @@ -89,6 +89,8 @@ cf_cc_test( name = "flags_parser_test", srcs = ["flags_parser_test.cc"], deps = [ + "//cuttlefish/common/libs/utils:flag_parser", + "//cuttlefish/host/commands/cvd/cli/parser:load_configs_parser", "//cuttlefish/host/commands/cvd/cli/parser:test_common", "//libbase", ], diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/flags_parser_test.cc b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/flags_parser_test.cc index 77c4f144dca..10580ac3424 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/flags_parser_test.cc +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/flags_parser_test.cc @@ -14,11 +14,15 @@ * limitations under the License. */ +#include +#include #include #include #include +#include "cuttlefish/common/libs/utils/flag_parser.h" +#include "cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h" #include "cuttlefish/host/commands/cvd/cli/parser/test_common.h" namespace cuttlefish { @@ -170,4 +174,116 @@ TEST(BootFlagsParserTest, ParseNetSimFlagEnabled) { << "netsim_uwb flag is missing or wrongly formatted"; } +TEST(CvdLoadFlagsTest, CredentialSourceGetterSetter) { + LoadFlags load_flags; + + // Test Setter + { + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = {"--credential_source=first_val"}; + auto result = ConsumeFlags(flags, args); + ASSERT_TRUE(result.ok()) << result.error().Trace(); + + ASSERT_EQ(load_flags.overrides.size(), 1); + EXPECT_EQ(load_flags.overrides[0].config_path, "fetch.credential_source"); + EXPECT_EQ(load_flags.overrides[0].new_value, "first_val"); + } + + // Test Getter (should return "first_val" now) + { + auto flags = BuildCvdLoadFlags(load_flags); + auto flag_it = std::find_if(flags.begin(), flags.end(), [](const Flag& f) { + return f.Name() == "credential_source"; + }); + ASSERT_NE(flag_it, flags.end()); + + std::stringstream ss; + ss << *flag_it; + EXPECT_NE(ss.str().find("Current value: \"first_val\""), std::string::npos) + << "Help text was: " << ss.str(); + } + + // Test Setter again (should append and override) + { + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = {"--credential_source=second_val"}; + auto result = ConsumeFlags(flags, args); + ASSERT_TRUE(result.ok()) << result.error().Trace(); + + ASSERT_EQ(load_flags.overrides.size(), 2); + EXPECT_EQ(load_flags.overrides[1].config_path, "fetch.credential_source"); + EXPECT_EQ(load_flags.overrides[1].new_value, "second_val"); + } + + // Test Getter again (should return "second_val" because it searches from the end) + { + auto flags = BuildCvdLoadFlags(load_flags); + auto flag_it = std::find_if(flags.begin(), flags.end(), [](const Flag& f) { + return f.Name() == "credential_source"; + }); + ASSERT_NE(flag_it, flags.end()); + + std::stringstream ss; + ss << *flag_it; + EXPECT_NE(ss.str().find("Current value: \"second_val\""), std::string::npos) + << "Help text was: " << ss.str(); + } +} + +TEST(CvdLoadFlagsTest, ProjectIDGetterSetter) { + LoadFlags load_flags; + + // Test Setter + { + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = {"--project_id=first_project"}; + auto result = ConsumeFlags(flags, args); + ASSERT_TRUE(result.ok()) << result.error().Trace(); + + ASSERT_EQ(load_flags.overrides.size(), 1); + EXPECT_EQ(load_flags.overrides[0].config_path, "fetch.project_id"); + EXPECT_EQ(load_flags.overrides[0].new_value, "first_project"); + } + + // Test Getter + { + auto flags = BuildCvdLoadFlags(load_flags); + auto flag_it = std::find_if(flags.begin(), flags.end(), [](const Flag& f) { + return f.Name() == "project_id"; + }); + ASSERT_NE(flag_it, flags.end()); + + std::stringstream ss; + ss << *flag_it; + EXPECT_NE(ss.str().find("Current value: \"first_project\""), std::string::npos) + << "Help text was: " << ss.str(); + } + + // Test Setter again + { + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = {"--project_id=second_project"}; + auto result = ConsumeFlags(flags, args); + ASSERT_TRUE(result.ok()) << result.error().Trace(); + + ASSERT_EQ(load_flags.overrides.size(), 2); + EXPECT_EQ(load_flags.overrides[1].config_path, "fetch.project_id"); + EXPECT_EQ(load_flags.overrides[1].new_value, "second_project"); + } + + // Test Getter again + { + auto flags = BuildCvdLoadFlags(load_flags); + auto flag_it = std::find_if(flags.begin(), flags.end(), [](const Flag& f) { + return f.Name() == "project_id"; + }); + ASSERT_NE(flag_it, flags.end()); + + std::stringstream ss; + ss << *flag_it; + EXPECT_NE(ss.str().find("Current value: \"second_project\""), std::string::npos) + << "Help text was: " << ss.str(); + } +} + } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp index 889c6607c32..5b60de4598f 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp @@ -246,13 +246,44 @@ EnvironmentSpecification FillEmptyInstanceNames( std::vector BuildCvdLoadFlags(LoadFlags& load_flags) { std::vector flags; flags.emplace_back( - GflagsCompatFlag("credential_source", load_flags.credential_source) + GflagsCompatFlag("credential_source") .Help("Source of credentials to access the Android Build Server API. " "Can be left empty in most cases, see the help for `login` and " - "`fetch` for details.")); - flags.emplace_back(GflagsCompatFlag("project_id", load_flags.project_id) - .Help("Google Cloud Project ID for Android Build " - "Server API access and quotas.")); + "`fetch` for details.") + .Setter([&load_flags](const FlagMatch& match) -> Result { + load_flags.overrides.push_back( + Override{.config_path = std::string(kCredentialSourceOverride), + .new_value = match.value}); + return {}; + }) + .Getter([&load_flags]() -> std::string { + for (auto it = load_flags.overrides.rbegin(); + it != load_flags.overrides.rend(); ++it) { + if (it->config_path == kCredentialSourceOverride) { + return it->new_value; + } + } + return ""; + })); + flags.emplace_back( + GflagsCompatFlag("project_id") + .Help("Google Cloud Project ID for Android Build " + "Server API access and quotas.") + .Setter([&load_flags](const FlagMatch& match) -> Result { + load_flags.overrides.push_back( + Override{.config_path = std::string(kProjectIDOverride), + .new_value = match.value}); + return {}; + }) + .Getter([&load_flags]() -> std::string { + for (auto it = load_flags.overrides.rbegin(); + it != load_flags.overrides.rend(); ++it) { + if (it->config_path == kProjectIDOverride) { + return it->new_value; + } + } + return ""; + })); flags.emplace_back( GflagsCompatFlag("base_directory", load_flags.base_dir) .Help(fmt::format( @@ -338,32 +369,6 @@ std::ostream& operator<<(std::ostream& out, const Override& override) { return out; } -Result ValidateCvdLoadFlags(LoadFlags& load_flags) { - auto working_directory = CurrentDirectory(); - - if (!load_flags.credential_source.empty()) { - for (const auto& flag : load_flags.overrides) { - CF_EXPECT(!absl::StartsWith(flag.config_path, kCredentialSourceOverride), - "Specifying both --override=fetch.credential_source and the " - "--credential_source flag is not allowed."); - } - load_flags.overrides.emplace_back( - Override{.config_path = std::string(kCredentialSourceOverride), - .new_value = load_flags.credential_source}); - } - if (!load_flags.project_id.empty()) { - for (const auto& flag : load_flags.overrides) { - CF_EXPECT(!absl::StartsWith(flag.config_path, kProjectIDOverride), - "Specifying both --override=fetch.project_id and the " - "--project_id flag is not allowed."); - } - load_flags.overrides.emplace_back( - Override{.config_path = std::string(kProjectIDOverride), - .new_value = load_flags.project_id}); - } - return {}; -} - Result GetEnvironmentSpecification( const std::string& config_path, const std::vector& overrides) { Json::Value json_configs = diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h index 21e0a794778..270d378fe90 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h @@ -44,15 +44,11 @@ std::ostream& operator<<(std::ostream& out, const Override& override); struct LoadFlags { std::vector overrides; - std::string credential_source; - std::string project_id; std::string base_dir; }; std::vector BuildCvdLoadFlags(LoadFlags& load_flags); -Result ValidateCvdLoadFlags(LoadFlags& load_flags); - Result GetEnvironmentSpecification( const std::string& config_path, const std::vector& overrides); From e61cff8c209170f7e367b0772f73c3e2532bc5c2 Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Wed, 20 May 2026 20:00:16 -0700 Subject: [PATCH 08/10] cvd load: Validate that override flags are unique Overriding the same property in the same instance is likely an error, so it's best to fail than produce unexpected results for the user. --- .../host/commands/cvd/cli/parser/BUILD.bazel | 1 + .../cvd/cli/parser/flags_parser_test.cc | 224 ++++++++++-------- .../cvd/cli/parser/load_configs_parser.cpp | 103 ++++---- .../cvd/cli/parser/load_configs_parser.h | 14 +- 4 files changed, 178 insertions(+), 164 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/BUILD.bazel index 4676f2c2034..e65d56d1f7d 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/BUILD.bazel @@ -92,6 +92,7 @@ cf_cc_test( "//cuttlefish/common/libs/utils:flag_parser", "//cuttlefish/host/commands/cvd/cli/parser:load_configs_parser", "//cuttlefish/host/commands/cvd/cli/parser:test_common", + "//cuttlefish/result:result_matchers", "//libbase", ], ) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/flags_parser_test.cc b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/flags_parser_test.cc index 10580ac3424..894dff6f2b9 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/flags_parser_test.cc +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/flags_parser_test.cc @@ -24,6 +24,7 @@ #include "cuttlefish/common/libs/utils/flag_parser.h" #include "cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h" #include "cuttlefish/host/commands/cvd/cli/parser/test_common.h" +#include "cuttlefish/result/result_matchers.h" namespace cuttlefish { @@ -174,116 +175,131 @@ TEST(BootFlagsParserTest, ParseNetSimFlagEnabled) { << "netsim_uwb flag is missing or wrongly formatted"; } -TEST(CvdLoadFlagsTest, CredentialSourceGetterSetter) { +TEST(CvdLoadFlagsTest, CredentialSourceSetter) { LoadFlags load_flags; - // Test Setter - { - auto flags = BuildCvdLoadFlags(load_flags); - std::vector args = {"--credential_source=first_val"}; - auto result = ConsumeFlags(flags, args); - ASSERT_TRUE(result.ok()) << result.error().Trace(); - - ASSERT_EQ(load_flags.overrides.size(), 1); - EXPECT_EQ(load_flags.overrides[0].config_path, "fetch.credential_source"); - EXPECT_EQ(load_flags.overrides[0].new_value, "first_val"); - } - - // Test Getter (should return "first_val" now) - { - auto flags = BuildCvdLoadFlags(load_flags); - auto flag_it = std::find_if(flags.begin(), flags.end(), [](const Flag& f) { - return f.Name() == "credential_source"; - }); - ASSERT_NE(flag_it, flags.end()); - - std::stringstream ss; - ss << *flag_it; - EXPECT_NE(ss.str().find("Current value: \"first_val\""), std::string::npos) - << "Help text was: " << ss.str(); - } - - // Test Setter again (should append and override) - { - auto flags = BuildCvdLoadFlags(load_flags); - std::vector args = {"--credential_source=second_val"}; - auto result = ConsumeFlags(flags, args); - ASSERT_TRUE(result.ok()) << result.error().Trace(); - - ASSERT_EQ(load_flags.overrides.size(), 2); - EXPECT_EQ(load_flags.overrides[1].config_path, "fetch.credential_source"); - EXPECT_EQ(load_flags.overrides[1].new_value, "second_val"); - } - - // Test Getter again (should return "second_val" because it searches from the end) - { - auto flags = BuildCvdLoadFlags(load_flags); - auto flag_it = std::find_if(flags.begin(), flags.end(), [](const Flag& f) { - return f.Name() == "credential_source"; - }); - ASSERT_NE(flag_it, flags.end()); - - std::stringstream ss; - ss << *flag_it; - EXPECT_NE(ss.str().find("Current value: \"second_val\""), std::string::npos) - << "Help text was: " << ss.str(); - } + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = {"--credential_source=foo"}; + auto result = ConsumeFlags(flags, args); + ASSERT_TRUE(result.ok()) << result.error().Trace(); + + ASSERT_EQ(load_flags.overrides.size(), 1); + EXPECT_EQ(load_flags.overrides.count("fetch.credential_source"), 1); + EXPECT_EQ(load_flags.overrides["fetch.credential_source"], "foo"); +} + +TEST(CvdLoadFlagsTest, CredentialSourceGetter) { + LoadFlags load_flags; + load_flags.overrides["fetch.credential_source"] = "bar"; + + auto flags = BuildCvdLoadFlags(load_flags); + auto flag_it = std::find_if(flags.begin(), flags.end(), [](const Flag& f) { + return f.Name() == "credential_source"; + }); + ASSERT_NE(flag_it, flags.end()); + + std::stringstream ss; + ss << *flag_it; + EXPECT_NE(ss.str().find("Current value: \"bar\""), std::string::npos) + << "Help text was: " << ss.str(); +} + +TEST(CvdLoadFlagsTest, CredentialSourceDuplicated) { + LoadFlags load_flags; + + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = {"--credential_source=first_val", + "--credential_source=second_val"}; + auto result = ConsumeFlags(flags, args); + EXPECT_THAT(result, IsError()); +} + +TEST(CvdLoadFlagsTest, ProjectIDSetter) { + LoadFlags load_flags; + + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = {"--project_id=foo"}; + auto result = ConsumeFlags(flags, args); + ASSERT_TRUE(result.ok()) << result.error().Trace(); + + ASSERT_EQ(load_flags.overrides.size(), 1); + EXPECT_EQ(load_flags.overrides.count("fetch.project_id"), 1); + EXPECT_EQ(load_flags.overrides["fetch.project_id"], "foo"); +} + +TEST(CvdLoadFlagsTest, ProjectIDGetter) { + LoadFlags load_flags; + load_flags.overrides["fetch.project_id"] = "bar"; + + auto flags = BuildCvdLoadFlags(load_flags); + auto flag_it = std::find_if(flags.begin(), flags.end(), [](const Flag& f) { + return f.Name() == "project_id"; + }); + ASSERT_NE(flag_it, flags.end()); + + std::stringstream ss; + ss << *flag_it; + EXPECT_NE(ss.str().find("Current value: \"bar\""), + std::string::npos) + << "Help text was: " << ss.str(); } -TEST(CvdLoadFlagsTest, ProjectIDGetterSetter) { +TEST(CvdLoadFlagsTest, ProjectIDDuplicated) { LoadFlags load_flags; - // Test Setter - { - auto flags = BuildCvdLoadFlags(load_flags); - std::vector args = {"--project_id=first_project"}; - auto result = ConsumeFlags(flags, args); - ASSERT_TRUE(result.ok()) << result.error().Trace(); - - ASSERT_EQ(load_flags.overrides.size(), 1); - EXPECT_EQ(load_flags.overrides[0].config_path, "fetch.project_id"); - EXPECT_EQ(load_flags.overrides[0].new_value, "first_project"); - } - - // Test Getter - { - auto flags = BuildCvdLoadFlags(load_flags); - auto flag_it = std::find_if(flags.begin(), flags.end(), [](const Flag& f) { - return f.Name() == "project_id"; - }); - ASSERT_NE(flag_it, flags.end()); - - std::stringstream ss; - ss << *flag_it; - EXPECT_NE(ss.str().find("Current value: \"first_project\""), std::string::npos) - << "Help text was: " << ss.str(); - } - - // Test Setter again - { - auto flags = BuildCvdLoadFlags(load_flags); - std::vector args = {"--project_id=second_project"}; - auto result = ConsumeFlags(flags, args); - ASSERT_TRUE(result.ok()) << result.error().Trace(); - - ASSERT_EQ(load_flags.overrides.size(), 2); - EXPECT_EQ(load_flags.overrides[1].config_path, "fetch.project_id"); - EXPECT_EQ(load_flags.overrides[1].new_value, "second_project"); - } - - // Test Getter again - { - auto flags = BuildCvdLoadFlags(load_flags); - auto flag_it = std::find_if(flags.begin(), flags.end(), [](const Flag& f) { - return f.Name() == "project_id"; - }); - ASSERT_NE(flag_it, flags.end()); - - std::stringstream ss; - ss << *flag_it; - EXPECT_NE(ss.str().find("Current value: \"second_project\""), std::string::npos) - << "Help text was: " << ss.str(); - } + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = {"--project_id=first_project", + "--project_id=second_project"}; + auto result = ConsumeFlags(flags, args); + EXPECT_FALSE(result.ok()) << "Expected duplicate flag to fail"; +} + +TEST(CvdLoadFlagsTest, CredentialSourceConflictWithOverride) { + LoadFlags load_flags; + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = { + "--override=fetch.credential_source:override_val", + "--credential_source=flag_val"}; + auto result = ConsumeFlags(flags, args); + EXPECT_FALSE(result.ok()) << "Expected override followed by flag to fail"; +} + +TEST(CvdLoadFlagsTest, OverrideConflictWithCredentialSource) { + LoadFlags load_flags; + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = { + "--credential_source=flag_val", + "--override=fetch.credential_source:override_val"}; + auto result = ConsumeFlags(flags, args); + EXPECT_FALSE(result.ok()) << "Expected flag followed by override to fail"; +} + +TEST(CvdLoadFlagsTest, ProjectIDConflictWithOverride) { + LoadFlags load_flags; + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = { + "--override=fetch.project_id:override_project", + "--project_id=flag_project"}; + auto result = ConsumeFlags(flags, args); + EXPECT_FALSE(result.ok()) << "Expected override followed by flag to fail"; +} + +TEST(CvdLoadFlagsTest, OverrideConflictWithProjectID) { + LoadFlags load_flags; + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = { + "--project_id=flag_project", + "--override=fetch.project_id:override_project"}; + auto result = ConsumeFlags(flags, args); + EXPECT_FALSE(result.ok()) << "Expected flag followed by override to fail"; +} + +TEST(CvdLoadFlagsTest, DuplicateOverridesFail) { + LoadFlags load_flags; + auto flags = BuildCvdLoadFlags(load_flags); + std::vector args = {"--override=fetch.credential_source:val1", "--override=fetch.credential_source:val2"}; + auto result = ConsumeFlags(flags, args); + EXPECT_FALSE(result.ok()) << "Expected duplicate overrides to fail"; } } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp index 5b60de4598f..bcfa7b5cb60 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp @@ -71,35 +71,39 @@ bool IsLocalBuild(std::string path) { } Flag GflagsCompatFlagOverride(const std::string& name, - std::vector& values) { + std::map& overrides) { return GflagsCompatFlag(name) - .Getter([&values]() { - return absl::StrJoin(values, ",", absl::StreamFormatter()); + .Getter([&overrides]() { + std::vector formatted; + for (const auto& [k, v] : overrides) { + formatted.push_back(fmt::format("({}=\"{}\")", k, v)); + } + return absl::StrJoin(formatted, ","); }) - .Setter([&values](const FlagMatch& match) -> Result { + .Setter([&overrides](const FlagMatch& match) -> Result { size_t separator_index = match.value.find(kOverrideSeparator); CF_EXPECTF(separator_index != std::string::npos, "Unable to find separator \"{}\" in input \"{}\"", kOverrideSeparator, match.value); - auto result = - Override{.config_path = match.value.substr(0, separator_index), - .new_value = match.value.substr(separator_index + 1)}; - CF_EXPECTF(!result.config_path.empty(), + auto property_path = match.value.substr(0, separator_index); + auto new_value = match.value.substr(separator_index + 1); + CF_EXPECTF(overrides.count(property_path) == 0, + "Property \"{}\" is already overridden", property_path); + CF_EXPECTF(!property_path.empty(), "Config path before the separator \"{}\" cannot be empty in " "input \"{}\"", kOverrideSeparator, match.value); - CF_EXPECTF(!result.new_value.empty(), + CF_EXPECTF(!new_value.empty(), "New value after the separator \"{}\" cannot be empty in " "input \"{}\"", kOverrideSeparator, match.value); - CF_EXPECTF(result.config_path.front() != '.' && - result.config_path.back() != '.', + CF_EXPECTF(property_path.front() != '.' && property_path.back() != '.', "Config path \"{}\" must not start or end with dot", - result.config_path); - CF_EXPECTF(result.config_path.find("..") == std::string::npos, + property_path); + CF_EXPECTF(property_path.find("..") == std::string::npos, "Config path \"{}\" cannot contain two consecutive dots", - result.config_path); - values.emplace_back(result); + property_path); + overrides[property_path] = new_value; return {}; }); } @@ -204,14 +208,11 @@ std::optional GetSystemHostPath( Result GetOverriddenConfig( const std::string& config_path, - const std::vector& override_flags) { + const std::map& override_flags) { Json::Value result = CF_EXPECT(ParseJsonFile(config_path)); - if (!override_flags.empty()) { - for (const auto& flag : override_flags) { - MergeTwoJsonObjs(result, - OverrideToJson(flag.config_path, flag.new_value)); - } + for (const auto& [key, val] : override_flags) { + MergeTwoJsonObjs(result, OverrideToJson(key, val)); } return result; @@ -245,23 +246,33 @@ EnvironmentSpecification FillEmptyInstanceNames( std::vector BuildCvdLoadFlags(LoadFlags& load_flags) { std::vector flags; + flags.emplace_back( + GflagsCompatFlagOverride("override", load_flags.overrides) + .Help( + "Override or add new properties to the group specification. This " + "flag may appear multiple times to affect different properties. " + "The format is `--override=:`. " + "For example: `--override=instance.0.vm.cpus=16`.")); flags.emplace_back( GflagsCompatFlag("credential_source") .Help("Source of credentials to access the Android Build Server API. " "Can be left empty in most cases, see the help for `login` and " "`fetch` for details.") .Setter([&load_flags](const FlagMatch& match) -> Result { - load_flags.overrides.push_back( - Override{.config_path = std::string(kCredentialSourceOverride), - .new_value = match.value}); + CF_EXPECTF(load_flags.overrides.count( + std::string(kCredentialSourceOverride)) == 0, + "Specifying both --override={} and the " + "--credential_source flag is not allowed.", + kCredentialSourceOverride); + load_flags.overrides[std::string(kCredentialSourceOverride)] = + match.value; return {}; }) .Getter([&load_flags]() -> std::string { - for (auto it = load_flags.overrides.rbegin(); - it != load_flags.overrides.rend(); ++it) { - if (it->config_path == kCredentialSourceOverride) { - return it->new_value; - } + auto it = load_flags.overrides.find( + std::string(kCredentialSourceOverride)); + if (it != load_flags.overrides.end()) { + return it->second; } return ""; })); @@ -270,17 +281,19 @@ std::vector BuildCvdLoadFlags(LoadFlags& load_flags) { .Help("Google Cloud Project ID for Android Build " "Server API access and quotas.") .Setter([&load_flags](const FlagMatch& match) -> Result { - load_flags.overrides.push_back( - Override{.config_path = std::string(kProjectIDOverride), - .new_value = match.value}); + CF_EXPECTF(load_flags.overrides.count( + std::string(kProjectIDOverride)) == 0, + "Specifying both --override={} and the --project_id " + "flag is not allowed.", + kProjectIDOverride); + load_flags.overrides[std::string(kProjectIDOverride)] = match.value; return {}; }) .Getter([&load_flags]() -> std::string { - for (auto it = load_flags.overrides.rbegin(); - it != load_flags.overrides.rend(); ++it) { - if (it->config_path == kProjectIDOverride) { - return it->new_value; - } + auto it = + load_flags.overrides.find(std::string(kProjectIDOverride)); + if (it != load_flags.overrides.end()) { + return it->second; } return ""; })); @@ -290,13 +303,6 @@ std::vector BuildCvdLoadFlags(LoadFlags& load_flags) { "Parent directory for artifacts and runtime files. When not " "provided a new directory under {}/{} will be created.", CvdDir(), getuid()))); - flags.emplace_back( - GflagsCompatFlagOverride("override", load_flags.overrides) - .Help( - "Override or add new properties to the group specification. This " - "flag may appear multiple times to affect different properties. " - "The format is `--override=:`. " - "Forexample: `--override=instance.0.vm.cpus=16`.")); return flags; } @@ -363,14 +369,11 @@ Result ParseCvdConfigs(const EnvironmentSpecification& env_spec, return flags; } -std::ostream& operator<<(std::ostream& out, const Override& override) { - fmt::print(out, "(config_path=\"{}\", new_value=\"{}\")", - override.config_path, override.new_value); - return out; -} + Result GetEnvironmentSpecification( - const std::string& config_path, const std::vector& overrides) { + const std::string& config_path, + const std::map& overrides) { Json::Value json_configs = CF_EXPECT(GetOverriddenConfig(config_path, overrides)); diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h index 270d378fe90..1a6cd36d6fb 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h @@ -16,7 +16,7 @@ #pragma once -#include +#include #include #include @@ -35,22 +35,16 @@ struct CvdFlags { std::string target_directory; }; -struct Override { - std::string config_path; - std::string new_value; -}; - -std::ostream& operator<<(std::ostream& out, const Override& override); - struct LoadFlags { - std::vector overrides; + std::map overrides; std::string base_dir; }; std::vector BuildCvdLoadFlags(LoadFlags& load_flags); Result GetEnvironmentSpecification( - const std::string& config_path, const std::vector& overrides); + const std::string& config_path, + const std::map& overrides); Result GetGroupCreationDirectories( const std::string& parent_directory, From b4190679b9c1b6334a440c842e2d7d6155c641c3 Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Wed, 27 May 2026 15:09:18 -0700 Subject: [PATCH 09/10] HelpParagraph class with wrapped and raw variants --- .../commands/cvd/cli/commands/BUILD.bazel | 1 + .../cvd/cli/commands/command_handler.cpp | 2 +- .../cvd/cli/commands/command_handler.h | 8 +--- .../cvd/cli/commands/load_configs.cpp | 38 +++++++++------ .../commands/cvd/cli/commands/load_configs.h | 3 +- .../host/commands/cvd/cli/commands/start.cpp | 12 +++-- .../host/commands/cvd/cli/commands/start.h | 2 +- .../host/commands/cvd/cli/help_format.cpp | 47 ++++++++++--------- .../host/commands/cvd/cli/help_format.h | 27 ++++++++--- 9 files changed, 84 insertions(+), 56 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel index 25b766ebd9e..e27d3cf39db 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel @@ -427,6 +427,7 @@ cf_cc_library( "//cuttlefish/common/libs/utils:subprocess_managed_stdio", "//cuttlefish/common/libs/utils:users", "//cuttlefish/host/commands/cvd/cli:command_request", + "//cuttlefish/host/commands/cvd/cli:help_format", "//cuttlefish/host/commands/cvd/cli:types", "//cuttlefish/host/commands/cvd/cli:utils", "//cuttlefish/host/commands/cvd/cli/commands:command_handler", diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.cpp index c6b35f220cd..fab53fcac7a 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.cpp @@ -34,7 +34,7 @@ namespace cuttlefish { bool CvdCommandHandler::RequiresDeviceExists() const { return false; } -std::vector CvdCommandHandler::Description() const { return {}; } +std::vector CvdCommandHandler::Description() const { return {}; } Result> CvdCommandHandler::Flags(const CommandRequest&) { return {}; diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.h b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.h index c1067a8f7f8..e70803354a0 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/command_handler.h @@ -20,6 +20,7 @@ #include "cuttlefish/common/libs/utils/flag_parser.h" #include "cuttlefish/host/commands/cvd/cli/command_request.h" +#include "cuttlefish/host/commands/cvd/cli/help_format.h" #include "cuttlefish/host/commands/cvd/cli/types.h" #include "cuttlefish/result/result.h" @@ -36,12 +37,7 @@ class CvdCommandHandler { virtual std::string SummaryHelp() const = 0; virtual bool RequiresDeviceExists() const; virtual Result DetailedHelp(const CommandRequest&); - // Each string returned by Description() is treated as a paragraph. The - // default implementation of DetailedHelp will visually separate each - // paragraph. That implementation will also split each paragraph in lines not - // longer than 80 characters, so implementations of Description() don't need - // to (and probably shouldn't) use line breaks for formatting. - virtual std::vector Description() const; + virtual std::vector Description() const; virtual Result> Flags(const CommandRequest&); virtual bool RequiresHostConfiguration() const { return true; } diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.cpp index b95762d84f8..f7786dd8bb7 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.cpp @@ -241,20 +241,23 @@ cvd_common::Args LoadConfigsCommand::CmdList() const { return {kLoadSubCmd}; } std::string LoadConfigsCommand::SummaryHelp() const { return kSummaryHelpText; } -std::vector LoadConfigsCommand::Description() const { - return { +std::vector LoadConfigsCommand::Description() const { + std::vector description; + description.emplace_back( "This command is an alias of `cvd create --config_file= " "[--override=:]...`, provided for convenience and backwards " - "compatibility.", + "compatibility."); - "Usage:", + description.emplace_back("Usage:"); - " cvd load [--override=:]", + description.emplace_back( + " cvd load [--override=:]"); + description.emplace_back( "Creates and starts a new instance group from a specification file. An " - "example specification file looks like:", + "example specification file looks like:"); - MarkAsRawText(R"( { + description.emplace_back(HelpParagraph::Raw(R"( { "instances": [ { "name": "ins-1", @@ -273,34 +276,39 @@ std::vector LoadConfigsCommand::Description() const { } } ] - })"), + })")); + description.emplace_back( "A complete reference of the specification file format can be found in " "https://github.com/google/android-cuttlefish/blob/main/base/cvd/" - "cuttlefish/host/commands/cvd/cli/parser/load_config.proto.", + "cuttlefish/host/commands/cvd/cli/parser/load_config.proto."); + description.emplace_back( "While most config file properties are self explanatory, the build " "properties (default_build, kernel.build, bootloader.build, etc) require " - "more explanation. These properties support two types of values:", + "more explanation. These properties support two types of values:"); - MarkAsRawText( - R"( - "@ab/[/[{}]]" - - "")"), + description.emplace_back(HelpParagraph::Raw( + R"( - "@ab/[/[{}]]" + - "")")); + description.emplace_back( "If the build value starts with \"@ab\", cvd will fetch the specified " "Android build target from the Android build servers. By default it will " "download the cuttlefish host package archive or the images zip as " "needed, but for more advanced use cases the file to download from the " "server can be specified with the optional parameter in curly " "braces. For more information on build fetching and caching operations " - "refer to `cvd help fetch`.", + "refer to `cvd help fetch`."); + description.emplace_back( "Alternatively, the build value may point to an absolute path (starts " "with '/') in the filesystem where the Android source code has been " "checked out and a Cuttlefish target has been built. This is " "particularly useful for rapid iteration during development in " "combination with incremental builds and the `cvd stop` and `cvd start` " - "subcommands."}; + "subcommands."); + return description; } Result> LoadConfigsCommand::Flags( diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.h b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.h index 8989b0f385e..5c1c46ecc01 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/load_configs.h @@ -20,6 +20,7 @@ #include "cuttlefish/host/commands/cvd/cli/command_request.h" #include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" +#include "cuttlefish/host/commands/cvd/cli/help_format.h" #include "cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h" #include "cuttlefish/host/commands/cvd/instances/instance_manager.h" #include "cuttlefish/host/commands/cvd/instances/local_instance_group.h" @@ -35,7 +36,7 @@ class LoadConfigsCommand : public CvdCommandHandler { Result Handle(const CommandRequest& request) override; cvd_common::Args CmdList() const override; std::string SummaryHelp() const override; - std::vector Description() const override; + std::vector Description() const override; Result> Flags(const CommandRequest&) override; private: diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.cpp index 942a60e05ea..a1f32dea81f 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.cpp @@ -51,6 +51,7 @@ #include "cuttlefish/host/commands/cvd/cli/command_request.h" #include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" #include "cuttlefish/host/commands/cvd/cli/commands/host_tool_target.h" +#include "cuttlefish/host/commands/cvd/cli/help_format.h" #include "cuttlefish/host/commands/cvd/cli/selector/selector.h" #include "cuttlefish/host/commands/cvd/cli/types.h" #include "cuttlefish/host/commands/cvd/cli/utils.h" @@ -507,19 +508,22 @@ Result CvdStartCommandHandler::LaunchDeviceInterruptible( return {}; } -std::vector CvdStartCommandHandler::Description() const { - return { +std::vector CvdStartCommandHandler::Description() const { + std::vector description; + description.emplace_back( "The `cvd start` command applies to the instance group, not specific " "instances because Cuttlefish instances in the same group must all be " "started (and stopped) in unisom. The group to be started is chosen " - "using the standard selector flags.", + "using the standard selector flags."); + description.emplace_back( "Flags that modify individual instances accept a comma separated list of " "values. If the number of values in one of these flags is less than the " "number of instances, then the last instances in the group will default " "to the first value in the list (as a result a single value provided " "applies to all instances). The empty string is interpreted as a valid " "value, to force a particular instance to use its intended default value " - "the special value \"unset\" must be given."}; + "the special value \"unset\" must be given."); + return description; } Result> CvdStartCommandHandler::Flags( diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.h b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.h index 01eaa5ac790..8091f4306e3 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.h @@ -37,7 +37,7 @@ class CvdStartCommandHandler : public CvdCommandHandler { std::string SummaryHelp() const override { return "Start all Cuttlefish Instances in a group"; } - std::vector Description() const override; + std::vector Description() const override; Result> Flags(const CommandRequest&) override; bool RequiresDeviceExists() const override { return true; } diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp index f15b2df2ae2..0e649d74c2c 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.cpp @@ -29,13 +29,8 @@ namespace cuttlefish { namespace { -constexpr char kRawTextMark[] = "_RAW_TEXT:"; - std::vector WrapAroundLine(std::string_view str, size_t max_line_length) { - if (absl::ConsumePrefix(&str, kRawTextMark)) { - return {std::string(str)}; - } std::vector ret; size_t total_word_sizes = 0; std::vector line; @@ -71,15 +66,30 @@ std::vector GetFlagHelpMessage(const Flag& flag) { } // namespace -std::string FormatHelpText(const std::vector& text, +HelpParagraph HelpParagraph::Raw(std::string text) { + return HelpParagraph(std::move(text), Style::Raw); +} + +HelpParagraph::HelpParagraph(std::string text) + : text_(std::move(text)), style_(Style::Wrapped) {} +HelpParagraph::HelpParagraph(std::string text, Style style) + : text_(std::move(text)), style_(style) {} + +std::string HelpParagraph::Formatted(size_t max_line_width) const { + switch(style_) { + case Style::Wrapped: + return absl::StrJoin(WrapAroundLine(text_, max_line_width), "\n"); + case Style::Raw: + return text_; + } +} + +std::string FormatHelpText(const std::vector& text, size_t max_line_width) { std::stringstream ss; - for (const std::string& paragraph : text) { - for (std::string_view line : WrapAroundLine(paragraph, max_line_width)) { - ss << line << "\n"; - } + for (const HelpParagraph& paragraph : text) { // Empty line after paragraphs - ss << "\n"; + ss << paragraph.Formatted(max_line_width) << "\n\n"; } return ss.str(); } @@ -91,13 +101,10 @@ std::string FormatFlagsHelp(const std::vector& flags, std::vector help_lines = GetFlagHelpMessage(flag); CHECK(!help_lines.empty()) << "Flag produced empty help message: " << flag.Name(); - // The first line contains the --flag=value examples, don't indent or wrap - // those. - ss << help_lines.front() << "\n"; - for (size_t i = 1; i < help_lines.size(); ++i) { - for (std::string_view l : - WrapAroundLine(help_lines[i], max_line_width - 4)) { - ss << " " << l << "\n"; + for (std::string_view line : help_lines) { + for (std::string_view wrapped_line : + WrapAroundLine(line, max_line_width - 4)) { + ss << " " << wrapped_line << "\n"; } } ss << "\n"; @@ -105,8 +112,4 @@ std::string FormatFlagsHelp(const std::vector& flags, return ss.str(); } -std::string MarkAsRawText(std::string_view str) { - return absl::StrCat(kRawTextMark, str); -} - } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h b/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h index 9e42dfd3e43..fffca57455d 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/help_format.h @@ -19,7 +19,6 @@ #include #include -#include #include #include "cuttlefish/common/libs/utils/flag_parser.h" @@ -28,12 +27,32 @@ namespace cuttlefish { constexpr size_t kDefaultMaxLineWidth = 80; +class HelpParagraph { + public: + static HelpParagraph Raw(std::string text); + + explicit HelpParagraph(std::string); + + std::string Formatted(size_t max_line_width) const; + + private: + enum class Style { + Wrapped, + Raw, + }; + + HelpParagraph(std::string, Style style); + + std::string text_; + Style style_; +}; + // Formats text (typically a command description) to be displayed in the // terminal. Each string in the input is considered to be a different paragraph // and should not contain line changes. Each paragraph will be broken into lines // of at most max_line_width columns without splitting individual words. An // empty line will be added after each paragraph. -std::string FormatHelpText(const std::vector& text, +std::string FormatHelpText(const std::vector& text, size_t max_line_width = kDefaultMaxLineWidth); // Formats the help messages of a list of flags to be displayed in the terminal. @@ -41,8 +60,4 @@ std::string FormatHelpText(const std::vector& text, // except when necessary to avoid splitting a word. std::string FormatFlagsHelp(const std::vector& flags, size_t max_line_width = kDefaultMaxLineWidth); - -// Marks a string as raw text so that FormatHelpText doesn't modify it. -std::string MarkAsRawText(std::string_view str); - } // namespace cuttlefish From cbfd678d0c1ce30afe0b0069c17e912eb32aba92 Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Wed, 27 May 2026 16:06:11 -0700 Subject: [PATCH 10/10] Use std::less to avoid converting to string --- .../cvd/cli/parser/load_configs_parser.cpp | 37 +++++++++---------- .../cvd/cli/parser/load_configs_parser.h | 5 ++- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp index bcfa7b5cb60..edb452250b4 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.cpp @@ -23,8 +23,8 @@ #include #include #include +#include #include -#include #include #include #include @@ -70,8 +70,9 @@ bool IsLocalBuild(std::string path) { return absl::StartsWith(path, "/"); } -Flag GflagsCompatFlagOverride(const std::string& name, - std::map& overrides) { +Flag GflagsCompatFlagOverride( + const std::string& name, + std::map>& overrides) { return GflagsCompatFlag(name) .Getter([&overrides]() { std::vector formatted; @@ -208,7 +209,7 @@ std::optional GetSystemHostPath( Result GetOverriddenConfig( const std::string& config_path, - const std::map& override_flags) { + const std::map>& override_flags) { Json::Value result = CF_EXPECT(ParseJsonFile(config_path)); for (const auto& [key, val] : override_flags) { @@ -259,18 +260,17 @@ std::vector BuildCvdLoadFlags(LoadFlags& load_flags) { "Can be left empty in most cases, see the help for `login` and " "`fetch` for details.") .Setter([&load_flags](const FlagMatch& match) -> Result { - CF_EXPECTF(load_flags.overrides.count( - std::string(kCredentialSourceOverride)) == 0, - "Specifying both --override={} and the " - "--credential_source flag is not allowed.", - kCredentialSourceOverride); - load_flags.overrides[std::string(kCredentialSourceOverride)] = - match.value; + CF_EXPECTF( + load_flags.overrides.count(kCredentialSourceOverride) == 0, + "Specifying both --override={} and the " + "--credential_source flag is not allowed.", + kCredentialSourceOverride); + load_flags.overrides.emplace(kCredentialSourceOverride, + match.value); return {}; }) .Getter([&load_flags]() -> std::string { - auto it = load_flags.overrides.find( - std::string(kCredentialSourceOverride)); + auto it = load_flags.overrides.find(kCredentialSourceOverride); if (it != load_flags.overrides.end()) { return it->second; } @@ -281,17 +281,16 @@ std::vector BuildCvdLoadFlags(LoadFlags& load_flags) { .Help("Google Cloud Project ID for Android Build " "Server API access and quotas.") .Setter([&load_flags](const FlagMatch& match) -> Result { - CF_EXPECTF(load_flags.overrides.count( - std::string(kProjectIDOverride)) == 0, + CF_EXPECTF(load_flags.overrides.count(kProjectIDOverride) == 0, "Specifying both --override={} and the --project_id " "flag is not allowed.", kProjectIDOverride); - load_flags.overrides[std::string(kProjectIDOverride)] = match.value; + load_flags.overrides.emplace(kProjectIDOverride, match.value); return {}; }) .Getter([&load_flags]() -> std::string { auto it = - load_flags.overrides.find(std::string(kProjectIDOverride)); + load_flags.overrides.find(kProjectIDOverride); if (it != load_flags.overrides.end()) { return it->second; } @@ -369,11 +368,9 @@ Result ParseCvdConfigs(const EnvironmentSpecification& env_spec, return flags; } - - Result GetEnvironmentSpecification( const std::string& config_path, - const std::map& overrides) { + const std::map>& overrides) { Json::Value json_configs = CF_EXPECT(GetOverriddenConfig(config_path, overrides)); diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h index 1a6cd36d6fb..7a68bea7894 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/parser/load_configs_parser.h @@ -16,6 +16,7 @@ #pragma once +#include #include #include #include @@ -36,7 +37,7 @@ struct CvdFlags { }; struct LoadFlags { - std::map overrides; + std::map> overrides; std::string base_dir; }; @@ -44,7 +45,7 @@ std::vector BuildCvdLoadFlags(LoadFlags& load_flags); Result GetEnvironmentSpecification( const std::string& config_path, - const std::map& overrides); + const std::map>& overrides); Result GetGroupCreationDirectories( const std::string& parent_directory,