Skip to content

Commit 2f33a25

Browse files
committed
WIP: Add a loading mechanism for compat-dependent API sets
This is a first WIP implementation for providing the underlying mechanism for a #54905. I haven't fully designed out what I want the final thing to look like, but I've thought about it enough that I think we need some hands-on examples for further design exploration, which is what this PR is designed to support. The core idea here is that if you specify in your package ``` [compact] julia = "0.14" MyDependency = "0.3,0.4" ``` Then this mechanism will ask `MyDependency` to provide you a view of its API that is compatible with its 0.3 version, even if 0.4 is installed. The objective of this mechanism is that downstreams of widely used packages (including `Base` and standard libraries) can be upgraded more gradually by allowing some portion of the packages in project to use the new 0.4 API set, while packages that have not yet upgraded can continue to use the 0.3 API set. Currently, whenever a widely used package changes an API there's a big frenzied rush to update downstreams, which just isn't sustainable as the ecosystem grows. In other languages, problems like these are solved by allowing multiple versions of the same package to be loaded. However, in julia, this is not particularly feasible by default because packages may have generic functions that need to be extended and unified and which break if there are multiple copies of such resources. That said, for simple packages, this mechanism could be used to emulate the multiple-version-loading strategy on an opt-in basis. The way that this works is that `loading` gains an additional layer of indirection that is provided the `compat` specification of the loading package. Packages can opt into this by providing a ``` function _get_versioned_api(compat) end ``` function. Where the default is equivalent to `_get_versioned_api(compat) = @__MODULE__`. The loader makes no further assumptions on either the structure of `compat` or how the package processes it. This is intentional to allow evolution without impacting the core loading logic (of course Pkg also looks at the structure of compat, as would the `_get_versioned_api` implementation in Base, so there are some constraints). That said, the envisioned usage of this is that we create a standard (in the sense of being widely used, not in the sense of it being a stdlib) package to help package authors define the APIs of their packages more precisely and with version information. This package would then create a set of modules under the hood that describe these APIs to the sytem and would set up `_get_versioned_api` appropriately (i.e. the mechanism is not intended to be used directly in most cases). To actually make this useful, we will likely need some additional features in the binding system, in particular: 1. #59859 and a few variants thereon to make the API modueles look more transparent. 2. A mechanism to intercept extension of generic function overloads in order to be able to provide compatibility (Design/PR for this forthcoming). Note that this PR itself is WIP, it is not yet fully complete and there is also a Pkg.jl side of this required to put compat info into the manifest.
1 parent 77ffa17 commit 2f33a25

File tree

2 files changed

+46
-34
lines changed

2 files changed

+46
-34
lines changed

base/loading.jl

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,8 @@ struct LoadingCache
256256
env_project_file::Dict{String, Union{Bool, String}}
257257
project_file_manifest_path::Dict{String, Union{Nothing, String}}
258258
require_parsed::Set{String}
259-
identified_where::Dict{Tuple{PkgId, String}, Union{Nothing, Tuple{PkgId, String}}}
260-
identified::Dict{String, Union{Nothing, Tuple{PkgId, String}}}
259+
identified_where::Dict{Tuple{PkgId, String}, Union{Nothing, Tuple{ApiId, String}}}
260+
identified::Dict{String, Union{Nothing, Tuple{ApiId, String}}}
261261
located::Dict{Tuple{PkgId, Union{String, Nothing}}, Union{Tuple{String, String}, Nothing}}
262262
end
263263
const LOADING_CACHE = Ref{Union{LoadingCache, Nothing}}(nothing) # n.b.: all access to and through this are protected by require_lock
@@ -351,36 +351,36 @@ function identify_package_env(where::Union{PkgId, Nothing}, name::String)
351351
cache_key = where === nothing ? name : (where, name)
352352
if cache !== nothing
353353
env_cache = where === nothing ? cache.identified : cache.identified_where
354-
pkg_env = get(env_cache, cache_key, missing)
355-
pkg_env === missing || return pkg_env
354+
api_env = get(env_cache, cache_key, missing)
355+
api_env === missing || return api_env
356356
end
357357

