Skip to content

Commit 28ac455

Browse files
author
KristofferC
committed
use a !# script maker instead, avoid the term "portable"
1 parent 33be86e commit 28ac455

14 files changed

+122
-113
lines changed

base/client.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ function exec_options(opts)
277277

278278
if arg_is_program && PROGRAM_FILE != "-" && Base.active_project(false) === nothing
279279
script_path = abspath(PROGRAM_FILE)
280-
Base.has_inline_project(script_path) && Base.set_active_project(script_path)
280+
Base.is_script(script_path) && Base.set_active_project(script_path)
281281
end
282282

283283
# Load Distributed module only if any of the Distributed options have been specified.
@@ -347,12 +347,12 @@ function exec_options(opts)
347347
include_string(Main, read(stdin, String), "stdin")
348348
else
349349
abs_script_path = abspath(PROGRAM_FILE)
350-
if has_inline_project(abs_script_path)
351-
set_portable_script_state(abs_script_path)
350+
if is_script(abs_script_path)
351+
set_script_state(abs_script_path)
352352
try
353353
include(Main, PROGRAM_FILE)
354354
finally
355-
global portable_script_state_global = nothing
355+
global script_state_global = nothing
356356
end
357357
else
358358
include(Main, PROGRAM_FILE)

base/initdefs.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,8 @@ function load_path_expand(env::AbstractString)::Union{String, Nothing}
288288
program_file = program_file != C_NULL ? unsafe_string(program_file) : nothing
289289
isnothing(program_file) && return nothing # User did not pass a script
290290

291-
# Check if the program file itself is a portable script first
292-
if env == "@script" && Base.has_inline_project(program_file)
291+
# Check if the program file itself is a script first
292+
if env == "@script" && Base.is_script(program_file)
293293
return abspath(program_file)
294294
end
295295

