Skip to content

Commit ce2245c

Browse files
authored
allow for a metadata table in a Package.toml in a registry and have a deprecated sub-table indicate the package is deprecated (#4433)
1 parent 63d720b commit ce2245c

File tree

7 files changed

+201
-14
lines changed

7 files changed

+201
-14
lines changed

docs/src/registries.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,39 @@ are the following files: `Compat.toml`, `Deps.toml`, `Package.toml`,
9999
and `Versions.toml`.
100100
The formats of these files are described below.
101101

102+
### Registry Package.toml
103+
104+
The `Package.toml` file contains basic metadata about the package, such as its name, UUID, repository URL, and optional metadata.
105+
106+
#### Package metadata
107+
108+
The `[metadata]` table in `Package.toml` provides a location for metadata about the package that doesn't fit into the other registry files. This is an extensible framework for adding package-level metadata.
109+
110+
#### Deprecated packages
111+
112+
One use of the `[metadata]` table is to mark packages as deprecated using `[metadata.deprecated]`. Deprecated packages will:
113+
- Show as `[deprecated]` in package status output
114+
- Be excluded from tab-completion suggestions
115+
- Still be installable and usable
116+
117+
The `[metadata.deprecated]` table can contain arbitrary metadata fields. Two special fields are recognized by Pkg and displayed when using `pkg> status --deprecated`:
118+
- `reason`: A string explaining why the package is deprecated
119+
- `alternative`: A string suggesting a replacement package
120+
121+
Example:
122+
123+
```toml
124+
name = "MyPackage"
125+
uuid = "..."
126+
repo = "..."
127+
128+
[metadata.deprecated]
129+
reason = "This package is no longer maintained"
130+
alternative = "ReplacementPackage"
131+
```
132+
133+
Other fields can be added to `[metadata.deprecated]` for use by registries or other tools.
134+
102135
### Registry Compat.toml
103136

104137
The `Compat.toml` file has a series of blocks specifying version

ext/REPLExt/completions.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function complete_remote_package!(comps, partial; hint::Bool)
7979
name in cmp && continue
8080
if startswith(regpkg.name, partial)
8181
pkg = Registry.registry_info(regpkg)
82+
Registry.isdeprecated(pkg) && continue
8283
compat_info = Registry.compat_info(pkg)
8384
# Filter versions
8485
for (v, uncompressed_compat) in compat_info

src/API.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,14 +1351,15 @@ end
13511351

13521352
@deprecate status(mode::PackageMode) status(mode = mode)
13531353

1354-
function status(ctx::Context, pkgs::Vector{PackageSpec}; diff::Bool = false, mode = PKGMODE_PROJECT, workspace::Bool = false, outdated::Bool = false, compat::Bool = false, extensions::Bool = false, io::IO = stdout_f())
1354+
function status(ctx::Context, pkgs::Vector{PackageSpec}; diff::Bool = false, mode = PKGMODE_PROJECT, workspace::Bool = false, outdated::Bool = false, deprecated::Bool = false, compat::Bool = false, extensions::Bool = false, io::IO = stdout_f())
13551355
if compat
13561356
diff && pkgerror("Compat status has no `diff` mode")
13571357
outdated && pkgerror("Compat status has no `outdated` mode")
1358+
deprecated && pkgerror("Compat status has no `deprecated` mode")
13581359
extensions && pkgerror("Compat status has no `extensions` mode")
13591360
Operations.print_compat(ctx, pkgs; io)
13601361
else
1361-
Operations.status(ctx.env, ctx.registries, pkgs; mode, git_diff = diff, io, outdated, extensions, workspace)
1362+
Operations.status(ctx.env, ctx.registries, pkgs; mode, git_diff = diff, io, outdated, deprecated, extensions, workspace)
13621363
end
13631364
return nothing
13641365
end

src/Operations.jl

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ function is_pkgversion_yanked(entry::PackageEntry, registries::Vector{Registry.R
5050
return is_pkgversion_yanked(entry.uuid, entry.version, registries)
5151
end
5252

53+
function get_pkg_deprecation_info(pkg::Union{PackageSpec, PackageEntry}, registries::Vector{Registry.RegistryInstance} = Registry.reachable_registries())
54+
pkg.uuid === nothing && return nothing
55+
for reg in registries
56+
reg_pkg = get(reg, pkg.uuid, nothing)
57+
if reg_pkg !== nothing
58+
info = Registry.registry_info(reg_pkg)
59+
if Registry.isdeprecated(info)
60+
return info.deprecated
61+
end
62+
end
63+
end
64+
return nothing
65+
end
66+
5367
function default_preserve()
5468
return if Base.get_bool_env("JULIA_PKG_PRESERVE_TIERED_INSTALLED", false)
5569
PRESERVE_TIERED_INSTALLED
@@ -3159,11 +3173,12 @@ struct PackageStatusData
31593173
compat_data::Union{Nothing, Tuple{Vector{String}, VersionNumber, VersionNumber}}
31603174
changed::Bool
31613175
extinfo::Union{Nothing, Vector{ExtInfo}}
3176+
deprecation_info::Union{Nothing, Dict{String, Any}}
31623177
end
31633178

31643179
function print_status(
31653180
env::EnvCache, old_env::Union{Nothing, EnvCache}, registries::Vector{Registry.RegistryInstance}, header::Symbol,
3166-
uuids::Vector, names::Vector; manifest = true, diff = false, ignore_indent::Bool, workspace::Bool, outdated::Bool, extensions::Bool, io::IO,
3181+
uuids::Vector, names::Vector; manifest = true, diff = false, ignore_indent::Bool, workspace::Bool, outdated::Bool, deprecated::Bool, extensions::Bool, io::IO,
31673182
mode::PackageMode, hidden_upgrades_info::Bool, show_usagetips::Bool = true
31683183
)
31693184
not_installed_indicator = sprint((io, args) -> printstyled(io, args...; color = Base.error_color()), "", context = io)
@@ -3267,6 +3282,19 @@ function print_status(
32673282
continue
32683283
end
32693284

3285+
# Deprecated info
3286+
deprecation_info = nothing
3287+
pkg_deprecated = false
3288+
if !isnothing(new)
3289+
pkg_spec = something(new, old)
3290+
deprecation_info = get_pkg_deprecation_info(pkg_spec, registries)
3291+
pkg_deprecated = deprecation_info !== nothing
3292+
end
3293+
3294+
# if we are running with deprecated, only show packages that are deprecated
3295+
if deprecated && !pkg_deprecated
3296+
continue
3297+
end
32703298

32713299
# TODO: Show extension deps for project as well?
32723300

@@ -3285,7 +3313,7 @@ function print_status(
32853313
no_visible_packages_heldback &= (!changed || !pkg_heldback)
32863314
no_packages_heldback &= !pkg_heldback
32873315

3288-
push!(package_statuses, PackageStatusData(uuid, old, new, pkg_downloaded, pkg_upgradable, pkg_heldback, cinfo, changed, ext_info))
3316+
push!(package_statuses, PackageStatusData(uuid, old, new, pkg_downloaded, pkg_upgradable, pkg_heldback, cinfo, changed, ext_info, deprecation_info))
32893317
end
32903318

32913319
for pkg in package_statuses
@@ -3318,6 +3346,23 @@ function print_status(
33183346
printstyled(io, " [yanked]"; color = :yellow)
33193347
end
33203348

3349+
# show if package is deprecated
3350+
if pkg.deprecation_info !== nothing
3351+
printstyled(io, " [deprecated]"; color = :yellow)
3352+
end
3353+
3354+
# show deprecation details when using --deprecated flag
3355+
if deprecated && !diff && pkg.deprecation_info !== nothing
3356+
reason = get(pkg.deprecation_info, "reason", nothing)
3357+
alternative = get(pkg.deprecation_info, "alternative", nothing)
3358+
if reason !== nothing
3359+
printstyled(io, " (reason: ", reason, ")"; color = :yellow)
3360+
end
3361+
if alternative !== nothing
3362+
printstyled(io, " (alternative: ", alternative, ")"; color = :yellow)
3363+
end
3364+
end
3365+
33213366
if outdated && !diff && pkg.compat_data !== nothing
33223367
packages_holding_back, max_version, max_version_compat = pkg.compat_data
33233368
if pkg.new.version !== max_version_compat && max_version_compat != max_version
@@ -3408,6 +3453,17 @@ function print_status(
34083453
It is recommended to update them to resolve a valid version.""", color = Base.warn_color(), ignore_indent)
34093454
end
34103455

3456+
# Check if any packages are deprecated for info message
3457+
any_deprecated_packages = any(pkg -> pkg.deprecation_info !== nothing, package_statuses)
3458+
3459+
# Add info for deprecated packages (only if not already in deprecated mode)
3460+
if !deprecated && any_deprecated_packages
3461+
deprecated_str = sprint((io, args) -> printstyled(io, args...; color = :yellow), "[deprecated]", context = io)
3462+
tipend = manifest ? " -m" : ""
3463+
tip = show_usagetips ? " Use `status --deprecated$tipend` to see more information." : ""
3464+
printpkgstyle(io, :Info, """Packages marked with $deprecated_str are no longer maintained.$tip""", color = Base.info_color(), ignore_indent)
3465+
end
3466+
34113467
return nothing
34123468
end
34133469

@@ -3439,7 +3495,7 @@ end
34393495
function status(
34403496
env::EnvCache, registries::Vector{Registry.RegistryInstance}, pkgs::Vector{PackageSpec} = PackageSpec[];
34413497
header = nothing, mode::PackageMode = PKGMODE_PROJECT, git_diff::Bool = false, env_diff = nothing, ignore_indent = true,
3442-
io::IO, workspace::Bool = false, outdated::Bool = false, extensions::Bool = false, hidden_upgrades_info::Bool = false, show_usagetips::Bool = true
3498+
io::IO, workspace::Bool = false, outdated::Bool = false, deprecated::Bool = false, extensions::Bool = false, hidden_upgrades_info::Bool = false, show_usagetips::Bool = true
34433499
)
34443500
io == Base.devnull && return
34453501
# if a package, print header
@@ -3470,10 +3526,10 @@ function status(
34703526
diff = old_env !== nothing
34713527
header = something(header, diff ? :Diff : :Status)
34723528
if mode == PKGMODE_PROJECT || mode == PKGMODE_COMBINED
3473-
print_status(env, old_env, registries, header, filter_uuids, filter_names; manifest = false, diff, ignore_indent, io, workspace, outdated, extensions, mode, hidden_upgrades_info, show_usagetips)
3529+
print_status(env, old_env, registries, header, filter_uuids, filter_names; manifest = false, diff, ignore_indent, io, workspace, outdated, deprecated, extensions, mode, hidden_upgrades_info, show_usagetips)
34743530
end
34753531
if mode == PKGMODE_MANIFEST || mode == PKGMODE_COMBINED
3476-
print_status(env, old_env, registries, header, filter_uuids, filter_names; diff, ignore_indent, io, workspace, outdated, extensions, mode, hidden_upgrades_info, show_usagetips)
3532+
print_status(env, old_env, registries, header, filter_uuids, filter_names; diff, ignore_indent, io, workspace, outdated, deprecated, extensions, mode, hidden_upgrades_info, show_usagetips)
34773533
end
34783534
return if is_manifest_current(env) === false
34793535
tip = if show_usagetips

src/REPLMode/command_declarations.jl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -441,16 +441,17 @@ compound_declarations = [
441441
PSA[:name => "manifest", :short_name => "m", :api => :mode => PKGMODE_MANIFEST],
442442
PSA[:name => "diff", :short_name => "d", :api => :diff => true],
443443
PSA[:name => "outdated", :short_name => "o", :api => :outdated => true],
444+
PSA[:name => "deprecated", :api => :deprecated => true],
444445
PSA[:name => "compat", :short_name => "c", :api => :compat => true],
445446
PSA[:name => "extensions", :short_name => "e", :api => :extensions => true],
446447
PSA[:name => "workspace", :api => :workspace => true],
447448
],
448449
:completions => :complete_installed_packages,
449450
:description => "summarize contents of and changes to environment",
450451
:help => md"""
451-
[st|status] [-d|--diff] [--workspace] [-o|--outdated] [pkgs...]
452-
[st|status] [-d|--diff] [--workspace] [-o|--outdated] [-p|--project] [pkgs...]
453-
[st|status] [-d|--diff] [--workspace] [-o|--outdated] [-m|--manifest] [pkgs...]
452+
[st|status] [-d|--diff] [--workspace] [-o|--outdated] [--deprecated] [pkgs...]
453+
[st|status] [-d|--diff] [--workspace] [-o|--outdated] [--deprecated] [-p|--project] [pkgs...]
454+
[st|status] [-d|--diff] [--workspace] [-o|--outdated] [--deprecated] [-m|--manifest] [pkgs...]
454455
[st|status] [-d|--diff] [--workspace] [-e|--extensions] [-p|--project] [pkgs...]
455456
[st|status] [-d|--diff] [--workspace] [-e|--extensions] [-m|--manifest] [pkgs...]
456457
[st|status] [-c|--compat] [pkgs...]
@@ -461,7 +462,10 @@ compound_declarations = [
461462
constraints. To see why use `pkg> status --outdated` which shows any packages
462463
that are not at their latest version and if any packages are holding them back.
463464
Packages marked with `[yanked]` have been yanked from the registry and should be
464-
updated or removed.
465+
updated or removed. Packages marked with `[deprecated]` are no longer maintained.
466+
467+
Use `pkg> status --deprecated` to show only deprecated packages along with deprecation
468+
information such as the reason and alternative packages (if provided by the registry).
465469
466470
Use `pkg> status --extensions` to show dependencies with extensions and what extension dependencies
467471
of those that are currently loaded.

src/Registry/registry_instance.jl

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ struct PkgInfo
4848
repo::Union{String, Nothing}
4949
subdir::Union{String, Nothing}
5050

51+
# Package.toml [metadata.deprecated]:
52+
deprecated::Union{Dict{String, Any}, Nothing}
53+
5154
# Versions.toml:
5255
version_info::Dict{VersionNumber, VersionInfo}
5356

@@ -68,6 +71,7 @@ end
6871

6972
isyanked(pkg::PkgInfo, v::VersionNumber) = pkg.version_info[v].yanked
7073
treehash(pkg::PkgInfo, v::VersionNumber) = pkg.version_info[v].git_tree_sha1
74+
isdeprecated(pkg::PkgInfo) = pkg.deprecated !== nothing
7175

7276
function uncompress(compressed::Dict{VersionRange, Dict{String, T}}, vsorted::Vector{VersionNumber}) where {T}
7377
@assert issorted(vsorted)
@@ -201,6 +205,11 @@ function init_package_info!(pkg::PkgEntry)
201205
repo = get(d_p, "repo", nothing)::Union{Nothing, String}
202206
subdir = get(d_p, "subdir", nothing)::Union{Nothing, String}
203207

208+
# The presence of a [metadata.deprecated] table indicates the package is deprecated
209+
# We store the raw table to allow other tools to use the metadata
210+
metadata = get(d_p, "metadata", nothing)::Union{Nothing, Dict{String, Any}}
211+
deprecated = metadata !== nothing ? get(metadata, "deprecated", nothing)::Union{Nothing, Dict{String, Any}} : nothing
212+
204213
# Versions.toml
205214
d_v = custom_isfile(pkg.in_memory_registry, pkg.registry_path, joinpath(pkg.path, "Versions.toml")) ?
206215
parsefile(pkg.in_memory_registry, pkg.registry_path, joinpath(pkg.path, "Versions.toml")) : Dict{String, Any}()
@@ -256,7 +265,7 @@ function init_package_info!(pkg::PkgEntry)
256265
end
257266

258267
@assert !isdefined(pkg, :info)
259-
pkg.info = PkgInfo(repo, subdir, version_info, compat, deps, weak_compat, weak_deps, pkg.info_lock)
268+
pkg.info = PkgInfo(repo, subdir, deprecated, version_info, compat, deps, weak_compat, weak_deps, pkg.info_lock)
260269

261270
return pkg.info
262271
end

test/registry.jl

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ using Pkg, UUIDs, LibGit2, Test
55
using Pkg: depots1
66
using Pkg.REPLMode: pkgstr
77
using Pkg.Types: PkgError, manifest_info, PackageSpec, EnvCache
8+
using Pkg.Operations: get_pkg_deprecation_info
9+
810
using Dates: Second
911

1012
using ..Utils
@@ -41,8 +43,6 @@ function setup_test_registries(dir = pwd())
4143
)
4244
write(
4345
joinpath(regpath, "Example", "Deps.toml"), """
44-
["0.5"]
45-
julia = "0.6-1.0"
4646
"""
4747
)
4848
write(
@@ -343,6 +343,89 @@ end
343343
end
344344
end
345345

346+
@testset "deprecated package" begin
347+
temp_pkg_dir() do depot
348+
# Set up test registries with an extra deprecated package
349+
regdir = mktempdir()
350+
setup_test_registries(regdir)
351+
352+
# Add a deprecated package to the first registry
353+
regpath = joinpath(regdir, "RegistryFoo1")
354+
mkpath(joinpath(regpath, "DeprecatedExample"))
355+
356+
# Add the deprecated package to Registry.toml
357+
registry_toml = read(joinpath(regpath, "Registry.toml"), String)
358+
registry_toml = replace(
359+
registry_toml,
360+
"[packages]" =>
361+
"[packages]\n11111111-1111-1111-1111-111111111111 = { name = \"DeprecatedExample\", path = \"DeprecatedExample\" }"
362+
)
363+
write(joinpath(regpath, "Registry.toml"), registry_toml)
364+
365+
# Create deprecated package with [metadata.deprecated] table
366+
write(
367+
joinpath(regpath, "DeprecatedExample", "Package.toml"), """
368+
name = "DeprecatedExample"
369+
uuid = "11111111-1111-1111-1111-111111111111"
370+
repo = "https://github.com/test/DeprecatedExample.jl.git"
371+
372+
[metadata.deprecated]
373+
reason = "This package is no longer maintained"
374+
alternative = "Example"
375+
"""
376+
)
377+
378+
write(
379+
joinpath(regpath, "DeprecatedExample", "Versions.toml"), """
380+
["1.0.0"]
381+
git-tree-sha1 = "1234567890abcdef1234567890abcdef12345678"
382+
"""
383+
)
384+
385+
git_init_and_commit(regpath)
386+
387+
# Add the test registry
388+
Pkg.Registry.add(url = regpath)
389+
390+
# Test that the package is marked as deprecated
391+
registries = Pkg.Registry.reachable_registries()
392+
reg_idx = findfirst(r -> r.name == "RegistryFoo", registries)
393+
@test reg_idx !== nothing
394+
395+
reg = registries[reg_idx]
396+
pkg_uuid = UUID("11111111-1111-1111-1111-111111111111")
397+
@test haskey(reg, pkg_uuid)
398+
399+
pkg_entry = reg[pkg_uuid]
400+
pkg_info = Pkg.Registry.registry_info(pkg_entry)
401+
402+
# Test that deprecated info is loaded correctly
403+
@test Pkg.Registry.isdeprecated(pkg_info)
404+
@test pkg_info.deprecated !== nothing
405+
@test pkg_info.deprecated["reason"] == "This package is no longer maintained"
406+
@test pkg_info.deprecated["alternative"] == "Example"
407+
408+
# Test that non-deprecated package is not marked as deprecated
409+
example1_uuid = UUID("c5f1542f-b8aa-45da-ab42-05303d706c66")
410+
example1_entry = reg[example1_uuid]
411+
example1_info = Pkg.Registry.registry_info(example1_entry)
412+
@test !Pkg.Registry.isdeprecated(example1_info)
413+
@test example1_info.deprecated === nothing
414+
415+
# Test get_pkg_deprecation_info function
416+
deprecated_pkg_spec = Pkg.Types.PackageSpec(name = "DeprecatedExample", uuid = pkg_uuid)
417+
normal_pkg_spec = Pkg.Types.PackageSpec(name = "Example1", uuid = example1_uuid)
418+
419+
dep_info = get_pkg_deprecation_info(deprecated_pkg_spec, registries)
420+
@test dep_info !== nothing
421+
@test dep_info["reason"] == "This package is no longer maintained"
422+
@test dep_info["alternative"] == "Example"
423+
424+
normal_info = get_pkg_deprecation_info(normal_pkg_spec, registries)
425+
@test normal_info === nothing
426+
end
427+
end
428+
346429
@testset "yanking" begin
347430
uuid = Base.UUID("7876af07-990d-54b4-ab0e-23690620f79a") # Example
348431
# Tests that [email protected] does not get installed

0 commit comments

Comments
 (0)