diff --git a/NEWS.md b/NEWS.md index 9092a45278548..d2d33e0b32972 100644 --- a/NEWS.md +++ b/NEWS.md @@ -78,6 +78,8 @@ New library features * `Base.ScopedValues.LazyScopedValue{T}` is introduced for scoped values that compute their default using a `OncePerProcess{T}` callback, allowing for lazy initialization of the default value. `AbstractScopedValue` is now the abstract base type for both `ScopedValue` and `LazyScopedValue`. ([#59372]) +* New `Base.active_manifest()` function to return the path of the active manifest, like `Base.active_project()`. + Also can return the manifest that would be used for a given project file ([#57937]) Standard library changes ------------------------ diff --git a/base/initdefs.jl b/base/initdefs.jl index 14b3d5d921083..2c9ded9c4033e 100644 --- a/base/initdefs.jl +++ b/base/initdefs.jl @@ -371,6 +371,27 @@ function set_active_project(projfile::Union{AbstractString,Nothing}) end end +""" + active_manifest() + active_manifest(project_file::AbstractString) + +Return the path of the active manifest file, or the manifest file that would be used for a given `project_file`. + +In a stacked environment (where multiple environments exist in the load path), this returns the manifest +file for the primary (active) environment only, not the manifests from other environments in the stack. +See the manual section on [Environment stacks](@ref) for more details on how stacked environments work. + +See [`Project environments`](@ref project-environments) for details on the difference between a project and a manifest, and the naming +options and their priority in package loading. + +See also [`Base.active_project`](@ref), [`Base.set_active_project`](@ref). +""" +function active_manifest(project_file::Union{AbstractString,Nothing}=nothing; search_load_path::Bool=true) + # If `project_file` was specified, use that, otherwise get the active project: + project_file = !isnothing(project_file) ? project_file : active_project(search_load_path) + project_file === nothing && return nothing + return project_file_manifest_path(project_file) +end """ load_path() diff --git a/base/loading.jl b/base/loading.jl index 9002d15ce1b37..1bb00c3d3cbbb 100644 --- a/base/loading.jl +++ b/base/loading.jl @@ -885,6 +885,7 @@ function project_file_manifest_path(project_file::String)::Union{Nothing,String} manifest_path === missing || return manifest_path end dir = abspath(dirname(project_file)) + isfile_casesensitive(project_file) || return nothing d = parsed_toml(project_file) base_manifest = workspace_manifest(project_file) if base_manifest !== nothing diff --git a/base/public.jl b/base/public.jl index 3329af875a835..413c859106240 100644 --- a/base/public.jl +++ b/base/public.jl @@ -53,6 +53,7 @@ public DL_LOAD_PATH, load_path, active_project, + active_manifest, # Reflection and introspection get_extension, diff --git a/doc/src/base/base.md b/doc/src/base/base.md index 3ab7cca6ffc51..a59689f4dc4de 100644 --- a/doc/src/base/base.md +++ b/doc/src/base/base.md @@ -44,6 +44,7 @@ ans err Base.active_project Base.set_active_project +Base.active_manifest ``` ## [Keywords](@id Keywords) diff --git a/doc/src/manual/code-loading.md b/doc/src/manual/code-loading.md index c673f8bd4f6d7..5871530720d22 100644 --- a/doc/src/manual/code-loading.md +++ b/doc/src/manual/code-loading.md @@ -64,7 +64,7 @@ Each kind of environment defines these three maps differently, as detailed in th !!! note For ease of understanding, the examples throughout this chapter show full data structures for roots, graph and paths. However, Julia's package loading code does not explicitly create these. Instead, it lazily computes only as much of each structure as it needs to load a given package. -### Project environments +### [Project environments](@id project-environments) A project environment is determined by a directory containing a project file called `Project.toml`, and optionally a manifest file called `Manifest.toml`. These files may also be called `JuliaProject.toml` and `JuliaManifest.toml`, in which case `Project.toml` and `Manifest.toml` are ignored. This allows for coexistence with other tools that might consider files called `Project.toml` and `Manifest.toml` significant. For pure Julia projects, however, the names `Project.toml` and `Manifest.toml` are preferred. However, from Julia v1.10.8 onwards, `(Julia)Manifest-v{major}.{minor}.toml` is recognized as a format to make a given julia version use a specific manifest file i.e. in the same folder, a `Manifest-v1.11.toml` would be used by v1.11 and `Manifest.toml` by any other julia version. diff --git a/test/loading.jl b/test/loading.jl index 19d437549a587..e3487c7050d70 100644 --- a/test/loading.jl +++ b/test/loading.jl @@ -708,6 +708,112 @@ mktempdir() do dir @test success(cmd) end +function _with_empty_load_path(f::Function) + old_load_path = copy(Base.LOAD_PATH) + try + empty!(Base.LOAD_PATH) + f() + finally + append!(Base.LOAD_PATH, old_load_path) + end +end +old_act_proj = Base.ACTIVE_PROJECT[] +function _with_activate(f::Function, project_file::Union{AbstractString, Nothing}) + try + Base.ACTIVE_PROJECT[] = project_file + f() + finally + Base.ACTIVE_PROJECT[] = old_act_proj + end +end +function _activate_and_get_active_manifest_noarg(project_file::Union{AbstractString, Nothing}) + _with_activate(project_file) do + Base.active_manifest() + end +end + +@testset "Base.active_manifest()" begin + test_dir = @__DIR__ + test_cases = [ + (joinpath(test_dir, "TestPkg", "Project.toml"), joinpath(test_dir, "TestPkg", "Manifest.toml")), + (joinpath(test_dir, "project", "Project.toml"), joinpath(test_dir, "project", "Manifest.toml")), + ] + + @testset "active_manifest() - no argument passed" begin + for (proj, expected_man) in test_cases + @test _activate_and_get_active_manifest_noarg(proj) == expected_man + # Base.active_manifest() should never return a file that doesn't exist: + @test isfile(_activate_and_get_active_manifest_noarg(proj)) + end + mktempdir() do dir + proj = joinpath(dir, "Project.toml") + + # If the project file doesn't exist, active_manifest() should return `nothing`: + @test _activate_and_get_active_manifest_noarg(proj) === nothing + + # If the project file exists but the manifest file does not, active_manifest() should still return `nothing`: + touch(proj) + @test _activate_and_get_active_manifest_noarg(proj) === nothing + + # If the project and manifest files both exist, active_manifest() should return the path to the manifest: + manif = joinpath(dir, "Manifest.toml") + touch(manif) + @test _activate_and_get_active_manifest_noarg(proj) == manif + # Base.active_manifest() should never return a file that doesn't exist: + @test isfile(_activate_and_get_active_manifest_noarg(proj)) + + # If the manifest file exists but the project file does not, active_manifest() should return `nothing`: + rm(proj) + @test _activate_and_get_active_manifest_noarg(proj) == nothing + end + end + + @testset "active_manifest(proj::AbstractString)" begin + Base.ACTIVE_PROJECT[] = old_act_proj + for (proj, expected_man) in test_cases + @test Base.active_manifest(proj) == expected_man + # Base.active_manifest() should never return a file that doesn't exist: + @test isfile(Base.active_manifest(proj)) + end + mktempdir() do dir + proj = joinpath(dir, "Project.toml") + + # If the project file doesn't exist, active_manifest(proj) should return `nothing`: + @test Base.active_manifest(proj) === nothing + + # If the project file exists but the manifest file does not, active_manifest(proj) should still return `nothing`: + touch(proj) + @test Base.active_manifest(proj) === nothing + + # If the project and manifest files both exist, active_manifest(proj) should return the path to the manifest: + manif = joinpath(dir, "Manifest.toml") + touch(manif) + @test Base.active_manifest(proj) == manif + # Base.active_manifest() should never return a file that doesn't exist: + @test isfile(Base.active_manifest(proj)) + + # If the manifest file exists but the project file does not, active_manifest(proj) should return `nothing`: + rm(proj) + @test Base.active_manifest(proj) === nothing + end + end + + @testset "ACTIVE_PROJECT[] is `nothing` => active_manifest() is nothing" begin + _with_activate(nothing) do; _with_empty_load_path() do + @test Base.active_manifest() === nothing + @test Base.active_manifest(nothing) === nothing + end; end + end + + @testset "Project file does not exist => active_manifest() is nothing" begin + mktempdir() do dir + proj = joinpath(dir, "Project.toml") + @test Base.active_manifest(proj) === nothing + @test _activate_and_get_active_manifest_noarg(proj) === nothing + end + end +end + @testset "expansion of JULIA_LOAD_PATH" begin s = Sys.iswindows() ? ';' : ':' tmp = "/this/does/not/exist"