Skip to content

Commit fde40d7

Browse files
committed
Solve in-process symbol problem
Create a python script that emulates being julia and simply hands off control. This is necessary because the precompilation and runtime environment needs to be the same in order for PyCall to be able to be used without major modification. Try a different approach Only use hack if python is statically linked New version yet again
1 parent 424ece2 commit fde40d7

File tree

5 files changed

+133
-10
lines changed

5 files changed

+133
-10
lines changed

fake-julia/README

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
This directory contains a python script that pretends to be the julia executable
2+
and is used as such to allow julia precompilation to happen in the same environment.

fake-julia/julia

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/sh
2+
export PYCALL_JULIA_FLAVOR=julia
3+
SCRIPTDIR=`cd "$(dirname "$0")" && pwd`
4+
exec ${PYCALL_PYTHON_EXE:-python} "$SCRIPTDIR/julia.py" -- "$@"

fake-julia/julia-debug

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/sh
2+
export PYCALL_JULIA_FLAVOR=julia-debug
3+
SCRIPTDIR=`cd "$(dirname "$0")" && pwd`
4+
exec ${PYCALL_PYTHON_EXE:-python} "$SCRIPTDIR/julia.py" -- "$@"

fake-julia/julia.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Minimal repl.c to support precompilation with python symbols already loaded
2+
import ctypes
3+
import sys
4+
import os
5+
from ctypes import *
6+
if sys.platform.startswith('darwin'):
7+
sh_ext = ".dylib"
8+
elif sys.platform.startswith('win32'):
9+
sh_ext = ".dll"
10+
else:
11+
sh_ext = ".so"
12+
libjulia = ctypes.CDLL(os.environ["PYCALL_LIBJULIA_PATH"] + "/lib" +
13+
os.environ["PYCALL_JULIA_FLAVOR"] + sh_ext, ctypes.RTLD_GLOBAL)
14+
os.environ["JULIA_HOME"] = os.environ["PYCALL_JULIA_HOME"]
15+
16+
# Set up the calls from libjulia we'll use
17+
libjulia.jl_parse_opts.argtypes = [POINTER(c_int), POINTER(POINTER(c_char_p))]
18+
libjulia.jl_parse_opts.restype = None
19+
libjulia.jl_init.argtypes = [c_void_p]
20+
libjulia.jl_init.restype = None
21+
libjulia.jl_get_global.argtypes = [c_void_p,c_void_p]
22+
libjulia.jl_get_global.restype = c_void_p
23+
libjulia.jl_symbol.argtypes = [c_char_p]
24+
libjulia.jl_symbol.restype = c_void_p
25+
libjulia.jl_apply_generic.argtypes = [POINTER(c_void_p), c_int]
26+
libjulia.jl_apply_generic.restype = c_void_p
27+
libjulia.jl_set_ARGS.argtypes = [c_int, POINTER(c_char_p)]
28+
libjulia.jl_set_ARGS.restype = None
29+
libjulia.jl_atexit_hook.argtypes = [c_int]
30+
libjulia.jl_atexit_hook.restype = None
31+
libjulia.jl_eval_string.argtypes = [c_char_p]
32+
libjulia.jl_eval_string.restype = None
33+
34+
# Ok, go
35+
argc = c_int(len(sys.argv)-1)
36+
argv = (c_char_p * (len(sys.argv)-1))()
37+
if sys.version_info[0] < 3:
38+
argv_strings = sys.argv
39+
else:
40+
argv_strings = [str.encode('utf-8') for str in sys.argv]
41+
argv[1:] = argv_strings[2:]
42+
argv[0] = argv_strings[0]
43+
argv2 = (POINTER(c_char_p) * 1)()
44+
argv2[0] = ctypes.cast(ctypes.addressof(argv),POINTER(c_char_p))
45+
libjulia.jl_parse_opts(byref(argc),argv2)
46+
libjulia.jl_init(0)
47+
libjulia.jl_set_ARGS(argc,argv2[0])
48+
jl_base_module = c_void_p.in_dll(libjulia, "jl_base_module")
49+
_start = libjulia.jl_get_global(jl_base_module, libjulia.jl_symbol(b"_start"))
50+
args = (c_void_p * 1)()
51+
args[0] = _start
52+
libjulia.jl_apply_generic(args, 1)
53+
libjulia.jl_atexit_hook(0)
54+
55+
# As an optimization, share precompiled packages with the main cache directory
56+
libjulia.jl_eval_string(b"""
57+
outputji = Base.JLOptions().outputji
58+
if outputji != C_NULL && !isdefined(Main, :PyCall)
59+
outputfile = unsafe_string(outputji)
60+
target = Base.LOAD_CACHE_PATH[2]
61+
targetpath = joinpath(target, basename(outputfile))
62+
if is_windows()
63+
cp(outputfile, targetpath)
64+
else
65+
mv(outputfile, targetpath; remove_destination = true)
66+
symlink(targetpath, outputfile)
67+
end
68+
end
69+
""")

