Skip to content

Commit 9d1833f

Browse files
tkfstevengj
authored andcommitted
Support virtualenv/venv without separate cache (#190)
* Avoid the cryptic error about fake-julia/../lib/julia/sys.so * More robust pre-compilation cache handling for Julia 0.6 fixes #175 * Run doctest via pytest * Don't install pytest 3.7.3 to avoid doctest failures in macOS * Make PyJulia usable in virtual environments * Python < 3.7 does not support namedtuple(..., defaults=...) * Print debug message from is_compatible_exe * More permissive test for finding_libpython * Run find_libpython in tox * Use absolute_import in find_libpython.py * Print jlinfo.libpython * Find the linked libpython using dladdr * Manually set SHLIB_SUFFIX in non-Linux platforms * Use sysconfig.get_config_var("VERSION") * Directly compare executable paths * Print PyCall/deps/build.log * Test against 1.0 instead of nightly in AppVeyor * Do not use platform.system() os.name and sys.platform are more well-defined/documented and we don't need more detail than them. * More manual suffix handling for macOS
1 parent 57eface commit 9d1833f

File tree

6 files changed

+424
-33
lines changed

6 files changed

+424
-33
lines changed

appveyor.yml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,32 @@ environment:
1313
BATDIR: ci\appveyor\win32
1414
CROSS_VERSION: 1
1515

16-
# 32 julia latest Python-35
17-
- JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe"
16+
# 32 julia-1.0 Python-35
17+
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/1.0/julia-1.0-latest-win32.exe"
1818
PYTHONDIR: "C:\\Python35"
1919
BATDIR: ci\appveyor\win32
2020

21+
# 32 julia latest Python-35
22+
# - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe"
23+
# PYTHONDIR: "C:\\Python35"
24+
# BATDIR: ci\appveyor\win32
25+
2126
# 64 julia-0.6 Python-35
2227
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe"
2328
PYTHONDIR: "C:\\Python35-x64"
2429
BATDIR: ci\appveyor\win64
2530
CROSS_VERSION: 1
2631

27-
# 64 julia latest Python-35
28-
- JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe"
32+
# 64 julia-1.0 Python-35
33+
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/1.0/julia-1.0-latest-win64.exe"
2934
PYTHONDIR: "C:\\Python35-x64"
3035
BATDIR: ci\appveyor\win64
3136

37+
# 64 julia latest Python-35
38+
# - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe"
39+
# PYTHONDIR: "C:\\Python35-x64"
40+
# BATDIR: ci\appveyor\win64
41+
3242
matrix:
3343
allow_failures:
3444
- JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe"

julia/core.py

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
# this is python 3.3 specific
3333
from 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())
4244
else:
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+
4553
class JuliaError(Exception):
4654
pass
4755

@@ -254,7 +262,9 @@ def determine_if_statically_linked():
254262

255263
JuliaInfo = 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

260270
def 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

288302
def 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

Comments
 (0)