Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion base/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()))
Expand Down
17 changes: 14 additions & 3 deletions base/initdefs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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[],)
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
187 changes: 159 additions & 28 deletions base/loading.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion base/precompilation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading