Skip to content

Commit 233677d

Browse files
committed
cool stuff indeed
1 parent 4a80fa0 commit 233677d

File tree

3 files changed

+156
-45
lines changed

3 files changed

+156
-45
lines changed

docs/src/toml-files.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ UUIDs etc.
1212
[Code Loading](https://docs.julialang.org/en/v1/manual/code-loading/)
1313
in the Julia manual.
1414

15-
1615
## `Project.toml`
1716

1817
The project file describes the project on a high level, for example, the package/project
@@ -542,3 +541,10 @@ uuid = "edca9bc6-334e-11e9-3554-9595dbb4349c"
542541

543542
There is now an array of the two `B` packages, and the `[deps]` section for `A` has been
544543
expanded to be explicit about which `B` package `A` depends on.
544+
## Portable scripts
545+
546+
Julia scripts can embed project and manifest data inline with fenced
547+
comments such as `#!project … #!project end` and `#!manifest … #!manifest end`.
548+
When the active project is set to a `.jl` file, Pkg reads and writes inline project metadata
549+
from that file. All commands that modify the project or manifest
550+
content (e.g. `Pkg.add`, `Pkg.rm`, `Pkg.update`) will update the inline sections accordingly.

src/Operations.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,8 +1537,8 @@ end
15371537
################################
15381538

15391539
function prune_manifest(env::EnvCache)
1540-
# if project uses another manifest, only prune project entry in manifest
1541-
if isempty(env.workspace) && dirname(env.project_file) != dirname(env.manifest_file)
1540+
# if project uses another manifest (and we are not a portable script), only prune project entry in manifest
1541+
if isempty(env.workspace) && dirname(env.project_file) != dirname(env.manifest_file) && !endswith(env.project_file, ".jl")
15421542
proj_entry = env.manifest[env.project.uuid]
15431543
proj_entry.deps = env.project.deps
15441544
else

src/Types.jl

Lines changed: 147 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -130,22 +130,57 @@ function _find_inline_section(source::String, kind::Symbol)
130130
line_start = findprev(isequal('\n'), source, first(begin_idx))
131131
line_start = line_start === nothing ? 1 : line_start + 1
132132

133+
# Check if there's a #region marker on the line before
134+
region_marker = "#region $(kind_str)"
135+
if line_start > 1
136+
prev_line_end = line_start - 1 # This is the newline character
137+
prev_line_start = findprev(isequal('\n'), source, prev_line_end - 1)
138+
prev_line_start = prev_line_start === nothing ? 1 : prev_line_start + 1
139+
prev_line = source[prev_line_start:prev_line_end-1]
140+
if strip(prev_line) == region_marker
141+
line_start = prev_line_start
142+
end
143+
end
144+
133145
# Determine format by checking if there's a #= after the begin marker
134146
# Look at content between begin and end markers
135147
content_start = last(begin_idx) + 1
136148
content_end = first(end_idx) - 1
137149
content_between = source[content_start:content_end]
138150
format = contains(content_between, "#=") ? :multiline : :line
139151

140-
# Find the newline after the end marker (if it exists)
152+
# Find the newline after the end marker
141153
char_after_end = last(end_idx) < lastindex(source) ? source[nextind(source, last(end_idx))] : nothing
154+
included_newline = false
142155
span_end_pos = if char_after_end == '\n' || (char_after_end == '\r' && last(end_idx) + 1 < lastindex(source) && source[last(end_idx) + 2] == '\n')
143156
# Include the newline in the span
157+
included_newline = true
144158
char_after_end == '\r' ? last(end_idx) + 2 : last(end_idx) + 1
145159
else
146160
last(end_idx)
147161
end
148162

163+
# Check if there's a #endregion marker on the next line after end marker
164+
endregion_marker = "#endregion $(kind_str)"
165+
if included_newline && span_end_pos < lastindex(source)
166+
# If we included a newline, start looking at the next character
167+
next_line_start = span_end_pos + 1
168+
next_line_end = findnext(isequal('\n'), source, next_line_start)
169+
if next_line_end !== nothing
170+
next_line = source[next_line_start:next_line_end-1]
171+
if strip(next_line) == endregion_marker
172+
# Include the #endregion line and its newline in the span
173+
span_end_pos = next_line_end + 1
174+
end
175+
elseif next_line_start <= lastindex(source)
176+
# No newline found, check if rest of file is the endregion marker
177+
next_line = source[next_line_start:end]
178+
if strip(next_line) == endregion_marker
179+
span_end_pos = lastindex(source)
180+
end
181+
end
182+
end
183+
149184
return (
150185
span_start = line_start,
151186
span_end = span_end_pos,
@@ -154,21 +189,43 @@ function _find_inline_section(source::String, kind::Symbol)
154189
)
155190
end
156191

157-
function update_inline_project!(path::AbstractString, toml::String)
192+
function _update_inline_section!(path::AbstractString, kind::Symbol, toml::String)
158193
source = read(path, String)
159-
section = _find_inline_section(source, :project)
194+
section = _find_inline_section(source, kind)
160195

161196
if section === nothing
162-
# No existing section, add one at the beginning
197+
# No existing section, add appropriately
163198
newline = contains(source, "\r\n") ? "\r\n" : "\n"
164-
replacement = _render_inline_block(:project, toml, newline, :line)
165-
new_source = isempty(source) ? replacement : replacement * newline * source
199+
replacement = _render_inline_block(kind, toml, newline, :line)
200+
201+
if kind === :project
202+
# Project goes at the beginning
203+
new_source = isempty(source) ? replacement : replacement * newline * source
204+
else
205+
# Manifest goes at the bottom
206+
project_section = _find_inline_section(source, :project)
207+
if project_section === nothing
208+
# No project section either, add empty project at top and manifest at bottom
209+
project_block = _render_inline_block(:project, "", newline, :line)
210+
if isempty(source)
211+
new_source = project_block * newline * replacement
212+
else
213+
new_source = project_block * newline * source * newline * replacement
214+
end
215+
else
216+
# Add manifest at the bottom of the file
217+
replacement = _render_inline_block(kind, toml, project_section.newline, project_section.format)
218+
new_source = source * project_section.newline * replacement
219+
end
220+
end
166221
else
167222
# Replace existing section
168-
replacement = _render_inline_block(:project, toml, section.newline, section.format)
223+
replacement = _render_inline_block(kind, toml, section.newline, section.format)
169224
prefix = section.span_start == firstindex(source) ? "" : source[firstindex(source):prevind(source, section.span_start)]
170225
suffix = section.span_end == lastindex(source) ? "" : source[nextind(source, section.span_end):lastindex(source)]
171-
new_source = prefix * replacement * suffix
226+
# Add a blank line after the section if there's content after it
227+
separator = !isempty(suffix) && !startswith(suffix, section.newline) ? section.newline : ""
228+
new_source = prefix * replacement * separator * suffix
172229
end
173230

174231
open(path, "w") do io
@@ -177,42 +234,29 @@ function update_inline_project!(path::AbstractString, toml::String)
177234
return nothing
178235
end
179236

180-
function update_inline_manifest!(path::AbstractString, toml::String)
237+
function remove_inline_section!(path::AbstractString, kind::Symbol)
181238
source = read(path, String)
182-
project_section = _find_inline_section(source, :project)
183-
manifest_section = _find_inline_section(source, :manifest)
184-
185-
if manifest_section === nothing
186-
# No existing manifest section, add one at the bottom
187-
if project_section === nothing
188-
# No project section either, add project at top and manifest at bottom
189-
newline = contains(source, "\r\n") ? "\r\n" : "\n"
190-
project_block = _render_inline_block(:project, "", newline, :line)
191-
manifest_block = _render_inline_block(:manifest, toml, newline, :line)
192-
if isempty(source)
193-
new_source = project_block * newline * manifest_block
194-
else
195-
new_source = project_block * newline * source * newline * manifest_block
196-
end
197-
else
198-
# Add manifest at the bottom of the file
199-
replacement = _render_inline_block(:manifest, toml, project_section.newline, project_section.format)
200-
new_source = source * project_section.newline * replacement
201-
end
202-
else
203-
# Replace existing manifest section
204-
replacement = _render_inline_block(:manifest, toml, manifest_section.newline, manifest_section.format)
205-
prefix = manifest_section.span_start == firstindex(source) ? "" : source[firstindex(source):prevind(source, manifest_section.span_start)]
206-
suffix = manifest_section.span_end == lastindex(source) ? "" : source[nextind(source, manifest_section.span_end):lastindex(source)]
207-
new_source = prefix * replacement * suffix
208-
end
239+
section = _find_inline_section(source, kind)
209240

210-
open(path, "w") do io
211-
write(io, new_source)
241+
if section !== nothing
242+
prefix = section.span_start == firstindex(source) ? "" : source[firstindex(source):prevind(source, section.span_start)]
243+
suffix = section.span_end >= lastindex(source) ? "" : source[nextind(source, section.span_end):lastindex(source)]
244+
new_source = prefix * suffix
245+
open(path, "w") do io
246+
write(io, new_source)
247+
end
212248
end
213249
return nothing
214250
end
215251

252+
function update_inline_project!(path::AbstractString, toml::String)
253+
return _update_inline_section!(path, :project, toml)
254+
end
255+
256+
function update_inline_manifest!(path::AbstractString, toml::String)
257+
return _update_inline_section!(path, :manifest, toml)
258+
end
259+
216260
###############
217261
# PackageSpec #
218262
###############
@@ -592,10 +636,38 @@ function EnvCache(env::Union{Nothing, String} = nothing)
592636
end
593637

594638
dir = abspath(project_dir)
595-
# For .jl files, always use the same file for both project and manifest (inline)
639+
640+
# Save the original project before any modifications
641+
original_project = deepcopy(project)
642+
643+
# For .jl files, handle inline_manifest flag and fix inconsistent states
596644
if endswith(project_file, ".jl")
597-
manifest_file = project_file
645+
inline_manifest = get(project.other, "inline_manifest", true)::Bool
646+
647+
# Case 1: inline_manifest=false but no manifest path
648+
# User wants external manifest but hasn't set it up yet
649+
if !inline_manifest && project.manifest === nothing
650+
# Generate a new UUID and set manifest path
651+
script_uuid = string(uuid4())
652+
script_name = splitext(basename(project_file))[1]
653+
manifest_file = joinpath(depots1(), "environments", "scripts", "$(script_name)_$(script_uuid)", "Manifest.toml")
654+
project.manifest = manifest_file
655+
# Case 2: inline_manifest=true (or default) but has manifest path
656+
# User wants inline manifest but still has external path set
657+
elseif inline_manifest && project.manifest !== nothing
658+
# Load from external path for reading this time
659+
manifest_file = isabspath(project.manifest) ? project.manifest : abspath(dir, project.manifest)
660+
# But clear the path so it gets written inline later
661+
# (We'll clean up the external file in write_env)
662+
# Case 3: inline_manifest=false and has manifest path (consistent state)
663+
elseif !inline_manifest && project.manifest !== nothing
664+
manifest_file = isabspath(project.manifest) ? project.manifest : abspath(dir, project.manifest)
665+
# Case 4: inline_manifest=true and no manifest path (consistent state, default)
666+
else
667+
manifest_file = project_file
668+
end
598669
else
670+
# For regular .toml files, use standard logic
599671
manifest_file = manifest_file !== nothing ?
600672
(isabspath(manifest_file) ? manifest_file : abspath(dir, manifest_file)) :
601673
manifestfile_path(dir)::String
@@ -611,7 +683,7 @@ function EnvCache(env::Union{Nothing, String} = nothing)
611683
project,
612684
workspace,
613685
manifest,
614-
deepcopy(project),
686+
original_project,
615687
deepcopy(manifest),
616688
)
617689

