Skip to content

Commit 495a004

Browse files
authored
Add support for "package extensions" to code loading (#47695)
* Add support for "glue packages" to code loading This allows packages to define "glue packages" which are modules that are automatically loaded when a set of other packages are loaded into the Julia session.
1 parent de4f1c3 commit 495a004

File tree

16 files changed

+406
-24
lines changed

16 files changed

+406
-24
lines changed

NEWS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ Julia v1.10 Release Notes
44
New language features
55
---------------------
66

7-
87
Language changes
98
----------------
109

@@ -39,6 +38,10 @@ Standard library changes
3938

4039
#### Package Manager
4140

41+
- "Package Extensions": support for loading a piece of code based on other
42+
packages being loaded in the Julia session.
43+
This has similar applications as the Requires.jl package but also
44+
supports precompilation and setting compatibility.
4245
#### LinearAlgebra
4346

4447

base/loading.jl

Lines changed: 220 additions & 23 deletions
Large diffs are not rendered by default.

doc/src/manual/code-loading.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,46 @@ The subscripted `rootsᵢ`, `graphᵢ` and `pathsᵢ` variables correspond to th
348348
2. Packages in non-primary environments can end up using incompatible versions of their dependencies even if their own environments are entirely compatible. This can happen when one of their dependencies is shadowed by a version in an earlier environment in the stack (either by graph or path, or both).
349349

350350
Since the primary environment is typically the environment of a project you're working on, while environments later in the stack contain additional tools, this is the right trade-off: it's better to break your development tools but keep the project working. When such incompatibilities occur, you'll typically want to upgrade your dev tools to versions that are compatible with the main project.
351+
### "Extension"s
351352

353+
An "extension" is a module that is automatically loaded when a specified set of other packages (its "extension dependencies") are loaded in the current Julia session. The extension dependencies of an extension are a subset of those packages listed under the `[weakdeps]` section of a Project file. Extensions are defined under the `[extensions]` section in the project file:
354+
355+
```toml
356+
name = "MyPackage"
357+
358+
[weakdeps]
359+
ExtDep = "c9a23..." # uuid
360+
OtherExtDep = "862e..." # uuid
361+
362+
[extensions]
363+
BarExt = ["ExtDep", "OtherExtDep"]
364+
FooExt = "ExtDep"
365+
...
366+
```
367+
368+
The keys under `extensions` are the name of the extensions.
369+
They are loaded when all the packages on the right hand side (the extension dependencies) of that extension are loaded.
370+
If an extension only has one extension dependency the list of extension dependencies can be written as just a string for brevity.
371+
The location for the entry point of the extension is either in `ext/FooExt.jl` or `ext/FooExt/FooExt.jl` for
372+
extension `FooExt`.
373+
The content of an extension is often structured as:
374+
375+
```
376+
module FooExt
377+
378+
# Load main package and extension dependencies
379+
using MyPackage, ExtDep
380+
381+
# Extend functionality in main package with types from the extension dependencies
382+
MyPackage.func(x::ExtDep.SomeStruct) = ...
383+
384+
end
385+
```
386+
387+
When a package with extensions is added to an environment, the `weakdeps` and `extensions` sections
388+
are stored in the manifest file in the section for that package. The dependency lookup rules for
389+
a package are the same as for its "parent" except that the listed extension dependencies are also considered as
390+
dependencies.
352391
### Package/Environment Preferences
353392

354393
Preferences are dictionaries of metadata that influence package behavior within an environment.

test/loading.jl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,5 +991,36 @@ end
991991
end
992992
end
993993

994+
@testset "Extensions" begin
995+
old_depot_path = copy(DEPOT_PATH)
996+
try
997+
tmp = mktempdir()
998+
push!(empty!(DEPOT_PATH), joinpath(tmp, "depot"))
999+
1000+
proj = joinpath(@__DIR__, "project", "Extensions", "HasDepWithExtensions.jl")
1001+
for i in 1:2 # Once when requiring precomilation, once where it is already precompiled
1002+
cmd = `$(Base.julia_cmd()) --project=$proj --startup-file=no -e '
1003+
begin
1004+
using HasExtensions
1005+
# Base.get_extension(HasExtensions, :Extension) === nothing || error("unexpectedly got an extension")
1006+
HasExtensions.ext_loaded && error("ext_loaded set")
1007+
using HasDepWithExtensions
1008+
# Base.get_extension(HasExtensions, :Extension).extvar == 1 || error("extvar in Extension not set")
1009+
HasExtensions.ext_loaded || error("ext_loaded not set")
1010+
HasExtensions.ext_folder_loaded && error("ext_folder_loaded set")
1011+
HasDepWithExtensions.do_something() || error("do_something errored")
1012+
using ExtDep2
1013+
HasExtensions.ext_folder_loaded || error("ext_folder_loaded not set")
1014+
1015+
end
1016+
'`
1017+
@test success(cmd)
1018+
end
1019+
finally
1020+
copy!(DEPOT_PATH, old_depot_path)
1021+
end
1022+
end
1023+
1024+
9941025
empty!(Base.DEPOT_PATH)
9951026
append!(Base.DEPOT_PATH, original_depot_path)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name = "ExtDep"
2+
uuid = "fa069be4-f60b-4d4c-8b95-f8008775090c"
3+
version = "0.1.0"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module ExtDep
2+
3+
struct ExtDepStruct end
4+
5+
end # module ExtDep
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name = "ExtDep2"
2+
uuid = "55982ee5-2ad5-4c40-8cfe-5e9e1b01500d"
3+
version = "0.1.0"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module ExtDep2
2+
3+
greet() = print("Hello World!")
4+
5+
end # module ExtDep2
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# This file is machine-generated - editing it directly is not advised
2+
3+
julia_version = "1.10.0-DEV"
4+
manifest_format = "2.0"
5+
project_hash = "7cbe1857ecc6692a8cc8be428a5ad5073531ff98"
6+
7+
[[deps.ExtDep]]
8+
path = "../ExtDep.jl"
9+
uuid = "fa069be4-f60b-4d4c-8b95-f8008775090c"
10+
version = "0.1.0"
11+
12+
[[deps.ExtDep2]]
13+
path = "../ExtDep2"
14+
uuid = "55982ee5-2ad5-4c40-8cfe-5e9e1b01500d"
15+
version = "0.1.0"
16+
17+
[[deps.HasExtensions]]
18+
weakdeps = ["ExtDep", "ExtDep2"]
19+
path = "../HasExtensions.jl"
20+
uuid = "4d3288b3-3afc-4bb6-85f3-489fffe514c8"
21+
version = "0.1.0"
22+
23+
[deps.HasExtensions.extensions]
24+
Extension = "ExtDep"
25+
ExtensionFolder = ["ExtDep", "ExtDep2"]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name = "HasDepWithExtensions"
2+
uuid = "d4ef3d4a-8e22-4710-85d8-c6cf2eb9efca"
3+
version = "0.1.0"
4+
5+
[deps]
6+
ExtDep = "fa069be4-f60b-4d4c-8b95-f8008775090c"
7+
ExtDep2 = "55982ee5-2ad5-4c40-8cfe-5e9e1b01500d"
8+
HasExtensions = "4d3288b3-3afc-4bb6-85f3-489fffe514c8"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module HasDepWithExtensions
2+
3+
using HasExtensions: HasExtensions, HasExtensionsStruct
4+
using ExtDep: ExtDepStruct
5+
# Loading ExtDep makes the extension "Extension" load
6+
7+
function do_something()
8+
HasExtensions.foo(HasExtensionsStruct()) == 1 || error()
9+
HasExtensions.foo(ExtDepStruct()) == 2 || error()
10+
return true
11+
end
12+
13+
end # module
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# This file is machine-generated - editing it directly is not advised
2+
3+
julia_version = "1.10.0-DEV"
4+
manifest_format = "2.0"
5+
project_hash = "c87947f1f1f070eea848950c304d668a112dec3d"
6+
7+
[deps]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name = "HasExtensions"
2+
uuid = "4d3288b3-3afc-4bb6-85f3-489fffe514c8"
3+
version = "0.1.0"
4+
5+
[weakdeps]
6+
ExtDep = "fa069be4-f60b-4d4c-8b95-f8008775090c"
7+
ExtDep2 = "55982ee5-2ad5-4c40-8cfe-5e9e1b01500d"
8+
9+
[extensions]
10+
Extension = "ExtDep"
11+
ExtensionFolder = ["ExtDep", "ExtDep2"]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module Extension
2+
3+
using HasExtensions, ExtDep
4+
5+
HasExtensions.foo(::ExtDep.ExtDepStruct) = 2
6+
7+
function __init__()
8+
HasExtensions.ext_loaded = true
9+
end
10+
11+
const extvar = 1
12+
13+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module ExtensionFolder
2+
3+
using ExtDep, ExtDep2, HasExtensions
4+
5+
function __init__()
6+
HasExtensions.ext_folder_loaded = true
7+
end
8+
9+
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module HasExtensions
2+
3+
struct HasExtensionsStruct end
4+
5+
foo(::HasExtensionsStruct) = 1
6+
7+
ext_loaded = false
8+
ext_folder_loaded = false
9+
10+
end # module

0 commit comments

Comments
 (0)