Skip to content

Commit 4a4c4a9

Browse files
committed
precompile: fail in (closer to) linear time
Do not allow serial compile fallbacks, since this causes performance and output degredation when serial precompile fails. The precompilepkg driver already ensured DAG ordering, so this work is not necessary or useful to repeat. A bit of code cleanup and asking for Claude to describe the argument list as well, plus a test to start to cover any of this code functionality in CI.
1 parent 0cad721 commit 4a4c4a9

File tree

3 files changed

+240
-12
lines changed

3 files changed

+240
-12
lines changed

base/precompilation.jl

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,73 @@ function collect_all_deps(direct_deps, dep, alldeps=Set{Base.PkgId}())
471471
end
472472

473473

474+
"""
475+
precompilepkgs(pkgs; kwargs...)
476+
477+
Precompile packages and their dependencies, with support for parallel compilation,
478+
progress tracking, and various compilation configurations.
479+
480+
`pkgs::Union{Vector{String}, Vector{PkgId}}`: Packages to precompile. When
481+
empty (default), precompiles all project dependencies. When specified,
482+
precompiles only the given packages and their dependencies (unless
483+
`manifest=true`).
484+
485+
# Keyword Arguments
486+
- `internal_call::Bool`: Indicates this is an automatic/internal precompilation call
487+
(e.g., triggered by package loading). When `true`, errors are handled gracefully: in
488+
interactive sessions, errors are stored in `Base.MainInclude.err` instead of throwing;
489+
in non-interactive sessions, errors are printed but not thrown. Default: `false`.
490+
491+
- `strict::Bool`: Controls error reporting scope. When `false` (default), only reports
492+
errors for direct project dependencies.
493+
494+
- `warn_loaded::Bool`: When `true` (default), checks for and warns about packages that are
495+
precompiled but already loaded with a different version. Displays a warning that Julia
496+
needs to be restarted to use the newly precompiled versions.
497+
498+
- `timing::Bool`: When `true` (not default), displays timing information for
499+
each package compilation, but only if compilation might have succeeded.
500+
Disables fancy progress bar output (timing is shown in simple text mode).
501+
502+
- `_from_loading::Bool`: Internal flag indicating the call originated from the
503+
package loading system. When `true` (not default): returns early instead of
504+
throwing when packages are not found; suppresses progress messages when not
505+
in an interactive session; allows packages outside the current environment to
506+
be added as serial precompilation jobs; skips LOADING_CACHE initialization;
507+
and changes cachefile locking behavior.
508+
509+
- `configs::Union{Config,Vector{Config}}`: Compilation configurations to use. Each Config
510+
is a `Pair{Cmd, Base.CacheFlags}` specifying command flags and cache flags. When
511+
multiple configs are provided, each package is precompiled for each configuration.
512+
513+
- `io::IO`: Output stream for progress messages, warnings, and errors. Can be
514+
redirected (e.g., to `devnull` when called from loading in non-interactive mode).
515+
516+
- `fancyprint::Bool`: Controls output format. When `true`, displays an animated progress
517+
bar with spinners. When `false`, instead enables `timing` mode. Automatically
518+
disabled when `timing=true` or when called from loading in non-interactive mode.
519+
520+
- `manifest::Bool`: Controls the scope of packages to precompile. When `false` (default),
521+
precompiles only packages specified in `pkgs` and their dependencies. When `true`,
522+
precompiles all packages in the manifest (workspace mode), typically used by Pkg for
523+
workspace precompile requests.
524+
525+
- `ignore_loaded::Bool`: Controls whether already-loaded packages affect cache
526+
freshness checks. When `false` (not default), loaded package versions are considered when
527+
determining if cache files are fresh.
528+
529+
# Return
530+
- `Vector{String}`: Paths to cache files for the requested packages.
531+
- `Nothing`: precompilation should be skipped
532+
533+
# Notes
534+
- Packages in circular dependency cycles are skipped with a warning.
535+
- Packages with `__precompile__(false)` are skipped if they are from loading to
536+
avoid repeated work on every session.
537+
- Parallel compilation is controlled by `JULIA_NUM_PRECOMPILE_TASKS` environment variable
538+
(defaults to CPU_THREADS + 1, capped at 16, halved on Windows).
539+
- Extensions are precompiled when all their triggers are available in the environment.
540+
"""
474541
function precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}=String[];
475542
internal_call::Bool=false,
476543
strict::Bool = false,
@@ -745,8 +812,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}},
745812
pkg_names = [pkg.name for pkg in project_deps]
746813
end
747814
keep = Set{Base.PkgId}()
748-
for dep in direct_deps
749-
dep_pkgid = first(dep)
815+
for dep_pkgid in keys(direct_deps)
750816
if dep_pkgid.name in pkg_names
751817
push!(keep, dep_pkgid)
752818
collect_all_deps(direct_deps, dep_pkgid, keep)
@@ -990,8 +1056,10 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}},
9901056
notify(was_processed[pkg_config])
9911057
continue
9921058
end
993-
# Heuristic for when precompilation is disabled
994-
if occursin(r"\b__precompile__\(\s*false\s*\)", read(sourcepath, String))
1059+
# Heuristic for when precompilation is disabled, which must not over-estimate however for any dependent
1060+
# since it will also block precompilation of all dependents
1061+
if _from_loading && single_requested_pkg && occursin(r"\b__precompile__\(\s*false\s*\)", read(sourcepath, String))
1062+
Base.@logmsg logcalls "Disabled precompiling $(repr("text/plain", pkg)) since the text `__precompile__(false)` was found in file."
9951063
notify(was_processed[pkg_config])
9961064
continue
9971065
end
@@ -1035,7 +1103,13 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}},
10351103
end
10361104
# for extensions, any extension in our direct dependencies is one we have a right to load
10371105
# for packages, we may load any extension (all possible triggers are accounted for above)
1038-
loadable_exts = haskey(ext_to_parent, pkg) ? filter((dep)->haskey(ext_to_parent, dep), direct_deps[pkg]) : nothing
1106+
loadable_exts = haskey(ext_to_parent, pkg) ? filter((dep)->haskey(ext_to_parent, dep), deps) : nothing
1107+
if !isempty(deps)
1108+
# if deps is empty, either it doesn't have any (so compiled-modules is
1109+
# irrelevant) or we couldn't compute them (so we actually should attempt
1110+
# serial compile, as the dependencies are not in the parallel list)
1111+
flags = `$flags --compiled-modules=strict`
1112+
end
10391113
if _from_loading && pkg in requested_pkgids
10401114
# loading already took the cachefile_lock and printed logmsg for its explicit requests
10411115
t = @elapsed ret = begin
@@ -1223,18 +1297,18 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}},
12231297
pluralde = n_direct_errs == 1 ? "y" : "ies"
12241298
direct = strict ? "" : "direct "
12251299
err_msg = "The following $n_direct_errs $(direct)dependenc$(pluralde) failed to precompile:\n$(String(take!(err_str)))"
1226-
if internal_call # aka. auto-precompilation
1227-
if isinteractive()
1300+
if internal_call # aka. decide which untested code path to run that does some unsafe behavior
1301+
if isinteractive() # XXX: this test is incorrect
12281302
plural1 = length(failed_deps) == 1 ? "y" : "ies"
12291303
println(io, " ", color_string("$(length(failed_deps))", Base.error_color()), " dependenc$(plural1) errored.")
12301304
println(io, " For a report of the errors see `julia> err`. To retry use `pkg> precompile`")
1231-
setglobal!(Base.MainInclude, :err, PkgPrecompileError(err_msg))
1305+
setglobal!(Base.MainInclude, :err, PkgPrecompileError(err_msg)) # XXX: this call is dangerous
12321306
else
12331307
# auto-precompilation shouldn't throw but if the user can't easily access the
12341308
# error messages, just show them
12351309
print(io, "\n", err_msg)
12361310
end
1237-
else
1311+
else # XXX: crashing is wrong
12381312
println(io)
12391313
throw(PkgPrecompileError(err_msg))
12401314
end