julia/core.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class JuliaModule(ModuleType):
4848
pass
4949

5050

51+
5152
# add custom import behavior for the julia "module"
5253
class JuliaImporter(object):
5354
def __init__(self, julia):
@@ -192,6 +193,15 @@ def module_functions(julia, module):
192193
pass
193194
return bases
194195

196+
def determine_if_statically_linked():
197+
"""Determines if this python executable is statically linked"""
198+
# Windows and OS X are generally always dynamically linked
199+
if not sys.platform.startswith('linux'):
200+
return False
201+
lddoutput = subprocess.check_output(["ldd",sys.executable])
202+
return not ("libpython" in lddoutput)
203+
204+
195205
_julia_runtime = [False]
196206

197207
class Julia(object):
@@ -246,9 +256,20 @@ def __init__(self, init_julia=True, jl_runtime_path=None, jl_init_path=None,
246256
[runtime, "-e",
247257
"""
248258
println(JULIA_HOME)
249-
println(Libdl.dlpath("libjulia"))
259+
println(Libdl.dlpath(string("lib",Base.julia_exename())))
260+
PyCall_depsfile = Pkg.dir("PyCall","deps","deps.jl")
261+
if isfile(PyCall_depsfile)
262+
eval(Module(:__anon__),
263+
Expr(:toplevel,
264+
:(using Compat),
265+
:(Main.Base.include($PyCall_depsfile)),
266+
:(println(python))))
267+
else
268+
println("nowhere")
269+
end
250270
"""])
251-
JULIA_HOME, libjulia_path = juliainfo.decode("utf-8").rstrip().split("\n")
271+
JULIA_HOME, libjulia_path, depsjlexe = juliainfo.decode("utf-8").rstrip().split("\n")
272+
exe_differs = not depsjlexe == sys.executable
252273
self._debug("JULIA_HOME = %s, libjulia_path = %s" % (JULIA_HOME, libjulia_path))
253274
if not os.path.exists(libjulia_path):
254275
raise JuliaError("Julia library (\"libjulia\") not found! {}".format(libjulia_path))
@@ -297,14 +318,37 @@ def __init__(self, init_julia=True, jl_runtime_path=None, jl_init_path=None,
297318
self.api.jl_exception_clear()
298319

299320
if init_julia:
300-
# Replace the cache directory with a private one. PyCall needs a different
301-
# configuration and so do any packages that depend on it. Ideally, we could
302-
# detect packages that depend on PyCall and only use LOAD_CACHE_PATH for them
303-
# but that would be significantly more complicated and brittle, and may not
304-
# be worth it.
305-
self._call(u"empty!(Base.LOAD_CACHE_PATH)")
306-
self._call(u"push!(Base.LOAD_CACHE_PATH, abspath(Pkg.Dir._pkgroot()," +
307-
"\"lib\", \"pyjulia-v$(VERSION.major).$(VERSION.minor)\"))")
321+
use_separate_cache = exe_differs or determine_if_statically_linked()
322+
if use_separate_cache:
323+
# First check that this is supported
324+
self._call("""
325+
if VERSION < v"0.5-"
326+
error(\"""Using pyjulia with a statically-compiled version
327+
of python or with a version of python that
328+
differs from that used by PyCall.jl is not
329+
supported on julia 0.4""\")
330+
end
331+
""")
332+
# Intercept precompilation
333+
os.environ["PYCALL_PYTHON_EXE"] = sys.executable
334+
PYCALL_JULIA_HOME = os.path.join(
335+
os.path.dirname(os.path.realpath(__file__)),"..","fake-julia").replace("\\","\\\\")
336+
os.environ["PYCALL_JULIA_HOME"] = PYCALL_JULIA_HOME
337+
os.environ["PYCALL_LIBJULIA_PATH"] = os.path.dirname(libjulia_path)
338+
self._call(u"eval(Base,:(JULIA_HOME=\""+PYCALL_JULIA_HOME+"\"))")
339+
# Add a private cache directory. PyCall needs a different
340+
# configuration and so do any packages that depend on it.
341+
self._call(u"unshift!(Base.LOAD_CACHE_PATH, abspath(Pkg.Dir._pkgroot()," +
342+
"\"lib\", \"pyjulia%s-v$(VERSION.major).$(VERSION.minor)\"))" % sys.version_info[0])
343+
# If PyCall.ji does not exist, create an empty file to force
344+
# recompilation
345+
self._call(u"""
346+
isdir(Base.LOAD_CACHE_PATH[1]) ||
347+
mkpath(Base.LOAD_CACHE_PATH[1])
348+
depsfile = joinpath(Base.LOAD_CACHE_PATH[1],"PyCall.ji")
349+
isfile(depsfile) || touch(depsfile)
350+
""")
351+
308352
self._call(u"using PyCall")
309353
# Whether we initialized Julia or not, we MUST create at least one
310354
# instance of PyObject and the convert function. Since these will be

0 commit comments

Comments
 (0)