From df51ae65258ee5a6e8d273774a1e714b02b35f21 Mon Sep 17 00:00:00 2001 From: Nicolae Cudlenco <146981376+ncudlenco@users.noreply.github.com> Date: Mon, 18 May 2026 23:53:14 +0200 Subject: [PATCH 1/3] feat(juliacall): allow supplying libjulia path / bindir to skip discovery subprocess init() starts a short-lived Julia process solely to print libjulia's path and Sys.BINDIR. In pre-built containers / system images these are static and known ahead of time; allow supplying them via the libpath / default_bindir options (PYTHON_JULIACALL_LIBPATH / PYTHON_JULIACALL_DEFAULT_BINDIR) to skip the extra process. Behaviour is unchanged unless both are set. Docs and CHANGELOG updated. --- CHANGELOG.md | 6 ++++++ docs/src/juliacall.md | 2 ++ pysrc/juliacall/__init__.py | 17 +++++++++++++---- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1366eb2..73edc266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased +* JuliaCall: added `libpath` / `default_bindir` options + (`PYTHON_JULIACALL_LIBPATH` / `PYTHON_JULIACALL_DEFAULT_BINDIR`) to supply + libjulia's path and `Sys.BINDIR` directly, skipping the discovery subprocess + at startup. Behaviour is unchanged unless both are set. + ## 0.9.34 (2026-05-18) * Bug fixes. diff --git a/docs/src/juliacall.md b/docs/src/juliacall.md index c49e150f..6f31ea5d 100644 --- a/docs/src/juliacall.md +++ b/docs/src/juliacall.md @@ -142,6 +142,8 @@ be configured in two ways: | `-X juliacall-heap-size-hint=` | `PYTHON_JULIACALL_HEAP_SIZE_HINT=` | Hint for initial heap size in bytes. | | `-X juliacall-exe=` | `PYTHON_JULIACALL_EXE=` | Path to Julia binary to use (overrides JuliaPkg). | | `-X juliacall-project=` | `PYTHON_JULIACALL_PROJECT=` | Path to the Julia project to use (overrides JuliaPkg). | +| `-X juliacall-libpath=` | `PYTHON_JULIACALL_LIBPATH=` | Path to libjulia. If set together with `default-bindir`, skips the subprocess that discovers it. | +| `-X juliacall-default-bindir=` | `PYTHON_JULIACALL_DEFAULT_BINDIR=` | Julia's `Sys.BINDIR`. If set together with `libpath`, skips the subprocess that discovers it. | | `-X juliacall-trace-compile=` | `PYTHON_JULIACALL_TRACE_COMPILE=` | Print precompile statements. | | `-X juliacall-trace-compile-timing` | `PYTHON_JULIACALL_TRACE_COMPILE_TIMING=` | Include timings with precompile statements. | diff --git a/pysrc/juliacall/__init__.py b/pysrc/juliacall/__init__.py index bdd25c4f..b1b32206 100644 --- a/pysrc/juliacall/__init__.py +++ b/pysrc/juliacall/__init__.py @@ -205,10 +205,19 @@ def args_from_config(config): exepath = CONFIG['exepath'] project = CONFIG['project'] - # Find the Julia library - cmd = [exepath, '--project='+project, '--startup-file=no', '-O0', '--compile=min', - '-e', 'import Libdl; print(abspath(Libdl.dlpath("libjulia")), "\\0", Sys.BINDIR)'] - libpath, default_bindir = subprocess.run(cmd, check=True, capture_output=True, encoding='utf8').stdout.split('\0') + # Find the Julia library. + # + # This normally starts a short-lived Julia process just to print libjulia's + # path and Sys.BINDIR. In deployment scenarios (e.g. a pre-built container + # or system image) these are static and known ahead of time, so they may be + # supplied directly via the `libpath` / `default_bindir` options to skip the + # extra process. Behaviour is unchanged unless both are set. + libpath = path_option('libpath', check_exists=True)[0] + default_bindir = path_option('default_bindir', check_exists=True)[0] + if libpath is None or default_bindir is None: + cmd = [exepath, '--project='+project, '--startup-file=no', '-O0', '--compile=min', + '-e', 'import Libdl; print(abspath(Libdl.dlpath("libjulia")), "\\0", Sys.BINDIR)'] + libpath, default_bindir = subprocess.run(cmd, check=True, capture_output=True, encoding='utf8').stdout.split('\0') assert os.path.exists(libpath) assert os.path.exists(default_bindir) CONFIG['libpath'] = libpath From 715c859a1e83f25003ebe6797f46588f4a1c3453 Mon Sep 17 00:00:00 2001 From: Christopher Rowley Date: Sun, 24 May 2026 15:27:51 +0100 Subject: [PATCH 2/3] change to just one new option (lib) with sensible discovery/defaults of libpath, exepath and bindir depending on if others are set --- CHANGELOG.md | 5 +--- docs/src/juliacall.md | 3 +-- pysrc/juliacall/__init__.py | 48 ++++++++++++++++++------------------- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73edc266..77f4bea1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,7 @@ # Changelog ## Unreleased -* JuliaCall: added `libpath` / `default_bindir` options - (`PYTHON_JULIACALL_LIBPATH` / `PYTHON_JULIACALL_DEFAULT_BINDIR`) to supply - libjulia's path and `Sys.BINDIR` directly, skipping the discovery subprocess - at startup. Behaviour is unchanged unless both are set. +* Added option `lib` to JuliaCall. Setting this will skip the discovery subprocess. ## 0.9.34 (2026-05-18) * Bug fixes. diff --git a/docs/src/juliacall.md b/docs/src/juliacall.md index 6f31ea5d..55ad5868 100644 --- a/docs/src/juliacall.md +++ b/docs/src/juliacall.md @@ -142,8 +142,7 @@ be configured in two ways: | `-X juliacall-heap-size-hint=` | `PYTHON_JULIACALL_HEAP_SIZE_HINT=` | Hint for initial heap size in bytes. | | `-X juliacall-exe=` | `PYTHON_JULIACALL_EXE=` | Path to Julia binary to use (overrides JuliaPkg). | | `-X juliacall-project=` | `PYTHON_JULIACALL_PROJECT=` | Path to the Julia project to use (overrides JuliaPkg). | -| `-X juliacall-libpath=` | `PYTHON_JULIACALL_LIBPATH=` | Path to libjulia. If set together with `default-bindir`, skips the subprocess that discovers it. | -| `-X juliacall-default-bindir=` | `PYTHON_JULIACALL_DEFAULT_BINDIR=` | Julia's `Sys.BINDIR`. If set together with `libpath`, skips the subprocess that discovers it. | +| `-X juliacall-lib=` | `PYTHON_JULIACALL_LIB=` | Path to libjulia. Set to skip the relatively slow discovery process. | | `-X juliacall-trace-compile=` | `PYTHON_JULIACALL_TRACE_COMPILE=` | Print precompile statements. | | `-X juliacall-trace-compile-timing` | `PYTHON_JULIACALL_TRACE_COMPILE_TIMING=` | Include timings with precompile statements. | diff --git a/pysrc/juliacall/__init__.py b/pysrc/juliacall/__init__.py index b1b32206..ed06ab0a 100644 --- a/pysrc/juliacall/__init__.py +++ b/pysrc/juliacall/__init__.py @@ -179,48 +179,46 @@ def args_from_config(config): CONFIG['opt_handle_signals'] = choice('handle_signals', ['yes', 'no'])[0] CONFIG['opt_startup_file'] = choice('startup_file', ['yes', 'no'])[0] CONFIG['opt_heap_size_hint'] = option('heap_size_hint')[0] - CONFIG['project'] = path_option('project', check_exists=True)[0] - CONFIG['exepath'] = executable_option('exe')[0] + CONFIG['project'] = project = path_option('project', check_exists=True)[0] + CONFIG['libpath'] = libpath = path_option('lib', check_exists=True)[0] + CONFIG['exepath'] = exepath = executable_option('exe')[0] # Stop if we already initialised if CONFIG['inited']: return - have_exepath = CONFIG['exepath'] is not None - have_project = CONFIG['project'] is not None - if have_exepath and have_project: + if (exepath is None) and (bindir is not None): + # if bindir is set then set exepath={bindir}/julia + CONFIG['exepath'] = exepath = os.path.join(bindir, 'julia.exe' if os.name == 'nt' else 'julia') + if (exepath is not None) and (project is not None): pass - elif (not have_exepath) and (not have_project): + elif (exepath is None) and (project is None): # we don't import this at the top level because it is not required when # juliacall is loaded by PythonCall and won't be available, or if both # `exepath` and `project` are set by the user. import juliapkg # Find the Julia executable and project - CONFIG['exepath'] = juliapkg.executable() - CONFIG['project'] = juliapkg.project() + CONFIG['exepath'] = exepath = juliapkg.executable() + CONFIG['project'] = project = juliapkg.project() else: raise Exception("Both PYTHON_JULIACALL_PROJECT and PYTHON_JULIACALL_EXE must be set together, not only one of them.") + if (libpath is not None) and (exepath is None): + raise Exception("PYTHON_JULIACALL_EXE is required if PYTHON_JULIACALL_LIB is set.") - exepath = CONFIG['exepath'] - project = CONFIG['project'] - - # Find the Julia library. - # - # This normally starts a short-lived Julia process just to print libjulia's - # path and Sys.BINDIR. In deployment scenarios (e.g. a pre-built container - # or system image) these are static and known ahead of time, so they may be - # supplied directly via the `libpath` / `default_bindir` options to skip the - # extra process. Behaviour is unchanged unless both are set. - libpath = path_option('libpath', check_exists=True)[0] - default_bindir = path_option('default_bindir', check_exists=True)[0] - if libpath is None or default_bindir is None: + # Find the Julia library, if not specified. + if libpath is None: cmd = [exepath, '--project='+project, '--startup-file=no', '-O0', '--compile=min', '-e', 'import Libdl; print(abspath(Libdl.dlpath("libjulia")), "\\0", Sys.BINDIR)'] - libpath, default_bindir = subprocess.run(cmd, check=True, capture_output=True, encoding='utf8').stdout.split('\0') + found_libpath, found_bindir = subprocess.run(cmd, check=True, capture_output=True, encoding='utf8').stdout.split('\0') + if libpath is None: + CONFIG['libpath'] = libpath = found_libpath + if bindir is None: + CONFIG['bindir'] = bindir = found_bindir + if bindir is None: + bindir = os.path.dirname(exepath) assert os.path.exists(libpath) - assert os.path.exists(default_bindir) - CONFIG['libpath'] = libpath + assert os.path.exists(bindir) # Add the Julia library directory to the PATH on Windows so Julia's system libraries can # be found. They are normally found because they are in the same directory as julia.exe, @@ -251,7 +249,7 @@ def args_from_config(config): jl_init.argtypes = [c.c_char_p, c.c_char_p] jl_init.restype = None jl_init( - (default_bindir if bindir is None else bindir).encode('utf8'), + None if bindir is None else bindir.encode('utf8'), None if sysimg is None else sysimg.encode('utf8'), ) From a8d80eeb3d03733e2f6de0f8006fb117bfd58eff Mon Sep 17 00:00:00 2001 From: Christopher Rowley Date: Sun, 24 May 2026 15:41:12 +0100 Subject: [PATCH 3/3] remove 'if' that is always true --- pysrc/juliacall/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pysrc/juliacall/__init__.py b/pysrc/juliacall/__init__.py index ed06ab0a..33991fae 100644 --- a/pysrc/juliacall/__init__.py +++ b/pysrc/juliacall/__init__.py @@ -210,9 +210,8 @@ def args_from_config(config): if libpath is None: cmd = [exepath, '--project='+project, '--startup-file=no', '-O0', '--compile=min', '-e', 'import Libdl; print(abspath(Libdl.dlpath("libjulia")), "\\0", Sys.BINDIR)'] - found_libpath, found_bindir = subprocess.run(cmd, check=True, capture_output=True, encoding='utf8').stdout.split('\0') - if libpath is None: - CONFIG['libpath'] = libpath = found_libpath + libpath, found_bindir = subprocess.run(cmd, check=True, capture_output=True, encoding='utf8').stdout.split('\0') + CONFIG['libpath'] = libpath if bindir is None: CONFIG['bindir'] = bindir = found_bindir if bindir is None: