Skip to content

Commit

Permalink
Windows: add wrapper .exe to work around lack of RPATH
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
staticfloat committed Jul 1, 2020
1 parent f935125 commit fdbbfcd
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 1 deletion.
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)/
Expand Down
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------
Expand Down
5 changes: 5 additions & 0 deletions base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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\\")
end
end

Expand Down
77 changes: 77 additions & 0 deletions contrib/windows/exe_path_wrapper.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <shlwapi.h>

/* 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(""));
}

// We also set JULIA_BINDIR to the directory holding this file, as otherwise it
// auto picks up `libexec` instead of `bin`.
SetEnvironmentVariable(TEXT("JULIA_BINDIR"), currFileDir);

// 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;
}
28 changes: 28 additions & 0 deletions ui/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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` && \
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions ui/repl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit fdbbfcd

Please sign in to comment.