Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,41 @@ jobs:
uses: codecov/codecov-action@v7
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

embedded:
name: Embedded (${{ matrix.os }}, py ${{ matrix.pair.pyversion }}, julia ${{ matrix.pair.julia }}, py-tls-context-first ${{ matrix.sslfirst }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
pair:
- {pyversion: "3.10", julia: "~1.12"}
- {pyversion: "3.14", julia: "~1.10"}
sslfirst: ["1", "0"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.pair.pyversion }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.pair.pyversion }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e . juliacall
- name: Remove Chocolatey Julia shim from PATH
if: runner.os == 'Windows'
shell: pwsh
run: |
$chocolateyBin = [IO.Path]::TrimEndingDirectorySeparator(
[IO.Path]::GetFullPath('C:\ProgramData\Chocolatey\bin'))
$pathEntries = $env:PATH -split [IO.Path]::PathSeparator | Where-Object {
[IO.Path]::TrimEndingDirectorySeparator([IO.Path]::GetFullPath($_)) -ine $chocolateyBin
}
"PATH=$($pathEntries -join [IO.Path]::PathSeparator)" >> $env:GITHUB_ENV
- name: Embedded session with mismatched OpenSSL
run: python test/embedded_openssl.py
env:
JULIAPKG_TEST_JULIA: ${{ matrix.pair.julia }}
JULIAPKG_TEST_SSL_FIRST: ${{ matrix.sslfirst }}
PYTHONFAULTHANDLER: "1"
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## Unreleased
* Julia's bundled OpenSSL is given private library names after installation, and its
stdlib `OpenSSL_jll` is pointed at those names, so a libcrypto already loaded by CPython
can no longer be substituted for it. Python's OpenSSL version therefore no longer
restricts which Julia may be used, and the `<=python` bound on `OpenSSL_jll` is dropped
from Julia 1.12 on, where it is a stdlib and cannot be pinned by Pkg. Only installations
juliapkg created are renamed. Where the Julia that would collide was found on the system,
one is installed and renamed instead; where even that is not possible, the previous
restriction to Julia <1.12 still applies. Projects resolved by an earlier version resolve
once more, so that their installed Julia is renamed too.

## v0.1.26 (2026-08-14)
* Add `julia_args` argument to `resolve()`.

Expand Down
111 changes: 102 additions & 9 deletions src/juliapkg/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import tomlkit
from filelock import FileLock

from . import openssl
from .compat import Compat, Version
from .find_julia import find_julia, julia_version
from .install_julia import log, log_script
Expand All @@ -27,8 +28,10 @@
# 4 - changed from timestamp/sys_path to deps_files tracking
# 5 - added hash_sha256 to deps_files for content verification
# 6 - added libjulia path to meta
# 8 - Julia's OpenSSL is renamed after installation, so a project resolved by an earlier
# version has an install that was never renamed and a cap that no longer applies
# increment whenever the format changes
META_VERSION = 7
META_VERSION = 8


def load_meta():
Expand Down Expand Up @@ -347,6 +350,8 @@ def find_requirements():

compats = {}
all_deps = {}
python_openssl_bounds = set()
python_openssl_compat = None
for fn in deps_files():
log("Found dependencies: {}".format(fn))
with open(fn) as fp:
Expand All @@ -360,16 +365,15 @@ def find_requirements():
os.path.normpath(os.path.join(os.path.dirname(fn), v))
)
dep.setdefault(k, {})[fn] = v
# special handling of `verion = "<=python"` for `OpenSSL_jll
if (
name == "OpenSSL_jll"
and dep.get("uuid").get(fn) == "458c3c95-2e84-50aa-8efc-19380b2a3a95"
and dep.get("version").get(fn) == "<=python"
and dep.get("uuid", {}).get(fn) == _OPENSSL_JLL_UUID
and dep.get("version", {}).get(fn) == "<=python"
):
oc, jc = openssl_compat()
dep["version"][fn] = oc
if jc is not None:
compats[fn + " (OpenSSL_jll)"] = Compat.parse(jc)
python_openssl_bounds.add(fn)
if python_openssl_compat is None:
python_openssl_compat = openssl_compat()[0]
dep["version"][fn] = python_openssl_compat
c = deps.get("julia")
if c is not None:
compats[fn] = Compat.parse(c)
Expand Down Expand Up @@ -449,6 +453,7 @@ def merge_preferences(dep, kfvs, k):
deps = []
for name, kfvs in all_deps.items():
kw = {"name": name}
version_files = set(kfvs.get("version", {}))
merge_unique(kw, kfvs, "uuid")
merge_unique(kw, kfvs, "path")
merge_unique(kw, kfvs, "subdir")
Expand All @@ -457,7 +462,14 @@ def merge_preferences(dep, kfvs, k):
merge_compat(kw, kfvs, "version")
merge_any(kw, kfvs, "dev")
merge_preferences(kw, kfvs, "preferences")
deps.append(PkgSpec(**kw))
pkg = PkgSpec(**kw)
pkg._openssl_python_bound = (
name == "OpenSSL_jll"
and pkg.uuid == _OPENSSL_JLL_UUID
and bool(version_files)
and version_files <= python_openssl_bounds
)
deps.append(pkg)
# julia compat
compat = None
for c in compats.values():
Expand All @@ -476,6 +488,86 @@ def merge_preferences(dep, kfvs, k):
return compat, deps


