Skip to content

Commit b6c514a

Browse files
author
Christopher Doris
committed
respect julia compat
1 parent 3357949 commit b6c514a

File tree

4 files changed

+95
-64
lines changed

4 files changed

+95
-64
lines changed

juliacall/deps.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from time import time
77

88
from . import CONFIG, __version__
9-
from .jlcompat import JuliaCompat, Version
9+
from .jlcompat import JuliaCompat, Version, julia_version_str
1010

1111
### META
1212

@@ -92,6 +92,13 @@ def can_skip_resolve():
9292
version = deps.get("version")
9393
if version is None or version != __version__:
9494
return False
95+
# resolve whenever Julia changes
96+
jlexe = deps.get("jlexe")
97+
if jlexe is None:
98+
return False
99+
jlver = deps.get("jlversion")
100+
if jlver is None or jlver != julia_version_str(jlexe):
101+
return False
95102
# resolve whenever swapping between dev/not dev
96103
isdev = deps.get("dev")
97104
if isdev is None or isdev != CONFIG["dev"]:
@@ -114,7 +121,7 @@ def can_skip_resolve():
114121
fn = os.path.join(path, "juliacalldeps.json")
115122
if os.path.exists(fn) and os.path.getmtime(fn) > timestamp:
116123
return False
117-
return True
124+
return deps
118125

119126
def deps_files():
120127
ans = []
@@ -201,13 +208,14 @@ def required_julia():
201208
raise Exception("'julia' compat entries have empty intersection:\n{}".format('\n'.join(['- {!r} at {}'.format(v,f) for (f,v) in compats.items()])))
202209
return compat
203210

