Skip to content

Commit aa2a044

Browse files
committed
allow setting a period for how old manifests should be considered obsolete
fixes #2685
1 parent ce2245c commit aa2a044

File tree

3 files changed

+127
-24
lines changed

3 files changed

+127
-24
lines changed

src/API.jl

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -579,21 +579,7 @@ end
579579
const UsageDict = Dict{String, DateTime}
580580
const UsageByDepotDict = Dict{String, UsageDict}
581581

582-
"""
583-
gc(ctx::Context=Context(); verbose=false, force=false, kwargs...)
584-
585-
Garbage-collect package and artifact installations by sweeping over all known
586-
`Manifest.toml` and `Artifacts.toml` files, noting those that have been deleted, and then
587-
finding artifacts and packages that are thereafter not used by any other projects.
588-
Unused packages, artifacts, repos, and scratch spaces are immediately deleted.
589-
590-
Garbage collection is only applied to the "user depot", e.g. the first entry in the
591-
depot path. If you want to run `gc` on all depots set `force=true` (this might require
592-
admin privileges depending on the setup).
593-
594-
Use verbose mode (`verbose=true`) for detailed output.
595-
"""
596-
function gc(ctx::Context = Context(); collect_delay::Union{Period, Nothing} = nothing, verbose = false, force = false, kwargs...)
582+
function gc(ctx::Context = Context(); collect_delay::Union{Period, Nothing} = nothing, verbose = false, force = false, collect_unused_for::Union{Period, Nothing} = nothing, kwargs...)
597583
Context!(ctx; kwargs...)
598584
if collect_delay !== nothing
599585
@warn "The `collect_delay` parameter is no longer used. Packages are now deleted immediately when they become unreachable."
@@ -690,6 +676,33 @@ function gc(ctx::Context = Context(); collect_delay::Union{Period, Nothing} = no
690676
all_scratch_dirs = Set(filter(Pkg.isdir_nothrow, all_scratch_dirs))
691677
all_scratch_parents = Set(filter(Pkg.isfile_nothrow, all_scratch_parents))
692678

679+
# Apply time-based filtering if collect_unused_for is specified
680+
# This creates a separate filtered set for marking packages as active,
681+
# but preserves the full manifest list for writing back to usage files
682+
manifest_tomls_for_gc = all_manifest_tomls
683+
if collect_unused_for !== nothing
684+
# Create a unified usage dict to check timestamps across all depots
685+
unified_manifest_usage = UsageDict()
686+
for (depot, usage) in manifest_usage_by_depot
687+
for (manifest, time) in usage
688+
# Keep the most recent time if a manifest appears in multiple depots
689+
unified_manifest_usage[manifest] = max(get(unified_manifest_usage, manifest, DateTime(0)), time)
690+
end
691+
end
692+
693+
cutoff_time = now() - collect_unused_for
694+
# Filter out manifests that haven't been used since the cutoff time
695+
# This only affects which packages are marked as active for this GC run
696+
manifest_tomls_for_gc = Set(f for f in all_manifest_tomls if get(unified_manifest_usage, f, DateTime(0)) >= cutoff_time)
697+
698+
if verbose
699+
n_filtered = length(all_manifest_tomls) - length(manifest_tomls_for_gc)
700+
if n_filtered > 0
701+
printpkgstyle(ctx.io, :Filtered, "$(n_filtered) manifest(s) older than $(collect_unused_for)")
702+
end
703+
end
704+
end
705+
693706
# Immediately write these back as condensed toml files
694707
function write_condensed_toml(f::Function, usage_by_depot, fname)
695708
for (depot, usage) in usage_by_depot
@@ -868,9 +881,10 @@ function gc(ctx::Context = Context(); collect_delay::Union{Period, Nothing} = no
868881

869882

870883
# Scan manifests, parse them, read in all UUIDs listed and mark those as active
884+
# Use manifest_tomls_for_gc which excludes old manifests if collect_unused_for is set
871885
# printpkgstyle(ctx.io, :Active, "manifests:")
872886
packages_to_keep = mark(
873-
process_manifest_pkgs, all_manifest_tomls, ctx,
887+
process_manifest_pkgs, manifest_tomls_for_gc, ctx,
874888
verbose = verbose, file_str = "manifest files"
875889
)
876890

@@ -881,7 +895,7 @@ function gc(ctx::Context = Context(); collect_delay::Union{Period, Nothing} = no
881895
x -> process_artifacts_toml(x, String[]),
882896
all_artifact_tomls, ctx; verbose = verbose, file_str = "artifact files"
883897
)
884-
repos_to_keep = mark(process_manifest_repos, all_manifest_tomls, ctx; do_print = false)
898+
repos_to_keep = mark(process_manifest_repos, manifest_tomls_for_gc, ctx; do_print = false)
885899
# printpkgstyle(ctx.io, :Active, "scratchspaces:")
886900
spaces_to_keep = mark(
887901
x -> process_scratchspace(x, String[]),

src/Pkg.jl

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -417,17 +417,21 @@ Pkg.test("foo"; test_args=["--extended"])
417417
const test = API.test
418418

419419
"""
420-
Pkg.gc(; collect_delay::Period=Day(7), io::IO=stderr)
420+
Pkg.gc(; verbose=false, force=false, collect_unused_for=nothing, kwargs...)
421421
422422
Garbage-collect package and artifact installations by sweeping over all known
423423
`Manifest.toml` and `Artifacts.toml` files, noting those that have been deleted, and then
424-
finding artifacts and packages that are thereafter not used by any other projects,
425-
marking them as "orphaned". This method will only remove orphaned objects (package
426-
versions, artifacts, and scratch spaces) that have been continually un-used for a period
427-
of `collect_delay`; which defaults to seven days.
424+
finding artifacts and packages that are thereafter not used by any other projects.
425+
Unused packages, artifacts, repos, and scratch spaces are immediately deleted.
428426
429-
To disable automatic garbage collection, you can set the environment variable
430-
`JULIA_PKG_GC_AUTO` to `"false"` before starting Julia or call `API.auto_gc(false)`.
427+
Garbage collection is only applied to the "user depot", e.g. the first entry in the
428+
depot path. If you want to run `gc` on all depots set `force=true` (this might require
429+
admin privileges depending on the setup).
430+
431+
Use verbose mode (`verbose=true`) for detailed output.
432+
433+
The `collect_unused_for` parameter can be set to a `Period` (e.g., `Day(30)`, `Week(2)`) to treat
434+
manifests that have not been used for longer than the specified time as obsolete.
431435
"""
432436
const gc = API.gc
433437

test/pkg.jl

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,91 @@ end
694694
end
695695
end
696696

697+
@testset "Pkg.gc with collect_unused_for" begin
698+
temp_pkg_dir() do project_path
699+
# Create a test environment with a package
700+
mktempdir() do env_dir
701+
cd(env_dir) do
702+
# Create a simple Project.toml
703+
write(
704+
"Project.toml", """
705+
name = "TestEnv"
706+
707+
[deps]
708+
Example = "7876af07-990d-54b4-ab0e-23690620f79a"
709+
"""
710+
)
711+
712+
# Create a Manifest.toml
713+
manifest_path = joinpath(env_dir, "Manifest.toml")
714+
write(
715+
manifest_path, """
716+
# This file is machine-generated - editing it directly is not advised
717+
718+
julia_version = "$(VERSION.major).$(VERSION.minor).$(VERSION.patch)"
719+
manifest_format = "2.0"
720+
721+
[[deps.Example]]
722+
git-tree-sha1 = "46e44e869b4d90b96bd8ed1fdcf32244fddfb6cc"
723+
uuid = "7876af07-990d-54b4-ab0e-23690620f79a"
724+
version = "0.5.3"
725+
"""
726+
)
727+
728+
# Manually create a manifest usage entry with an old timestamp
729+
usage_file = joinpath(Pkg.logdir(), "manifest_usage.toml")
730+
mkpath(dirname(usage_file))
731+
732+
# Create usage data with a timestamp from 60 days ago
733+
old_time = Dates.now() - Dates.Day(60)
734+
usage_dict = Dict(
735+
manifest_path => [Dict("time" => old_time)]
736+
)
737+
open(usage_file, "w") do io
738+
TOML.print(io, usage_dict)
739+
end
740+
741+
# Run gc with collect_unused_for=Day(30) - should filter this manifest
742+
# We capture IO to check for filtering message
743+
io_buf = IOBuffer()
744+
Pkg.gc(verbose = true, collect_unused_for = Dates.Day(30), io = io_buf)
745+
output = String(take!(io_buf))
746+
747+
# The manifest should not be in the active set when collect_unused_for is used
748+
@test !occursin(manifest_path, output) || occursin("Filtered", output)
749+
750+
# Now update the timestamp to be recent
751+
recent_time = Dates.now()
752+
usage_dict = Dict(
753+
manifest_path => [Dict("time" => recent_time)]
754+
)
755+
open(usage_file, "w") do io
756+
TOML.print(io, usage_dict)
757+
end
758+
759+
# Run gc with collect_unused_for=Day(30) - should NOT filter this manifest
760+
io_buf = IOBuffer()
761+
Pkg.gc(verbose = true, collect_unused_for = Dates.Day(30), io = io_buf)
762+
output = String(take!(io_buf))
763+
764+
# The manifest should be in the active set
765+
@test occursin(manifest_path, output) || !occursin("Filtered.*1.*manifest", output)
766+
end
767+
end
768+
end
769+
770+
# Test that gc accepts different Period types
771+
temp_pkg_dir() do project_path
772+
with_temp_env() do
773+
# These should not error
774+
Pkg.gc(collect_unused_for = Dates.Day(7))
775+
Pkg.gc(collect_unused_for = Dates.Week(2))
776+
Pkg.gc(collect_unused_for = Dates.Month(1))
777+
@test true
778+
end
779+
end
780+
end
781+
697782
if isdefined(Base.Filesystem, :delayed_delete_ref)
698783
@testset "Pkg.gc for delayed deletes" begin
699784
mktempdir() do root

0 commit comments

Comments
 (0)