@@ -1565,12 +1637,45 @@ function write_env(
15651637
pkgerror("Cannot modify a readonly environment. The project at $(env.project_file) is marked as readonly.")
15661638
end
15671639

1640+
# Handle transitions for portable scripts
1641+
transitioning_to_inline = false
1642+
if endswith(env.project_file, ".jl")
1643+
inline_manifest = get(env.project.other, "inline_manifest", true)::Bool
1644+
1645+
# If transitioning to inline and we had an external manifest, clean it up
1646+
if inline_manifest && env.project.manifest !== nothing
1647+
transitioning_to_inline = true
1648+
external_manifest_path = isabspath(env.project.manifest) ? env.project.manifest :
1649+
abspath(dirname(env.project_file), env.project.manifest)
1650+
# Clear the manifest path so it writes inline
1651+
env.project.manifest = nothing
1652+
# Update manifest_file to point to the script file for inline writing
1653+
env.manifest_file = env.project_file
1654+
# Clean up external manifest directory
1655+
external_dir = dirname(external_manifest_path)
1656+
if isdir(external_dir)
1657+
rm(external_dir; recursive=true, force=true)
1658+
end
1659+
end
1660+
end
1661+
15681662
if (env.project != env.original_project) && (!skip_writing_project)
15691663
write_project(env, skip_readonly_check)
15701664
end
1571-
if env.manifest != env.original_manifest
1665+
# Force manifest write when transitioning to inline, even if manifest hasn't changed
1666+
if env.manifest != env.original_manifest || transitioning_to_inline
15721667
write_manifest(env)
15731668
end
1669+
1670+
# Remove inline manifest section if we have external manifest
1671+
if endswith(env.project_file, ".jl")
1672+
inline_manifest = get(env.project.other, "inline_manifest", true)::Bool
1673+
if !inline_manifest
1674+
# Remove the inline manifest section since we're using external
1675+
remove_inline_section!(env.project_file, :manifest)
1676+
end
1677+
end
1678+
15741679
return update_undo && Pkg.API.add_snapshot_to_undo(env)
15751680
end
15761681

0 commit comments

Comments
 (0)