204-
def best_julia_version():
211+
def best_julia_version(compat=None):
205212
"""
206213
Selects the best Julia version available matching required_julia().
207214
208215
It's based on jill.utils.version_utils.latest_version() and jill.install.install_julia().
209216
"""
210-
compat = required_julia()
217+
if compat is None:
218+
compat = required_julia()
211219
system = jill.install.current_system()
212220
arch = jill.install.current_architecture()
213221
if system == 'linux' and jill.install.current_libc() == 'musl':
@@ -232,6 +240,8 @@ def record_resolve(pkgs):
232240
set_meta("pydeps", {
233241
"version": __version__,
234242
"dev": CONFIG["dev"],
243+
"jlversion": CONFIG.get("exever"),
244+
"jlexe": CONFIG.get("exepath"),
235245
"timestamp": time(),
236246
"sys_path": sys.path,
237247
"pkgs": [pkg.dict() for pkg in pkgs],

juliacall/init.py

Lines changed: 65 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os, os.path, ctypes as c, shutil, subprocess, jill.install as jli
2-
from . import CONFIG, __version__, deps
2+
from . import CONFIG, __version__, deps, jlcompat
33

44
# Determine if this is a development version of juliacall
55
# i.e. it is installed from the github repo, which contains Project.toml
@@ -18,7 +18,6 @@
1818
jlbin = os.path.join(jlprefix, "bin")
1919
jlinstall = os.path.join(jlprefix, "install")
2020
jldownload = os.path.join(jlprefix, "download")
21-
jlexe = os.path.join(jlbin, "julia.cmd" if os.name == "nt" else "julia")
2221

2322
# Determine where to put the julia environment
2423
venvprefix = os.environ.get("VIRTUAL_ENV")
@@ -38,65 +37,73 @@
3837
CONFIG['jlenv'] = os.path.join(jlenv)
3938
CONFIG['meta'] = os.path.join(jlenv, "PythonCallMeta.json")
4039

41-
# Find the Julia library
40+
# Determine whether or not to skip resolving julia/package versions
41+
skip = deps.can_skip_resolve()
42+
43+
# Find the Julia library, possibly installing Julia
4244
libpath = os.environ.get('PYTHON_JULIACALL_LIB')
43-
if libpath is None:
44-
# Find the Julia executable...
45-
# TODO: Check the Julia executable is compatible with jlcompat
46-
# - If Julia is not found, install a compatible one.
47-
# - If Julia is found in the default prefix and not compatible, reinstall.
48-
# - If Julia is found elsewhere, emit a warning?
49-
jlcompat = deps.required_julia()
50-
# ... in a specified location
45+
if libpath is not None:
46+
if not os.path.exists(libpath):
47+
raise ValueError('PYTHON_JULIACALL_LIB={!r} does not exist'.format(libpath))
48+
else:
49+
# Find the Julia executable
5150
exepath = os.environ.get('PYTHON_JULIACALL_EXE')
52-
# ... in the default prefix
53-
if exepath is None:
54-
exepath = shutil.which(jlexe)
55-
# ... preinstalled
56-
if exepath is None:
57-
exepath = shutil.which("julia")
58-
# ... preinstalled but not in path but still callable somehow (e.g. juliaup)
59-
if exepath is None:
60-
try:
61-
subprocess.run(["julia", "--version"], stdout=subprocess.DEVNULL)
62-
exepath = "julia"
63-
except:
64-
pass
65-
# ... after installing in the default prefix
66-
if exepath is None:
67-
os.makedirs(jldownload, exist_ok=True)
68-
d = os.getcwd()
69-
p = os.environ.get("PATH")
70-
try:
71-
if p is None:
72-
os.environ["PATH"] = jlbin
73-
else:
74-
os.environ["PATH"] += os.pathsep + jlbin
75-
os.chdir(jldownload)
76-
jli.install_julia(confirm=True, install_dir=jlinstall, symlink_dir=jlbin)
77-
finally:
78-
if p is None:
79-
del os.environ["PATH"]
80-
else:
81-
os.environ["PATH"] = p
82-
os.chdir(d)
83-
exepath = shutil.which(jlexe)
84-
if exepath is None:
85-
raise Exception('Installed julia in \'%s\' but cannot find it' % jlbin)
86-
assert exepath is not None
87-
# Test the executable is executable
88-
try:
89-
subprocess.run([exepath, "--version"], stdout=subprocess.DEVNULL)
90-
except:
91-
raise Exception('Julia executable %s does not exist' % repr(exepath))
92-
# Find the corresponding libjulia
51+
if exepath is not None:
52+
v = jlcompat.julia_version_str(exepath)
53+
if v is None:
54+
raise ValueError("PYTHON_JULIACALL_EXE={!r} does not exist".format(exepath))
55+
else:
56+
CONFIG["exever"] = v
57+
else:
58+
compat = deps.required_julia()
59+
# Default scenario
60+
if skip:
61+
# Already know where Julia is
62+
exepath = skip["jlexe"]
63+
else:
64+
# Find the best available version
65+
exepath = None
66+
exever = deps.best_julia_version(compat)
67+
v = jlcompat.julia_version_str("julia")
68+
if v is not None and v == exever:
69+
exepath = "julia"
70+
elif os.path.isdir(jlbin):
71+
for f in os.listdir(jlbin):
72+
if f.startswith("julia"):
73+
x = os.path.join(jlbin, f)
74+
v = jlcompat.julia_version_str(x)
75+
if v is not None and v == exever:
76+
exepath = x
77+
break
78+
# If no such version, install it
79+
if exepath is None:
80+
os.makedirs(jldownload, exist_ok=True)
81+
d = os.getcwd()
82+
p = os.environ.get("PATH")
83+
try:
84+
if p is None:
85+
os.environ["PATH"] = jlbin
86+
else:
87+
os.environ["PATH"] += os.pathsep + jlbin
88+
os.chdir(jldownload)
89+
jli.install_julia(confirm=True, install_dir=jlinstall, symlink_dir=jlbin)
90+
finally:
91+
if p is None:
92+
del os.environ["PATH"]
93+
else:
94+
os.environ["PATH"] = p
95+
os.chdir(d)
96+
exepath = os.path.join(jlbin, "julia.cmd" if os.name == "nt" else "julia")
97+
if not os.path.isfile(exepath):
98+
raise Exception('Installed julia in {!r} but cannot find it'.format(jlbin))
99+
# Check the version is compatible
100+
v = jlcompat.julia_version_str(exepath)
101+
assert v is not None and (compat is None or jlcompat.Version(v) in compat)
102+
CONFIG['exever'] = v
93103
CONFIG['exepath'] = exepath
94104
libpath = subprocess.run([exepath, '--startup-file=no', '-O0', '--compile=min', '-e', 'import Libdl; print(abspath(Libdl.dlpath("libjulia")))'], stdout=(subprocess.PIPE)).stdout.decode('utf8')
95-
else:
96-
if not os.path.isfile(libpath):
97-
raise Exception('PYTHON_JULIACALL_LIB=%s does not exist' % repr(libpath))
98105

99-
# Initialize Julia
106+
# Initialize Julia, including installing required packages
100107
d = os.getcwd()
101108
try:
102109
os.chdir(os.path.dirname(libpath))
@@ -108,8 +115,7 @@
108115
lib.jl_init__threading()
109116
lib.jl_eval_string.argtypes = [c.c_char_p]
110117
lib.jl_eval_string.restype = c.c_void_p
111-
if deps.can_skip_resolve():
112-
pkgs = None
118+
if skip:
113119
install = ''
114120
else:
115121
# get required packages
@@ -164,7 +170,7 @@
164170
res = lib.jl_eval_string(script.encode('utf8'))
165171
if res is None:
166172
raise Exception('PythonCall.jl did not start properly')
167-
if pkgs is not None:
173+
if not skip:
168174
deps.record_resolve(pkgs)
169175
finally:
170176
os.chdir(d)

juliacall/jlcompat.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import subprocess
2+
13
class JuliaCompat:
24
def __init__(self, src):
35
if isinstance(src, str):
@@ -138,3 +140,16 @@ def __contains__(self, v):
138140
return self.v0 <= v and v < self.v1
139141
def __repr__(self):
140142
return 'Range({!r}, {!r})'.format(self.v0, self.v1)
143+
144+
def julia_version_str(exe):
145+
"""
146+
If exe is a julia executable, return its version as a string. Otherwise return None.
147+
"""
148+
try:
149+
proc = subprocess.Popen([exe, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
150+
proc.wait()
151+
words = proc.stdout.read().decode("utf-8").split()
152+
assert words[0] == "julia" and words[1] == "version"
153+
return words[2]
154+
except:
155+
pass

src/deps.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ function can_skip_resolve()
178178
stat(fn).mtime < timestamp || return false
179179
end
180180
end
181-
return true
181+
return deps
182182
end
183183

184184
"""

0 commit comments

Comments
 (0)