_OPENSSL_JLL_UUID = "458c3c95-2e84-50aa-8efc-19380b2a3a95"


def _installed_by_juliapkg(exe):
install = os.path.realpath(STATE["install"])
exe = os.path.realpath(exe)
try:
return os.path.commonpath((install, exe)) == install
except ValueError:
return False


def _shield_julia(exe, ver, *, owned):
safe, note = openssl.shield(exe, ver, owned=owned)
log(f"Julia's OpenSSL: {note}")
return safe


def _drop_generated_openssl_bound(pkgs, ver):
for pkg in pkgs:
if getattr(pkg, "_openssl_python_bound", False):
log(f"Dropping the '<=python' bound on {pkg.name}; Julia {ver} pins it")
pkg.version = None


def _reconcile_openssl(exe, ver, compat, pkgs):
"""Use a private OpenSSL name or select a compatible Julia."""
if (ver.major, ver.minor) < (1, 12):
return exe, ver

_, python_cap = openssl_compat()
owned = _installed_by_juliapkg(exe)
safe = _shield_julia(exe, ver, owned=owned)
compatible_foreign = (
not owned
and sys.platform.startswith("linux")
and (ver.major, ver.minor) == (1, 12)
and python_cap is None
)
if safe or compatible_foreign:
_drop_generated_openssl_bound(pkgs, ver)
return exe, ver

if STATE["override_executable"]:
raise Exception(
f"juliapkg_exe={exe} selects Julia {ver}, whose OpenSSL cannot be "
"used safely in this process. Use Julia 1.11 or earlier, use Python "
"with OpenSSL 3.5 or newer, or unset juliapkg_exe so juliapkg can "
"install and protect Julia."
)

if not owned and not STATE["offline"]:
exe, ver = find_julia(
compat=compat,
prefix=STATE["install"],
install=True,
upgrade=True,
system=False,
)
if (ver.major, ver.minor) < (1, 12):
return exe, ver
owned = _installed_by_juliapkg(exe)
if _shield_julia(exe, ver, owned=owned):
_drop_generated_openssl_bound(pkgs, ver)
return exe, ver

safe_julia = Compat.parse("1 - 1.11")
log(
f"WARNING: restricting Julia to {safe_julia}, because its OpenSSL cannot be "
"given a private name"
)
compat = safe_julia if compat is None else compat & safe_julia
return find_julia(
compat=compat,
prefix=STATE["install"],
install=True,
upgrade=True,
)


def resolve(force=False, dry_run=False, update=False, julia_args=None):
"""
Resolve the dependencies.
Expand Down Expand Up @@ -531,6 +623,7 @@ def resolve(force=False, dry_run=False, update=False, julia_args=None):
exe, ver = find_julia(
compat=compat, prefix=STATE["install"], install=True, upgrade=True
)
exe, ver = _reconcile_openssl(exe, ver, compat, pkgs)
log(f"Using Julia {ver} at {exe}")
# get libjulia path
libjulia_script = [
Expand Down
8 changes: 5 additions & 3 deletions src/juliapkg/find_julia.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def julia_version(exe):
pass


def find_julia(compat=None, prefix=None, install=False, upgrade=False):
def find_julia(compat=None, prefix=None, install=False, upgrade=False, system=True):
"""Find a Julia executable compatible with compat.

Args:
Expand All @@ -30,6 +30,8 @@ def find_julia(compat=None, prefix=None, install=False, upgrade=False):
install: If True, install Julia if it is not found. This will use JuliaUp if
available, otherwise will install into the given prefix.
upgrade: If True, find the latest compatible release. Implies install=True.
system: If False, skip JuliaUp and PATH. A configured executable is checked
first and still wins; otherwise only the given prefix is used.

As a special case, upgrade=True does not apply when Julia is found in the PATH,
because if it is already installed then the user is already managing their own Julia
Expand Down Expand Up @@ -66,8 +68,8 @@ def find_julia(compat=None, prefix=None, install=False, upgrade=False):
if bestcompat is None or pr_ver in bestcompat:
return (pr_exe, pr_ver)
# see if juliaup is installed
try_jl = True
ju_exe = shutil.which("juliaup")
try_jl = system
ju_exe = shutil.which("juliaup") if system else None
if ju_exe:
ju_compat = (
Compat.parse("=" + ju_best_julia_version(compat)[0]) if upgrade else compat
Expand Down
Loading