From 65430bd92a61ca924941b252b4e261ff668efa34 Mon Sep 17 00:00:00 2001 From: Matthew Fernandez Date: Sun, 13 Nov 2022 19:18:11 -0800 Subject: [PATCH] gvedit: locate attrs.txt based on the location of our own executable This commit adapts 24bd92c1e5d49b354141cd06d88ca658991b9825 that taught Smyrna how to find the share directory without a build time define to Gvedit. The pattern is essentially the same, but we can take advantage of some nicer C++ mechanisms instead of C. The motivation for this is to make integration of Gvedit into the CMake build system easier. By doing location of the share directory in the code instead of the build system, we can more easily ensure the same behavior between build systems. A side effect of this is the `gvedit` binary becoming more relocatable. Note that the Windows discovery mechanism differs from what it was before, but it is now more in line with how discovery works on other platforms. Gitlab: #1836 --- CHANGELOG.md | 7 ++ cmd/gvedit/Makefile.am | 3 +- cmd/gvedit/csettings.cpp | 195 +++++++++++++++++++++++++++++++-------- 3 files changed, 162 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 558b02688..6a1bb61e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `cluster`, `dot_builtins`, `dot2gxl`, `gv2gxl`, and `prune` utilities are now included in the CMake build system. #1836 +### Changed + +- `gvedit` now uses a relative path from its own executable to discover its + attributes file, `../share/graphviz/gvedit/attrs.txt`. This should make it + more relocatable and make its behavior more consistent across operating + systems. + ### Fixed - `gxl2gv`, when dealing with `name` attributes, may be less likely to crash. We diff --git a/cmd/gvedit/Makefile.am b/cmd/gvedit/Makefile.am index b5c1f24f3..8b711623c 100644 --- a/cmd/gvedit/Makefile.am +++ b/cmd/gvedit/Makefile.am @@ -27,8 +27,7 @@ gvedit_CPPFLAGS = \ $(AM_CPPFLAGS) \ $(QTGUI_CFLAGS) \ $(QTCORE_CFLAGS) \ - -DDEMAND_LOADING=1 \ - -DGVEDIT_DATADIR='"$(pkgdatadir)/gvedit"' + -DDEMAND_LOADING=1 gvedit_LDADD = \ $(top_builddir)/lib/gvc/libgvc.la \ diff --git a/cmd/gvedit/csettings.cpp b/cmd/gvedit/csettings.cpp index b0fe35473..38cad5618 100644 --- a/cmd/gvedit/csettings.cpp +++ b/cmd/gvedit/csettings.cpp @@ -10,7 +10,9 @@ #ifdef _WIN32 #include "windows.h" #endif +#include #include "csettings.h" +#include #include "qmessagebox.h" #include "qfiledialog.h" #include @@ -19,6 +21,21 @@ #include "string.h" #include "mainwindow.h" #include +#include +#include + +#ifdef __APPLE__ +#include +#endif + +#ifdef __FreeBSD__ +#include +#include +#endif + +#if !defined(_WIN32) +#include +#endif extern int errorPipe(char *errMsg); @@ -29,37 +46,143 @@ typedef struct { int cur; } rdr_t; -#ifdef _WIN32 -#define BSZ 1024 +#if !defined(__APPLE__) && !defined(_WIN32) +/// read the given symlink in the /proc file system +static std::string read_proc(const std::string &path) { -QString findAttrFile () -{ - char line[BSZ]; - int r; - char* s; - QString path; - MEMORY_BASIC_INFORMATION mbi; + std::vector buf(PATH_MAX + 1, '\0'); + if (readlink(path.c_str(), buf.data(), buf.size()) < 0) + return ""; - if (VirtualQuery (&findAttrFile, &mbi, sizeof(mbi)) == 0) { - errout << "failed to get handle for executable.\n"; - return path; - } - r = GetModuleFileNameA((HMODULE)mbi.AllocationBase, line, BSZ); - if (!r || r == BSZ) { - errout << "failed to get path for executable.\n"; - return path; + // was the path too long? + if (buf.back() != '\0') + return ""; + + return buf.data(); +} +#endif + +/// find an absolute path to the current executable +static std::string find_me(void) { + + // macOS +#ifdef __APPLE__ + { + // determine how many bytes we will need to allocate + uint32_t buf_size = 0; + int rc = _NSGetExecutablePath(NULL, &buf_size); + assert(rc != 0); + assert(buf_size > 0); + + std::vector path(buf_size); + + // retrieve the actual path + if (_NSGetExecutablePath(path.data(), &buf_size) < 0) { + errout << "failed to get path for executable.\n"; + return ""; } - s = strstr(line,"\\bin\\"); - if (!s) { - errout << "no \"\\bin\" in path " << line << "\n"; - return path; + + // try to resolve any levels of symlinks if possible + while (true) { + std::vector buf(PATH_MAX + 1, '\0'); + if (readlink(path.data(), buf.data(), buf.size()) < 0) + return path.data(); + + path = buf; } - *s = '\0'; - path.append(line); - path.append("\\share\\graphviz\\gvedit\\attributes.txt"); + } +#elif defined(_WIN32) + { + std::vector path; + int rc = 0; + + do { + { + size_t size = path.empty() ? 1024 : (path.size() * 2); + path.resize(size); + } + + rc = GetModuleFileName(NULL, path.data(), path.size()); + if (rc == 0) { + errout << "failed to get path for executable.\n"; + return ""; + } + + } while (rc == path.size()); + + return path.data(); + } +#else + + // Linux + std::string path = read_proc("/proc/self/exe"); + if (path != "") return path; + + // DragonFly BSD, FreeBSD + path = read_proc("/proc/curproc/file"); + if (path != "") + return path; + + // NetBSD + path = read_proc("/proc/curproc/exe"); + if (path != "") + return path; + +// /proc-less FreeBSD +#ifdef __FreeBSD__ + { + int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; + static const size_t MIB_LENGTH = sizeof(mib) / sizeof(mib[0]); + std::vector buf(PATH_MAX + 1, '\0'); + size_t buf_size = buf.size(); + if (sysctl(mib, MIB_LENGTH, buf.data(), &buf_size, NULL, 0) == 0) + return buf.data(); + } +#endif +#endif + + errout << "failed to get path for executable.\n"; + return ""; } + +/// find an absolute path to where Smyrna auxiliary files are stored +static std::string find_share(void) { + +#ifdef _WIN32 + const char PATH_SEPARATOR = '\\'; +#else + const char PATH_SEPARATOR = '/'; #endif + + // find the path to the `gvedit` binary + std::string gvedit_exe = find_me(); + if (gvedit_exe == "") + return ""; + + // assume it is of the form …/bin/gvedit[.exe] and construct + // …/share/graphviz/gvedit + + size_t slash = gvedit_exe.rfind(PATH_SEPARATOR); + if (slash == std::string::npos) { + errout << "no path separator in path to self, " << gvedit_exe.c_str() + << '\n'; + return ""; + } + + std::string bin = gvedit_exe.substr(0, slash); + slash = bin.rfind(PATH_SEPARATOR); + if (slash == std::string::npos) { + errout << "no path separator in directory containing self, " + << bin.c_str() << '\n'; + return ""; + } + + std::string install_prefix = bin.substr(0, slash); + + return install_prefix + PATH_SEPARATOR + "share" + PATH_SEPARATOR + "graphviz" + + PATH_SEPARATOR + "gvedit"; +} bool loadAttrs(const QString fileName, QComboBox * cbNameG, QComboBox * cbNameN, QComboBox * cbNameE) @@ -117,13 +240,14 @@ CFrmSettings::CFrmSettings() graph = nullptr; activeWindow = nullptr; QString path; + char *s = NULL; #ifndef _WIN32 - char *s = getenv("GVEDIT_PATH"); + s = getenv("GVEDIT_PATH"); +#endif if (s) path = s; else - path = GVEDIT_DATADIR; -#endif + path = QString::fromStdString(find_share()); connect(WIDGET(QPushButton, pbAdd), SIGNAL(clicked()), this, SLOT(addSlot())); @@ -146,21 +270,10 @@ CFrmSettings::CFrmSettings() this, SLOT(scopeChangedSlot(int))); scopeChangedSlot(0); - -#ifndef _WIN32 - loadAttrs(path + "/attrs.txt", WIDGET(QComboBox, cbNameG), - WIDGET(QComboBox, cbNameN), WIDGET(QComboBox, cbNameE)); -#else - if (loadAttrs("../share/graphviz/gvedit/attributes.txt", - WIDGET(QComboBox, cbNameG), WIDGET(QComboBox, cbNameN), - WIDGET(QComboBox, cbNameE))) { - path = findAttrFile(); - if (!path.isEmpty()) - loadAttrs(path, - WIDGET(QComboBox, cbNameG), WIDGET(QComboBox, cbNameN), - WIDGET(QComboBox, cbNameE)); + if (path != "") { + loadAttrs(path + "/attrs.txt", WIDGET(QComboBox, cbNameG), + WIDGET(QComboBox, cbNameN), WIDGET(QComboBox, cbNameE)); } -#endif setWindowIcon(QIcon(":/images/icon.png")); } -- 2.40.0