Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Pkg v1.14 Release Notes
=======================

- `Pkg.resolve` and `Pkg.instantiate` no longer rewrite `Project.toml`. Project.toml is authored input and the
manifest is the derived output; these commands only produce the manifest and must not edit the project as a side
effect. Previously, resolving or instantiating an environment whose `Project.toml` contained a `[sources]` entry
(for example a top-level project in a workspace) could rewrite the file, normalizing paths and dropping comments
and formatting. Pass `skip_writing_project=false` to opt back into the old behavior. ([#4713])
- `Pkg.instantiate` now accepts an `update_on_mismatch::Bool` keyword argument (and a corresponding `-u` /
`--update_on_mismatch` REPL flag) that falls back to `Pkg.update()` when the manifest does not match the project
or was resolved with a different Julia minor version, instead of warning or erroring. Useful for tooling and
Expand Down
8 changes: 5 additions & 3 deletions src/API.jl
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ function up(
end

resolve(; io::IO = stderr_f(), kwargs...) = resolve(Context(; io); kwargs...)
function resolve(ctx::Context; skip_writing_project::Bool = false, kwargs...)
function resolve(ctx::Context; skip_writing_project::Bool = true, kwargs...)
up(ctx; level = UPLEVEL_FIXED, mode = PKGMODE_MANIFEST, update_registry = false, skip_writing_project, kwargs...)
return nothing
end
Expand Down Expand Up @@ -1321,7 +1321,8 @@ function instantiate(
if (!isfile(ctx.env.manifest_file) && manifest === nothing) || manifest == false
# given no manifest exists, only allow invoking a registry update if there are project deps
allow_registry_update = isfile(ctx.env.project_file) && !isempty(ctx.env.project.deps)
up(ctx; update_registry = update_registry && allow_registry_update)
# instantiate produces the manifest; it must not rewrite the authored Project.toml.
up(ctx; update_registry = update_registry && allow_registry_update, skip_writing_project = true)
allow_autoprecomp && Pkg._auto_precompile(ctx, already_instantiated = true)
return
end
Expand All @@ -1340,7 +1341,8 @@ function instantiate(
saved_initial_snapshot[] = true
end
printpkgstyle(ctx.io, :Update, "manifest does not match project or Julia version, falling back to `Pkg.update()`", color = Base.info_color())
up(ctx; update_registry, mode = workspace ? PKGMODE_MANIFEST : PKGMODE_PROJECT)
# instantiate produces the manifest; it must not rewrite the authored Project.toml.
up(ctx; update_registry, mode = workspace ? PKGMODE_MANIFEST : PKGMODE_PROJECT, skip_writing_project = true)
allow_autoprecomp && Pkg._auto_precompile(ctx, already_instantiated = true)
return
end
Expand Down
5 changes: 3 additions & 2 deletions test/sources.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ temp_pkg_dir() do project_path
path = copy_test_package(dir, "WithSources")
cd(path) do
with_current_env() do
Pkg.resolve()
# Request project writing explicitly to sync [sources] back into Project.toml.
Pkg.resolve(; skip_writing_project = false)
@test !isempty(Pkg.project().sources["Example"])
project_backup = cp("Project.toml", "Project.toml.bak"; force = true)
Pkg.free("Example")
Expand All @@ -21,7 +22,7 @@ temp_pkg_dir() do project_path
@test Pkg.project().sources["Example"] == Dict("url" => "https://github.com/JuliaLang/Example.jl/", "rev" => "78406c204b8")
cp("Project.toml.bak", "Project.toml"; force = true)
cp("BadManifest.toml", "Manifest.toml"; force = true)
Pkg.resolve()
Pkg.resolve(; skip_writing_project = false)
@test Pkg.project().sources["Example"] == Dict("rev" => "master", "url" => "https://github.com/JuliaLang/Example.jl")
@test Pkg.project().sources["LocalPkg"] == Dict("path" => "LocalPkg")
end
Expand Down
85 changes: 85 additions & 0 deletions test/workspaces.jl
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,89 @@ end
end
end

# Project.toml is authored input; `resolve` and `instantiate` produce the manifest and
# must never rewrite it. A top-level workspace [sources] entry with a deliberately
# non-canonical path and an authored comment is the tripwire: writing the project would
# normalize the path (`./Leaf` -> `Leaf`) and drop the comment, so any project write shows
# up as a byte change.
@testset "resolve/instantiate only read Project.toml" begin
leaf_uuid = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
mktempdir() do dir
# A local leaf package tracked by path from the top-level project.
mkpath(joinpath(dir, "Leaf", "src"))
write(
joinpath(dir, "Leaf", "Project.toml"),
"""
name = "Leaf"
uuid = "$leaf_uuid"
version = "0.1.0"
"""
)
write(joinpath(dir, "Leaf", "src", "Leaf.jl"), "module Leaf\nend\n")

# A workspace member, so this is a genuine workspace.
mkpath(joinpath(dir, "sub", "src"))
write(
joinpath(dir, "sub", "Project.toml"),
"""
name = "Sub"
uuid = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
version = "0.1.0"
"""
)
write(joinpath(dir, "sub", "src", "Sub.jl"), "module Sub\nend\n")

# The top-level project carries [workspace] and a [sources] entry whose path is
# written non-canonically and is preceded by a hand-authored comment.
project_file = joinpath(dir, "Project.toml")
write(
project_file,
"""
name = "Root"
uuid = "cccccccc-cccc-cccc-cccc-cccccccccccc"
version = "0.1.0"

[deps]
Leaf = "$leaf_uuid"

[workspace]
projects = ["sub"]

# DO-NOT-DROP: authored comment a lossy project rewrite would delete
[sources]
Leaf = {path = "./Leaf"}
"""
)
authored = read(project_file, String)
manifest_file = joinpath(dir, "Manifest.toml")

cd(dir) do
with_current_env() do
Pkg.activate(".")

# `resolve` produces the manifest and must leave Project.toml untouched.
Pkg.resolve()
@test read(project_file, String) == authored

# `instantiate` with a present, consistent manifest must not rewrite it.
Pkg.instantiate()
@test read(project_file, String) == authored

# `instantiate` with no manifest falls back to `up`; still no project write.
rm(manifest_file; force = true)
Pkg.instantiate()
@test isfile(manifest_file)
@test read(project_file, String) == authored

# `instantiate(update_on_mismatch=true)` on a mismatched manifest also falls
# back to `up`; the manifest is refreshed but Project.toml must not change.
stale = replace(read(manifest_file, String), r"julia_version = \"[^\"]*\"" => "julia_version = \"0.0.0\"")
write(manifest_file, stale)
Pkg.instantiate(update_on_mismatch = true)
@test read(project_file, String) == authored
end
end
end
end

end # module
Loading