Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8191b0a
Add an Fstat method to Fd
larsers Aug 21, 2026
e573482
Parse gs:// and https:// URLs as build strings
larsers Aug 21, 2026
8e90d72
Resolve URL build artifact names without any I/O
larsers Aug 21, 2026
ae0d437
Add GcsBuild and HttpBuild to the resolved build variant
larsers Aug 21, 2026
a1710eb
Read Cloud Storage builds through the JSON API
larsers Aug 21, 2026
dc81cd2
Read builds from plain https:// URLs
larsers Aug 21, 2026
e885b57
Read remote zips whose size the caller already knows
larsers Aug 21, 2026
fa20fe3
Route each build to the API that owns its source
larsers Aug 21, 2026
31c377d
Resolve Cloud Storage credentials on their own ladder
larsers Aug 21, 2026
ba84a2d
Serve `cvd fetch` from every build source
larsers Aug 21, 2026
ea780a3
Resolve build zip names from a URL build's namespace
larsers Aug 21, 2026
a0153e1
Pass URL build sources through `cvd load`
larsers Aug 21, 2026
d6b4679
Keep query strings out of URL build string parse errors
larsers Aug 21, 2026
31cff9d
Pass an environment to `cvd create --config_file`
larsers Aug 21, 2026
60ec816
Add e2e tests for https:// build sources
larsers Aug 21, 2026
e0bc6f8
Verify downloaded URL build artifacts against their digests
larsers Aug 21, 2026
20ecad5
Key the artifact cache on the version the source reports
larsers Aug 21, 2026
2685ae3
Resume an interrupted URL artifact download
larsers Aug 21, 2026
8739df4
Pin ranged Cloud Storage reads to one object generation
larsers Aug 21, 2026
7418bc7
Start a partial download from the offset it will be written at
larsers Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions base/cvd/cuttlefish/common/libs/fs/fd.cc
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,15 @@ Result<void> Fd::Flock(int operation) {
return {};
}

Result<struct stat> Fd::Fstat() {
LocalErrno record_errno(errno_);

struct stat file_info = {};
CF_EXPECT(TEMP_FAILURE_RETRY(fstat(fd_, &file_info)) == 0,
::cuttlefish::StrError(errno));
return file_info;
}

