# This file is a part of Julia. License is MIT: https://julialang.org/license

using Test, Distributed, Random, Logging, Libdl
using REPL # testing the doc lookup function should be outside of the scope of this file, but is currently tested here

include("precompile_utils.jl")
include("tempdepot.jl")

Foo_module = :Foo4b3a94a1a081a8cb
foo_incl_dep = :foo4b3a94a1a081a8cb
bar_incl_dep = :bar4b3a94a1a081a8cb
Foo2_module = :F2oo4b3a94a1a081a8cb
FooBase_module = :FooBase4b3a94a1a081a8cb
@eval module ConflictingBindings
    export $Foo_module, $FooBase_module
    $Foo_module = 232
    $FooBase_module = 9134
end
using .ConflictingBindings

@testset "object_build_id" begin
    @test Base.object_build_id([1]) === nothing
    @test Base.object_build_id(Base) == Base.module_build_id(Base)
end

# method root provenance

rootid(m::Module) = Base.module_build_id(Base.parentmodule(m)) % UInt64
rootid(m::Method) = rootid(m.module)

function root_provenance(m::Method, i::Int)
    mid = rootid(m)
    isdefined(m, :root_blocks) || return mid
    idxs = view(m.root_blocks, 2:2:length(m.root_blocks))
    j = searchsortedfirst(idxs, i) - 1   # RLE roots are 0-indexed
    j == 0 && return mid
    return m.root_blocks[2*j-1]
end

struct RLEIterator{T}   # for method roots, T = UInt64 (even on 32-bit)
    items::Vector{Any}
    blocks::Vector{T}
    defaultid::T
end
function RLEIterator(roots, blocks, defaultid)
    T = promote_type(eltype(blocks), typeof(defaultid))
    return RLEIterator{T}(convert(Vector{Any}, roots), blocks, defaultid)
end
RLEIterator(m::Method) = RLEIterator(m.roots, m.root_blocks, rootid(m))
Base.iterate(iter::RLEIterator) = iterate(iter, (0, 0, iter.defaultid))
function Base.iterate(iter::RLEIterator, (i, j, cid))
    i += 1
    i > length(iter.items) && return nothing
    r = iter.items[i]
    while (j + 1 < length(iter.blocks) && i > iter.blocks[j+2])
        cid = iter.blocks[j+1]
        j += 2
    end
    return cid => r, (i, j, cid)
end

function group_roots(m::Method)
    mid = rootid(m)
    isdefined(m, :root_blocks) || return Dict(mid => m.roots)
    group_roots(RLEIterator(m.roots, m.root_blocks, mid))
end
function group_roots(iter::RLEIterator)
    rootsby = Dict{typeof(iter.defaultid),Vector{Any}}()
    for (id, r) in iter
        list = get!(valtype(rootsby), rootsby, id)
        push!(list, r)
    end
    return rootsby
end

