From 2c7942e4f3077564d76a9340e8f1e40629a93ee2 Mon Sep 17 00:00:00 2001 From: Elliot Saba Date: Tue, 21 Apr 2020 14:58:42 -0700 Subject: [PATCH] Windows: add wrapper `.exe` to work around lack of `RPATH` Windows has no concept of `RPATH`, which makes shipping binaries with relatively-located dependencies quite challenging. Despite investigating many potential avenues of `RPATH` emulation, (including but not limited to Application Manifests, PE file patching, and bundling the true Julia executable as a resource inside of a launcher) the most reliable (And least breaking for external workflows) was found to be to create a launcher executable that invokes the true Julia executable from within the `libexec` directory. --- Makefile | 8 ++- NEWS.md | 4 ++ contrib/windows/exe_path_wrapper.c | 81 ++++++++++++++++++++++++++++++ ui/Makefile | 28 +++++++++++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 contrib/windows/exe_path_wrapper.c diff --git a/Makefile b/Makefile index 9e1a697b739ee..893bfe03b3e55 100644 --- a/Makefile +++ b/Makefile @@ -295,8 +295,14 @@ ifeq ($(OS),WINNT) -$(INSTALL_M) $(filter-out $(build_bindir)/libjulia-debug.dll,$(wildcard $(build_bindir)/*.dll)) $(DESTDIR)$(bindir)/ -$(INSTALL_M) $(build_libdir)/libjulia.dll.a $(DESTDIR)$(libdir)/ - # We have a single exception; we want 7z.dll to live in libexec, not bin, so that 7z.exe can find it. + # we want 7z.dll to live in libexec, not bin, so that 7z.exe can find it. -mv $(DESTDIR)$(bindir)/7z.dll $(DESTDIR)$(libexecdir)/ + + # We also have a `julia.exe` and `julia-debug.exe` that live in $(libexecdir) + $(INSTALL_M) $(build_libexecdir)/julia$(EXE) $(DESTDIR)$(libexecdir)/ +ifeq ($(BUNDLE_DEBUG_LIBS),1) + $(INSTALL_M) $(build_libexecdir)/julia-debug$(EXE) $(DESTDIR)$(libexecdir)/ +endif ifeq ($(BUNDLE_DEBUG_LIBS),1) -$(INSTALL_M) $(build_bindir)/libjulia-debug.dll $(DESTDIR)$(bindir)/ -$(INSTALL_M) $(build_libdir)/libjulia-debug.dll.a $(DESTDIR)$(libdir)/ diff --git a/NEWS.md b/NEWS.md index c3173351ec9c5..cc3529b656bfc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -30,6 +30,10 @@ Multi-threading changes Build system changes -------------------- +* The windows executable now uses a launcher `.exe` to transparently set up environment variables necessary + for the "true" Julia executable to find its dependent libraries, as they are no longer located within the + main `bin` directory. ([#35629]) + New library functions --------------------- diff --git a/contrib/windows/exe_path_wrapper.c b/contrib/windows/exe_path_wrapper.c new file mode 100644 index 0000000000000..3800793fe98f4 --- /dev/null +++ b/contrib/windows/exe_path_wrapper.c @@ -0,0 +1,81 @@ +#include +#include +#include +#include + +/* The maximum path length we'll allow */ +#define MAX_PATH_LEN 1920 + +/* PATH_ENTRIES is our simulated RPATH, usually of the form 'TEXT("../path1"), TEXT("../path2"), ...' */ +#ifndef PATH_ENTRIES +#define PATH_ENTRIES TEXT("") +#endif + +LPWSTR pathEntries[] = { + PATH_ENTRIES +}; + +/* JULIA_EXE_PATH is the relative path to julia.exe */ +#ifndef JULIA_EXE_PATH +#define JULIA_EXE_PATH "../libexec/julia.exe" +#endif + +int wmain(int argc, wchar_t *argv[], wchar_t *envp[]) { + // Determine absolute path to true julia.exe sitting in `libexec/` + WCHAR currFileDir[MAX_PATH_LEN]; + WCHAR juliaPath[MAX_PATH_LEN]; + if (!GetModuleFileName(NULL, currFileDir, MAX_PATH_LEN)) { + fprintf(stderr, "ERROR: GetModuleFileName() failed with code %d\n", GetLastError()); + exit(1); + } + PathRemoveFileSpec(currFileDir); + PathCombine(juliaPath, currFileDir, TEXT(JULIA_EXE_PATH)); + + // On windows, we simulate RPATH by pushing onto PATH + LPWSTR pathVal = (LPWSTR) malloc(MAX_PATH_LEN*sizeof(WCHAR)); + DWORD dwRet = GetEnvironmentVariable(TEXT("PATH"), pathVal, MAX_PATH_LEN); + DWORD numPathEntries = sizeof(pathEntries)/sizeof(pathEntries[0]); + if (dwRet == 0) { + // If we cannot get PATH, then our job is easy! + pathVal[0] = '\0'; + } + else { + // Otherwise, we append, if we have enough space to: + DWORD currFileDirLen = wcslen(currFileDir); + DWORD totalPathLen = dwRet + 1 + currFileDirLen; + int env_idx; + for (env_idx = 0; env_idx < numPathEntries; ++env_idx) { + totalPathLen += 1 + currFileDirLen + 1 + wcslen(pathEntries[env_idx]); + } + if (MAX_PATH_LEN < totalPathLen) { + fprintf(stderr, "ERROR: Cannot append entries to PATH: not enough space in environment block. Reduce size of PATH!"); + exit(1); + } + lstrcat(pathVal, TEXT(";")); + } + // We always add the current directory (e.g. `bin/`) to PATH so that we can find e.g. libjulia.dll + lstrcat(pathVal, currFileDir); + + // For each entry in PATH_ENTRIES, tack it on to the end, relative to the current directory: + int env_idx; + for (env_idx = 0; env_idx < numPathEntries; ++env_idx) { + lstrcat(pathVal, TEXT(";")); + lstrcat(pathVal, currFileDir); + lstrcat(pathVal, TEXT("\\")); + lstrcat(pathVal, pathEntries[env_idx]); + } + SetEnvironmentVariable(TEXT("PATH"), pathVal); + free(pathVal); + + STARTUPINFO info; + PROCESS_INFORMATION processInfo; + DWORD exit_code = 1; + GetStartupInfo(&info); + if (CreateProcess(juliaPath, GetCommandLine(), NULL, NULL, TRUE, 0, NULL, NULL, &info, &processInfo)) { + WaitForSingleObject(processInfo.hProcess, INFINITE); + GetExitCodeProcess(processInfo.hProcess, &exit_code); + CloseHandle(processInfo.hProcess); + CloseHandle(processInfo.hThread); + } + return exit_code; +} diff --git a/ui/Makefile b/ui/Makefile index 6ccb8c1fafb03..afa45ba2c23d9 100644 --- a/ui/Makefile +++ b/ui/Makefile @@ -45,6 +45,8 @@ $(BUILDDIR)/%.dbg.obj: $(SRCDIR)/%.c $(HEADERS) @$(call PRINT_CC, $(CC) $(CPPFLAGS) $(CFLAGS) $(DEBUGFLAGS) -c $< -o $@) ifeq ($(OS),WINNT) +JLDFLAGS += -municode + ifneq ($(USEMSVC), 1) $(BUILDDIR)/julia_res.o: $(JULIAHOME)/contrib/windows/julia.rc $(JULIAHOME)/VERSION JLVER=`cat $(JULIAHOME)/VERSION` && \ @@ -82,10 +84,36 @@ else CXXLD := $(LD) endif +# Add `.exe` wrapper generation to properly setup `PATH` on windows to simulate `RPATH` +ifeq ($(OS),WINNT) +define rel_path_entry +TEXT(\"$$(echo $(call rel_path,$(build_libexecdir),$(1)) | sed -e 's_/_\\\\_g')\") +endef +CPPFLAGS_PATH = -DPATH_ENTRIES="$(call rel_path_entry,$(build_shlibdir))" +define julia_exe_path +-DJULIA_EXE_PATH="\"$$(echo '$(libexecdir_rel)\\$(1)' | sed -e 's_\\_\\\\_g')\"" +endef +define gen_wrapper_exe + -mv $(build_bindir)/$(1) $(build_libexecdir)/$(1) + @$(call PRINT_CC, $(CC) $(CPPFLAGS) $(CFLAGS) $(SHIPFLAGS) $(call julia_exe_path,$(1)) $(CPPFLAGS_PATH) $(JULIAHOME)/contrib/windows/exe_path_wrapper.c -o $(build_bindir)/$(1) $(JLDFLAGS) -lshlwapi) +endef + +$(build_bindir)/julia$(EXE): $(JULIAHOME)/contrib/windows/exe_path_wrapper.c +$(build_bindir)/julia-debug$(EXE): $(JULIAHOME)/contrib/windows/exe_path_wrapper.c +endif + + $(build_bindir)/julia$(EXE): $(OBJS) @$(call PRINT_LINK, $(CXXLD) $(CXXFLAGS) $(CXXLDFLAGS) $(LINK_FLAGS) $(SHIPFLAGS) $(OBJS) -o $@ -L$(build_private_libdir) -L$(build_libdir) -L$(build_shlibdir) -ljulia $(JLDFLAGS) $(CXXLDFLAGS)) +ifeq ($(OS),WINNT) + @$(call gen_wrapper_exe,julia$(EXE)) +endif + $(build_bindir)/julia-debug$(EXE): $(DOBJS) @$(call PRINT_LINK, $(CXXLD) $(CXXFLAGS) $(CXXLDFLAGS) $(LINK_FLAGS) $(DEBUGFLAGS) $(DOBJS) -o $@ -L$(build_private_libdir) -L$(build_libdir) -L$(build_shlibdir) -ljulia-debug $(JLDFLAGS) $(CXXLDFLAGS)) +ifeq ($(OS),WINNT) + @$(call gen_wrapper_exe,julia-debug$(EXE)) +endif clean: | $(CLEAN_TARGETS) rm -f *.o *.dbg.obj