Skip to content

Commit 70c220b

Browse files
committed
wip: Support portable scripts
1 parent 6aae107 commit 70c220b

File tree

8 files changed

+325
-33
lines changed

8 files changed

+325
-33
lines changed

ext/REPLExt/REPLExt.jl

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,20 @@ function projname(project_file::String)
6767
nothing
6868
end
6969
if project === nothing || project.name === nothing
70-
name = basename(dirname(project_file))
70+
# For .jl files, use the filename with extension as fallback
71+
# For directories/Project.toml, use the directory name
72+
if endswith(project_file, ".jl")
73+
name = basename(project_file)
74+
else
75+
name = basename(dirname(project_file))
76+
end
7177
else
72-
name = project.name::String
78+
# For portable scripts, include .jl extension to make it clear
79+
if endswith(project_file, ".jl")
80+
name = project.name::String * ".jl"
81+
else
82+
name = project.name::String
83+
end
7384
end
7485
for depot in Base.DEPOT_PATH
7586
envdir = joinpath(depot, "environments")
@@ -94,17 +105,25 @@ function promptf()
94105
else
95106
project_name = projname(project_file)
96107
if project_name !== nothing
97-
root = Types.find_root_base_project(project_file)
98-
rootname = projname(root)
99-
if root !== project_file
100-
path_prefix = "/" * dirname(Types.relative_project_path(root, project_file))
108+
# For portable scripts (.jl files), don't show directory path
109+
if endswith(project_file, ".jl")
110+
if textwidth(project_name) > 30
111+
project_name = first(project_name, 27) * "..."
112+
end
113+
prefix = "($(project_name)) "
101114
else
102-
path_prefix = ""
103-
end
104-
if textwidth(rootname) > 30
105-
rootname = first(rootname, 27) * "..."
115+
root = Types.find_root_base_project(project_file)
116+
rootname = projname(root)
117+
if root !== project_file
118+
path_prefix = "/" * dirname(Types.relative_project_path(root, project_file))
119+
else
120+
path_prefix = ""
121+
end
122+
if textwidth(rootname) > 30
123+
rootname = first(rootname, 27) * "..."
124+
end
125+
prefix = "($(rootname)$(path_prefix)) "
106126
end
107-
prefix = "($(rootname)$(path_prefix)) "
108127
prev_prefix = prefix
109128
prev_project_timestamp = mtime(project_file)
110129
prev_project_file = project_file

