Skip to content

Commit 72e4af2

Browse files
committed
remove multiline comment support, add requirement that project has to be first, manifest last
1 parent e679ccc commit 72e4af2

File tree

10 files changed

+188
-161
lines changed

10 files changed

+188
-161
lines changed

base/loading.jl

Lines changed: 59 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -258,88 +258,80 @@ end
258258

259259

260260
function extract_inline_section(path::String, type::Symbol)
261-
buf = IOBuffer()
262-
start_fence = "#!$type begin"
263-
end_fence = "#!$type end"
261+
# Read all lines
262+
lines = readlines(path)
263+
264+
# For manifest, read backwards by reversing the lines
265+
if type === :manifest
266+
lines = reverse(lines)
267+
start_marker = "#!manifest end"
268+
end_marker = "#!manifest begin"
269+
section_name = "manifest"
270+
position_error = "must come last"
271+
else
272+
start_marker = "#!project begin"
273+
end_marker = "#!project end"
274+
section_name = "project"
275+
position_error = "must come first"
276+
end
277+
264278
state = :none
265-
multiline_mode = false
266-
in_multiline = false
279+
at_start = true
280+
content_lines = String[]
267281

268-
for (lineno, line) in enumerate(eachline(path))
282+
for (lineno, line) in enumerate(lines)
269283
stripped = lstrip(line)
270-
state == :done && break
271284

272-
if startswith(stripped, start_fence)
273-
state = :reading_first
285+
# Skip empty lines and comments (including shebang) before content
286+
if at_start && (isempty(stripped) || startswith(stripped, '#'))
287+
if startswith(stripped, start_marker)
288+
state = :reading
289+
at_start = false
290+
continue
291+
end
274292
continue
275-
elseif startswith(stripped, end_fence)
276-
state = :done
293+
end
294+
295+
# Found start marker after content - error
296+
if startswith(stripped, start_marker)
297+
if !at_start
298+
error("#!$section_name section $position_error in $path")
299+
end
300+
state = :reading
301+
at_start = false
277302
continue
278-
elseif state === :reading_first
279-
# First line determines the format
280-
if startswith(stripped, "#=")
281-
multiline_mode = true
282-
state = :reading
283-
# Check if the opening #= and closing =# are on the same line
284-
if endswith(rstrip(stripped), "=#")
285-
# Single-line multi-line comment
286-
content = rstrip(stripped)[3:end-2]
287-
write(buf, content)
288-
in_multiline = false
289-
else
290-
# Multi-line comment continues
291-
in_multiline = true
292-
content = stripped[3:end] # Remove #= from start
293-
write(buf, content)
294-
write(buf, '\n')
295-
end
303+
end
304+
305+
at_start = false
306+
307+
# Found end marker
308+
if startswith(stripped, end_marker) && state === :reading
309+
state = :done
310+
break
311+
end
312+
313+
# Extract content
314+
if state === :reading
315+
if startswith(stripped, '#')
316+
toml_line = lstrip(chop(stripped, head=1, tail=0))
317+
push!(content_lines, toml_line)
296318
else
297-
# Line-by-line format
298-
multiline_mode = false
299-
state = :reading
300-
# Process this first line
301-
if startswith(stripped, '#')
302-
toml_line = lstrip(chop(stripped, head=1, tail=0))
303-
write(buf, toml_line)
304-
else
305-
write(buf, line)
306-
end
307-
write(buf, '\n')
319+
push!(content_lines, line)
308320
end
309-
elseif state === :reading
310-
if multiline_mode && in_multiline
311-
# In multi-line comment mode, look for closing =#
312-
if endswith(rstrip(stripped), "=#")
313-
# Found closing delimiter
314-
content = rstrip(stripped)[1:end-2] # Remove =# from end
315-
write(buf, content)
316-
in_multiline = false
317-
else
318-
# Still inside multi-line comment
319-
write(buf, line)
320-
write(buf, '\n')
321-
end
322-
elseif !multiline_mode
323-
# Line-by-line comment mode, strip # from each line
324-
if startswith(stripped, '#')
325-
toml_line = lstrip(chop(stripped, head=1, tail=0))
326-
write(buf, toml_line)
327-
else
328-
write(buf, line)
329-
end
330-
write(buf, '\n')
331-
end
332-
# If multiline_mode && !in_multiline, the multiline comment has ended.
333-
# Don't accumulate any more content; just wait for the end fence.
334321
end
335322
end
336323