int Fd::GetSockName(struct sockaddr* addr, socklen_t* addrlen) {
LocalErrno record_errno(errno_);

Expand Down
2 changes: 2 additions & 0 deletions base/cvd/cuttlefish/common/libs/fs/fd.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ class Fd : public ReaderWriterSeeker {

Result<void> Flock(int operation);

Result<struct stat> Fstat();

int GetErrno() const { return errno_; }
int GetSockName(struct sockaddr* addr, socklen_t* addrlen);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,14 @@ std::vector<HelpParagraph> LoadConfigsCommand::CommonCommandDescription() {
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 the following values:");

description.emplace_back(HelpParagraph::Raw(
R"( - "@ab/<branch_or_build_id>[/<target>[{<filepath>}]]"
- "gs://<bucket>/<prefix>/"
- "gs://<bucket>/<object>[{<filepath>}]"
- "https://<host>/<directory>/"
- "https://<host>/<object>[{<filepath>}]"
- "<absolute_path>")"));

description.emplace_back(
Expand All @@ -307,6 +311,18 @@ std::vector<HelpParagraph> LoadConfigsCommand::CommonCommandDescription() {
"braces. For more information on build fetching and caching operations "
"refer to `cvd help fetch`.");

description.emplace_back(
"A \"gs://\" or \"https://\" value names a build outside the Android "
"build servers. A URL ending in '/' names the directory holding the "
"build's artifacts, which cvd picks from by name; a URL naming a single "
"object is a build of just that artifact, and <filepath> then selects a "
"member of it if it is a zip. A \"gs://\" build is read with the "
"credentials `cvd fetch` would use for the build servers, while an "
"\"https://\" build is read as given, so any credential has to travel "
"in the URL itself as in a pre-signed URL. A directory URL over plain "
"\"https://\" cannot be listed, so a build named that way needs the "
"host package named separately.");

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 "
Expand Down
2 changes: 2 additions & 0 deletions base/cvd/cuttlefish/host/commands/cvd/cli/parser/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ cf_cc_library(
"//cuttlefish/host/commands/cvd/cli/parser:cf_configs_common",
"//cuttlefish/host/commands/cvd/cli/parser:load_config_cc_proto",
"//cuttlefish/host/commands/cvd/fetch:fetch_cvd_parser",
"//cuttlefish/host/libs/web:android_build_string",
"//cuttlefish/host/libs/web/http_client:scrub_secrets",
"//cuttlefish/result",
"@abseil-cpp//absl/strings",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>

#include "absl/strings/match.h"
#include "absl/strings/strip.h"

#include "cuttlefish/host/commands/cvd/cli/parser/cf_configs_common.h"
#include "cuttlefish/host/commands/cvd/cli/parser/load_config.pb.h"
#include "cuttlefish/host/commands/cvd/fetch/fetch_cvd_parser.h"
#include "cuttlefish/host/libs/web/android_build_string.h"
#include "cuttlefish/host/libs/web/http_client/scrub_secrets.h"
#include "cuttlefish/result/result.h"

namespace cuttlefish {
Expand Down Expand Up @@ -56,15 +60,26 @@ bool ShouldFetch(const Instance& instance) {

Result<std::string> GetFetchBuildString(const std::string& strVal) {
std::string_view view = strVal;
if (!absl::ConsumePrefix(&view, kFetchPrefix)) {
// intentionally return an empty string when there are local, non-prefixed
// paths. Fetch does not process the local paths
return "";
if (absl::ConsumePrefix(&view, kFetchPrefix)) {
CF_EXPECTF(!view.empty(),
"\"{}\" prefixed build string was not followed by a value",
kFetchPrefix);
CF_EXPECT(ParseBuildString(view));
return std::string(view);
}
CF_EXPECTF(!view.empty(),
"\"{}\" prefixed build string was not followed by a value",
kFetchPrefix);
return std::string(view);
if (absl::StrContains(strVal, "://")) {
const BuildString parsed = CF_EXPECT(ParseBuildString(strVal));
// A local path can hold a "://" without naming a URL scheme, and parses
// as a directory build string.
CF_EXPECTF(std::holds_alternative<GcsBuildString>(parsed) ||
std::holds_alternative<HttpBuildString>(parsed),
"'{}' contains '://' but is not a 'gs://' or 'https://' URL.",
ScrubUrl(strVal));
return strVal;
}
// intentionally return an empty string when there are local, non-prefixed
// paths. Fetch does not process the local paths
return "";
}

Result<Instance> RemoveNonPrefixedBuildStrings(const Instance& instance) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ Result<std::vector<std::string>> FetchCvdParserTester(const Json::Value& root) {

} // namespace

using ::testing::AllOf;
using ::testing::HasSubstr;

TEST(FetchConfigParserTests, AndroidEfiLoaderBuildOnlySuccess) {
const char* test_string = R""""(
{
Expand Down Expand Up @@ -105,9 +108,258 @@ TEST(FetchConfigParserTests, NoBuildStringsProducesNoFlagsSuccess) {
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

Result<std::vector<std::string>> flags = FetchCvdParserTester(json_configs);
EXPECT_THAT(flags, IsOkAndValue(std::vector<std::string>{}));
}

TEST(FetchConfigParserTests, MalformedAbBuildStringFail) {
const char* test_string = R""""(
{
"instances": [
{
"disk": {
"default_build": "@ab/branch/target/extra"
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

EXPECT_THAT(FetchCvdParserTester(json_configs), IsError());
}

TEST(FetchConfigParserTests, ObjectUrlBuildsSuccess) {
const char* test_string = R""""(
{
"common": {
"host_package": "gs://bucket/dist/cvd-host_package.tar.gz"
},
"instances": [
{
"disk": {
"default_build": "gs://bucket/dist/phone-img-1.zip"
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

Result<std::vector<std::string>> flags = FetchCvdParserTester(json_configs);
ASSERT_THAT(flags, IsOk());
EXPECT_TRUE(flags->empty());
EXPECT_TRUE(
FindConfig(*flags, "--default_build=gs://bucket/dist/phone-img-1.zip"));
EXPECT_TRUE(FindConfig(
*flags, "--host_package_build=gs://bucket/dist/cvd-host_package.tar.gz"));
}

TEST(FetchConfigParserTests, DirectoryUrlBuildsSuccess) {
const char* test_string = R""""(
{
"common": {
"host_package": "gs://bucket/host/"
},
"instances": [
{
"disk": {
"default_build": "gs://bucket/dist/"
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

Result<std::vector<std::string>> flags = FetchCvdParserTester(json_configs);
ASSERT_THAT(flags, IsOk());
EXPECT_TRUE(FindConfig(*flags, "--default_build=gs://bucket/dist/"));
EXPECT_TRUE(FindConfig(*flags, "--host_package_build=gs://bucket/host/"));
}

TEST(FetchConfigParserTests, HttpsDirectoryUrlBuildSuccess) {
const char* test_string = R""""(
{
"instances": [
{
"disk": {
"default_build": "https://example.com/dist/"
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

Result<std::vector<std::string>> flags = FetchCvdParserTester(json_configs);
ASSERT_THAT(flags, IsOk());
EXPECT_TRUE(FindConfig(*flags, "--default_build=https://example.com/dist/"));
}

TEST(FetchConfigParserTests, OtaToolsUrlBuildSuccess) {
const char* test_string = R""""(
{
"instances": [
{
"disk": {
"otatools": "gs://bucket/dist/"
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

Result<std::vector<std::string>> flags = FetchCvdParserTester(json_configs);
ASSERT_THAT(flags, IsOk());
EXPECT_TRUE(FindConfig(*flags, "--otatools_build=gs://bucket/dist/"));
}

TEST(FetchConfigParserTests, UrlFilepathSelectorSuccess) {
const char* test_string = R""""(
{
"instances": [
{
"boot": {
"build": "gs://bucket/dist/images.zip{boot.img}"
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

Result<std::vector<std::string>> flags = FetchCvdParserTester(json_configs);
ASSERT_THAT(flags, IsOk());
EXPECT_TRUE(
FindConfig(*flags, "--boot_build=gs://bucket/dist/images.zip{boot.img}"));
}

TEST(FetchConfigParserTests, CleartextHttpUrlFail) {
const char* test_string = R""""(
{
"instances": [
{
"disk": {
"default_build": "http://example.com/dist/"
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

EXPECT_THAT(
FetchCvdParserTester(json_configs),
IsErrorAndMessage(AllOf(HasSubstr("http://"), HasSubstr("https://"))));
}

TEST(FetchConfigParserTests, UnsupportedUrlSchemeFail) {
const char* test_string = R""""(
{
"instances": [
{
"disk": {
"default_build": "s3://bucket/dist/"
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

EXPECT_THAT(FetchCvdParserTester(json_configs),
IsErrorAndMessage(AllOf(HasSubstr("s3"), HasSubstr("gs://"))));
}

// The URL build string format reserves ',', so a value holding one is
// refused even where nothing splits it.
TEST(FetchConfigParserTests, UrlWithCommaFail) {
const char* test_string = R""""(
{
"instances": [
{
"disk": {
"default_build": "https://example.com/d/img.zip?k=a,b"
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

EXPECT_THAT(FetchCvdParserTester(json_configs),
IsErrorAndMessage(HasSubstr("comma")));
}

TEST(FetchConfigParserTests, LocalPathProducesNoFlagsSuccess) {
const char* test_string = R""""(
{
"instances": [
{
"disk": {
"default_build": "/home/user/local_images"
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

Result<std::vector<std::string>> flags = FetchCvdParserTester(json_configs);
EXPECT_THAT(flags, IsOkAndValue(std::vector<std::string>{}));
}

TEST(FetchConfigParserTests, UnprefixedRelativeValuesProduceNoFlagsSuccess) {
const char* test_string = R""""(
{
"instances": [
{
"disk": {
"default_build": "out/target/product/vsoc_x86_64",
"otatools": "branch/target"
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

Result<std::vector<std::string>> flags = FetchCvdParserTester(json_configs);
EXPECT_THAT(flags, IsOkAndValue(std::vector<std::string>{}));
}

} // namespace cuttlefish
Loading