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.has_inline_project(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 has_inline_project(abs_script_path)
set_portable_script_state(abs_script_path)
try
include(Main, PROGRAM_FILE)
finally
global portable_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 portable script first
if env == "@script" && Base.has_inline_project(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 [portable script](@ref portable-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 [portable script](@ref portable-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 [portable script](@ref portable-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
194 changes: 166 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,136 @@ 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)

# For manifest, read backwards by reversing the lines
if type === :manifest
lines = reverse(lines)
start_marker = "#!manifest end"
end_marker = "#!manifest begin"
section_name = "manifest"
position_error = "must come last"
else
start_marker = "#!project begin"
end_marker = "#!project end"
section_name = "project"
position_error = "must come first"
end

state = :none
at_start = true
content_lines = String[]

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

# Skip empty lines and comments (including shebang) before content
if at_start && (isempty(stripped) || startswith(stripped, '#'))
if startswith(stripped, start_marker)
state = :reading
at_start = false
continue
end
continue
end

# Found start marker after content - error
if startswith(stripped, start_marker)
if !at_start
error("#!$section_name section $position_error in $path")
end
state = :reading
at_start = false
continue
end

at_start = false

# 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

# For manifest, reverse the content back to original order
if type === :manifest && !isempty(content_lines)
content_lines = reverse(content_lines)
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 has_inline_project(path::String)::Bool
for line in eachline(path)
stripped = lstrip(line)
if startswith(stripped, "#!project begin")
return true
end
end
return false
end


struct PortableScriptState
path::String
pkg::PkgId
end

portable_script_state_global::Union{PortableScriptState, Nothing} = nothing

function set_portable_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 portable_script_state_global = PortableScriptState(abs_path, pkg)
end


struct LoadingCache
load_path::Vector{String}
dummy_uuid::Dict{String, UUID}
Expand All @@ -273,26 +387,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
# Portable 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 +458,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 && portable_script_state_global !== nothing
return identify_package_env(portable_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 +787,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 = has_inline_project(env) ? env : false
else
project_file = false
end
Expand Down Expand Up @@ -897,7 +1030,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 +1045,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
# portable 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 @@ -1038,7 +1176,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 +1249,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 +1678,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 +2654,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