Skip to content

Commit e0954b9

Browse files
committed
add support for glue packages
1 parent 79c0c59 commit e0954b9

File tree

22 files changed

+485
-81
lines changed

22 files changed

+485
-81
lines changed

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ makedocs(
3535
"managing-packages.md",
3636
"environments.md",
3737
"creating-packages.md",
38+
"gluedeps.md",
3839
"compatibility.md",
3940
"registries.md",
4041
"artifacts.md",

docs/src/creating-packages.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,81 @@ using Test
245245
Every dependency should in general have a compatibility constraint on it.
246246
This is an important topic so there is a separate chapter about it: [Compatibility](@ref Compatibility).
247247

248+
## Conditional loading of code in packages (Glue packages)
249+
250+
!!! note
251+
This is a somewhat advanced section which can be skipped for people new to Julia and Julia packages.
252+
253+
It is sometimes desirable to be able to extend some functionality of a package without having to
254+
unconditionally take on the cost (in terms of e.g. load time) of adding an extra dependency.
255+
A *glue package* is a file that gets automatically loaded when some other set of packages are
256+
loaded into the Julia session.
257+
258+
A useful application of glue packages could be for a plotting package that should be able to plot
259+
objects from a wide variety of different Julia packages.
260+
Adding all those different Julia packages as dependencies
261+
could be expensive since they would end up getting loaded even if they were never used.
262+
Instead, these code required to plot objects for specific packages can be put into separate files
263+
(glue packages) which is only loaded when
264+
265+
Below is an example of how the code can be structured for a use case as outlined above.
266+
267+
`Project.toml`:
268+
```toml
269+
name = "Plotting"
270+
version = "0.1.0"
271+
uuid = "..."
272+
273+
[deps]
274+
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
275+
276+
[gluedeps]
277+
Contour = "d38c429a-6771-53c6-b99e-75d170b6e991"
278+
279+
[gluepkgs]
280+
# name of glue package to the left
281+
# glue dependencies required to load the glue pkg to the right
282+
# use a list for multiple glue dependencies
283+
GlueContour = "Contour"
284+
285+
[compat] # compat can also be given on glue dependencies
286+
Colors = "0.12.8"
287+
Contour = "0.6.2"
288+
```
289+
290+
`src/Plotting.jl`:
291+
```julia
292+
module Plotting
293+
using Colors
294+
295+
function plot(x::Colors.Color)
296+
# Some functionality for plotting a color here
297+
end
298+
299+
end # module
300+
```
301+
302+
`glue/GlueContour.jl` (can also be in `glue/GlueContour/GlueContour.jl`):
303+
```julia
304+
module GlueContour
305+
306+
using Plotting, Contour
307+
308+
function Plotting.plot(c::Contour.ContourCollection)
309+
# Some functionality for plotting a contour here
310+
end
311+
312+
end # module
313+
```
314+
315+
A user that depends on `Plotting` will not pay the code of the "glue code" inside the `GlueContour` module.
316+
It is only when the `Contour` package actually gets loaded that the `GlueCountour` glue package will get loaded
317+
and provide the new functionality.
318+
319+
Compatibility can be set on glue dependencies just like normal dependencies.
320+
321+
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.
322+
248323
## Package naming guidelines
249324

250325
Package names should be sensible to most Julia users, *even to those who are not domain experts*.

src/API.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ for f in (:develop, :add, :rm, :up, :pin, :free, :test, :build, :status, :why)
154154
pkgs = deepcopy(pkgs) # don't mutate input
155155
foreach(handle_package_input!, pkgs)
156156
ret = $f(ctx, pkgs; kwargs...)
157-
$(f in (:add, :up, :pin, :free, :build)) && Pkg._auto_precompile(ctx)
157+
$(f in (:add, :up, :pin, :free, :build)) && Pkg._auto_precompile(ctx) # rm does too, but it's handled differently
158158
$(f in (:up, :pin, :free, :rm)) && Pkg._auto_gc(ctx)
159159
return ret
160160
end
@@ -301,9 +301,11 @@ function rm(ctx::Context, pkgs::Vector{PackageSpec}; mode=PKGMODE_PROJECT, all_p
301301
ensure_resolved(ctx, ctx.env.manifest, pkgs)
302302

303303
Operations.rm(ctx, pkgs; mode)
304+
304305
return
305306
end
306307

308+
307309
function append_all_pkgs!(pkgs, ctx, mode)
308310
if mode == PKGMODE_PROJECT || mode == PKGMODE_COMBINED
309311
for (name::String, uuid::UUID) in ctx.env.project.deps
@@ -1163,6 +1165,8 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
11631165
isempty(depsmap) && pkgerror("No direct dependencies found matching $(repr(pkgs))")
11641166
end
11651167

1168+
target = string(isempty(pkgs) ? "project" : join(pkgs, ", "), "...")
1169+
11661170
pkg_queue = Base.PkgId[]
11671171
failed_deps = Dict{Base.PkgId, String}()
11681172
skipped_deps = Base.PkgId[]
@@ -1309,7 +1313,7 @@ function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::
13091313
iob = IOBuffer()
13101314
name = is_direct_dep ? pkg.name : string(color_string(pkg.name, :light_black))
13111315
!fancyprint && lock(print_lock) do
1312-
isempty(pkg_queue) && printpkgstyle(io, :Precompiling, "environment...")
1316+
isempty(pkg_queue) && printpkgstyle(io, :Precompiling, target)
13131317
end
13141318
push!(pkg_queue, pkg)
13151319
started[pkg] = true
@@ -1613,6 +1617,7 @@ function status(ctx::Context, pkgs::Vector{PackageSpec}; diff::Bool=false, mode=
16131617
if compat
16141618
diff && pkgerror("Compat status has no `diff` mode")
16151619
outdated && pkgerror("Compat status has no `outdated` mode")
1620+
16161621
Operations.print_compat(ctx, pkgs; io)
16171622
else
16181623
Operations.status(ctx.env, ctx.registries, pkgs; mode, git_diff=diff, io, outdated)

src/Operations.jl

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,15 @@ function update_manifest!(env::EnvCache, pkgs::Vector{PackageSpec}, deps_map, ju
142142
else
143143
entry.deps = deps_map[pkg.uuid]
144144
end
145+
v = joinpath(source_path(env.project_file, pkg), "Project.toml")
146+
if isfile(v)
147+
p = Types.read_project(v)
148+
entry.gluedeps = p.gluedeps
149+
entry.gluepkgs = p.gluepkgs
150+
for (name, _) in p.gluedeps
151+
delete!(entry.deps, name)
152+
end
153+
end
145154
env.manifest[pkg.uuid] = entry
146155
end
147156
prune_manifest(env)
@@ -198,32 +207,37 @@ function reset_all_compat!(proj::Project)
198207
return nothing
199208
end
200209

201-
function collect_project!(pkg::PackageSpec, path::String,
202-
deps_map::Dict{UUID,Vector{PackageSpec}})
203-
deps_map[pkg.uuid] = PackageSpec[]
210+
function collect_project(pkg::PackageSpec, path::String)
211+
deps = PackageSpec[]
212+
glues = Set{UUID}()
204213
project_file = projectfile_path(path; strict=true)
205214
if project_file === nothing
206215
pkgerror("could not find project file for package $(err_rep(pkg)) at `$path`")
207216
end
208217
project = read_package(project_file)
209-
julia_compat = get_compat(project, "julia")
210218
#=
211219
# TODO, this should either error or be quiet
220+
julia_compat = get_compat(project, "julia")
212221
if julia_compat !== nothing && !(VERSION in julia_compat)
213222
println(io, "julia version requirement for package $(err_rep(pkg)) not satisfied")
214223
end
215224
=#
216225
for (name, uuid) in project.deps
217226
vspec = get_compat(project, name)
218-
push!(deps_map[pkg.uuid], PackageSpec(name, uuid, vspec))
227+
push!(deps, PackageSpec(name, uuid, vspec))
228+
end
229+
for (name, uuid) in project.gluedeps
230+
vspec = get_compat(project, name)
231+
push!(deps, PackageSpec(name, uuid, vspec))
232+
push!(glues, uuid)
219233
end
220234
if project.version !== nothing
221235
pkg.version = project.version
222236
else
223237
# @warn("project file for $(pkg.name) is missing a `version` entry")
224238
pkg.version = VersionNumber(0)
225239
end
226-
return
240+
return deps, glues
227241
end
228242

229243
is_tracking_path(pkg) = pkg.path !== nothing
@@ -258,17 +272,22 @@ end
258272

259273
function collect_fixed!(env::EnvCache, pkgs::Vector{PackageSpec}, names::Dict{UUID, String})
260274
deps_map = Dict{UUID,Vector{PackageSpec}}()
275+
glue_map = Dict{UUID,Set{UUID}}()
261276
if env.pkg !== nothing
262277
pkg = env.pkg
263-
collect_project!(pkg, dirname(env.project_file), deps_map)
278+
deps, glues = collect_project(pkg, dirname(env.project_file))
279+
deps_map[pkg.uuid] = deps
280+
glue_map[pkg.uuid] = glues
264281
names[pkg.uuid] = pkg.name
265282
end
266283
for pkg in pkgs
267284
path = project_rel_path(env, source_path(env.project_file, pkg))
268285
if !isdir(path)
269286
pkgerror("expected package $(err_rep(pkg)) to exist at path `$path`")
270287
end
271-
collect_project!(pkg, path, deps_map)
288+
deps, glues = collect_project(pkg, path)
289+
deps_map[pkg.uuid] = deps
290+
glue_map[pkg.uuid] = glues
272291
end
273292

274293
fixed = Dict{UUID,Resolve.Fixed}()
@@ -285,7 +304,7 @@ function collect_fixed!(env::EnvCache, pkgs::Vector{PackageSpec}, names::Dict{UU
285304
idx = findfirst(pkg -> pkg.uuid == uuid, pkgs)
286305
fix_pkg = pkgs[idx]
287306
end
288-
fixed[uuid] = Resolve.Fixed(fix_pkg.version, q)
307+
fixed[uuid] = Resolve.Fixed(fix_pkg.version, q, glue_map[uuid])
289308
end
290309
return fixed
291310
end
@@ -407,6 +426,7 @@ function deps_graph(env::EnvCache, registries::Vector{Registry.RegistryInstance}
407426

408427
# pkg -> version -> (dependency => compat):
409428
all_compat = Dict{UUID,Dict{VersionNumber,Dict{UUID,VersionSpec}}}()
429+
glue_compat = Dict{UUID,Dict{VersionNumber,Set{UUID}}}()
410430

411431
for (fp, fx) in fixed
412432
all_compat[fp] = Dict(fx.version => Dict{UUID,VersionSpec}())
@@ -418,7 +438,8 @@ function deps_graph(env::EnvCache, registries::Vector{Registry.RegistryInstance}
418438
for uuid in unseen
419439
push!(seen, uuid)
420440
uuid in keys(fixed) && continue
421-
all_compat_u = get_or_make!(all_compat, uuid)
441+
all_compat_u = get_or_make!(all_compat, uuid)
442+
glue_compat_u = get_or_make!(glue_compat, uuid)
422443

423444
uuid_is_stdlib = false
424445
stdlib_name = ""
@@ -446,35 +467,56 @@ function deps_graph(env::EnvCache, registries::Vector{Registry.RegistryInstance}
446467
push!(uuids, other_uuid)
447468
all_compat_u_vr[other_uuid] = VersionSpec()
448469
end
470+
471+
if !isempty(proj.gluedeps)
472+
glue_all_compat_u_vr = get_or_make!(glue_compat_u, v)
473+
for (_, other_uuid) in proj.gluedeps
474+
push!(uuids, other_uuid)
475+
all_compat_u_vr[other_uuid] = VersionSpec()
476+
push!(glue_all_compat_u_vr, other_uuid)
477+
end
478+
end
449479
else
450480
for reg in registries
451481
pkg = get(reg, uuid, nothing)
452482
pkg === nothing && continue
453483
info = Registry.registry_info(pkg)
454-
for (v, compat_info) in Registry.compat_info(info)
455-
# Filter yanked and if we are in offline mode also downloaded packages
456-
# TODO, pull this into a function
457-
Registry.isyanked(info, v) && continue
458-
if Pkg.OFFLINE_MODE[]
459-
pkg_spec = PackageSpec(name=pkg.name, uuid=pkg.uuid, version=v, tree_hash=Registry.treehash(info, v))
460-
is_package_downloaded(env.project_file, pkg_spec) || continue
461-
end
462484

463-
# Skip package version that are not the same as external packages in sysimage
464-
if PKGORIGIN_HAVE_VERSION && RESPECT_SYSIMAGE_VERSIONS[] && julia_version == VERSION
465-
pkgid = Base.PkgId(uuid, pkg.name)
466-
if Base.in_sysimage(pkgid)
467-
pkgorigin = get(Base.pkgorigins, pkgid, nothing)
468-
if pkgorigin !== nothing && pkgorigin.version !== nothing
469-
if v != pkgorigin.version
470-
continue
485+
function add_compat!(d, cinfo)
486+
for (v, compat_info) in cinfo
487+
# Filter yanked and if we are in offline mode also downloaded packages
488+
# TODO, pull this into a function
489+
Registry.isyanked(info, v) && continue
490+
if Pkg.OFFLINE_MODE[]
491+
pkg_spec = PackageSpec(name=pkg.name, uuid=pkg.uuid, version=v, tree_hash=Registry.treehash(info, v))
492+
is_package_downloaded(env.project_file, pkg_spec) || continue
493+
end
494+
495+
# Skip package version that are not the same as external packages in sysimage
496+
if PKGORIGIN_HAVE_VERSION && RESPECT_SYSIMAGE_VERSIONS[] && julia_version == VERSION
497+
pkgid = Base.PkgId(uuid, pkg.name)
498+
if Base.in_sysimage(pkgid)
499+
pkgorigin = get(Base.pkgorigins, pkgid, nothing)
500+
if pkgorigin !== nothing && pkgorigin.version !== nothing
501+
if v != pkgorigin.version
502+
continue
503+
end
471504
end
472505
end
473506
end
507+
dv = get_or_make!(d, v)
508+
merge!(dv, compat_info)
509+
union!(uuids, keys(compat_info))
510+
end
511+
end
512+
add_compat!(all_compat_u, Registry.compat_info(info))
513+
glue_compat_info = Registry.glue_compat_info(info)
514+
if glue_compat_info !== nothing
515+
add_compat!(all_compat_u, glue_compat_info)
516+
# Version to Set
517+
for (v, compat_info) in glue_compat_info
518+
glue_compat_u[v] = keys(compat_info)
474519
end
475-
476-
all_compat_u[v] = compat_info
477-
union!(uuids, keys(compat_info))
478520
end
479521
end
480522
end
@@ -493,7 +535,7 @@ function deps_graph(env::EnvCache, registries::Vector{Registry.RegistryInstance}
493535
end
494536
end
495537

496-
return Resolve.Graph(all_compat, uuid_to_name, reqs, fixed, false, julia_version),
538+
return Resolve.Graph(all_compat, glue_compat, uuid_to_name, reqs, fixed, false, julia_version),
497539
all_compat
498540
end
499541

@@ -1735,9 +1777,13 @@ function gen_target_project(ctx::Context, pkg::PackageSpec, source_path::String,
17351777
test_project.deps = source_env.project.deps
17361778
# collect test dependencies
17371779
for name in get(source_env.project.targets, target, String[])
1738-
uuid = get(source_env.project.extras, name, nothing)
1780+
uuid = nothing
1781+
for list in [source_env.project.extras, source_env.project.gluedeps]
1782+
uuid = get(list, name, nothing)
1783+
uuid === nothing || break
1784+
end
17391785
if uuid === nothing
1740-
pkgerror("`$name` declared as a `$target` dependency, but no such entry in `extras`")
1786+
pkgerror("`$name` declared as a `$target` dependency, but no such entry in `extras` or `gluedeps`")
17411787
end
17421788
test_project.deps[name] = uuid
17431789
end
@@ -2083,6 +2129,7 @@ function print_status(env::EnvCache, old_env::Union{Nothing,EnvCache}, registrie
20832129
lpadding = 2
20842130

20852131
package_statuses = PackageStatusData[]
2132+
installed_cache = Dict{Base.PkgId, Bool}()
20862133
for (uuid, old, new) in xs
20872134
if Types.is_project_uuid(env, uuid)
20882135
continue
@@ -2116,6 +2163,7 @@ function print_status(env::EnvCache, old_env::Union{Nothing,EnvCache}, registrie
21162163
no_packages_upgradable &= (!changed || !pkg_upgradable)
21172164
no_visible_packages_heldback &= (!changed || !pkg_heldback)
21182165
no_packages_heldback &= !pkg_heldback
2166+
21192167
push!(package_statuses, PackageStatusData(uuid, old, new, pkg_downloaded, pkg_upgradable, pkg_heldback, cinfo, changed))
21202168
end
21212169

src/Pkg.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -735,9 +735,9 @@ end
735735
# Precompilation #
736736
##################
737737

738-
function _auto_precompile(ctx::Types.Context; warn_loaded = true, already_instantiated = false)
738+
function _auto_precompile(ctx::Types.Context, pkgs::Vector{String}=String[]; warn_loaded = true, already_instantiated = false)
739739
if Base.JLOptions().use_compiled_modules == 1 && get_bool_env("JULIA_PKG_PRECOMPILE_AUTO"; default="true")
740-
Pkg.precompile(ctx; internal_call=true, warn_loaded = warn_loaded, already_instantiated = already_instantiated)
740+
Pkg.precompile(ctx, pkgs; internal_call=true, warn_loaded = warn_loaded, already_instantiated = already_instantiated)
741741
end
742742
end
743743

0 commit comments

Comments
 (0)