src/API.jl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,7 +1405,10 @@ function activate(; temp = false, shared = false, prev = false, io::IO = stderr_
14051405
end
14061406
Base.ACTIVE_PROJECT[] = nothing
14071407
p = Base.active_project()
1408-
p === nothing || printpkgstyle(io, :Activating, "project at $(pathrepr(dirname(p)))")
1408+
if p !== nothing
1409+
loc = endswith(p, ".jl") ? pathrepr(p) : "$(pathrepr(dirname(p)))"
1410+
printpkgstyle(io, :Activating, "project at $loc")
1411+
end
14091412
add_snapshot_to_undo()
14101413
return nothing
14111414
end
@@ -1465,7 +1468,8 @@ function activate(path::AbstractString; shared::Bool = false, temp::Bool = false
14651468
p = Base.active_project()
14661469
if p !== nothing
14671470
n = ispath(p) ? "" : "new "
1468-
printpkgstyle(io, :Activating, "$(n)project at $(pathrepr(dirname(p)))")
1471+
loc = endswith(p, ".jl") ? pathrepr(p) : "$(pathrepr(dirname(p)))"
1472+
printpkgstyle(io, :Activating, "$(n)project at $loc")
14691473
end
14701474
add_snapshot_to_undo()
14711475
return nothing

src/Operations.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3477,8 +3477,8 @@ function git_head_env(env, project_dir)
34773477
git_path = LibGit2.path(repo)
34783478
project_path = relpath(env.project_file, git_path)
34793479
manifest_path = relpath(env.manifest_file, git_path)
3480-
new_env.project = read_project(GitTools.git_file_stream(repo, "HEAD:$project_path", fakeit = true))
3481-
new_env.manifest = read_manifest(GitTools.git_file_stream(repo, "HEAD:$manifest_path", fakeit = true))
3480+
new_env.project = read_project(GitTools.git_file_stream(repo, "HEAD:$project_path", fakeit = true); source_file = project_path)
3481+
new_env.manifest = read_manifest(GitTools.git_file_stream(repo, "HEAD:$manifest_path", fakeit = true); source_file = manifest_path)
34823482
return new_env
34833483
end
34843484
catch err

src/Types.jl

Lines changed: 157 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ end
5656
const TOML_CACHE = Base.TOMLCache(Base.TOML.Parser{Dates}())
5757
const TOML_LOCK = ReentrantLock()
5858
# 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}
6161

6262
#################
6363
# Pkg Error #
@@ -68,6 +68,148 @@ end
6868
pkgerror(msg::String...) = throw(PkgError(join(msg)))
6969
Base.showerror(io::IO, err::PkgError) = print(io, err.msg)
7070

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+
71213
###############
72214
# PackageSpec #
73215
###############
@@ -212,19 +354,21 @@ function find_project_file(env::Union{Nothing, String} = nothing)
212354
project_file = Base.load_path_expand(env)
213355
project_file === nothing && pkgerror("package environment does not exist: $env")
214356
elseif env isa String
215-
if isdir(env)
357+
if isfile(env)
358+
project_file = abspath(env)
359+
elseif isdir(env)
216360
isempty(readdir(env)) || pkgerror("environment is a package directory: $env")
217361
project_file = joinpath(env, Base.project_names[end])
218362
else
219363
project_file = endswith(env, ".toml") ? abspath(env) :
220364
abspath(env, Base.project_names[end])
221365
end
222366
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")
224368
pkgerror(
225369
"""
226370
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.
228372
"""
229373
)
230374
end
@@ -445,9 +589,14 @@ function EnvCache(env::Union{Nothing, String} = nothing)
445589
end
446590

447591
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
451600
write_env_usage(manifest_file, "manifest_usage.toml")
452601
manifest = read_manifest(manifest_file)
453602

src/manifest.jl

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -295,12 +295,31 @@ function Manifest(raw::Dict{String, Any}, f_or_io::Union{String, IO})::Manifest
295295
return validate_manifest(julia_version, project_hash, manifest_format, stage1, other, registries)
296296
end
297297

298-
function read_manifest(f_or_io::Union{String, IO})
298+
function read_manifest(f_or_io::Union{String, IO}; source_file::Union{String, Nothing} = nothing)
299299
raw = try
300300
if f_or_io isa IO
301-
TOML.parse(read(f_or_io, String))
302-
else
303-
isfile(f_or_io) ? parse_toml(f_or_io) : return Manifest()
301+
# TODO Ugly
302+
# If source_file indicates a .jl file, write to temp file and parse as inline
303+
if source_file !== nothing && endswith(source_file, ".jl")
304+
content = read(f_or_io, String)
305+
temp_file = tempname() * ".jl"
306+
try
307+
write(temp_file, content)
308+
parse_toml(temp_file, manifest=true)
309+
finally
310+
rm(temp_file; force=true)
311+
end
312+
else
313+
TOML.parse(read(f_or_io, String))
314+
end
315+
elseif f_or_io isa String
316+
if !isfile(f_or_io)
317+
return Manifest()
318+
elseif endswith(f_or_io, ".jl")
319+
parse_toml(f_or_io, manifest=true)
320+
else
321+
parse_toml(f_or_io)
322+
end
304323
end
305324
catch e
306325
if e isa TOML.ParserError
@@ -315,7 +334,7 @@ function read_manifest(f_or_io::Union{String, IO})
315334
raw = convert_v1_format_manifest(raw)
316335
end
317336
end
318-
return Manifest(raw, f_or_io)
337+
return Manifest(raw, source_file !== nothing ? source_file : f_or_io)
319338
end
320339

321340
function convert_v1_format_manifest(old_raw_manifest::Dict)
@@ -476,6 +495,11 @@ function write_manifest(io::IO, raw_manifest::Dict)
476495
return nothing
477496
end
478497
function write_manifest(raw_manifest::Dict, manifest_file::AbstractString)
498+
if endswith(manifest_file, ".jl")
499+
str = sprint(write_manifest, raw_manifest)
500+
update_inline_manifest!(manifest_file, str)
501+
return nothing
502+
end
479503
str = sprint(write_manifest, raw_manifest)
480504
mkpath(dirname(manifest_file))
481505
return write(manifest_file, str)

0 commit comments

Comments
 (0)