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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Pkg v1.13 Release Notes
improving efficiency when performing several environment changes. ([#4262])
- Added `Pkg.autoprecompilation_enabled(state::Bool)` to globally enable or disable automatic precompilation for Pkg
operations. ([#4262])
- Added `Pkg.downgrade`/`pkg> down` command to resolve packages to their lowest compatible versions ([#XXXX])
- Implemented atomic TOML writes to prevent data corruption when Pkg operations are interrupted or multiple processes
write simultaneously. All TOML files are now written atomically using temporary files and atomic moves. ([#4293])
- Implemented lazy loading for RegistryInstance to significantly improve startup performance for operations that don't
Expand Down
27 changes: 21 additions & 6 deletions src/API.jl
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ function check_readonly(ctx::Context)
end

# Provide some convenience calls
for f in (:develop, :add, :rm, :up, :pin, :free, :test, :build, :status, :why, :precompile)
for f in (:develop, :add, :rm, :up, :down, :pin, :free, :test, :build, :status, :why, :precompile)
@eval begin
$f(pkg::Union{AbstractString, PackageSpec}; kwargs...) = $f([pkg]; kwargs...)
$f(pkgs::Vector{<:AbstractString}; kwargs...) = $f([PackageSpec(pkg) for pkg in pkgs]; kwargs...)
Expand All @@ -170,8 +170,8 @@ for f in (:develop, :add, :rm, :up, :pin, :free, :test, :build, :status, :why, :
pkgs = deepcopy(pkgs) # don't mutate input
foreach(handle_package_input!, pkgs)
ret = $f(ctx, pkgs; kwargs...)
$(f in (:up, :pin, :free, :build)) && Pkg._auto_precompile(ctx)
$(f in (:up, :pin, :free, :rm)) && Pkg._auto_gc(ctx)
$(f in (:up, :down, :pin, :free, :build)) && Pkg._auto_precompile(ctx)
$(f in (:up, :down, :pin, :free, :rm)) && Pkg._auto_gc(ctx)
return ret
end
$f(ctx::Context; kwargs...) = $f(ctx, PackageSpec[]; kwargs...)
Expand All @@ -181,7 +181,7 @@ for f in (:develop, :add, :rm, :up, :pin, :free, :test, :build, :status, :why, :
url = nothing, rev = nothing, path = nothing, mode = PKGMODE_PROJECT, subdir = nothing, kwargs...
)
pkg = PackageSpec(; name, uuid, version, url, rev, path, subdir)
if $f === status || $f === rm || $f === up
if $f === status || $f === rm || $f === up || $f === down
kwargs = merge((; kwargs...), (:mode => mode,))
end
# Handle $f() case
Expand Down Expand Up @@ -436,13 +436,16 @@ function up(
preserve::Union{Nothing, PreserveLevel} = isempty(pkgs) ? nothing : PRESERVE_ALL,
update_registry::Bool = true,
skip_writing_project::Bool = false,
downgrade::Bool = false,
kwargs...
)
Context!(ctx; kwargs...)
Operations.ensure_manifest_registries!(ctx)
check_readonly(ctx)
if Operations.is_fully_pinned(ctx)
printpkgstyle(ctx.io, :Update, "All dependencies are pinned - nothing to update.", color = Base.info_color())
msg = downgrade ? "All dependencies are pinned - nothing to downgrade." : "All dependencies are pinned - nothing to update."
action = downgrade ? :Downgrade : :Update
printpkgstyle(ctx.io, action, msg, color = Base.info_color())
return
end
if update_registry
Expand All @@ -462,10 +465,22 @@ function up(
for pkg in pkgs
update_source_if_set(ctx.env, pkg)
end
Operations.up(ctx, pkgs, level; skip_writing_project, preserve)
Operations.up(ctx, pkgs, level; skip_writing_project, preserve, downgrade)
return
end

function down(
ctx::Context, pkgs::Vector{PackageSpec};
level::UpgradeLevel = UPLEVEL_MAJOR, mode::PackageMode = PKGMODE_PROJECT,
preserve::Union{Nothing, PreserveLevel} = isempty(pkgs) ? nothing : PRESERVE_ALL,
update_registry::Bool = true,
skip_writing_project::Bool = false,
kwargs...
)
# down is just up with downgrade=true
return up(ctx, pkgs; level, mode, preserve, update_registry, skip_writing_project, downgrade = true, kwargs...)
end

resolve(; io::IO = stderr_f(), kwargs...) = resolve(Context(; io); kwargs...)
function resolve(ctx::Context; skip_writing_project::Bool = false, kwargs...)
up(ctx; level = UPLEVEL_FIXED, mode = PKGMODE_MANIFEST, update_registry = false, skip_writing_project, kwargs...)
Expand Down
26 changes: 16 additions & 10 deletions src/Operations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ end
# all versioned packages should have a `tree_hash`
function resolve_versions!(
env::EnvCache, registries::Vector{Registry.RegistryInstance}, pkgs::Vector{PackageSpec}, julia_version,
installed_only::Bool
installed_only::Bool, downgrade::Bool = false
)
installed_only = installed_only || OFFLINE_MODE[]

Expand Down Expand Up @@ -763,7 +763,7 @@ function resolve_versions!(
# happened on a different julia version / commit and the stdlib version in the manifest is not the current stdlib version
unbind_stdlibs = julia_version === VERSION
reqs = Resolve.Requires(pkg.uuid => is_stdlib(pkg.uuid) && unbind_stdlibs ? VersionSpec("*") : VersionSpec(pkg.version) for pkg in pkgs)
graph, compat_map = deps_graph(env, registries, names, reqs, fixed, julia_version, installed_only)
graph, compat_map = deps_graph(env, registries, names, reqs, fixed, julia_version, installed_only, downgrade)
Resolve.simplify_graph!(graph)
vers = Resolve.resolve(graph)

Expand Down Expand Up @@ -835,7 +835,7 @@ const PKGORIGIN_HAVE_VERSION = :version in fieldnames(Base.PkgOrigin)
function deps_graph(
env::EnvCache, registries::Vector{Registry.RegistryInstance}, uuid_to_name::Dict{UUID, String},
reqs::Resolve.Requires, fixed::Dict{UUID, Resolve.Fixed}, julia_version,
installed_only::Bool
installed_only::Bool, downgrade::Bool = false
)
uuids = Set{UUID}()
union!(uuids, keys(reqs))
Expand Down Expand Up @@ -981,7 +981,7 @@ function deps_graph(
fixed = fixed_filtered
end

return Resolve.Graph(all_compat, weak_compat, uuid_to_name, reqs, fixed, false, julia_version),
return Resolve.Graph(all_compat, weak_compat, uuid_to_name, reqs, fixed, false, julia_version, downgrade),
all_compat
end

Expand Down Expand Up @@ -2324,18 +2324,17 @@ function load_manifest_deps_up(
return pkgs
end

function targeted_resolve_up(env::EnvCache, registries::Vector{Registry.RegistryInstance}, pkgs::Vector{PackageSpec}, preserve::PreserveLevel, julia_version)
function targeted_resolve_up(env::EnvCache, registries::Vector{Registry.RegistryInstance}, pkgs::Vector{PackageSpec}, preserve::PreserveLevel, julia_version, downgrade::Bool = false)
pkgs = load_manifest_deps_up(env, pkgs; preserve = preserve)
check_registered(registries, pkgs)
deps_map = resolve_versions!(env, registries, pkgs, julia_version, preserve == PRESERVE_ALL_INSTALLED)
deps_map = resolve_versions!(env, registries, pkgs, julia_version, preserve == PRESERVE_ALL_INSTALLED, downgrade)
return pkgs, deps_map
end

function up(
ctx::Context, pkgs::Vector{PackageSpec}, level::UpgradeLevel;
skip_writing_project::Bool = false, preserve::Union{Nothing, PreserveLevel} = nothing
skip_writing_project::Bool = false, preserve::Union{Nothing, PreserveLevel} = nothing, downgrade::Bool = false
)

requested_pkgs = pkgs

new_git = Set{UUID}()
Expand All @@ -2353,11 +2352,11 @@ function up(
up_load_manifest_info!(pkg, entry)
end
if preserve !== nothing
pkgs, deps_map = targeted_resolve_up(ctx.env, ctx.registries, pkgs, preserve, ctx.julia_version)
pkgs, deps_map = targeted_resolve_up(ctx.env, ctx.registries, pkgs, preserve, ctx.julia_version, downgrade)
else
pkgs = load_direct_deps(ctx.env, pkgs; preserve = (level == UPLEVEL_FIXED ? PRESERVE_NONE : PRESERVE_DIRECT))
check_registered(ctx.registries, pkgs)
deps_map = resolve_versions!(ctx.env, ctx.registries, pkgs, ctx.julia_version, false)
deps_map = resolve_versions!(ctx.env, ctx.registries, pkgs, ctx.julia_version, false, downgrade)
end
update_manifest!(ctx.env, pkgs, deps_map, ctx.julia_version, ctx.registries)
new_apply = download_source(ctx)
Expand Down Expand Up @@ -2395,6 +2394,13 @@ function up(
return build_versions(ctx, union(new_apply, new_git))
end

function down(
ctx::Context, pkgs::Vector{PackageSpec}, level::UpgradeLevel;
skip_writing_project::Bool = false, preserve::Union{Nothing, PreserveLevel} = nothing
)
return up(ctx, pkgs, level; skip_writing_project, preserve, downgrade = true)
end

function update_package_pin!(registries::Vector{Registry.RegistryInstance}, pkg::PackageSpec, entry::Union{Nothing, PackageEntry})
if entry === nothing
cmd = Pkg.in_repl_mode() ? "pkg> resolve" : "Pkg.resolve()"
Expand Down
11 changes: 10 additions & 1 deletion src/Pkg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export UpgradeLevel, UPLEVEL_MAJOR, UPLEVEL_MINOR, UPLEVEL_PATCH
export PreserveLevel, PRESERVE_TIERED_INSTALLED, PRESERVE_TIERED, PRESERVE_ALL_INSTALLED, PRESERVE_ALL, PRESERVE_DIRECT, PRESERVE_SEMVER, PRESERVE_NONE
export Registry, RegistrySpec

public activate, add, build, compat, develop, free, gc, generate, instantiate,
public activate, add, build, compat, develop, downgrade, free, gc, generate, instantiate,
pin, precompile, readonly, redo, rm, resolve, status, test, undo, update, why

depots() = Base.DEPOT_PATH
Expand Down Expand Up @@ -353,6 +353,15 @@ See also [`PackageSpec`](@ref), [`PackageMode`](@ref), [`UpgradeLevel`](@ref).
"""
const update = API.up

"""
Pkg.downgrade(; kwargs...)
Pkg.downgrade(pkg::Union{String, Vector{String}}; kwargs...)
Pkg.downgrade(pkgs::Union{PackageSpec, Vector{PackageSpec}}; kwargs...)

Same as `Pkg.update` but prefer lower versions over higher.
"""
const downgrade = API.down

"""
Pkg.test(; kwargs...)
Pkg.test(pkg::Union{String, Vector{String}; kwargs...)
Expand Down
28 changes: 28 additions & 0 deletions src/REPLMode/command_declarations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,34 @@ compound_declarations = [
After any package updates the project will be precompiled. For more information see `pkg> ?precompile`.
""",
],
PSA[
:name => "downgrade",
:short_name => "down",
:api => API.down,
:should_splat => false,
:arg_count => 0 => Inf,
:arg_parser => parse_package,
:option_spec => [
PSA[:name => "project", :short_name => "p", :api => :mode => PKGMODE_PROJECT],
PSA[:name => "manifest", :short_name => "m", :api => :mode => PKGMODE_MANIFEST],
PSA[:name => "major", :api => :level => UPLEVEL_MAJOR],
PSA[:name => "minor", :api => :level => UPLEVEL_MINOR],
PSA[:name => "patch", :api => :level => UPLEVEL_PATCH],
PSA[:name => "fixed", :api => :level => UPLEVEL_FIXED],
PSA[:name => "preserve", :takes_arg => true, :api => :preserve => do_preserve],
],
:completions => :complete_installed_packages,
:description => "downgrade packages to minimum compatible versions",
:help => md"""
[down|downgrade] [-p|--project] [opts] pkg[=uuid] [@version] ...
[down|downgrade] [-m|--manifest] [opts] pkg[=uuid] [@version] ...

opts: --major | --minor | --patch | --fixed
--preserve=<all/direct/none>

Same as `update` except prefer lower versions instead of higher.
""",
],
PSA[
:name => "generate",
:api => API.generate,
Expand Down
16 changes: 11 additions & 5 deletions src/Resolve/Resolve.jl
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ function _resolve(graph::Graph, lower_bound::Union{Vector{Int}, Nothing}, previo
np = graph.np
spp = graph.spp
gconstr = graph.gconstr
downgrade = graph.downgrade

if lower_bound ≢ nothing
for p0 in 1:np
Expand Down Expand Up @@ -134,6 +135,10 @@ function _resolve(graph::Graph, lower_bound::Union{Vector{Int}, Nothing}, previo
log_event_global!(graph, "the solver found an optimal configuration")
return sol
else
if downgrade
log_event_global!(graph, "downgrade mode: using feasible configuration without further optimization")
return sol
end
enforce_optimality!(sol, graph)
if lower_bound ≢ nothing
@assert all(sol .≥ lower_bound)
Expand Down Expand Up @@ -308,6 +313,7 @@ function greedysolver(graph::Graph)
gadj = graph.gadj
gmsk = graph.gmsk
np = graph.np
downgrade = graph.downgrade

push_snapshot!(graph)
gconstr = graph.gconstr
Expand All @@ -320,10 +326,10 @@ function greedysolver(graph::Graph)
# since it may include implicit requirements)
req_inds = Set{Int}(p0 for p0 in 1:np if !gconstr[p0][end])

# set up required packages to their highest allowed versions
# set up required packages to their highest (or lowest in downgrade mode) allowed versions
for rp0 in req_inds
# look for the highest version which satisfies the requirements
rv0 = findlast(gconstr[rp0])
# look for the highest/lowest version which satisfies the requirements
rv0 = downgrade ? findfirst(gconstr[rp0]) : findlast(gconstr[rp0])
@assert rv0 ≢ nothing && rv0 ≠ spp[rp0]
sol[rp0] = rv0
fill!(gconstr[rp0], false)
Expand Down Expand Up @@ -353,8 +359,8 @@ function greedysolver(graph::Graph)
# scan dependencies
for (j1, p1) in enumerate(gadj[p0])
msk = gmsk[p0][j1]
# look for the highest version which satisfies the requirements
v1 = findlast(msk[:, s0] .& gconstr[p1])
# look for the highest/lowest version which satisfies the requirements
v1 = downgrade ? findfirst(msk[:, s0] .& gconstr[p1]) : findlast(msk[:, s0] .& gconstr[p1])
v1 == spp[p1] && continue # p1 is not required by p0's current version
# if we found a version, and the package was uninstalled
# or the same version was already selected, we're ok;
Expand Down
36 changes: 26 additions & 10 deletions src/Resolve/graphtype.jl
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ mutable struct Graph
# number of packages (all Vectors above have this length)
np::Int

# downgrade mode: if true, prefer lower versions instead of higher
downgrade::Bool

# some workspace vectors
newmsg::Vector{FieldValue}
diff::Vector{FieldValue}
Expand All @@ -241,7 +244,8 @@ mutable struct Graph
reqs::Requires,
fixed::Dict{UUID, Fixed},
verbose::Bool = false,
julia_version::Union{VersionNumber, Nothing} = VERSION
julia_version::Union{VersionNumber, Nothing} = VERSION,
downgrade::Bool = false
)

# Tell the resolver about julia itself
Expand Down Expand Up @@ -342,7 +346,7 @@ mutable struct Graph

graph = new(
data, gadj, gmsk, gconstr, adjdict, req_inds, fix_inds, ignored, solve_stack, spp, np,
FieldValue[], FieldValue[], FieldValue[]
downgrade, FieldValue[], FieldValue[], FieldValue[]
)

_add_fixed!(graph, fixed)
Expand All @@ -366,8 +370,9 @@ mutable struct Graph
fix_inds = copy(graph.fix_inds)
ignored = copy(graph.ignored)
solve_stack = [([copy(gc0) for gc0 in sav_gconstr], copy(sav_ignored)) for (sav_gconstr, sav_ignored) in graph.solve_stack]
downgrade = graph.downgrade

return new(data, gadj, gmsk, gconstr, adjdict, req_inds, fix_inds, ignored, solve_stack, spp, np)
return new(data, gadj, gmsk, gconstr, adjdict, req_inds, fix_inds, ignored, solve_stack, spp, np, downgrade)
end
end

Expand Down Expand Up @@ -1206,6 +1211,7 @@ function validate_versions!(graph::Graph, sources::Set{Int} = Set{Int}(); skim::
id(p0::Int) = pkgID(p0, graph)

log_event_global!(graph, "validating versions [mode=$(skim ? "skim" : "deep")]")
downgrade = graph.downgrade

sumspp = sum(count(gconstr[p0]) for p0 in 1:np)

Expand All @@ -1220,7 +1226,9 @@ function validate_versions!(graph::Graph, sources::Set{Int} = Set{Int}(); skim::

gconstr0 = gconstr[p0]
old_gconstr0 = copy(gconstr0)
for v0 in reverse!(findall(gconstr0))
version_indices = findall(gconstr0)
downgrade || reverse!(version_indices) # prefer higher versions when upgrading
for v0 in version_indices
push_snapshot!(graph)
fill!(graph.gconstr[p0], false)
graph.gconstr[p0][v0] = true
Expand All @@ -1244,13 +1252,15 @@ function validate_versions!(graph::Graph, sources::Set{Int} = Set{Int}(); skim::
changed = true
unsat = !any(gconstr0)
if unsat
# we'll trigger a failure by pinning the highest version
v0 = findlast(old_gconstr0[1:(end - 1)])
# we'll trigger a failure by pinning the highest (or lowest) version
selector = downgrade ? findfirst : findlast
v0 = selector(old_gconstr0[1:(end - 1)])
@assert v0 ≢ nothing # this should be ensured by a previous pruning
# @info "pinning $(logstr(id(p0))) to version $(pvers[p0][v0])"
log_event_pin!(graph, pkgs[p0], pvers[p0][v0])
graph.gconstr[p0][v0] = true
err_msg_preamble = "Package $(logstr(id(p0))) has no possible versions; here is the log when trying to validate the highest version left until this point, $(logstr(id(p0), pvers[p0][v0]))):\n"
direction = downgrade ? "lowest" : "highest"
err_msg_preamble = "Package $(logstr(id(p0))) has no possible versions; here is the log when trying to validate the $direction version left until this point, $(logstr(id(p0), pvers[p0][v0]))):\n"
propagate_constraints!(graph, Set{Int}([p0]); err_msg_preamble)
@assert false # the above call must fail
end
Expand Down Expand Up @@ -1397,6 +1407,7 @@ function build_eq_classes_soft1!(graph::Graph, p0::Int)
gmsk = graph.gmsk
gconstr = graph.gconstr
ignored = graph.ignored
downgrade = graph.downgrade

# concatenate all the constraints; the columns of the
# result encode the behavior of each version
Expand All @@ -1417,10 +1428,15 @@ function build_eq_classes_soft1!(graph::Graph, p0::Int)
neq == eff_spp0 && return # nothing to do here

# group versions into sets that behave identically
# each set is represented by its highest-valued member
repr_vers = sort!(Int[findlast(isequal(repr_vecs[w0]), cvecs) for w0 in 1:neq])
# each set is represented by its extreme member (highest for upgrade, lowest for downgrade)
selector = downgrade ? findfirst : findlast
repr_vers = sort!(Int[selector(isequal(repr_vecs[w0]), cvecs) for w0 in 1:neq])
@assert all(>(0), repr_vers)
@assert repr_vers[end] == eff_spp0
if downgrade
@assert repr_vers[1] == 1
else
@assert repr_vers[end] == eff_spp0
end

# convert the version numbers into the original numbering
repr_vers = findall(gconstr0)[repr_vers]
Expand Down
Loading
Loading