precompile_test_harness("basic precompile functionality") do dir2
precompile_test_harness(false) do dir
    Foo_file = joinpath(dir, "$Foo_module.jl")
    Foo2_file = joinpath(dir, "$Foo2_module.jl")
    FooBase_file = joinpath(dir, "$FooBase_module.jl")
    foo_file = joinpath(dir, "$foo_incl_dep.jl")
    bar_file = joinpath(dir, "$bar_incl_dep.jl")

    write(FooBase_file,
          """
          false && __precompile__(false)
          module $FooBase_module
              import Base: hash, >
              struct fmpz end
              struct typeA end
              >(x::fmpz, y::Int) = Base.cmp(x, y) > 0
              function hash(a::typeA, h::UInt)
                  d = den(a)
                  return h
              end
              abstract type AbstractAlgebraMap{A} end
              struct GAPGroupHomomorphism{A, B} <: AbstractAlgebraMap{GAPGroupHomomorphism{B, A}} end
              # issue #62047: supertype whose parameters reach back through the subtype
              struct Wrap62047{T} end
              struct Recur62047{T} <: AbstractAlgebraMap{Wrap62047{Recur62047{T}}} end
              # two types sharing the identical supertype object
              struct ShareA62047{T} <: AbstractAlgebraMap{T} end
              struct ShareB62047{T} <: AbstractAlgebraMap{T} end

              global process_state_calls::Int = 0
              const process_state = Base.OncePerProcess{typeof(getpid())}() do
                  @assert (global process_state_calls += 1) == 1
                  return getpid()
              end
              const mypid = process_state()
              @assert process_state_calls === 1
              process_state_calls = 0
              @assert process_state() === process_state()
              @assert process_state_calls === 0

              const empty_state = Base.OncePerProcess{Nothing}() do
                  return nothing
              end
              @assert empty_state() === nothing
          end
          """)
    write(Foo2_file,
          """
          module $Foo2_module
              export override, overridenc
              override(x::Integer) = 2
              override(x::AbstractFloat) = Float64(override(1))
              overridenc(x::Integer) = rand()+1
              overridenc(x::AbstractFloat) = Float64(overridenc(1))
          end
          """)
    write(Foo_file,
          """
          module $Foo_module
              import $FooBase_module, $FooBase_module.typeA, $FooBase_module.GAPGroupHomomorphism
              import $FooBase_module: Wrap62047, Recur62047, ShareA62047, ShareB62047
              import $Foo2_module: $Foo2_module, override, overridenc
              import $FooBase_module.hash
              import Test
              public foo, Bar
              module Inner
                  import $FooBase_module.hash
                  using ..$Foo_module
                  import ..$Foo2_module
              end

              struct typeB
                  y::typeA
              end
              hash(x::typeB) = hash(x.y)

              # test that docs get reconnected
              @doc "foo function" foo(x) = x + 1
              include_dependency("$foo_incl_dep.jl")
              include_dependency("$foo_incl_dep.jl")
              module Bar
                  public bar
                  include_dependency("$bar_incl_dep.jl")
              end
              @doc "Bar module" Bar # this needs to define the META dictionary via eval
              @eval Bar @doc "bar function" bar(x) = x + 2

              # test for creation of some reasonably complicated type
              struct MyType{T} end
              const t17809s = Any[
                    Tuple{
                        Type{Ptr{MyType{i}}},
                        Ptr{Type{MyType{i}}},
                        Array{Ptr{MyType{MyType{:sym}()}}(0), 0},
                        Val{Complex{Int}(1, 2)},
                        Val{3},
                        Val{nothing}}
                    for i = 0:25]

              # test that types and methods get reconnected correctly
              # issue 16529 (adding a method to a type with no instances)
              (::Task)(::UInt8, ::UInt16, ::UInt32) = 2

              # issue 16471
              Base.sin(::UInt8, ::UInt16, ::UInt32; x = 52) = x
              const sinkw = Core.kwcall

              # issue 16908 (some complicated types and external method definitions)
              abstract type CategoricalPool{T, R <: Integer, V} end
              abstract type CategoricalValue{T, R <: Integer} end
              struct NominalPool{T, R <: Integer, V} <: CategoricalPool{T, R, V}
                  index::Vector{T}
                  invindex::Dict{T, R}
                  order::Vector{R}
                  ordered::Vector{T}
                  valindex::Vector{V}
              end
              struct NominalValue{T, R <: Integer} <: CategoricalValue{T, R}
                  level::R
                  pool::NominalPool{T, R, NominalValue{T, R}}
              end
              struct OrdinalValue{T, R <: Integer} <: CategoricalValue{T, R}
                  level::R
                  pool::NominalPool{T, R, NominalValue{T, R}}
              end
              (::Union{Type{NominalValue}, Type{OrdinalValue}})() = 1
              (::Union{Type{NominalValue{T}}, Type{OrdinalValue{T}}})() where {T} = 2
              (::Type{Vector{NominalValue{T, R}}})() where {T, R} = 3
              (::Type{Vector{NominalValue{T, T}}})() where {T} = 4
              (::Type{Vector{NominalValue{Int, Int}}})() = 5

              # more tests for method signature involving a complicated type
              # issue 18343
              struct Pool18343{R, V}
                  valindex::Vector{V}
              end
              struct Value18343{T, R}
                  pool::Pool18343{R, Value18343{T, R}}
              end
              Base.convert(::Type{Some{S}}, ::Value18343{Some}) where {S} = 2
              Base.convert(::Type{Some{Value18343}}, ::Value18343{Some}) = 2
              Base.convert(::Type{Ref}, ::Value18343{T}) where {T} = 3

              const GAPType1 = GAPGroupHomomorphism{Nothing, Nothing}
              const GAPType2 = GAPGroupHomomorphism{1, 2}

              # issue #62047
              const Type62047 = Wrap62047{Recur62047{Int}}
              const Shared62047 = (ShareA62047{Int}, ShareB62047{Int})

              # issue #28297
              mutable struct Result
                  result::Union{Int,Missing}
              end

              const x28297 = Result(missing)

              const d29936a = UnionAll(Dict.var, UnionAll(Dict.body.var, Dict.body.body))
              const d29936b = UnionAll(Dict.body.var, UnionAll(Dict.var, Dict.body.body))

              # issue #28998
              const x28998 = [missing, 2, missing, 6, missing,
                              missing, missing, missing,
                              missing, missing, missing,
                              missing, missing, 6]

              let some_method = which(Base.include, (Module, String,))
                    # global const some_method // FIXME: support for serializing a direct reference to an external Method not implemented
                  global const some_linfo = Core.Compiler.specialize_method(some_method,
                      Tuple{typeof(Base.include), Module, String}, Core.svec())
              end

              g() = override(1.0)
              Test.@test g() === 2.0 # compile this
              gnc() = overridenc(1.0)
              Test.@test 1 < gnc() < 5 # compile this

              abigfloat_f() = big"12.34"
              const abigfloat_x = big"43.21"
              abigint_f() = big"123"
              const abigint_x = big"124"

              # issue #51111
              abigfloat_to_f32() = Float32(big"1.5")

              # issue #31488
              _v31488 = Base.StringVector(2)
              resize!(_v31488, 0)
              const a31488 = fill(String(_v31488), 100)

              const ptr1 = Ptr{UInt8}(1)
              ptr2 = Ptr{UInt8}(1)
              const ptr3 = Ptr{UInt8}(-1)
              const layout1 = Ptr{Int8}[Ptr{Int8}(0), Ptr{Int8}(1), Ptr{Int8}(-1)]
              const layout2 = Any[Ptr{Int8}(0), Ptr{Int16}(1), Ptr{Int32}(-1)]
              const layout3 = collect(x.match for x in eachmatch(r"..", "abcdefghijk"))::Vector{SubString{String}}

              # create a backedge that includes Type{Union{}}, to ensure lookup can handle that
              call_bottom() = show(stdout, Union{})
              Core.Compiler.return_type(call_bottom, Tuple{})

              # check that @ccallable works from precompiled modules
              Base.@ccallable Cint f35014(x::Cint) = x+Cint(1)
              Base.@ccallable "f35014_other" f35014_2(x::Cint)::Cint = x+Cint(1)

              # check that Tasks work from serialized state
              ch1 = Channel(x -> nothing)
              ch2 = Channel(x -> (push!(x, 2); nothing), Inf)

              # check that Memory aliasing is respected
              a_vec_int = Int[]
              push!(a_vec_int, 1, 2)
              a_mat_int = reshape(a_vec_int, (1, 2))

              a_vec_any = Any[]
              push!(a_vec_any, 1, 2)
              a_mat_any = reshape(a_vec_any, (1, 2))

              a_vec_union = Union{Int,Nothing}[]
              push!(a_vec_union, 1, 2)
              a_mat_union = reshape(a_vec_union, (1, 2))

              a_vec_inline = Pair{Int,Any}[]
              push!(a_vec_inline, 1=>2, 3=>4)
              a_mat_inline = reshape(a_vec_inline, (1, 2))

              oid_vec_int = objectid(a_vec_int)
              oid_mat_int = objectid(a_mat_int)

              using $FooBase_module: process_state, mypid as FooBase_pid, process_state_calls
              const mypid = process_state()
          end
          """)
    # Issue #52063
    touch(foo_file); touch(bar_file)
    # Issue #12623
    @test __precompile__(false) === nothing

    # Issue #21307
    Foo2 = Base.require(Main, Foo2_module)
    @eval $Foo2.override(::Int) = 'a'
    @eval $Foo2.override(::Float32) = 'b'
    @eval $Foo2.overridenc(::Int) = rand() + 97.0
    @eval $Foo2.overridenc(::Float32) = rand() + 100.0

    Foo = Base.require(Main, Foo_module)
    Base.invokelatest() do # use invokelatest to see the results of loading the compile
        @test Foo.foo(17) == 18
        @test Foo.Bar.bar(17) == 19

        # Issue #21307
        @test Foo.g() === 97.0
        @test 96 < Foo.gnc() < 99
        @test Foo.override(1.0e0) == Float64('a')
        @test Foo.override(1.0f0) == 'b'
        @test Foo.override(UInt(1)) == 2
        @test 96 < Foo.overridenc(1.0e0) < 99
        @test 99 < Foo.overridenc(1.0f0) < 102
        @test 0 < Foo.overridenc(UInt(1)) < 3

        # Issue #15722
        @test Foo.abigfloat_f()::BigFloat == big"12.34"
        @test (Foo.abigfloat_x::BigFloat + 21) == big"64.21"
        @test Foo.abigint_f()::BigInt == big"123"
        @test Foo.abigint_x::BigInt + 1 == big"125"

        # Issue #51111
        @test Foo.abigfloat_to_f32() == 1.5f0

        @test Foo.x28297.result === missing

        @test Foo.d29936a === Dict
        @test Foo.d29936b === Dict{K,V} where {V,K}

        @test Foo.x28998[end] == 6

        @test Foo.a31488 == fill("", 100)

        @test Foo.ptr1 === Ptr{UInt8}(1)
        @test Foo.ptr2 === Ptr{UInt8}(0)
        @test Foo.ptr3 === Ptr{UInt8}(-1)
        @test Foo.layout1::Vector{Ptr{Int8}} == Ptr{Int8}[Ptr{Int8}(0), Ptr{Int8}(0), Ptr{Int8}(-1)]
        @test Foo.layout2 == Any[Ptr{Int8}(0), Ptr{Int16}(0), Ptr{Int32}(-1)]
        @test typeof.(Foo.layout2) == [Ptr{Int8}, Ptr{Int16}, Ptr{Int32}]
        @test Foo.layout3 == ["ab", "cd", "ef", "gh", "ij"]

        @test !isopen(Foo.ch1)
        @test !isopen(Foo.ch2)
        @test !isready(Foo.ch1)
        @test isready(Foo.ch2)
        @test take!(Foo.ch2) === 2
        @test !isready(Foo.ch2)

        @test Foo.process_state_calls === 0
        @test Foo.process_state() === getpid()
        @test Foo.mypid !== getpid()
        @test Foo.FooBase_pid !== getpid()
        @test Foo.mypid !== Foo.FooBase_pid
        @test Foo.process_state_calls === 1
    end

    let
        @test Foo.a_vec_int == Int[1, 2]
        @test Foo.a_mat_int == Int[1 2]
        Foo.a_mat_int[1, 2] = 3
        @test Foo.a_vec_int[2] === 3

        @test Foo.a_vec_any == Int[1, 2]
        @test Foo.a_mat_any == Int[1 2]
        Foo.a_mat_any[1, 2] = 3
        @test Foo.a_vec_any[2] === 3

        @test Foo.a_vec_union == Union{Int,Nothing}[1, 2]
        @test Foo.a_mat_union == Union{Int,Nothing}[1 2]
        Foo.a_mat_union[1, 2] = 3
        @test Foo.a_vec_union[2] === 3
        Foo.a_mat_union[1, 2] = nothing
        @test Foo.a_vec_union[2] === nothing

        @test Foo.a_vec_inline == Pair{Int,Any}[1=>2, 3=>4]
        @test Foo.a_mat_inline == Pair{Int,Any}[1=>2 3=>4]
        Foo.a_mat_inline[1, 2] = 5=>6
        @test Foo.a_vec_inline[2] === Pair{Int,Any}(5, 6)

        @test objectid(Foo.a_vec_int) === Foo.oid_vec_int
        @test objectid(Foo.a_mat_int) === Foo.oid_mat_int
        @test Foo.oid_vec_int !== Foo.oid_mat_int
        @test Base.object_build_id(Foo.a_vec_int) == Base.object_build_id(Foo.a_mat_int)
        @test Base.object_build_id(Foo) == Base.module_build_id(Foo)
        @test Base.object_build_id(Foo.a_vec_int) == Base.module_build_id(Foo)
    end

    cachedir = joinpath(dir, "compiled", "v$(VERSION.major).$(VERSION.minor)")
    cachedir2 = joinpath(dir2, "compiled", "v$(VERSION.major).$(VERSION.minor)")
    cachefile = joinpath(cachedir, "$Foo_module.ji")
    @test isfile(cachefile)
    do_pkgimg = Base.JLOptions().use_pkgimages == 1 && Base.JLOptions().permalloc_pkgimg == 1
    if do_pkgimg ||  Base.JLOptions().use_pkgimages == 0
        if do_pkgimg
            ocachefile = Base.ocachefile_from_cachefile(cachefile)::String
            @test isfile(ocachefile)
            let foo_ptr = Libdl.dlopen(ocachefile::String, RTLD_NOLOAD)
                f35014_ptr = Libdl.dlsym(foo_ptr, :f35014)
                @test ccall(f35014_ptr, Int32, (Int32,), 3) == 4
                f35014_other_ptr = Libdl.dlsym(foo_ptr, :f35014_other)
                @test ccall(f35014_other_ptr, Int32, (Int32,), 3) == 4
            end
        else
            ocachefile = nothing
        end
        # use _require_from_serialized to ensure that the test fails if
        # the module doesn't reload from the image:
        @test_logs (:warn, "Replacing module `$Foo_module`") begin
            m = Base._require_from_serialized(Base.PkgId(Foo), cachefile, ocachefile, Foo_file)
            @test isa(m, Module)
        end
    end

    @test_throws MethodError Foo.foo(17) # world shouldn't be visible yet
    Base.invokelatest() do # use invokelatest to see the results of loading the compile
        @test Foo.foo(17) == 18
        @test Foo.Bar.bar(17) == 19

        # Issue #21307
        @test Foo.g() === 97.0
        @test Foo.override(1.0e0) == Float64('a')
        @test Foo.override(1.0f0) == 'b'
        @test Foo.override(UInt(1)) == 2

        # issue #12284:
        @test string(Base.Docs.doc(Foo.foo)) == "foo function\n"
        @test string(Base.Docs.doc(Foo.Bar.bar)) == "bar function\n"
        @test string(Base.Docs.doc(Foo.Bar)) == "Bar module\n"

        modules, (deps, _, requires), required_modules, _... = Base.parse_cache_header(cachefile)
        discard_module = mod_fl_mt -> mod_fl_mt.filename
        @test modules == [ Base.PkgId(Foo) => Base.module_build_id(Foo) % UInt64 ]
        @test map(x -> x.filename, deps) == [ Foo_file, joinpath("@depot", foo_file), joinpath("@depot", bar_file) ]
        @test requires == [ Base.PkgId(Foo) => Base.PkgId(string(FooBase_module)),
                            Base.PkgId(Foo) => Base.PkgId(Foo2),
                            Base.PkgId(Foo) => Base.PkgId(Test),
                            Base.PkgId(Foo) => Base.PkgId(string(FooBase_module)) ]
        srctxt = Base.read_dependency_src(cachefile, Foo_file)
        @test !isempty(srctxt) && srctxt == read(Foo_file, String)
        @test_throws ErrorException Base.read_dependency_src(cachefile, "/tmp/nonexistent.txt")
        # dependencies declared with `include_dependency` should not be stored
        @test_throws ErrorException Base.read_dependency_src(cachefile, joinpath(dir, foo_file))

        modules, deps1 = Base.cache_dependencies(cachefile)
        modules_ok = merge(
            Dict(let m = Base.PkgId(s)
                    m => Base.module_build_id(Base.root_module(m))
                 end for s in
                 [ "Base", "Core", "Main",
                   string(Foo2_module), string(FooBase_module),]),
            # plus modules included in the system image
            Dict(let m = Base.root_module(Base, s)
                     Base.PkgId(m) => Base.module_build_id(m)
                 end for s in [Symbol(x.name) for x in Base._sysimage_modules if !(x.name in ["Base", "Core", "Main"])]),
            # plus test module,
            Dict(Base.PkgId(Base.root_module(Base, :Test)) => Base.module_build_id(Base.root_module(Base, :Test))),
            # plus dependencies of test module
            Dict(Base.PkgId(Base.root_module(Base, :InteractiveUtils)) => Base.module_build_id(Base.root_module(Base, :InteractiveUtils))),
            Dict(Base.PkgId(Base.root_module(Base, :Logging)) => Base.module_build_id(Base.root_module(Base, :Logging))),
            Dict(Base.PkgId(Base.root_module(Base, :Random)) => Base.module_build_id(Base.root_module(Base, :Random))),
            Dict(Base.PkgId(Base.root_module(Base, :Serialization)) => Base.module_build_id(Base.root_module(Base, :Serialization))),
            # and their dependencies
            Dict(Base.PkgId(Base.root_module(Base, :SHA)) => Base.module_build_id(Base.root_module(Base, :SHA))),
            Dict(Base.PkgId(Base.root_module(Base, :Markdown)) => Base.module_build_id(Base.root_module(Base, :Markdown))),
            Dict(Base.PkgId(Base.root_module(Base, :JuliaSyntaxHighlighting)) => Base.module_build_id(Base.root_module(Base, :JuliaSyntaxHighlighting))),
            Dict(Base.PkgId(Base.root_module(Base, :StyledStrings)) => Base.module_build_id(Base.root_module(Base, :StyledStrings))),

            # and their dependencies
            Dict(Base.PkgId(Base.root_module(Base, :Base64)) => Base.module_build_id(Base.root_module(Base, :Base64))),
        )
        @test Dict(modules) == modules_ok

        @test discard_module.(deps) == deps1
        modules, (_, deps, requires), required_modules, _... = Base.parse_cache_header(cachefile)
        @test map(x -> x.filename, deps) == [Foo_file]

        @test current_task()(0x01, 0x4000, 0x30031234) == 2
        @test sin(0x01, 0x4000, 0x30031234) == 52
        @test sin(0x01, 0x4000, 0x30031234; x = 9142) == 9142
        @test Foo.sinkw === Core.kwcall

        @test Foo.NominalValue() == 1
        @test Foo.OrdinalValue() == 1
        @test Foo.NominalValue{Int}() == 2
        @test Foo.OrdinalValue{Int}() == 2
        let T = Vector{Foo.NominalValue{Int}}
            @test isa(T(), T)
        end
        @test Vector{Foo.NominalValue{Int32, Int64}}() == 3
        @test Vector{Foo.NominalValue{UInt, UInt}}() == 4
        @test Vector{Foo.NominalValue{Int, Int}}() == 5
        @test all(i -> Foo.t17809s[i + 1] ===
            Tuple{
                Type{Ptr{Foo.MyType{i}}},
                Ptr{Type{Foo.MyType{i}}},
                Array{Ptr{Foo.MyType{Foo.MyType{:sym}()}}(0), 0},
                Val{Complex{Int}(1, 2)},
                Val{3},
                Val{nothing}},
            0:25)
        some_method = which(Base.include, (Module, String,))
        some_linfo = Core.Compiler.specialize_method(some_method, Tuple{typeof(Base.include), Module, String}, Core.svec())
        @test Foo.some_linfo::Core.MethodInstance === some_linfo

        ft = Base.datatype_fieldtypes
        PV = ft(Foo.Value18343{Some}.body)[1]
        VR = ft(PV)[1].parameters[1]
        @test ft(PV)[1] === Array{VR,1}
        @test pointer_from_objref(ft(PV)[1]) ===
              pointer_from_objref(ft(ft(ft(PV)[1].parameters[1])[1])[1])
        @test PV === ft(ft(PV)[1].parameters[1])[1]
        @test pointer_from_objref(PV) === pointer_from_objref(ft(ft(PV)[1].parameters[1])[1])
    end

    Nest_module = :Nest4b3a94a1a081a8cb
    Nest_file = joinpath(dir, "$Nest_module.jl")
    NestInner_file = joinpath(dir, "$(Nest_module)Inner.jl")
    NestInner2_file = joinpath(dir, "$(Nest_module)Inner2.jl")
    write(Nest_file,
        """
        module $Nest_module
        include("$(escape_string(NestInner_file))")
        end
        """)
    write(NestInner_file,
        """
        module NestInner
        include("$(escape_string(NestInner2_file))")
        end
        """)
    write(NestInner2_file,
        """
        f() = 22
        """)
    Nest = Base.require(Main, Nest_module)
    cachefile = joinpath(cachedir, "$Nest_module.ji")
    modules, (deps, _, requires), required_modules, _... = Base.parse_cache_header(cachefile)
    @test last(deps).modpath == ["NestInner"]

    UsesB_module = :UsesB4b3a94a1a081a8cb
    B_module     = :UsesB4b3a94a1a081a8cb_B
    UsesB_file = joinpath(dir, "$UsesB_module.jl")
    B_file = joinpath(dir, "$(B_module).jl")
    write(UsesB_file,
        """
        module $UsesB_module
        using $B_module
        end
        """)
    write(B_file,
        """
        module $B_module
        export bfunc
        bfunc() = 33
        end
        """)
    UsesB = Base.require(Main, UsesB_module)
    cachefile = joinpath(cachedir, "$UsesB_module.ji")
    modules, (deps, _, requires), required_modules, _... = Base.parse_cache_header(cachefile)
    id1, id2 = only(requires)
    @test Base.pkgorigins[id1].cachepath == cachefile
    @test Base.pkgorigins[id2].cachepath == joinpath(cachedir, "$B_module.ji")

    Baz_file = joinpath(dir, "Baz.jl")
    write(Baz_file,
          """
          haskey(Base.loaded_modules, Base.PkgId("UseBaz")) || __precompile__(false)
          module Baz
          baz() = 1
          end
          """)

    @test Base.compilecache(Base.PkgId("Baz")) == Base.PrecompilableError() # due to __precompile__(false)

    OverwriteMethodError_file = joinpath(dir, "OverwriteMethodError.jl")
    write(OverwriteMethodError_file,
          """
          module OverwriteMethodError
              Base.:(+)(x::Bool, y::Bool) = false
          end
          """)

    @test (@test_warn "overwritten in module OverwriteMethodError" Base.compilecache(Base.PkgId("OverwriteMethodError"))) == Base.PrecompilableError() # due to piracy

    UseBaz_file = joinpath(dir, "UseBaz.jl")
    write(UseBaz_file,
          """
          module UseBaz
          biz() = 1
          @assert haskey(Base.loaded_modules, Base.PkgId("UseBaz"))
          @assert !haskey(Base.loaded_modules, Base.PkgId("Baz"))
          using Baz
          @assert haskey(Base.loaded_modules, Base.PkgId("Baz"))
          buz() = 2
          const generating = ccall(:jl_generating_output, Cint, ())
          const incremental = Base.JLOptions().incremental
          end
          """)

    @test Base.compilecache(Base.PkgId("UseBaz")) == Base.PrecompilableError() # due to __precompile__(false)
    @eval using UseBaz
    @test haskey(Base.loaded_modules, Base.PkgId("UseBaz"))
    @test haskey(Base.loaded_modules, Base.PkgId("Baz"))
    invokelatest() do
        @test UseBaz.biz() === 1
        @test UseBaz.buz() === 2
        @test UseBaz.generating == 0
        @test UseBaz.incremental == 0
    end
    @eval using Baz
    invokelatest() do
        @test Baz.baz() === 1
        @test Baz === UseBaz.Baz
    end

    # should not throw if the cachefile does not exist
    @test !isfile("DoesNotExist.ji")
    @test Base.stale_cachefile("", "DoesNotExist.ji") === true

    # Issue #12720
    FooBar1_file = joinpath(dir, "FooBar1.jl")
    write(FooBar1_file,
          """
          module FooBar1
              using FooBar
          end
          """)
    sleep(2) # give FooBar and FooBar1 different timestamps, in reverse order too
    FooBar_file = joinpath(dir, "FooBar.jl")
    write(FooBar_file,
          """
          module FooBar
          end
          """)

    cachefile, _ = @test_logs (:debug, r"Generating object cache file for FooBar") min_level=Logging.Debug match_mode=:any Base.compilecache(Base.PkgId("FooBar"))
    @test cachefile == Base.compilecache_path(Base.PkgId("FooBar"), "")
    @test isfile(joinpath(cachedir, "FooBar.ji"))
    Tsc = Bool(Base.JLOptions().use_pkgimages) ? Tuple{<:Vector, String, UInt128} : Tuple{<:Vector, Nothing, UInt128}
    @test Base.stale_cachefile(FooBar_file, joinpath(cachedir, "FooBar.ji")) isa Tsc
    @test !isdefined(Main, :FooBar)
    @test !isdefined(Main, :FooBar1)

    relFooBar_file = joinpath(dir, "subfolder", "..", "FooBar.jl")
    @test Base.stale_cachefile(relFooBar_file, joinpath(cachedir, "FooBar.ji")) isa (Sys.iswindows() ? Tuple{<:Vector, String, UInt128} : Bool) # `..` is not a symlink on Windows
    mkdir(joinpath(dir, "subfolder"))
    @test Base.stale_cachefile(relFooBar_file, joinpath(cachedir, "FooBar.ji")) isa Tsc

    @eval using FooBar
    fb_uuid = invokelatest(()->Base.module_build_id(FooBar))
    sleep(2); touch(FooBar_file)
    insert!(DEPOT_PATH, 1, dir2)
    @test Base.stale_cachefile(FooBar_file, joinpath(cachedir, "FooBar.ji")) isa Tsc
    @eval using FooBar1
    @test !isfile(joinpath(cachedir2, "FooBar.ji"))
    @test !isfile(joinpath(cachedir, "FooBar1.ji"))
    @test isfile(joinpath(cachedir2, "FooBar1.ji"))
    @test Base.stale_cachefile(FooBar_file, joinpath(cachedir, "FooBar.ji")) isa Tsc
    @test Base.stale_cachefile(FooBar1_file, joinpath(cachedir2, "FooBar1.ji")) isa Tsc
    invokelatest() do
        @test fb_uuid == Base.module_build_id(FooBar)
        fb_uuid1 = Base.module_build_id(FooBar1)
        @test fb_uuid != fb_uuid1
    end

    # test checksum
    open(joinpath(cachedir2, "FooBar1.ji"), "a") do f
        write(f, 0x076cac96) # append 4 random bytes
    end
    @test Base.stale_cachefile(FooBar1_file, joinpath(cachedir2, "FooBar1.ji")) === true

    # test behavior of precompile modules that throw errors
    FooBar2_file = joinpath(dir, "FooBar2.jl")
    write(FooBar2_file,
          """
          module FooBar2
          error("break me")
          end
          """)
    @test_throws Base.Precompilation.PkgPrecompileError Base.require(Main, :FooBar2)

    # Test that trying to eval into closed modules during precompilation is an error
    FooBar3_file = joinpath(dir, "FooBar3.jl")
    FooBar3_inc = joinpath(dir, "FooBar3_inc.jl")
    write(FooBar3_inc, "x=1\n")
    for code in ["Core.eval(Base, :(x=1))",
                 "Base.include(Base, \"FooBar3_inc.jl\")",
                 "Core.define_method(Base, :FooBar3_closed_module_gf)"]
        write(FooBar3_file, """
        module FooBar3
        $code
        end
        """)
        @test_throws Base.Precompilation.PkgPrecompileError Base.require(Main, :FooBar3)
    end

    # Declaring an already-existing generic function of a closed module is a
    # no-op and must not error during precompilation
    FooBar3b_file = joinpath(dir, "FooBar3b.jl")
    write(FooBar3b_file, """
    module FooBar3b
    Core.eval(Main, Expr(:function, GlobalRef(Base, :length)))
    end
    """)
    @test Base.require(Main, :FooBar3b) isa Module

    # Test transitive dependency for #21266
    FooBarT_file = joinpath(dir, "FooBarT.jl")
    write(FooBarT_file,
          """
          module FooBarT
          end
          """)
    FooBarT1_file = joinpath(dir, "FooBarT1.jl")
    write(FooBarT1_file,
          """
          module FooBarT1
              using FooBarT
          end
          """)
    FooBarT2_file = joinpath(dir, "FooBarT2.jl")
    write(FooBarT2_file,
          """
          module FooBarT2
              using FooBarT1
          end
          """)
    Base.compilecache(Base.PkgId("FooBarT2"))
    write(FooBarT1_file,
          """
          module FooBarT1
          end
          """)
    rm(FooBarT_file)
    @test Base.stale_cachefile(FooBarT2_file, joinpath(cachedir2, "FooBarT2.ji")) === true
    @test Base.require(Main, :FooBarT2) isa Module
end
end