358358
# Main part: Search through all environments in the load path to see if we have
359359
# a matching entry.
360-
pkg_env = nothing
360+
api_env = nothing
361361
for env in load_path()
362-
pkgid = environment_deps_get(env, where, name)
362+
apiid = environment_deps_get(env, where, name)
363363
# If we didn't find `where` at all, keep looking through the environment stack
364-
pkgid === nothing && continue
365-
if pkgid.uuid !== nothing || where === nothing
366-
pkg_env = pkgid, env
364+
apiid === nothing && continue
365+
if apiid.pkg.uuid !== nothing || where === nothing
366+
api_env = apiid, env
367367
end
368368
# If we don't have pkgid.uuid, still break here - this is a sentinel that indicates
369369
# that we've found `where` but it did not have the required dependency. We terminate the search.
370370
break
371371
end
372-
if pkg_env === nothing && where !== nothing && is_stdlib(where)
372+
if api_env === nothing && where !== nothing && is_stdlib(where)
373373
# if not found it could be that manifests are from a different julia version/commit
374374
# where stdlib dependencies have changed, so look up deps based on the stdlib Project.toml
375375
# as a fallback
376-
pkg_env = identify_stdlib_project_dep(where, name)
376+
api_env = identify_stdlib_project_dep(where, name)
377377
end
378378

379379
# Cache the result
380380
if cache !== nothing
381-
env_cache[cache_key] = pkg_env
381+
env_cache[cache_key] = api_env
382382
end
383-
return pkg_env
383+
return api_env
384384
end
385385
identify_package_env(name::String) = identify_package_env(nothing, name)
386386

@@ -711,9 +711,9 @@ end
711711
function package_get_here(project_file, name::String)::Union{Nothing,ApiId}
712712
# if `where` matches the project, use [deps] section as manifest, and stop searching
713713
pkg_uuid = explicit_project_deps_get(project_file, name)
714-
pkg_uuid === nothing && return PkgId(name)
715-
return ApiId(PkgId(pkg_uuid, name),
716-
explicit_project_compat_get(project_file, name))
714+
pkg_compat = explicit_project_compat_get(project_file, name)
715+
pkg_uuid === nothing && return ApiId(PkgId(name), pkg_compat)
716+
return ApiId(PkgId(pkg_uuid, name), pkg_compat)
717717
end
718718

