Skip to content

Commit 4a2c593

Browse files
authored
add code loading + precompilation support for workspaces (#53653)
This is similar to workspaces in cargo where multiple projects share a manifest https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html and upon resolving the dependencies and compat of all projects in the workspace is adhered to. The idea is to use this for e.g. test, doc environments where you want to "overlay" a dependency graph on top of a base one. The code change in Base adds support for the code loading and precompilation part of this, those changes are: - Finding the manifest from any active project in the workspace - Merge preferences among projects in a workspace. - Allowing one to pass `manifest=true` to `precompilepkgs` to compile every package in the manifest. - The effect of giving no packages to `precompilepkgs` was changed from compiling all packages in the manifest to only those in the active project (which is equivalent in case of no workspace being used but different when it is used).
1 parent 9bd7343 commit 4a2c593

File tree

16 files changed

+304
-29
lines changed

16 files changed

+304
-29
lines changed

base/loading.jl

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,23 @@ function env_project_file(env::String)::Union{Bool,String}
611611
end
612612
end
613613

614+
function base_project(project_file)
615+
base_dir = abspath(joinpath(dirname(project_file), ".."))
616+
base_project_file = env_project_file(base_dir)
617+
base_project_file isa String || return nothing
618+
d = parsed_toml(base_project_file)
619+
workspace = get(d, "workspace", nothing)::Union{Dict{String, Any}, Nothing}
620+
if workspace === nothing
621+
return nothing
622+
end
623+
projects = get(workspace, "projects", nothing)::Union{Vector{String}, Nothing, String}
624+
projects === nothing && return nothing
625+
if projects isa Vector && basename(dirname(project_file)) in projects
626+
return base_project_file
627+
end
628+
return nothing
629+
end
630+
614631
function project_deps_get(env::String, name::String)::Union{Nothing,PkgId}
615632
project_file = env_project_file(env)
616633
if project_file isa String
@@ -622,21 +639,27 @@ function project_deps_get(env::String, name::String)::Union{Nothing,PkgId}
622639
return nothing
623640
end
624641

642+
function package_get(project_file, where::PkgId, name::String)
643+
proj = project_file_name_uuid(project_file, where.name)
644+
if proj == where
645+
# if `where` matches the project, use [deps] section as manifest, and stop searching
646+
pkg_uuid = explicit_project_deps_get(project_file, name)
647+
return PkgId(pkg_uuid, name)
648+
end
649+
return nothing
650+
end
651+
625652
function manifest_deps_get(env::String, where::PkgId, name::String)::Union{Nothing,PkgId}
626653
uuid = where.uuid
627654
@assert uuid !== nothing
628655
project_file = env_project_file(env)
629656
if project_file isa String
630-
# first check if `where` names the Project itself
631-
proj = project_file_name_uuid(project_file, where.name)
632-
if proj == where
633-
# if `where` matches the project, use [deps] section as manifest, and stop searching
634-
pkg_uuid = explicit_project_deps_get(project_file, name)
635-
return PkgId(pkg_uuid, name)
636-
end
657+
pkg = package_get(project_file, where, name)
658+
pkg === nothing || return pkg
637659
d = parsed_toml(project_file)
638660
exts = get(d, "extensions", nothing)::Union{Dict{String, Any}, Nothing}
639661
if exts !== nothing
662+
proj = project_file_name_uuid(project_file, where.name)
640663
# Check if `where` is an extension of the project
641664
if where.name in keys(exts) && where.uuid == uuid5(proj.uuid::UUID, where.name)
642665
# Extensions can load weak deps...
@@ -726,6 +749,14 @@ function project_file_path(project_file::String)
726749
joinpath(dirname(project_file), get(d, "path", "")::String)
727750
end
728751

752+
function workspace_manifest(project_file)
753+
base = base_project(project_file)
754+
if base !== nothing
755+
return project_file_manifest_path(base)
756+
end
757+
return nothing
758+
end
759+
729760
# find project file's corresponding manifest file
730761
function project_file_manifest_path(project_file::String)::Union{Nothing,String}
731762
@lock require_lock begin
@@ -736,6 +767,10 @@ function project_file_manifest_path(project_file::String)::Union{Nothing,String}
736767
end
737768
dir = abspath(dirname(project_file))
738769
d = parsed_toml(project_file)
770+
base_manifest = workspace_manifest(project_file)
771+
if base_manifest !== nothing
772+
return base_manifest
773+
end
739774
explicit_manifest = get(d, "manifest", nothing)::Union{String, Nothing}
740775
manifest_path = nothing
741776
if explicit_manifest !== nothing
@@ -3355,9 +3390,27 @@ function recursive_prefs_merge(base::Dict{String, Any}, overrides::Dict{String,
33553390
return new_base
33563391
end
33573392

3393+
function get_projects_workspace_to_root(project_file)
3394+
projects = String[project_file]
3395+
while true
3396+
project_file = base_project(project_file)
3397+
if project_file === nothing
3398+
return projects
3399+
end
3400+
push!(projects, project_file)
3401+
end
3402+
end
3403+
33583404
function get_preferences(uuid::Union{UUID,Nothing} = nothing)
33593405
merged_prefs = Dict{String,Any}()
3360-
for env in reverse(load_path())
3406+
loadpath = load_path()
3407+
projects_to_merge_prefs = String[]
3408+
append!(projects_to_merge_prefs, Iterators.drop(loadpath, 1))
3409+
if length(loadpath) >= 1
3410+
prepend!(projects_to_merge_prefs, get_projects_workspace_to_root(first(loadpath)))
3411+
end
3412+
3413+
for env in reverse(projects_to_merge_prefs)
33613414
project_toml = env_project_file(env)
33623415
if !isa(project_toml, String)
33633416
continue

base/precompilation.jl

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
module Precompilation
22

33
using Base: PkgId, UUID, SHA1, parsed_toml, project_file_name_uuid, project_names,
4-
project_file_manifest_path, get_deps, preferences_names, isaccessibledir, isfile_casesensitive
4+
project_file_manifest_path, get_deps, preferences_names, isaccessibledir, isfile_casesensitive,
5+
base_project
56

67
# This is currently only used for pkgprecompile but the plan is to use this in code loading in the future
78
# see the `kc/codeloading2.0` branch
@@ -59,6 +60,19 @@ function ExplicitEnv(envpath::String=Base.active_project())
5960
delete!(project_deps, name)
6061
end
6162

63+
# This project might be a package, in that case, that is also a "dependency"
64+
# of the project.
65+
proj_name = get(project_d, "name", nothing)::Union{String, Nothing}
66+
_proj_uuid = get(project_d, "uuid", nothing)::Union{String, Nothing}
67+
proj_uuid = _proj_uuid === nothing ? nothing : UUID(_proj_uuid)
68+
69+
project_is_package = proj_name !== nothing && proj_uuid !== nothing
70+
if project_is_package
71+
# TODO: Error on missing uuid?
72+
project_deps[proj_name] = UUID(proj_uuid)
73+
names[UUID(proj_uuid)] = proj_name
74+
end
75+
6276
project_extensions = Dict{String, Vector{UUID}}()
6377
# Collect all extensions of the project
6478
for (name, triggers::Union{String, Vector{String}}) in get(Dict{String, Any}, project_d, "extensions")::Dict{String, Any}
@@ -76,18 +90,6 @@ function ExplicitEnv(envpath::String=Base.active_project())
7690
project_extensions[name] = uuids
7791
end
7892

79-
# This project might be a package, in that case, that is also a "dependency"
80-
# of the project.
81-
proj_name = get(project_d, "name", nothing)::Union{String, Nothing}
82-
_proj_uuid = get(project_d, "uuid", nothing)::Union{String, Nothing}
83-
proj_uuid = _proj_uuid === nothing ? nothing : UUID(_proj_uuid)
84-
85-
if proj_name !== nothing && proj_uuid !== nothing
86-
# TODO: Error on missing uuid?
87-
project_deps[proj_name] = UUID(proj_uuid)
88-
names[UUID(proj_uuid)] = proj_name
89-
end
90-
9193
manifest = project_file_manifest_path(envpath)
9294
manifest_d = manifest === nothing ? Dict{String, Any}() : parsed_toml(manifest)
9395

@@ -355,8 +357,8 @@ function precompilepkgs(pkgs::Vector{String}=String[];
355357
configs::Union{Config,Vector{Config}}=(``=>Base.CacheFlags()),
356358
io::IO=stderr,
357359
# asking for timing disables fancy mode, as timing is shown in non-fancy mode
358-
fancyprint::Bool = can_fancyprint(io) && !timing
359-
)
360+
fancyprint::Bool = can_fancyprint(io) && !timing,
361+
manifest::Bool=false,)
360362

361363
configs = configs isa Config ? [configs] : configs
362364

@@ -512,9 +514,15 @@ function precompilepkgs(pkgs::Vector{String}=String[];
512514
end
513515
@debug "precompile: circular dep check done"
514516

515-
# if a list of packages is given, restrict to dependencies of given packages
516-
if !isempty(pkgs)
517-
function collect_all_deps(depsmap, dep, alldeps=Set{Base.PkgId}())
517+
if !manifest
518+
if isempty(pkgs)
519+
pkgs = [pkg.name for pkg in direct_deps]
520+
target = "all packages"
521+
else
522+
target = join(pkgs, ", ")
523+
end
524+
# restrict to dependencies of given packages
525+
function collect_all_deps(depsmap, dep, alldeps=Set{Base.PkgId}())
518526
for _dep in depsmap[dep]
519527
if !(_dep in alldeps)
520528
push!(alldeps, _dep)
@@ -544,13 +552,13 @@ function precompilepkgs(pkgs::Vector{String}=String[];
544552
# TODO: actually handle packages from other envs in the stack
545553
return
546554
else
547-
error("No direct dependencies outside of the sysimage found matching $(repr(pkgs))")
555+
return
548556
end
549557
end
550-
target = join(pkgs, ", ")
551558
else
552-
target = "project"
559+
target = "manifest"
553560
end
561+
554562
nconfigs = length(configs)
555563
if nconfigs == 1
556564
if !isempty(only(configs)[1])

doc/src/manual/code-loading.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,28 @@ are stored in the manifest file in the section for that package. The dependency
394394
a package are the same as for its "parent" except that the listed extension dependencies are also considered as
395395
dependencies.
396396

397+
### [Workspaces](@id workspaces)
398+
399+
A project file can define a workspace by giving a set of projects that is part of that workspace:
400+
401+
```toml
402+
[workspace]
403+
projects = ["test", "benchmarks", "docs", "SomePackage"]
404+
```
405+
406+
Each subfolder contains its own `Project.toml` file, which may include additional dependencies and compatibility constraints. In such cases, the package manager gathers all dependency information from all the projects in the workspace generating a single manifest file that combines the versions of all dependencies.
407+
408+
Furthermore, workspaces can be "nested", meaning a project defining a workspace can also be part of another workspace. In this scenario, a single manifest file is still utilized, stored alongside the "root project" (the project that doesn't have another workspace including it). An example file structure could look like this:
409+
410+
```
411+
Project.toml # projects = ["MyPackage"]
412+
Manifest.toml
413+
MyPackage/
414+
Project.toml # projects = ["test"]
415+
test/
416+
Project.toml
417+
```
418+
397419
### [Package/Environment Preferences](@id preferences)
398420

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

test/loading.jl

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,3 +1550,61 @@ end
15501550
rot13proj = joinpath(@__DIR__, "project", "Rot13")
15511551
@test readchomp(`$(Base.julia_cmd()) --startup-file=no --project=$rot13proj -m Rot13 --project nowhere ABJURER`) == "--cebwrpg abjurer NOWHERE "
15521552
end
1553+
1554+
@testset "workspace loading" begin
1555+
old_load_path = copy(LOAD_PATH)
1556+
try
1557+
empty!(LOAD_PATH)
1558+
push!(LOAD_PATH, joinpath(@__DIR__, "project", "SubProject"))
1559+
@test Base.get_preferences()["value"] == 1
1560+
@test Base.get_preferences()["x"] == 1
1561+
1562+
empty!(LOAD_PATH)
1563+
push!(LOAD_PATH, joinpath(@__DIR__, "project", "SubProject", "sub"))
1564+
id = Base.identify_package("Devved")
1565+
@test isfile(Base.locate_package(id))
1566+
@test Base.identify_package("Devved2") === nothing
1567+
id3 = Base.identify_package("MyPkg")
1568+
@test isfile(Base.locate_package(id3))
1569+
1570+
empty!(LOAD_PATH)
1571+
push!(LOAD_PATH, joinpath(@__DIR__, "project", "SubProject", "PackageThatIsSub"))
1572+
id_pkg = Base.identify_package("PackageThatIsSub")
1573+
@test Base.identify_package(id_pkg, "Devved") === nothing
1574+
id_dev2 = Base.identify_package(id_pkg, "Devved2")
1575+
@test isfile(Base.locate_package(id_dev2))
1576+
id_mypkg = Base.identify_package("MyPkg")
1577+
@test isfile(Base.locate_package(id_mypkg))
1578+
id_dev = Base.identify_package(id_mypkg, "Devved")
1579+
@test isfile(Base.locate_package(id_dev))
1580+
@test Base.get_preferences()["value"] == 2
1581+
@test Base.get_preferences()["x"] == 1
1582+
@test Base.get_preferences()["y"] == 2
1583+
1584+
empty!(LOAD_PATH)
1585+
push!(LOAD_PATH, joinpath(@__DIR__, "project", "SubProject", "PackageThatIsSub", "test"))
1586+
id_pkg = Base.identify_package("PackageThatIsSub")
1587+
@test isfile(Base.locate_package(id_pkg))
1588+
@test Base.identify_package(id_pkg, "Devved") === nothing
1589+
id_dev2 = Base.identify_package(id_pkg, "Devved2")
1590+
@test isfile(Base.locate_package(id_dev2))
1591+
id_mypkg = Base.identify_package("MyPkg")
1592+
@test isfile(Base.locate_package(id_mypkg))
1593+
id_dev = Base.identify_package(id_mypkg, "Devved")
1594+
@test isfile(Base.locate_package(id_dev))
1595+
@test Base.get_preferences()["value"] == 3
1596+
@test Base.get_preferences()["x"] == 1
1597+
@test Base.get_preferences()["y"] == 2
1598+
@test Base.get_preferences()["z"] == 3
1599+
1600+
empty!(LOAD_PATH)
1601+
push!(LOAD_PATH, joinpath(@__DIR__, "project", "SubProject", "test"))
1602+
id_mypkg = Base.identify_package("MyPkg")
1603+
id_dev = Base.identify_package(id_mypkg, "Devved")
1604+
@test isfile(Base.locate_package(id_dev))
1605+
@test Base.identify_package("Devved2") === nothing
1606+
1607+
finally
1608+
copy!(LOAD_PATH, old_load_path)
1609+
end
1610+
end
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name = "Devved"
2+
uuid = "cbce3a6e-7a3d-4e84-8e6d-b87208df7599"
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 Devved
2+
3+
greet() = print("Hello World!")
4+
5+
end # module Devved
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name = "Devved2"
2+
uuid = "08f74b90-50f5-462f-80b9-a72b1258a17b"
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 Devved2
2+
3+
greet() = print("Hello World!")
4+
5+
end # module Devved2

test/project/SubProject/Manifest.toml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# This file is machine-generated - editing it directly is not advised
2+
3+
julia_version = "1.12.0-DEV"
4+
manifest_format = "2.0"
5+
project_hash = "620b9377bc807ff657e6618c8ccc24887eb40285"
6+
7+
[[deps.Base64]]
8+
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
9+
version = "1.11.0"
10+
11+
[[deps.Devved]]
12+
path = "Devved"
13+
uuid = "cbce3a6e-7a3d-4e84-8e6d-b87208df7599"
14+
version = "0.1.0"
15+
16+
[[deps.Devved2]]
17+
path = "Devved2"
18+
uuid = "08f74b90-50f5-462f-80b9-a72b1258a17b"
19+
version = "0.1.0"
20+
21+
[[deps.InteractiveUtils]]
22+
deps = ["Markdown"]
23+
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
24+
version = "1.11.0"
25+
26+
[[deps.Logging]]
27+
deps = ["StyledStrings"]
28+
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
29+
version = "1.11.0"
30+
31+
[[deps.Markdown]]
32+
deps = ["Base64"]
33+
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
34+
version = "1.11.0"
35+
36+
[[deps.MyPkg]]
37+
deps = ["Devved", "Devved2"]
38+
path = "."
39+
uuid = "0cafdeb2-d7a2-40d0-8d22-4411fcc2c4ee"
40+
version = "0.0.0"
41+
42+
[[deps.PackageThatIsSub]]
43+
deps = ["Devved2", "MyPkg"]
44+
path = "PackageThatIsSub"
45+
uuid = "1efb588c-9412-4e40-90a4-710420bd84aa"
46+
version = "0.1.0"
47+
48+
[[deps.Random]]
49+
deps = ["SHA"]
50+
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
51+
version = "1.11.0"
52+
53+
[[deps.SHA]]
54+
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
55+
version = "0.7.0"
56+
57+
[[deps.Serialization]]
58+
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
59+
version = "1.11.0"
60+
61+
[[deps.StyledStrings]]
62+
uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b"
63+
version = "1.11.0"
64+
65+
[[deps.Test]]
66+
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
67+
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
68+
version = "1.11.0"

0 commit comments

Comments
 (0)