diff --git a/src/Types.jl b/src/Types.jl index c63f60ac44..2fd0f00eb2 100644 --- a/src/Types.jl +++ b/src/Types.jl @@ -10,7 +10,7 @@ import Base.string using TOML import ..Pkg, ..Registry -import ..Pkg: GitTools, depots, depots1, logdir, set_readonly, safe_realpath, pkg_server, stdlib_dir, stdlib_path, isurl, stderr_f, RESPECT_SYSIMAGE_VERSIONS, atomic_toml_write, create_cachedir_tag +import ..Pkg: GitTools, depots, depots1, logdir, set_readonly, safe_realpath, pkg_server, stdlib_dir, stdlib_path, isurl, stderr_f, RESPECT_SYSIMAGE_VERSIONS, atomic_toml_write, create_cachedir_tag, normalize_path_for_toml import Base.BinaryPlatforms: Platform using ..Pkg.Versions import FileWatching diff --git a/src/manifest.jl b/src/manifest.jl index 4a6e1fe120..ec47f9e5c6 100644 --- a/src/manifest.jl +++ b/src/manifest.jl @@ -329,14 +329,14 @@ function destructure(manifest::Manifest)::Dict entry!(new_entry, "git-tree-sha1", entry.tree_hash) entry!(new_entry, "pinned", entry.pinned; default = false) path = entry.path - if path !== nothing && Sys.iswindows() && !isabspath(path) - path = join(splitpath(path), "/") + if path !== nothing + path = normalize_path_for_toml(path) end entry!(new_entry, "path", path) entry!(new_entry, "entryfile", entry.entryfile) repo_source = entry.repo.source - if repo_source !== nothing && Sys.iswindows() && !isabspath(repo_source) && !isurl(repo_source) - repo_source = join(splitpath(repo_source), "/") + if repo_source !== nothing && !isurl(repo_source) + repo_source = normalize_path_for_toml(repo_source) end entry!(new_entry, "repo-url", repo_source) entry!(new_entry, "repo-rev", entry.repo.rev) diff --git a/src/project.jl b/src/project.jl index 182736b8d0..c4b5f82983 100644 --- a/src/project.jl +++ b/src/project.jl @@ -289,7 +289,21 @@ function destructure(project::Project)::Dict entry!("entryfile", project.entryfile) entry!("deps", merge(project.deps, project._deps_weak)) entry!("weakdeps", project.weakdeps) - entry!("sources", project.sources) + + # Normalize paths in sources to use forward slashes on Windows (matching Manifest.toml behavior) + normalized_sources = project.sources + if !isempty(project.sources) + normalized_sources = Dict{String, Dict{String, String}}() + for (name, source) in project.sources + normalized_source = copy(source) + path = get(source, "path", nothing) + if path !== nothing + normalized_source["path"] = normalize_path_for_toml(path) + end + normalized_sources[name] = normalized_source + end + end + entry!("sources", normalized_sources) entry!("extras", project.extras) entry!("compat", Dict(name => x.str for (name, x) in project.compat)) entry!("targets", project.targets) diff --git a/src/utils.jl b/src/utils.jl index b4b9054ef2..ce69a6832b 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -38,6 +38,20 @@ function pathrepr(path::String) return "`" * Base.contractuser(path) * "`" end +""" + normalize_path_for_toml(path::String) + +Normalize a path for writing to TOML files (Project.toml/Manifest.toml). +On Windows, converts relative paths to use forward slashes for cross-platform compatibility. +Absolute paths are left unchanged as they are platform-specific by nature. +""" +function normalize_path_for_toml(path::String) + if Sys.iswindows() && !isabspath(path) + return join(splitpath(path), "/") + end + return path +end + function set_readonly(path) for (root, dirs, files) in walkdir(path) for file in files diff --git a/test/misc.jl b/test/misc.jl index 49e8dfcae1..e2e3dc3ed0 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -26,6 +26,25 @@ end end end +@testset "normalize_path_for_toml" begin + # Test that relative paths with backslashes are normalized to forward slashes on Windows + # and left unchanged on other platforms + if Sys.iswindows() + @test Pkg.normalize_path_for_toml("foo\\bar\\baz") == "foo/bar/baz" + @test Pkg.normalize_path_for_toml("..\\parent\\dir") == "../parent/dir" + @test Pkg.normalize_path_for_toml(".\\current") == "./current" + # Absolute paths should not be normalized (they're platform-specific) + @test Pkg.normalize_path_for_toml("C:\\absolute\\path") == "C:\\absolute\\path" + @test Pkg.normalize_path_for_toml("\\\\network\\share") == "\\\\network\\share" + else + # On Unix-like systems, paths should be unchanged + @test Pkg.normalize_path_for_toml("foo/bar/baz") == "foo/bar/baz" + @test Pkg.normalize_path_for_toml("../parent/dir") == "../parent/dir" + @test Pkg.normalize_path_for_toml("./current") == "./current" + @test Pkg.normalize_path_for_toml("/absolute/path") == "/absolute/path" + end +end + @test eltype([PackageSpec(a) for a in []]) == PackageSpec @testset "PackageSpec version default" begin diff --git a/test/sources.jl b/test/sources.jl index 4d6010ffa6..8c8454cab7 100644 --- a/test/sources.jl +++ b/test/sources.jl @@ -51,6 +51,43 @@ temp_pkg_dir() do project_path end end end + + @testset "path normalization in Project.toml [sources]" begin + mktempdir() do tmp + cd(tmp) do + # Create a minimal Project.toml with sources containing a path + write( + "Project.toml", + """ + name = "TestPackage" + uuid = "12345678-1234-1234-1234-123456789abc" + + [deps] + LocalPkg = "87654321-4321-4321-4321-cba987654321" + + [sources] + LocalPkg = { path = "subdir/LocalPkg" } + """ + ) + + # Read the project + project = Pkg.Types.read_project("Project.toml") + + # Verify the path is read correctly (will have native separators internally) + @test haskey(project.sources, "LocalPkg") + @test haskey(project.sources["LocalPkg"], "path") + + # Write it back + Pkg.Types.write_project(project, "Project.toml") + + # Read the written file as string and verify forward slashes are used + project_content = read("Project.toml", String) + @test occursin("path = \"subdir/LocalPkg\"", project_content) + # Verify backslashes are NOT in the path (would indicate Windows path wasn't normalized) + @test !occursin("path = \"subdir\\\\LocalPkg\"", project_content) + end + end + end end end # module