@@ -333,7 +333,7 @@ load_path_expand(::Nothing) = nothing
333333
active_project()
334334
335335
Return the path of the active project (either a `Project.toml` file or a julia
336-
file when using a [portable script](@ref portable-scripts)).
336+
file when using a [script](@ref scripts)).
337337
See also [`Base.set_active_project`](@ref).
338338
"""
339339
function active_project(search_load_path::Bool=true)
@@ -364,7 +364,7 @@ end
364364
set_active_project(projfile::Union{AbstractString,Nothing})
365365
366366
Set the active `Project.toml` file to `projfile`. The `projfile` can be a path to a traditional
367-
`Project.toml` file, a [portable script](@ref portable-scripts) with inline metadata, or `nothing`
367+
`Project.toml` file, a [script](@ref scripts) with inline metadata, or `nothing`
368368
to clear the active project. See also [`Base.active_project`](@ref).
369369
370370
!!! compat "Julia 1.8"
@@ -386,7 +386,7 @@ end
386386
active_manifest(project_file::AbstractString)
387387
388388
Return the path of the active manifest file, or the manifest file that would be used for a given `project_file`.
389-
When a [portable script](@ref portable-scripts) is active, this returns the script path itself.
389+
When a [script](@ref scripts) is active, this returns the script path itself.
390390
391391
In a stacked environment (where multiple environments exist in the load path), this returns the manifest
392392
file for the primary (active) environment only, not the manifests from other environments in the stack.

base/loading.jl

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -261,49 +261,37 @@ function extract_inline_section(path::String, type::Symbol)
261261
# Read all lines
262262
lines = readlines(path)
263263

264-
# For manifest, read backwards by reversing the lines
265264
if type === :manifest
266-
lines = reverse(lines)
267-
start_marker = "#!manifest end"
268-
end_marker = "#!manifest begin"
265+
start_marker = "#!manifest begin"
266+
end_marker = "#!manifest end"
269267
section_name = "manifest"
270-
position_error = "must come last"
271268
else
272269
start_marker = "#!project begin"
273270
end_marker = "#!project end"
274271
section_name = "project"
275-
position_error = "must come first"
276272
end
277273

278274
state = :none
279-
at_start = true
280275
content_lines = String[]
276+
project_line = nothing
277+
manifest_line = nothing
281278

282279
for (lineno, line) in enumerate(lines)
283280
stripped = lstrip(line)
284281

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
292-
continue
282+
# Track positions of sections for validation
283+
if startswith(stripped, "#!project begin")
284+
project_line = lineno
285+
elseif startswith(stripped, "#!manifest begin")
286+
manifest_line = lineno
293287
end
294288

295-
# Found start marker after content - error
289+
# Found start marker
296290
if startswith(stripped, start_marker)
297-
if !at_start
298-
error("#!$section_name section $position_error in $path")
299-
end
300291
state = :reading
301-
at_start = false
302292
continue
303293
end
304294

305-
at_start = false
306-
307295
# Found end marker
308296
if startswith(stripped, end_marker) && state === :reading
309297
state = :done
@@ -321,9 +309,9 @@ function extract_inline_section(path::String, type::Symbol)
321309
end
322310
end
323311

324-
# For manifest, reverse the content back to original order
325-
if type === :manifest && !isempty(content_lines)
326-
content_lines = reverse(content_lines)
312+
# Validate that project comes before manifest
313+
if project_line !== nothing && manifest_line !== nothing && project_line > manifest_line
314+
error("#!manifest section must come after #!project section in $path")
327315
end
328316

329317
if state === :done
@@ -335,32 +323,36 @@ function extract_inline_section(path::String, type::Symbol)
335323
end
336324
end
337325

338-
function has_inline_project(path::String)::Bool
326+
function is_script(path::String)::Bool
339327
for line in eachline(path)
340328
stripped = lstrip(line)
341-
if startswith(stripped, "#!project begin")
329+
# Only whitespace and comments allowed before #!script
330+
if !isempty(stripped) && !startswith(stripped, '#')
331+
return false
332+
end
333+
if startswith(stripped, "#!script")
342334
return true
343335
end
344336
end
345337
return false
346338
end
347339

348340

349-
struct PortableScriptState
341+
struct ScriptState
350342
path::String
351343
pkg::PkgId
352344
end
353345

354-
portable_script_state_global::Union{PortableScriptState, Nothing} = nothing
346+
script_state_global::Union{ScriptState, Nothing} = nothing
355347

356-
function set_portable_script_state(abs_path::Union{Nothing, String})
348+
function set_script_state(abs_path::Union{Nothing, String})
357349
pkg = project_file_name_uuid(abs_path, splitext(basename(abs_path))[1])
358350

359351
# Verify the project and manifest delimiters:
360352
parsed_toml(abs_path)
361353
parsed_toml(abs_path; manifest=true)
362354

363-
global portable_script_state_global = PortableScriptState(abs_path, pkg)
355+
global script_state_global = ScriptState(abs_path, pkg)
364356
end
365357

366358

@@ -393,7 +385,7 @@ function parsed_toml(toml_file::AbstractString, toml_cache::TOMLCache, toml_lock
393385
manifest::Bool=false, project::Bool=!manifest)
394386
manifest && project && throw(ArgumentError("cannot request both project and manifest TOML"))
395387
lock(toml_lock) do
396-
# Portable script?
388+
# Script?
397389
if endswith(toml_file, ".jl") && isfile_casesensitive(toml_file)
398390
kind = manifest ? :manifest : :project
399391
cache_key = "$(toml_file)::$(kind)"
@@ -459,8 +451,8 @@ Same as [`Base.identify_package`](@ref) except that the path to the environment
459451
is also returned, except when the identity is not identified.
460452
"""
461453
function identify_package_env(where::Module, name::String)
462-
if where === Main && portable_script_state_global !== nothing
463-
return identify_package_env(portable_script_state_global.pkg, name)
454+
if where === Main && script_state_global !== nothing
455+
return identify_package_env(script_state_global.pkg, name)
464456
end
465457
return identify_package_env(PkgId(where), name)
466458
end
@@ -788,7 +780,11 @@ function env_project_file(env::String)::Union{Bool,String}
788780
elseif basename(env) in project_names && isfile_casesensitive(env)
789781
project_file = env
790782
elseif endswith(env, ".jl") && isfile_casesensitive(env)
791-
project_file = has_inline_project(env) ? env : false
783+
if is_script(env)
784+
project_file = env
785+
else
786+
error("$env is missing #!script marker)")
787+
end
792788
else
793789
project_file = false
794790
end
@@ -1046,7 +1042,7 @@ function project_file_manifest_path(project_file::String)::Union{Nothing,String}
10461042
end
10471043
end
10481044
if manifest_path === nothing && endswith(project_file, ".jl") && has_file
1049-
# portable script: manifest is the same file as the project file
1045+
# script: manifest is the same file as the project file
10501046
manifest_path = project_file
10511047
end
10521048
if manifest_path === nothing

doc/src/manual/code-loading.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -397,11 +397,17 @@ are stored in the manifest file in the section for that package. The dependency
397397
a package are the same as for its "parent" except that the listed triggers are also considered as
398398
dependencies.
399399

400-
### [Portable scripts](@id portable-scripts)
400+
### [Scripts](@id scripts)
401401