719719
function package_get(project_file, where::Union{Nothing, PkgId}, name::String)::Union{Nothing,ApiId}
@@ -752,7 +752,7 @@ function package_extension_get(project_file, where::PkgId, name::String)::Union{
752752
return nothing
753753
end
754754

755-
function environment_deps_get(env::String, where::Union{Nothing,PkgId}, name::String)::Union{Nothing,PkgId}
755+
function environment_deps_get(env::String, where::Union{Nothing,PkgId}, name::String)::Union{Nothing,ApiId}
756756
@assert where === nothing || where.uuid !== nothing
757757
project_file = env_project_file(env)
758758
implicit_manifest = !(project_file isa String)
@@ -762,7 +762,7 @@ function environment_deps_get(env::String, where::Union{Nothing,PkgId}, name::St
762762
# Toplevel load with a directory (implicit manifest) - all we look for is the
763763
# existence of the package name in the directory.
764764
pkg = implicit_manifest_pkgid(env, name)
765-
return pkg
765+
return ApiId(pkg, nothing)
766766
end
767767
project_file = implicit_manifest_project(env, where)
768768
project_file === nothing && return nothing
@@ -783,7 +783,7 @@ function environment_deps_get(env::String, where::Union{Nothing,PkgId}, name::St
783783
# uses the same code path. Otherwise this is the active project.
784784
pkg = package_get(project_file, where, name)
785785
if pkg !== nothing
786-
if where === nothing && pkg.uuid === nothing
786+
if where === nothing && pkg.pkg.uuid === nothing
787787
# This is a top-level load - even though we didn't find the dependency
788788
# here, we still want to keep looking through the top-level environment stack.
789789
return nothing
@@ -804,7 +804,7 @@ function environment_deps_get(env::String, where::Union{Nothing,PkgId}, name::St
804804
# With an implicit manifest, getting here means that our (implicit) environment
805805
# *has* the package `where`. If we don't find it, it just means that `where` doesn't
806806
# have `name` as a dependency - c.f. the analogous case in `explicit_manifest_deps_get`.
807-
return PkgId(name)
807+
return ApiId(PkgId(name), nothing)
808808
end
809809

810810
# All other cases, dependencies come from the (top-level) manifest
@@ -1032,18 +1032,19 @@ function dep_stanza_get(stanza::Dict{String, Any}, name)::Union{ApiId, Nothing}
10321032
if dep == name
10331033
if uuid_or_obj isa String
10341034
# uuid specified, but no API compat
1035-
return ApiId(PkgId(name, UUID(uuid_or_obj)), nothing)
1035+
return ApiId(PkgId(UUID(uuid_or_obj), name), nothing)
10361036
else
10371037
api_compat = get(uuid_or_obj, "compat", nothing)::Union{Nothing, String}
10381038
uuid = get(uuid_or_obj, "uuid", nothing)::Union{Nothing, String}
1039-
uuid === nothing && @goto done
1039+
uuid === nothing && return nothing
1040+
return ApiId(PkgId(UUID(uuid), name), api_compat)
10401041
end
10411042
end
10421043
end
10431044
return nothing
10441045
end
10451046

1046-
function dep_stanza_get(stanza::Vector{String}, name::String)::Union{Nothing, PkgId}
1047+
function dep_stanza_get(stanza::Vector{String}, name::String)::Union{Nothing, ApiId}
10471048
name in stanza && return ApiId(PkgId(name), nothing)
10481049
return nothing
10491050
end
@@ -1063,7 +1064,7 @@ function explicit_manifest_deps_get(project_file::String, where::PkgId, name::St
10631064
# deps is either a list of names (deps = ["DepA", "DepB"]) or
10641065
# a table of entries (deps = {"DepA" = "6ea...", "DepB" = "55d..."}
10651066
deps = get(entry, "deps", nothing)::Union{Vector{String}, Dict{String, Any}, Nothing}
1066-
local dep::Union{Nothing, PkgId}
1067+
local dep::Union{Nothing, ApiId}
10671068
if UUID(uuid) === where.uuid
10681069
dep = dep_stanza_get(deps, name)
10691070

@@ -1100,7 +1101,7 @@ function explicit_manifest_deps_get(project_file::String, where::PkgId, name::St
11001101
end
11011102

11021103
@label have_dep
1103-
dep.uuid !== nothing && return dep
1104+
dep.pkg.uuid !== nothing && return dep
11041105

11051106
# We have the dep, but it did not specify a UUID. In this case,
11061107
# it must be that the name is unique in the manifest - so lookup
@@ -1112,7 +1113,7 @@ function explicit_manifest_deps_get(project_file::String, where::PkgId, name::St
11121113
entry = first(name_deps::Vector{Any})::Dict{String, Any}
11131114
uuid = get(entry, "uuid", nothing)::Union{String, Nothing}
11141115
uuid === nothing && return PkgId(name)
1115-
return PkgId(UUID(uuid), name)
1116+
return ApiId(PkgId(UUID(uuid), name), nothing)
11161117
end
11171118
end
11181119

@@ -1486,6 +1487,7 @@ function run_module_init(mod::Module, i::Int=1)
14861487
end
14871488
end
14881489

1490+
run_package_callbacks(modkey::ApiId) = run_package_callbacks(modkey.pkg)
14891491
function run_package_callbacks(modkey::PkgId)
14901492
run_extension_callbacks(modkey)
14911493
assert_havelock(require_lock)
@@ -1520,6 +1522,7 @@ const EXT_PRIMED = Dict{PkgId,Vector{PkgId}}() # Extension -> Parent + Triggers
15201522
const EXT_DORMITORY = Dict{PkgId,Vector{ExtensionId}}() # Trigger -> Extensions that can be triggered by it
15211523
const EXT_DORMITORY_FAILED = ExtensionId[]
15221524

1525+
insert_extension_triggers(api::ApiId) = insert_extension_triggers(api.pkg)
15231526
function insert_extension_triggers(pkg::PkgId)
15241527
pkg.uuid === nothing && return
15251528
path_env_loc = locate_package_env(pkg)
@@ -2278,6 +2281,12 @@ function canstart_loading(modkey::PkgId, build_id::UInt128, stalecheck::Bool)
22782281
return cond
22792282
end
22802283

2284+
function start_loading(modkey::ApiId, build_id::UInt128, stalecheck::Bool)
2285+
m = start_loading(modkey.pkg, build_id, true)
2286+
m === nothing && return nothing
2287+
return api_for_loaded_module(modkey, m)
2288+
end
2289+
22812290
function start_loading(modkey::PkgId, build_id::UInt128, stalecheck::Bool)
22822291
# handle recursive and concurrent calls to require
22832292
while true
@@ -2293,6 +2302,7 @@ function start_loading(modkey::PkgId, build_id::UInt128, stalecheck::Bool)
22932302
end
22942303
end
22952304

2305+
end_loading(apikey::ApiId, @nospecialize loaded) = end_loading(apikey.pkg, loaded)
22962306
function end_loading(modkey::PkgId, @nospecialize loaded)
22972307
assert_havelock(require_lock)
22982308
loading = pop!(package_locks, modkey)
@@ -2513,7 +2523,7 @@ function __require(into::Module, mod::Symbol)
25132523
end
25142524
uuidkey, env = uuidkey_env
25152525
if _track_dependencies[]
2516-
path = binpack(uuidkey)
2526+
path = binpack(uuidkey.pkg)
25172527
push!(_require_dependencies, (into, path, UInt64(0), UInt32(0), 0.0))
25182528
end
25192529
return _require_prelocked(uuidkey, env)
@@ -2580,8 +2590,8 @@ function require(uuidkey::PkgId)
25802590
end
25812591
return invoke_in_world(world, __require, uuidkey)
25822592
end
2583-
__require(uuidkey::PkgId) = @lock require_lock _require_prelocked(uuidkey)
2584-
function _require_prelocked(uuidkey::PkgId, env=nothing)
2593+
__require(uuidkey::Union{PkgId, ApiId}) = @lock require_lock _require_prelocked(uuidkey)
2594+
function _require_prelocked(uuidkey::Union{PkgId, ApiId}, env=nothing)
25852595
assert_havelock(require_lock)
25862596
m = start_loading(uuidkey, UInt128(0), true)
25872597
if m === nothing
@@ -2840,16 +2850,18 @@ function __require_prelocked(pkg::PkgId, env)
28402850
return loaded
28412851
end
28422852

2853+
__require_prelocked(apikey::ApiId, env) =
2854+
api_for_loaded_module(apikey, __require_prelocked(apikey.pkg, env))
2855+
28432856
function lookup_api(loaded::Module, api::ApiId)
28442857
if !isdefined(loaded, :_get_versioned_api)
28452858
return loaded
28462859
end
2847-
return loaded._get_versioned_api(api.api)
2860+
return loaded._get_versioned_api(api.compat)::Module
28482861
end
28492862

2850-
function __require_prelocked(apikey::ApiId, env)
2851-
loaded = __require_prelocked(apikey.pkg, env)
2852-
if apikey.api === nothing
2863+
function api_for_loaded_module(apikey::ApiId, loaded::Module)
2864+
if apikey.compat === nothing
28532865
return loaded
28542866
end
28552867
return invokelatest(lookup_api, loaded, apikey)

base/pkgid.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,4 @@ function hash(api::ApiId, h::UInt)
5757
end
5858

5959
show(io::IO, ::MIME"text/plain", api::ApiId) =
60-
print(io, api.pkg, api.compat === nothing ? "" : " at version " * api.compat)
60+
print(io, api.pkg, api.compat === nothing ? "" : " at version compat " * api.compat)

0 commit comments

Comments
 (0)