From: Matthew Fernandez Date: Tue, 1 Nov 2022 03:26:33 +0000 (-0700) Subject: fix installing example graphs during CPack; rewrite 'SMYRNA_PATH' discovery X-Git-Tag: 7.0.1~6^2~1 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=24bd92c1e5d49b354141cd06d88ca658991b9825;p=graphviz fix installing example graphs during CPack; rewrite 'SMYRNA_PATH' discovery This is effectively two commits in one. But there seems to be no way to separate these changes without the parts individually failing tests/test_tools.py::test_tools[smyrna]. Smyrna auxiliary files were being installed incorrectly prior to this commit, so installing them correctly and fixing how Smyrna finds them needs to be done in a single change. --- CMake: fix installing example graphs during CPack When running `cpack`, instead of staging the Smyrna auxiliary files (share/**) and the example graphs (graphs/**) in build-local directories, they would be incorrectly staged into their final install paths. This was quite unexpected as it would result in errors if you did not have permission to write to the install directories or deleting+replacing files from a previous Graphviz installation if you did. This bug was introduced in 67f88eb86c9966daa4d8c3a6f25aee8cd35a046d as an accidental side effect of enabling Smyrna in the CMake build system. A close re-reading of the CPack¹ and `GNUInstallDirs`² docs suggests that absolute paths should never be used in `install` rules: `CMAKE_INSTALL_` Destination for files of a given type. This value may be passed to the `DESTINATION` options of `install()` commands for the corresponding file type. It should typically be a path relative to the installation prefix so that it can be converted to an absolute path in a relocatable way (see `CMAKE_INSTALL_FULL_`). However, an absolute path is also allowed. `CMAKE_INSTALL_FULL_` The absolute path generated from the corresponding `CMAKE_INSTALL_` value. If the value is not already an absolute path, an absolute path is constructed typically by prepending the value of the `CMAKE_INSTALL_PREFIX` variable. However, there are some special cases as documented below. This change brings the behavior on Linux in line with how CPack operates on other platforms – the default value of `CMAKE_INSTALL_DATAROOTDIR` is `share` which is the hard coded value set on non-Linux platforms. ---- smyrna: rewrite 'SMYRNA_PATH' discovery This aligns Smyrna on other platforms with how Smyrna on Windows locates its templates, examples etc. This resolves a problem where a build time path was being baked into the Smyrna binary, preventing it from being relocatable. The code for locating our own executable in this commit is adapted from public domain source.³ This commit also lifts the 1024 character limit on path discovery in the Windows branch of this logic. It now dynamically expands the target buffer until the current executable name will fit. Gitlab: fixes #2232 Reported-by: Magnus Jacobsson ¹ https://cmake.org/cmake/help/book/mastering-cmake/chapter/Packaging%20With%20CPack.html ² https://cmake.org/cmake/help/latest/module/GNUInstallDirs.html ³ https://github.com/Smattr/clink/blob/main/clink/src/find_me.c as of commit 8cadfc49a74e429fa69afdc460cb2b0662d81260 --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b8208fff..dc55cfb99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Failure of arrowhead and arrowtail to respect penwidth #372 \ Fixed also for the `diamond` [edge arrow shape](https://graphviz.org/doc/info/arrows.html#primitive-shapes). +- The CMake build system no longer uses the final install location as the + staging area for example graphs and templates during `cpack`. This bug was + introduced in Graphviz 4.0.0. #2232 ## [7.0.0] – 2022-10-22 diff --git a/CMakeLists.txt b/CMakeLists.txt index bc3f5e96c..5d708e288 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,7 +64,7 @@ if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") set(MAN_INSTALL_DIR "${CMAKE_INSTALL_MANDIR}") set(libdir "${CMAKE_INSTALL_FULL_LIBDIR}") set(includedir "${CMAKE_INSTALL_FULL_INCLUDEDIR}") - set(DATA_INSTALL_DIR "${CMAKE_INSTALL_FULL_DATAROOTDIR}") + set(DATA_INSTALL_DIR "${CMAKE_INSTALL_DATAROOTDIR}") else() set(BINARY_INSTALL_DIR bin) set(LIBRARY_INSTALL_DIR lib) diff --git a/cmd/smyrna/CMakeLists.txt b/cmd/smyrna/CMakeLists.txt index 1b7597b25..babe6eb14 100644 --- a/cmd/smyrna/CMakeLists.txt +++ b/cmd/smyrna/CMakeLists.txt @@ -12,8 +12,6 @@ if(with_smyrna) pkg_check_modules(GTS REQUIRED gts) pkg_check_modules(XRENDER REQUIRED xrender) - add_definitions(-DSMYRNA_PATH="${DATA_INSTALL_DIR}/graphviz/smyrna") - add_executable(smyrna arcball.h draw.h diff --git a/cmd/smyrna/Makefile.am b/cmd/smyrna/Makefile.am index 442e0adc2..017459934 100644 --- a/cmd/smyrna/Makefile.am +++ b/cmd/smyrna/Makefile.am @@ -1,7 +1,6 @@ ## Process this file with automake to produce Makefile.in AM_CPPFLAGS = \ - -DSMYRNA_PATH='"$(pkgdatadir)/smyrna"' \ -I$(top_srcdir)/lib \ -I$(top_srcdir)/lib/cgraph \ -I$(top_srcdir)/lib/cdt \ diff --git a/cmd/smyrna/main.c b/cmd/smyrna/main.c index d6c045819..b89a5ba6a 100644 --- a/cmd/smyrna/main.c +++ b/cmd/smyrna/main.c @@ -36,7 +36,23 @@ #include "glutrender.h" #include -#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include +#endif + +#ifdef __FreeBSD__ +#include +#include +#endif + +#if !defined(__APPLE__) && !defined(_WIN32) +#include +#endif static char *smyrnaDir; /* path to directory containin smyrna data files */ static char *smyrnaGlade; @@ -168,49 +184,153 @@ static void windowedMode(int argc, char *argv[]) gtk_main(); } +#if !defined(__APPLE__) && !defined(_WIN32) +/// read the given symlink in the /proc file system +static char *read_proc(const char *path) { + char buf[PATH_MAX + 1] = {0}; + if (readlink(path, buf, sizeof(buf)) < 0) + return NULL; + // was the path too long? + if (buf[sizeof(buf) - 1] != '\0') + return NULL; -int main(int argc, char *argv[]) -{ - smyrnaDir = getenv("SMYRNA_PATH"); - if (!smyrnaDir) { -#ifdef _WIN32 -#define BSZ 1024 -#define SMYRNA "\\share\\graphviz\\smyrna" + return gv_strdup(buf); +} +#endif - int r; - char line[BSZ]; - char* s; +/// find an absolute path to the current executable +static char *find_me(void) { - r = GetModuleFileName(NULL, line, BSZ); - if (!r || (r == BSZ)) { - fprintf (stderr,"failed to get path for executable.\n"); - graphviz_exit(1); - } + // 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); - /* line contains path to smyrna: "$GVROOT\bin\smyrna.exe" - * We want to obtain $GVROOT. We find the second rightmost '\' - * and store a '\0' there. - */ - s = strrchr(line,'\\'); - if (!s) { - fprintf (stderr,"no backslash in path %s.\n", line); - graphviz_exit(1); - } - while ((s != line) && (*(--s) != '\\')) ; - if (s == line) { - fprintf (stderr,"no backslash in path %s.\n", line); - graphviz_exit(1); - } - *s = '\0'; + path = gv_alloc(buf_size); + + // retrieve the actual path + if (_NSGetExecutablePath(path, &buf_size) < 0) { + fprintf(stderr, "failed to get path for executable.\n"); + graphviz_exit(EXIT_FAILURE); + } - smyrnaDir = gv_calloc(strlen(line) + sizeof(SMYRNA), sizeof(char)); - strcpy (smyrnaDir, line); - strcat(smyrnaDir, SMYRNA); + // try to resolve any levels of symlinks if possible + while (true) { + char buf[PATH_MAX + 1] = {0}; + if (readlink(path, buf, sizeof(buf)) < 0) + return path; + + free(path); + path = gv_strdup(buf); + } + } +#elif defined(_WIN32) + { + char *path = NULL; + size_t path_size = 0; + int rc = 0; + + do { + size_t size = path_size == 0 ? 1024 : (path_size * 2); + path = gv_realloc(path, path_size, size); + path_size = size; + + rc = GetModuleFileName(NULL, path, path_size); + if (rc == 0) { + fprintf(stderr, "failed to get path for executable.\n"); + graphviz_exit(EXIT_FAILURE); + } + + } while (rc == path_size); + + return path; + } +#else + + // Linux + char *path = read_proc("/proc/self/exe"); + if (path != NULL) + return path; + + // DragonFly BSD, FreeBSD + path = read_proc("/proc/curproc/file"); + if (path != NULL) + return path; + + // NetBSD + path = read_proc("/proc/curproc/exe"); + if (path != NULL) + 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]); + char buf[PATH_MAX + 1] = {0}; + size_t buf_size = sizeof(buf); + if (sysctl(mib, MIB_LENGTH, buf, &buf_size, NULL, 0) == 0) + return gv_strdup(buf); + } +#endif +#endif + + fprintf(stderr, "failed to get path for executable.\n"); + graphviz_exit(EXIT_FAILURE); +} + +/// find an absolute path to where Smyrna auxiliary files are stored +static char *find_share(void) { + +#ifdef _WIN32 + const char PATH_SEPARATOR = '\\'; #else - smyrnaDir = SMYRNA_PATH; + const char PATH_SEPARATOR = '/'; #endif + + // find the path to the `smyrna` binary + char *smyrna_exe = find_me(); + + // assume it is of the form …/bin/smyrna[.exe] and construct + // …/share/graphviz/smyrna + + char *slash = strrchr(smyrna_exe, PATH_SEPARATOR); + if (slash == NULL) { + fprintf(stderr, "no path separator in path to self, %s\n", smyrna_exe); + free(smyrna_exe); + graphviz_exit(EXIT_FAILURE); + } + + *slash = '\0'; + slash = strrchr(smyrna_exe, PATH_SEPARATOR); + if (slash == NULL) { + fprintf(stderr, "no path separator in directory containing self, %s\n", + smyrna_exe); + free(smyrna_exe); + graphviz_exit(EXIT_FAILURE); + } + + *slash = '\0'; + size_t share_len = strlen(smyrna_exe) + strlen("/share/graphviz/smyrna") + 1; + char *share = gv_alloc(share_len); + snprintf(share, share_len, "%s%cshare%cgraphviz%csmyrna", smyrna_exe, + PATH_SEPARATOR, PATH_SEPARATOR, PATH_SEPARATOR); + free(smyrna_exe); + + return share; +} + +int main(int argc, char *argv[]) +{ + smyrnaDir = getenv("SMYRNA_PATH"); + if (!smyrnaDir) { + smyrnaDir = find_share(); } load_attributes();