Skip to content

Commit c0ee1ec

Browse files
authored
add pip_backend preference (#150)
* add pip_backend preference * explicit --prefix arg to uv * always print debugging info when testing * remove unneeded uv pip --prefix arg * ensure python installed for uv * add debug statements in resolve control flow --------- Co-authored-by: Christopher Doris <github.com/cjdoris>
1 parent de8aa48 commit c0ee1ec

File tree

4 files changed

+99
-29
lines changed

4 files changed

+99
-29
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## Unreleased
4+
* Add `pip_backend` preference.
5+
36
## 0.2.23 (2024-07-20)
47
* Pip packages are now installed using [`uv`](https://pypi.org/project/uv/) if it is installed.
58
* Special handling of `openssl` for compatibility with `OpenSSL_jll` if it is installed.

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ in more detail.
158158
| `offline` | `JULIA_CONDAPKG_OFFLINE` | When `true`, work in offline mode. |
159159
| `env` | `JULIA_CONDAPKG_ENV` | Path to the Conda environment to use. |
160160
| `verbosity` | `JULIA_CONDAPKG_VERBOSITY` | One of `-1`, `0`, `1` or `2`. |
161+
| `pip_backend` | `JULIA_CONDAPKG_PIP_BACKEND` | One of `pip` or `uv`. |
161162

162163
The easiest way to set these preferences is with the
163164
[`PreferenceTools`](https://github.com/cjdoris/PreferenceTools.jl)
@@ -219,6 +220,13 @@ You can control the verbosity of any `conda` or `pip` commands executed by setti
219220
- `0` is normal mode (the default).
220221
- `1`, `2`, etc. are verbose modes, useful for debugging.
221222

223+
### Pip Backends
224+
225+
You can control which package manager is used to install pip dependencies by setting the
226+
`pip_backend` preference to one of:
227+
- `pip`
228+
- `uv` (the default).
229+
222230
## Frequently Asked Questions
223231

224232
### Can I get my package to use a specific Conda environment?

src/resolve.jl

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,42 @@ function _resolve_env_is_clean(conda_env, meta)
2727
end
2828

2929
function _resolve_can_skip_1(conda_env, load_path, meta_file)
30-
isdir(conda_env) || return false
31-
isfile(meta_file) || return false
30+
if !isdir(conda_env)
31+
@debug "conda env does not exist" conda_env
32+
return false
33+
end
34+
if !isfile(meta_file)
35+
@debug "meta file does not exist" meta_file
36+
return false
37+
end
3238
meta = open(read_meta, meta_file)
33-
meta !== nothing || return false
34-
meta.version == VERSION || return false
35-
meta.load_path == load_path || return false
36-
meta.conda_env == conda_env || return false
39+
if meta === nothing
40+
@debug "meta file was not readable" meta_file
41+
return false
42+
end
43+
if meta.version != VERSION
44+
@debug "meta version has changed" meta.version VERSION
45+
return false
46+
end
47+
if meta.load_path != load_path
48+
@debug "load path has changed" meta.load_path load_path
49+
return false
50+
end
51+
if meta.conda_env != conda_env
52+
@debug "conda env has changed" meta.conda_env conda_env
53+
return false
54+
end
3755
timestamp = max(meta.timestamp, stat(meta_file).mtime)
3856
for env in [meta.load_path; meta.extra_path]
3957
dir = isfile(env) ? dirname(env) : isdir(env) ? env : continue
4058
if isdir(dir)
4159
if stat(dir).mtime > timestamp
60+
@debug "environment has changed" env dir timestamp
4261
return false
4362
else
4463
fn = joinpath(dir, "CondaPkg.toml")
4564
if isfile(fn) && stat(fn).mtime > timestamp
65+
@debug "environment has changed" env fn timestamp
4666
return false
4767
end
4868
end
@@ -358,19 +378,20 @@ function _resolve_conda_remove(io, conda_env, pkgs)
358378
nothing
359379
end
360380

361-
function _which_pip()
362-
uv = which("uv")
363-
if uv !== nothing
364-
return `$uv pip`, :uv
365-
end
366-
pip = which("pip")
367-
if pip !== nothing
368-
return `$pip`, :pip
381+
function _pip_cmd(backend::Symbol)
382+
if backend == :uv
383+
uv = which("uv")
384+
uv === nothing && error("uv not installed")
385+
return `$uv pip`
386+
else
387+
@assert backend == :pip
388+
pip = which("pip")
389+
pip === nothing && error("pip not installed")
390+
return `$pip`
369391
end
370-
error("expecting pip (or uv) to be installed")
371392
end
372393

373-
function _resolve_pip_install(io, pip_specs, load_path)
394+
function _resolve_pip_install(io, pip_specs, load_path, backend)
374395
args = String[]
375396
for spec in pip_specs
376397
if spec.binary == "only"
@@ -389,7 +410,8 @@ function _resolve_pip_install(io, pip_specs, load_path)
389410
STATE.resolved = true
390411
STATE.load_path = load_path
391412
withenv() do
392-
pip, _ = _which_pip()
413+
@debug "pip install" get(ENV, "CONDA_PREFIX", "")
414+
pip = _pip_cmd(backend)
393415
cmd = `$pip install $vrb $args`
394416
_run(io, cmd, "Installing Pip packages", flags = flags)
395417
end
@@ -400,19 +422,20 @@ function _resolve_pip_install(io, pip_specs, load_path)
400422
nothing
401423
end
402424

403-
function _resolve_pip_remove(io, pkgs, load_path)
425+
function _resolve_pip_remove(io, pkgs, load_path, backend)
404426
vrb = _verbosity_flags()
405427
flags = append!(["-y"], vrb)
406428
old_load_path = STATE.load_path
407429
try
408430
STATE.resolved = true
409431
STATE.load_path = load_path
410432
withenv() do
411-
pip, kind = _which_pip()
412-
if kind == :uv
433+
@debug "pip uninstall" get(ENV, "CONDA_PREFIX", "")
434+
pip = _pip_cmd(backend)
435+
if backend == :uv
413436
cmd = `$pip uninstall $vrb $pkgs`
414437
else
415-
cmd = `$pip uninstall $vrb -y $pkgs`
438+
cmd = `$pip uninstall -y $vrb $pkgs`
416439
end
417440
_run(io, cmd, "Removing Pip packages", flags = flags)
418441
end
@@ -468,6 +491,17 @@ function offline()
468491
getpref(Bool, "offline", "JULIA_CONDAPKG_OFFLINE", false)
469492
end
470493

494+
function _pip_backend()
495+
b = getpref(String, "pip_backend", "JULIA_CONDAPKG_PIP_BACKEND", "uv")
496+
if b == "pip"
497+
:pip
498+
elseif b == "uv"
499+
:uv
500+
else
501+
error("pip_backend must be pip or uv, got $b")
502+
end
503+
end
504+
471505
function resolve(;
472506
force::Bool = false,
473507
io::IO = stderr,
@@ -479,6 +513,7 @@ function resolve(;
479513
# if backend is Null, assume resolved
480514
back = backend()
481515
if back === :Null
516+
@debug "using the null backend"
482517
interactive && _log(io, "Using the Null backend, nothing to do")
483518
STATE.resolved = true
484519
return
@@ -487,6 +522,7 @@ function resolve(;
487522
# this is a very fast check which avoids touching the file system
488523
load_path = Base.load_path()
489524
if !force && STATE.resolved && STATE.load_path == load_path
525+
@debug "already resolved (fast path)"
490526
interactive && _log(io, "Dependencies already up to date (resolved)")
491527
return
492528
end
@@ -533,6 +569,7 @@ function resolve(;
533569
try
534570
# skip resolving if nothing has changed since the metadata was updated
535571
if !force && _resolve_can_skip_1(conda_env, load_path, meta_file)
572+
@debug "already resolved"
536573
STATE.resolved = true
537574
interactive && _log(io, "Dependencies already up to date")
538575
return
@@ -541,11 +578,30 @@ function resolve(;
541578
(packages, channels, pip_packages, extra_path) =
542579
_resolve_find_dependencies(io, load_path)
543580
# install pip if there are pip packages to install
544-
if !isempty(pip_packages) && !haskey(packages, "pip")
545-
get!(Dict{String,PkgSpec}, packages, "pip")["<internal>"] =
546-
PkgSpec("pip", version = ">=22.0.0")
547-
if !any(c.name in ("conda-forge", "anaconda") for c in channels)
548-
push!(channels, ChannelSpec("conda-forge"))
581+
pip_backend = _pip_backend()
582+
if !isempty(pip_packages)
583+
if pip_backend == :pip
584+
if !haskey(packages, "pip")
585+
if !any(c.name in ("conda-forge", "anaconda") for c in channels)
586+
push!(channels, ChannelSpec("conda-forge"))
587+
end
588+
end
589+
get!(Dict{String,PkgSpec}, packages, "pip")["<internal>"] =
590+
PkgSpec("pip", version = ">=22.0.0")
591+
else
592+
@assert pip_backend == :uv
593+
if !haskey(packages, "uv")
594+
if !any(c.name in ("conda-forge",) for c in channels)
595+
push!(channels, ChannelSpec("conda-forge"))
596+
end
597+
end
598+
get!(Dict{String,PkgSpec}, packages, "uv")["<internal>"] =
599+
PkgSpec("uv", version = ">=0.4")
600+
if !haskey(packages, "python")
601+
# uv will not detect the conda environment if python is not installed
602+
get!(Dict{String,PkgSpec}, packages, "python")["<internal>"] =
603+
PkgSpec("python")
604+
end
549605
end
550606
end
551607
# sort channels
@@ -599,7 +655,7 @@ function resolve(;
599655
if !isempty(removed_pip_pkgs) && !shared
600656
dry_run && return
601657
changed = true
602-
_resolve_pip_remove(io, removed_pip_pkgs, load_path)
658+
_resolve_pip_remove(io, removed_pip_pkgs, load_path, pip_backend)
603659
end
604660
if !isempty(removed_pkgs) && !shared
605661
dry_run && return
@@ -620,7 +676,7 @@ function resolve(;
620676
(!isempty(added_pip_pkgs) || !isempty(changed_pip_pkgs) || changed)
621677
dry_run && return
622678
changed = true
623-
_resolve_pip_install(io, pip_specs, load_path)
679+
_resolve_pip_install(io, pip_specs, load_path, pip_backend)
624680
end
625681
changed || _log(io, "Dependencies already up to date")
626682
else
@@ -639,7 +695,8 @@ function resolve(;
639695
# create conda environment
640696
_resolve_conda_install(io, conda_env, specs, channels; create = create)
641697
# install pip packages
642-
isempty(pip_specs) || _resolve_pip_install(io, pip_specs, load_path)
698+
isempty(pip_specs) ||
699+
_resolve_pip_install(io, pip_specs, load_path, pip_backend)
643700
end
644701
# save metadata
645702
meta = Meta(

test/setup.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ CondaPkg.STATE.meta_dir = ""
2020
CondaPkg.STATE.frozen = false
2121
CondaPkg.STATE.conda_env = ""
2222
CondaPkg.STATE.shared = false
23+
24+
ENV["JULIA_DEBUG"] = "CondaPkg"

0 commit comments

Comments
 (0)