# method root provenance & external code caching
precompile_test_harness("code caching") do dir
    Cache_module = :Cacheb8321416e8a3e2f1
    # Note: calling setindex!(::Dict{K,V}, ::Any, ::K) adds both compression and codegen roots
    write(joinpath(dir, "$Cache_module.jl"),
          """
          module $Cache_module
              struct X end
              struct X2 end
              @noinline function f(d)
                  @noinline d[X()] = nothing
              end
              @noinline fpush(dest) = @noinline push!(dest, X())
              function callboth()
                  f(Dict{X,Any}())
                  fpush(X[])
                  nothing
              end
              function getelsize(list::Vector{T}) where T
                  n = 0
                  for item in list
                      n += sizeof(T)
                  end
                  return n
              end
              precompile(callboth, ())
              precompile(getelsize, (Vector{Int32},))
          end
          """)
    pkgid = Base.PkgId(string(Cache_module))
    @test !Base.isprecompiled(pkgid)
    Base.compilecache(pkgid)
    @test Base.isprecompiled(pkgid)
    @eval using $Cache_module
    M = invokelatest(getglobal, @__MODULE__, Cache_module)
    Mid = rootid(M)
    invokelatest() do
        # Test that this cache file "owns" all the roots
        for name in (:f, :fpush, :callboth)
            func = getglobal(M, name)
            m = only(collect(methods(func)))
            @test all(i -> root_provenance(m, i) == Mid, 1:length(m.roots))
        end
        # Check that we can cache external CodeInstances:
        # length(::Vector) has an inferred specialization for `Vector{X}`
        msize = which(length, (Vector{<:Any},))
        hasspec = false
        for mi in Base.specializations(msize)
            if mi.specTypes == Tuple{typeof(length),Vector{Cacheb8321416e8a3e2f1.X}}
                if (isdefined(mi, :cache) && isa(mi.cache, Core.CodeInstance) &&
                    mi.cache.max_world == typemax(UInt) && mi.cache.inferred !== nothing)
                    hasspec = true
                    break
                end
            end
        end
        @test hasspec

        # Check that internal methods and their roots are accounted appropriately
        minternal = which(M.getelsize, (Vector,))
        mi = minternal.specializations::Core.MethodInstance
        @test mi.specTypes == Tuple{typeof(M.getelsize),Vector{Int32}}
        ci = mi.cache
        @test (codeunits(ci.inferred::String)[end]) === 0x01
        @test ci.inferred !== nothing
        # ...and that we can add "untracked" roots & non-relocatable CodeInstances to them too
        Base.invokelatest() do
            M.getelsize(M.X2[])
        end
        mispecs = minternal.specializations::Core.SimpleVector
        @test mispecs[1] === mi
        mi = mispecs[2]::Core.MethodInstance
        mi.specTypes == Tuple{typeof(M.getelsize),Vector{M.X2}}
        ci = mi.cache
        @test (codeunits(ci.inferred::String)[end]) == 0x00
    end
    # PkgA loads PkgB, and both add roots to the same `push!` method (both before and after loading B)
    Cache_module2 = :Cachea1544c83560f0c99
    write(joinpath(dir, "$Cache_module2.jl"),
          """
          module $Cache_module2
              struct Y end
              @noinline f(dest) = @noinline push!(dest, Y())
              callf() = f(Y[])
              callf()
              using $(Cache_module)
              struct Z end
              @noinline g(dest) = @noinline push!(dest, Z())
              callg() = g(Z[])
              callg()
          end
          """)
    Base.compilecache(Base.PkgId(string(Cache_module2)))
    @eval using $Cache_module2
    invokelatest() do
        M2 = getfield(@__MODULE__, Cache_module2)
        M2id = rootid(M2)
        dest = []
        Base.invokelatest() do  # use invokelatest to see the results of loading the compile
            M2.f(dest)
            M.fpush(dest)
            M2.g(dest)
            @test dest == [M2.Y(), M.X(), M2.Z()]
            @test M2.callf() == [M2.Y()]
            @test M2.callg() == [M2.Z()]
            @test M.fpush(M.X[]) == [M.X()]
        end
        mT = which(push!, (Vector{T} where T, Any))
        groups = group_roots(mT)
        @test Memory{M2.Y} ∈ groups[M2id]
        @test Memory{M2.Z} ∈ groups[M2id]
        @test Memory{M.X} ∈ groups[Mid]
        @test Memory{M.X} ∉ groups[M2id]
    end
    # backedges of external MethodInstances
    # Root gets used by RootA and RootB, and both consumers end up inferring the same MethodInstance from Root
    # Do both callers get listed as backedges?
    RootModule = :Root_0xab07d60518763a7e
    write(joinpath(dir, "$RootModule.jl"),
          """
          module $RootModule
          function f(x)
              while x < 10
                  x += oftype(x, 1)
              end
              return x
          end
          g1() = f(Int16(9))
          g2() = f(Int16(9))
          # all deliberately uncompiled
          end
          """)
    RootA = :RootA_0xab07d60518763a7e
    write(joinpath(dir, "$RootA.jl"),
          """
          module $RootA
          using $RootModule
          fA() = $RootModule.f(Int8(4))
          fA()
          $RootModule.g1()
          end
          """)
    RootB = :RootB_0xab07d60518763a7e
    write(joinpath(dir, "$RootB.jl"),
          """
          module $RootB
          using $RootModule
          fB() = $RootModule.f(Int8(4))
          fB()
          $RootModule.g2()
          end
          """)
    Base.compilecache(Base.PkgId(string(RootA)))
    Base.compilecache(Base.PkgId(string(RootB)))
    @eval using $RootA
    @eval using $RootB
    invokelatest() do
        MA = getfield(@__MODULE__, RootA)
        MB = getfield(@__MODULE__, RootB)
        M = getfield(MA, RootModule)
        function backedge_callers(mi::Core.MethodInstance)
            callers = Any[]
            i = 1
            while i <= length(mi.backedges)
                mi.backedges[i] isa Type && (i += 1)
                caller = mi.backedges[i]
                @assert caller isa Union{Core.MethodInstance,Core.CodeInstance}
                push!(callers, caller)
                i += 1
            end
            return callers
        end
        caller_method(caller::Core.MethodInstance) = caller.def::Method
        caller_method(caller::Core.CodeInstance) = caller_method(caller.def)
        m = which(M.f, (Any,))
        for mi in Base.specializations(m)
            mi === nothing && continue
            mi = mi::Core.MethodInstance
            if mi.specTypes.parameters[2] === Int8
                # external callers
                mods = Set(caller_method(caller).module
                    for caller in backedge_callers(mi))
                @test MA ∈ mods
                @test MB ∈ mods
                @test length(mods) == 2
            elseif mi.specTypes.parameters[2] === Int16
                # internal callers
                meths = Set(caller_method(caller)
                    for caller in backedge_callers(mi))
                @test which(M.g1, ()) ∈ meths
                @test which(M.g2, ()) ∈ meths
                @test length(meths) == 2
            end
        end
    end

    # Invalidations (this test is adapted from SnoopCompile)
    function hasvalid(mi, world)
        isdefined(mi, :cache) || return false
        ci = mi.cache
        while true
            ci.max_world >= world && return true
            isdefined(ci, :next) || return false
            ci = ci.next
        end
    end

    StaleA = :StaleA_0xab07d60518763a7e
    StaleB = :StaleB_0xab07d60518763a7e
    StaleC = :StaleC_0xab07d60518763a7e
    write(joinpath(dir, "$StaleA.jl"),
        """
        module $StaleA

        stale(x) = rand(1:8)
        stale(x::Int) = length(digits(x))

        not_stale(x::String) = first(x)

        use_stale(c) = stale(c[1]) + not_stale("hello")
        build_stale(x) = use_stale(Any[x])

        # bindings
        struct InvalidatedBinding
            x::Int
        end
        struct Wrapper
            ib::InvalidatedBinding
        end
        makewib(x) = Wrapper(InvalidatedBinding(x))
        const gib = makewib(1)
        fib() = gib.ib.x

        struct LogBindingInvalidation
            x::Int
        end
        const glbi = LogBindingInvalidation(1)
        flbi() = @__MODULE__().glbi.x

        # force precompilation
        build_stale(37)
        stale('c')

        ## Reporting tests (unrelated to the above)
        nbits(::Int8) = 8
        nbits(::Int16) = 16

        end
        """
    )
    write(joinpath(dir, "$StaleB.jl"),
        """
        module $StaleB

        # StaleB does not know about StaleC when it is being built.
        # However, if StaleC is loaded first, we get `"jl_insert_method_instance"`
        # invalidations.
        using $StaleA

        # This will be invalidated if StaleC is loaded
        useA() = $StaleA.stale("hello")
        useA2() = useA()

        useflbi() = $StaleA.flbi()

        # force precompilation, force call so that inlining heuristics don't affect the result
        begin
            Base.Experimental.@force_compile
            @noinline useA2()
            @noinline useflbi()
        end
        precompile($StaleA.fib, ())

        ## Reporting tests
        call_nbits(x::Integer) = $StaleA.nbits(x)
        map_nbits() = map(call_nbits, Integer[Int8(1), Int16(1)])
        map_nbits()

        end
        """
    )
    write(joinpath(dir, "$StaleC.jl"),
        """
        module $StaleC

        using $StaleA

        $StaleA.stale(x::String) = length(x)
        call_buildstale(x) = $StaleA.build_stale(x)

        call_buildstale("hey")

        end # module
        """
    )
    for pkg in (StaleA, StaleB, StaleC)
        Base.compilecache(Base.PkgId(string(pkg)))
    end
    @eval using $StaleA
    MA = invokelatest(getglobal, @__MODULE__, StaleA)
    Base.eval(MA, :(nbits(::UInt8) = 8))
    Base.eval(MA, quote
        struct InvalidatedBinding
            x::Float64
        end
        struct Wrapper
            ib::InvalidatedBinding
        end
        const gib = makewib(2.0)
    end)
    # TODO: test a "method_globalref" invalidation also
    Base.eval(MA, quote
        struct LogBindingInvalidation # binding invalidations can't be done during precompilation
            x::Float64
        end
        const glbi = LogBindingInvalidation(2.0)
    end)
    @eval using $StaleC
    invalidations = Base.ReinferUtils.debug_method_invalidation(true)
    @eval using $StaleB
    Base.ReinferUtils.debug_method_invalidation(false)
    invokelatest() do
        MB = getfield(@__MODULE__, StaleB)
        MC = getfield(@__MODULE__, StaleC)
        world = Base.get_world_counter()
        m = only(methods(MA.use_stale))
        mi = m.specializations::Core.MethodInstance
        @test hasvalid(mi, world)   # it was re-inferred by StaleC
        m = only(methods(MA.build_stale))
        mis = filter(!isnothing, collect(m.specializations::Core.SimpleVector))
        @test length(mis) == 2
        for mi in mis
            mi = mi::Core.MethodInstance
            if mi.specTypes.parameters[2] == Int
                @test mi.cache.max_world < world
            else
                # The variant for String got "healed" by recompilation in StaleC
                @test mi.specTypes.parameters[2] == String
                @test mi.cache.max_world == typemax(UInt)
            end
        end
        m = only(methods(MB.useA))
        mi = m.specializations::Core.MethodInstance
        @test !hasvalid(mi, world)      # invalidated by the stale(x::String) method in StaleC
        m = only(methods(MC.call_buildstale))
        mi = m.specializations::Core.MethodInstance
        @test hasvalid(mi, world)       # was compiled with the new method
        m = only(methods(MA.fib))
        for mi in Base.specializations(m)
            @test !hasvalid(mi, world)      # invalidated by redefining `gib` before loading StaleB
        end
        @test MA.fib() === 2.0

        # Reporting test (ensure SnoopCompile works)
        @test all(i -> isassigned(invalidations, i), eachindex(invalidations))
        m = only(methods(MB.call_nbits))
        for mi in Base.specializations(m)
            hv = hasvalid(mi, world)
            @test mi.specTypes.parameters[end] === Integer ? !hv : hv
        end

        idxs = findall(==("verify_methods"), invalidations)
        idxsbits = filter(idxs) do i
            mi = invalidations[i-1]
            mi.def.def === m
        end
        idx = only(idxsbits)
        tagbad = invalidations[idx+1]
        @test isa(tagbad, Core.CodeInstance)
        j = findfirst(==(tagbad), invalidations)
        @test invalidations[j-1] == "insert_backedges_callee"
        @test isa(invalidations[j-2], Type)
        @test isa(invalidations[j+1], Vector{Any}) # [nbits(::UInt8)]
        m = only(methods(MB.useA2))
        mi = only(Base.specializations(m))
        @test !hasvalid(mi, world)
        @test any(x -> x isa Core.CodeInstance && x.def === mi, invalidations)

        idxb = findfirst(x -> x isa Core.Binding, invalidations)
        @test invalidations[idxb+1] == "insert_backedges_callee"
        # Proof flattening may change the path from the binding to `flbi`, but the
        # downstream `useflbi` invalidation must still identify `flbi` as its cause.
        useflbi_method = only(methods(MB.useflbi))
        flbi_method = only(methods(MA.flbi))
        idxv = findfirst(eachindex(invalidations)) do i
            1 < i < length(invalidations) || return false
            invalidations[i] == "verify_methods" || return false
            caller = invalidations[i-1]
            cause = invalidations[i+1]
            return caller isa Core.CodeInstance && cause isa Core.CodeInstance &&
                caller.def.def === useflbi_method && cause.def.def === flbi_method
        end
        @test idxv !== nothing

        m = only(methods(MB.map_nbits))
        @test !hasvalid(m.specializations::Core.MethodInstance, world+1) # insert_backedges invalidations also trigger their backedges
    end
end

precompile_test_harness("precompiletools") do dir
    PrecompileToolsModule = :PCTb8321416e8a3e2f1
    write(joinpath(dir, "$PrecompileToolsModule.jl"),
        """
        module $PrecompileToolsModule
            struct MyType
                x::Int
            end

            function call_findfirst(x, list)
                # call a method defined in Base by runtime dispatch
                return findfirst(==(Base.inferencebarrier(x)), Base.inferencebarrier(list))
            end

            let
                ccall(:jl_tag_newly_inferred_enable, Cvoid, ())
                call_findfirst(MyType(2), [MyType(1), MyType(2), MyType(3)])
                ccall(:jl_tag_newly_inferred_disable, Cvoid, ())
            end
        end
        """
    )
    pkgid = Base.PkgId(string(PrecompileToolsModule))
    @test !Base.isprecompiled(pkgid)
    Base.compilecache(pkgid)
    @test Base.isprecompiled(pkgid)
    @eval using $PrecompileToolsModule
    M = invokelatest(getglobal, @__MODULE__, PrecompileToolsModule)
    invokelatest() do
        m = which(Tuple{typeof(findfirst), Base.Fix2{typeof(==), T}, Vector{T}} where T)
        success = 0
        for mi in Base.specializations(m)
            sig = Base.unwrap_unionall(mi.specTypes)
            success += sig.parameters[3] === Vector{M.MyType}
        end
        @test success == 1
    end
end

precompile_test_harness("invoke") do dir
    InvokeModule = :Invoke0x030e7e97c2365aad
    CallerModule = :Caller0x030e7e97c2365aad
    write(joinpath(dir, "$InvokeModule.jl"),
          """
          module $InvokeModule
              export f, g, h, q, fnc, gnc, hnc, qnc   # nc variants do not infer to a Const
              export f44320, g44320
              export getlast
              # f is for testing invoke that occurs within a dependency
              f(x::Real) = 0
              f(x::Int) = x < 5 ? 1 : invoke(f, Tuple{Real}, x)
              fnc(x::Real) = rand()-1
              fnc(x::Int) = x < 5 ? rand()+1 : invoke(fnc, Tuple{Real}, x)
              # g is for testing invoke that occurs from a dependent
              g(x::Real) = 0
              g(x::Int) = 1
              gnc(x::Real) = rand()-1
              gnc(x::Int) = rand()+1
              # h will be entirely superseded by a new method (full invalidation)
              h(x::Real) = 0
              h(x::Int) = x < 5 ? 1 : invoke(h, Tuple{Integer}, x)
              hnc(x::Real) = rand()-1
              hnc(x::Int) = x < 5 ? rand()+1 : invoke(hnc, Tuple{Integer}, x)
              # q will have some callers invalidated
              q(x::Integer) = 0
              qnc(x::Integer) = rand()-1
              # Issue #44320
              f44320(::Int) = 1
              f44320(::Any) = 2
              g44320() = invoke(f44320, Tuple{Any}, 0)
              g44320()
              # Issue #57115
              f57115(@nospecialize(::Any)) = error("unimplemented")
              function g57115(@nospecialize(x))
                  if @noinline rand(Bool)
                      # Add an 'invoke' edge from 'foo' to 'bar'
                      Core.invoke(f57115, Tuple{Any}, x)
                  else
                      # ... and also an identical 'call' edge
                      @noinline f57115(x)
                  end
              end

              # Adding new specializations should not invalidate `invoke`s
              function getlast(itr)
                  x = nothing
                  for y in itr
                      x = y
                  end
                  return x
              end
              getlast(a::AbstractArray) = invoke(getlast, Tuple{Any}, a)
          end
          """)
          write(joinpath(dir, "$CallerModule.jl"),
          """
          module $CallerModule
              using $InvokeModule
              import $InvokeModule: f57115, g57115

              # involving external modules
              callf(x) = f(x)
              callg(x) = x < 5 ? g(x) : invoke(g, Tuple{Real}, x)
              callh(x) = h(x)
              callq(x) = q(x)
              callqi(x) = invoke(q, Tuple{Integer}, x)
              callfnc(x) = fnc(x)
              callgnc(x) = x < 5 ? gnc(x) : invoke(gnc, Tuple{Real}, x)
              callhnc(x) = hnc(x)
              callqnc(x) = qnc(x)
              callqnci(x) = invoke(qnc, Tuple{Integer}, x)

              # Purely internal
              internal(x::Real) = 0
              internal(x::Int) = x < 5 ? 1 : invoke(internal, Tuple{Real}, x)
              internalnc(x::Real) = rand()-1
              internalnc(x::Int) = x < 5 ? rand()+1 : invoke(internalnc, Tuple{Real}, x)

              # Issue #44320
              f44320(::Real) = 3
              # Issue #57115
              f57115(::Int) = 1

              call_getlast(x) = getlast(x)

              # force precompilation, force call so that inlining heuristics don't affect the result
              begin
                  Base.Experimental.@force_compile
                  @noinline callf(3)
                  @noinline callg(3)
                  @noinline callh(3)
                  @noinline callq(3)
                  @noinline callqi(3)
                  @noinline callfnc(3)
                  @noinline callgnc(3)
                  @noinline callhnc(3)
                  @noinline callqnc(3)
                  @noinline callqnci(3)
                  @noinline internal(3)
                  @noinline internalnc(3)
                  @noinline call_getlast([1,2,3])
              end
              precompile(g57115, (Any,))

              # Now that we've precompiled, invalidate with a new method that overrides the `invoke` dispatch
              $InvokeModule.h(x::Integer) = -1
              $InvokeModule.hnc(x::Integer) = rand() - 20
              # ...and for q, override with a more specialized method that should leave only the invoked version still valid
              $InvokeModule.q(x::Int) = -1
              $InvokeModule.qnc(x::Int) = rand()+1
          end
          """)
    Base.compilecache(Base.PkgId(string(CallerModule)))
    @eval using $InvokeModule: $InvokeModule
    MI = invokelatest(getglobal, @__MODULE__, InvokeModule)
    @eval $MI.getlast(a::UnitRange) = a.stop
    @eval using $CallerModule
    invokelatest() do
        M = getfield(@__MODULE__, CallerModule)

        get_method_for_type(func, @nospecialize(T)) = which(func, (T,)) # return the method func(::T)
        function nvalid(mi::Core.MethodInstance)
            isdefined(mi, :cache) || return 0
            ci = mi.cache
            n = Int(ci.max_world == typemax(UInt))
            while isdefined(ci, :next)
                ci = ci.next
                n += ci.max_world == typemax(UInt)
            end
            return n
        end

        for func in (M.f, M.g, M.internal, M.fnc, M.gnc, M.internalnc)
            m = get_method_for_type(func, Real)
            mi = m.specializations::Core.MethodInstance
            @test length(mi.backedges) == 2 || length(mi.backedges) == 4 # internalnc might have a constprop edge
            @test mi.backedges[1] === Tuple{typeof(func), Real}
            @test isa(mi.backedges[2], Core.CodeInstance)
            if length(mi.backedges) == 4
                @test mi.backedges[3] === Tuple{typeof(func), Real}
                @test isa(mi.backedges[4], Core.CodeInstance)
                @test mi.backedges[2] !== mi.backedges[4]
                @test mi.backedges[2].def === mi.backedges[4].def
            end
            @test mi.cache.max_world == typemax(mi.cache.max_world)
        end
        for func in (M.q, M.qnc)
            m = get_method_for_type(func, Integer)
            mi = m.specializations::Core.MethodInstance
            @test length(mi.backedges) == 2
            @test mi.backedges[1] === Tuple{typeof(func), Integer}
            @test isa(mi.backedges[2], Core.CodeInstance)
            @test mi.cache.max_world == typemax(mi.cache.max_world)
        end

        m = get_method_for_type(M.h, Real)
        @test m.specializations === Core.svec()
        m = get_method_for_type(M.hnc, Real)
        @test m.specializations === Core.svec()
        m = only(methods(M.callq))
        @test nvalid(m.specializations::Core.MethodInstance) == 1
        m = only(methods(M.callqnc))
        @test nvalid(m.specializations::Core.MethodInstance) == 1
        m = only(methods(M.callqi))
        @test (m.specializations::Core.MethodInstance).specTypes == Tuple{typeof(M.callqi), Int}
        m = only(methods(M.callqnci))
        @test (m.specializations::Core.MethodInstance).specTypes == Tuple{typeof(M.callqnci), Int}

        m = only(methods(M.g44320))
        @test (m.specializations::Core.MethodInstance).cache.max_world == typemax(UInt)

        m = only(methods(M.g57115))
        mi = m.specializations::Core.MethodInstance

        f_m = get_method_for_type(M.f57115, Any)
        f_mi = f_m.specializations::Core.MethodInstance

        # Make sure that f57115(::Any) has a 'call' backedge to 'g57115'
        has_f_call_backedge = false
        i = 1
        while i ≤ length(f_mi.backedges)
            if f_mi.backedges[i] isa DataType
                # invoke edge - skip
                i += 2
            else
                caller = f_mi.backedges[i]::Core.CodeInstance
                if caller.def === mi
                    has_f_call_backedge = true
                    break
                end
                i += 1
            end
        end
        @test has_f_call_backedge

        m = which(MI.getlast, (Any,))
        @test (m.specializations::Core.MethodInstance).cache.max_world == typemax(UInt)
    end

    # Precompile specific methods for arbitrary arg types
    invokeme(x) = 1
    invokeme(::Int) = 2
    m_any, m_int = sort(collect(methods(invokeme)); by=m->(m.file,m.line))
    @test precompile(invokeme, (Int,), m_any)
    @test (m_any.specializations::Core.MethodInstance).specTypes === Tuple{typeof(invokeme), Int}
    @test isempty(Base.specializations(m_int))
