Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add project hash to manifest metadata and warn when manifest and project are out of sync #2815

Merged
17 changes: 13 additions & 4 deletions src/API.jl
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,8 @@ function test(ctx::Context, pkgs::Vector{PackageSpec};
return
end

is_manifest_current(ctx::Context = Context()) = Operations.is_manifest_current(ctx.env)

const UsageDict = Dict{String,DateTime}
const UsageByDepotDict = Dict{String,UsageDict}

Expand Down Expand Up @@ -1031,10 +1033,10 @@ precompile(pkg::String; kwargs...) = precompile(Context(), pkg; kwargs...)
precompile(ctx::Context, pkg::String; kwargs...) = precompile(ctx, [pkg]; kwargs...)
precompile(pkgs::Vector{String}=String[]; kwargs...) = precompile(Context(), pkgs; kwargs...)
function precompile(ctx::Context, pkgs::Vector{String}=String[]; internal_call::Bool=false,
strict::Bool=false, warn_loaded = true, kwargs...)
strict::Bool=false, warn_loaded = true, already_instantiated = false, kwargs...)
Context!(ctx; kwargs...)
internal_call || resolve(ctx, silent_no_change = true)
instantiate(ctx; allow_autoprecomp=false, kwargs...)
already_instantiated || instantiate(ctx; allow_autoprecomp=false, kwargs...)
time_start = time_ns()