test/loading.jl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,10 +1376,8 @@ end
13761376
""")
13771377
write(joinpath(foo_path, "Manifest.toml"),
13781378
"""
1379-
# This file is machine-generated - editing it directly is not advised
1380-
julia_version = "1.13.0-DEV"
1379+
julia_version = "1.13.0"
13811380
manifest_format = "2.0"
1382-
project_hash = "8699765aeeac181c3e5ddbaeb9371968e1f84d6b"
13831381
13841382
[[deps.Foo51989]]
13851383
path = "."

test/precompile.jl

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2544,4 +2544,160 @@ let io = IOBuffer()
25442544
@test isempty(String(take!(io)))
25452545
end
25462546

2547+
# Test --compiled-modules=strict in precompilepkgs
2548+
@testset "compiled-modules=strict with dependencies" begin
2549+
mkdepottempdir() do depot
2550+
# Create three packages: one that fails to precompile, one that loads it, one that doesn't
2551+
project_path = joinpath(depot, "testenv")
2552+
mkpath(project_path)
2553+
2554+
# Create FailPkg - a package that can't be precompiled
2555+
fail_pkg_path = joinpath(depot, "dev", "FailPkg")
2556+
mkpath(joinpath(fail_pkg_path, "src"))
2557+
write(joinpath(fail_pkg_path, "Project.toml"),
2558+
"""
2559+
name = "FailPkg"
2560+
uuid = "10000000-0000-0000-0000-000000000001"
2561+
version = "0.1.0"
2562+
""")
2563+
write(joinpath(fail_pkg_path, "src", "FailPkg.jl"),
2564+
"""
2565+
module FailPkg
2566+
print("FailPkg precompiling.\n")
2567+
error("fail")
2568+
end
2569+
""")
2570+
2571+
# Create LoadsFailPkg - depends on and loads FailPkg (should fail with strict)
2572+
loads_pkg_path = joinpath(depot, "dev", "LoadsFailPkg")
2573+
mkpath(joinpath(loads_pkg_path, "src"))
2574+
write(joinpath(loads_pkg_path, "Project.toml"),
2575+
"""
2576+
name = "LoadsFailPkg"
2577+
uuid = "20000000-0000-0000-0000-000000000002"
2578+
version = "0.1.0"
2579+
2580+
[deps]
2581+
FailPkg = "10000000-0000-0000-0000-000000000001"
2582+
""")
2583+
write(joinpath(loads_pkg_path, "src", "LoadsFailPkg.jl"),
2584+
"""
2585+
module LoadsFailPkg
2586+
print("LoadsFailPkg precompiling.\n")
2587+
import FailPkg
2588+
print("unreachable\n")
2589+
end
2590+
""")
2591+
2592+
# Create DependsOnly - depends on FailPkg but doesn't load it (should succeed)
2593+
depends_pkg_path = joinpath(depot, "dev", "DependsOnly")
2594+
mkpath(joinpath(depends_pkg_path, "src"))
2595+
write(joinpath(depends_pkg_path, "Project.toml"),
2596+
"""
2597+
name = "DependsOnly"
2598+
uuid = "30000000-0000-0000-0000-000000000003"
2599+
version = "0.1.0"
2600+
2601+
[deps]
2602+
FailPkg = "10000000-0000-0000-0000-000000000001"
2603+
""")
2604+
write(joinpath(depends_pkg_path, "src", "DependsOnly.jl"),
2605+
"""
2606+
module DependsOnly
2607+
# Has FailPkg as a dependency but doesn't load it
2608+
print("DependsOnly precompiling.\n")
2609+
end
2610+
""")
2611+
2612+
# Create main project with all packages
2613+
write(joinpath(project_path, "Project.toml"),
2614+
"""
2615+
[deps]
2616+
LoadsFailPkg = "20000000-0000-0000-0000-000000000002"
2617+
DependsOnly = "30000000-0000-0000-0000-000000000003"
2618+
""")
2619+
write(joinpath(project_path, "Manifest.toml"),
2620+
"""
2621+
julia_version = "1.13.0"
2622+
manifest_format = "2.0"
2623+
2624+
[[DependsOnly]]
2625+
deps = ["FailPkg"]
2626+
uuid = "30000000-0000-0000-0000-000000000003"
2627+
version = "0.1.0"
2628+
2629+
[[FailPkg]]
2630+
uuid = "10000000-0000-0000-0000-000000000001"
2631+
version = "0.1.0"
2632+
2633+
[[LoadsFailPkg]]
2634+
deps = ["FailPkg"]
2635+
uuid = "20000000-0000-0000-0000-000000000002"
2636+
version = "0.1.0"
2637+
2638+
[[deps.DependsOnly]]
2639+
deps = ["FailPkg"]
2640+
path = "../dev/DependsOnly/"
2641+
uuid = "30000000-0000-0000-0000-000000000003"
2642+
version = "0.1.0"
2643+
2644+
[[deps.FailPkg]]
2645+
path = "../dev/FailPkg/"
2646+
uuid = "10000000-0000-0000-0000-000000000001"
2647+
version = "0.1.0"
2648+
2649+
[[deps.LoadsFailPkg]]
2650+
deps = ["FailPkg"]
2651+
path = "../dev/LoadsFailPkg/"
2652+
uuid = "20000000-0000-0000-0000-000000000002"
2653+
version = "0.1.0"
2654+
""")
2655+
2656+
# Call precompilepkgs with output redirected to a file
2657+
LoadsFailPkg_output = joinpath(depot, "LoadsFailPkg_output.txt")
2658+
DependsOnly_output = joinpath(depot, "DependsOnly_output.txt")
2659+
original_depot_path = copy(Base.DEPOT_PATH)
2660+
old_proj = Base.active_project()
2661+
try
2662+
push!(empty!(DEPOT_PATH), depot)
2663+
Base.set_active_project(project_path)
2664+
loadsfailpkg = open(LoadsFailPkg_output, "w") do io
2665+
# set internal_call to bypass buggy code
2666+
Base.Precompilation.precompilepkgs(["LoadsFailPkg"]; io, fancyprint=true, internal_call=true)
2667+
end
2668+
@test isempty(loadsfailpkg::Vector{String})
2669+
dependsonly = open(DependsOnly_output, "w") do io
2670+
# set internal_call to bypass buggy code
2671+
Base.Precompilation.precompilepkgs(["DependsOnly"]; io, fancyprint=true, internal_call=true)
2672+
end
2673+
@test length(dependsonly::Vector{String}) == 1
2674+
finally
2675+
Base.set_active_project(old_proj)
2676+
append!(empty!(DEPOT_PATH), original_depot_path)
2677+
end
2678+
2679+
output = read(LoadsFailPkg_output, String)
2680+
# LoadsFailPkg should fail because it tries to load FailPkg with --compiled-modules=strict
2681+
@test_broken count(output, "ERROR: fail") > 0
2682+
@test_broken count(output, "ERROR: fail") == 1
2683+
@test count("✗ FailPkg", output) > 0
2684+
@test count("✗ LoadsFailPkg", output) > 0
2685+
@test count("FailPkg precompiling.", output) > 0
2686+
@test_broken count("FailPkg precompiling.", output) == 1
2687+
@test 0 < count("LoadsFailPkg precompiling.", output) <= 2
2688+
@test_broken count("LoadsFailPkg precompiling.", output) == 1
2689+
@test !contains(output, "DependsOnly precompiling.")
2690+
2691+
# DependsOnly should succeed because it doesn't actually load FailPkg
2692+
output = read(DependsOnly_output, String)
2693+
@test_broken count(output, "ERROR: fail") > 0
2694+
@test_broken count(output, "ERROR: fail") == 1
2695+
@test count("✗ FailPkg", output) > 0
2696+
@test count("Precompiling DependsOnly finished.", output) == 1
2697+
@test_broken count("FailPkg precompiling.", output) > 0
2698+
@test_broken count("FailPkg precompiling.", output) == 1
2699+
@test count("DependsOnly precompiling.", output) == 1
2700+
end
2701+
end
2702+
25472703
finish_precompile_test!()

0 commit comments

Comments
 (0)