end

# test --compiled-modules=no command line option
precompile_test_harness("--compiled-modules=no") do dir
    Time_module = :Time4b3a94a1a081a8cb
    write(joinpath(dir, "$Time_module.jl"),
          """
          module $Time_module
              time = Base.time()
          end
          """)
    Base.compilecache(Base.PkgId("Time4b3a94a1a081a8cb"))
    exename = `$(Base.julia_cmd()) --compiled-modules=yes --startup-file=no`
    testcode = """
        insert!(LOAD_PATH, 1, $(repr(dir)))
        insert!(DEPOT_PATH, 1, $(repr(dir)))
        using $Time_module
        getfield($Time_module, :time)
    """

    t1_yes = readchomp(`$exename --compiled-modules=yes -E $(testcode)`)
    t2_yes = readchomp(`$exename --compiled-modules=yes -E $(testcode)`)
    @test t1_yes == t2_yes

    t1_no = readchomp(`$exename --compiled-modules=no -E $(testcode)`)
    t2_no = readchomp(`$exename --compiled-modules=no -E $(testcode)`)
    @test t1_no != t2_no
    @test parse(Float64, t1_no) < parse(Float64, t2_no)
end

# test loading a package with conflicting namespace
precompile_test_harness("conflicting namespaces") do dir
    Test_module = :Test6c92f26
    write(joinpath(dir, "Iterators.jl"),
          """
          module Iterators
          end
          """)
    write(joinpath(dir, "$Test_module.jl"),
          """
          module $Test_module
               import Iterators # FIXME: use `using`
          end
          """)
    testcode = """
        insert!(LOAD_PATH, 1, $(repr(dir)))
        insert!(DEPOT_PATH, 1, $(repr(dir)))
        using $Test_module
        println(stderr, $Test_module.Iterators)
    """

    exename = `$(Base.julia_cmd()) --startup-file=no`
    let fname = tempname()
        try
            for i = 1:2
                @test readchomp(pipeline(`$exename -E $(testcode)`, stderr=fname)) == "nothing"
                @test endswith(read(fname, String), "Iterators\n")
            end
        finally
            rm(fname, force=true)
        end
    end
end

precompile_test_harness("package_callbacks") do dir
    loaded_modules = Channel{Symbol}(32)
    callback = (mod::Base.PkgId) -> put!(loaded_modules, Symbol(mod.name))
    push!(Base.package_callbacks, callback)
    try
        Test1_module = :Teste4095a81
        Test2_module = :Teste4095a82
        Test3_module = :Teste4095a83

        write(joinpath(dir, "$(Test1_module).jl"),
              """
              module $(Test1_module)
              end
              """)
        Base.compilecache(Base.PkgId("$(Test1_module)"))

        write(joinpath(dir, "$(Test2_module).jl"),
              """
              module $(Test2_module)
                  using $(Test1_module)
              end
              """)
        Base.compilecache(Base.PkgId("$(Test2_module)"))

        Base.require(Main, Test2_module)
        @test take!(loaded_modules) == Test1_module
        @test take!(loaded_modules) == Test2_module
        write(joinpath(dir, "$(Test3_module).jl"),
              """
              module $(Test3_module)
                  using $(Test3_module)
              end
              """)
        Base.require(Main, Test3_module)
        @test take!(loaded_modules) == Test3_module
    finally
        pop!(Base.package_callbacks)
    end
    L = ReentrantLock()
    E = Base.Event()
    t = errormonitor(@async lock(L) do
                     wait(E)
                     Base.root_module_key(Base)
                     end)
    Test4_module = :Teste4095a84
    write(joinpath(dir, "$(Test4_module).jl"),
          """
          module $(Test4_module)
          end
          """)
    Base.compilecache(Base.PkgId("$(Test4_module)"))
    push!(Base.package_callbacks, _->(notify(E); lock(L) do; end))
    # should not hang here
    try
        @eval using $(Symbol(Test4_module))
        wait(t)
    finally
        pop!(Base.package_callbacks)
    end
    Test5_module = :Teste4095a85
    write(joinpath(dir, "$(Test5_module).jl"),
    """
    module $(Test5_module)
    end
    """)
    Base.compilecache(Base.PkgId("$(Test5_module)"))
    cnt = 0
    push!(Base.package_callbacks, _->(cnt += 1))
    try
        @eval using $(Symbol(Test5_module))
        @eval using $(Symbol(Test5_module))
        @eval using $(Symbol(Test5_module))
        @eval using $(Symbol(Test5_module))
        @eval using $(Symbol(Test5_module))
        @test cnt == 1
    finally
        pop!(Base.package_callbacks)
    end
end

# Issue #19960
(f -> f())() do # wrap in function scope, so we can test world errors
    test_workers = addprocs(1)
    push!(test_workers, myid())
    save_cwd = pwd()
    temp_path = mkdepottempdir()
    try
        cd(temp_path)
        load_path = mktempdir(temp_path)
        load_cache_path = mkdepottempdir(temp_path)

        ModuleA = :Issue19960A
        ModuleB = :Issue19960B

        write(joinpath(load_path, "$ModuleA.jl"),
            """
            module $ModuleA
                import Distributed: myid
                export f
                f() = myid()
            end
            """)

        write(joinpath(load_path, "$ModuleB.jl"),
            """
            module $ModuleB
                using $ModuleA
                export g
                g() = f()
            end
            """)

        @everywhere test_workers begin
            pushfirst!(LOAD_PATH, $load_path)
            pushfirst!(DEPOT_PATH, $load_cache_path)
        end
        try
            @eval using $ModuleB
            invokelatest() do
                uuid = Base.module_build_id(Base.root_module(Main, ModuleB))
                for wid in test_workers
                    @test Distributed.remotecall_eval(Main, wid, quote
                            Base.module_build_id(Base.root_module(Main, $(QuoteNode(ModuleB))))
                        end) == uuid
                    if wid != myid() # avoid world-age errors on the local proc
                        @test remotecall_fetch(g, wid) == wid
                    end
                end
            end
        finally
            @everywhere test_workers begin
                popfirst!(LOAD_PATH)
                popfirst!(DEPOT_PATH)
            end
        end
    finally
        cd(save_cwd)
        pop!(test_workers) # remove myid
        rmprocs(test_workers)
    end
end

# Ensure that module-loading plays nicely with Base.delete_method
# wrapped in function scope, so we can test world errors
precompile_test_harness("delete_method") do dir
    A_module = :Aedb164bd3a126418
    B_module = :Bedb164bd3a126418
    A_file = joinpath(dir, "$A_module.jl")
    B_file = joinpath(dir, "$B_module.jl")
    write(A_file,
          """
          module $A_module

          export apc, anopc, apcnc, anopcnc

          # Infer to a const
          apc(::Int, ::Int) = 1
          apc(::Any, ::Any) = 2

          anopc(::Int, ::Int) = 1
          anopc(::Any, ::Any) = 2

          # Do not infer to a const
          apcnc(::Int, ::Int) = rand() - 1
          apcnc(::Any, ::Any) = rand() + 1

          anopcnc(::Int, ::Int) = rand() - 1
          anopcnc(::Any, ::Any) = rand() + 1

          end
          """)
    write(B_file,
          """
          module $B_module

          using $A_module

          bpc(x) = apc(x, x)
          bnopc(x) = anopc(x, x)
          bpcnc(x) = apcnc(x, x)
          bnopcnc(x) = anopcnc(x, x)

          precompile(bpc, (Int,))
          precompile(bpc, (Float64,))
          precompile(bpcnc, (Int,))
          precompile(bpcnc, (Float64,))

          end
          """)
    A = Base.require(Main, A_module)
    for mths in (collect(methods(A.apc)), collect(methods(A.anopc)), collect(methods(A.apcnc)), collect(methods(A.anopcnc)))
        idx = findfirst(m -> m.sig.parameters[end] === Int, mths)
        Base.delete_method(mths[idx])
    end
    B = Base.require(Main, B_module)
    for f in (B.bpc, B.bnopc, B.bpcnc, B.bnopcnc)
        @test Base.invokelatest(f, 1) > 1
        @test Base.invokelatest(f, 1.0) > 1
    end
end

precompile_test_harness("Issues #19030 and #25279") do load_path
    ModuleA = :Issue19030
    write(joinpath(load_path, "$ModuleA.jl"),
        """
        module $ModuleA
            __init__() = push!(Base.package_callbacks, sym->nothing)
        end
        """)
    l0 = length(Base.package_callbacks)
    @eval using $ModuleA
    @test length(Base.package_callbacks) == l0 + 1
end

precompile_test_harness("Issue #25604") do load_path
    write(joinpath(load_path, "A25604.jl"),
        """
        module A25604
        using B25604
        using C25604
        end
        """)
    write(joinpath(load_path, "B25604.jl"),
        """
        module B25604
        end
        """)
    write(joinpath(load_path, "C25604.jl"),
        """
        module C25604
        using B25604
        end
        """)
    Base.compilecache(Base.PkgId("A25604"))
    @test_nowarn @eval using A25604
end

precompile_test_harness("Issue #26028") do load_path
    write(joinpath(load_path, "Foo26028.jl"),
        """
        module Foo26028
        module Bar26028
            using Foo26028: Foo26028 as InnerFoo1
            using ..Foo26028: Foo26028 as InnerFoo2
            x = 0
            y = 0
        end
        function __init__()
            Baz = @eval module Baz26028
                  using Test
                  public @test_throws
                  import Foo26028.Bar26028.y as y1
                  import ..Foo26028.Bar26028.y as y2
                  end
            @eval Base \$Baz.@test_throws(ConcurrencyViolationError("deadlock detected in loading Foo26028 using Foo26028"),
                                         import Foo26028.Bar26028.x)
        end
        end
        """)
    Base.compilecache(Base.PkgId("Foo26028"))
    @test_nowarn @eval using Foo26028
    invokelatest() do
        @test Foo26028 === Foo26028.Bar26028.InnerFoo1 === Foo26028.Bar26028.InnerFoo2
    end
end

precompile_test_harness("Issue #29936") do load_path
    write(joinpath(load_path, "Foo29936.jl"),
          """
          module Foo29936
          const global m = Val{nothing}()
          const global h = Val{:hey}()
          wab = [("a", m), ("b", h),]
          end
          """)
    @eval using Foo29936
    invokelatest() do
        @test [("Plan", Foo29936.m), ("Plan", Foo29936.h),] isa Vector{Tuple{String,Val}}
    end
end

precompile_test_harness("Issue #25971") do load_path
    sourcefile = joinpath(load_path, "Foo25971.jl")
    write(sourcefile, "module Foo25971 end")
    chmod(sourcefile, 0o666)
    cachefile, _ = Base.compilecache(Base.PkgId("Foo25971"))
    @test filemode(sourcefile) == filemode(cachefile)
    chmod(sourcefile, 0o600)
    cachefile, _ = Base.compilecache(Base.PkgId("Foo25971"))
    @test filemode(sourcefile) == filemode(cachefile)
    chmod(sourcefile, 0o444)
    cachefile, _ = Base.compilecache(Base.PkgId("Foo25971"))
    # Check writable
    @test touch(cachefile) == cachefile
end

precompile_test_harness("Issue #38312") do load_path
    TheType = """Array{Ref{Val{1}}, 1}"""
    write(joinpath(load_path, "Foo38312.jl"),
        """
        module Foo38312
        const TheType = $TheType
        end
        """)
    write(joinpath(load_path, "Bar38312.jl"),
        """
        module Bar38312
        const TheType = $TheType
        end
        """)
    Base.compilecache(Base.PkgId("Foo38312"))
    Base.compilecache(Base.PkgId("Bar38312"))
    @test pointer_from_objref((@eval (using Foo38312; Foo38312)).TheType) ===
          pointer_from_objref(eval(Meta.parse(TheType))) ===
          pointer_from_objref((@eval (using Bar38312; Bar38312)).TheType)
end

precompile_test_harness("Opaque Closure") do load_path
    write(joinpath(load_path, "OCPrecompile.jl"),
        """
        module OCPrecompile
        using Base.Experimental: @opaque
        f(x) = @opaque y->x+y
        end
        """)
    Base.compilecache(Base.PkgId("OCPrecompile"))
    f = (@eval (using OCPrecompile; OCPrecompile)).f
    @test Base.invokelatest(f, 1)(2) == 3
end

# issue #39405
precompile_test_harness("Renamed Imports") do load_path
    write(joinpath(load_path, "RenameImports.jl"),
          """
          module RenameImports
          import Base.Experimental as ex
          test() = ex
          end
          """)
    Base.compilecache(Base.PkgId("RenameImports"))
    @test (@eval (using RenameImports; RenameImports.test())) isa Module
end

# issue #41872 (example from #38983)
precompile_test_harness("No external edges") do load_path
    write(joinpath(load_path, "NoExternalEdges.jl"),
          """
          module NoExternalEdges
          bar(x::Int) = hcat(rand())
          @inline bar() = hcat(rand())
          bar(x::Float64) = bar()
          foo1() = bar(1)
          foo2() = bar(1.0)
          foo3() = bar()
          foo4() = hcat(rand())
          precompile(foo1, ())
          precompile(foo2, ())
          precompile(foo3, ())
          precompile(foo4, ())
          end
          """)
    Base.compilecache(Base.PkgId("NoExternalEdges"))
    @eval begin
        using NoExternalEdges
        @test (only(methods(NoExternalEdges.foo1)).specializations::Core.MethodInstance).cache.max_world != 0
        @test (only(methods(NoExternalEdges.foo2)).specializations::Core.MethodInstance).cache.max_world != 0
        @test (only(methods(NoExternalEdges.foo3)).specializations::Core.MethodInstance).cache.max_world != 0
        @test (only(methods(NoExternalEdges.foo4)).specializations::Core.MethodInstance).cache.max_world != 0
    end
