diff --git a/base/client.jl b/base/client.jl index 1b62f870ae1b8..a5e72dceabe0a 100644 --- a/base/client.jl +++ b/base/client.jl @@ -275,6 +275,11 @@ function exec_options(opts) # remove filename from ARGS global PROGRAM_FILE = arg_is_program ? popfirst!(ARGS) : "" + if arg_is_program && PROGRAM_FILE != "-" && Base.active_project(false) === nothing + script_path = abspath(PROGRAM_FILE) + Base.is_script(script_path) && Base.set_active_project(script_path) + end + # Load Distributed module only if any of the Distributed options have been specified. distributed_mode = (opts.worker == 1) || (opts.nprocs > 0) || (opts.machine_file != C_NULL) if distributed_mode @@ -341,7 +346,17 @@ function exec_options(opts) if PROGRAM_FILE == "-" include_string(Main, read(stdin, String), "stdin") else - include(Main, PROGRAM_FILE) + abs_script_path = abspath(PROGRAM_FILE) + if is_script(abs_script_path) + set_script_state(abs_script_path) + try + include(Main, PROGRAM_FILE) + finally + global script_state_global = nothing + end + else + include(Main, PROGRAM_FILE) + end end catch invokelatest(display_error, scrub_repl_backtrace(current_exceptions())) diff --git a/base/initdefs.jl b/base/initdefs.jl index 2c9ded9c4033e..109c9edb7fe61 100644 --- a/base/initdefs.jl +++ b/base/initdefs.jl @@ -288,10 +288,16 @@ function load_path_expand(env::AbstractString)::Union{String, Nothing} program_file = program_file != C_NULL ? unsafe_string(program_file) : nothing isnothing(program_file) && return nothing # User did not pass a script + # Check if the program file itself is a script first + if env == "@script" && Base.is_script(program_file) + return abspath(program_file) + end + # Expand trailing relative path dir = dirname(program_file) dir = env != "@script" ? (dir * env[length("@script")+1:end]) : dir - return current_project(dir) + project = current_project(dir) + return project end env = replace(env, '#' => VERSION.major, count=1) env = replace(env, '#' => VERSION.minor, count=1) @@ -326,7 +332,9 @@ load_path_expand(::Nothing) = nothing """ active_project() -Return the path of the active `Project.toml` file. See also [`Base.set_active_project`](@ref). +Return the path of the active project (either a `Project.toml` file or a julia +file when using a [script](@ref scripts)). +See also [`Base.set_active_project`](@ref). """ function active_project(search_load_path::Bool=true) for project in (ACTIVE_PROJECT[],) @@ -355,7 +363,9 @@ end """ set_active_project(projfile::Union{AbstractString,Nothing}) -Set the active `Project.toml` file to `projfile`. See also [`Base.active_project`](@ref). +Set the active `Project.toml` file to `projfile`. The `projfile` can be a path to a traditional +`Project.toml` file, a [script](@ref scripts) with inline metadata, or `nothing` +to clear the active project. See also [`Base.active_project`](@ref). !!! compat "Julia 1.8" This function requires at least Julia 1.8. @@ -376,6 +386,7 @@ end active_manifest(project_file::AbstractString) Return the path of the active manifest file, or the manifest file that would be used for a given `project_file`. +When a [script](@ref scripts) is active, this returns the script path itself. In a stacked environment (where multiple environments exist in the load path), this returns the manifest file for the primary (active) environment only, not the manifests from other environments in the stack. diff --git a/base/loading.jl b/base/loading.jl index 1721969cfbf01..68074f2a359dd 100644 --- a/base/loading.jl +++ b/base/loading.jl @@ -212,22 +212,20 @@ mutable struct CachedTOMLDict size::Int64 hash::UInt32 d::Dict{String, Any} + kind::Symbol # :full (regular TOML), :project, :manifest (inline) end -function CachedTOMLDict(p::TOML.Parser, path::String) +function CachedTOMLDict(p::TOML.Parser, path::String, kind) s = stat(path) - content = read(path) + content = if kind === :full + String(read(path)) + else + String(extract_inline_section(path, kind)) + end crc32 = _crc32c(content) - TOML.reinit!(p, String(content); filepath=path) + TOML.reinit!(p, content; filepath=path) d = TOML.parse(p) - return CachedTOMLDict( - path, - s.inode, - s.mtime, - s.size, - crc32, - d, - ) + return CachedTOMLDict(path, s.inode, s.mtime, s.size, crc32, d, kind) end function get_updated_dict(p::TOML.Parser, f::CachedTOMLDict) @@ -236,20 +234,128 @@ function get_updated_dict(p::TOML.Parser, f::CachedTOMLDict) # identical but that is solvable by not doing in-place updates, and not # rapidly changing these files if s.inode != f.inode || s.mtime != f.mtime || f.size != s.size - content = read(f.path) - new_hash = _crc32c(content) + file_content = read(f.path) + new_hash = _crc32c(file_content) if new_hash != f.hash f.inode = s.inode f.mtime = s.mtime f.size = s.size f.hash = new_hash - TOML.reinit!(p, String(content); filepath=f.path) + + # Extract the appropriate TOML content based on kind + toml_content = if f.kind == :full + String(file_content) + else + String(extract_inline_section(f.path, f.kind)) + end + + TOML.reinit!(p, toml_content; filepath=f.path) return f.d = TOML.parse(p) end end return f.d end + +function extract_inline_section(path::String, type::Symbol) + # Read all lines + lines = readlines(path) + + if type === :manifest + start_marker = "#!manifest begin" + end_marker = "#!manifest end" + section_name = "manifest" + else + start_marker = "#!project begin" + end_marker = "#!project end" + section_name = "project" + end + + state = :none + content_lines = String[] + project_line = nothing + manifest_line = nothing + + for (lineno, line) in enumerate(lines) + stripped = lstrip(line) + + # Track positions of sections for validation + if startswith(stripped, "#!project begin") + project_line = lineno + elseif startswith(stripped, "#!manifest begin") + manifest_line = lineno + end + + # Found start marker + if startswith(stripped, start_marker) + state = :reading + continue + end + + # Found end marker + if startswith(stripped, end_marker) && state === :reading + state = :done + break + end + + # Extract content + if state === :reading + if startswith(stripped, '#') + toml_line = lstrip(chop(stripped, head=1, tail=0)) + push!(content_lines, toml_line) + else + push!(content_lines, line) + end + end + end + + # Validate that project comes before manifest + if project_line !== nothing && manifest_line !== nothing && project_line > manifest_line + error("#!manifest section must come after #!project section in $path") + end + + if state === :done + return strip(join(content_lines, '\n')) + elseif state === :none + return "" + else + error("incomplete inline $section_name block in $path (missing #!$section_name end)") + end +end + +function is_script(path::String)::Bool + for line in eachline(path) + stripped = lstrip(line) + # Only whitespace and comments allowed before #!script + if !isempty(stripped) && !startswith(stripped, '#') + return false + end + if startswith(stripped, "#!script") + return true + end + end + return false +end + + +struct ScriptState + path::String + pkg::PkgId +end + +script_state_global::Union{ScriptState, Nothing} = nothing + +function set_script_state(abs_path::Union{Nothing, String}) + pkg = project_file_name_uuid(abs_path, splitext(basename(abs_path))[1]) + + # Verify the project and manifest delimiters: + parsed_toml(abs_path) + parsed_toml(abs_path; manifest=true) + + global script_state_global = ScriptState(abs_path, pkg) +end + + struct LoadingCache load_path::Vector{String} dummy_uuid::Dict{String, UUID} @@ -273,26 +379,38 @@ TOMLCache(p::TOML.Parser, d::Dict{String, Dict{String, Any}}) = TOMLCache(p, con const TOML_CACHE = TOMLCache(TOML.Parser{nothing}()) -parsed_toml(project_file::AbstractString) = parsed_toml(project_file, TOML_CACHE, require_lock) -function parsed_toml(project_file::AbstractString, toml_cache::TOMLCache, toml_lock::ReentrantLock) +parsed_toml(toml_file::AbstractString; manifest::Bool=false, project::Bool=!manifest) = + parsed_toml(toml_file, TOML_CACHE, require_lock; manifest=manifest, project=project) +function parsed_toml(toml_file::AbstractString, toml_cache::TOMLCache, toml_lock::ReentrantLock; + manifest::Bool=false, project::Bool=!manifest) + manifest && project && throw(ArgumentError("cannot request both project and manifest TOML")) lock(toml_lock) do + # Script? + if endswith(toml_file, ".jl") && isfile_casesensitive(toml_file) + kind = manifest ? :manifest : :project + cache_key = "$(toml_file)::$(kind)" + else + kind = :full + cache_key = toml_file + end + cache = LOADING_CACHE[] - dd = if !haskey(toml_cache.d, project_file) - d = CachedTOMLDict(toml_cache.p, project_file) - toml_cache.d[project_file] = d + dd = if !haskey(toml_cache.d, cache_key) + d = CachedTOMLDict(toml_cache.p, toml_file, kind) + toml_cache.d[cache_key] = d d.d else - d = toml_cache.d[project_file] + d = toml_cache.d[cache_key] # We are in a require call and have already parsed this TOML file # assume that it is unchanged to avoid hitting disk - if cache !== nothing && project_file in cache.require_parsed + if cache !== nothing && cache_key in cache.require_parsed d.d else get_updated_dict(toml_cache.p, d) end end if cache !== nothing - push!(cache.require_parsed, project_file) + push!(cache.require_parsed, cache_key) end return dd end @@ -332,7 +450,12 @@ end Same as [`Base.identify_package`](@ref) except that the path to the environment where the package is identified is also returned, except when the identity is not identified. """ -identify_package_env(where::Module, name::String) = identify_package_env(PkgId(where), name) +function identify_package_env(where::Module, name::String) + if where === Main && script_state_global !== nothing + return identify_package_env(script_state_global.pkg, name) + end + return identify_package_env(PkgId(where), name) +end function identify_package_env(where::Union{PkgId, Nothing}, name::String) # Special cases if where !== nothing @@ -656,6 +779,8 @@ function env_project_file(env::String)::Union{Bool,String} project_file = locate_project_file(env) elseif basename(env) in project_names && isfile_casesensitive(env) project_file = env + elseif endswith(env, ".jl") && isfile_casesensitive(env) + project_file = is_script(env) ? env : false else project_file = false end @@ -897,7 +1022,8 @@ function project_file_manifest_path(project_file::String)::Union{Nothing,String} manifest_path === missing || return manifest_path end dir = abspath(dirname(project_file)) - isfile_casesensitive(project_file) || return nothing + has_file = isfile_casesensitive(project_file) + has_file || return nothing d = parsed_toml(project_file) base_manifest = workspace_manifest(project_file) if base_manifest !== nothing @@ -911,6 +1037,10 @@ function project_file_manifest_path(project_file::String)::Union{Nothing,String} manifest_path = manifest_file end end + if manifest_path === nothing && endswith(project_file, ".jl") && has_file + # script: manifest is the same file as the project file + manifest_path = project_file + end if manifest_path === nothing for mfst in manifest_names manifest_file = joinpath(dir, mfst) @@ -958,6 +1088,7 @@ end # Find the project file for the extension `ext` in the implicit env `dir`` function implicit_env_project_file_extension(dir::String, ext::PkgId) for pkg in readdir(dir; join=true) + isdir(pkg) || continue project_file = env_project_file(pkg) project_file isa String || continue path = project_file_ext_path(project_file, ext) @@ -1038,7 +1169,7 @@ dep_stanza_get(stanza::Nothing, name::String) = nothing function explicit_manifest_deps_get(project_file::String, where::PkgId, name::String)::Union{Nothing,PkgId} manifest_file = project_file_manifest_path(project_file) manifest_file === nothing && return nothing # manifest not found--keep searching LOAD_PATH - d = get_deps(parsed_toml(manifest_file)) + d = get_deps(parsed_toml(manifest_file; manifest=true)) for (dep_name, entries) in d entries::Vector{Any} for entry in entries @@ -1111,7 +1242,7 @@ function explicit_manifest_uuid_path(project_file::String, pkg::PkgId)::Union{No manifest_file = project_file_manifest_path(project_file) manifest_file === nothing && return nothing # no manifest, skip env - d = get_deps(parsed_toml(manifest_file)) + d = get_deps(parsed_toml(manifest_file; manifest=true)) entries = get(d, pkg.name, nothing)::Union{Nothing, Vector{Any}} if entries !== nothing for entry in entries @@ -1540,7 +1671,7 @@ function insert_extension_triggers(env::String, pkg::PkgId)::Union{Nothing,Missi project_file isa String || return nothing manifest_file = project_file_manifest_path(project_file) manifest_file === nothing && return - d = get_deps(parsed_toml(manifest_file)) + d = get_deps(parsed_toml(manifest_file; manifest=true)) for (dep_name, entries) in d entries::Vector{Any} for entry in entries @@ -2516,7 +2647,7 @@ function find_unsuitable_manifests_versions() project_file isa String || continue # no project file manifest_file = project_file_manifest_path(project_file) manifest_file isa String || continue # no manifest file - m = parsed_toml(manifest_file) + m = parsed_toml(manifest_file; manifest=true) man_julia_version = get(m, "julia_version", nothing) man_julia_version isa String || @goto mark man_julia_version = VersionNumber(man_julia_version) diff --git a/base/precompilation.jl b/base/precompilation.jl index adecc50f6d1e4..82cc966baaa9b 100644 --- a/base/precompilation.jl +++ b/base/precompilation.jl @@ -108,7 +108,7 @@ function ExplicitEnv(envpath::String) end manifest = project_file_manifest_path(envpath) - manifest_d = manifest === nothing ? Dict{String, Any}() : parsed_toml(manifest) + manifest_d = manifest === nothing ? Dict{String, Any}() : parsed_toml(manifest; manifest=true) # Dependencies in a manifest can either be stored compressed (when name is unique among all packages) # in which case it is a `Vector{String}` or expanded where it is a `name => uuid` mapping. diff --git a/doc/src/manual/code-loading.md b/doc/src/manual/code-loading.md index 5871530720d22..e6a0b20651fc6 100644 --- a/doc/src/manual/code-loading.md +++ b/doc/src/manual/code-loading.md @@ -397,6 +397,37 @@ are stored in the manifest file in the section for that package. The dependency a package are the same as for its "parent" except that the listed triggers are also considered as dependencies. +### [Scripts](@id scripts) + +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`: + +```julia +#!/usr/bin/env julia +#!script + +using Markdown +println(md"# Hello, single-file world!") + +#!project begin +# name = "HelloApp" +# uuid = "9c5fa7d8-7220-48e8-b2f7-0042191c5f6d" +# version = "0.1.0" +# [deps] +# Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +#!project end + +#!manifest begin +# [[deps]] +# name = "Markdown" +# uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +# version = "1.0.0" +#!manifest end +``` + +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. + +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. + ### [Workspaces](@id workspaces) A project file can define a workspace by giving a set of projects that is part of that workspace: diff --git a/test/loading.jl b/test/loading.jl index ad5eab3768760..3fad0e9e15751 100644 --- a/test/loading.jl +++ b/test/loading.jl @@ -1870,3 +1870,85 @@ end module M58272_to end @eval M58272_to import ..M58272_1: M58272_2.y, x @test @eval M58272_to x === 1 + +@testset "Scripts" begin + # Test with line-by-line comment syntax and path dependencies + script = joinpath(@__DIR__, "project", "scripts", "script.jl") + output = read(`$(Base.julia_cmd()) --startup-file=no $script`, String) + + @test occursin("Active project: $script", output) + @test occursin("Active manifest: $script", output) + @test occursin("✓ Random (stdlib) loaded successfully", output) + @test occursin("✓ Rot13 (path dependency) loaded successfully", output) + @test occursin("✓ Rot13 methods available", output) + + # Test with custom manifest= entry in project section + script_cm = joinpath(@__DIR__, "project", "scripts", "script_custom_manifest.jl") + output_cm = read(`$(Base.julia_cmd()) --startup-file=no $script_cm`, String) + expected_cm = joinpath(@__DIR__, "project", "scripts", "script_custom.toml") + + @test occursin("Active project: $script_cm", output_cm) + @test occursin("Active manifest: $expected_cm", output_cm) + @test occursin("✓ Custom manifest file is being used: $expected_cm", output_cm) + @test occursin("✓ Random.rand()", output_cm) + @test occursin("✓ All checks passed!", output_cm) + + # Test @script behavior with script + # When using --project=@script, it should use the script file as the project + output_script = read(`$(Base.julia_cmd()) --startup-file=no --project=@script $script`, String) + @test occursin("Active project: $script", output_script) + @test occursin("Active manifest: $script", output_script) + @test occursin("✓ Random (stdlib) loaded successfully", output_script) + + # Test that regular Julia files (without #!script) can be set as active project + regular_script = joinpath(@__DIR__, "project", "scripts", "regular_script.jl") + + # Running the script with --project= should set it as active project + output = read(`$(Base.julia_cmd()) --startup-file=no --project=$regular_script $regular_script`, String) + @test occursin("ACTIVE_PROJECT: $regular_script", output) + @test occursin("Hello from regular script", output) + @test occursin("x = 42", output) + + # Running the script without --project should NOT set it as active project + output = read(`$(Base.julia_cmd()) --startup-file=no $regular_script`, String) + @test !occursin("ACTIVE_PROJECT: $regular_script", output) + @test occursin("Hello from regular script", output) + @test occursin("x = 42", output) + + script_missing = joinpath(@__DIR__, "project", "scripts", "script_missing_dep.jl") + err_output = IOBuffer() + result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no $script_missing`), stderr=err_output)) + @test !success(result) + @test occursin("Package Rot13 not found in current path", String(take!(err_output))) + + # Test 1: #!script marker not at top (has code before it) - runs as regular script + invalid_project_not_first = joinpath(@__DIR__, "project", "scripts", "invalid_project_not_first.jl") + result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no $invalid_project_not_first`))) + @test success(result) + + # Test 2: Manifest section before project section - should error when parsed + invalid_manifest_not_last = joinpath(@__DIR__, "project", "scripts", "invalid_manifest_not_last.jl") + err_output = IOBuffer() + result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no $invalid_manifest_not_last`), stderr=err_output)) + @test !success(result) + @test occursin("#!manifest section must come after #!project section", String(take!(err_output))) + + # Test 3: Code before #!script marker with --project - should error + invalid_both = joinpath(@__DIR__, "project", "scripts", "invalid_both.jl") + err_output = IOBuffer() + result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no --project=$invalid_both -e "using Test"`), stderr=err_output)) + @test !success(result) + @test occursin("is missing #!script marker", String(take!(err_output))) + + # Test 4: Code between sections is now valid + valid_code_between = joinpath(@__DIR__, "project", "scripts", "valid_code_between.jl") + result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no $valid_code_between`))) + @test success(result) + + # Test 5: Using --project on a non-script file errors when loading packages + regular_script = joinpath(@__DIR__, "project", "scripts", "regular_script.jl") + err_output = IOBuffer() + result = run(pipeline(ignorestatus(`$(Base.julia_cmd()) --startup-file=no --project=$regular_script -e "using Test"`), stderr=err_output)) + @test !success(result) + @test occursin("is missing #!script marker", String(take!(err_output))) +end diff --git a/test/project/scripts/invalid_both.jl b/test/project/scripts/invalid_both.jl new file mode 100644 index 0000000000000..acb69e8f4943e --- /dev/null +++ b/test/project/scripts/invalid_both.jl @@ -0,0 +1,11 @@ +function foo() + return 42 +end + +#!script + +#!project begin +#!project end + +#!manifest begin +#!manifest end diff --git a/test/project/scripts/invalid_manifest_not_last.jl b/test/project/scripts/invalid_manifest_not_last.jl new file mode 100644 index 0000000000000..2e6ff5e6cb103 --- /dev/null +++ b/test/project/scripts/invalid_manifest_not_last.jl @@ -0,0 +1,12 @@ +#!script + +#!manifest begin +# julia_version = "1.11.0" +#!manifest end + +#!project begin +# [deps] +# Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +#!project end + +println("Manifest before project - should error") diff --git a/test/project/scripts/invalid_project_not_first.jl b/test/project/scripts/invalid_project_not_first.jl new file mode 100644 index 0000000000000..d8605d7654588 --- /dev/null +++ b/test/project/scripts/invalid_project_not_first.jl @@ -0,0 +1,11 @@ +# Some code before #!script marker + +x = 1 + +#!script + +#!project begin +# name = "Test" +#!project end + +println("test") diff --git a/test/project/scripts/regular_script.jl b/test/project/scripts/regular_script.jl new file mode 100644 index 0000000000000..1b7213dd02d43 --- /dev/null +++ b/test/project/scripts/regular_script.jl @@ -0,0 +1,5 @@ +# This is a regular Julia file without any inline project/manifest sections +println("ACTIVE_PROJECT: ", Base.active_project()) +println("Hello from regular script") +x = 42 +println("x = ", x) diff --git a/test/project/scripts/script.jl b/test/project/scripts/script.jl new file mode 100644 index 0000000000000..9aad0fbbabbd9 --- /dev/null +++ b/test/project/scripts/script.jl @@ -0,0 +1,91 @@ +#!/usr/bin/env julia +#!script + +#!project begin +# name = "PortableScriptTest" +# uuid = "f7e12c4d-9a2b-4c3f-8e5d-6a7b8c9d0e1f" +# version = "0.1.0" +# [deps] +# Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +# Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +# Rot13 = "43ef800a-eac4-47f4-949b-25107b932e8f" +#!project end + +using Random +using Test +using Rot13 + +# Verify the script environment is active +println("Active project: ", Base.active_project()) +println("Active manifest: ", Base.active_manifest()) +println() + + +# Test Random (stdlib) +Random.seed!(42) +r = rand() +@test 0 <= r <= 1 +println("✓ Random (stdlib) loaded successfully") + +# Test Rot13 (path-based dependency) +@test Rot13.rot13("Hello") == "Uryyb" +@test Rot13.rot13("World") == "Jbeyq" +println("✓ Rot13 (path dependency) loaded successfully") + +# Test that Rot13 module has expected functions +@test hasmethod(Rot13.rot13, (Char,)) +@test hasmethod(Rot13.rot13, (AbstractString,)) +println("✓ Rot13 methods available") + + + +#!manifest begin +# julia_version = "1.13.0" +# manifest_format = "2.0" +# project_hash = "abc123" +# +# [[deps.Random]] +# uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +# version = "1.11.0" +# +# [[deps.Test]] +# deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] +# uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +# version = "1.11.0" +# +# [[deps.InteractiveUtils]] +# deps = ["Markdown"] +# uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +# version = "1.11.0" +# +# [[deps.Logging]] +# uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +# version = "1.11.0" +# +# [[deps.Serialization]] +# uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +# version = "1.11.0" +# +# [[deps.Markdown]] +# deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] +# uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +# version = "1.11.0" +# +# [[deps.Base64]] +# uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +# version = "1.11.0" +# +# [[deps.JuliaSyntaxHighlighting]] +# deps = ["StyledStrings"] +# uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +# version = "1.12.0" +# +# [[deps.StyledStrings]] +# uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" +# version = "1.11.0" +# +# [[deps.Rot13]] +# path = "../Rot13" +# uuid = "43ef800a-eac4-47f4-949b-25107b932e8f" +# version = "0.1.0" +#!manifest end diff --git a/test/project/scripts/script_custom.toml b/test/project/scripts/script_custom.toml new file mode 100644 index 0000000000000..51b97d021e2f9 --- /dev/null +++ b/test/project/scripts/script_custom.toml @@ -0,0 +1,6 @@ +julia_version = "1.13.0" +manifest_format = "2.0" + +[[deps.Random]] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" diff --git a/test/project/scripts/script_custom_manifest.jl b/test/project/scripts/script_custom_manifest.jl new file mode 100644 index 0000000000000..9c664e99a9ed5 --- /dev/null +++ b/test/project/scripts/script_custom_manifest.jl @@ -0,0 +1,35 @@ +#!/usr/bin/env julia +#!script + +#!project begin +# name = "PortableScriptCustomManifestTest" +# uuid = "1a2b3c4d-5e6f-7890-abcd-ef1234567890" +# version = "0.1.0" +# manifest = "script_custom.toml" +# [deps] +# Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +#!project end + +using Random + +# Test that the custom manifest is being used +println("Active project: ", Base.active_project()) +println("Active manifest: ", Base.active_manifest()) + +# Verify that the active manifest points to our custom manifest file +expected_manifest = joinpath(dirname(Base.active_project()), "script_custom.toml") +actual_manifest = Base.active_manifest() + +if actual_manifest == expected_manifest + println("✓ Custom manifest file is being used: $actual_manifest") +else + error("Expected manifest: $expected_manifest, but got: $actual_manifest") +end + +# Test that Random works +Random.seed!(456) +x = rand() +@assert 0 <= x <= 1 +println("✓ Random.rand() = ", x) + +println("✓ All checks passed!") diff --git a/test/project/scripts/script_missing_dep.jl b/test/project/scripts/script_missing_dep.jl new file mode 100644 index 0000000000000..9c61a3c432a15 --- /dev/null +++ b/test/project/scripts/script_missing_dep.jl @@ -0,0 +1,11 @@ +#!/usr/bin/env julia +#!script + +#!project begin +# name = "PortableScriptMissingDep" +# uuid = "2a4c4b38-9a08-4e35-9b2c-2d1f6890d5f7" +# version = "0.1.0" +# [deps] +#!project end + +using Rot13 diff --git a/test/project/scripts/valid_code_between.jl b/test/project/scripts/valid_code_between.jl new file mode 100644 index 0000000000000..69efc515ef1ad --- /dev/null +++ b/test/project/scripts/valid_code_between.jl @@ -0,0 +1,14 @@ +#!script + +#!project begin +#!project end + +# Some actual Julia code +function bar() + println("hello") +end + +#!manifest begin +#!manifest end + +bar()