diff --git a/CHANGELOG.md b/CHANGELOG.md index 5df2e18dcf..36836c7b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/API.jl b/src/API.jl index ab5a774961..07adeb6857 100644 --- a/src/API.jl +++ b/src/API.jl @@ -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 @@ -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 @@ -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 diff --git a/test/sources.jl b/test/sources.jl index e5b5508ec3..13a5786e30 100644 --- a/test/sources.jl +++ b/test/sources.jl @@ -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") @@ -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 diff --git a/test/workspaces.jl b/test/workspaces.jl index 87654ccf4c..a5aa726734 100644 --- a/test/workspaces.jl +++ b/test/workspaces.jl @@ -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