# Windows sometimes hits a ReadOnlyMemoryError, so we halve the default number of tasks. Issue #2323
Expand Down Expand Up @@ -1493,6 +1495,13 @@ function instantiate(ctx::Context; manifest::Union{Bool, Nothing}=nothing,
pkgerror("expected manifest file at `$(ctx.env.manifest_file)` but it does not exist")
end
Types.check_warn_manifest_julia_version_compat(ctx.env.manifest, ctx.env.manifest_file)

if Operations.is_manifest_current(ctx.env) === false
@warn """The project and manifest may be out of sync as either project dependencies have been \
added/removed or compat entries have changed since the manifest was last resolved.
Try `Pkg.resolve()` or consider `Pkg.update()` if necessary."""
end

Operations.prune_manifest(ctx.env)
for (name, uuid) in ctx.env.project.deps
get(ctx.env.manifest, uuid, nothing) === nothing || continue
Expand All @@ -1503,7 +1512,7 @@ function instantiate(ctx::Context; manifest::Union{Bool, Nothing}=nothing,
end
# check if all source code and artifacts are downloaded to exit early
if Operations.is_instantiated(ctx.env)
allow_autoprecomp && Pkg._auto_precompile(ctx)
allow_autoprecomp && Pkg._auto_precompile(ctx, already_instantiated = true)
return
end

Expand Down Expand Up @@ -1560,7 +1569,7 @@ function instantiate(ctx::Context; manifest::Union{Bool, Nothing}=nothing,
# Run build scripts
allow_build && Operations.build_versions(ctx, union(new_apply, new_git); verbose=verbose)

allow_autoprecomp && Pkg._auto_precompile(ctx)
allow_autoprecomp && Pkg._auto_precompile(ctx, already_instantiated = true)
end


Expand Down
22 changes: 22 additions & 0 deletions src/Operations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ function update_manifest!(env::EnvCache, pkgs::Vector{PackageSpec}, deps_map, ju
env.manifest[pkg.uuid] = entry
end
prune_manifest(env)
record_project_hash(env)
end


Expand Down Expand Up @@ -801,6 +802,9 @@ function prune_manifest(manifest::Manifest, keep::Vector{UUID})
return manifest
end

function record_project_hash(env::EnvCache)
env.manifest.other["project_hash"] = Types.project_resolve_hash(env.project)
end

#########
# Build #
Expand Down Expand Up @@ -1058,6 +1062,7 @@ function rm(ctx::Context, pkgs::Vector{PackageSpec}; mode::PackageMode)
end
# only keep reachable manifest entires
prune_manifest(ctx.env)
record_project_hash(ctx.env)
# update project & manifest
write_env(ctx.env)
show_update(ctx.env, ctx.registries; io=ctx.io)
Expand Down Expand Up @@ -1432,6 +1437,7 @@ function sandbox_preserve(env::EnvCache, target::PackageSpec, test_project::Stri
# preserve important nodes
keep = [target.uuid]
append!(keep, collect(values(read_project(test_project).deps)))
record_project_hash(env)
# prune and return
return prune_manifest(env.manifest, keep)
end
Expand Down Expand Up @@ -1993,6 +1999,22 @@ function status(env::EnvCache, registries::Vector{Registry.RegistryInstance}, pk
if mode == PKGMODE_MANIFEST || mode == PKGMODE_COMBINED
print_status(env, old_env, registries, header, filter_uuids, filter_names; diff, ignore_indent, io, outdated, silent_no_change)
end
if is_manifest_current(env) === false
printpkgstyle(io, :Warning, """The project and manifest may be out of sync as either project dependencies have been \
added/removed or compat entries have changed since the manifest was last resolved. \
Try `Pkg.resolve()` or consider `Pkg.update()` if necessary.""", ignore_indent; color=Base.warn_color())
end
end

function is_manifest_current(env::EnvCache)
if haskey(env.manifest.other, "project_hash")
recorded_hash = env.manifest.other["project_hash"]
current_hash = Types.project_resolve_hash(env.project)
return recorded_hash == current_hash
else
# Manifest doesn't have a hash of the source Project recorded
return nothing
end
end

function compat_line(io, pkg, uuid, compat_str, longest_dep_len; indent = " ")
Expand Down
14 changes: 12 additions & 2 deletions src/Pkg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,16 @@ Upgrades the format of the manifest file from v1.0 to v2.0 without re-resolving.
"""
const upgrade_manifest = API.upgrade_manifest

"""
is_manifest_current(ctx::Context = Context())

Returns whether the active manifest was resolved from the active project state.
For instance, if the project had compat entries changed, but the manifest wasn't re-resolved, this would return false.

If the manifest doesn't have the project hash recorded, `nothing` is returned.
"""
const is_manifest_current = API.is_manifest_current
KristofferC marked this conversation as resolved.
Show resolved Hide resolved

function __init__()
if isdefined(Base, :active_repl)
REPLMode.repl_init(Base.active_repl)
Expand Down Expand Up @@ -692,9 +702,9 @@ end
# Precompilation #
##################

function _auto_precompile(ctx::Types.Context; warn_loaded = true)
function _auto_precompile(ctx::Types.Context; warn_loaded = true, already_instantiated = false)
if Base.JLOptions().use_compiled_modules == 1 && tryparse(Int, get(ENV, "JULIA_PKG_PRECOMPILE_AUTO", "1")) == 1
Pkg.precompile(ctx; internal_call=true, warn_loaded = warn_loaded)
Pkg.precompile(ctx; internal_call=true, warn_loaded = warn_loaded, already_instantiated = already_instantiated)
end
end

Expand Down
7 changes: 7 additions & 0 deletions src/Types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ end
Base.:(==)(t1::Project, t2::Project) = all(x -> (getfield(t1, x) == getfield(t2, x))::Bool, fieldnames(Project))
Base.hash(t::Project, h::UInt) = foldr(hash, [getfield(t, x) for x in fieldnames(Project)], init=h)

# only hash the deps and compat fields as they are the only fields that affect a resolve
function project_resolve_hash(t::Project)
iob = IOBuffer()
foreach(((name, uuid),) -> println(iob, name, "=", uuid), sort!(collect(t.deps); by=first))
foreach(((name, compat),) -> println(iob, name, "=", compat.val), sort!(collect(t.compat); by=first))
return bytes2hex(sha1(seekstart(iob)))
end

Base.@kwdef mutable struct PackageEntry
name::Union{String,Nothing} = nothing
Expand Down
30 changes: 30 additions & 0 deletions test/manifests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,36 @@ end
end
end
end
@testset "project_hash for identifying out of sync manifest" begin
isolate(loaded_depot=true) do
iob = IOBuffer()

Pkg.activate(; temp=true)
Pkg.add("Example")
@test Pkg.is_manifest_current() === true

Pkg.compat("Example", "0.4")
@test Pkg.is_manifest_current() === false
Pkg.status(io = iob)
@test occursin("The project and manifest may be out of sync", String(take!(iob)))
@test_logs (:warn, r"The project and manifest may be out of sync") Pkg.instantiate()

Pkg.update()
@test Pkg.is_manifest_current() === true
Pkg.status(io = iob)
@test !occursin("The project and manifest may be out of sync", String(take!(iob)))

Pkg.compat("Example", "0.5")
Pkg.status(io = iob)
@test occursin("The project and manifest may be out of sync", String(take!(iob)))
@test_logs (:warn, r"The project and manifest may be out of sync") Pkg.instantiate()

Pkg.rm("Example")
@test Pkg.is_manifest_current() === true
Pkg.status(io = iob)
@test !occursin("The project and manifest may be out of sync", String(take!(iob)))
end
end
end
end

Expand Down