Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial package updates #17132

Merged
merged 6 commits into from
Jul 12, 2016
Merged
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
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ Library improvements
* Package-development functions like `Pkg.tag` and `Pkg.publish`
have been moved to an external [PkgDev] package ([#13387]).

* Updating only a subset of the packages is now supported,
e.g. `Pkg.update("Example")` ([#17132])

* The `Base.Test` module now has a `@testset` feature to bundle
tests together and delay throwing an error until the end ([#13062]).

Expand Down
23 changes: 19 additions & 4 deletions base/pkg/entry.jl
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ function pin(pkg::AbstractString, ver::VersionNumber)
pin(pkg, avail[ver].sha1)
end

function update(branch::AbstractString)
function update(branch::AbstractString, upkgs::Set{String})
info("Updating METADATA...")
with(GitRepo, "METADATA") do repo
try
Expand Down Expand Up @@ -390,7 +390,14 @@ function update(branch::AbstractString)
end
end
instd = Read.installed(avail)
free = Read.free(instd)
reqs = Reqs.parse("REQUIRE")
if !isempty(upkgs)
for (pkg, (v,f)) in instd
satisfies(pkg, v, reqs) || throw(PkgError("Package $pkg: current package status does not satisfy the requirements, cannot do a partial update; use `Pkg.update()`"))
end
end
dont_update = Query.partial_update_mask(instd, avail, upkgs)
free = Read.free(instd,dont_update)
for (pkg,ver) in free
try
Cache.prefetch(pkg, Read.url(pkg), [a.sha1 for (v,a)=avail[pkg]])
Expand All @@ -400,10 +407,11 @@ function update(branch::AbstractString)
end
end
creds = LibGit2.CachedCredentials()
fixed = Read.fixed(avail,instd)
fixed = Read.fixed(avail,instd,dont_update)
stopupdate = false
for (pkg,ver) in fixed
ispath(pkg,".git") || continue
pkg in dont_update && continue
with(GitRepo, pkg) do repo
if LibGit2.isattached(repo)
if LibGit2.isdirty(repo)
Expand Down Expand Up @@ -443,7 +451,7 @@ function update(branch::AbstractString)
end
end
info("Computing changes...")
resolve(Reqs.parse("REQUIRE"), avail, instd, fixed, free)
resolve(reqs, avail, instd, fixed, free, upkgs)
# Don't use instd here since it may have changed
updatehook(sort!(collect(keys(installed()))))

Expand All @@ -459,7 +467,9 @@ function resolve(
instd :: Dict = Read.installed(avail),
fixed :: Dict = Read.fixed(avail,instd),
have :: Dict = Read.free(instd),
upkgs :: Set{String} = Set{String}()
)
orig_reqs = reqs
reqs = Query.requirements(reqs,fixed,avail)
deps, conflicts = Query.dependencies(avail,fixed)

Expand All @@ -480,6 +490,11 @@ function resolve(
deps = Query.prune_dependencies(reqs,deps)
want = Resolve.resolve(reqs,deps)

if !isempty(upkgs)
orig_deps, _ = Query.dependencies(avail)
Query.check_partial_updates(orig_reqs,orig_deps,want,fixed,upkgs)
end

# compare what is installed with what should be
changes = Query.diff(have, want, avail, fixed)
isempty(changes) && return info("No packages to install, update or remove")
Expand Down
7 changes: 5 additions & 2 deletions base/pkg/pkg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,16 @@ Pin `pkg` at registered version `version`.
pin(pkg::AbstractString, ver::VersionNumber) = cd(Entry.pin,pkg,ver)

"""
update()
update(pkgs...)

Update the metadata repo – kept in `Pkg.dir("METADATA")` – then update any fixed packages
that can safely be pulled from their origin; then call `Pkg.resolve()` to determine a new
optimal set of packages versions.

Without arguments, updates all installed packages. When one or more package names are provided as
arguments, only those packages and their dependencies are updated.
"""
update() = cd(Entry.update,Dir.getmetabranch())
update(upkgs::AbstractString...) = cd(Entry.update,Dir.getmetabranch(),Set{String}([upkgs...]))

"""
resolve()
Expand Down
88 changes: 84 additions & 4 deletions base/pkg/query.jl
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,86 @@ function dependencies(avail::Dict, fix::Dict = Dict{String,Fixed}("julia"=>Fixed
avail, conflicts
end

function partial_update_mask(instd::Dict{String,Tuple{VersionNumber,Bool}}, avail::Dict{String,Dict{VersionNumber,Available}}, upkgs::Set{String})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the Bool in this Tuple?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the fixed tag as returned by Read.installed, see here.

I tend to spell out the type signatures in full in these functions (rather than just writing instd::Dict) mostly for "documentation" purposes.

dont_update = Set{String}()
isempty(upkgs) && return dont_update
avail_new = deepcopy(avail)
for p in upkgs
haskey(instd, p) || throw(PkgError("Package $p is not installed"))
v = instd[p][1]
if haskey(avail, p)
for vn in keys(avail[p])
vn < v && delete!(avail_new[p], vn)
end
end
end
avail_new = dependencies_subset(avail_new, upkgs)

for p in keys(avail)
!haskey(avail_new, p) && push!(dont_update, p)
end
for (p,_) in instd
!haskey(avail_new, p) && !(p in upkgs) && push!(dont_update, p)
end
return dont_update
end

# Try to produce some helpful message in case of a partial update which does not go all the way
# (Does not do a full analysis, it only checks requirements and direct dependents.)
function check_partial_updates(reqs::Requires, deps::Dict{String,Dict{VersionNumber,Available}}, want::Dict{String,VersionNumber}, fixed::Dict{String,Fixed}, upkgs::Set{String})
for p in upkgs
if !haskey(want, p)
if !haskey(fixed, p)
warn("Something went wrong with the update of package $p, please submit a bug report")
continue
end
v = fixed[p].version
else
v = want[p]
if haskey(fixed, p) && v != fixed[p].version
warn("Something went wrong with the update of package $p, please submit a bug report")
continue
end
end
haskey(deps, p) || continue
vers = sort!(collect(keys(deps[p])))
higher_vers = vers[vers .> v]
isempty(higher_vers) && continue # package p has been set to the highest available version

# Determine if there are packages which depend on `p` and somehow prevent its update to
# the latest version
blocking_parents = Set{String}()
for (p1,d1) in deps
p1 in upkgs && continue # package `p1` is among the ones to be updated, skip the check
Copy link
Contributor

@tkelman tkelman Jul 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ the comments

haskey(fixed, p1) || continue # if package `p1` is not fixed, it can't be blocking
r1 = fixed[p1].requires # get `p1` requirements
haskey(r1, p) || continue # check if package `p1` requires `p`
vs1 = r1[p] # get the versions of `p` allowed by `p1` requirements
any(hv in vs1 for hv in higher_vers) && continue # package `p1` would allow some of the higher versions,
# therefore it's not responsible for blocking `p`
push!(blocking_parents, p1) # package `p1` is blocking the update of `p`
end

# Determine if the update of `p` is prevented by explicit user-provided requirements
blocking_reqs = (haskey(reqs, p) && all(hv ∉ reqs[p] for hv in higher_vers))

# Determine if the update of `p` is prevented by it being fixed (e.g. it's dirty, or pinned...)
isfixed = haskey(fixed, p)

msg = "Package $p was set to version $v, but a higher version $(vers[end]) exists.\n"
if isfixed
msg *= " The package is fixed. You can try using `Pkg.free(\"$p\")` to update it."
elseif blocking_reqs
msg *= " The update is prevented by explicit requirements constraints. Edit your REQUIRE file to change this."
elseif !isempty(blocking_parents)
msg *= string(" To install the latest version, you could try updating these packages as well: ", join(blocking_parents, ", ", " and "), ".")
else
msg *= " To install the latest version, you could try doing a full update with `Pkg.update()`."
end
info(msg)
end
end

typealias PackageState Union{Void,VersionNumber}

function diff(have::Dict, want::Dict, avail::Dict, fixed::Dict)
Expand Down Expand Up @@ -324,12 +404,12 @@ end
# Build a subgraph incuding only the (direct and indirect) dependencies
# of a given package set
function dependencies_subset(deps::Dict{String,Dict{VersionNumber,Available}}, pkgs::Set{String})
staged = pkgs
allpkgs = copy(pkgs)
staged::Set{String} = filter(p->p in keys(deps), pkgs)
allpkgs = copy(staged)
while !isempty(staged)
staged_next = Set{String}()
for p in staged, a in values(deps[p]), rp in keys(a.requires)
if !(rp in allpkgs)
for p in staged, a in values(get(deps, p, Dict{VersionNumber,Available}())), rp in keys(a.requires)
if !(rp in allpkgs) && rp ≠ "julia"
push!(staged_next, rp)
end
end
Expand Down
8 changes: 4 additions & 4 deletions base/pkg/read.jl
Original file line number Diff line number Diff line change
Expand Up @@ -216,22 +216,22 @@ function installed(avail::Dict=available())
return pkgs
end

function fixed(avail::Dict=available(), inst::Dict=installed(avail),
function fixed(avail::Dict=available(), inst::Dict=installed(avail), dont_update::Set{String}=Set{String}(),
julia_version::VersionNumber=VERSION)
pkgs = Dict{String,Fixed}()
for (pkg,(ver,fix)) in inst
fix || continue
(fix || pkg in dont_update) || continue
ap = get(avail,pkg,Dict{VersionNumber,Available}())
pkgs[pkg] = Fixed(ver,requires_dict(pkg,ap))
end
pkgs["julia"] = Fixed(julia_version)
return pkgs
end

function free(inst::Dict=installed())
function free(inst::Dict=installed(), dont_update::Set{String}=Set{String}())
pkgs = Dict{String,VersionNumber}()
for (pkg,(ver,fix)) in inst
fix && continue
(fix || pkg in dont_update) && continue
pkgs[pkg] = ver
end
return pkgs
Expand Down
9 changes: 9 additions & 0 deletions doc/manual/packages.rst
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,15 @@ A package is considered fixed if it is one of the following:
If any of these are the case, the package manager cannot freely change the installed version of the package, so its requirements must be satisfied by whatever other package versions it picks.
The combination of top-level requirements in ``~/.julia/v0.4/REQUIRE`` and the requirement of fixed packages are used to determine what should be installed.

You can also update only a subset of the installed packages, by providing arguments to the `Pkg.update` function. In that case, only the packages provided as arguments and their dependencies will be updated::

julia> Pkg.update("Example")
INFO: Updating METADATA...
INFO: Computing changes...
INFO: Upgrading Example: v0.4.0 => 0.4.1

This partial update process still computes the new set of package versions according to top-level requirements and "fixed" packages, but it additionally considers all other packages except those explicitly provided, and their dependencies, as fixed.

Checkout, Pin and Free
----------------------

Expand Down
4 changes: 3 additions & 1 deletion doc/stdlib/pkg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,14 @@ Functions for package development (e.g. ``tag``, ``publish``, etc.) have been mo

Prints out a summary of what version and state ``pkg``\ , specifically, is in.

.. function:: update()
.. function:: update(pkgs...)

.. Docstring generated from Julia source

Update the metadata repo – kept in ``Pkg.dir("METADATA")`` – then update any fixed packages that can safely be pulled from their origin; then call ``Pkg.resolve()`` to determine a new optimal set of packages versions.

Without arguments, updates all installed packages. When one or more package names are provided as arguments, only those packages and their dependencies are updated.

.. function:: checkout(pkg, [branch="master"]; merge=true, pull=true)

.. Docstring generated from Julia source
Expand Down
Loading