end

@testset "issue 38149" begin
    M = Module()
    @eval M begin
        @nospecialize
        f(x, y) = x + y
        f(x::Int, y) = 2x + y
    end
    @test precompile(M.f, (Int, Any))
    @test precompile(M.f, (AbstractFloat, Any))
    mis = map(methods(M.f)) do m
        m.specializations::Core.MethodInstance
    end
    @test any(mi -> mi.specTypes.parameters[2] === Any, mis)
    @test all(mi -> isa(mi.cache, Core.CodeInstance), mis)
end

# Test that the cachepath is available in pkgorigins during the
# __init__ callback
precompile_test_harness("__init__ cachepath") do load_path
    write(joinpath(load_path, "InitCachePath.jl"),
          """
          module InitCachePath
            __init__() = Base.pkgorigins[Base.PkgId(InitCachePath)]
          end
          """)
    @test isa((@eval (using InitCachePath; InitCachePath)), Module)
end

# Test that precompilation can handle invalidated methods created from `precompile`,
# not via backedges.
precompile_test_harness("Issue #46558") do load_path
    write(joinpath(load_path, "Foo46558.jl"),
        """
        module Foo46558
        foo(x::Real) = 1
        end
        """)
    write(joinpath(load_path, "Bar46558.jl"),
        """
        module Bar46558
        using Foo46558
        precompile(Foo46558.foo, (Int,))
        end
        """)
    Base.compilecache(Base.PkgId("Foo46558"))
    Base.compilecache(Base.PkgId("Bar46558"))
    Foo = (@eval (using Foo46558; Foo46558))
    @eval ($Foo.foo)(x::Int) = 2
    Bar = (@eval (using Bar46558; Bar46558))
    @test (@eval $Foo.foo(1)) == 2
end

# TODO: Decide if we need to keep supporting this.
precompile_test_harness("issue #46296") do load_path
    write(joinpath(load_path, "CodeInstancePrecompile.jl"),
        """
        module CodeInstancePrecompile

        mi = first(Base.specializations(first(methods(identity))))
        ci = Core.CodeInstance(mi, nothing, Any, Any, nothing, nothing, zero(Int32), typemin(UInt),
                               typemax(UInt), zero(UInt32), nothing, Core.DebugInfo(mi), Core.svec())

        __init__() = @assert ci isa Core.CodeInstance

        end
        """)
    Base.compilecache(Base.PkgId("CodeInstancePrecompile"))
    (@eval (using CodeInstancePrecompile))
end

@testset "Precompile external abstract interpreter" begin
    dir = @__DIR__
    @test success(pipeline(Cmd(`$(Base.julia_cmd()) --startup-file=no precompile_absint1.jl`; dir); stdout, stderr))
    @test success(pipeline(Cmd(`$(Base.julia_cmd()) --startup-file=no precompile_absint2.jl`; dir); stdout, stderr))
    @test success(pipeline(Cmd(`$(Base.julia_cmd()) --startup-file=no precompile_extmi.jl`; dir); stdout, stderr))
end

precompile_test_harness("Recursive types") do load_path
    write(joinpath(load_path, "RecursiveTypeDef.jl"),
        """
        module RecursiveTypeDef

        struct C{T,O} end
        struct A{T,N,O} <: AbstractArray{C{T,A{T,N,O}},N}
            sz::NTuple{N,Int}
        end

        end
        """)
    Base.compilecache(Base.PkgId("RecursiveTypeDef"))
    (@eval (using RecursiveTypeDef))
    invokelatest() do
        a = Base.invokelatest(RecursiveTypeDef.A{Float64,2,String}, (3, 3))
        @test isa(a, AbstractArray)
    end
end

@testset "issue 46778" begin
    f46778(::Any, ::Type{Int}) = 1
    f46778(::Any, ::DataType) = 2
    @test precompile(Tuple{typeof(f46778), Int, DataType})
    @test (which(f46778, Tuple{Any,DataType}).specializations::Core.MethodInstance).cache.invoke != C_NULL
end


precompile_test_harness("Module tparams") do load_path
    write(joinpath(load_path, "ModuleTparams.jl"),
        """
        module ModuleTparams
            module TheTParam
            end

            struct ParamStruct{T}; end
            const the_struct = ParamStruct{TheTParam}()
        end
        """)
    Base.compilecache(Base.PkgId("ModuleTparams"))
    (@eval (using ModuleTparams))
    invokelatest() do
        @test ModuleTparams.the_struct === Base.invokelatest(ModuleTparams.ParamStruct{ModuleTparams.TheTParam})
    end
end