324+
# For manifest, reverse the content back to original order
325+
if type === :manifest && !isempty(content_lines)
326+
content_lines = reverse(content_lines)
327+
end
328+
337329
if state === :done
338-
return strip(String(take!(buf)))
330+
return strip(join(content_lines, '\n'))
339331
elseif state === :none
340332
return ""
341333
else
342-
error("incomplete inline $type block in $path (missing #!$type end)")
334+
error("incomplete inline $section_name block in $path (missing #!$section_name end)")
343335
end
344336
end
345337

doc/src/manual/code-loading.md

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -410,31 +410,18 @@ Julia also understands *portable scripts*: scripts that embed their own `Project
410410
# Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
411411
#!project end
412412

413+
using Markdown
414+
println(md"# Hello, single-file world!")
415+
413416
#!manifest begin
414417
# [[deps]]
415418
# name = "Markdown"
416419
# uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
417420
# version = "1.0.0"
418421
#!manifest end
419-
420-
using Markdown
421-
println(md"# Hello, single-file world!")
422-
```
423-
424-
Lines inside the fenced blocks may either start with `#` (as in the example), be plain TOML, or be wrapped in multi-line comment delimiters `#= ... =#`:
425-
426-
```julia
427-
#!project begin
428-
#=
429-
name = "HelloApp"
430-
uuid = "9c5fa7d8-7220-48e8-b2f7-0042191c5f6d"
431-
version = "0.1.0"
432-
[deps]
433-
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
434-
=#
435-
#!project end
436422
```
437423

424+
Lines inside the fenced blocks should be commented with `#` (as in the example) or be plain TOML lines. The `#!project` section must come first in the file (after an optional shebang and empty lines). If a `#!manifest` section is present, it must come after the `#!project` section, and no Julia code is allowed after the `#!manifest end` delimiter.
438425

439426
Running `julia hello.jl` automatically activates the embedded project. The script path becomes the active project entry in `LOAD_PATH`, so package loading works exactly as if `Project.toml` and `Manifest.toml` lived next to the script. The `--project=@script` flag also expands to the script itself when no on-disk project exists but inline metadata is present.
440427

test/loading.jl

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1885,16 +1885,6 @@ module M58272_to end
18851885
@test occursin("Portable Script Tests", output)
18861886
@test occursin("Pass", output)
18871887

1888-
# Test with multiline comment syntax
1889-
portable_script_ml = joinpath(@__DIR__, "project", "portable_script_multiline.jl")
1890-
output_ml = read(`$(Base.julia_cmd()) --startup-file=no $portable_script_ml`, String)
1891-
1892-
@test occursin("Active project: $portable_script_ml", output_ml)
1893-
@test occursin("Active manifest: $portable_script_ml", output_ml)
1894-
@test occursin("✓ Portable script with multiline comment syntax works!", output_ml)
1895-
@test occursin("✓ Random.rand()", output_ml)
1896-
@test occursin("✓ All checks passed!", output_ml)
1897-
18981888
# Test with custom manifest= entry in project section
18991889
portable_script_cm = joinpath(@__DIR__, "project", "portable_script_custom_manifest.jl")
19001890
output_cm = read(`$(Base.julia_cmd()) --startup-file=no $portable_script_cm`, String)
@@ -1912,4 +1902,47 @@ module M58272_to end
19121902
@test occursin("Active project: $portable_script", output_script)
19131903
@test occursin("Active manifest: $portable_script", output_script)
19141904
@test occursin("✓ Random (stdlib) loaded successfully", output_script)
1905+
1906+
# Test that regular Julia files (without inline sections) work fine as projects
1907+
regular_script = joinpath(@__DIR__, "project", "regular_script.jl")
1908+
1909+
# Running the script with --project= should set it as active project
1910+
output = read(`$(Base.julia_cmd()) --startup-file=no --project=$regular_script $regular_script`, String)
1911+
@test occursin("ACTIVE_PROJECT: $regular_script", output)
1912+
@test occursin("Hello from regular script", output)
1913+
@test occursin("x = 42", output)
1914+
1915+
# Running the script without --project should NOT set it as active project
1916+
output = read(`$(Base.julia_cmd()) --startup-file=no $regular_script`, String)
1917+
@test !occursin("ACTIVE_PROJECT: $regular_script", output)
1918+
@test occursin("Hello from regular script", output)
1919+
@test occursin("x = 42", output)
1920+
1921+
# Test 1: Project section not first (has code before it)
1922+
invalid_project_not_first = joinpath(@__DIR__, "project", "invalid_project_not_first.jl")
1923+
err_output = IOBuffer()
1924+
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no $invalid_project_not_first`), stderr=err_output))
1925+
@test !success(result)
1926+
@test occursin("#!project section must come first", String(take!(err_output)))
1927+
1928+
# Test 2: Manifest section not last (has code after it)
1929+
invalid_manifest_not_last = joinpath(@__DIR__, "project", "invalid_manifest_not_last.jl")
1930+
err_output = IOBuffer()
1931+
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no $invalid_manifest_not_last`), stderr=err_output))
1932+
@test !success(result)
1933+
@test occursin("#!manifest section must come last", String(take!(err_output)))
1934+
1935+
# Test 3: Project not first, but manifest present
1936+
invalid_both = joinpath(@__DIR__, "project", "invalid_both.jl")
1937+
err_output = IOBuffer()
1938+
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no --project=$invalid_both -e "using Test"`), stderr=err_output))
1939+
@test !success(result)
1940+
@test occursin("#!project section must come first", String(take!(err_output)))
1941+
1942+
# Test 4: Manifest with code in between sections
1943+
invalid_code_between = joinpath(@__DIR__, "project", "invalid_code_between.jl")
1944+
err_output = IOBuffer()
1945+
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no --project=$invalid_code_between -e "using Test"`), stderr=err_output))
1946+
@test !success(result)
1947+
@test occursin("#!manifest section must come last", String(take!(err_output)))
19151948
end

