mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 21:30:12 +01:00
When building in Bazel actions, notably building runtimes, using absolute paths makes the results non-hermetic and generally less cache-friendly. This restructures the code to only form an absolute path as part of the `bazel run` change of working directory. It also tries to make the API for doing this a bit more clear by taking the `exe_path` and transforming it internally. To support this, this PR also generalizes the `RemovingDir` to support relative paths. While these can be tricky -- the working directory needs to not change while they exist -- that isn't a reason to fully exclude them and they're useful for implementing relative-path runtimes, etc.
522 lines
19 KiB
C++
522 lines
19 KiB
C++
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
// Exceptions. See /LICENSE for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
#include "toolchain/driver/runtimes_cache.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <memory>
|
|
#include <numeric>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <system_error>
|
|
#include <utility>
|
|
#include <variant>
|
|
|
|
#include "common/filesystem.h"
|
|
#include "common/version.h"
|
|
#include "common/vlog.h"
|
|
#include "llvm/ADT/ArrayRef.h"
|
|
#include "llvm/ADT/ScopeExit.h"
|
|
#include "llvm/ADT/StringExtras.h"
|
|
#include "llvm/ADT/StringRef.h"
|
|
#include "llvm/Support/FormatAdapters.h"
|
|
#include "llvm/Support/Program.h"
|
|
#include "llvm/Support/SHA256.h"
|
|
|
|
namespace Carbon {
|
|
|
|
auto Runtimes::OpenExisting(std::filesystem::path path,
|
|
llvm::raw_ostream* vlog_stream)
|
|
-> ErrorOr<Runtimes> {
|
|
CARBON_ASSIGN_OR_RETURN(Filesystem::Dir dir, Filesystem::Cwd().OpenDir(path));
|
|
return Runtimes(std::move(path), std::move(dir), {}, {}, vlog_stream);
|
|
}
|
|
|
|
auto Runtimes::Make(std::filesystem::path path, llvm::raw_ostream* vlog_stream)
|
|
-> ErrorOr<Runtimes> {
|
|
CARBON_ASSIGN_OR_RETURN(Filesystem::Dir dir,
|
|
Filesystem::Cwd().CreateDirectories(path));
|
|
return Runtimes(std::move(path), std::move(dir), {}, {}, vlog_stream);
|
|
}
|
|
|
|
auto Runtimes::Destroy() -> void {
|
|
// Release the lock on the runtimes and close the lock file.
|
|
flock_ = {};
|
|
auto close_result = std::move(lock_file_).Close();
|
|
if (!close_result.ok()) {
|
|
// Log and continue on close errors.
|
|
CARBON_VLOG("Error closing lock file for runtimes '{0}': {1}", base_path_,
|
|
close_result.error());
|
|
}
|
|
}
|
|
|
|
auto Runtimes::Get(Component component) -> ErrorOr<std::filesystem::path> {
|
|
std::filesystem::path path = base_path_ / ComponentPath(component);
|
|
auto open_result =
|
|
base_dir_.OpenDir(ComponentPath(component), Filesystem::OpenExisting);
|
|
if (open_result.ok()) {
|
|
return path;
|
|
}
|
|
return open_result.error().ToError();
|
|
}
|
|
|
|
auto Runtimes::Build(Component component)
|
|
-> ErrorOr<std::variant<std::filesystem::path, Builder>> {
|
|
return BuildImpl(component, BuildLockDeadline, BuildLockPollInterval);
|
|
}
|
|
|
|
auto Runtimes::Remove(Component component) -> ErrorOr<Success> {
|
|
CARBON_RETURN_IF_ERROR(base_dir_.Rmtree(ComponentPath(component)));
|
|
return Success();
|
|
}
|
|
|
|
auto Runtimes::BuildImpl(Component component, Filesystem::Duration deadline,
|
|
Filesystem::Duration poll_interval)
|
|
-> ErrorOr<std::variant<std::filesystem::path, Builder>> {
|
|
// Try to get an existing resource directory first.
|
|
auto existing_result = Get(component);
|
|
if (existing_result.ok()) {
|
|
return {*std::move(existing_result)};
|
|
}
|
|
|
|
// Otherwise, we will need to build the runtimes and commit them into this
|
|
// directory once ready. Try and acquire an advisory lock to avoid redundant
|
|
// computation.
|
|
std::string_view component_path = ComponentPath(component);
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
Filesystem::ReadWriteFile lock_file,
|
|
base_dir_.OpenReadWrite(
|
|
llvm::formatv(LockFileFormat, component_path).str(),
|
|
Filesystem::OpenAlways, /*creation_mode=*/0700));
|
|
CARBON_VLOG("PID {0} locking cache path: {1}\n", getpid(),
|
|
base_path_ / component_path);
|
|
Filesystem::FileLock flock;
|
|
auto flock_result = lock_file.TryLock(Filesystem::FileLock::Exclusive,
|
|
deadline, poll_interval);
|
|
if (flock_result.ok()) {
|
|
flock = *std::move(flock_result);
|
|
CARBON_VLOG("Successfully locked cache path\n");
|
|
// As a debugging aid, write our PID into the lock file when we
|
|
// successfully acquire it. Ignore errors here though.
|
|
(void)lock_file.WriteFileFromString(std::to_string(getpid()));
|
|
} else if (!flock_result.error().would_block()) {
|
|
// Some unexpected filesystem error, report that rather than trying to
|
|
// continue.
|
|
return std::move(flock_result).error();
|
|
} else {
|
|
CARBON_VLOG("Unable to lock cache path, held by: {1}\n",
|
|
*lock_file.ReadFileToString());
|
|
(void)std::move(lock_file).Close();
|
|
}
|
|
|
|
// See if another process has built the runtimes while we waited on the lock.
|
|
// We do this even if we didn't successfully acquire the lock because we
|
|
// ensure that a successful build atomically creates a viable directory.
|
|
existing_result = Get(component);
|
|
if (existing_result.ok()) {
|
|
// Clear and close the lock file.
|
|
(void)lock_file.WriteFileFromString("");
|
|
flock = {};
|
|
(void)std::move(lock_file).Close();
|
|
return {*std::move(existing_result)};
|
|
}
|
|
|
|
// Whether we hold the lock file or not, we're going to now build these
|
|
// runtimes. Create a temporary directory where we can do that safely
|
|
// regardless of what else is happening.
|
|
std::filesystem::path tmp_path =
|
|
base_path_ / llvm::formatv(".{0}.tmp", component_path).str();
|
|
CARBON_ASSIGN_OR_RETURN(Filesystem::RemovingDir tmp_dir,
|
|
Filesystem::MakeTmpDirWithPrefix(tmp_path));
|
|
return {Builder(*this, std::move(lock_file), std::move(flock),
|
|
std::move(tmp_dir), component_path)};
|
|
}
|
|
|
|
auto Runtimes::Cache::FindXdgCachePath()
|
|
-> std::optional<std::filesystem::path> {
|
|
if (const char* xdg_cache_home = getenv("XDG_CACHE_HOME");
|
|
xdg_cache_home != nullptr) {
|
|
std::filesystem::path path = xdg_cache_home;
|
|
if (path.is_absolute()) {
|
|
CARBON_VLOG("Using '$XDG_CACHE_HOME' cache: {0}", path);
|
|
return path;
|
|
}
|
|
}
|
|
|
|
// Unable to use the standard environment variable. Try the designated
|
|
// fallback of `$HOME/.cache`.
|
|
const char* home = getenv("HOME");
|
|
if (home == nullptr) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::filesystem::path path = home;
|
|
if (!path.is_absolute()) {
|
|
return std::nullopt;
|
|
}
|
|
path /= ".cache";
|
|
CARBON_VLOG("Using '$HOME/.cache' cache: {0}", path);
|
|
return path;
|
|
}
|
|
|
|
auto Runtimes::Cache::InitTmpSystemCache() -> ErrorOr<Success> {
|
|
CARBON_ASSIGN_OR_RETURN(dir_owner_, Filesystem::MakeTmpDir());
|
|
path_ = std::get<Filesystem::RemovingDir>(dir_owner_).path();
|
|
dir_ = std::get<Filesystem::RemovingDir>(dir_owner_);
|
|
CARBON_VLOG("Using temporary cache: {0}", path_);
|
|
return Success();
|
|
}
|
|
|
|
auto Runtimes::Cache::InitSystemCache(const InstallPaths& install)
|
|
-> ErrorOr<Success> {
|
|
constexpr llvm::StringLiteral CachePath = "carbon_runtimes";
|
|
|
|
// If we have a digest to use as the cache key, save it and we can try to
|
|
// use persistent caches.
|
|
auto read_digest_result =
|
|
Filesystem::Cwd().ReadFileToString(install.digest_path());
|
|
if (!read_digest_result.ok()) {
|
|
return InitTmpSystemCache();
|
|
}
|
|
cache_key_ = *std::move(read_digest_result);
|
|
|
|
auto xdg_path_result = FindXdgCachePath();
|
|
if (!xdg_path_result) {
|
|
return InitTmpSystemCache();
|
|
}
|
|
|
|
// We have a candidate XDG-based cache path. Try to open that, and a
|
|
// directory below it for Carbon's runtimes. Note that we don't error on a
|
|
// missing directory, we fall through to using a temporary directory.
|
|
auto open_result = Filesystem::Cwd().OpenDir(*xdg_path_result);
|
|
if (!open_result.ok()) {
|
|
if (!open_result.error().no_entity()) {
|
|
// Some other unexpected error in the filesystem, propagate that.
|
|
return std::move(open_result).error();
|
|
}
|
|
// Otherwise we fall back to a temporary system cache.
|
|
return InitTmpSystemCache();
|
|
}
|
|
|
|
path_ = *std::move(xdg_path_result);
|
|
// Now open a subdirectory of the cache for Carbon's usage. This will
|
|
// create a subdirectory if one doesn't yet exist.
|
|
path_ /= std::string_view(CachePath);
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
dir_owner_, open_result->OpenDir(CachePath.str(), Filesystem::OpenAlways,
|
|
/*creation_mode=*/0700));
|
|
dir_ = std::get<Filesystem::Dir>(dir_owner_);
|
|
|
|
// Ensure the directory has narrow permissions so runtimes can't be
|
|
// overwritten.
|
|
CARBON_ASSIGN_OR_RETURN(auto dir_stat, dir_.Stat());
|
|
if (dir_stat.permissions() != 0700 || dir_stat.unix_uid() != geteuid()) {
|
|
return Error(llvm::formatv(
|
|
"Found runtimes cache path '{0}' with excessive permissions ({1}) "
|
|
"or an invalid owning UID ({2})",
|
|
path_, dir_stat.permissions(), dir_stat.unix_uid()));
|
|
}
|
|
|
|
return Success();
|
|
}
|
|
|
|
auto Runtimes::Cache::InitCachePath(const InstallPaths& install,
|
|
std::filesystem::path cache_path)
|
|
-> ErrorOr<Success> {
|
|
auto read_digest_result =
|
|
Filesystem::Cwd().ReadFileToString(install.digest_path());
|
|
if (read_digest_result.ok()) {
|
|
// If we have a digest to use as the cache key, save it and we can try to
|
|
// use persistent caches.
|
|
cache_key_ = *std::move(read_digest_result);
|
|
} else {
|
|
// Without a digest, use the path itself as the key.
|
|
cache_key_ = cache_path.string();
|
|
}
|
|
|
|
CARBON_ASSIGN_OR_RETURN(dir_owner_, Filesystem::Cwd().OpenDir(cache_path));
|
|
dir_ = std::get<Filesystem::Dir>(dir_owner_);
|
|
path_ = std::move(cache_path);
|
|
CARBON_VLOG("Using custom cache: {0}", path_);
|
|
return Success();
|
|
}
|
|
|
|
auto Runtimes::Cache::Lookup(const Features& features) -> ErrorOr<Runtimes> {
|
|
// Compute the hash of the features. We'll use this to build the subdirectory
|
|
// within the cache.
|
|
|
|
llvm::SHA256 entry_hasher;
|
|
// First incorporate our cache key that comes from the installation's digest.
|
|
// This ensures we don't share a cache entry with any other Carbon
|
|
// installations using different inputs.
|
|
entry_hasher.update(cache_key_);
|
|
// Then incorporate the specific features that are enabled in this entry.
|
|
entry_hasher.update(features.target);
|
|
|
|
std::array<uint8_t, 32> entry_digest = entry_hasher.final();
|
|
std::filesystem::path entry_path =
|
|
llvm::formatv("runtimes-{0}-{1}", Version::String,
|
|
llvm::toHex(entry_digest, /*LowerCase=*/true))
|
|
.str();
|
|
|
|
Filesystem::Dir entry_dir;
|
|
auto open_result = dir_.OpenDir(entry_path, Filesystem::OpenExisting);
|
|
if (open_result.ok()) {
|
|
entry_dir = *std::move(open_result);
|
|
} else {
|
|
if (!open_result.error().no_entity()) {
|
|
return std::move(open_result).error();
|
|
}
|
|
|
|
// We're going to potentially create a new set of runtimes, prune the
|
|
// existing runtimes first to provide a bound on the total size of runtimes.
|
|
PruneStaleRuntimes(entry_path);
|
|
|
|
// Now we can create or open, we don't care if a racing process created the
|
|
// same runtime directory.
|
|
CARBON_ASSIGN_OR_RETURN(entry_dir,
|
|
dir_.OpenDir(entry_path, Filesystem::OpenAlways));
|
|
}
|
|
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
auto lock_file, entry_dir.OpenWriteOnly(".lock", Filesystem::OpenAlways));
|
|
CARBON_RETURN_IF_ERROR(lock_file.UpdateTimes());
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
Filesystem::FileLock flock,
|
|
lock_file.TryLock(Filesystem::FileLock::Shared, RuntimesLockDeadline,
|
|
RuntimesLockPollInterval));
|
|
|
|
return Runtimes(path_ / entry_path, std::move(entry_dir),
|
|
std::move(lock_file), std::move(flock), vlog_stream_);
|
|
}
|
|
|
|
auto Runtimes::Cache::ComputeEntryAges(
|
|
llvm::SmallVector<std::filesystem::path> entry_paths)
|
|
-> llvm::SmallVector<Entry> {
|
|
llvm::SmallVector<Entry> entries;
|
|
|
|
Filesystem::TimePoint now = Filesystem::Clock::now();
|
|
for (auto& path : entry_paths) {
|
|
// We use the `mtime` from the lock file in the directory rather than the
|
|
// directory itself to avoid any oddities with `mtime` on directories.
|
|
//
|
|
// Note that we also ignore errors here as if we can't read the stamp file
|
|
// we will pick an arbitrary old time stamp, and we want pruning to be
|
|
// maximally resilient to partially deleted or corrupted caches in order to
|
|
// prune them back into a healthy state.
|
|
auto stat_result = dir_.Lstat(path / ".lock");
|
|
auto mtime = stat_result.ok()
|
|
? stat_result->mtime()
|
|
: Filesystem::TimePoint(Filesystem::Duration(0));
|
|
entries.push_back({.path = std::move(path), .age = now - mtime});
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
auto Runtimes::Cache::PruneStaleRuntimes(
|
|
const std::filesystem::path& new_entry_path) -> void {
|
|
llvm::SmallVector<std::filesystem::path> dir_entries;
|
|
llvm::SmallVector<std::filesystem::path> non_dir_entries;
|
|
auto read_result = dir_.AppendEntriesIf(
|
|
dir_entries, non_dir_entries,
|
|
[](llvm::StringRef name) { return name.starts_with("runtimes-"); });
|
|
if (!read_result.ok()) {
|
|
CARBON_VLOG("Unable to read cache directory to prune stale entries: {0}",
|
|
read_result.error());
|
|
return;
|
|
}
|
|
|
|
// Directly attempt to remove non-directory and bad directory entries.
|
|
for (const auto& name : non_dir_entries) {
|
|
CARBON_VLOG("Unlinking non-directory entry '{0}'", name);
|
|
auto result = dir_.Unlink(name);
|
|
if (!result.ok()) {
|
|
CARBON_VLOG("Error unlinking non-directory entry '{0}': {1}", name,
|
|
result.error());
|
|
}
|
|
}
|
|
|
|
// If we only have a small number of entries, no need to prune.
|
|
if (dir_entries.size() < MinNumEntries) {
|
|
return;
|
|
}
|
|
|
|
llvm::SmallVector<Entry> entries = ComputeEntryAges(std::move(dir_entries));
|
|
|
|
auto rm_entry = [&](const std::filesystem::path& entry_name) {
|
|
// Note that we don't propagate errors here because we want to prune as much
|
|
// as possible. We do log them.
|
|
CARBON_VLOG("Removing cache entry '{0}'", entry_name);
|
|
auto rm_result = dir_.Rmtree(entry_name);
|
|
if (!rm_result.ok() && !rm_result.error().no_entity()) {
|
|
CARBON_VLOG("Unable to remove old runtimes '{0}': {1}", entry_name,
|
|
rm_result.error());
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// Remove entries older than our max first. We don't need to check for locking
|
|
// or other issues here given the age.
|
|
llvm::erase_if(entries, [&](const Entry& entry) {
|
|
return entry.age > MaxEntryAge && rm_entry(entry.path);
|
|
});
|
|
|
|
// Sort the entries so that the oldest is first.
|
|
llvm::sort(entries, [](const Entry& lhs, const Entry& rhs) {
|
|
return lhs.age > rhs.age;
|
|
});
|
|
|
|
// Now try to get the number of entries below our max target by removing the
|
|
// least-recently used entries that are either more than our max locked age or
|
|
// unlocked.
|
|
auto rm_unlocked_entry = [&](const std::filesystem::path& name,
|
|
Filesystem::Duration age) {
|
|
// Past a certain age, bypass the locking for efficiency and to avoid
|
|
// retaining entries with stale locks.
|
|
if (age > MaxLockedEntryAge) {
|
|
return rm_entry(name);
|
|
}
|
|
|
|
CARBON_VLOG("Attempting to lock cache entry '{0}'", name);
|
|
auto lock_file_open_result =
|
|
dir_.OpenReadOnly(name / ".lock", Filesystem::OpenAlways);
|
|
if (!lock_file_open_result.ok()) {
|
|
if (lock_file_open_result.error().no_entity() ||
|
|
lock_file_open_result.error().not_dir()) {
|
|
// The only way these failures should be possible is if something
|
|
// removed the cache directory between our read above and here. Assume
|
|
// the entry is gone and continue.
|
|
return true;
|
|
}
|
|
|
|
// For other errors, assume locked.
|
|
CARBON_VLOG("Error opening lock file for cache entry '{0}': {1}", name,
|
|
lock_file_open_result.error());
|
|
return false;
|
|
}
|
|
|
|
Filesystem::ReadFile lock_file = *std::move(lock_file_open_result);
|
|
auto lock_result =
|
|
lock_file.TryLock(Filesystem::FileLock::Exclusive, RuntimesLockDeadline,
|
|
RuntimesLockPollInterval);
|
|
if (!lock_result.ok()) {
|
|
// The normal case is when locking would block, log anything else.
|
|
if (!lock_result.error().would_block()) {
|
|
CARBON_VLOG("Error locking cache entry '{0}': {1}", name,
|
|
lock_result.error());
|
|
}
|
|
// However, don't try to remove it as we didn't acquire the lock.
|
|
return false;
|
|
}
|
|
|
|
// The lock is held, remove the entry.
|
|
return rm_entry(name);
|
|
};
|
|
|
|
int num_entries = entries.size();
|
|
for (const auto& [name, age] : entries) {
|
|
if (num_entries < MaxNumEntries) {
|
|
break;
|
|
}
|
|
|
|
// Don't prune the currently being built entry. We should only reach here
|
|
// when some other process created this entry in a race, and we don't want
|
|
// to remove it or trigger rebuilds.
|
|
if (name == new_entry_path) {
|
|
continue;
|
|
}
|
|
|
|
if (rm_unlocked_entry(name, age)) {
|
|
--num_entries;
|
|
}
|
|
}
|
|
|
|
if (num_entries >= MaxNumEntries) {
|
|
CARBON_VLOG(
|
|
"Unable to prune cache to our target size due to held locks on recent "
|
|
"cache entries or removal errors, leaving {0} entries in the cache",
|
|
num_entries);
|
|
}
|
|
}
|
|
|
|
auto Runtimes::Builder::Commit() && -> ErrorOr<std::filesystem::path> {
|
|
std::filesystem::path dest_path = runtimes_->base_path() / dest_;
|
|
|
|
// First, try to do the atomic commit of the built runtimes into the final
|
|
// location.
|
|
CARBON_CHECK(dir_.path().parent_path() == runtimes_->base_path(),
|
|
"Building a temporary directory '{0}' that is not in the "
|
|
"runtimes tree '{1}'",
|
|
dir_.path(), runtimes_->base_path());
|
|
auto rename_result = runtimes_->base_dir().Rename(
|
|
dir_.path().filename(), runtimes_->base_dir(), dest_);
|
|
// If the rename was successful, then we don't need to remove anything so
|
|
// release that state.
|
|
if (rename_result.ok()) {
|
|
std::move(dir_).Release();
|
|
} else if (rename_result.error().not_empty()) {
|
|
// Some other runtimes were successfully committed before ours, so we want
|
|
// to discard ours. We report errors cleaning up here as we don't want to
|
|
// pollute the filesystem excessively.
|
|
//
|
|
// TODO: Consider instead being more resilient to errors here and just log
|
|
// them.
|
|
CARBON_VLOG("PID {0} found racily built runtimes in cache path: {1}",
|
|
getpid(), dest_path);
|
|
CARBON_RETURN_IF_ERROR(std::move(dir_).Remove());
|
|
} else {
|
|
// An unexpected error occurred, propagate it and let the normal cleanup
|
|
// occur.
|
|
//
|
|
// TODO: It's possible we need to handle `EBUSY` here, likely by ensuring it
|
|
// is the *destination* that is busy and an existing, valid directory built
|
|
// concurrently.
|
|
return std::move(rename_result).error();
|
|
}
|
|
|
|
// Now that we've got a final path in place successfully, clear the flock if
|
|
// it is currently held.
|
|
ReleaseFileLock();
|
|
|
|
// Finally, the build is committed so finish putting this into the moved-from
|
|
// state by clearing the runtimes pointer.
|
|
runtimes_ = nullptr;
|
|
return dest_path;
|
|
}
|
|
|
|
auto Runtimes::Builder::ReleaseFileLock() -> void {
|
|
CARBON_CHECK(runtimes_ != nullptr);
|
|
|
|
if (flock_.is_locked()) {
|
|
std::filesystem::path dest_path = runtimes_->base_path() / dest_;
|
|
CARBON_VLOG("PID {0} releasing lock on cache path: {1}", getpid(),
|
|
dest_path);
|
|
(void)lock_file_.WriteFileFromString("");
|
|
flock_ = {};
|
|
(void)std::move(lock_file_).Close();
|
|
} else {
|
|
CARBON_CHECK(!lock_file_.is_valid());
|
|
}
|
|
}
|
|
|
|
auto Runtimes::Builder::Destroy() -> void {
|
|
// If the runtimes are null, no in-flight build is owned so nothing to do.
|
|
if (runtimes_ == nullptr) {
|
|
CARBON_CHECK(
|
|
!lock_file_.is_valid() && !flock_.is_locked() && !dir_.is_valid(),
|
|
"Builder left in a partially cleared state!");
|
|
return;
|
|
}
|
|
|
|
// Otherwise we need to abandon an in-flight build. First release the lock.
|
|
ReleaseFileLock();
|
|
|
|
// The rest of the cleanup is handled by the `RemovingDir` destructor.
|
|
}
|
|
|
|
} // namespace Carbon
|