precompile_test_harness("PkgCacheInspector") do load_path
    # Test functionality needed by PkgCacheInspector.jl
    write(joinpath(load_path, "PCI.jl"),
        """
        module PCI
        Base.repl_cmd() = 55            # external method
        f() = Base.repl_cmd(7, "hello")   # external specialization (should never exist otherwise)
        try
            f()
        catch
        end
        end
        """)
    cachefile, ocachefile = Base.compilecache(Base.PkgId("PCI"))

    # Get the depmods
    local depmods
    @lock Base.require_lock begin
        local depmodnames
        io = open(cachefile, "r")
        try
            # isvalid_cache_header returns checksum id or nothing
            Base.isvalid_cache_header(io) === nothing && throw(ArgumentError("Invalid header in cache file $cachefile."))
            depmodnames = Base.parse_cache_header(io, cachefile)[3]
            Base.isvalid_file_crc(io) || throw(ArgumentError("Invalid checksum in cache file $cachefile."))
        finally
            close(io)
        end
        ndeps = length(depmodnames)
        depmods = Vector{Any}(undef, ndeps)
        for i in 1:ndeps
            modkey, build_id = depmodnames[i]
            dep = Base._tryrequire_from_serialized(modkey, build_id)
            if !isa(dep, Module)
                return dep
            end
            depmods[i] = dep
        end
    end

    if ocachefile !== nothing
        sv = ccall(:jl_restore_package_image_from_file, Any, (Cstring, Any, Cint, Cstring, Cint),
            ocachefile, depmods, #=completeinfo=#true, "PCI", false)
    else
        sv = ccall(:jl_restore_incremental, Any, (Cstring, Any, Cint, Cstring),
            cachefile, depmods, #=completeinfo=#true, "PCI")
    end

    modules, init_order, internal_methods, extext_methods, new_method_roots, cache_sizes = sv
    for m in internal_methods::Vector{Any}
        m isa Core.MethodInstance || continue
        m = m.func::Method
        if m.name !== :f
            @test m.name == :repl_cmd && m.nargs == 1
        end
    end
    @test any(internal_methods) do ci
        ci isa Core.CodeInstance || return false
        mi = ci.def::Core.MethodInstance
        return mi.specTypes == Tuple{typeof(Base.repl_cmd), Int, String}
    end
end

precompile_test_harness("custom MethodTable dispatch status") do load_path
    # Custom method-table methods loaded from a package image need dispatch fast-path bits restored.
    pkg = :OverlayDispatchStatus
    write(joinpath(load_path, "OverlayDispatchStatus.jl"),
        """
        module OverlayDispatchStatus
        function f end
        Base.Experimental.@MethodTable(mt)
        Base.Experimental.@overlay mt f(x::Int) = x + 1
        # generic code that resolves `f` through the overlay table when inferred
        # with an overlay-aware interpreter (like GPU runtime library functions)
        g(x) = f(x) * 2
        end
        """)
    # A dependent package whose image holds CodeInstances with call edges that
    # were resolved through the dependency's overlay table: without the dispatch
    # bits restored on the overlay method, edge revalidation drops these CIs on
    # image load (the whole cross-session cache of GPUCompiler-style consumers).
    newinterp_path = abspath(joinpath(@__DIR__, "../Compiler/test/newinterp.jl"))
    write(joinpath(load_path, "OverlayDispatchStatusUser.jl"),
        """
        module OverlayDispatchStatusUser
        import OverlayDispatchStatus

        module Custom
            import Base.Compiler: Compiler
            include($(repr(newinterp_path)))
            @newinterp OverlayDispatchStatusInterp
            import OverlayDispatchStatus
            Compiler.method_table(interp::OverlayDispatchStatusInterp) =
                Compiler.OverlayMethodTable(Compiler.get_inference_world(interp),
                                            OverlayDispatchStatus.mt)
        end

        # a call edge directly to the dependency's overlay method
        caller(x) = OverlayDispatchStatus.f(x)
        # an overlay-resolved edge reached through generic code in the dependency
        chain(x) = OverlayDispatchStatus.g(x)

        let interp = Custom.OverlayDispatchStatusInterp()
            Base.return_types(caller, (Int,); interp)
            Base.return_types(chain, (Int,); interp)
        end
        end
        """)
    Base.compilecache(Base.PkgId(string(pkg)))
    @eval using $pkg
    M = invokelatest(getglobal, @__MODULE__, pkg)
    invokelatest() do
        ms = Base._methods_by_ftype(Tuple{typeof(M.f), Int}, M.mt, 1, Base.get_world_counter())
        method = only(ms).method
        @test method.module === M
        @test !iszero(method.dispatch_status & Base.ReinferUtils.METHOD_SIG_LATEST_WHICH)
        @test !iszero(method.dispatch_status & Base.ReinferUtils.METHOD_SIG_LATEST_ONLY)
    end
    Base.compilecache(Base.PkgId("OverlayDispatchStatusUser"))
    @eval using OverlayDispatchStatusUser
    invokelatest() do
        U = OverlayDispatchStatusUser
        owner = U.Custom.OverlayDispatchStatusInterp
        # dependent CIs with overlay-resolved call edges must survive loading U's image
        for m in (only(methods(U.caller)),          # edge to the overlay method itself
                  only(methods(U.chain)),           # edge into dependency generic code
                  only(methods(M.g)))               # dependency code with the overlay edge
            mi = only(Base.specializations(m))
            @test check_presence(mi, owner) !== nothing
        end
    end
end

precompile_test_harness("DynamicExpressions") do load_path
    # https://github.com/JuliaLang/julia/pull/47184#issuecomment-1364716312
    write(joinpath(load_path, "Float16MWE.jl"),
        """
        module Float16MWE
        struct Node{T}
            val::T
        end
        doconvert(::Type{<:Node}, val) = convert(Float16, val)
        precompile(Tuple{typeof(doconvert), Type{Node{Float16}}, Float64})
        end # module Float16MWE
        """)
    Base.compilecache(Base.PkgId("Float16MWE"))
    @eval using Float16MWE
    invokelatest() do
        @test Float16MWE.doconvert(Float16MWE.Node{Float16}, -1.2) === Float16(-1.2)
    end
end

precompile_test_harness("BadInvalidations") do load_path
    write(joinpath(load_path, "BadInvalidations.jl"),
        """
        module BadInvalidations
        Base.Experimental.@compiler_options compile=min optimize=1
        getval() = Base.a_method_to_overwrite_in_test()
        getval()
        end # module BadInvalidations
        """)
    Base.compilecache(Base.PkgId("BadInvalidations"))
    @eval Base a_method_to_overwrite_in_test() = inferencebarrier(2)
    @eval using BadInvalidations
    invokelatest() do
        @test BadInvalidations.getval() === 2
    end
end

# https://github.com/JuliaLang/julia/issues/48074
precompile_test_harness("WindowsCacheOverwrite") do load_path
    # https://github.com/JuliaLang/julia/pull/47184#issuecomment-1364716312
    write(joinpath(load_path, "WindowsCacheOverwrite.jl"),
        """
        module WindowsCacheOverwrite
        end # module
        """)
    ji, ofile = Base.compilecache(Base.PkgId("WindowsCacheOverwrite"))
    @eval using WindowsCacheOverwrite

    write(joinpath(load_path, "WindowsCacheOverwrite.jl"),
        """
        module WindowsCacheOverwrite
        f() = "something new"
        end # module
        """)

    ji_2, ofile_2 = Base.compilecache(Base.PkgId("WindowsCacheOverwrite"))
    @test ofile_2 == Base.ocachefile_from_cachefile(ji_2)
end

precompile_test_harness("Issue #48391") do load_path
    write(joinpath(load_path, "I48391.jl"),
        """
        module I48391
        struct SurrealFinite <: Real end
        precompile(Tuple{typeof(Base.isless), SurrealFinite, SurrealFinite})
        Base.:(<)(x::SurrealFinite, y::SurrealFinite) = "good"
        end
        """)
    ji, ofile = Base.compilecache(Base.PkgId("I48391"))
    @eval using I48391
    x = invokelatest(()->I48391.SurrealFinite())
    @test Base.invokelatest(isless, x, x) === "good"
    @test_throws ErrorException isless(x, x)
end

precompile_test_harness("Generator nospecialize") do load_path
    write(joinpath(load_path, "GenNoSpec.jl"),
        """
        module GenNoSpec
        @generated function f(x...)
            :((\$(Base.Meta.quot(x)),))
        end
        @assert precompile(Tuple{typeof(which(f, (Any,Any)).generator.gen), Any, Any})
        end
        """)
    ji, ofile = Base.compilecache(Base.PkgId("GenNoSpec"))
    @eval using GenNoSpec
end

precompile_test_harness("Issue #50538") do load_path
    write(joinpath(load_path, "I50538.jl"),
        """
        module I50538
        const newglobal = try
            eval(Expr(:global, GlobalRef(Base, :newglobal)))
        catch ex
            ex isa ErrorException || rethrow()
            ex
        end
        const newtype = try
            Core.eval(Base, :(global newglobal::Any))
        catch ex
            ex isa ErrorException || rethrow()
            ex
        end
        global undefglobal::Any
        end
        """)
    ji, ofile = Base.compilecache(Base.PkgId("I50538"))
    @eval using I50538
    invokelatest() do
        @test I50538.newglobal.msg == "Creating a new global in closed module `Base` (`newglobal`) breaks incremental compilation because the side effects will not be permanent."
        @test I50538.newtype.msg == "Evaluation into the closed module `Base` breaks incremental compilation because the side effects will not be permanent. This is likely due to some other module mutating `Base` with `eval` during precompilation - don't do this."
        @test_throws(ErrorException("cannot set type for global I50538.undefglobal. It already has a value or is already set to a different type."),
                    Core.eval(I50538, :(global undefglobal::Int)))
        Core.eval(I50538, :(global undefglobal::Any))
        invokelatest() do
            @test Core.get_binding_type(I50538, :undefglobal) === Any
            @test !isdefined(I50538, :undefglobal)
        end
    end
end

precompile_test_harness("Test flags") do load_path
    write(joinpath(load_path, "TestFlags.jl"),
          """
          module TestFlags
          end
          """)

    current_flags = Base.CacheFlags()
    modified_flags = Base.CacheFlags(
        current_flags.use_pkgimages,
        current_flags.debug_level,
        2,
        current_flags.inline,
        3
    )
    ji, ofile = Base.compilecache(Base.PkgId("TestFlags"); flags=`--check-bounds=no -O3`)
    open(ji, "r") do io
        Base.isvalid_cache_header(io)
        _, _, _, _, _, _, flags = Base.parse_cache_header(io, ji)
        cacheflags = Base.CacheFlags(flags)
        @test cacheflags.check_bounds == 2
        @test cacheflags.opt_level == 3
    end
end

if Base.get_bool_env("CI", false) && (Sys.ARCH === :x86_64 || Sys.ARCH === :aarch64)
    @testset "Multiversioning" begin # This test isn't the most robust because it relies on being in CI,
        pkg = Base.identify_package("Test")  # but we need better target reflection to make a better one.
        cachefiles = Base.find_all_in_cache_path(pkg)
        pkgpath = Base.locate_package(pkg)
        idx = findfirst(cachefiles) do cf
            Base.stale_cachefile(pkgpath, cf) !== true
        end
        targets = Base.parse_image_targets(Base.parse_cache_header(cachefiles[idx])[6])
        @test length(targets) > 1
    end
end

precompile_test_harness("No backedge precompile") do load_path
    # Test that the system doesn't accidentally forget to revalidate a method without backedges
    write(joinpath(load_path, "NoBackEdges.jl"),
          """
          module NoBackEdges
          @eval f(a::Int, b::Int) = \$(Core.Intrinsics.add_int)(a, b)
          precompile(f, (Int, Int))
          end
          """)
    ji, ofile = Base.compilecache(Base.PkgId("NoBackEdges"))
    @eval using NoBackEdges
    invokelatest() do
        @test first(methods(NoBackEdges.f)).specializations.cache.max_world === typemax(UInt)
    end
end

precompile_test_harness("Pre-compile Core methods") do load_path
    # Core methods should support pre-compilation as external CI's like anything else
    # https://github.com/JuliaLang/julia/issues/58497
    write(joinpath(load_path, "CorePrecompilation.jl"),
          """
          module CorePrecompilation
          struct Foo end
          precompile(Tuple{Type{Vector{Foo}}, UndefInitializer, Tuple{Int}})
          end
          """)
    ji, ofile = Base.compilecache(Base.PkgId("CorePrecompilation"))
    @eval using CorePrecompilation
    invokelatest() do
        # the compiled (dispatch) specialization is the `TypeEgal`-keyed one
        let tt = Tuple{Core.TypeEgal{Vector{CorePrecompilation.Foo}}, UndefInitializer, Tuple{Int}},
            match = first(Base._methods_by_ftype(tt, -1, Base.get_world_counter())),
            mi = Base.specialize_method(match)
            @test isdefined(mi, :cache)
            @test mi.cache.max_world === typemax(UInt)
            @test mi.cache.invoke != C_NULL
        end
    end
end

# Test precompilation of generated functions that return opaque closures
# (with constprop marker set to false).
precompile_test_harness("Generated Opaque") do load_path
    write(joinpath(load_path, "GeneratedOpaque.jl"),
        """
        module GeneratedOpaque
        using Base.Experimental: @opaque
        using InteractiveUtils
        const_int_barrier() = Base.inferencebarrier(1)::typeof(1)
        const lno = LineNumberNode(1, :none)

        const ci = @code_lowered const_int_barrier()
        @generated function oc_re_generated_no_partial()
            Expr(:new_opaque_closure, Tuple{}, Any, Any, false,
                Expr(:opaque_closure_method, nothing, 0, false, lno, ci))
        end
        @assert oc_re_generated_no_partial()() === 1
        @generated function oc_re_generated_no_partial_macro()
            AT = nothing
            RT = nothing
            allow_partial = false # makes this legal to generate during pre-compile
            return Expr(:opaque_closure, AT, RT, RT, allow_partial, :(()->const_int_barrier()))
        end
        @assert oc_re_generated_no_partial_macro()() === 1
        end
        """)
    Base.compilecache(Base.PkgId("GeneratedOpaque"))
    @eval using GeneratedOpaque
    let oc = invokelatest(()->GeneratedOpaque.oc_re_generated_no_partial())
        @test oc.source.specializations.cache.max_world === typemax(UInt)
        @test oc() === 1
    end
end

precompile_test_harness("Issue #52063") do load_path
    fname = joinpath(load_path, "i_do_not_exist.jl")
    @test try
        include_dependency(fname); false
    catch e
        @test e isa SystemError
        @test e.prefix == "opening file or folder $(repr(fname))"
        true
    end
    touch(fname)
    @test include_dependency(fname) === nothing
    chmod(fname, 0x000)
    @test try
        include_dependency(fname); false
    catch e
        @test e isa SystemError
        @test e.prefix == "opening file or folder $(repr(fname))"
        true
    end skip = (Sys.isunix() && Libc.geteuid() == 0)
    dir = mktempdir() do dir
        @test include_dependency(dir) === nothing
        chmod(dir, 0x000)
        @test try
             include_dependency(dir); false
        catch e
            @test e isa SystemError
            @test e.prefix == "opening file or folder $(repr(dir))"
            true
        end skip = (Sys.isunix() && Libc.geteuid() == 0)
        dir
    end
    @test try
        include_dependency(dir); false
    catch e
        @test e isa SystemError
        @test e.prefix == "opening file or folder $(repr(dir))"
        true
    end
end

precompile_test_harness("Binding Unique") do load_path
    write(joinpath(load_path, "UniqueBinding1.jl"),
        """
        module UniqueBinding1
            export x
            global x = 1
        end
        """)
    write(joinpath(load_path, "UniqueBinding2.jl"),
        """
        module UniqueBinding2
            using UniqueBinding1
            const thebinding = ccall(:jl_get_module_binding, Ref{Core.Binding}, (Any, Any, Cint), UniqueBinding1, :x, true)
            const thebinding2 = ccall(:jl_get_module_binding, Ref{Core.Binding}, (Any, Any, Cint), @__MODULE__, :thebinding, true)
        end
        """)

    @eval using UniqueBinding1
    @eval using UniqueBinding2
    invokelatest() do
        @test UniqueBinding2.thebinding === ccall(:jl_get_module_binding, Ref{Core.Binding}, (Any, Any, Cint), UniqueBinding1, :x, true)
        @test UniqueBinding2.thebinding2 === ccall(:jl_get_module_binding, Ref{Core.Binding}, (Any, Any, Cint), UniqueBinding2, :thebinding, true)
    end
end

precompile_test_harness("Detecting importing outside of a package module") do load_path
    io = IOBuffer()
    write(joinpath(load_path, "ImportBeforeMod.jl"),
    """
    import Printf
    module ImportBeforeMod
    end #module
    """)
    @test_throws r"Failed to precompile ImportBeforeMod" Base.compilecache(Base.identify_package("ImportBeforeMod"), io, io)
    @test occursin(
        "`using/import Printf` outside of a Module detected. Importing a package outside of a module is not allowed during package precompilation.",
        String(take!(io)))


    write(joinpath(load_path, "HarmlessComments.jl"),
    """
    # import Printf
    #=
    import Printf
    =#
    module HarmlessComments
    end #module
    # import Printf
    #=
    import Printf
    =#
    """)
    Base.compilecache(Base.identify_package("HarmlessComments"))


    write(joinpath(load_path, "ImportAfterMod.jl"), """
    module ImportAfterMod
    end #module
    import Printf
    """)
    @test_throws r"Failed to precompile ImportAfterMod" Base.compilecache(Base.identify_package("ImportAfterMod"), io, io)
    @test occursin(
        "`using/import Printf` outside of a Module detected. Importing a package outside of a module is not allowed during package precompilation.",
        String(take!(io)))
end

precompile_test_harness("No package module") do load_path
    io = IOBuffer()
    write(joinpath(load_path, "NoModule.jl"),
    """
    1
    """)
    @test_throws r"Failed to precompile NoModule" Base.compilecache(Base.identify_package("NoModule"), io, io)
    @test occursin(
        "package `NoModule` did not define the expected module `NoModule`, check for typos in package module name",
        String(take!(io)))


    write(joinpath(load_path, "WrongModuleName.jl"),
    """
    module DifferentName
    x = 1
    end #module
    """)
    @test_throws r"Failed to precompile WrongModuleName" Base.compilecache(Base.identify_package("WrongModuleName"), io, io)
    @test occursin(
        "package `WrongModuleName` did not define the expected module `WrongModuleName`, check for typos in package module name",
        String(take!(io)))


    write(joinpath(load_path, "NoModuleWithImport.jl"), """
    import Printf
    """)
    @test_throws r"Failed to precompile NoModuleWithImport" Base.compilecache(Base.identify_package("NoModuleWithImport"), io, io)
    @test occursin(
        "`using/import Printf` outside of a Module detected. Importing a package outside of a module is not allowed during package precompilation.",
        String(take!(io)))
end

precompile_test_harness("Constprop CodeInstance invalidation") do load_path
    write(joinpath(load_path, "DefineTheMethod.jl"),
        """
        module DefineTheMethod
            export the_method
            the_method_val(::Val{x}) where {x} = x
            the_method_val(::Val{1}) = 0xdeadbeef
            the_method_val(::Val{2}) = 2
            the_method_val(::Val{3}) = 3
            the_method_val(::Val{4}) = 4
            the_method_val(::Val{5}) = 5
            Base.@constprop :aggressive the_method(x) = the_method_val(Val{x}())
            the_method(2)
        end
        """)
    Base.compilecache(Base.PkgId("DefineTheMethod"))
    write(joinpath(load_path, "CallTheMethod.jl"),
        """
        module CallTheMethod
            using DefineTheMethod
            call_the_method() = the_method(1)
            call_the_method()
        end
        """)
    Base.compilecache(Base.PkgId("CallTheMethod"))
    @eval using DefineTheMethod
    @eval using CallTheMethod
    @eval DefineTheMethod.the_method_val(::Val{1}) = Int(0)
    invokelatest() do
        @test Int(0) == CallTheMethod.call_the_method()
    end
end

precompile_test_harness("llvmcall validation") do load_path
    write(joinpath(load_path, "LLVMCall.jl"),
        """
        module LLVMCall
        using Base: llvmcall
        @noinline do_llvmcall() = llvmcall("ret i32 0", UInt32, Tuple{})
        do_llvmcall2() = do_llvmcall()
        do_llvmcall2()
        end
        """)
    # Also test with --pkgimages=no
    testcode = """
        insert!(LOAD_PATH, 1, $(repr(load_path)))
        insert!(DEPOT_PATH, 1, $(repr(load_path)))
        using LLVMCall
        LLVMCall.do_llvmcall2()
    """
    @test readchomp(`$(Base.julia_cmd()) --startup-file=no --pkgimages=no -E $(testcode)`) == repr(UInt32(0))
    # Now the regular way
    @eval using LLVMCall
    invokelatest() do
        @test LLVMCall.do_llvmcall2() == UInt32(0)
        @test first(methods(LLVMCall.do_llvmcall)).specializations.cache.max_world === typemax(UInt)
    end
end

precompile_test_harness("BindingReplaceDisallow") do load_path
    write(joinpath(load_path, "BindingReplaceDisallow.jl"),
        """
        module BindingReplaceDisallow
        const sinreplace = try
            eval(Expr(:block,
                Expr(:const, GlobalRef(Base, :sin), 1),
                nothing))
        catch ex
            ex isa ErrorException || rethrow()
            ex
        end
        end
        """)
    ji, ofile = Base.compilecache(Base.PkgId("BindingReplaceDisallow"))
    @eval using BindingReplaceDisallow
    invokelatest() do
        @test BindingReplaceDisallow.sinreplace.msg == "Creating a new global in closed module `Base` (`sin`) breaks incremental compilation because the side effects will not be permanent."
    end
end

precompile_test_harness("MainImportDisallow") do load_path
    write(joinpath(load_path, "MainImportDisallow.jl"),
        """
        module MainImportDisallow
            const importvar = try
                import Base.Main: cant_get_at_me
            catch ex
                ex isa ErrorException || rethrow()
                ex
            end
            const usingmain = try
                using Base.Main
            catch ex
                ex isa ErrorException || rethrow()
                ex
            end
            # Import `Main` is permitted, because it does not look at bindings inside `Main`
            import Base.Main
        end
        """)
    ji, ofile = Base.compilecache(Base.PkgId("MainImportDisallow"))
    @eval using MainImportDisallow
    invokelatest() do
        @test MainImportDisallow.importvar.msg == "Any `import` or `using` from `Main` is prohibited during incremental compilation."
        @test MainImportDisallow.usingmain.msg == "Any `import` or `using` from `Main` is prohibited during incremental compilation."
    end
end

precompile_test_harness("Package top-level load itself") do load_path
    write(joinpath(load_path, "UsingSelf.jl"),
        """
        __precompile__(false)
        module UsingSelf
        using UsingSelf
        x = 3
        end
          """)
    @eval using UsingSelf
    invokelatest() do
        @test UsingSelf.x == 3
    end
end

precompile_test_harness("Package precompilation works without manifest") do load_path
    pkg_dir = joinpath(load_path, "TestPkgNoManifest")
    mkpath(pkg_dir)

    # Create Project.toml with stdlib dependencies
    write(joinpath(pkg_dir, "Project.toml"), """
        name = "TestPkgNoManifest"
        uuid = "f47a8e44-5f82-4c5c-9076-4b4e8b7e8e8e"
        version = "0.1.0"

        [deps]
        Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
        Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
        """)

    # Create src directory and main module file
    src_dir = joinpath(pkg_dir, "src")
    mkpath(src_dir)
    write(joinpath(src_dir, "TestPkgNoManifest.jl"), """
        module TestPkgNoManifest
        end
        """)

    old_active_project = Base.active_project()
    try
        # Activate the new package environment
        Base.set_active_project(joinpath(pkg_dir, "Project.toml"))

        # Ensure there's no manifest file (this is the key to the test)
        manifest_path = joinpath(pkg_dir, "Manifest.toml")
        isfile(manifest_path) && rm(manifest_path)

        # This should work without errors - precompiling a package with no manifest
        @eval using TestPkgNoManifest
    finally
        # Restore original load path and active project
        Base.set_active_project(old_active_project)
    end
end

# Verify that inference / caching was not performed for any macros in the sysimage
let m = only(methods(Base.var"@lazy_str"))
    for mi in Base.specializations(m)
        isdefined(mi, :cache) || continue
        ci = mi.cache
        @test !isdefined(ci, :inferred)
        @test !isdefined(ci, :next)
    end
end

# Issue #58841 - make sure we don't accidentally throw away code for inference
let io = IOBuffer()
    run(pipeline(`$(Base.julia_cmd()) --startup-file=no --trace-compile=stderr -e 'f() = sin(1.) == 0. ? 1 : 0; exit(f())'`, stderr=io))
    @test isempty(String(take!(io)))
end

# Test --compiled-modules=strict in precompilepkgs
@testset "compiled-modules=strict with dependencies" begin
    mkdepottempdir() do depot
        # Create three packages: one that fails to precompile, one that loads it, one that doesn't
        project_path = joinpath(depot, "testenv")
        mkpath(project_path)

        # Create FailPkg - a package that can't be precompiled
        fail_pkg_path = joinpath(depot, "dev", "FailPkg")
        mkpath(joinpath(fail_pkg_path, "src"))
        write(joinpath(fail_pkg_path, "Project.toml"),
              """
              name = "FailPkg"
              uuid = "10000000-0000-0000-0000-000000000001"
              version = "0.1.0"
              """)
        write(joinpath(fail_pkg_path, "src", "FailPkg.jl"),
              """
              module FailPkg
              print("Now FailPkg is running.\n")
              error("expected fail")
              end
              """)

        # Create LoadsFailPkg - depends on and loads FailPkg (should fail with strict)
        loads_pkg_path = joinpath(depot, "dev", "LoadsFailPkg")
        mkpath(joinpath(loads_pkg_path, "src"))
        write(joinpath(loads_pkg_path, "Project.toml"),
              """
              name = "LoadsFailPkg"
              uuid = "20000000-0000-0000-0000-000000000002"
              version = "0.1.0"

              [deps]
              FailPkg = "10000000-0000-0000-0000-000000000001"
              """)
        write(joinpath(loads_pkg_path, "src", "LoadsFailPkg.jl"),
              """
              module LoadsFailPkg
              print("Now LoadsFailPkg is running.\n")
              import FailPkg
              print("unreachable\n")
              end
              """)

        # Create DependsOnly - depends on FailPkg but doesn't load it (should succeed)
        depends_pkg_path = joinpath(depot, "dev", "DependsOnly")
        mkpath(joinpath(depends_pkg_path, "src"))
        write(joinpath(depends_pkg_path, "Project.toml"),
              """
              name = "DependsOnly"
              uuid = "30000000-0000-0000-0000-000000000003"
              version = "0.1.0"

              [deps]
              FailPkg = "10000000-0000-0000-0000-000000000001"
              """)
        write(joinpath(depends_pkg_path, "src", "DependsOnly.jl"),
              """
              module DependsOnly
              # Has FailPkg as a dependency but doesn't load it
              print("Now DependsOnly is running.\n")
              end
              """)

        # Create main project with all packages
        write(joinpath(project_path, "Project.toml"),
              """
              [deps]
              LoadsFailPkg = "20000000-0000-0000-0000-000000000002"
              DependsOnly = "30000000-0000-0000-0000-000000000003"
              """)
        write(joinpath(project_path, "Manifest.toml"),
              """
              julia_version = "1.13.0"
              manifest_format = "2.0"

              [[DependsOnly]]
              deps = ["FailPkg"]
              uuid = "30000000-0000-0000-0000-000000000003"
              version = "0.1.0"

              [[FailPkg]]
              uuid = "10000000-0000-0000-0000-000000000001"
              version = "0.1.0"

              [[LoadsFailPkg]]
              deps = ["FailPkg"]
              uuid = "20000000-0000-0000-0000-000000000002"
              version = "0.1.0"

              [[deps.DependsOnly]]
              deps = ["FailPkg"]
              path = "../dev/DependsOnly/"
              uuid = "30000000-0000-0000-0000-000000000003"
              version = "0.1.0"

              [[deps.FailPkg]]
              path = "../dev/FailPkg/"
              uuid = "10000000-0000-0000-0000-000000000001"
              version = "0.1.0"

              [[deps.LoadsFailPkg]]
              deps = ["FailPkg"]
              path = "../dev/LoadsFailPkg/"
              uuid = "20000000-0000-0000-0000-000000000002"
              version = "0.1.0"
              """)

        # Call precompilepkgs with output redirected to a file
        LoadsFailPkg_output = joinpath(depot, "LoadsFailPkg_output.txt")
        DependsOnly_output = joinpath(depot, "DependsOnly_output.txt")
        original_depot_path = copy(Base.DEPOT_PATH)
        old_proj = Base.active_project()
        try
            push!(empty!(DEPOT_PATH), depot)
            Base.set_active_project(project_path)
            precompile_capture(file, pkg) = open(file, "w") do io
                try
                    r = Base.Precompilation.precompilepkgs([pkg]; io, fancyprint=true)
                    @test r isa Vector{String}
                    r
                catch ex
                    ex isa Base.Precompilation.PkgPrecompileError || rethrow()
                    ex
                end
            end
            loadsfailpkg = precompile_capture(LoadsFailPkg_output, "LoadsFailPkg")
            @test loadsfailpkg isa Base.Precompilation.PkgPrecompileError
            dependsonly = precompile_capture(DependsOnly_output, "DependsOnly")
            @test length(dependsonly) == 1
        finally
            Base.set_active_project(old_proj)
            append!(empty!(DEPOT_PATH), original_depot_path)
        end

        output = read(LoadsFailPkg_output, String)
        # LoadsFailPkg should fail because it tries to load FailPkg with --compiled-modules=strict
        @test count("LoadError: expected fail", output) == 1
        @test count("expected fail", output) == 1
        @test count("✗ FailPkg", output) > 0
        @test count("✗ LoadsFailPkg", output) > 0
        @test count("Now FailPkg is running.", output) == 1
        @test count("Now LoadsFailPkg is running.", output) == 1
        @test count("DependsOnly precompiling.", output) == 0

        # DependsOnly should succeed because it doesn't actually load FailPkg
        output = read(DependsOnly_output, String)
        @test count("LoadError: expected fail", output) == 0
        @test count("expected fail", output) == 0
        @test count("✗ FailPkg", output) > 0
        @test count("Precompiling DependsOnly finished.", output) == 1
        @test count("Now FailPkg is running.", output) == 0
        @test count("Now DependsOnly is running.", output) == 1
    end
end

precompile_test_harness("invalidation for 'foreign-keyed' Preferences") do load_path
    # Test that compile-time preferences invalidate, even when queried from a
    # "foreign" UUID / package namespace
    foreign_uuid_str = "b5f1a95c-d45e-4b9e-84c6-b7ea3b5e5f22"
    foreign_uuid = Base.UUID(foreign_uuid_str)

    # Package that records compile-time preferences for a "foreign" UUID
    write(joinpath(load_path, "PkgCrossPrefs.jl"),
          """
          module PkgCrossPrefs
              # Query preferences for a foreign package UUID (cross-UUID tracking)
              Base.record_compiletime_preference(Base.UUID("$foreign_uuid_str"), "debug")
              # Also query a preference that won't be set
              Base.record_compiletime_preference(Base.UUID("$foreign_uuid_str"), "unset_opt")
          end
          """)

    env_base_dir    = mkpath(joinpath(load_path, "env_base"))
    env_changed_dir = mkpath(joinpath(load_path, "env_changed"))
    env_unset_dir   = mkpath(joinpath(load_path, "env_unset_set"))

    env_base      = joinpath(env_base_dir, "Project.toml")
    env_changed   = joinpath(env_changed_dir, "Project.toml")
    env_unset_set = joinpath(env_unset_dir, "Project.toml")

    write(env_base, """
    [deps]
    PkgForeign = "$foreign_uuid_str"

    [preferences.PkgForeign]
    debug = false
    """)
    write(env_changed, """
    [deps]
    PkgForeign = "$foreign_uuid_str"

    [preferences.PkgForeign]
    debug = true
    """)
    write(env_unset_set, """
    [deps]
    PkgForeign = "$foreign_uuid_str"

    [preferences.PkgForeign]
    debug = false
    unset_opt = "now_set"
    """)

    old_proj = Base.active_project()
    try
        Base.set_active_project(env_base)
        cachefile, _ = Base.compilecache(Base.PkgId("PkgCrossPrefs"))
        pkg_file = joinpath(load_path, "PkgCrossPrefs.jl")

        # Cache is not stale with the original preferences
        @test Base.stale_cachefile(pkg_file, cachefile) !== true
        # Changing the (foreign) preferences makes the cache stale
        Base.set_active_project(env_changed)
        @test Base.stale_cachefile(pkg_file, cachefile) === true
        # Restoring the original preferences makes it not stale again
        Base.set_active_project(env_base)
        @test Base.stale_cachefile(pkg_file, cachefile) !== true
        # Setting a previously-unset preference also causes staleness
        Base.set_active_project(env_unset_set)
        @test Base.stale_cachefile(pkg_file, cachefile) === true
    finally
        Base.set_active_project(old_proj)
    end
end

precompile_test_harness("Preferences hash collision (issue #59344), part 1") do load_path
    # Test for the preference hash collision bug (https://github.com/JuliaLang/julia/issues/59344)
    # related to skipping unset preferences in preference hash
    pkg_uuid_str = "c9de8a70-0fad-4996-b3e2-f9c45bce31af"
    pkg_uuid = Base.UUID(pkg_uuid_str)

    pkg_src_dir = mkpath(joinpath(load_path, "PkgHashCollision", "src"))
    write(joinpath(pkg_src_dir, "PkgHashCollision.jl"),
          """
          module PkgHashCollision
              Base.record_compiletime_preference(Base.UUID("$pkg_uuid_str"), "xyz")
              Base.record_compiletime_preference(Base.UUID("$pkg_uuid_str"), "abc")
          end
          """)
    write(joinpath(load_path, "PkgHashCollision", "Project.toml"), """
          name = "PkgHashCollision"
          uuid = "$pkg_uuid_str"
          """)

    env_xyz_dir = mkpath(joinpath(load_path, "env_xyz"))
    env_abc_dir = mkpath(joinpath(load_path, "env_abc"))

    env_xyz = joinpath(env_xyz_dir, "Project.toml")
    env_abc = joinpath(env_abc_dir, "Project.toml")

    # First config: xyz has the value, abc is unset
    write(env_xyz, """
    [deps]
    PkgHashCollision = "$pkg_uuid_str"

    [preferences.PkgHashCollision]
    xyz = "same_value"
    """)
    # Second config: abc has the same value, xyz is unset
    write(env_abc, """
    [deps]
    PkgHashCollision = "$pkg_uuid_str"

    [preferences.PkgHashCollision]
    abc = "same_value"
    """)

    old_proj = Base.active_project()
    try
        Base.set_active_project(env_xyz)
        cachefile, _ = Base.compilecache(Base.PkgId(pkg_uuid, "PkgHashCollision"))
        pkg_file = joinpath(load_path, "PkgHashCollision", "src", "PkgHashCollision.jl")

        @test Base.stale_cachefile(pkg_file, cachefile) !== true
        Base.set_active_project(env_abc)
        @test Base.stale_cachefile(pkg_file, cachefile) === true
    finally
        Base.set_active_project(old_proj)
    end
end

precompile_test_harness("Preferences hash collision (issue #59344), part 2") do load_path
    # On Julia ≥1.13, `pref = false` and an unset preference triggered a hash collision,
    # causing preference changes not to invalidate. Test that this invalidates as expected.
    pkg_uuid_str = "d2e8c371-1b3f-4f5a-8c7e-9a2b1d4e6f8c"
    pkg_uuid = Base.UUID(pkg_uuid_str)

    pkg_src_dir = mkpath(joinpath(load_path, "PkgFalsePrefs", "src"))
    write(joinpath(pkg_src_dir, "PkgFalsePrefs.jl"),
          """
          module PkgFalsePrefs
              Base.record_compiletime_preference(Base.UUID("$pkg_uuid_str"), "flag")
          end
          """)
    write(joinpath(load_path, "PkgFalsePrefs", "Project.toml"), """
          name = "PkgFalsePrefs"
          uuid = "$pkg_uuid_str"
          """)

    env_false_dir = mkpath(joinpath(load_path, "env_false"))
    env_none_dir  = mkpath(joinpath(load_path, "env_none"))

    env_false = joinpath(env_false_dir, "Project.toml")
    env_none  = joinpath(env_none_dir,  "Project.toml")

    # Compiled with flag = false
    write(env_false, """
    [deps]
    PkgFalsePrefs = "$pkg_uuid_str"

    [preferences.PkgFalsePrefs]
    flag = false
    """)
    # Preference removed entirely
    write(env_none, """
    [deps]
    PkgFalsePrefs = "$pkg_uuid_str"
    """)

    old_proj = Base.active_project()
    try
        Base.set_active_project(env_false)
        cachefile, _ = Base.compilecache(Base.PkgId(pkg_uuid, "PkgFalsePrefs"))
        pkg_file = joinpath(load_path, "PkgFalsePrefs", "src", "PkgFalsePrefs.jl")

        @test Base.stale_cachefile(pkg_file, cachefile) !== true
        Base.set_active_project(env_none)
        @test Base.stale_cachefile(pkg_file, cachefile) === true
    finally
        Base.set_active_project(old_proj)
    end
end

# Workspace sub-environment precompilation should not precompile packages from other sub-environments
@testset "workspace sub-environment precompilation scoping" begin
    mkdepottempdir() do depot
        workspace_path = joinpath(@__DIR__, "project", "Workspaces", "PrecompileExt")
        fooenv_path = joinpath(workspace_path, "FooEnv")

        original_depot_path = copy(Base.DEPOT_PATH)
        old_proj = Base.active_project()
        try
            push!(empty!(DEPOT_PATH), depot)
            # Activate FooEnv (only depends on Foo, not Bar)
            Base.set_active_project(fooenv_path)

            io = IOBuffer()
            ioc = IOContext(io, :color => false)
            Base.Precompilation.precompilepkgs(; io=ioc, fancyprint=false)
            output = String(take!(io))

            # Foo should be precompiled
            @test occursin("Foo", output)
            # Bar should NOT be precompiled (it's in another sub-environment)
            @test !occursin("Bar", output)
        finally
            Base.set_active_project(old_proj)
            append!(empty!(DEPOT_PATH), original_depot_path)
        end
    end
end

# TypeEq payloads that mention package-image datatypes must be restored as new
# before restore-side type-cache lookup compares them to cached types.
@testset "precompile TypeEq references to new datatypes" begin
    mkdepottempdir() do depot
        project_path = joinpath(depot, "testenv")
        dev_path = joinpath(depot, "dev")
        mkpath(project_path)
        mkpath(dev_path)

        trigger_uuid = "10000000-0000-0000-0000-000000000103"
        parent_uuid = "10000000-0000-0000-0000-000000000104"

        trigger_dir = joinpath(dev_path, "TypeEqTrigger")
        mkpath(joinpath(trigger_dir, "src"))
        write(joinpath(trigger_dir, "Project.toml"), """
            name = "TypeEqTrigger"
            uuid = "$trigger_uuid"
            version = "0.1.0"
            """)
        write(joinpath(trigger_dir, "src", "TypeEqTrigger.jl"), """
            module TypeEqTrigger
            macro latestworld_if_toplevel()
                Expr(Symbol("latestworld-if-toplevel"))
            end
            workload() = tuple(values(Dict(:a => 1))...)
            if ccall(:jl_generating_output, Cint, ()) == 1
                ccall(:jl_tag_newly_inferred_enable, Cvoid, ())
                try
                    @latestworld_if_toplevel
                    workload()
                finally
                    ccall(:jl_tag_newly_inferred_disable, Cvoid, ())
                end
            end
            end
            """)

        parent_dir = joinpath(dev_path, "TypeEqParent")
        mkpath(joinpath(parent_dir, "src"))
        write(joinpath(parent_dir, "Project.toml"), """
            name = "TypeEqParent"
            uuid = "$parent_uuid"
            version = "0.1.0"
            """)
        write(joinpath(parent_dir, "src", "TypeEqParent.jl"), """
            module TypeEqParent
            import Base: range

            abstract type Left{N} end
            abstract type Right{N} end
            struct Iter{C}
                c::C
            end

            Base.iterate(itr::Iter{C}, state::Int=0) where
                {N, C <: Union{Left{N},Right{N}}} =
                    state >= N ? nothing : (0, state + 1)
            Base.BroadcastStyle(::Type{<:Iter{C}}) where
                {N, C <: Union{Left{N},Right{N}}} =
                    Base.BroadcastStyle(NTuple{N,Int})
            Base.broadcastable(itr::Iter{C}) where
                {N, C <: Union{Left{N},Right{N}}} =
                    (itr...,)::NTuple{N,Int}

            comps(c::C) where {N, C <: Union{Left{N},Right{N}}} = Iter(c)
            range(start::T; stop::T, length::Integer=100) where
                {N, T <: Union{Left{N},Right{N}}} =
                    comps(start) .+ comps(stop)
            range(start::T, stop::T; kwargs...) where
                {N, T <: Union{Left{N},Right{N}}} =
                    range(start; stop=stop, kwargs...)

            macro latestworld_if_toplevel()
                Expr(Symbol("latestworld-if-toplevel"))
            end
            function workload()
                for T in (Float64, Int)
                    range(one(T), T(2); length=2)
                end
            end
            if ccall(:jl_generating_output, Cint, ()) == 1
                ccall(:jl_tag_newly_inferred_enable, Cvoid, ())
                try
                    @latestworld_if_toplevel
                    workload()
                finally
                    ccall(:jl_tag_newly_inferred_disable, Cvoid, ())
                end
            end
            end
            """)

        write(joinpath(project_path, "Project.toml"), """
            [deps]
            TypeEqParent = "$parent_uuid"
            TypeEqTrigger = "$trigger_uuid"
            """)
        write(joinpath(project_path, "Manifest.toml"), """
            manifest_format = "2.0"

            [[deps.TypeEqParent]]
            path = "../dev/TypeEqParent"
            uuid = "$parent_uuid"
            version = "0.1.0"

            [[deps.TypeEqTrigger]]
            path = "../dev/TypeEqTrigger"
            uuid = "$trigger_uuid"
            version = "0.1.0"
            """)

        original_depot_path = copy(Base.DEPOT_PATH)
        old_proj = Base.active_project()
        try
            push!(empty!(DEPOT_PATH), depot)
            Base.set_active_project(project_path)
            Base.Precompilation.precompilepkgs(; io=IOBuffer(), fancyprint=false)
            @test Base.require(Main, :TypeEqParent) isa Module
            @test Base.require(Main, :TypeEqTrigger) isa Module
        finally
            Base.set_active_project(old_proj)
            append!(empty!(DEPOT_PATH), original_depot_path)
        end
    end
end

# Issue #61198 - extensions with superset triggers must be in the precompilation dep graph
@testset "precompilation dep graph includes transitively-triggered extensions" begin
    mkdepottempdir() do depot
        project_path = joinpath(depot, "testenv")
        mkpath(project_path)

        parent_uuid = "10000000-0000-0000-0000-000000000023"
        triga_uuid  = "10000000-0000-0000-0000-000000000050"
        trigb_uuid  = "20000000-0000-0000-0000-000000000001"
        top_uuid    = "10000000-0000-0000-0000-000000000064"

        # ParentPkg with two extensions: ExtA triggered by TrigA,
        # ExtAB triggered by [TrigA, TrigB] (superset of ExtA's triggers)
        parent_dir = joinpath(depot, "dev", "ParentPkg")
        mkpath(joinpath(parent_dir, "src"))
        mkpath(joinpath(parent_dir, "ext"))
        write(joinpath(parent_dir, "Project.toml"), """
            name = "ParentPkg"
            uuid = "$parent_uuid"
            version = "0.1.0"

            [weakdeps]
            TrigA = "$triga_uuid"
            TrigB = "$trigb_uuid"

            [extensions]
            ExtA = "TrigA"
            ExtAB = ["TrigA", "TrigB"]
            """)
        write(joinpath(parent_dir, "src", "ParentPkg.jl"), """
            module ParentPkg
            end
            """)
        write(joinpath(parent_dir, "ext", "ExtA.jl"), """
            module ExtA
            using ParentPkg, TrigA
            end
            """)
        write(joinpath(parent_dir, "ext", "ExtAB.jl"), """
            module ExtAB
            using ParentPkg, TrigA, TrigB
            end
            """)

        triga_dir = joinpath(depot, "dev", "TrigA")
        mkpath(joinpath(triga_dir, "src"))
        write(joinpath(triga_dir, "Project.toml"), """
            name = "TrigA"
            uuid = "$triga_uuid"
            version = "0.1.0"
            """)
        write(joinpath(triga_dir, "src", "TrigA.jl"), """
            module TrigA
            end
            """)

        trigb_dir = joinpath(depot, "dev", "TrigB")
        mkpath(joinpath(trigb_dir, "src"))
        write(joinpath(trigb_dir, "Project.toml"), """
            name = "TrigB"
            uuid = "$trigb_uuid"
            version = "0.1.0"
            """)
        write(joinpath(trigb_dir, "src", "TrigB.jl"), """
            module TrigB
            end
            """)

        # TopPkg depends on ParentPkg + both triggers, so both extensions fire
        top_dir = joinpath(depot, "dev", "TopPkg")
        mkpath(joinpath(top_dir, "src"))
        write(joinpath(top_dir, "Project.toml"), """
            name = "TopPkg"
            uuid = "$top_uuid"
            version = "0.1.0"

            [deps]
            ParentPkg = "$parent_uuid"
            TrigA = "$triga_uuid"
            TrigB = "$trigb_uuid"
            """)
        write(joinpath(top_dir, "src", "TopPkg.jl"), """
            module TopPkg
            using ParentPkg, TrigA, TrigB
            end
            """)

        write(joinpath(project_path, "Project.toml"), """
            [deps]
            ParentPkg = "$parent_uuid"
            TrigA = "$triga_uuid"
            TrigB = "$trigb_uuid"
            TopPkg = "$top_uuid"
            """)

        write(joinpath(project_path, "Manifest.toml"), """
            manifest_format = "2.0"

            [[deps.ParentPkg]]
            path = "../dev/ParentPkg/"
            uuid = "$parent_uuid"
            version = "0.1.0"

            [deps.ParentPkg.weakdeps]
            TrigA = "$triga_uuid"
            TrigB = "$trigb_uuid"

            [deps.ParentPkg.extensions]
            ExtA = "TrigA"
            ExtAB = ["TrigA", "TrigB"]

            [[deps.TrigA]]
            path = "../dev/TrigA/"
            uuid = "$triga_uuid"
            version = "0.1.0"

            [[deps.TrigB]]
            path = "../dev/TrigB/"
            uuid = "$trigb_uuid"
            version = "0.1.0"

            [[deps.TopPkg]]
            deps = ["ParentPkg", "TrigA", "TrigB"]
            path = "../dev/TopPkg/"
            uuid = "$top_uuid"
            version = "0.1.0"
            """)

        original_depot_path = copy(Base.DEPOT_PATH)
        old_proj = Base.active_project()
        try
            push!(empty!(DEPOT_PATH), depot)
            Base.set_active_project(project_path)

            # First precompilation: compile everything
            Base.Precompilation.precompilepkgs(; io=IOBuffer(), fancyprint=false)

            # Second precompilation: should not recompile anything
            io = IOBuffer()
            Base.Precompilation.precompilepkgs(; io, fancyprint=false)
            @test isempty(takestring!(io))
        finally
            Base.set_active_project(old_proj)
            append!(empty!(DEPOT_PATH), original_depot_path)
        end
    end
end

# Test that warn_loaded names loaded packages and counts affected dependents
@testset "warn_loaded names packages and counts dependents" begin
    mkdepottempdir() do depot; mktempdir() do dir
        # Create LoadedDep old source — loaded at the start of the script
        loaded_dep_old_path = joinpath(dir, "dev", "LoadedDepOld")
        mkpath(joinpath(loaded_dep_old_path, "src"))
        write(joinpath(loaded_dep_old_path, "Project.toml"),
              """
              name = "LoadedDep"
              uuid = "a1a1a1a1-0000-0000-0000-000000000001"
              version = "0.1.0"
              """)
        write(joinpath(loaded_dep_old_path, "src", "LoadedDep.jl"),
              """
              module LoadedDep
              const _v = 1  # old version marker forces a different build_id
              end
              """)

        # Create LoadedDep new source — resolved by the environment after switching
        loaded_dep_new_path = joinpath(dir, "dev", "LoadedDepNew")
        mkpath(joinpath(loaded_dep_new_path, "src"))
        write(joinpath(loaded_dep_new_path, "Project.toml"),
              """
              name = "LoadedDep"
              uuid = "a1a1a1a1-0000-0000-0000-000000000001"
              version = "0.2.0"
              """)
        write(joinpath(loaded_dep_new_path, "src", "LoadedDep.jl"),
              """
              module LoadedDep
              const _v = 2  # new version marker forces a different build_id
              end
              """)

        # Create DepUser — depends on LoadedDep
        depuser_path = joinpath(dir, "dev", "DepUser")
        mkpath(joinpath(depuser_path, "src"))
        write(joinpath(depuser_path, "Project.toml"),
              """
              name = "DepUser"
              uuid = "b2b2b2b2-0000-0000-0000-000000000002"
              version = "0.1.0"

              [deps]
              LoadedDep = "a1a1a1a1-0000-0000-0000-000000000001"
              """)
        write(joinpath(depuser_path, "src", "DepUser.jl"),
              """
              module DepUser
              import LoadedDep
              end
              """)

        # old_project: used to load the old version of LoadedDep
        old_project_path = joinpath(dir, "old_project")
        mkpath(old_project_path)
        write(joinpath(old_project_path, "Project.toml"),
              """
              [deps]
              LoadedDep = "a1a1a1a1-0000-0000-0000-000000000001"
              """)
        write(joinpath(old_project_path, "Manifest.toml"),
              """
              manifest_format = "2.0"

              [[deps.LoadedDep]]
              path = "../dev/LoadedDepOld/"
              uuid = "a1a1a1a1-0000-0000-0000-000000000001"
              version = "0.1.0"
              """)

        # new_project: resolved after loading old version; has LoadedDep new source
        new_project_path = joinpath(dir, "new_project")
        mkpath(new_project_path)
        write(joinpath(new_project_path, "Project.toml"),
              """
              [deps]
              DepUser = "b2b2b2b2-0000-0000-0000-000000000002"
              LoadedDep = "a1a1a1a1-0000-0000-0000-000000000001"
              """)
        write(joinpath(new_project_path, "Manifest.toml"),
              """
              manifest_format = "2.0"

              [[deps.DepUser]]
              deps = ["LoadedDep"]
              path = "../dev/DepUser/"
              uuid = "b2b2b2b2-0000-0000-0000-000000000002"
              version = "0.1.0"

              [[deps.LoadedDep]]
              path = "../dev/LoadedDepNew/"
              uuid = "a1a1a1a1-0000-0000-0000-000000000001"
              version = "0.2.0"
              """)

        # Load the old LoadedDep first, then switch to the new project whose manifest
        # resolves a different source for LoadedDep — this causes a build_id mismatch
        # and should trigger the warn_loaded warning.
        script = """
            using LoadedDep
            Base.set_active_project($(repr(new_project_path)))
            Base.Precompilation.precompilepkgs(; fancyprint=false, warn_loaded=true)
            """

        cmd = addenv(`$(Base.julia_cmd()) --startup-file=no --project=$(old_project_path) -e $script`,
                     "JULIA_DEPOT_PATH" => depot)

        out = Base.PipeEndpoint()
        log = @async read(out, String)
        try
            proc = run(pipeline(cmd, stdout=out, stderr=out))
            @test success(proc)
        catch
            @show fetch(log)
            rethrow()
        end
        output = fetch(log)
        @test occursin("currently loaded", output)
        @test occursin("LoadedDep", output)
    end end
end

# Test that warn_loaded does not warn when the loaded dep is already at the correct version
@testset "warn_loaded does not warn when loaded dep matches env version" begin
    mkdepottempdir() do depot; mktempdir() do dir
        loaded_dep_path = joinpath(dir, "dev", "LoadedDep")
        mkpath(joinpath(loaded_dep_path, "src"))
        write(joinpath(loaded_dep_path, "Project.toml"),
              """
              name = "LoadedDep"
              uuid = "a1a1a1a1-0000-0000-0000-000000000001"
              version = "0.1.0"
              """)
        write(joinpath(loaded_dep_path, "src", "LoadedDep.jl"),
              """
              module LoadedDep
              end
              """)

        depuser_path = joinpath(dir, "dev", "DepUser")
        mkpath(joinpath(depuser_path, "src"))
        write(joinpath(depuser_path, "Project.toml"),
              """
              name = "DepUser"
              uuid = "b2b2b2b2-0000-0000-0000-000000000002"
              version = "0.1.0"

              [deps]
              LoadedDep = "a1a1a1a1-0000-0000-0000-000000000001"
              """)
        write(joinpath(depuser_path, "src", "DepUser.jl"),
              """
              module DepUser
              import LoadedDep
              end
              """)

        project_path = joinpath(dir, "project")
        mkpath(project_path)
        write(joinpath(project_path, "Project.toml"),
              """
              [deps]
              DepUser = "b2b2b2b2-0000-0000-0000-000000000002"
              LoadedDep = "a1a1a1a1-0000-0000-0000-000000000001"
              """)
        # Manifest points to the same source as what gets loaded — versions match
        write(joinpath(project_path, "Manifest.toml"),
              """
              manifest_format = "2.0"

              [[deps.DepUser]]
              deps = ["LoadedDep"]
              path = "../dev/DepUser/"
              uuid = "b2b2b2b2-0000-0000-0000-000000000002"
              version = "0.1.0"

              [[deps.LoadedDep]]
              path = "../dev/LoadedDep/"
              uuid = "a1a1a1a1-0000-0000-0000-000000000001"
              version = "0.1.0"
              """)

        # Load LoadedDep first (caching it), then run precompilepkgs — the env resolves the
        # same version that is loaded, so no version-mismatch warning should appear.
        script = """
            using LoadedDep
            Base.Precompilation.precompilepkgs(; fancyprint=false, warn_loaded=true)
            """

        cmd = addenv(`$(Base.julia_cmd()) --startup-file=no --project=$(project_path) -e $script`,
                     "JULIA_DEPOT_PATH" => depot)

        out = Base.PipeEndpoint()
        log = @async read(out, String)
        try
            proc = run(pipeline(cmd, stdout=out, stderr=out))
            @test success(proc)
        catch
            @show fetch(log)
            rethrow()
        end
        output = fetch(log)
        @test !occursin("currently loaded", output)
    end end
end

# PR #61915: a precompiled value with an inline `Type{Union{}}` field used to abort
# in `record_memoryrefs_inside` while writing the cache, because the `TypeEq` field
# type is laid out as the singleton `typeof(Union{})` `DataType`.
precompile_test_harness("Type{Union{}} inline field") do dir
    TypeBottomField = :TypeBottomField61915
    write(joinpath(dir, "$TypeBottomField.jl"),
          """
          module $TypeBottomField
              struct HoldsBottom
                  t::Type{Union{}}
                  x::Int
              end
              const X = HoldsBottom(Union{}, 7)
          end
          """)
    @test Base.compilecache(Base.PkgId(string(TypeBottomField))) isa Tuple
    @eval using $TypeBottomField
    @test (@eval $TypeBottomField.X.x) == 7
    @test (@eval $TypeBottomField.X.t) === Union{}
end

# Cache rejection reasons and how they are reported when loading triggers precompilation:
# caches left behind by another version of Julia collapse into one generic message, while
# actionable reasons (e.g. changed source) are reported by name without version noise.
precompile_test_harness("cache rejection reasons") do dir
    pkgfile = joinpath(dir, "RejectReasons.jl")
    write(pkgfile,
          """
          module RejectReasons
          end
          """)
    cachefile, _ = Base.compilecache(Base.PkgId("RejectReasons"))

    # fresh cache: nothing recorded, nothing reported
    reasons = Dict{Symbol,Int}()
    @test Base.stale_cachefile(pkgfile, cachefile; reasons) !== true
    @test isempty(reasons)
    @test Base.list_reasons(reasons) == ""

    # a cache left in the depot after updating Julia fails the header check, which
    # embeds the Julia version and commit (simulated here with junk bytes); that
    # alone reports only the generic version message
    oldcache = joinpath(dirname(cachefile), "RejectReasons_oldversion.ji")
    write(oldcache, fill(0xff, 1024))
    reasons = Dict{Symbol,Int}()
    @test Base.stale_cachefile(pkgfile, oldcache; reasons) === true
    @test reasons == Dict(:incompatible_header => 1)
    @test Base.list_reasons(reasons) == " (no compatible cache for this version of Julia)"

    # changing the source makes the compatible cache stale for an actionable reason
    write(pkgfile,
          """
          module RejectReasons
          f() = 1
          end
          """)
    reasons = Dict{Symbol,Int}()
    @test Base.stale_cachefile(pkgfile, cachefile; reasons) === true
    @test length(reasons) == 1
    @test only(keys(reasons)) in (:mtime_changed, :fsize_changed, :content_changed)
    msg = Base.list_reasons(reasons)
    @test startswith(msg, " (cache not reused: ")

    # actionable reasons are reported over rejections of other-version caches
    Base.record_reason(reasons, :incompatible_header)
    @test Base.list_reasons(reasons) == msg

    # rejections of caches that weren't the ones searched for are never reported
    @test Base.list_reasons(Dict(:buildid_mismatch => 2)) == ""
    @test Base.list_reasons(nothing) == ""

    # the full counted list, including internal reasons, is available at debug level
    @test_logs (:debug, r"Caches not reused: 2 for different build identifier") min_level=Logging.Debug match_mode=:any Base.list_reasons(Dict(:buildid_mismatch => 2))
end

# `include(mapexpr, …)` records its non-identity `mapexpr` into a per-root-module side-table
# (`Base.include_mapexprs`) that is serialized into the package image, so the exact transform
# used at precompile time is recoverable after load (used by revision tools such as Revise).
precompile_test_harness("include mapexpr persistence") do dir
    Pkg = :IncludeMapexpr9f2c
    write(joinpath(dir, "plain.jl"), "p = 1\n")
    write(joinpath(dir, "named.jl"), "n = 2\n")
    write(joinpath(dir, "closure.jl"), "c = 2\n")
    write(joinpath(dir, "submod.jl"), "s = 2\n")
    write(joinpath(dir, "$Pkg.jl"),
          """
          module $Pkg
              # named-function transform: bump an integer rhs by 40
              bump40(ex) = (Meta.isexpr(ex, :(=)) && ex.args[2] isa Int && (ex.args[2] = ex.args[2] + 40); ex)
              # closure capturing state computed at load time -- the case that could NOT be
              # reconstructed by re-parsing, only by serializing the actual object
              const OFFSET = 40
              addoffset = let off = OFFSET
                  ex -> (Meta.isexpr(ex, :(=)) && ex.args[2] isa Int && (ex.args[2] = ex.args[2] + off); ex)
              end

              include("plain.jl")               # identity: must NOT be recorded
              include(bump40, "named.jl")       # include(mapexpr, path)
              include(addoffset, "closure.jl")  # state-capturing closure
              module Sub end
              Base.include(bump40, Sub, "submod.jl")  # include(mapexpr, mod, path)
          end
          """)
    @test Base.compilecache(Base.PkgId(string(Pkg))) isa Tuple
    @eval using $Pkg
    M = @eval $Pkg

    # The transforms actually executed (load-time behavior), via the package's own includes
    @test M.p == 1                # untransformed
    @test M.n == 42               # bump40
    @test M.c == 42               # addoffset (closure)
    @test M.Sub.s == 42           # include(mapexpr, mod, path)

    # A package with only identity includes records nothing (the common, zero-allocation case).
    NoMx = :IncludeMapexprNone9f2c
    write(joinpath(dir, "$NoMx.jl"), "module $NoMx\n    include(\"plain.jl\")\nend\n")
    @test Base.compilecache(Base.PkgId(string(NoMx))) isa Tuple
    @eval using $NoMx
    @test Base.include_mapexprs(@eval $NoMx) === nothing

    # The side-table survived precompilation and is keyed by (including_module, abspath)
    mapexprs = Base.include_mapexprs(M)
    @test mapexprs isa Dict{Tuple{Module,String},Any}
    plainpath   = normpath(joinpath(dir, "plain.jl"))
    namedpath   = normpath(joinpath(dir, "named.jl"))
    closurepath = normpath(joinpath(dir, "closure.jl"))
    submodpath  = normpath(joinpath(dir, "submod.jl"))
    @test !haskey(mapexprs, (M, plainpath))           # identity is never recorded
    @test haskey(mapexprs, (M, namedpath))
    @test haskey(mapexprs, (M, closurepath))
    @test haskey(mapexprs, (M.Sub, submodpath))       # keyed by the including (sub)module

    # The recorded objects are the actual functions, captured state and all: applying the
    # deserialized closure reproduces the +OFFSET transform. `invokelatest` because the package
    # (hence these methods) was loaded in this same world.
    @test Base.invokelatest(mapexprs[(M, namedpath)], Expr(:(=), :z, 2)) == Expr(:(=), :z, 42)
    @test Base.invokelatest(mapexprs[(M, closurepath)], Expr(:(=), :z, 2)) == Expr(:(=), :z, 42)

    # Exactly the three non-identity includes were recorded (the identity one was not).
    @test length(mapexprs) == 3
end

precompile_test_harness("cancellation source relinking") do dir
    write(joinpath(dir, "CancelRelink.jl"),
          """
          module CancelRelink
              using Base: CancellationToken, CancellationTokenSource
              const ROOT = CancellationTokenSource()
              const LEFT = CancellationTokenSource(CancellationToken(ROOT))
              const RIGHT = CancellationTokenSource(CancellationToken(ROOT))
              const CHILD = CancellationTokenSource(CancellationToken(LEFT), CancellationToken(RIGHT))
          end
          """)
    Base.compilecache(Base.PkgId("CancelRelink"))
    @eval using CancelRelink
    invokelatest() do
        # the sources were serialized into the package image with their weak
        # child lists dropped; loading must have relinked them under their
        # parents so that cancellation still propagates through the diamond
        @test CancelRelink.CHILD.nparents == 2
        @test Base._cancel_parent(CancelRelink.CHILD, 1) === CancelRelink.LEFT
        @test !Base.iscancelled(CancelRelink.CHILD)
        Base.cancel!(CancelRelink.ROOT)
        @test Base.iscancelled(CancelRelink.LEFT)
        @test Base.iscancelled(CancelRelink.RIGHT)
        @test Base.iscancelled(CancelRelink.CHILD)
    end
end

precompile_test_harness("cancellation relink under cancelled external parent") do dir
    write(joinpath(dir, "CancelExtA.jl"),
          """
          module CancelExtA
              using Base: CancellationTokenSource
              const A_ROOT = CancellationTokenSource()
          end
          """)
    write(joinpath(dir, "CancelExtB.jl"),
          """
          module CancelExtB
              using CancelExtA
              using Base: CancellationToken, CancellationTokenSource
              const B_MID = CancellationTokenSource(CancellationToken(CancelExtA.A_ROOT))
              const B_CHILD = CancellationTokenSource(CancellationToken(B_MID))
              const B_GRAND = CancellationTokenSource(CancellationToken(B_CHILD))
              # a cancel! interrupted mid-walk (state raised, children not
              # visited) captured in the image: load-time propagation must
              # not treat the already-raised state as having been walked
              Base._raise_state!(B_CHILD, 0x01)
          end
          """)
    Base.compilecache(Base.PkgId("CancelExtA"))
    Base.compilecache(Base.PkgId("CancelExtB"))
    @eval using CancelExtA
    invokelatest() do
        Base.cancel!(CancelExtA.A_ROOT)
    end
    @eval using CancelExtB
    invokelatest() do
        # CancelExtB's sources re-attached at load time under the already-
        # cancelled A_ROOT: B_MID is born cancelled during its relink, and
        # that state must reach every descendant regardless of the order in
        # which the image's fixups relinked them - including through
        # B_CHILD, whose already-cancelled (but never walked) state must
        # not prune the propagation
        @test Base.iscancelled(CancelExtB.B_MID)
        @test Base.iscancelled(CancelExtB.B_CHILD)
        @test Base.iscancelled(CancelExtB.B_GRAND)
    end
end

finish_precompile_test!()
