Skip to content

Commit 2a31e94

Browse files
committed
JuliaPy#184, fix JuliaPy#190: support julia options 'bindir' and 'sysimage'
1 parent 567c01b commit 2a31e94

File tree

1 file changed

+173
-150
lines changed

1 file changed

+173
-150
lines changed

pysrc/juliacall/__init__.py

Lines changed: 173 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,173 @@
1-
# This module gets modified by PythonCall when it is loaded, e.g. to include Core, Base
2-
# and Main modules.
3-
4-
__version__ = '0.9.1'
5-
6-
_newmodule = None
7-
8-
def newmodule(name):
9-
"A new module with the given name."
10-
global _newmodule
11-
if _newmodule is None:
12-
_newmodule = Main.seval("name -> (n1=Symbol(name); n2=gensym(n1); Main.@eval(module $n2; module $n1; end; end); Main.@eval $n2.$n1)")
13-
return _newmodule(name)
14-
15-
_convert = None
16-
17-
def convert(T, x):
18-
"Convert x to a Julia T."
19-
global _convert
20-
if _convert is None:
21-
_convert = PythonCall.seval("pyjlcallback((T,x)->pyjl(pyconvert(pyjlvalue(T)::Type,x)))")
22-
return _convert(T, x)
23-
24-
class JuliaError(Exception):
25-
"An error arising in Julia code."
26-
def __init__(self, exception, backtrace=None):
27-
super().__init__(exception, backtrace)
28-
def __str__(self):
29-
e = self.exception
30-
b = self.backtrace
31-
if b is None:
32-
return Base.sprint(Base.showerror, e)
33-
else:
34-
return Base.sprint(Base.showerror, e, b)
35-
@property
36-
def exception(self):
37-
return self.args[0]
38-
@property
39-
def backtrace(self):
40-
return self.args[1]
41-
42-
CONFIG = {'inited': False}
43-
44-
def init():
45-
import os
46-
import ctypes as c
47-
import sys
48-
import subprocess
49-
50-
def option(name, default=None):
51-
"""Get an option.
52-
53-
Options can be set as command line arguments '-X juliacall_{name}={value}' or as
54-
environment variables 'PYTHON_JULIACALL_{NAME}={value}'.
55-
"""
56-
k = 'juliacall_'+name.lower()
57-
v = sys._xoptions.get(k)
58-
if v is not None:
59-
return v
60-
k = 'PYTHON_JULIACALL_'+name.upper()
61-
v = os.getenv(k)
62-
if v is not None:
63-
return v
64-
return default
65-
66-
def choice(name, choices, default=None):
67-
k = option(name)
68-
if k is None:
69-
return default
70-
if k in choices:
71-
if isinstance(k, dict):
72-
return choices[k]
73-
else:
74-
return k
75-
raise ValueError(f'invalid value for option: JULIACALL_{name.upper()}={k}, expecting one of {", ".join(choices)}')
76-
77-
def path_option(name, default=None):
78-
path = option(name)
79-
if path is not None:
80-
return os.path.abspath(path)
81-
return default
82-
83-
# Determine if we should skip initialising.
84-
CONFIG['init'] = choice('init', ['yes', 'no'], default='yes') == 'yes'
85-
if not CONFIG['init']:
86-
return
87-
88-
# Parse some more options
89-
CONFIG['opt_bindir'] = path_option('bindir') # TODO
90-
CONFIG['opt_check_bounds'] = choice('check_bounds', ['yes', 'no']) # TODO
91-
CONFIG['opt_compile'] = choice('compile', ['yes', 'no', 'all', 'min']) # TODO
92-
CONFIG['opt_compiled_modules'] = choice('compiled_modules', ['yes', 'no']) # TODO
93-
CONFIG['opt_depwarn'] = choice('depwarn', ['yes', 'no', 'error']) # TODO
94-
CONFIG['opt_inline'] = choice('inline', ['yes', 'no']) # TODO
95-
CONFIG['opt_optimize'] = choice('optimize', ['0', '1', '2', '3']) # TODO
96-
CONFIG['opt_sysimage'] = path_option('sysimage') # TODO
97-
CONFIG['opt_warn_overwrite'] = choice('warn_overwrite', ['yes', 'no']) # TODO
98-
99-
# Stop if we already initialised
100-
if CONFIG['inited']:
101-
return
102-
103-
# we don't import this at the top level because it is not required when juliacall is
104-
# loaded by PythonCall and won't be available
105-
import juliapkg
106-
107-
# Find the Julia executable and project
108-
CONFIG['exepath'] = exepath = juliapkg.executable()
109-
CONFIG['project'] = project = juliapkg.project()
110-
111-
# Find the Julia library
112-
cmd = [exepath, '--project='+project, '--startup-file=no', '-O0', '--compile=min', '-e', 'import Libdl; print(abspath(Libdl.dlpath("libjulia")))']
113-
CONFIG['libpath'] = libpath = subprocess.run(cmd, check=True, capture_output=True, encoding='utf8').stdout
114-
assert os.path.exists(libpath)
115-
116-
# Initialise Julia
117-
d = os.getcwd()
118-
try:
119-
# Open the library
120-
os.chdir(os.path.dirname(libpath))
121-
CONFIG['lib'] = lib = c.CDLL(libpath, mode=c.RTLD_GLOBAL)
122-
lib.jl_init__threading.argtypes = []
123-
lib.jl_init__threading.restype = None
124-
lib.jl_init__threading()
125-
lib.jl_eval_string.argtypes = [c.c_char_p]
126-
lib.jl_eval_string.restype = c.c_void_p
127-
os.environ['JULIA_PYTHONCALL_LIBPTR'] = str(c.pythonapi._handle)
128-
os.environ['JULIA_PYTHONCALL_EXE'] = sys.executable or ''
129-
os.environ['JULIA_PYTHONCALL_PROJECT'] = project
130-
script = '''
131-
try
132-
import Pkg
133-
Pkg.activate(ENV["JULIA_PYTHONCALL_PROJECT"], io=devnull)
134-
import PythonCall
135-
catch err
136-
print(stderr, "ERROR: ")
137-
showerror(stderr, err, catch_backtrace())
138-
flush(stderr)
139-
rethrow()
140-
end
141-
'''
142-
res = lib.jl_eval_string(script.encode('utf8'))
143-
if res is None:
144-
raise Exception('PythonCall.jl did not start properly')
145-
finally:
146-
os.chdir(d)
147-
148-
CONFIG['inited'] = True
149-
150-
init()
1+
# This module gets modified by PythonCall when it is loaded, e.g. to include Core, Base
2+
# and Main modules.
3+
4+
__version__ = '0.9.1'
5+
6+
_newmodule = None
7+
8+
def newmodule(name):
9+
"A new module with the given name."
10+
global _newmodule
11+
if _newmodule is None:
12+
_newmodule = Main.seval("name -> (n1=Symbol(name); n2=gensym(n1); Main.@eval(module $n2; module $n1; end; end); Main.@eval $n2.$n1)")
13+
return _newmodule(name)
14+
15+
_convert = None
16+
17+
def convert(T, x):
18+
"Convert x to a Julia T."
19+
global _convert
20+
if _convert is None:
21+
_convert = PythonCall.seval("pyjlcallback((T,x)->pyjl(pyconvert(pyjlvalue(T)::Type,x)))")
22+
return _convert(T, x)
23+
24+
class JuliaError(Exception):
25+
"An error arising in Julia code."
26+
def __init__(self, exception, backtrace=None):
27+
super().__init__(exception, backtrace)
28+
def __str__(self):
29+
e = self.exception
30+
b = self.backtrace
31+
if b is None:
32+
return Base.sprint(Base.showerror, e)
33+
else:
34+
return Base.sprint(Base.showerror, e, b)
35+
@property
36+
def exception(self):
37+
return self.args[0]
38+
@property
39+
def backtrace(self):
40+
return self.args[1]
41+
42+
CONFIG = {'inited': False}
43+
44+
julia_info_query = r"""
45+
import Libdl
46+
println(Base.Sys.BINDIR)
47+
println(abspath(Libdl.dlpath("libjulia")))
48+
println(unsafe_string(Base.JLOptions().image_file))
49+
"""
50+
51+
def init():
52+
import os
53+
import ctypes as c
54+
import sys
55+
import subprocess
56+
57+
def option(name, default=None):
58+
"""Get an option.
59+
60+
Options can be set as command line arguments '-X juliacall_{name}={value}' or as
61+
environment variables 'PYTHON_JULIACALL_{NAME}={value}'.
62+
"""
63+
k = 'juliacall_'+name.lower()
64+
v = sys._xoptions.get(k)
65+
if v is not None:
66+
return v
67+
k = 'PYTHON_JULIACALL_'+name.upper()
68+
v = os.getenv(k)
69+
if v is not None:
70+
return v
71+
return default
72+
73+
def choice(name, choices, default=None):
74+
k = option(name)
75+
if k is None:
76+
return default
77+
if k in choices:
78+
if isinstance(k, dict):
79+
return choices[k]
80+
else:
81+
return k
82+
raise ValueError(f'invalid value for option: JULIACALL_{name.upper()}={k}, expecting one of {", ".join(choices)}')
83+
84+
def path_option(name, default=None):
85+
path = option(name)
86+
if path is not None:
87+
return os.path.abspath(path)
88+
return default
89+
90+
# Determine if we should skip initialising.
91+
CONFIG['init'] = choice('init', ['yes', 'no'], default='yes') == 'yes'
92+
if not CONFIG['init']:
93+
return
94+
95+
# Parse some more options
96+
CONFIG['opt_bindir'] = path_option('bindir')
97+
CONFIG['opt_check_bounds'] = choice('check_bounds', ['yes', 'no']) # TODO
98+
CONFIG['opt_compile'] = choice('compile', ['yes', 'no', 'all', 'min']) # TODO
99+
CONFIG['opt_compiled_modules'] = choice('compiled_modules', ['yes', 'no']) # TODO
100+
CONFIG['opt_depwarn'] = choice('depwarn', ['yes', 'no', 'error']) # TODO
101+
CONFIG['opt_inline'] = choice('inline', ['yes', 'no']) # TODO
102+
CONFIG['opt_optimize'] = choice('optimize', ['0', '1', '2', '3']) # TODO
103+
CONFIG['opt_sysimage'] = path_option('sysimage')
104+
CONFIG['opt_warn_overwrite'] = choice('warn_overwrite', ['yes', 'no']) # TODO
105+
106+
# Stop if we already initialised
107+
if CONFIG['inited']:
108+
return
109+
110+
# we don't import this at the top level because it is not required when juliacall is
111+
# loaded by PythonCall and won't be available
112+
import juliapkg
113+
114+
# Find the Julia executable and project
115+
CONFIG['exepath'] = exepath = juliapkg.executable()
116+
CONFIG['project'] = project = juliapkg.project()
117+
118+
# Find the Julia library
119+
cmd = [exepath, '--project='+project, '--startup-file=no', '-O0', '--compile=min', '-e', julia_info_query]
120+
121+
default_bindir, default_libpath, default_sysimage = subprocess.run(cmd, check=True, capture_output=True, encoding='utf8').stdout.splitlines()
122+
CONFIG['libpath'] = libpath = default_libpath
123+
assert os.path.exists(libpath)
124+
125+
if not CONFIG.get('opt_bindir'):
126+
CONFIG['opt_bindir'] = default_bindir
127+
if not CONFIG.get("opt_sysimage"):
128+
CONFIG['opt_sysimage'] = default_sysimage
129+
130+
# Initialise Julia
131+
d = os.getcwd()
132+
try:
133+
# Open the library
134+
os.chdir(os.path.dirname(libpath))
135+
CONFIG['lib'] = lib = c.CDLL(libpath, mode=c.RTLD_GLOBAL)
136+
try:
137+
init_func = lib.jl_init_with_image
138+
except AttributeError:
139+
init_func = lib.jl_init_with_image__threading
140+
141+
init_func.argtypes = [c.c_char_p, c.c_char_p]
142+
init_func.restype = None
143+
init_func(
144+
str(CONFIG['opt_bindir']).encode('utf-8'),
145+
str(CONFIG['opt_sysimage']).encode('utf-8')
146+
)
147+
148+
lib.jl_eval_string.argtypes = [c.c_char_p]
149+
lib.jl_eval_string.restype = c.c_void_p
150+
os.environ['JULIA_PYTHONCALL_LIBPTR'] = str(c.pythonapi._handle)
151+
os.environ['JULIA_PYTHONCALL_EXE'] = sys.executable or ''
152+
os.environ['JULIA_PYTHONCALL_PROJECT'] = project
153+
script = '''
154+
try
155+
import Pkg
156+
Pkg.activate(ENV["JULIA_PYTHONCALL_PROJECT"], io=devnull)
157+
import PythonCall
158+
catch err
159+
print(stderr, "ERROR: ")
160+
showerror(stderr, err, catch_backtrace())
161+
flush(stderr)
162+
rethrow()
163+
end
164+
'''
165+
res = lib.jl_eval_string(script.encode('utf8'))
166+
if res is None:
167+
raise Exception('PythonCall.jl did not start properly')
168+
finally:
169+
os.chdir(d)
170+
171+
CONFIG['inited'] = True
172+
173+
init()

0 commit comments

Comments
 (0)