402-
Julia also understands *portable scripts*: scripts that embed their own `Project.toml` (and optionally `Manifest.toml`) so they can be executed as self-contained environments. To do this, place TOML data inside comment fences named `#!project` and `#!manifest`:
402+
Julia also understands *scripts* that can embed their own `Project.toml` (and optionally `Manifest.toml`) so they can be executed as self-contained environments. A script is identified by a `#!script` marker at the top of the file (only whitespace and comments may appear before it). The embedded project and manifest data are placed inside comment fences named `#!project` and `#!manifest`:
403403

404404
```julia
405+
#!/usr/bin/env julia
406+
#!script
407+
408+
using Markdown
409+
println(md"# Hello, single-file world!")
410+
405411
#!project begin
406412
# name = "HelloApp"
407413
# uuid = "9c5fa7d8-7220-48e8-b2f7-0042191c5f6d"
@@ -410,9 +416,6 @@ Julia also understands *portable scripts*: scripts that embed their own `Project
410416
# Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
411417
#!project end
412418

413-
using Markdown
414-
println(md"# Hello, single-file world!")
415-
416419
#!manifest begin
417420
# [[deps]]
418421
# name = "Markdown"
@@ -421,9 +424,9 @@ println(md"# Hello, single-file world!")
421424
#!manifest end
422425
```
423426

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.
427+
Lines inside the fenced blocks should be commented with `#` (as in the example) or be plain TOML lines. The `#!script` marker must appear at the top of the file, with only whitespace and comments (including an optional shebang) allowed before it. The `#!project` and `#!manifest` sections can appear anywhere in the file (by convention at the bottom), but `#!project` must come before `#!manifest` if both are present.
425428

426-
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.
429+
Running `julia hello.jl` automatically activates the embedded project if the file contains a `#!script` marker. The dependency loading rules for such a script is the same as for a package with the same project and manifest file. The `--project=@script` flag also expands to the script itself if the `#!script` marker is present. Using `--project=script.jl` explicitly requires that the script contains the `#!script` marker.
427430

428431
### [Workspaces](@id workspaces)
429432

test/loading.jl

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,40 +1871,37 @@ module M58272_to end
18711871
@eval M58272_to import ..M58272_1: M58272_2.y, x
18721872
@test @eval M58272_to x === 1
18731873

1874-
@testset "Portable scripts" begin
1874+
@testset "Scripts" begin
18751875
# Test with line-by-line comment syntax and path dependencies
1876-
portable_script = joinpath(@__DIR__, "project", "portable", "portable_script.jl")
1877-
output = read(`$(Base.julia_cmd()) --startup-file=no $portable_script`, String)
1876+
script = joinpath(@__DIR__, "project", "scripts", "script.jl")
1877+
output = read(`$(Base.julia_cmd()) --startup-file=no $script`, String)
18781878

1879-
@test occursin("Active project: $portable_script", output)
1880-
@test occursin("Active manifest: $portable_script", output)
1879+
@test occursin("Active project: $script", output)
1880+
@test occursin("Active manifest: $script", output)
18811881
@test occursin("✓ Random (stdlib) loaded successfully", output)
18821882
@test occursin("✓ Rot13 (path dependency) loaded successfully", output)
18831883
@test occursin("✓ Rot13 methods available", output)
1884-
@test occursin("Test Summary:", output)
1885-
@test occursin("Portable Script Tests", output)
1886-
@test occursin("Pass", output)
18871884

18881885
# Test with custom manifest= entry in project section
1889-
portable_script_cm = joinpath(@__DIR__, "project", "portable", "portable_script_custom_manifest.jl")
1890-
output_cm = read(`$(Base.julia_cmd()) --startup-file=no $portable_script_cm`, String)
1891-
expected_cm = joinpath(@__DIR__, "project", "portable", "portable_script_custom.toml")
1886+
script_cm = joinpath(@__DIR__, "project", "scripts", "script_custom_manifest.jl")
1887+
output_cm = read(`$(Base.julia_cmd()) --startup-file=no $script_cm`, String)
1888+
expected_cm = joinpath(@__DIR__, "project", "scripts", "script_custom.toml")
18921889

1893-
@test occursin("Active project: $portable_script_cm", output_cm)
1890+
@test occursin("Active project: $script_cm", output_cm)
18941891
@test occursin("Active manifest: $expected_cm", output_cm)
18951892
@test occursin("✓ Custom manifest file is being used: $expected_cm", output_cm)
18961893
@test occursin("✓ Random.rand()", output_cm)
18971894
@test occursin("✓ All checks passed!", output_cm)
18981895

1899-
# Test @script behavior with portable script
1896+
# Test @script behavior with script
19001897
# When using --project=@script, it should use the script file as the project
1901-
output_script = read(`$(Base.julia_cmd()) --startup-file=no --project=@script $portable_script`, String)
1902-
@test occursin("Active project: $portable_script", output_script)
1903-
@test occursin("Active manifest: $portable_script", output_script)
1898+
output_script = read(`$(Base.julia_cmd()) --startup-file=no --project=@script $script`, String)
1899+
@test occursin("Active project: $script", output_script)
1900+
@test occursin("Active manifest: $script", output_script)
19041901
@test occursin("✓ Random (stdlib) loaded successfully", output_script)
19051902

