diff --git a/docs/src/registries.md b/docs/src/registries.md index cada0bdadf..d5e3657f19 100644 --- a/docs/src/registries.md +++ b/docs/src/registries.md @@ -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 diff --git a/ext/REPLExt/completions.jl b/ext/REPLExt/completions.jl index da75efc19e..8a494cd2ff 100644 --- a/ext/REPLExt/completions.jl +++ b/ext/REPLExt/completions.jl @@ -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 diff --git a/src/API.jl b/src/API.jl index ca965f0d2e..d7d3410ead 100644 --- a/src/API.jl +++ b/src/API.jl @@ -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 diff --git a/src/Operations.jl b/src/Operations.jl index 591e0305e4..1dea0e182c 100644 --- a/src/Operations.jl +++ b/src/Operations.jl @@ -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 @@ -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) @@ -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? @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src/REPLMode/command_declarations.jl b/src/REPLMode/command_declarations.jl index c6bf19db3a..b818f7344a 100644 --- a/src/REPLMode/command_declarations.jl +++ b/src/REPLMode/command_declarations.jl @@ -435,6 +435,7 @@ 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], @@ -442,9 +443,9 @@ compound_declarations = [ :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...] @@ -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. diff --git a/src/Registry/registry_instance.jl b/src/Registry/registry_instance.jl index 38e3cd7077..447815dae8 100644 --- a/src/Registry/registry_instance.jl +++ b/src/Registry/registry_instance.jl @@ -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} @@ -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) @@ -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}() @@ -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 diff --git a/test/registry.jl b/test/registry.jl index ad30823bd9..1948016ffc 100644 --- a/test/registry.jl +++ b/test/registry.jl @@ -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 @@ -41,8 +43,6 @@ function setup_test_registries(dir = pwd()) ) write( joinpath(regpath, "Example", "Deps.toml"), """ - ["0.5"] - julia = "0.6-1.0" """ ) write( @@ -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 Example@0.5.1 does not get installed