From 27c408099f87512d8a47b92553b726535df0b540 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Fri, 21 Aug 2026 17:09:20 -0700 Subject: [PATCH 1/2] Seed Revise's cache source hash with a UInt, so 32 bit runs can revise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revise passes the precompile cache header's content hash to `hash` as a `UInt64`. The seed `hash` takes is a `UInt`, 32 bits wide on a 32 bit platform, so on every `x86` leg Revise's package callback dies with MethodError: no method matching hash(::UInt64, ::UInt64) at cache_src_id (packages/Revise/src/pkgs.jl:138) and every test item that loads a package in that process reports the failed callback as its own error — which is how it surfaced on JuliaMCP's `rc~x86` legs, in items that have nothing to do with Revise. The bug is upstream's and the vendored copy is already at 3.16.4, the latest tag, so re-vendoring is no help; `packages/` is not edited by hand. The method is replaced instead where `pkg_imports.jl` assembles the `Revise` module, right after the vendored `packagedef.jl` is included. Truncating with `% UInt` is enough: the value only identifies a source snapshot within one session, and on 64 bit it is the value Revise already computes. No unit test — `cache_src_id` lives in the module the test process assembles, and reaching it from this suite means loading the whole vendored stack for a one-line method. The `x86` legs are the check. Co-Authored-By: Claude Opus 5 --- testprocess/TestItemServer/src/pkg_imports.jl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/testprocess/TestItemServer/src/pkg_imports.jl b/testprocess/TestItemServer/src/pkg_imports.jl index d6f39be..23245aa 100644 --- a/testprocess/TestItemServer/src/pkg_imports.jl +++ b/testprocess/TestItemServer/src/pkg_imports.jl @@ -90,6 +90,21 @@ module Revise using ...LoweredCodeUtils: next_or_nothing!, callee_matches include("../../../packages/Revise/src/packagedef.jl") + + # Revise seeds the cache source hash with a `UInt64`, while the seed `hash` takes is a + # `UInt` — 32 bits wide on a 32 bit platform. Its package callback dies there with + # `MethodError: no method matching hash(::UInt64, ::UInt64)`, and every test item that + # loads a package in that process reports the failed callback as its own error. + # + # `packages/` holds git subtrees that are never edited by hand, so the method is + # replaced here instead, where this module is assembled. The value only has to identify + # a source snapshot within one session, so truncating is fine, and on 64 bit it is the + # same value Revise computes. Guarded like the definition it replaces + # (https://github.com/JuliaLang/julia/pull/49866); older versions hash `inc.mtime` and + # are unaffected. Drop this once the fix is in a released Revise. + @static if VERSION >= v"1.11.0-DEV.683" + cache_src_id(inc) = hash(inc.fsize, inc.hash % UInt) + end elseif VERSION >= v"1.6.0" using ..OrderedCollections using ..LoweredCodeUtils From 6321055f6afbd2f3bfba90da5d4b1699c9158f06 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Fri, 21 Aug 2026 19:49:59 -0700 Subject: [PATCH 2/2] Add the 32 bit cache_src_id method instead of overwriting Revise's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redefining `cache_src_id` after the vendored `packagedef.jl` is included has the signature the vendored definition already has, which makes it a method overwrite — and Julia rejects method overwriting outright while a package precompiles: WARNING: Method definition cache_src_id(Any) in module Revise at packages/Revise/src/pkgs.jl:138 overwritten at testprocess/TestItemServer/src/pkg_imports.jl:106. ERROR: Method overwriting is not permitted during Module precompilation. 24769.9 ms ? TestItemServer Precompilation is warn-only on the path a test process takes, so the package still loaded — at the cost of reloading the whole vendored stack from source in every test process, and of a precompile banner on its stderr that landed in whatever a test item captured. That is what reddened the 64 bit legs: the assertions that a process's output carries no ANSI escapes, the coverage items whose processes then went unanswered, and the recycling items that timed out on the extra startup. Add the method rather than replace it: both call sites pass a `Base.CacheHeaderIncludes`, so a method on that type is strictly more specific and wins dispatch without touching upstream's `cache_src_id(::Any)`. Guarded to 32 bit, where the bug is — on 64 bit the vendored definition already computes the right value, and leaving its dispatch and invalidation exactly as upstream ships them is one less thing to reason about. `inc.hash` is a `UInt32`, so `UInt(inc.hash)` is an exact widening on either word size; `isdefined` covers the internal Base type. The suite had nothing asserting that TestItemServer's cache is actually built — this only surfaced through unrelated assertions about process output — so add a test item that precompiles the version-matched test process environment and fails on a `?`. `Base.compilecache` rather than `Pkg.precompile`, because it rebuilds unconditionally and reports failure as a return value instead of exiting 0. Co-Authored-By: Claude Opus 5 --- test/test_testserver_precompile.jl | 42 +++++++++++++++++++ testprocess/TestItemServer/src/pkg_imports.jl | 15 ++++--- 2 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 test/test_testserver_precompile.jl diff --git a/test/test_testserver_precompile.jl b/test/test_testserver_precompile.jl new file mode 100644 index 0000000..9bd350d --- /dev/null +++ b/test/test_testserver_precompile.jl @@ -0,0 +1,42 @@ +@testitem "The test process environment precompiles cleanly" begin + # `TestItemServer` assembles the vendored packages by hand, and an override that + # *redefines* one of their methods rather than adding a more specific one is a method + # overwrite — which Julia rejects outright while a package precompiles. + # + # Nothing else in this suite notices directly: precompilation is warn-only on the path a + # test process takes, so the package still loads. What it costs is a full source reload + # of the vendored stack in every test process (tens of seconds each) and a precompile + # banner on its stderr, which then lands in whatever a test item captures. Those + # second-order effects are how it last surfaced, in assertions about process output. + # Assert the cache is actually built instead. + env_dir = normpath(joinpath(@__DIR__, "..", "testprocess", "environments")) + versioned = joinpath(env_dir, "v$(VERSION.major).$(VERSION.minor)") + project = isdir(versioned) ? versioned : joinpath(env_dir, "fallback") + + # `Base.compilecache` rather than `Pkg.precompile`: it rebuilds unconditionally, so a + # cache some other test item already wrote cannot short-circuit the check, and it reports + # a failure as a `Core.PrecompilableError` return value — `Pkg.precompile` reports one as + # a `?` in its progress list and still exits 0. + code = """ + result = Base.compilecache(Base.identify_package("TestItemServer")) + result isa Tuple || exit(1) + """ + julia = joinpath(Sys.BINDIR, Base.julia_exename()) + cmd = `$julia --startup-file=no --history-file=no --project=$project -e $code` + + io = IOBuffer() + process = run(pipeline(ignorestatus(cmd), stdout=io, stderr=io)) + output = String(take!(io)) + + ok = success(process) && + !occursin("Method overwriting is not permitted", output) && + !occursin("overwritten at", output) + if !ok + println("── precompiling TestItemServer in $project ──") + println(output) + end + + @test success(process) + @test !occursin("Method overwriting is not permitted", output) + @test !occursin("overwritten at", output) +end diff --git a/testprocess/TestItemServer/src/pkg_imports.jl b/testprocess/TestItemServer/src/pkg_imports.jl index 23245aa..3cd473d 100644 --- a/testprocess/TestItemServer/src/pkg_imports.jl +++ b/testprocess/TestItemServer/src/pkg_imports.jl @@ -96,14 +96,17 @@ module Revise # `MethodError: no method matching hash(::UInt64, ::UInt64)`, and every test item that # loads a package in that process reports the failed callback as its own error. # - # `packages/` holds git subtrees that are never edited by hand, so the method is - # replaced here instead, where this module is assembled. The value only has to identify - # a source snapshot within one session, so truncating is fine, and on 64 bit it is the - # same value Revise computes. Guarded like the definition it replaces + # `packages/` holds git subtrees that are never edited by hand, so the method is added + # here instead, where this module is assembled. It is deliberately *more specific* than + # the vendored `cache_src_id(inc)` rather than a redefinition of it: redefining is + # method overwriting, which Julia rejects outright while this package precompiles. Both + # call sites pass a `Base.CacheHeaderIncludes`, so this one wins dispatch. `inc.hash` is + # a `UInt32`, so widening it to `UInt` is exact. Guarded like the definition it shadows # (https://github.com/JuliaLang/julia/pull/49866); older versions hash `inc.mtime` and # are unaffected. Drop this once the fix is in a released Revise. - @static if VERSION >= v"1.11.0-DEV.683" - cache_src_id(inc) = hash(inc.fsize, inc.hash % UInt) + @static if Sys.WORD_SIZE == 32 && VERSION >= v"1.11.0-DEV.683" && + isdefined(Base, :CacheHeaderIncludes) + cache_src_id(inc::Base.CacheHeaderIncludes) = hash(inc.fsize, UInt(inc.hash)) end elseif VERSION >= v"1.6.0" using ..OrderedCollections