test/project/invalid_both.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Test
2+
3+
function foo()
4+
return 42
5+
end
6+
7+
#!project begin
8+
#!project end
9+
10+
#!manifest begin
11+
#!manifest end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!project begin
2+
#!project end
3+
4+
using Test
5+
6+
# Some actual Julia code
7+
function bar()
8+
println("hello")
9+
end
10+
11+
#!manifest begin
12+
#!manifest end
13+
14+
bar()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!project begin
2+
# [deps]
3+
# Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
4+
#!project end
5+
6+
using Test
7+
8+
#!manifest begin
9+
# julia_version = "1.11.0"
10+
#!manifest end
11+
12+
println("Code after manifest")
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Some code before project section
2+
3+
using Test
4+
x = 1
5+
6+
#!project begin
7+
# name = "Test"
8+
#!project end
9+
10+
println("test")

test/project/portable_script.jl

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,35 @@
1010
# Rot13 = "43ef800a-eac4-47f4-949b-25107b932e8f"
1111
#!project end
1212

13+
using Random
14+
using Test
15+
using Rot13
16+
17+
# Verify the portable script environment is active
18+
println("Active project: ", Base.active_project())
19+
println("Active manifest: ", Base.active_manifest())
20+
println()
21+
22+
# Test that stdlib packages work
23+
@testset "Portable Script Tests" begin
24+
# Test Random (stdlib)
25+
Random.seed!(42)
26+
r = rand()
27+
@test 0 <= r <= 1
28+
println("✓ Random (stdlib) loaded successfully")
29+
30+
# Test Rot13 (path-based dependency)
31+
@test Rot13.rot13("Hello") == "Uryyb"
32+
@test Rot13.rot13("World") == "Jbeyq"
33+
println("✓ Rot13 (path dependency) loaded successfully")
34+
35+
# Test that Rot13 module has expected functions
36+
@test hasmethod(Rot13.rot13, (Char,))
37+
@test hasmethod(Rot13.rot13, (AbstractString,))
38+
println("✓ Rot13 methods available")
39+
end
40+
41+
1342
#!manifest begin
1443
# julia_version = "1.13.0"
1544
# manifest_format = "2.0"
@@ -59,32 +88,4 @@
5988
# path = "Rot13"
6089
# uuid = "43ef800a-eac4-47f4-949b-25107b932e8f"
6190
# version = "0.1.0"
62-
#!manifest end
63-
64-
using Random
65-
using Test
66-
using Rot13
67-
68-
# Verify the portable script environment is active
69-
println("Active project: ", Base.active_project())
70-
println("Active manifest: ", Base.active_manifest())
71-
println()
72-
73-
# Test that stdlib packages work
74-
@testset "Portable Script Tests" begin
75-
# Test Random (stdlib)
76-
Random.seed!(42)
77-
r = rand()
78-
@test 0 <= r <= 1
79-
println("✓ Random (stdlib) loaded successfully")
80-
81-
# Test Rot13 (path-based dependency)
82-
@test Rot13.rot13("Hello") == "Uryyb"
83-
@test Rot13.rot13("World") == "Jbeyq"
84-
println("✓ Rot13 (path dependency) loaded successfully")
85-
86-
# Test that Rot13 module has expected functions
87-
@test hasmethod(Rot13.rot13, (Char,))
88-
@test hasmethod(Rot13.rot13, (AbstractString,))
89-
println("✓ Rot13 methods available")
90-
end
91+
#!manifest end

0 commit comments

Comments
 (0)