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

add support for package extensions #3264

Merged
merged 27 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e0954b9
add support for glue packages
KristofferC Jul 7, 2022
2d6adbb
add some general explanation comments
IanButterworth Nov 25, 2022
83ad655
precompile glue packages too
IanButterworth Nov 25, 2022
1a69736
add support for glue packages
KristofferC Jul 7, 2022
8ae5d35
Merge pull request #3265 from IanButterworth/ib/glue_precompile
KristofferC Nov 25, 2022
041e9f0
fix order of sections in Project file
KristofferC Nov 25, 2022
f2376f8
tweak printing of glue pkgs in precompilation
KristofferC Nov 25, 2022
6eb01e1
add docs for backwards compat
KristofferC Nov 25, 2022
b591e39
add a status option for glue
KristofferC Nov 25, 2022
e23c49e
fixup precompile
KristofferC Nov 25, 2022
941998b
add tests for glue packages retrieved from a registry
KristofferC Nov 25, 2022
2bfd2ab
add small note
KristofferC Nov 25, 2022
abc9e21
Update docs/src/creating-packages.md
KristofferC Nov 25, 2022
92bed4c
Update src/API.jl
KristofferC Nov 25, 2022
49cd28a
Update src/API.jl
KristofferC Nov 25, 2022
a9e5ffa
fixup fixup_glue
KristofferC Nov 25, 2022
c52bf27
weak -> glue
KristofferC Nov 25, 2022
f1a47ed
fix tests passing when dep warnings are errors (as they are on Julia CI)
KristofferC Nov 27, 2022
169e128
gluedeps -> weakdeps
KristofferC Nov 29, 2022
fa6dbfe
ignore packages both in weakdeps and deps for backwards compat
KristofferC Nov 30, 2022
abe7000
glue -> extension
KristofferC Dec 3, 2022
5085eb7
fix REPL for extension option
Dec 6, 2022
644a176
Clarify RegistrySpec docstring (#3155)
LilithHafner Nov 25, 2022
50a0938
slightly simplify API internals (#3156)
LilithHafner Nov 25, 2022
0c2bafd
REPLMode: Remove constructor for `APIOptions`
barucden Nov 16, 2022
acefa5f
REPLMode: Remove constructor for `PackageToken`
barucden Nov 16, 2022
a2aef89
review fixes
KristofferC Dec 7, 2022
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",
"gluedeps.md",
"compatibility.md",
"registries.md",
"artifacts.md",
Expand Down
105 changes: 105 additions & 0 deletions docs/src/creating-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,111 @@ using Test
Every dependency should in general have a compatibility constraint on it.
This is an important topic so there is a separate chapter about it: [Compatibility](@ref Compatibility).

## Conditional loading of code in packages (Glue packages)

!!! note
This is a somewhat advanced section which can be skipped for people new to Julia and Julia packages.

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 *glue package* is a file that gets automatically loaded when some other set of packages are
loaded into the Julia session. This is very similar to functionality that the external package
Requires.jl used to provide, but which is now available directly through Julia.

A useful application of glue packages could be for a plotting package that should be able to plot
objects from a wide variety of different Julia packages.
Adding all those different Julia packages as dependencies
could be expensive since they would end up getting loaded even if they were never used.
Instead, these code required to plot objects for specific packages can be put into separate files
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Instead, these code required to plot objects for specific packages can be put into separate files
Instead, the code required to plot objects for specific packages can be put into separate files

(glue packages) which is only loaded when
KristofferC marked this conversation as resolved.
Show resolved Hide resolved

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 = "..."

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

[gluepkgs]
# name of glue package to the left
# glue dependencies required to load the glue pkg to the right
# use a list for multiple glue dependencies
GlueContour = "Contour"

[compat] # compat can also be given on glue dependencies
Contour = "0.6.2"
```

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

function plot(x::Vector)
# Some functionality for plotting a vector here
end

end # module
```

`glue/GlueContour.jl` (can also be in `glue/GlueContour/GlueContour.jl`):
```julia
module GlueContour # Should be same name as the file (just like a normal package)

using Plotting, Contour

function Plotting.plot(c::Contour.ContourCollection)
# Some functionality for plotting a contour here
end

end # module
```

A user that depends on `Plotting` will not pay the cost of the "glue code" inside the `GlueContour` module.
It is only when the `Contour` package actually gets loaded that the `GlueCountour` glue package is loaded
and provide the new functionality.
KristofferC marked this conversation as resolved.
Show resolved Hide resolved

Compatibility can be set on glue dependencies just like normal dependencies and they can be used in the `test`
target to make them available when testing.

A glue package will only be loaded if the glue dependencies are loaded from the same environment or environments higher in the environment stack than the package itself.


!!! compat
In order to be compatible with earlier versions of Julia, some extra steps have to be taken.
These are: Duplicate the packages under `[gluedeps]` into `[extras]`. This is an unfortunate
duplication but without doing this the project verifier under older Julia versions will complain (error).

### Backwards compatibility with Requires.jl

Since glue packages and Requires.jl are solving the same problem,
it is useful to know how to use Requires.jl on older Julia versions while seamlessly
transitioning into using glue packages on newer Julia versions that support it.
This is done by making the following changes (using the example above):

- Add the following to the package file. This makes it so that Requires.jl loads and inserts the
callback only when glue packages are not supported
```julia
# This symbol is only defined on Julia versions that support glue packages
if isdefined(Base, :get_gluepkg)
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
if isdefined(Base, :get_gluepkg)
if !isdefined(Base, :get_gluepkg)

If I understand correctly?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, thanks :)

using Requires
function __init__()
@require Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" include("../glue/GlueContour.jl)
end
end
```

- Do the following change in the glue packages for loading the glue dependency:
```julia
isdefined(Base, :get_gluepkg) ? (using Contour) : (using ..Contour)
```

The package should now work with Requires.jl on Julia versions before glue packages were introduced
and with glue packages afterward.

## Package naming guidelines

Package names should be sensible to most Julia users, *even to those who are not domain experts*.
Expand Down
68 changes: 53 additions & 15 deletions src/API.jl
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,11 @@ function rm(ctx::Context, pkgs::Vector{PackageSpec}; mode=PKGMODE_PROJECT, all_p
ensure_resolved(ctx, ctx.env.manifest, pkgs)

Operations.rm(ctx, pkgs; mode)

return
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 @@ -1094,15 +1096,41 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
for (name, uuid) in ctx.env.project.deps if !Base.in_sysimage(Base.PkgId(uuid, name))
]

man = ctx.env.manifest
deps_pair_or_nothing = Iterators.map(man) do dep
gluepkgs = Dict{Base.PkgId, String}() # gluepkg -> parent
# make a flat map of each dep and its deps
depsmap = Dict{Base.PkgId, Vector{Base.PkgId}}()
pkg_specs = PackageSpec[]
for dep in ctx.env.manifest
pkg = Base.PkgId(first(dep), last(dep).name)
Base.in_sysimage(pkg) && return nothing
Base.in_sysimage(pkg) && continue
deps = [Base.PkgId(last(x), first(x)) for x in last(dep).deps]
return pkg => filter!(!Base.in_sysimage, deps)
depsmap[pkg] = filter!(!Base.in_sysimage, deps)
# add any glue packages
gluedeps = last(dep).gluedeps
for (gluepkg_name, gluedep_names) in last(dep).gluepkgs
gluepkg_deps = Base.PkgId[]
push!(gluepkg_deps, pkg) # depends on parent package
all_gluedeps_available = true
gluedep_names = gluedep_names isa String ? String[gluedep_names] : gluedep_names
for gluedep_name in gluedep_names
gluedep_uuid = gluedeps[gluedep_name]
if gluedep_uuid in keys(ctx.env.manifest.deps)
push!(gluepkg_deps, Base.PkgId(gluedep_uuid, gluedep_name))
else
all_gluedeps_available = false
break
end
end
all_gluedeps_available || continue
gluepkg_uuid = Base.uuid5(pkg.uuid, gluepkg_name)
gluepkg = Base.PkgId(gluepkg_uuid, gluepkg_name)
push!(pkg_specs, PackageSpec(uuid = gluepkg_uuid, name = gluepkg_name)) # create this here as the name cannot be looked up easily later via the uuid
depsmap[gluepkg] = filter!(!Base.in_sysimage, gluepkg_deps)
gluepkgs[gluepkg] = pkg.name
end
end
depsmap = Dict{Base.PkgId, Vector{Base.PkgId}}(Iterators.filter(!isnothing, deps_pair_or_nothing)) #flat map of each dep and its deps

# if the active environment is a package, add that
ctx_env_pkg = ctx.env.pkg
if ctx_env_pkg !== nothing && isfile( joinpath( dirname(ctx.env.project_file), "src", "$(ctx_env_pkg.name).jl") )
depsmap[Base.PkgId(ctx_env_pkg.uuid, ctx_env_pkg.name)] = [
Expand All @@ -1112,21 +1140,25 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
push!(direct_deps, Base.PkgId(ctx_env_pkg.uuid, ctx_env_pkg.name))
end

# return early if no deps
isempty(depsmap) && return

# initialize signalling
started = Dict{Base.PkgId,Bool}()
was_processed = Dict{Base.PkgId,Base.Event}()
was_recompiled = Dict{Base.PkgId,Bool}()
pkg_specs = PackageSpec[]
for pkgid in keys(depsmap)
started[pkgid] = false
was_processed[pkgid] = Base.Event()
was_recompiled[pkgid] = false
push!(pkg_specs, get_or_make_pkgspec(pkg_specs, ctx, pkgid.uuid))
end

# remove packages that are suspended because they errored before
# note that when `Pkg.precompile` is manually called, all suspended packages are unsuspended
precomp_prune_suspended!(pkg_specs)

# guarding against circular deps
# find and guard against circular deps
circular_deps = Base.PkgId[]
function in_deps(_pkgs, deps, dmap)
isempty(deps) && return false
Expand All @@ -1143,8 +1175,8 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
@warn """Circular dependency detected. Precompilation will be skipped for:\n $(join(string.(circular_deps), "\n "))"""
end

# if a list of packages is given, restrict to dependencies of given packages
if !isempty(pkgs)
# if a list of packages is given, restrict to dependencies of given packages
function collect_all_deps(depsmap, dep, alldeps=Base.PkgId[])
append!(alldeps, depsmap[dep])
for _dep in depsmap[dep]
Expand All @@ -1162,6 +1194,7 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
filter!(d->in(first(d), keep), depsmap)
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}()
Expand Down Expand Up @@ -1199,7 +1232,8 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
end
end

t_print = @async begin # fancy print loop
## fancy print loop
t_print = @async begin
try
wait(first_started)
(isempty(pkg_queue) || interrupted_or_done.set) && return
Expand Down Expand Up @@ -1234,7 +1268,8 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
final_loop || print(iostr, sprint(io -> show_progress(io, bar; termwidth = displaysize(ctx.io)[2]); context=io), "\n")
for dep in pkg_queue_show
loaded = warn_loaded && haskey(Base.loaded_modules, dep)
name = dep in direct_deps ? dep.name : string(color_string(dep.name, :light_black))
_name = haskey(gluepkgs, dep) ? string(gluepkgs[dep], " → ", dep.name) : dep.name
name = dep in direct_deps ? _name : string(color_string(_name, :light_black))
if dep in precomperr_deps
print(iostr, color_string(" ? ", Base.warn_color()), name, "\n")
elseif haskey(failed_deps, dep)
Expand Down Expand Up @@ -1274,7 +1309,8 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
end
tasks = Task[]
Base.LOADING_CACHE[] = Base.LoadingCache()
for (pkg, deps) in depsmap # precompilation loop
## precompilation loop
for (pkg, deps) in depsmap
paths = Base.find_all_in_cache_path(pkg)
sourcepath = Base.locate_package(pkg)
if sourcepath === nothing
Expand Down Expand Up @@ -1307,9 +1343,10 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
Base.acquire(parallel_limiter)
is_direct_dep = pkg in direct_deps
iob = IOBuffer()
name = is_direct_dep ? pkg.name : string(color_string(pkg.name, :light_black))
_name = haskey(gluepkgs, pkg) ? string(gluepkgs[pkg], " → ", pkg.name) : pkg.name
name = is_direct_dep ? _name : string(color_string(_name, :light_black))
!fancyprint && lock(print_lock) do
isempty(pkg_queue) && printpkgstyle(io, :Precompiling, "environment...")
isempty(pkg_queue) && printpkgstyle(io, :Precompiling, target)
end
push!(pkg_queue, pkg)
started[pkg] = true
Expand Down Expand Up @@ -1609,13 +1646,14 @@ 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, glue::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")
glue && pkgerror("Compat status has no `glue` 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, glue)
end
return nothing
end
Expand Down
Loading