diff --git a/base/cvd/cuttlefish/common/libs/utils/files.cpp b/base/cvd/cuttlefish/common/libs/utils/files.cpp index 10bd51e30b4..174d12d34bb 100644 --- a/base/cvd/cuttlefish/common/libs/utils/files.cpp +++ b/base/cvd/cuttlefish/common/libs/utils/files.cpp @@ -115,46 +115,89 @@ Result AreHardLinked(const std::string& source, CF_EXPECT(FileInodeNumber(destination))); } -Result CreateHardLink(const std::string& target, - const std::string& hardlink, - const bool overwrite_existing) { - if (FileExists(hardlink)) { - if (CF_EXPECT(AreHardLinked(target, hardlink))) { - return hardlink; +Result AreFilesIdentical(const std::string& path1, + const std::string& path2) { + struct stat st1; + struct stat st2; + CF_EXPECTF(stat(path1.c_str(), &st1) == 0, "stat failed for {}", path1); + CF_EXPECTF(stat(path2.c_str(), &st2) == 0, "stat failed for {}", path2); + if (st1.st_size != st2.st_size) { + return false; + } + + SharedFD fd1 = SharedFD::Open(path1, O_RDONLY); + SharedFD fd2 = SharedFD::Open(path2, O_RDONLY); + CF_EXPECTF(fd1->IsOpen(), "Failed to open \"{}\"", path1); + CF_EXPECTF(fd2->IsOpen(), "Failed to open \"{}\"", path2); + + char buf1[4096]; + char buf2[4096]; + while (true) { + auto r1 = fd1->Read(buf1, sizeof(buf1)); + auto r2 = fd2->Read(buf2, sizeof(buf2)); + CF_EXPECTF(r1 >= 0, "Read failed for \"{}\"", path1); + CF_EXPECTF(r2 >= 0, "Read failed for \"{}\"", path2); + if (r1 != r2) { + return false; + } + if (r1 == 0) { + break; + } + if (memcmp(buf1, buf2, r1) != 0) { + return false; + } + } + return true; +} + +Result LinkOrCopy(const std::string& target, + const std::string& destination, + const bool overwrite_existing) { + if (FileExists(destination)) { + if (CF_EXPECT(AreHardLinked(target, destination))) { + return destination; } if (!overwrite_existing) { + if (CF_EXPECT(AreFilesIdentical(target, destination))) { + return destination; + } return CF_ERRF( - "Cannot hardlink from \"{}\" to \"{}\", the second file already " - "exists and is not hardlinked to the first", - target, hardlink); + "Cannot link/copy from \"{}\" to \"{}\", the second file already " + "exists and is different from the first", + target, destination); } - LOG(WARNING) << "Overwriting existing file \"" << hardlink << "\" with \"" - << target << "\" from the cache"; - CF_EXPECTF(unlink(hardlink.c_str()) == 0, - "Failed to unlink \"{}\" with error: {}", hardlink, + LOG(WARNING) << "Overwriting existing file \"" << destination + << "\" with \"" << target << "\" from the cache"; + CF_EXPECTF(unlink(destination.c_str()) == 0, + "Failed to unlink \"{}\" with error: {}", destination, strerror(errno)); } - CF_EXPECTF(link(target.c_str(), hardlink.c_str()) == 0, - "link() failed trying to create hardlink from \"{}\" to \"{}\" " - "with error: {}", - target, hardlink, strerror(errno)); - VLOG(1) << "Created hard link from \"" << target << "\" to \"" << hardlink + if (link(target.c_str(), destination.c_str()) == 0) { + VLOG(1) << "Created hard link from \"" << target << "\" to \"" + << destination << "\""; + return destination; + } + CF_EXPECTF(Copy(target, destination), "Failed to copy \"{}\" to \"{}\"", + target, destination); + VLOG(1) << "Copied file from \"" << target << "\" to \"" << destination << "\""; - return hardlink; + + return destination; } bool FileHasContent(const std::string& path) { return FileSize(path) > 0; } -Result HardLinkDirecoryContentsRecursively( +Result LinkOrCopyDirectoryContentsRecursively( const std::string& source, const std::string& destination) { CF_EXPECTF(IsDirectory(source), "Source '{}' is not a directory", source); CF_EXPECT(EnsureDirectoryExists(destination, 0755)); - auto linker = [&source, &destination]( - const std::string& filepath) mutable -> Result { + auto linker_or_copier = + [&source, + &destination](const std::string& filepath) mutable -> Result { const std::string src_path = filepath; const std::string dst_path = destination + "/" + filepath.substr(source.size() + 1); @@ -163,10 +206,10 @@ Result HardLinkDirecoryContentsRecursively( return {}; } const bool overwrite_existing = true; - CF_EXPECT(CreateHardLink(src_path, dst_path, overwrite_existing)); + CF_EXPECT(LinkOrCopy(src_path, dst_path, overwrite_existing)); return {}; }; - CF_EXPECT(WalkDirectory(source, linker)); + CF_EXPECT(WalkDirectory(source, linker_or_copier)); return {}; } diff --git a/base/cvd/cuttlefish/common/libs/utils/files.h b/base/cvd/cuttlefish/common/libs/utils/files.h index 776358f927d..65c0a83ae18 100644 --- a/base/cvd/cuttlefish/common/libs/utils/files.h +++ b/base/cvd/cuttlefish/common/libs/utils/files.h @@ -39,10 +39,11 @@ inline Result CanRename(const std::string& source, Result FileInodeNumber(const std::string& path); Result AreHardLinked(const std::string& source, const std::string& destination); -Result CreateHardLink(const std::string& target, - const std::string& hardlink, - bool overwrite_existing = false); -Result HardLinkDirecoryContentsRecursively( + +Result LinkOrCopy(const std::string& target, + const std::string& destination, + bool overwrite_existing = false); +Result LinkOrCopyDirectoryContentsRecursively( const std::string& source, const std::string& destination); // Merges the contents of the source directory into the destination directory. // The source directory is empty after this operation. diff --git a/base/cvd/cuttlefish/common/libs/utils/files_test.cpp b/base/cvd/cuttlefish/common/libs/utils/files_test.cpp index d3dc3f302d7..45131b736ec 100644 --- a/base/cvd/cuttlefish/common/libs/utils/files_test.cpp +++ b/base/cvd/cuttlefish/common/libs/utils/files_test.cpp @@ -63,15 +63,16 @@ class FilesTests : public ::testing::Test { std::string dst_dir_; }; -TEST_F(FilesTests, HardLinkRecursivelyFailsIfSourceIsNotADirectory) { - Result result = HardLinkDirecoryContentsRecursively( +TEST_F(FilesTests, LinkOrCopyRecursivelyFailsIfSourceIsNotADirectory) { + Result result = LinkOrCopyDirectoryContentsRecursively( src_dir_ + "/file1.txt", dst_dir_ + "/file1.txt"); EXPECT_THAT(result, IsError()); } -TEST_F(FilesTests, HardLinkRecursively) { - Result result = HardLinkDirecoryContentsRecursively(src_dir_, dst_dir_); +TEST_F(FilesTests, LinkOrCopyRecursively) { + Result result = + LinkOrCopyDirectoryContentsRecursively(src_dir_, dst_dir_); EXPECT_THAT(result, IsOk()); Result resultHardLinked = diff --git a/base/cvd/cuttlefish/host/commands/cvd/fetch/downloaders.cc b/base/cvd/cuttlefish/host/commands/cvd/fetch/downloaders.cc index 2c421186284..bd1b78e2bfe 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/fetch/downloaders.cc +++ b/base/cvd/cuttlefish/host/commands/cvd/fetch/downloaders.cc @@ -93,7 +93,7 @@ Result Downloaders::Create(const BuildApiFlags& flags, impl->android_creds_.get(), flags.wait_retry_period, impl->cas_downloader_.get()); - if (flags.enable_caching && CanCache(target_directory, cache_base_path)) { + if (flags.enable_caching) { impl->caching_build_api_ = std::make_unique( *impl->android_build_api_, cache_base_path); } diff --git a/base/cvd/cuttlefish/host/commands/cvd/fetch/extract_image_contents.cc b/base/cvd/cuttlefish/host/commands/cvd/fetch/extract_image_contents.cc index c63be89566f..425d2fc4d29 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/fetch/extract_image_contents.cc +++ b/base/cvd/cuttlefish/host/commands/cvd/fetch/extract_image_contents.cc @@ -47,8 +47,9 @@ Result> ExtractImageContents( CF_EXPECT(WalkDirectory(image_filepath, file_collector)); if (keep_archive) { - // Must use hard linking due to the way fetch_cvd uses the cache. - CF_EXPECT(HardLinkDirecoryContentsRecursively(image_filepath, target_dir)); + // Prefer hard linking, but fallback to copy if necessary. + CF_EXPECT( + LinkOrCopyDirectoryContentsRecursively(image_filepath, target_dir)); } else { CF_EXPECT(MoveDirectoryContents(image_filepath, target_dir)); // Ignore even if removing directory fails - harmless. diff --git a/base/cvd/cuttlefish/host/libs/web/caching_build_api.cpp b/base/cvd/cuttlefish/host/libs/web/caching_build_api.cpp index 215cc062f34..ce6feea51bc 100644 --- a/base/cvd/cuttlefish/host/libs/web/caching_build_api.cpp +++ b/base/cvd/cuttlefish/host/libs/web/caching_build_api.cpp @@ -82,22 +82,6 @@ bool IsInCache(const std::string& filepath) { } // namespace -bool CanCache(const std::string& target_directory, - const std::string& cache_base_path) { - const Result result = CanHardLink(target_directory, cache_base_path); - if (result.ok() && *result) { - return true; - } - if (!result.ok()) { - LOG(ERROR) << "Error during hard link check: " << result.error(); - } - LOG(WARNING) - << "Caching disabled, unable to hard link between fetch directory \"" - << target_directory << "\" and cache directory \"" << cache_base_path - << "\""; - return false; -} - CachingBuildApi::CachingBuildApi(BuildApi& build_api, std::string cache_base_path) : build_api_(build_api), cache_base_path_(std::move(cache_base_path)) {}; @@ -114,8 +98,8 @@ Result CachingBuildApi::DownloadFile( if (!IsInCache(paths.cache_artifact)) { CF_EXPECT(build_api_.DownloadFile(build, paths.build_cache, artifact_name)); } - return CF_EXPECT(CreateHardLink(paths.cache_artifact, paths.target_artifact, - kOverwriteExistingFile)); + return CF_EXPECT(LinkOrCopy(paths.cache_artifact, paths.target_artifact, + kOverwriteExistingFile)); } diff --git a/base/cvd/cuttlefish/host/libs/web/caching_build_api.h b/base/cvd/cuttlefish/host/libs/web/caching_build_api.h index ff39eae35c7..fe712089a11 100644 --- a/base/cvd/cuttlefish/host/libs/web/caching_build_api.h +++ b/base/cvd/cuttlefish/host/libs/web/caching_build_api.h @@ -24,9 +24,6 @@ namespace cuttlefish { -bool CanCache(const std::string& target_directory, - const std::string& cache_base_path); - class CachingBuildApi : public BuildApi { public: CachingBuildApi(BuildApi& build_api, std::string cache_base_path); diff --git a/container/src/podcvd/internal/host.go b/container/src/podcvd/internal/host.go index c98624450ef..8895ab06370 100644 --- a/container/src/podcvd/internal/host.go +++ b/container/src/podcvd/internal/host.go @@ -85,6 +85,10 @@ func ExecFetchCmdOnDisposableHost(ccm CuttlefishContainerManager, cvdArgs *CvdAr if err := os.MkdirAll(cvdDataHome, 0755); err != nil { return fmt.Errorf("failed to eusure directory at %q: %w", cvdDataHome, err) } + cacheDir := hostCacheDir() + if err := os.MkdirAll(cacheDir, 0755); err != nil { + return fmt.Errorf("failed to ensure cache directory at %q: %w", cacheDir, err) + } targetDir := cvdArgs.GetStringFlagValueOnSubCommandArgs("target_directory") if targetDir == "" { return fmt.Errorf("target_directory is missing") @@ -96,6 +100,7 @@ func ExecFetchCmdOnDisposableHost(ccm CuttlefishContainerManager, cvdArgs *CvdAr "--label", fmt.Sprintf("%s=%s", labelCreatedBy, valueCreatedBy), "-v", fmt.Sprintf("%s:/root/.local/share/cvd:ro", cvdDataHome), "-v", fmt.Sprintf("%s:%s:rw", targetDir, targetDir), + "-v", fmt.Sprintf("%s:/var/tmp/cvd/0/cache:rw", cacheDir), } containerID, err := ccm.CreateAndStartContainer(context.Background(), extraFlags, "") if err != nil { @@ -132,6 +137,11 @@ func cvdDataHome() (string, error) { return "", fmt.Errorf("failed to find cvd data home dir") } +func hostCacheDir() string { + uid := os.Getuid() + return filepath.Join("/var/tmp/cvd", strconv.Itoa(uid), "cache") +} + func resolveHostPath(path string) string { if strings.TrimSpace(path) == "" { return "" @@ -196,12 +206,13 @@ func mountablePathsFromConfigFile(cvdArgs *CvdArgs) []string { return extractPaths(data) } -func collectMountSpecs(pathsToMount []string, hostOut, productOut, cvdDataHome, podcvdHomeDir string) []string { +func collectMountSpecs(pathsToMount []string, hostOut, productOut, cvdDataHome, podcvdHomeDir, cacheDir string) []string { bindMap := make(map[string]string) bindMap["/host_out"] = fmt.Sprintf("%s:/host_out:O", hostOut) bindMap["/product_out"] = fmt.Sprintf("%s:/product_out:O", productOut) bindMap["/root/.local/share/cvd"] = fmt.Sprintf("%s:/root/.local/share/cvd:ro", cvdDataHome) bindMap["/podcvd_home"] = fmt.Sprintf("%s:/podcvd_home:rw", podcvdHomeDir) + bindMap["/var/tmp/cvd/0/cache"] = fmt.Sprintf("%s:/var/tmp/cvd/0/cache:rw", cacheDir) for _, p := range pathsToMount { if spec, ok := bindMap[p]; ok { host := strings.SplitN(spec, ":", 2)[0] @@ -297,6 +308,10 @@ func createAndStartContainer(ccm CuttlefishContainerManager, cvdArgs *CvdArgs) ( if err := os.MkdirAll(cvdDataHome, 0755); err != nil { return "", fmt.Errorf("failed to eusure directory at %q: %w", cvdDataHome, err) } + cacheDir := hostCacheDir() + if err := os.MkdirAll(cacheDir, 0755); err != nil { + return "", fmt.Errorf("failed to ensure cache directory at %q: %w", cacheDir, err) + } currentDir, err := os.Getwd() if err != nil { return "", fmt.Errorf("failed to get current directory: %w", err) @@ -346,7 +361,7 @@ func createAndStartContainer(ccm CuttlefishContainerManager, cvdArgs *CvdArgs) ( pathsToMount = append(pathsToMount, realPath) } } - mountSpecs := collectMountSpecs(pathsToMount, hostOut, productOut, cvdDataHome, podcvdHomeDir) + mountSpecs := collectMountSpecs(pathsToMount, hostOut, productOut, cvdDataHome, podcvdHomeDir, cacheDir) extraFlags := []string{ "-e", "ANDROID_HOST_OUT=/host_out", @@ -446,9 +461,14 @@ func createAndStartToolingContainer(ccm CuttlefishContainerManager) error { if err := os.MkdirAll(cvdDataHome, 0755); err != nil { return fmt.Errorf("failed to eusure directory at %q: %w", cvdDataHome, err) } + cacheDir := hostCacheDir() + if err := os.MkdirAll(cacheDir, 0755); err != nil { + return fmt.Errorf("failed to ensure cache directory at %q: %w", cacheDir, err) + } extraFlags := []string{ "--label", fmt.Sprintf("%s=%s", labelCreatedBy, valueCreatedBy), "-v", fmt.Sprintf("%s:/root/.local/share/cvd:rw", cvdDataHome), + "-v", fmt.Sprintf("%s:/var/tmp/cvd/0/cache:rw", cacheDir), } if _, err := ccm.CreateAndStartContainer(context.Background(), extraFlags, ToolingContainerName); err != nil { return err diff --git a/container/src/podcvd/internal/main.go b/container/src/podcvd/internal/main.go index ab9b7badc9a..e7e913c4cff 100644 --- a/container/src/podcvd/internal/main.go +++ b/container/src/podcvd/internal/main.go @@ -65,7 +65,7 @@ func Main(args []string) error { if err := fleetAllCuttlefishHosts(ccm); err != nil { return err } - case "help", "lint", "login", "version": + case "cache", "help", "lint", "login", "version": if err := handleToolingSubcommands(ccm, cvdArgs); err != nil { return err } @@ -75,9 +75,6 @@ func Main(args []string) error { } case "setup": return setupPodcvd() - case "cache": - // TODO(seungjaeyoo): Support other subcommands of cvd as well. - return fmt.Errorf("subcommand %q is not implemented yet", subcommand) default: return fmt.Errorf("unknown subcommand %q", subcommand) } @@ -354,6 +351,10 @@ func handleToolingSubcommands(ccm CuttlefishContainerManager, cvdArgs *CvdArgs) } subcommand := cvdArgs.SubCommandArgs[0] switch subcommand { + case "cache": + if err := handleCacheExecution(ccm, cvdArgs); err != nil { + return err + } case "lint": if err := handleLintExecution(ccm, cvdArgs); err != nil { return err @@ -368,6 +369,34 @@ func handleToolingSubcommands(ccm CuttlefishContainerManager, cvdArgs *CvdArgs) return nil } +func handleCacheExecution(ccm CuttlefishContainerManager, cvdArgs *CvdArgs) error { + cacheDir := hostCacheDir() + + if len(cvdArgs.SubCommandArgs) > 1 && cvdArgs.SubCommandArgs[1] == "empty" { + entries, err := os.ReadDir(cacheDir) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to empty cache directory %q: %w", cacheDir, err) + } + for _, entry := range entries { + if err := os.RemoveAll(filepath.Join(cacheDir, entry.Name())); err != nil { + return fmt.Errorf("failed to empty cache directory %q: %w", cacheDir, err) + } + } + fmt.Printf("Cache at %q has been emptied\n", cacheDir) + return nil + } + + args := append([]string{"cvd"}, cvdArgs.SerializeCommonArgs()...) + args = append(args, cvdArgs.SubCommandArgs...) + var stdoutBuf bytes.Buffer + if err := ccm.ExecOnContainer(context.Background(), ToolingContainerName, args, os.Stdin, &stdoutBuf, os.Stderr); err != nil { + return err + } + translatedOutput := strings.ReplaceAll(stdoutBuf.String(), "/var/tmp/cvd/0/cache", cacheDir) + _, err := os.Stdout.WriteString(translatedOutput) + return err +} + func handleLintExecution(ccm CuttlefishContainerManager, cvdArgs *CvdArgs) error { if len(cvdArgs.SubCommandArgs) < 2 { return fmt.Errorf("missing JSON config file path")