365 lines
11 KiB
Julia
365 lines
11 KiB
Julia
# This file is a part of Julia. License is MIT: https://julialang.org/license
|
|
|
|
module PkgToMaxSumInterface
|
|
|
|
using ...Types, ...Query, ..VersionWeights
|
|
|
|
export Interface, compute_output_dict, greedysolver,
|
|
verify_solution, enforce_optimality!
|
|
|
|
# A collection of objects which allow interfacing external (Pkg) and
|
|
# internal (MaxSum) representation
|
|
mutable struct Interface
|
|
# requirements and dependencies, in external representation
|
|
reqs::Requires
|
|
deps::Dict{String,Dict{VersionNumber,Available}}
|
|
|
|
# packages list
|
|
pkgs::Vector{String}
|
|
|
|
# number of packages
|
|
np::Int
|
|
|
|
# states per package: one per version + uninstalled
|
|
spp::Vector{Int}
|
|
|
|
# pakage dict: associates an index to each package name
|
|
pdict::Dict{String,Int}
|
|
|
|
# package versions: for each package, keep the list of the
|
|
# possible version numbers; this defines a
|
|
# mapping from version numbers of a package
|
|
# to indices
|
|
pvers::Vector{Vector{VersionNumber}}
|
|
|
|
# versions dict: associates a version index to each package
|
|
# version; such that
|
|
# pvers[p0][vdict[p0][vn]] = vn
|
|
vdict::Vector{Dict{VersionNumber,Int}}
|
|
|
|
# version weights: the weight for each version of each package
|
|
# (versions include the uninstalled state; the
|
|
# higher the weight, the more favored the version)
|
|
vweight::Vector{Vector{VersionWeight}}
|
|
|
|
function Interface(reqs::Requires, deps::Dict{String,Dict{VersionNumber,Available}})
|
|
# generate pkgs
|
|
pkgs = sort!(String[keys(deps)...])
|
|
|
|
np = length(pkgs)
|
|
|
|
# generate pdict
|
|
pdict = Dict{String,Int}(pkgs[i] => i for i = 1:np)
|
|
|
|
# generate spp and pvers
|
|
spp = Vector{Int}(np)
|
|
|
|
pvers = [VersionNumber[] for i = 1:np]
|
|
|
|
for (p,depsp) in deps, vn in keys(depsp)
|
|
p0 = pdict[p]
|
|
push!(pvers[p0], vn)
|
|
end
|
|
for p0 = 1:np
|
|
sort!(pvers[p0])
|
|
spp[p0] = length(pvers[p0]) + 1
|
|
end
|
|
|
|
# generate vdict
|
|
vdict = [Dict{VersionNumber,Int}() for p0 = 1:np]
|
|
for (p,depsp) in deps
|
|
p0 = pdict[p]
|
|
vdict0 = vdict[p0]
|
|
pvers0 = pvers[p0]
|
|
for vn in keys(depsp)
|
|
for v0 in 1:length(pvers0)
|
|
if pvers0[v0] == vn
|
|
vdict0[vn] = v0
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
## generate wveights:
|
|
vweight = Vector{Vector{VersionWeight}}(np)
|
|
for p0 = 1:np
|
|
pvers0 = pvers[p0]
|
|
spp0 = spp[p0]
|
|
vweight0 = vweight[p0] = Vector{VersionWeight}(spp0)
|
|
for v0 = 1:spp0-1
|
|
vweight0[v0] = VersionWeight(pvers0[v0])
|
|
end
|
|
vweight0[spp0] = VersionWeight(v"0") # last version means uninstalled
|
|
end
|
|
|
|
return new(reqs, deps, pkgs, np, spp, pdict, pvers, vdict, vweight)
|
|
end
|
|
end
|
|
|
|
# The output format is a Dict which associates a VersionNumber to each installed package name
|
|
function compute_output_dict(sol::Vector{Int}, interface::Interface)
|
|
pkgs = interface.pkgs
|
|
np = interface.np
|
|
pvers = interface.pvers
|
|
spp = interface.spp
|
|
|
|
want = Dict{String,VersionNumber}()
|
|
for p0 = 1:np
|
|
p = pkgs[p0]
|
|
s = sol[p0]
|
|
if s != spp[p0]
|
|
v = pvers[p0][s]
|
|
want[p] = v
|
|
end
|
|
end
|
|
|
|
return want
|
|
end
|
|
|
|
# Produce a trivial solution: try to maximize each version;
|
|
# bail out as soon as some non-trivial requirements are detected.
|
|
function greedysolver(interface::Interface)
|
|
reqs = interface.reqs
|
|
deps = interface.deps
|
|
spp = interface.spp
|
|
pdict = interface.pdict
|
|
pvers = interface.pvers
|
|
np = interface.np
|
|
|
|
# initialize solution: all uninstalled
|
|
sol = [spp[p0] for p0 = 1:np]
|
|
|
|
# set up required packages to their highest allowed versions
|
|
for (rp,rvs) in reqs
|
|
rp0 = pdict[rp]
|
|
# look for the highest version which satisfies the requirements
|
|
rv = spp[rp0] - 1
|
|
while rv > 0
|
|
rvn = pvers[rp0][rv]
|
|
rvn ∈ rvs && break
|
|
rv -= 1
|
|
end
|
|
@assert rv > 0
|
|
sol[rp0] = rv
|
|
end
|
|
|
|
# we start from required packages and explore the graph
|
|
# following dependencies
|
|
staged = Set{String}(keys(reqs))
|
|
seen = copy(staged)
|
|
|
|
while !isempty(staged)
|
|
staged_next = Set{String}()
|
|
for p in staged
|
|
p0 = pdict[p]
|
|
@assert sol[p0] < spp[p0]
|
|
vn = pvers[p0][sol[p0]]
|
|
a = deps[p][vn]
|
|
|
|
# scan dependencies
|
|
for (rp,rvs) in a.requires
|
|
rp0 = pdict[rp]
|
|
# look for the highest version which satisfies the requirements
|
|
rv = spp[rp0] - 1
|
|
while rv > 0
|
|
rvn = pvers[rp0][rv]
|
|
rvn ∈ rvs && break
|
|
rv -= 1
|
|
end
|
|
# if we found a version, and the package was uninstalled
|
|
# or the same version was already selected, we're ok;
|
|
# otherwise we can't be sure what the optimal configuration is
|
|
# and we bail out
|
|
if rv > 0 && (sol[rp0] == spp[rp0] || sol[rp0] == rv)
|
|
sol[rp0] = rv
|
|
else
|
|
return (false, Int[])
|
|
end
|
|
|
|
rp ∈ seen || push!(staged_next, rp)
|
|
end
|
|
end
|
|
union!(seen, staged_next)
|
|
staged = staged_next
|
|
end
|
|
|
|
@assert verify_solution(sol, interface)
|
|
|
|
return true, sol
|
|
end
|
|
|
|
# verifies that the solution fulfills all hard constraints
|
|
# (requirements and dependencies)
|
|
function verify_solution(sol::Vector{Int}, interface::Interface)
|
|
reqs = interface.reqs
|
|
deps = interface.deps
|
|
spp = interface.spp
|
|
pdict = interface.pdict
|
|
pvers = interface.pvers
|
|
vdict = interface.vdict
|
|
|
|
# verify requirements
|
|
for (p,vs) in reqs
|
|
p0 = pdict[p]
|
|
sol[p0] != spp[p0] || return false
|
|
vn = pvers[p0][sol[p0]]
|
|
vn ∈ vs || return false
|
|
end
|
|
|
|
# verify dependencies
|
|
for (p,d) in deps
|
|
p0 = pdict[p]
|
|
vdict0 = vdict[p0]
|
|
for (vn,a) in d
|
|
v0 = vdict0[vn]
|
|
if sol[p0] == v0
|
|
for (rp, rvs) in a.requires
|
|
p1 = pdict[rp]
|
|
if sol[p1] == spp[p1]
|
|
println("""
|
|
VERIFICATION ERROR: REQUIRED DEPENDENCY NOT INSTALLED
|
|
package p=$p (p0=$p0) version=$vn (v0=$v0) requires package rp=$rp in version set rvs=$rvs
|
|
but package $rp is not being installed (p1=$p1 sol[p1]=$(sol[p1]) == spp[p1]=$(spp[p1]))
|
|
""")
|
|
return false
|
|
end
|
|
vn1 = pvers[p1][sol[p1]]
|
|
if vn1 ∉ rvs
|
|
println("""
|
|
VERIFICATION ERROR: INVALID VERSION
|
|
package p=$p (p0=$p0) version=$vn (v0=$v0) requires package rp=$rp in version set rvs=$rvs
|
|
but package $rp version is being set to $vn1 (p1=$p1 sol[p1]=$(sol[p1]) spp[p1]=$(spp[p1]))
|
|
""")
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
# Push the given solution to a local optimium if needed
|
|
function enforce_optimality!(sol::Vector{Int}, interface::Interface)
|
|
np = interface.np
|
|
|
|
reqs = interface.reqs
|
|
deps = interface.deps
|
|
pkgs = interface.pkgs
|
|
spp = interface.spp
|
|
pdict = interface.pdict
|
|
pvers = interface.pvers
|
|
vdict = interface.vdict
|
|
|
|
# prepare some useful structures
|
|
# pdeps[p0][v0] has all dependencies of package p0 version v0
|
|
pdeps = [Vector{Requires}(spp[p0]-1) for p0 = 1:np]
|
|
# prevdeps[p1][p0][v0] is the VersionSet of package p1 which package p0 version v0
|
|
# depends upon
|
|
prevdeps = [Dict{Int,Dict{Int,VersionSet}}() for p0 = 1:np]
|
|
|
|
for (p,d) in deps
|
|
p0 = pdict[p]
|
|
vdict0 = vdict[p0]
|
|
for (vn,a) in d
|
|
v0 = vdict0[vn]
|
|
pdeps[p0][v0] = a.requires
|
|
for (rp, rvs) in a.requires
|
|
p1 = pdict[rp]
|
|
if !haskey(prevdeps[p1], p0)
|
|
prevdeps[p1][p0] = Dict{Int,VersionSet}()
|
|
end
|
|
prevdeps[p1][p0][v0] = rvs
|
|
end
|
|
end
|
|
end
|
|
|
|
restart = true
|
|
while restart
|
|
restart = false
|
|
for p0 = 1:np
|
|
s0 = sol[p0]
|
|
if s0 >= spp[p0] - 1
|
|
# either the package is not installed,
|
|
# or it's already at the maximum version
|
|
continue
|
|
end
|
|
viol = false
|
|
# check if the higher version has a depencency which
|
|
# would be violated by the state of the remaining packages
|
|
for (p,vs) in pdeps[p0][s0+1]
|
|
p1 = pdict[p]
|
|
if sol[p1] == spp[p1]
|
|
# the dependency is violated because
|
|
# the other package is not being installed
|
|
viol = true
|
|
break
|
|
end
|
|
vn = pvers[p1][sol[p1]]
|
|
if vn ∉ vs
|
|
# the dependency is violated because
|
|
# the other package version is invalid
|
|
viol = true
|
|
break
|
|
end
|
|
end
|
|
viol && continue
|
|
|
|
# check if bumping the version would violate some
|
|
# dependency of another package
|
|
for (p1,d) in prevdeps[p0]
|
|
vs = get(d, sol[p1], nothing)
|
|
vs === nothing && continue
|
|
vn = pvers[p0][s0+1]
|
|
if vn ∉ vs
|
|
# bumping the version would violate
|
|
# the dependency
|
|
viol = true
|
|
break
|
|
end
|
|
end
|
|
viol && continue
|
|
# So the solution is non-optimal: we bump it manually
|
|
#warn("nonoptimal solution for package $(interface.pkgs[p0]): sol=$s0")
|
|
sol[p0] += 1
|
|
restart = true
|
|
end
|
|
end
|
|
|
|
# Finally uninstall unneeded packages:
|
|
# start from the required ones and keep only
|
|
# the packages reachable from them along the graph
|
|
uninst = trues(np)
|
|
staged = Set{String}(keys(reqs))
|
|
seen = copy(staged)
|
|
|
|
while !isempty(staged)
|
|
staged_next = Set{String}()
|
|
for p in staged
|
|
p0 = pdict[p]
|
|
uninst[p0] = false
|
|
@assert sol[p0] < spp[p0]
|
|
vn = pvers[p0][sol[p0]]
|
|
a = deps[p][vn]
|
|
|
|
# scan dependencies
|
|
for (rp,rvs) in a.requires
|
|
rp0 = pdict[rp]
|
|
@assert sol[rp0] < spp[rp0] && pvers[rp0][sol[rp0]] ∈ rvs
|
|
rp ∈ seen || push!(staged_next, rp)
|
|
end
|
|
end
|
|
union!(seen, staged_next)
|
|
staged = staged_next
|
|
end
|
|
|
|
for p0 in find(uninst)
|
|
sol[p0] = spp[p0]
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
end
|