Skip to content

Precompile the test environment from a base-target child under a portable JULIA_CPU_TARGET - #108

Merged
davidanthoff merged 1 commit into
mainfrom
portable-cpu-target-precompile
Sep 13, 2026
Merged

Precompile the test environment from a base-target child under a portable JULIA_CPU_TARGET#108
davidanthoff merged 1 commit into
mainfrom
portable-cpu-target-precompile

Conversation

@davidanthoff

Copy link
Copy Markdown
Member

The failure

Every Julia 1.13 leg of JuliaWorkspaces.jl #322 and LanguageServer.jl #1474 went red once julia-run-testitems@v2 started giving the test processes a portable JULIA_CPU_TARGET (generic;sandybridge,-xsaveopt,clone_all;haswell,-rdrnd,base(1) on x86_64) so that the cached per-leg depot survives moving between runner CPUs:

ERROR: LoadError: Precompiled image Base.PkgId(..., "JuliaSyntax") not available with flags CacheFlags(...)
Failed to precompile JuliaWorkspaces [...]

and, on the 32-bit legs, a cascade of UndefVarError: URIs2 not defined in JuliaWorkspaces after require fell back to loading the package from source and left the half-evaluated module registered.

Why

JULIA_CPU_TARGET never changes the process it is set in. Julia reads it in two places only: when it spawns a precompilation worker, which gets it as -C (Base.create_expr_cache), and when it names a cache file. The test process keeps running on native and judges cache freshness against that (checked: with the variable set, Base.current_image_targets() still names the host CPU while the worker command carries -C 'generic;...'). So it accepted the native images the restored depot held, compiled only the stale package under test in a worker, and that worker — on base target generic — refused the very same native images. Julia ≤ 1.12 workers recompiled the dependency nested (that is why activation took minutes on those legs); 1.13 launches workers with --compiled-modules=strict (base/precompilation.jl:1186), so activation "succeeds" and every using fails.

The change

Under such a target, activate_env_request now runs Pkg.precompile() in a child Julia started with -C <base variant> (-C generic for the target above) instead of in-process. The child rejects native images the way the workers do and rebuilds them under the portable target once; afterwards the native test process and any worker it spawns agree on every image in the depot, whatever the depot held before and whoever else wrote to it.

  • Child = Base.julia_cmd(; cpu_target), which replicates the cache-affecting flags (--check-bounds, -O, -g, --inline, --pkgimages, --compiled-modules, coverage), plus the expanded LOAD_PATH as JULIA_LOAD_PATH — what create_expr_cache gives Pkg's own workers — with stdout/stderr inherited so its output stays process-level activation output. The request's cancellation token is polled and kills the child.
  • Sandbox branch: TestEnv.activate runs with JULIA_PKG_PRECOMPILE_AUTO=0 (Pkg's should_autoprecompile() honours it), then the child. Workspace-member branch: the child instead of Pkg.precompile().
  • Only on Julia 1.10+ (package images are checked against the CPU from there) with compiled modules and package images on. Without the variable, or with native, nothing changes.
  • testprocess/TestItemServer/src/cpu_target_precompile.jl holds the target parser, the predicate and the child runner (Base only, so the controller suite includes it directly); docs paragraph in internals.md; CHANGELOG entry.

Tests

test/test_cpu_target.jl: unit items for the parser, predicate and command/env shape, and an end-to-end item that is the CI failure in miniature — Portable depends on path-tracked Leaf, a private depot gets native images, Portable is made stale, and the run repeats under the host's portable target. Verified locally on 1.13.0: with TestItemServer.jl/precompile.jl stashed the item fails with exactly the CI error (Precompiled image ... Leaf not available with flags, Failed to precompile Portable); with the change all four items pass, and so do test_testserver_precompile, test_scratch_env, test_coverage, test_broken_env, test_run_passing, test_no_package, test_process_reuse (34 items).

Expect this PR's own 1.13 CI legs to be red

The CI test processes run the released TestItemServer from the toolkit (TestItemControllers 1.11.0), not this branch, so they hit the very bug this fixes. They go green once this is released and julia-run-testitems picks the release up (follow-up PR there).

Version

Project.toml stays at 1.11.2-DEV; a 1.11.2 release is what the action's follow-up needs (TestItemRuns' compat is "1.11", so 1.12.0 would force a TestItemRuns release too).

