diff --git a/CHANGELOG.md b/CHANGELOG.md index f0cc2664fc..eada20d58e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ Pkg v1.13 Release Notes - `update` now shows a helpful tip when trying to upgrade a specific package that can be upgraded but is held back because it's part of a less optimal resolver solution ([#4266]) - `Pkg.status` now displays yanked packages with a `[yanked]` indicator and shows a warning when yanked packages are present. `Pkg.resolve` errors also display warnings about yanked packages that are not resolvable. ([#4310]) - Added `pkg> compat --current` command to automatically populate missing compat entries with the currently resolved package versions. Use `pkg> compat --current` for all packages or `pkg> compat Foo --current` for specific packages. ([#3266]) +- Added `Pkg.precompile() do` block syntax to delay autoprecompilation until after multiple operations complete, improving efficiency when performing several environment changes. ([#4262]) +- Added `Pkg.autoprecompilation_enabled(state::Bool)` to globally enable or disable automatic precompilation for Pkg operations. ([#4262]) Pkg v1.12 Release Notes ======================= diff --git a/docs/src/api.md b/docs/src/api.md index afe34d8635..d87169077f 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -39,6 +39,7 @@ Pkg.gc Pkg.status Pkg.compat Pkg.precompile +Pkg.autoprecompilation_enabled Pkg.offline Pkg.why Pkg.dependencies diff --git a/docs/src/environments.md b/docs/src/environments.md index d22ef8e58b..44497edbac 100644 --- a/docs/src/environments.md +++ b/docs/src/environments.md @@ -190,16 +190,53 @@ If a given package version errors during auto-precompilation, Pkg will remember automatically tries and will skip that package with a brief warning. Manual precompilation can be used to force these packages to be retried, as `pkg> precompile` will always retry all packages. -To disable the auto-precompilation, set `ENV["JULIA_PKG_PRECOMPILE_AUTO"]=0`. - The indicators next to the package names displayed during precompilation -indicate the status of that package's precompilation. +indicate the status of that package's precompilation. - `[◐, ◓, ◑, ◒]` Animated "clock" characters indicate that the package is currently being precompiled. - `✓` A green checkmark indicates that the package has been successfully precompiled (after which that package will disappear from the list). If the checkmark is yellow it means that the package is currently loaded so the session will need to be restarted to access the version that was just precompiled. - `?` A question mark character indicates that a `PrecompilableError` was thrown, indicating that precompilation was disallowed, i.e. `__precompile__(false)` in that package. - `✗` A cross indicates that the package failed to precompile. +#### Controlling Auto-precompilation + +Auto-precompilation can be controlled in several ways: + +- **Environment variable**: Set `ENV["JULIA_PKG_PRECOMPILE_AUTO"]=0` to disable auto-precompilation globally. +- **Programmatically**: Use `Pkg.autoprecompilation_enabled(false)` to disable auto-precompilation for the current session, or `Pkg.autoprecompilation_enabled(true)` to re-enable it. +- **Scoped control**: Use `Pkg.precompile(f, args...; kwargs...)` to execute a function `f` with auto-precompilation temporarily disabled, then automatically trigger precompilation afterward if any packages were modified during the execution. + +!!! compat "Julia 1.13" + The `Pkg.autoprecompilation_enabled()` function and `Pkg.precompile()` do-block syntax require at least Julia 1.13. + +For example, to add multiple packages without triggering precompilation after each one: + +```julia-repl +julia> Pkg.precompile() do + Pkg.add("Example") + Pkg.dev("JSON") + Pkg.update("HTTP") + end + Resolving package versions... + ... +Precompiling environment... + 14 dependencies successfully precompiled in 25 seconds +``` + +Or to temporarily disable auto-precompilation: + +```julia-repl +julia> Pkg.autoprecompilation_enabled(false) +false + +julia> Pkg.add("Example") # No precompilation happens + Resolving package versions... + ... + +julia> Pkg.autoprecompilation_enabled(true) +true +``` + ### Precompiling new versions of loaded packages If a package that has been updated is already loaded in the session, the precompilation process will go ahead and precompile diff --git a/src/API.jl b/src/API.jl index da2cb0a23a..bc913abad0 100644 --- a/src/API.jl +++ b/src/API.jl @@ -12,7 +12,7 @@ import FileWatching import Base: StaleCacheKey -import ..depots, ..depots1, ..logdir, ..devdir, ..printpkgstyle +import ..depots, ..depots1, ..logdir, ..devdir, ..printpkgstyle, .._autoprecompilation_enabled_scoped import ..Operations, ..GitTools, ..Pkg, ..Registry import ..can_fancyprint, ..pathrepr, ..isurl, ..PREV_ENV_PATH, ..atomic_toml_write using ..Types, ..TOML @@ -1185,6 +1185,13 @@ function precompile(ctx::Context, pkgs::Vector{PackageSpec}; internal_call::Bool end end +function precompile(f, args...; kwargs...) + Base.ScopedValues.@with _autoprecompilation_enabled_scoped => false begin + f() + Pkg.precompile(args...; kwargs...) + end +end + function tree_hash(repo::LibGit2.GitRepo, tree_hash::String) try return LibGit2.GitObject(repo, tree_hash) diff --git a/src/Pkg.jl b/src/Pkg.jl index 8ba2f4e107..2b0085c00b 100644 --- a/src/Pkg.jl +++ b/src/Pkg.jl @@ -70,7 +70,20 @@ const PREV_ENV_PATH = Ref{String}("") usable_io(io) = (io isa Base.TTY) || (io isa IOContext{IO} && io.io isa Base.TTY) can_fancyprint(io::IO) = (usable_io(io)) && (get(ENV, "CI", nothing) != "true") -should_autoprecompile() = Base.JLOptions().use_compiled_modules == 1 && Base.get_bool_env("JULIA_PKG_PRECOMPILE_AUTO", true) + +_autoprecompilation_enabled::Bool = true +const _autoprecompilation_enabled_scoped = Base.ScopedValues.ScopedValue{Bool}(true) +autoprecompilation_enabled(state::Bool) = (global _autoprecompilation_enabled = state) +function should_autoprecompile() + if Base.JLOptions().use_compiled_modules == 1 && + _autoprecompilation_enabled && + _autoprecompilation_enabled_scoped[] && + Base.get_bool_env("JULIA_PKG_PRECOMPILE_AUTO", true) + return true + else + return false + end +end """ in_repl_mode() @@ -206,11 +219,21 @@ const add = API.add Pkg.precompile(; strict::Bool=false, timing::Bool=false) Pkg.precompile(pkg; strict::Bool=false, timing::Bool=false) Pkg.precompile(pkgs; strict::Bool=false, timing::Bool=false) + Pkg.precompile(f, args...; kwargs...) Precompile all or specific dependencies of the project in parallel. Set `timing=true` to show the duration of the precompilation of each dependency. +To delay autoprecompilation of multiple Pkg actions until the end use. +This may be most efficient while manipulating the environment in various ways. + +```julia +Pkg.precompile() do + # Pkg actions here +end +``` + !!! note Errors will only throw when precompiling the top-level dependencies, given that not all manifest dependencies may be loaded by the top-level dependencies on the given system. @@ -228,6 +251,9 @@ Set `timing=true` to show the duration of the precompilation of each dependency. !!! compat "Julia 1.9" Timing mode requires at least Julia 1.9. +!!! compat "Julia 1.13" + The `Pkg.precompile(f, args...; kwargs...)` do-block syntax requires at least Julia 1.13. + # Examples ```julia Pkg.precompile() @@ -237,6 +263,39 @@ Pkg.precompile(["Foo", "Bar"]) """ const precompile = API.precompile +""" + Pkg.autoprecompilation_enabled(state::Bool) + +Enable or disable automatic precompilation for Pkg operations. + +When `state` is `true` (default), Pkg operations that modify the project environment +will automatically trigger precompilation of affected packages. When `state` is `false`, +automatic precompilation is disabled and packages will only be precompiled when +explicitly requested via [`Pkg.precompile`](@ref). + +This setting affects the global state and persists across Pkg operations in the same +Julia session. It can be used in combination with [`Pkg.precompile`](@ref) do-syntax +for more fine-grained control over when precompilation occurs. + +!!! compat "Julia 1.13" + This function requires at least Julia 1.13. + +# Examples +```julia +# Disable automatic precompilation +Pkg.autoprecompilation_enabled(false) +Pkg.add("Example") # Will not trigger auto-precompilation +Pkg.precompile() # Manual precompilation + +# Re-enable automatic precompilation +Pkg.autoprecompilation_enabled(true) +Pkg.add("AnotherPackage") # Will trigger auto-precompilation +``` + +See also [`Pkg.precompile`](@ref). +""" +autoprecompilation_enabled + """ Pkg.rm(pkg::Union{String, Vector{String}}; mode::PackageMode = PKGMODE_PROJECT) Pkg.rm(pkg::Union{PackageSpec, Vector{PackageSpec}}; mode::PackageMode = PKGMODE_PROJECT) diff --git a/test/api.jl b/test/api.jl index 65c40ca37e..a298cdce56 100644 --- a/test/api.jl +++ b/test/api.jl @@ -158,6 +158,67 @@ import .FakeTerminals.FakeTerminal @test !occursin("Precompiling", String(take!(iob))) # test that the previous precompile was a no-op end + dep8_path = git_init_package(tmp, joinpath("packages", "Dep8")) + function clear_dep8_cache() + rm(joinpath(Pkg.depots1(), "compiled", "v$(VERSION.major).$(VERSION.minor)", "Dep8"), force=true, recursive=true) + end + @testset "delayed precompilation with do-syntax" begin + iob = IOBuffer() + # Test that operations inside Pkg.precompile() do block don't trigger auto-precompilation + Pkg.precompile(io=iob) do + Pkg.add(Pkg.PackageSpec(path=dep8_path)) + Pkg.rm("Dep8") + clear_dep8_cache() + Pkg.add(Pkg.PackageSpec(path=dep8_path)) + end + + # The precompile should happen once at the end + @test count(r"Precompiling", String(take!(iob))) == 1 # should only precompile once + + # Verify it was precompiled by checking a second call is a no-op + Pkg.precompile(io=iob) + @test !occursin("Precompiling", String(take!(iob))) + end + + Pkg.rm("Dep8") + + @testset "autoprecompilation_enabled global control" begin + iob = IOBuffer() + withenv("JULIA_PKG_PRECOMPILE_AUTO" => nothing) do + original_state = Pkg._autoprecompilation_enabled + try + Pkg.autoprecompilation_enabled(false) + @test Pkg._autoprecompilation_enabled == false + + # Operations should not trigger autoprecompilation when globally disabled + clear_dep8_cache() + Pkg.add(Pkg.PackageSpec(path=dep8_path), io=iob) + @test !occursin("Precompiling", String(take!(iob))) + + # Manual precompile should still work + @test Base.isprecompiled(Base.identify_package("Dep8")) == false + Pkg.precompile(io=iob) + @test occursin("Precompiling", String(take!(iob))) + @test Base.isprecompiled(Base.identify_package("Dep8")) + + # Re-enable autoprecompilation + Pkg.autoprecompilation_enabled(true) + @test Pkg._autoprecompilation_enabled == true + + # Operations should now trigger autoprecompilation again + Pkg.rm("Dep8", io=iob) + clear_dep8_cache() + Pkg.add(Pkg.PackageSpec(path=dep8_path), io=iob) + @test Base.isprecompiled(Base.identify_package("Dep8")) + @test occursin("Precompiling", String(take!(iob))) + + finally + # Restore original state + Pkg.autoprecompilation_enabled(original_state) + end + end + end + @testset "instantiate" begin iob = IOBuffer() Pkg.activate("packages/Dep7")