Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Windows: add wrapper .exe to work around lack of RPATH #35629

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
78 changes: 78 additions & 0 deletions contrib/windows/exe_path_wrapper.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#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];
Copy link
Contributor

@musm musm Jul 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be MAX_PATH and same for the variable below if using PathCombine

A pointer to a buffer that, when this function returns successfully, receives the combined path string. You must set the size of this buffer to MAX_PATH to ensure that it is large enough to hold the returned string.

https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-pathcombinew

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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PathCombine is not recommended from Microsoft:

Note This function, PathCchCombineEx, or PathAllocCombine should be used in place of PathCombine to prevent the possibility of a buffer overrun.

https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-pathcombinew


// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be !dwRet && dwRet <= MAX_PATH_LEN

However looking at the documentation for GetEnvironmentVariable. we should handle resizing the buffer instead of assuming MAX_PATH_LEN

SetEnvironmentVariable(TEXT("JULIA_ORIGINAL_PATH"), pathVal);
} else {
staticfloat marked this conversation as resolved.
Show resolved Hide resolved
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`, but only if it is not already set.
if (GetEnvironmentVariable(TEXT("JULIA_BINDIR"), pathVal, MAX_PATH_LEN) == 0)
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
staticfloat marked this conversation as resolved.
Show resolved Hide resolved
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;
}
7 changes: 5 additions & 2 deletions stdlib/InteractiveUtils/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,11 @@ end
@test occursin("Environment:", ver)
end
let exename = `$(Base.julia_cmd()) --startup-file=no`
@test !occursin("Environment:", read(setenv(`$exename -e 'using InteractiveUtils; versioninfo()'`,
String[]), String))
# Windows wrapper .exe unconditionally adds a JULIA_BINDIR environment mapping
if !Sys.iswindows()
@test !occursin("Environment:", read(setenv(`$exename -e 'using InteractiveUtils; versioninfo()'`,
String[]), String))
end
@test occursin("Environment:", read(setenv(`$exename -e 'using InteractiveUtils; versioninfo()'`,
String["JULIA_CPU_THREADS=1"]), String))
end
Expand Down
20 changes: 15 additions & 5 deletions test/spawn.jl
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,23 @@ end

# setup_stdio for AbstractPipe
let out = Pipe(),
proc = run(pipeline(`$exename --startup-file=no -e 'println(getpid())'`, stdout=IOContext(out, :foo => :bar)), wait=false)
# < don't block here before getpid call >
pid = getpid(proc)
proc = run(pipeline(`$echocmd "Hello World"`, stdout=IOContext(out,stdout)), wait=false)
close(out.in)
@test parse(Int32, read(out, String)) === pid > 1
@test read(out, String) == "Hello World\n"
@test success(proc)
@test_throws Base.IOError getpid(proc)
end

# Test `getpid()` on processes, but not on Windows, since our exe wrapper interferes with this test
if !Sys.iswindows()
let out = Pipe(),
proc = run(pipeline(`$exename --startup-file=no -e 'println(getpid())'`, stdout=IOContext(out, :foo => :bar)), wait=false)
# < don't block here before getpid call >
pid = getpid(proc)
close(out.in)
@test parse(Int32, read(out, String)) === pid > 1
@test success(proc)
@test_throws Base.IOError getpid(proc)
end
end

# issue #5904
Expand Down
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