From 23d82c5d410cb3e4c31b2b0dbb6c1c36319cfc33 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. In order to not run afoul of PATH length limits, we store the original path in a separate sidechannel environment variable (`JULIA_ORIGINAL_PATH`) then restore it after startup is complete. --- Makefile | 8 +++- NEWS.md | 4 ++ base/Base.jl | 5 ++ contrib/windows/exe_path_wrapper.c | 73 ++++++++++++++++++++++++++++++ ui/Makefile | 28 ++++++++++++ ui/repl.c | 13 ++++++ 6 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 contrib/windows/exe_path_wrapper.c diff --git a/Makefile b/Makefile index cad588c148d42..1140ebc2e5159 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 be35c543efd60..d936bf34c18b9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -32,6 +32,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/base/Base.jl b/base/Base.jl index b9821b6856324..1b1af8a344686 100644 --- a/base/Base.jl +++ b/base/Base.jl @@ -168,6 +168,9 @@ include(string((length(Core.ARGS)>=2 ? Core.ARGS[2] : ""), "version_git.jl")) # # Initialize DL_LOAD_PATH as early as possible. We are defining things here in # a slightly more verbose fashion than usual, because we're running so early. +# On Windows, we initialize this such that we always search `bin`, even though +# the true `julia.exe` is located wtihin `libexec`. This allows us to use +# `ccall((func, lib), ...)` syntax where that library is located in `bin`. const DL_LOAD_PATH = String[] let os = ccall(:jl_get_UNAME, Any, ()) if os === :Darwin || os === :Apple @@ -177,6 +180,8 @@ let os = ccall(:jl_get_UNAME, Any, ()) push!(DL_LOAD_PATH, "@loader_path/julia") end push!(DL_LOAD_PATH, "@loader_path") + elseif os === :Windows || os === :NT + push!(DL_LOAD_PATH, "@executable_path\\..\\bin") end end diff --git a/contrib/windows/exe_path_wrapper.c b/contrib/windows/exe_path_wrapper.c new file mode 100644 index 0000000000000..b9d5bc51ecc5e --- /dev/null +++ b/contrib/windows/exe_path_wrapper.c @@ -0,0 +1,73 @@ +#include +#include +#include +#include + +/* The maximum path length we allow the environment to impose upon us */ +#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, usually of the form "../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 %lu\n", GetLastError()); + exit(1); + } + PathRemoveFileSpec(currFileDir); + PathCombine(juliaPath, currFileDir, TEXT(JULIA_EXE_PATH)); + + // On windows, we simulate RPATH by generating a new PATH, and hiding the original + // PATH into a sidechannel variable, JULIA_ORIGINAL_PATH, which will be restored + // within the callee. + LPWSTR pathVal = (LPWSTR) malloc(MAX_PATH_LEN*sizeof(WCHAR)); + DWORD dwRet = GetEnvironmentVariable(TEXT("PATH"), pathVal, MAX_PATH_LEN); + if (dwRet > 0 && dwRet < MAX_PATH_LEN) { + SetEnvironmentVariable(TEXT("JULIA_ORIGINAL_PATH"), pathVal); + } else { + SetEnvironmentVariable(TEXT("JULIA_ORIGINAL_PATH"), TEXT("")); + } + + // Next, we construct a new PATH variable specifically for launching the inner `julia.exe` + DWORD numPathEntries = sizeof(pathEntries)/sizeof(pathEntries[0]); + pathVal[0] = '\0'; + // 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 diff --git a/ui/repl.c b/ui/repl.c index 07e1949f93e1e..c60773ac75634 100644 --- a/ui/repl.c +++ b/ui/repl.c @@ -210,6 +210,19 @@ int wmain(int argc, wchar_t *argv[], wchar_t *envp[]) if (!WideCharToMultiByte(CP_UTF8, 0, warg, -1, arg, len, NULL, NULL)) return 1; argv[i] = (wchar_t*)arg; } + + // If the exe wrapper has passed us the original PATH value in a separate + // sidechannel variable, apply that to Julia now, before doing anything else. + { + #define MAX_PATH_LEN 1920 + WCHAR pathVal[MAX_PATH_LEN]; + DWORD dwRet = GetEnvironmentVariable(TEXT("JULIA_ORIGINAL_PATH"), pathVal, MAX_PATH_LEN); + if (dwRet > 0 && dwRet < MAX_PATH_LEN) { + // Copy the value over to PATH and remove that sidechannel variable + SetEnvironmentVariable(TEXT("PATH"), pathVal); + SetEnvironmentVariable(TEXT("JULIA_ORIGINAL_PATH"), NULL); + } + } #endif libsupport_init(); int lisp_prompt = (argc >= 2 && strcmp((char*)argv[1],"--lisp") == 0);