3232# this is python 3.3 specific
3333from types import ModuleType , FunctionType
3434
35+ from .find_libpython import find_libpython , normalize_path
36+
3537#-----------------------------------------------------------------------------
3638# Classes and funtions
3739#-----------------------------------------------------------------------------
@@ -42,6 +44,12 @@ def iteritems(d): return iter(d.items())
4244else :
4345 iteritems = dict .iteritems
4446
47+
48+ # As setting up Julia modifies os.environ, we need to cache it for
49+ # launching subprocesses later in the original environment.
50+ _enviorn = os .environ .copy ()
51+
52+
4553class JuliaError (Exception ):
4654 pass
4755
@@ -254,7 +262,9 @@ def determine_if_statically_linked():
254262
255263JuliaInfo = namedtuple (
256264 'JuliaInfo' ,
257- ['JULIA_HOME' , 'libjulia_path' , 'image_file' , 'pyprogramname' ])
265+ ['JULIA_HOME' , 'libjulia_path' , 'image_file' ,
266+ # Variables in PyCall/deps/deps.jl:
267+ 'pyprogramname' , 'libpython' ])
258268
259269
260270def juliainfo (runtime = 'julia' ):
@@ -277,24 +287,61 @@ def juliainfo(runtime='julia'):
277287 if PyCall_depsfile !== nothing && isfile(PyCall_depsfile)
278288 include(PyCall_depsfile)
279289 println(pyprogramname)
290+ println(libpython)
280291 end
281- """ ])
292+ """ ],
293+ # Use the original environment variables to avoid a cryptic
294+ # error "fake-julia/../lib/julia/sys.so: cannot open shared
295+ # object file: No such file or directory":
296+ env = _enviorn )
282297 args = output .decode ("utf-8" ).rstrip ().split ("\n " )
283- if len (args ) == 3 :
284- args .append (None ) # no pyprogramname set
298+ args .extend ([None ] * (len (JuliaInfo ._fields ) - len (args )))
285299 return JuliaInfo (* args )
286300
287301
288302def is_same_path (a , b ):
289- a = os .path .normpath (os .path .normcase (a ))
290- b = os .path .normpath (os .path .normcase (b ))
303+ a = os .path .realpath (os .path .normcase (a ))
304+ b = os .path .realpath (os .path .normcase (b ))
291305 return a == b
292306
293307
294- def is_different_exe (pyprogramname , sys_executable ):
295- if pyprogramname is None :
308+ def is_compatible_exe (jlinfo , _debug = lambda * _ : None ):
309+ """
310+ Determine if Python used by PyCall.jl is compatible with this Python.
311+
312+ Current Python executable is considered compatible if it is dynamically
313+ linked to libpython (usually the case in macOS and Windows) and
314+ both of them are using identical libpython. If this function returns
315+ `True`, PyJulia use the same precompilation cache of PyCall.jl used by
316+ Julia itself.
317+
318+ Parameters
319+ ----------
320+ jlinfo : JuliaInfo
321+ A `JuliaInfo` object returned by `juliainfo` function.
322+ """
323+ _debug ("jlinfo.libpython =" , jlinfo .libpython )
324+ if jlinfo .libpython is None :
325+ _debug ("libpython cannot be read from PyCall/deps/deps.jl" )
326+ return False
327+
328+ if determine_if_statically_linked ():
329+ _debug (sys .executable , "is statically linked." )
330+ return False
331+
332+ # Note that the following check is OK since statically linked case
333+ # is already excluded.
334+ if is_same_path (jlinfo .pyprogramname , sys .executable ):
335+ # In macOS and Windows, find_libpython does not work as good
336+ # as in Linux. We add this shortcut so that PyJulia can work
337+ # in those environments.
296338 return True
297- return not is_same_path (pyprogramname , sys_executable )
339+
340+ py_libpython = find_libpython ()
341+ jl_libpython = normalize_path (jlinfo .libpython )
342+ _debug ("py_libpython =" , py_libpython )
343+ _debug ("jl_libpython =" , jl_libpython )
344+ return py_libpython == jl_libpython
298345
299346
300347_julia_runtime = [False ]
@@ -349,11 +396,10 @@ def __init__(self, init_julia=True, jl_runtime_path=None, jl_init_path=None,
349396 runtime = jl_runtime_path
350397 else :
351398 runtime = 'julia'
352- JULIA_HOME , libjulia_path , image_file , depsjlexe = juliainfo (runtime )
399+ jlinfo = juliainfo (runtime )
400+ JULIA_HOME , libjulia_path , image_file , depsjlexe = jlinfo [:4 ]
353401 self ._debug ("pyprogramname =" , depsjlexe )
354402 self ._debug ("sys.executable =" , sys .executable )
355- exe_differs = is_different_exe (depsjlexe , sys .executable )
356- self ._debug ("exe_differs =" , exe_differs )
357403 self ._debug ("JULIA_HOME = %s, libjulia_path = %s" % (JULIA_HOME , libjulia_path ))
358404 if not os .path .exists (libjulia_path ):
359405 raise JuliaError ("Julia library (\" libjulia\" ) not found! {}" .format (libjulia_path ))
@@ -371,7 +417,8 @@ def __init__(self, init_julia=True, jl_runtime_path=None, jl_init_path=None,
371417 else :
372418 jl_init_path = JULIA_HOME .encode ("utf-8" ) # initialize with JULIA_HOME
373419
374- use_separate_cache = exe_differs or determine_if_statically_linked ()
420+ use_separate_cache = not is_compatible_exe (jlinfo , _debug = self ._debug )
421+ self ._debug ("use_separate_cache =" , use_separate_cache )
375422 if use_separate_cache :
376423 PYCALL_JULIA_HOME = os .path .join (
377424 os .path .dirname (os .path .realpath (__file__ )),"fake-julia" ).replace ("\\ " ,"\\ \\ " )
@@ -441,16 +488,37 @@ def __init__(self, init_julia=True, jl_runtime_path=None, jl_init_path=None,
441488 # configuration and so do any packages that depend on it.
442489 self ._call (u"unshift!(Base.LOAD_CACHE_PATH, abspath(Pkg.Dir._pkgroot()," +
443490 "\" lib\" , \" pyjulia%s-v$(VERSION.major).$(VERSION.minor)\" ))" % sys .version_info [0 ])
444- # If PyCall.ji does not exist, create an empty file to force
445- # recompilation
491+
492+ # If PyCall.jl is already pre-compiled, for the global
493+ # environment, hide it while we are loading PyCall.jl
494+ # for PyJulia which has to compile a new cache if it
495+ # does not exist. However, Julia does not compile a
496+ # new cache if it exists in Base.LOAD_CACHE_PATH[2:end].
497+ # https://github.com/JuliaPy/pyjulia/issues/92#issuecomment-289303684
446498 self ._call (u"""
447- isdir(Base.LOAD_CACHE_PATH[1]) ||
448- mkpath(Base.LOAD_CACHE_PATH[1])
449- depsfile = joinpath(Base.LOAD_CACHE_PATH[1],"PyCall.ji")
450- isfile(depsfile) || touch(depsfile)
499+ for path in Base.LOAD_CACHE_PATH[2:end]
500+ cache = joinpath(path, "PyCall.ji")
501+ backup = joinpath(path, "PyCall.ji.backup")
502+ if isfile(cache)
503+ mv(cache, backup; remove_destination=true)
504+ end
505+ end
451506 """ )
452507
453508 self ._call (u"using PyCall" )
509+
510+ if use_separate_cache :
511+ self ._call (u"""
512+ for path in Base.LOAD_CACHE_PATH[2:end]
513+ cache = joinpath(path, "PyCall.ji")
514+ backup = joinpath(path, "PyCall.ji.backup")
515+ if !isfile(cache) && isfile(backup)
516+ mv(backup, cache)
517+ end
518+ rm(backup; force=true)
519+ end
520+ """ )
521+
454522 # Whether we initialized Julia or not, we MUST create at least one
455523 # instance of PyObject and the convert function. Since these will be
456524 # needed on every call, we hold them in the Julia object itself so
0 commit comments