diff --git a/include/lsst/cpputils/packaging.h b/include/lsst/cpputils/packaging.h index 77e39df..26ccab7 100644 --- a/include/lsst/cpputils/packaging.h +++ b/include/lsst/cpputils/packaging.h @@ -39,6 +39,23 @@ namespace cpputils { */ std::string getPackageDir(std::string const& packageName); +/*! + * \brief return the root directory of the package whose shared library contains + * the given address + * + * The address is resolved to its containing shared library via `dladdr`, and the + * package root is derived from that library's location (`.../lib/libFoo.so` -> + * `...`). This allows a package to locate its own data files without relying on + * environment variables, provided the address belongs to a symbol compiled into + * that package's own shared library. + * + * \param[in] addressInLibrary address of a symbol residing in the target + * package's shared library + * + * \throw lsst::pex::exceptions::NotFoundError if the library cannot be located + */ +std::string getPackageDirFromAddress(void const* addressInLibrary); + } } // namespace lsst::cpputils diff --git a/src/packaging.cc b/src/packaging.cc index 5c1c277..81347d8 100644 --- a/src/packaging.cc +++ b/src/packaging.cc @@ -22,6 +22,9 @@ #include "lsst/cpputils/packaging.h" +#include + +#include #include #include #include @@ -44,4 +47,34 @@ std::string getPackageDir(std::string const& packageName) { return dir; } +std::string getPackageDirFromAddress(void const* addressInLibrary) { + // A null pointer is a caller precondition violation, not a lookup failure; + // reject it explicitly rather than relying on dladdr's unspecified behavior. + if (addressInLibrary == nullptr) { + throw LSST_EXCEPT(lsst::pex::exceptions::InvalidParameterError, + "Null address passed to getPackageDirFromAddress"); + } + + // dladdr resolves an address to the shared object whose memory map contains + // it, regardless of which library's code is calling dladdr. Passing an + // address from the target package's own library therefore yields that + // library's path (works identically for .so on Linux and .dylib on macOS). + Dl_info info{}; + if (dladdr(addressInLibrary, &info) == 0 || info.dli_fname == nullptr) { + throw LSST_EXCEPT(lsst::pex::exceptions::NotFoundError, + "Could not locate the shared library for the given address"); + } + + std::error_code ec; + std::filesystem::path libraryPath = std::filesystem::canonical(info.dli_fname, ec); + if (ec) { + throw LSST_EXCEPT(lsst::pex::exceptions::NotFoundError, + "Could not resolve shared library path '" + + std::string(info.dli_fname) + "': " + ec.message()); + } + + // ...//lib/lib.{so,dylib} -> + return libraryPath.parent_path().parent_path().string(); +} + }} // namespace lsst::cpputils diff --git a/tests/test_packaging.cc b/tests/test_packaging.cc index 45a13c1..d77860f 100644 --- a/tests/test_packaging.cc +++ b/tests/test_packaging.cc @@ -42,4 +42,20 @@ BOOST_AUTO_TEST_CASE(GetPackage) { lsst::pex::exceptions::NotFoundError); } +BOOST_AUTO_TEST_CASE(GetPackageDirFromAddress) { + // The address of a symbol defined in libcpputils resolves, via dladdr, to + // the cpputils package directory -- independent of any environment variable. + auto anchor = reinterpret_cast(&getPackageDirFromAddress); + // A valid address must resolve without throwing. + std::filesystem::path cpputilsPath; + BOOST_REQUIRE_NO_THROW(cpputilsPath = getPackageDirFromAddress(anchor)); + BOOST_CHECK(std::filesystem::is_regular_file(cpputilsPath / "tests" / "test_packaging.cc")); + // It must agree with the environment-variable-based lookup for the same package. + BOOST_CHECK_EQUAL(std::filesystem::canonical(cpputilsPath), + std::filesystem::canonical(getPackageDir("cpputils"))); + // A null address is a precondition violation. + BOOST_CHECK_THROW(getPackageDirFromAddress(nullptr), + lsst::pex::exceptions::InvalidParameterError); +} + BOOST_AUTO_TEST_SUITE_END()