|
56 | 56 | const TOML_CACHE = Base.TOMLCache(Base.TOML.Parser{Dates}()) |
57 | 57 | const TOML_LOCK = ReentrantLock() |
58 | 58 | # Some functions mutate the returning Dict so return a copy of the cached value here |
59 | | -parse_toml(toml_file::AbstractString) = |
60 | | - Base.invokelatest(deepcopy_toml, Base.parsed_toml(toml_file, TOML_CACHE, TOML_LOCK))::Dict{String, Any} |
| 59 | +parse_toml(toml_file::AbstractString; manifest::Bool=false, project::Bool=!manifest) = |
| 60 | + Base.invokelatest(deepcopy_toml, Base.parsed_toml(toml_file, TOML_CACHE, TOML_LOCK; manifest, project))::Dict{String, Any} |
61 | 61 |
|
62 | 62 | ################# |
63 | 63 | # Pkg Error # |
|
68 | 68 | pkgerror(msg::String...) = throw(PkgError(join(msg))) |
69 | 69 | Base.showerror(io::IO, err::PkgError) = print(io, err.msg) |
70 | 70 |
|
| 71 | +################################# |
| 72 | +# Portable script functionality # |
| 73 | +################################# |
| 74 | +function _render_inline_block(kind::Symbol, toml::String, newline::String, format::Symbol) |
| 75 | + kind_str = kind === :project ? "project" : "manifest" |
| 76 | + buf = IOBuffer() |
| 77 | + function emit(line) |
| 78 | + write(buf, line) |
| 79 | + write(buf, newline) |
| 80 | + end |
| 81 | + emit("#!" * kind_str * " begin") |
| 82 | + |
| 83 | + if format === :multiline |
| 84 | + # Use multi-line comment format: #= ... =# |
| 85 | + write(buf, "#=" * newline) |
| 86 | + write(buf, toml) |
| 87 | + if !endswith(toml, newline) |
| 88 | + write(buf, newline) |
| 89 | + end |
| 90 | + write(buf, "=#" * newline) |
| 91 | + else |
| 92 | + # Use line-by-line format with # prefix |
| 93 | + lines = split(toml, '\n'; keepempty = true) |
| 94 | + # Remove trailing empty line if toml ends with newline |
| 95 | + if !isempty(lines) && isempty(lines[end]) |
| 96 | + pop!(lines) |
| 97 | + end |
| 98 | + for raw_line in lines |
| 99 | + if isempty(raw_line) |
| 100 | + emit("#") |
| 101 | + else |
| 102 | + emit("# " * raw_line) |
| 103 | + end |
| 104 | + end |
| 105 | + end |
| 106 | + |
| 107 | + emit("#!" * kind_str * " end") |
| 108 | + return String(take!(buf)) |
| 109 | +end |
| 110 | + |
| 111 | +function _find_inline_section(source::String, kind::Symbol) |
| 112 | + kind_str = kind === :project ? "project" : "manifest" |
| 113 | + begin_marker = "#!$(kind_str) begin" |
| 114 | + end_marker = "#!$(kind_str) end" |
| 115 | + |
| 116 | + # Find begin marker |
| 117 | + begin_idx = findfirst(begin_marker, source) |
| 118 | + begin_idx === nothing && return nothing |
| 119 | + |
| 120 | + # Find end marker after begin |
| 121 | + end_idx = findnext(end_marker, source, last(begin_idx) + 1) |
| 122 | + end_idx === nothing && return nothing |
| 123 | + |
| 124 | + # Determine newline style |
| 125 | + newline = contains(source, "\r\n") ? "\r\n" : "\n" |
| 126 | + |
| 127 | + # Find the start of the line containing begin marker |
| 128 | + line_start = findprev(isequal('\n'), source, first(begin_idx)) |
| 129 | + line_start = line_start === nothing ? 1 : line_start + 1 |
| 130 | + |
| 131 | + # Determine format by checking if there's a #= after the begin marker |
| 132 | + # Look at content between begin and end markers |
| 133 | + content_start = last(begin_idx) + 1 |
| 134 | + content_end = first(end_idx) - 1 |
| 135 | + content_between = source[content_start:content_end] |
| 136 | + format = contains(content_between, "#=") ? :multiline : :line |
| 137 | + |
| 138 | + # Find the newline after the end marker (if it exists) |
| 139 | + char_after_end = last(end_idx) < lastindex(source) ? source[nextind(source, last(end_idx))] : nothing |
| 140 | + span_end_pos = if char_after_end == '\n' || (char_after_end == '\r' && last(end_idx) + 1 < lastindex(source) && source[last(end_idx) + 2] == '\n') |
| 141 | + # Include the newline in the span |
| 142 | + char_after_end == '\r' ? last(end_idx) + 2 : last(end_idx) + 1 |
| 143 | + else |
| 144 | + last(end_idx) |
| 145 | + end |
| 146 | + |
| 147 | + return ( |
| 148 | + span_start = line_start, |
| 149 | + span_end = span_end_pos, |
| 150 | + newline = newline, |
| 151 | + format = format |
| 152 | + ) |
| 153 | +end |
| 154 | + |
| 155 | +function update_inline_project!(path::AbstractString, toml::String) |
| 156 | + source = read(path, String) |
| 157 | + section = _find_inline_section(source, :project) |
| 158 | + |
| 159 | + if section === nothing |
| 160 | + # No existing section, add one at the beginning |
| 161 | + newline = contains(source, "\r\n") ? "\r\n" : "\n" |
| 162 | + replacement = _render_inline_block(:project, toml, newline, :line) |
| 163 | + new_source = isempty(source) ? replacement : replacement * newline * source |
| 164 | + else |
| 165 | + # Replace existing section |
| 166 | + replacement = _render_inline_block(:project, toml, section.newline, section.format) |
| 167 | + prefix = section.span_start == firstindex(source) ? "" : source[firstindex(source):prevind(source, section.span_start)] |
| 168 | + suffix = section.span_end == lastindex(source) ? "" : source[nextind(source, section.span_end):lastindex(source)] |
| 169 | + new_source = prefix * replacement * suffix |
| 170 | + end |
| 171 | + |
| 172 | + open(path, "w") do io |
| 173 | + write(io, new_source) |
| 174 | + end |
| 175 | + return nothing |
| 176 | +end |
| 177 | + |
| 178 | +function update_inline_manifest!(path::AbstractString, toml::String) |
| 179 | + source = read(path, String) |
| 180 | + project_section = _find_inline_section(source, :project) |
| 181 | + manifest_section = _find_inline_section(source, :manifest) |
| 182 | + |
| 183 | + if manifest_section === nothing |
| 184 | + # No existing manifest section, add one after project section |
| 185 | + if project_section === nothing |
| 186 | + # No project section either, add both |
| 187 | + newline = contains(source, "\r\n") ? "\r\n" : "\n" |
| 188 | + project_block = _render_inline_block(:project, "", newline, :line) |
| 189 | + manifest_block = _render_inline_block(:manifest, toml, newline, :line) |
| 190 | + new_source = isempty(source) ? project_block * newline * manifest_block : project_block * newline * manifest_block * newline * source |
| 191 | + else |
| 192 | + # Add manifest after project |
| 193 | + replacement = _render_inline_block(:manifest, toml, project_section.newline, project_section.format) |
| 194 | + insertion_point = project_section.span_end |
| 195 | + prefix = source[firstindex(source):insertion_point] |
| 196 | + suffix = insertion_point == lastindex(source) ? "" : source[nextind(source, insertion_point):lastindex(source)] |
| 197 | + new_source = prefix * project_section.newline * replacement * suffix |
| 198 | + end |
| 199 | + else |
| 200 | + # Replace existing manifest section |
| 201 | + replacement = _render_inline_block(:manifest, toml, manifest_section.newline, manifest_section.format) |
| 202 | + prefix = manifest_section.span_start == firstindex(source) ? "" : source[firstindex(source):prevind(source, manifest_section.span_start)] |
| 203 | + suffix = manifest_section.span_end == lastindex(source) ? "" : source[nextind(source, manifest_section.span_end):lastindex(source)] |
| 204 | + new_source = prefix * replacement * suffix |
| 205 | + end |
| 206 | + |
| 207 | + open(path, "w") do io |
| 208 | + write(io, new_source) |
| 209 | + end |
| 210 | + return nothing |
| 211 | +end |
| 212 | + |
71 | 213 | ############### |
72 | 214 | # PackageSpec # |
73 | 215 | ############### |
@@ -212,19 +354,21 @@ function find_project_file(env::Union{Nothing, String} = nothing) |
212 | 354 | project_file = Base.load_path_expand(env) |
213 | 355 | project_file === nothing && pkgerror("package environment does not exist: $env") |
214 | 356 | elseif env isa String |
215 | | - if isdir(env) |
| 357 | + if isfile(env) |
| 358 | + project_file = abspath(env) |
| 359 | + elseif isdir(env) |
216 | 360 | isempty(readdir(env)) || pkgerror("environment is a package directory: $env") |
217 | 361 | project_file = joinpath(env, Base.project_names[end]) |
218 | 362 | else |
219 | 363 | project_file = endswith(env, ".toml") ? abspath(env) : |
220 | 364 | abspath(env, Base.project_names[end]) |
221 | 365 | end |
222 | 366 | end |
223 | | - if isfile(project_file) && !contains(basename(project_file), "Project") |
| 367 | + if isfile(project_file) && !contains(basename(project_file), "Project") && !endswith(project_file, ".jl") |
224 | 368 | pkgerror( |
225 | 369 | """ |
226 | 370 | The active project has been set to a file that isn't a Project file: $project_file |
227 | | - The project path must be to a Project file or directory. |
| 371 | + The project path must be to a Project file or directory or a julia file. |
228 | 372 | """ |
229 | 373 | ) |
230 | 374 | end |
@@ -445,9 +589,14 @@ function EnvCache(env::Union{Nothing, String} = nothing) |
445 | 589 | end |
446 | 590 |
|
447 | 591 | dir = abspath(project_dir) |
448 | | - manifest_file = manifest_file !== nothing ? |
449 | | - (isabspath(manifest_file) ? manifest_file : abspath(dir, manifest_file)) : |
450 | | - manifestfile_path(dir)::String |
| 592 | + # For .jl files, always use the same file for both project and manifest (inline) |
| 593 | + if endswith(project_file, ".jl") |
| 594 | + manifest_file = project_file |
| 595 | + else |
| 596 | + manifest_file = manifest_file !== nothing ? |
| 597 | + (isabspath(manifest_file) ? manifest_file : abspath(dir, manifest_file)) : |
| 598 | + manifestfile_path(dir)::String |
| 599 | + end |
451 | 600 | write_env_usage(manifest_file, "manifest_usage.toml") |
452 | 601 | manifest = read_manifest(manifest_file) |
453 | 602 |
|
|
0 commit comments