1906-
# Test that regular Julia files (without inline sections) work fine as projects
1907-
regular_script = joinpath(@__DIR__, "project", "portable", "regular_script.jl")
1903+
# Test that regular Julia files (without #!script) can be set as active project
1904+
regular_script = joinpath(@__DIR__, "project", "scripts", "regular_script.jl")
19081905

19091906
# Running the script with --project= should set it as active project
19101907
output = read(`$(Base.julia_cmd()) --startup-file=no --project=$regular_script $regular_script`, String)
@@ -1918,37 +1915,40 @@ module M58272_to end
19181915
@test occursin("Hello from regular script", output)
19191916
@test occursin("x = 42", output)
19201917

1921-
portable_script_missing = joinpath(@__DIR__, "project", "portable", "portable_script_missing_dep.jl")
1918+
script_missing = joinpath(@__DIR__, "project", "scripts", "script_missing_dep.jl")
19221919
err_output = IOBuffer()
1923-
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no $portable_script_missing`), stderr=err_output))
1920+
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no $script_missing`), stderr=err_output))
19241921
@test !success(result)
19251922
@test occursin("Package Rot13 not found in current path", String(take!(err_output)))
19261923

1927-
# Test 1: Project section not first (has code before it)
1928-
invalid_project_not_first = joinpath(@__DIR__, "project", "portable", "invalid_project_not_first.jl")
1929-
err_output = IOBuffer()
1930-
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no $invalid_project_not_first`), stderr=err_output))
1931-
@test !success(result)
1932-
@test occursin("#!project section must come first", String(take!(err_output)))
1924+
# Test 1: #!script marker not at top (has code before it) - runs as regular script
1925+
invalid_project_not_first = joinpath(@__DIR__, "project", "scripts", "invalid_project_not_first.jl")
1926+
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no $invalid_project_not_first`)))
1927+
@test success(result)
19331928

1934-
# Test 2: Manifest section not last (has code after it)
1935-
invalid_manifest_not_last = joinpath(@__DIR__, "project", "portable", "invalid_manifest_not_last.jl")
1929+
# Test 2: Manifest section before project section - should error when parsed
1930+
invalid_manifest_not_last = joinpath(@__DIR__, "project", "scripts", "invalid_manifest_not_last.jl")
19361931
err_output = IOBuffer()
19371932
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no $invalid_manifest_not_last`), stderr=err_output))
19381933
@test !success(result)
1939-
@test occursin("#!manifest section must come last", String(take!(err_output)))
1934+
@test occursin("#!manifest section must come after #!project section", String(take!(err_output)))
19401935

1941-
# Test 3: Project not first, but manifest present
1942-
invalid_both = joinpath(@__DIR__, "project", "portable", "invalid_both.jl")
1936+
# Test 3: Code before #!script marker with --project - should error
1937+
invalid_both = joinpath(@__DIR__, "project", "scripts", "invalid_both.jl")
19431938
err_output = IOBuffer()
19441939
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no --project=$invalid_both -e "using Test"`), stderr=err_output))
19451940
@test !success(result)
1946-
@test occursin("#!project section must come first", String(take!(err_output)))
1941+
@test occursin("is missing #!script marker", String(take!(err_output)))
1942+
1943+
# Test 4: Code between sections is now valid
1944+
valid_code_between = joinpath(@__DIR__, "project", "scripts", "valid_code_between.jl")
1945+
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no $valid_code_between`)))
1946+
@test success(result)
19471947

1948-
# Test 4: Manifest with code in between sections
1949-
invalid_code_between = joinpath(@__DIR__, "project", "portable", "invalid_code_between.jl")
1948+
# Test 5: Using --project on a non-script file errors when loading packages
1949+
regular_script = joinpath(@__DIR__, "project", "scripts", "regular_script.jl")
19501950
err_output = IOBuffer()
1951-
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no --project=$invalid_code_between -e "using Test"`), stderr=err_output))
1951+
result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no --project=$regular_script -e "using Test"`), stderr=err_output))
19521952
@test !success(result)
1953-
@test occursin("#!manifest section must come last", String(take!(err_output)))
1953+
@test occursin("is missing #!script marker", String(take!(err_output)))
19541954
end

test/project/portable/invalid_both.jl renamed to test/project/scripts/invalid_both.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ function foo()
22
return 42
33
end
44

5+
#!script
6+
57
#!project begin
68
#!project end
79

0 commit comments

Comments
 (0)