Skip to content

Commit

Permalink
[common] Refactor FindRunfile error handling for clarity (RobotLocomo…
Browse files Browse the repository at this point in the history
…tion#22293)

In light of some upcoming changes, tracking initialization errors
in-band with the data is too difficult to maintain; instead we'll
use a std::expected-like variant to tease it apart.
  • Loading branch information
jwnimmer-tri authored Dec 11, 2024
1 parent 0944b39 commit 16c84d5
Showing 1 changed file with 72 additions and 61 deletions.
133 changes: 72 additions & 61 deletions common/find_runfiles.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <cstdlib>
#include <memory>
#include <stdexcept>
#include <variant>

#ifdef __APPLE__
#include <mach-o/dyld.h>
Expand All @@ -29,30 +30,38 @@ const char* nullable_to_string(const char* arg) {
return arg ? arg : "nullptr";
}

// Either a bazel_tools Runfiles object xor an error string.
// Runfiles details as loaded / provided by Bazel.
struct RunfilesSingleton {
// This is the nominal, full-featured Runfiles; never nullptr.
std::unique_ptr<Runfiles> runfiles;
// This is the RUNFILES_DIR of `runfiles`; never empty.
std::string runfiles_dir;
std::string error;
};

// Our singleton latch-initialization logic might fail. This type is akin to
// std::expected<RunfilesSingleton, ...> and is used to capture the result of
// initialization. Upon any failure, it will be the string error message instead
// of the singleton.
using RunfilesSingletonOrError =
std::variant<std::string /* error */, RunfilesSingleton>;

// Create a bazel_tools Runfiles object xor an error string. This is memoized
// by GetSingletonRunfiles (i.e., this is only called once per process).
RunfilesSingleton Create() {
RunfilesSingletonOrError Create() {
RunfilesSingleton singleton;

// Choose a mechanism based on environment variables.
const char* mechanism{};
RunfilesSingleton result;
std::string bazel_error;

// Chose a mechanism based on environment variables.
if (std::getenv("TEST_SRCDIR")) {
// When running under bazel test, use the test heuristics.
mechanism = "TEST_SRCDIR";
result.runfiles.reset(Runfiles::CreateForTest(&bazel_error));
singleton.runfiles.reset(Runfiles::CreateForTest(&bazel_error));
} else if ((std::getenv("RUNFILES_MANIFEST_FILE") != nullptr) ||
(std::getenv("RUNFILES_DIR") != nullptr)) {
// When running with some RUNFILES_* env already set, just use that.
mechanism = "RUNFILES_{MANIFEST_FILE,DIR}";
result.runfiles.reset(Runfiles::Create({}, &bazel_error));
singleton.runfiles.reset(Runfiles::Create({}, &bazel_error));
} else {
// When running from the user's command line, use argv0.
mechanism = "argv0";
Expand All @@ -70,85 +79,87 @@ RunfilesSingleton Create() {
const std::string& argv0 = fs::read_symlink({"/proc/self/exe"}).string();
drake::log()->debug("readlink(/proc/self/exe) = {}", argv0);
#endif
result.runfiles.reset(Runfiles::Create(argv0, &bazel_error));
singleton.runfiles.reset(Runfiles::Create(argv0, &bazel_error));
}
drake::log()->debug("FindRunfile mechanism = {}", mechanism);
drake::log()->debug("cwd = \"{}\"", fs::current_path().string());

// If there were runfiles, identify the RUNFILES_DIR.
if (result.runfiles) {
for (const auto& key_value : result.runfiles->EnvVars()) {
if (key_value.first == "RUNFILES_DIR") {
// N.B. We must normalize the path; otherwise the path may include
// `parent/./path` if the binary was run using `./bazel-bin/target` vs
// `bazel-bin/target`.
// TODO(eric.cousineau): Show this in Drake itself. This behavior was
// encountered in Anzu issue 5653, in a Python binary.
fs::path path = key_value.second;
path = fs::absolute(path);
path = path.lexically_normal();
result.runfiles_dir = path.string();
break;
}
}
// If we didn't find it, something was very wrong.
if (result.runfiles_dir.empty()) {
bazel_error = "RUNFILES_DIR was not provided by the Runfiles object";
result.runfiles.reset();
} else if (!fs::is_directory({result.runfiles_dir})) {
bazel_error =
fmt::format("RUNFILES_DIR '{}' does not exist", result.runfiles_dir);
result.runfiles.reset();
}
}

// Report any error.
if (!result.runfiles) {
result.runfiles_dir.clear();
result.error = fmt::format(
// We'll use a reusable helper function to provide a detailed error message.
auto wrap_error = [&mechanism](const std::string& base_message) {
std::string full_message = fmt::format(
"{} (created using {} with TEST_SRCDIR={} and "
"RUNFILES_MANIFEST_FILE={} and RUNFILES_DIR={})",
bazel_error, mechanism, nullable_to_string(std::getenv("TEST_SRCDIR")),
base_message, mechanism, nullable_to_string(std::getenv("TEST_SRCDIR")),
nullable_to_string(std::getenv("RUNFILES_MANIFEST_FILE")),
nullable_to_string(std::getenv("RUNFILES_DIR")));
drake::log()->debug("FindRunfile error: {}", result.error);
drake::log()->debug("FindRunfile error: {}", full_message);
return full_message;
};

// If there were no runfiles, error out.
if (singleton.runfiles == nullptr) {
return wrap_error(bazel_error);
}

// Sanity check our return value.
if (result.runfiles.get() == nullptr) {
DRAKE_DEMAND(result.runfiles_dir.empty());
DRAKE_DEMAND(result.error.length() > 0);
} else {
DRAKE_DEMAND(result.runfiles_dir.length() > 0);
DRAKE_DEMAND(result.error.empty());
// Identify the RUNFILES_DIR. Bazel always provides this in EnvVars.
for (const auto& [key, value] : singleton.runfiles->EnvVars()) {
if (key == "RUNFILES_DIR") {
// N.B. We must normalize the path; otherwise the path may include
// `parent/./path` if the binary was run using `./bazel-bin/target` vs
// `bazel-bin/target`.
// TODO(eric.cousineau): Show this in Drake itself. This behavior was
// encountered in Anzu issue 5653, in a Python binary.
fs::path path = value;
path = fs::absolute(path);
path = path.lexically_normal();
if (!fs::is_directory(path)) {
return wrap_error(fmt::format("RUNFILES_DIR {} does not exist",
fmt_debug_string(path.string())));
}
singleton.runfiles_dir = path.string();
break;
}
}
if (singleton.runfiles_dir.empty()) {
// This should be effectively unreachable code, but if something went
// terribly wrong internally to Bazel, maybe it could happen.
return wrap_error("RUNFILES_DIR was not provided by the Runfiles object");
}

return result;
// Sanity check our return value.
DRAKE_DEMAND(singleton.runfiles != nullptr);
DRAKE_DEMAND(!singleton.runfiles_dir.empty());

return singleton;
}

// Returns the RunfilesSingleton for the current process, latch-initializing it
// first if necessary.
const RunfilesSingleton& GetRunfilesSingleton() {
static const never_destroyed<RunfilesSingleton> result{Create()};
// Returns the runfiles singleton for the current process, latch-initializing it
// first if necessary -- or a (latched) error message if the initialization was
// not successful.
const RunfilesSingletonOrError& GetRunfilesSingletonOrError() {
static const never_destroyed<RunfilesSingletonOrError> result{Create()};
return result.access();
}

} // namespace

bool HasRunfiles() {
return GetRunfilesSingleton().runfiles.get() != nullptr;
const RunfilesSingletonOrError& maybe = GetRunfilesSingletonOrError();
return std::holds_alternative<RunfilesSingleton>(maybe);
}

RlocationOrError FindRunfile(const std::string& resource_path) {
const auto& singleton = GetRunfilesSingleton();
const RunfilesSingletonOrError& singleton_or_error =
GetRunfilesSingletonOrError();

// Check for HasRunfiles.
// Check for HasRunfiles and grab the RunfilesSingleton (if it exists).
RlocationOrError result;
if (!singleton.runfiles) {
DRAKE_DEMAND(!singleton.error.empty());
result.error = singleton.error;
if (std::holds_alternative<std::string>(singleton_or_error)) {
result.error = std::get<std::string>(singleton_or_error);
return result;
}
const RunfilesSingleton& singleton =
std::get<RunfilesSingleton>(singleton_or_error);

// Check the user input.
if (resource_path.empty()) {
Expand Down Expand Up @@ -193,7 +204,7 @@ RlocationOrError FindRunfile(const std::string& resource_path) {
} else {
DRAKE_DEMAND(by_man_ok && !by_dir_ok);
detail =
"and it is on the manifest"
"and it is on the manifest "
"but the file does not exist at that location";
}
result.error = fmt::format(
Expand Down

0 comments on commit 16c84d5

Please sign in to comment.