Skip to content
Closed
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 docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ makedocs(
"managing-packages.md",
"environments.md",
"creating-packages.md",
"weakdeps.md",
"compatibility.md",
"registries.md",
"artifacts.md",
Expand Down
77 changes: 77 additions & 0 deletions docs/src/weakdeps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Weak dependencies

It is sometimes desirable to be able to extend some functionality of a package without having to
unconditionally take on the cost (in terms of e.g. load time) of adding an extra dependency.
A *weak* dependency is a package that is only available to load if some other package in the
current environment has that package as a normal (or strong) dependency.

Weak dependencies are listed in a `Project.toml` file under the `[weakdeps]` section which can be compared to a
(strong) dependency which is under the `[deps]` section.
Compatibility on weak dependencies is specified like a normal dependency in the `[compat]` section.

A useful application of weak dependencies could be for a plotting package that should be able to plot
objects from different Julia packages. Adding all those different Julia packages as dependencies
could be expensive. Instead, these packages are added as weak dependencies so that they are available only
if there is a chance that someone might call the function with such a type.

Below is an example of how the code can be structured for a use case as outlined above.

`Project.toml`:
```toml
name = "Plotting"
version = "0.1.0"
uuid = "..."

[deps] # strong dependencies
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"

[weakdeps]
Contour = "d38c429a-6771-53c6-b99e-75d170b6e991"

[compat] # compat can also be given on weak dependencies
Colors = "0.12.8"
Contour = "0.6.2"
```

`src/Plotting.jl`:
```julia
module Plotting

using Colors

if Base.@hasdep Contour
using Contour
function plot(c::Contour.ContourCollection
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
function plot(c::Contour.ContourCollection
function plot(c::Contour.ContourCollection)

...
end
end

end # module
```

## Compatibility with older Julia versions.

It is possible to have a dependency be a weak version in Julia versions that support it and be a strong dependency in earlier
Julia versions. This is done by having the dependency as *both* a strong and weak dependency. Older Julia versions will ignore
the specification of the dependency as weak while new Julia versions will tag it as a weak dependency.

The above code would then look like this:

```julia
module Plotting

using Colors

if !isdefined(Base, :hasdep) || Base.hasdep(@__MODULE__, :Contour)
using Contour
function plot(c::Contour.ContourCollection
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
function plot(c::Contour.ContourCollection
function plot(c::Contour.ContourCollection)

...
end
end

end # module
```

where the "conditional code" is executed unconditionally on old Julia versions and based on the presence of the weak
dependency on the new Julia version. Here the functional form of `@hasdep` was used which requires a module as the first
argument.
44 changes: 39 additions & 5 deletions src/API.jl
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ 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 (:add, :up, :pin, :free, :build)) && Pkg._auto_precompile(ctx)
$(f in (:add, :up, :pin, :free, :build)) && Pkg._auto_precompile(ctx) # rm does too, but it's handled differently
$(f in (:up, :pin, :free, :rm)) && Pkg._auto_gc(ctx)
return ret
end
Expand Down Expand Up @@ -301,9 +301,39 @@ function rm(ctx::Context, pkgs::Vector{PackageSpec}; mode=PKGMODE_PROJECT, all_p
ensure_resolved(ctx, ctx.env.manifest, pkgs)

Operations.rm(ctx, pkgs; mode)

# After a `rm`, weak deps may have been removed and thus packages that were precompiled
# with them present will need re-precompiling, so precompile those.
# This autoprecomp is more targetted than after other pkg actions because `rm` is lighter-touch.
deps_that_may_need_re_precomp = identify_deps_with_inactive_weak_deps(ctx)
if !isempty(deps_that_may_need_re_precomp)
Pkg._auto_precompile(ctx, deps_that_may_need_re_precomp, already_instantiated = true)
end
return
end

# Identifies any deps that have weak deps that were strong, but removed from
# the manifest in the previous pkg action.
function identify_deps_with_inactive_weak_deps(ctx::Context)
dep_list = String[]
new_uuids = keys(ctx.env.manifest)
old_uuids = keys(ctx.env.original_manifest)
for (uuid, pkgentry) in ctx.env.manifest.deps
(isnothing(pkgentry.weakdeps) || isempty(pkgentry.weakdeps)) && continue
weak_uuids = values(pkgentry.weakdeps)
old_num_promoted = count(in(old_uuids), weak_uuids)
new_num_promoted = count(in(new_uuids), weak_uuids)

# @show pkgentry pkgentry.weakdeps old_uuids new_uuids new_num_promoted old_num_promoted
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
# @show pkgentry pkgentry.weakdeps old_uuids new_uuids new_num_promoted old_num_promoted


if new_num_promoted < old_num_promoted
push!(dep_list, pkgentry.name)
end
end
# @show dep_list
return dep_list
end

function append_all_pkgs!(pkgs, ctx, mode)
if mode == PKGMODE_PROJECT || mode == PKGMODE_COMBINED
for (name::String, uuid::UUID) in ctx.env.project.deps
Expand Down Expand Up @@ -1154,6 +1184,8 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
isempty(depsmap) && pkgerror("No direct dependencies found matching $(repr(pkgs))")
end

target = string(isempty(pkgs) ? "project" : join(pkgs, ", "), "...")

pkg_queue = Base.PkgId[]
failed_deps = Dict{Base.PkgId, String}()
skipped_deps = Base.PkgId[]
Expand Down Expand Up @@ -1195,7 +1227,7 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
wait(first_started)
(isempty(pkg_queue) || interrupted_or_done.set) && return
fancyprint && lock(print_lock) do
printpkgstyle(io, :Precompiling, "project...")
printpkgstyle(io, :Precompiling, target)
print(io, ansi_disablecursor)
end
t = Timer(0; interval=1/10)
Expand Down Expand Up @@ -1300,7 +1332,7 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
iob = IOBuffer()
name = is_direct_dep ? pkg.name : string(color_string(pkg.name, :light_black))
!fancyprint && lock(print_lock) do
isempty(pkg_queue) && printpkgstyle(io, :Precompiling, "project...")
isempty(pkg_queue) && printpkgstyle(io, :Precompiling, target)
end
push!(pkg_queue, pkg)
started[pkg] = true
Expand Down Expand Up @@ -1606,13 +1638,15 @@ end

@deprecate status(mode::PackageMode) status(mode=mode)

function status(ctx::Context, pkgs::Vector{PackageSpec}; diff::Bool=false, mode=PKGMODE_PROJECT, outdated::Bool=false, compat::Bool=false, io::IO=stdout_f(), kwargs...)
function status(ctx::Context, pkgs::Vector{PackageSpec}; diff::Bool=false, mode=PKGMODE_PROJECT, outdated::Bool=false, compat::Bool=false, weak::Bool=false, io::IO=stdout_f(), kwargs...)
if compat
diff && pkgerror("Compat status has no `diff` mode")
outdated && pkgerror("Compat status has no `outdated` mode")
weak && pkgerror("Compat status has no `weak` mode")

Operations.print_compat(ctx, pkgs; io)
else
Operations.status(ctx.env, ctx.registries, pkgs; mode, git_diff=diff, io, outdated)
Operations.status(ctx.env, ctx.registries, pkgs; mode, git_diff=diff, io, outdated, weak)
end
return nothing
end
Expand Down
Loading