Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/src/registries.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,39 @@ are the following files: `Compat.toml`, `Deps.toml`, `Package.toml`,
and `Versions.toml`.
The formats of these files are described below.

### Registry Package.toml

The `Package.toml` file contains basic metadata about the package, such as its name, UUID, repository URL, and optional metadata.

#### Package metadata

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.

#### Deprecated packages

One use of the `[metadata]` table is to mark packages as deprecated using `[metadata.deprecated]`. Deprecated packages will:
- Show as `[deprecated]` in package status output
- Be excluded from tab-completion suggestions
- Still be installable and usable

The `[metadata.deprecated]` table can contain arbitrary metadata fields. Two special fields are recognized by Pkg and displayed when using `pkg> status --deprecated`:
- `reason`: A string explaining why the package is deprecated
- `alternative`: A string suggesting a replacement package

Example:

```toml
name = "MyPackage"
uuid = "..."
repo = "..."

[metadata.deprecated]
reason = "This package is no longer maintained"
alternative = "ReplacementPackage"
```

Other fields can be added to `[metadata.deprecated]` for use by registries or other tools.

### Registry Compat.toml

The `Compat.toml` file has a series of blocks specifying version
Expand Down
1 change: 1 addition & 0 deletions ext/REPLExt/completions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ function complete_remote_package!(comps, partial; hint::Bool)
name in cmp && continue
if startswith(regpkg.name, partial)
pkg = Registry.registry_info(regpkg)
Registry.isdeprecated(pkg) && continue
compat_info = Registry.compat_info(pkg)
# Filter versions
for (v, uncompressed_compat) in compat_info
Expand Down
5 changes: 3 additions & 2 deletions src/API.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1314,14 +1314,15 @@ end

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

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())
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())
if compat
diff && pkgerror("Compat status has no `diff` mode")
outdated && pkgerror("Compat status has no `outdated` mode")
deprecated && pkgerror("Compat status has no `deprecated` mode")
extensions && pkgerror("Compat status has no `extensions` mode")
Operations.print_compat(ctx, pkgs; io)
else
Operations.status(ctx.env, ctx.registries, pkgs; mode, git_diff = diff, io, outdated, extensions, workspace)
Operations.status(ctx.env, ctx.registries, pkgs; mode, git_diff = diff, io, outdated, deprecated, extensions, workspace)
end
return nothing
end
Expand Down
66 changes: 61 additions & 5 deletions src/Operations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ function is_pkgversion_yanked(entry::PackageEntry, registries::Vector{Registry.R
return is_pkgversion_yanked(entry.uuid, entry.version, registries)
end

function get_pkg_deprecation_info(pkg::Union{PackageSpec, PackageEntry}, registries::Vector{Registry.RegistryInstance} = Registry.reachable_registries())
pkg.uuid === nothing && return nothing
for reg in registries
reg_pkg = get(reg, pkg.uuid, nothing)
if reg_pkg !== nothing
info = Registry.registry_info(reg_pkg)
if Registry.isdeprecated(info)
return info.deprecated
end
end
end
return nothing
end

function default_preserve()
return if Base.get_bool_env("JULIA_PKG_PRESERVE_TIERED_INSTALLED", false)
PRESERVE_TIERED_INSTALLED
Expand Down Expand Up @@ -2915,11 +2929,12 @@ struct PackageStatusData
compat_data::Union{Nothing, Tuple{Vector{String}, VersionNumber, VersionNumber}}
changed::Bool
extinfo::Union{Nothing, Vector{ExtInfo}}
deprecation_info::Union{Nothing, Dict{String, Any}}
end

function print_status(
env::EnvCache, old_env::Union{Nothing, EnvCache}, registries::Vector{Registry.RegistryInstance}, header::Symbol,
uuids::Vector, names::Vector; manifest = true, diff = false, ignore_indent::Bool, workspace::Bool, outdated::Bool, extensions::Bool, io::IO,
uuids::Vector, names::Vector; manifest = true, diff = false, ignore_indent::Bool, workspace::Bool, outdated::Bool, deprecated::Bool, extensions::Bool, io::IO,
mode::PackageMode, hidden_upgrades_info::Bool, show_usagetips::Bool = true
)
not_installed_indicator = sprint((io, args) -> printstyled(io, args...; color = Base.error_color()), "→", context = io)
Expand Down Expand Up @@ -3003,6 +3018,19 @@ function print_status(
continue
end

# Deprecated info
deprecation_info = nothing
pkg_deprecated = false
if !isnothing(new)
pkg_spec = something(new, old)
deprecation_info = get_pkg_deprecation_info(pkg_spec, registries)
pkg_deprecated = deprecation_info !== nothing
end

# if we are running with deprecated, only show packages that are deprecated
if deprecated && !pkg_deprecated
continue
end

# TODO: Show extension deps for project as well?

Expand All @@ -3021,7 +3049,7 @@ function print_status(
no_visible_packages_heldback &= (!changed || !pkg_heldback)
no_packages_heldback &= !pkg_heldback

push!(package_statuses, PackageStatusData(uuid, old, new, pkg_downloaded, pkg_upgradable, pkg_heldback, cinfo, changed, ext_info))
push!(package_statuses, PackageStatusData(uuid, old, new, pkg_downloaded, pkg_upgradable, pkg_heldback, cinfo, changed, ext_info, deprecation_info))
end

for pkg in package_statuses
Expand Down Expand Up @@ -3054,6 +3082,23 @@ function print_status(
printstyled(io, " [yanked]"; color = :yellow)
end

# show if package is deprecated
if pkg.deprecation_info !== nothing
printstyled(io, " [deprecated]"; color = :yellow)
end

# show deprecation details when using --deprecated flag
if deprecated && !diff && pkg.deprecation_info !== nothing
reason = get(pkg.deprecation_info, "reason", nothing)
alternative = get(pkg.deprecation_info, "alternative", nothing)
if reason !== nothing
printstyled(io, " (reason: ", reason, ")"; color = :yellow)
end
if alternative !== nothing
printstyled(io, " (alternative: ", alternative, ")"; color = :yellow)
end
end

if outdated && !diff && pkg.compat_data !== nothing
packages_holding_back, max_version, max_version_compat = pkg.compat_data
if pkg.new.version !== max_version_compat && max_version_compat != max_version
Expand Down Expand Up @@ -3144,6 +3189,17 @@ function print_status(
It is recommended to update them to resolve a valid version.""", color = Base.warn_color(), ignore_indent)
end

# Check if any packages are deprecated for info message
any_deprecated_packages = any(pkg -> pkg.deprecation_info !== nothing, package_statuses)

# Add info for deprecated packages (only if not already in deprecated mode)
if !deprecated && any_deprecated_packages
deprecated_str = sprint((io, args) -> printstyled(io, args...; color = :yellow), "[deprecated]", context = io)
tipend = manifest ? " -m" : ""
tip = show_usagetips ? " Use `status --deprecated$tipend` to see more information." : ""
printpkgstyle(io, :Info, """Packages marked with $deprecated_str are no longer maintained.$tip""", color = Base.info_color(), ignore_indent)
end

return nothing
end

Expand Down Expand Up @@ -3175,7 +3231,7 @@ end
function status(
env::EnvCache, registries::Vector{Registry.RegistryInstance}, pkgs::Vector{PackageSpec} = PackageSpec[];
header = nothing, mode::PackageMode = PKGMODE_PROJECT, git_diff::Bool = false, env_diff = nothing, ignore_indent = true,
io::IO, workspace::Bool = false, outdated::Bool = false, extensions::Bool = false, hidden_upgrades_info::Bool = false, show_usagetips::Bool = true
io::IO, workspace::Bool = false, outdated::Bool = false, deprecated::Bool = false, extensions::Bool = false, hidden_upgrades_info::Bool = false, show_usagetips::Bool = true
)
io == Base.devnull && return
# if a package, print header
Expand Down Expand Up @@ -3206,10 +3262,10 @@ function status(
diff = old_env !== nothing
header = something(header, diff ? :Diff : :Status)
if mode == PKGMODE_PROJECT || mode == PKGMODE_COMBINED
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)
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)
end
if mode == PKGMODE_MANIFEST || mode == PKGMODE_COMBINED
print_status(env, old_env, registries, header, filter_uuids, filter_names; diff, ignore_indent, io, workspace, outdated, extensions, mode, hidden_upgrades_info, show_usagetips)
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)
end
return if is_manifest_current(env) === false
tip = if show_usagetips
Expand Down
12 changes: 8 additions & 4 deletions src/REPLMode/command_declarations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -435,16 +435,17 @@ compound_declarations = [
PSA[:name => "manifest", :short_name => "m", :api => :mode => PKGMODE_MANIFEST],
PSA[:name => "diff", :short_name => "d", :api => :diff => true],
PSA[:name => "outdated", :short_name => "o", :api => :outdated => true],
PSA[:name => "deprecated", :api => :deprecated => true],
PSA[:name => "compat", :short_name => "c", :api => :compat => true],
PSA[:name => "extensions", :short_name => "e", :api => :extensions => true],
PSA[:name => "workspace", :api => :workspace => true],
],
:completions => :complete_installed_packages,
:description => "summarize contents of and changes to environment",
:help => md"""
[st|status] [-d|--diff] [--workspace] [-o|--outdated] [pkgs...]
[st|status] [-d|--diff] [--workspace] [-o|--outdated] [-p|--project] [pkgs...]
[st|status] [-d|--diff] [--workspace] [-o|--outdated] [-m|--manifest] [pkgs...]
[st|status] [-d|--diff] [--workspace] [-o|--outdated] [--deprecated] [pkgs...]
[st|status] [-d|--diff] [--workspace] [-o|--outdated] [--deprecated] [-p|--project] [pkgs...]
[st|status] [-d|--diff] [--workspace] [-o|--outdated] [--deprecated] [-m|--manifest] [pkgs...]
[st|status] [-d|--diff] [--workspace] [-e|--extensions] [-p|--project] [pkgs...]
[st|status] [-d|--diff] [--workspace] [-e|--extensions] [-m|--manifest] [pkgs...]
[st|status] [-c|--compat] [pkgs...]
Expand All @@ -455,7 +456,10 @@ compound_declarations = [
constraints. To see why use `pkg> status --outdated` which shows any packages
that are not at their latest version and if any packages are holding them back.
Packages marked with `[yanked]` have been yanked from the registry and should be
updated or removed.
updated or removed. Packages marked with `[deprecated]` are no longer maintained.
Use `pkg> status --deprecated` to show only deprecated packages along with deprecation
information such as the reason and alternative packages (if provided by the registry).
Use `pkg> status --extensions` to show dependencies with extensions and what extension dependencies
of those that are currently loaded.
Expand Down
11 changes: 10 additions & 1 deletion src/Registry/registry_instance.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ struct PkgInfo
repo::Union{String, Nothing}
subdir::Union{String, Nothing}

# Package.toml [metadata.deprecated]:
deprecated::Union{Dict{String, Any}, Nothing}

# Versions.toml:
version_info::Dict{VersionNumber, VersionInfo}

Expand All @@ -68,6 +71,7 @@ end

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

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

# The presence of a [metadata.deprecated] table indicates the package is deprecated
# We store the raw table to allow other tools to use the metadata
metadata = get(d_p, "metadata", nothing)::Union{Nothing, Dict{String, Any}}
deprecated = metadata !== nothing ? get(metadata, "deprecated", nothing)::Union{Nothing, Dict{String, Any}} : nothing

# Versions.toml
d_v = custom_isfile(pkg.in_memory_registry, pkg.registry_path, joinpath(pkg.path, "Versions.toml")) ?
parsefile(pkg.in_memory_registry, pkg.registry_path, joinpath(pkg.path, "Versions.toml")) : Dict{String, Any}()
Expand Down Expand Up @@ -256,7 +265,7 @@ function init_package_info!(pkg::PkgEntry)
end

@assert !isdefined(pkg, :info)
pkg.info = PkgInfo(repo, subdir, version_info, compat, deps, weak_compat, weak_deps, pkg.info_lock)
pkg.info = PkgInfo(repo, subdir, deprecated, version_info, compat, deps, weak_compat, weak_deps, pkg.info_lock)

return pkg.info
end
Expand Down
87 changes: 85 additions & 2 deletions test/registry.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ using Pkg, UUIDs, LibGit2, Test
using Pkg: depots1
using Pkg.REPLMode: pkgstr
using Pkg.Types: PkgError, manifest_info, PackageSpec, EnvCache
using Pkg.Operations: get_pkg_deprecation_info

using Dates: Second

using ..Utils
Expand Down Expand Up @@ -41,8 +43,6 @@ function setup_test_registries(dir = pwd())
)
write(
joinpath(regpath, "Example", "Deps.toml"), """
["0.5"]
julia = "0.6-1.0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this change?

Copy link
Member Author

@KristofferC KristofferC Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a completely bogus entry, the Deps.toml file don't have compat info like that. It has name -> uuid entries, e.g.:

https://github.com/JuliaRegistries/General/blob/master/A/AAindex/Deps.toml

I guess nothing read that file before.

"""
)
write(
Expand Down Expand Up @@ -343,6 +343,89 @@ end
end
end

@testset "deprecated package" begin
temp_pkg_dir() do depot
# Set up test registries with an extra deprecated package
regdir = mktempdir()
setup_test_registries(regdir)

# Add a deprecated package to the first registry
regpath = joinpath(regdir, "RegistryFoo1")
mkpath(joinpath(regpath, "DeprecatedExample"))

# Add the deprecated package to Registry.toml
registry_toml = read(joinpath(regpath, "Registry.toml"), String)
registry_toml = replace(
registry_toml,
"[packages]" =>
"[packages]\n11111111-1111-1111-1111-111111111111 = { name = \"DeprecatedExample\", path = \"DeprecatedExample\" }"
)
write(joinpath(regpath, "Registry.toml"), registry_toml)

# Create deprecated package with [metadata.deprecated] table
write(
joinpath(regpath, "DeprecatedExample", "Package.toml"), """
name = "DeprecatedExample"
uuid = "11111111-1111-1111-1111-111111111111"
repo = "https://github.com/test/DeprecatedExample.jl.git"

[metadata.deprecated]
reason = "This package is no longer maintained"
alternative = "Example"
"""
)

write(
joinpath(regpath, "DeprecatedExample", "Versions.toml"), """
["1.0.0"]
git-tree-sha1 = "1234567890abcdef1234567890abcdef12345678"
"""
)

git_init_and_commit(regpath)

# Add the test registry
Pkg.Registry.add(url = regpath)

# Test that the package is marked as deprecated
registries = Pkg.Registry.reachable_registries()
reg_idx = findfirst(r -> r.name == "RegistryFoo", registries)
@test reg_idx !== nothing

reg = registries[reg_idx]
pkg_uuid = UUID("11111111-1111-1111-1111-111111111111")
@test haskey(reg, pkg_uuid)

pkg_entry = reg[pkg_uuid]
pkg_info = Pkg.Registry.registry_info(pkg_entry)

# Test that deprecated info is loaded correctly
@test Pkg.Registry.isdeprecated(pkg_info)
@test pkg_info.deprecated !== nothing
@test pkg_info.deprecated["reason"] == "This package is no longer maintained"
@test pkg_info.deprecated["alternative"] == "Example"

# Test that non-deprecated package is not marked as deprecated
example1_uuid = UUID("c5f1542f-b8aa-45da-ab42-05303d706c66")
example1_entry = reg[example1_uuid]
example1_info = Pkg.Registry.registry_info(example1_entry)
@test !Pkg.Registry.isdeprecated(example1_info)
@test example1_info.deprecated === nothing

# Test get_pkg_deprecation_info function
deprecated_pkg_spec = Pkg.Types.PackageSpec(name = "DeprecatedExample", uuid = pkg_uuid)
normal_pkg_spec = Pkg.Types.PackageSpec(name = "Example1", uuid = example1_uuid)

dep_info = get_pkg_deprecation_info(deprecated_pkg_spec, registries)
@test dep_info !== nothing
@test dep_info["reason"] == "This package is no longer maintained"
@test dep_info["alternative"] == "Example"

normal_info = get_pkg_deprecation_info(normal_pkg_spec, registries)
@test normal_info === nothing
end
end

@testset "yanking" begin
uuid = Base.UUID("7876af07-990d-54b4-ab0e-23690620f79a") # Example
# Tests that [email protected] does not get installed
Expand Down