🤖 Generated with Claude Code

…able JULIA_CPU_TARGET

CI gives the test processes a portable, multi-versioned `JULIA_CPU_TARGET`
(`generic;sandybridge,-xsaveopt,clone_all;haswell,-rdrnd,base(1)` on x86_64) so
that a cached depot survives moving between runner CPUs. That variable never
changes the process it is set in: Julia reads it only when it spawns a
precompilation worker, which gets it as `-C`, and when it names a cache file.
The test process itself keeps running on `native` and judges cache freshness
against that. So it accepted whatever native images the restored depot already
held, compiled only the stale package under test in a worker, and that worker --
on base target `generic` -- refused the very same native images. Julia 1.12 and
older recompiled the dependency nested inside the worker, which is why
activation took minutes; Julia 1.13 launches its workers with
`--compiled-modules=strict` (base/precompilation.jl:1186), so activation
"succeeded" and every `using` in the run failed with "Precompiled image ... not
available with flags ...". Every 1.13 leg of JuliaWorkspaces.jl #322 and
LanguageServer.jl #1474 went down that way, and the 32-bit legs added a cascade
of `UndefVarError: X not defined in JuliaWorkspaces` on top, because `require`
had fallen back to loading the package from source and left the half-evaluated
module registered.

Under such a target, environment activation now runs `Pkg.precompile()` in a
child Julia started with `-C <base variant>` (`-C generic` for the target above)
instead of in-process. The child rejects native images the way the workers do
and rebuilds them under the portable target once; the native test process and
any worker it later spawns then agree on every image in the depot. The child is
`Base.julia_cmd(; cpu_target)`, which replicates the cache-affecting flags
(`--check-bounds`, `-O`, `-g`, `--inline`, `--pkgimages`, `--compiled-modules`,
coverage), plus the expanded `LOAD_PATH` as `JULIA_LOAD_PATH` -- what
`create_expr_cache` gives Pkg's own workers -- with stdout and stderr inherited,
so its output is process-level activation output like before. In the sandbox
branch `TestEnv.activate` runs with `JULIA_PKG_PRECOMPILE_AUTO=0` so its
in-process `Pkg._auto_precompile` is skipped; the workspace-member branch calls
the child instead of `Pkg.precompile()`. The request's cancellation token is
polled and kills the child. Only on Julia 1.10+ (package images are checked
against the CPU from there), with compiled modules and package images on --
everything else is byte-for-byte what it was, and without the variable nothing
changes at all.

The new end-to-end item is the CI failure in miniature: `Portable` depends on
path-tracked `Leaf`, a private depot gets native images, `Portable` is made
stale, and the run repeats under the portable target. Without this change it
fails on 1.13 with exactly the CI error; with it, it passes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@davidanthoff
davidanthoff merged commit 9c6f7a9 into main Sep 13, 2026
10 of 18 checks passed
@davidanthoff
davidanthoff deleted the portable-cpu-target-precompile branch September 13, 2026 20:37
davidanthoff added a commit to julia-actions/julia-run-testitems that referenced this pull request Sep 13, 2026
…depot themselves

#12 gave the test processes a portable `JULIA_CPU_TARGET` and broke every Julia
1.13 leg of JuliaWorkspaces.jl #322 and LanguageServer.jl #1474. The variable
never changes the process it is set in -- Julia reads it only when spawning
precompilation workers -- so a test process accepted the native images the
restored depot held from earlier runs, compiled only the stale package under
test, and handed it to a worker that refused those same images; 1.13's workers
run with `--compiled-modules=strict`, so that was a failed run rather than the
nested recompilation older versions did.

TestItemControllers 1.11.2 (julia-testitems/TestItemControllers.jl#108) makes
environment activation precompile from a child Julia on the target's base CPU
whenever the target is portable, which rebuilds such images once and makes the
test processes robust to whatever the depot holds. This bumps the manifest to it
(the only change `Pkg.update` made) and says in the input description, the
toolkit step and the README why setting a target here is safe again -- and that
`julia-buildpkg` is not needed alongside this action, while
`julia-actions/cache` still is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant