From 9242bbb5d17c202287ed88f8b35d040e7d5f8069 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Tue, 30 Jan 2024 11:46:43 +0100 Subject: [PATCH 01/12] wip on app support in Pkg --- ext/REPLExt/completions.jl | 17 ++ src/Apps/Apps.jl | 403 +++++++++++++++++++++++++++ src/Operations.jl | 11 +- src/Pkg.jl | 1 + src/REPLMode/REPLMode.jl | 2 +- src/REPLMode/argument_parsers.jl | 9 + src/REPLMode/command_declarations.jl | 67 +++++ src/Types.jl | 13 +- src/manifest.jl | 24 ++ src/project.jl | 14 + 10 files changed, 553 insertions(+), 8 deletions(-) create mode 100644 src/Apps/Apps.jl diff --git a/ext/REPLExt/completions.jl b/ext/REPLExt/completions.jl index 5db7fe4ca1..71d7eb8464 100644 --- a/ext/REPLExt/completions.jl +++ b/ext/REPLExt/completions.jl @@ -164,6 +164,23 @@ function complete_add_dev(options, partial, i1, i2; hint::Bool) return comps, idx, !isempty(comps) end +# TODO: Move +import Pkg: Operations, Types, Apps +function complete_installed_apps(options, partial) + manifest = try + Types.read_manifest(joinpath(Apps.APP_ENV_FOLDER, "AppManifest.toml")) + catch err + err isa PkgError || rethrow() + return String[] + end + apps = String[] + for (uuid, entry) in manifest.deps + append!(apps, keys(entry.apps)) + push!(apps, entry.name) + end + return unique!(apps) +end + ######################## # COMPLETION INTERFACE # ######################## diff --git a/src/Apps/Apps.jl b/src/Apps/Apps.jl new file mode 100644 index 0000000000..b55a4f4caa --- /dev/null +++ b/src/Apps/Apps.jl @@ -0,0 +1,403 @@ +module Apps + +using Pkg +using Pkg.Types: AppInfo, PackageSpec, Context, EnvCache, PackageEntry, handle_repo_add!, handle_repo_develop!, write_manifest, write_project, + pkgerror +using Pkg.Operations: print_single, source_path +using Pkg.API: handle_package_input! +using TOML, UUIDs +import Pkg.Registry + +############# +# Constants # +############# + +const APP_ENV_FOLDER = joinpath(homedir(), ".julia", "environments", "apps") +const APP_MANIFEST_FILE = joinpath(APP_ENV_FOLDER, "AppManifest.toml") +const JULIA_BIN_PATH = joinpath(homedir(), ".julia", "bin") + +################## +# Helper Methods # +################## + +function handle_project_file(sourcepath) + project_file = joinpath(sourcepath, "Project.toml") + isfile(project_file) || error("Project file not found: $project_file") + + project = Pkg.Types.read_project(project_file) + isempty(project.apps) && error("No apps found in Project.toml for package $(project.name) at version $(project.version)") + return project +end + +function update_app_manifest(pkg) + manifest = Pkg.Types.read_manifest(APP_MANIFEST_FILE) + manifest.deps[pkg.uuid] = pkg + write_manifest(manifest, APP_MANIFEST_FILE) +end + +function overwrite_if_different(file, content) + if !isfile(file) || read(file, String) != content + open(file, "w") do f + write(f, content) + end + end +end + +function get_latest_version_register(pkg::PackageSpec, regs) + max_v = nothing + tree_hash = nothing + for reg in regs + if get(reg, pkg.uuid, nothing) !== nothing + reg_pkg = get(reg, pkg.uuid, nothing) + reg_pkg === nothing && continue + pkg_info = Registry.registry_info(reg_pkg) + for (version, info) in pkg_info.version_info + info.yanked && continue + if pkg.version isa VersionNumber + pkg.version == version || continue + else + version in pkg.version || continue + end + if max_v === nothing || version > max_v + max_v = version + tree_hash = info.git_tree_sha1 + end + end + end + end + if max_v === nothing + error("Suitable package version for $(pkg.name) not found in any registries.") + end + return (max_v, tree_hash) +end + +app_context() = Context(env=EnvCache(joinpath(APP_ENV_FOLDER, "Project.toml"))) + +################## +# Main Functions # +################## + +# TODO: Add functions similar to API that takes name, Vector{String} etc and promotes it to `Vector{PackageSpec}`.. + +function add(pkg::String) + pkg = PackageSpec(pkg) + add(pkg) +end + +function add(pkg::Vector{PackageSpec}) + for p in pkg + add(p) + end +end + +function add(pkg::PackageSpec) + handle_package_input!(pkg) + + ctx = app_context() + new = false + if pkg.repo.source !== nothing || pkg.repo.rev !== nothing + entry = Pkg.API.manifest_info(ctx.env.manifest, pkg.uuid) + pkg = Pkg.Operations.update_package_add(ctx, pkg, entry, false) + new = handle_repo_add!(ctx, pkg) + else + pkgs = [pkg] + Pkg.Operations.registry_resolve!(ctx.registries, pkgs) + Pkg.Operations.ensure_resolved(ctx, ctx.env.manifest, pkgs, registry=true) + + pkg.version, pkg.tree_hash = get_latest_version_register(pkg, ctx.registries) + + new = Pkg.Operations.download_source(ctx, pkgs) + end + + sourcepath = source_path(ctx.env.manifest_file, pkg) + project = handle_project_file(sourcepath) + project.path = sourcepath + + # TODO: Type stab + # appdeps = get(project, "appdeps", Dict()) + # merge!(project.deps, appdeps) + + projectfile = joinpath(APP_ENV_FOLDER, pkg.name, "Project.toml") + mkpath(dirname(projectfile)) + write_project(project, projectfile) + + # Move manifest if it exists here. + + Pkg.activate(joinpath(APP_ENV_FOLDER, pkg.name)) + Pkg.instantiate() + + if new + # TODO: Call build on the package if it was freshly installed? + end + + # Create the new package env. + entry = PackageEntry(;apps = project.apps, name = pkg.name, version = project.version, tree_hash = pkg.tree_hash, path = pkg.path, repo = pkg.repo, uuid=pkg.uuid) + update_app_manifest(entry) + generate_shims_for_apps(entry.name, entry.apps, dirname(projectfile)) +end + + +function develop(pkg::String) + develop(PackageSpec(pkg)) +end + +function develop(pkg::PackageSpec) + handle_package_input!(pkg) + ctx = app_context() + + handle_repo_develop!(ctx, pkg, #=shared =# true) + + + project = handle_project_file(pkg.path) + + # Seems like the `.repo.source` field is not cleared. + # At least repo-url is still in the manifest after doing a dev with a path + # Figure out why for normal dev this is not needed. + # XXX: Why needed? + if pkg.path !== nothing + pkg.repo.source = nothing + end + + entry = PackageEntry(;apps = project.apps, name = pkg.name, version = project.version, tree_hash = pkg.tree_hash, path = pkg.path, repo = pkg.repo, uuid=pkg.uuid) + update_app_manifest(entry) + generate_shims_for_apps(entry.name, entry.apps, entry.path) +end + +function status(pkgs_or_apps::Vector) + if isempty(pkgs_or_apps) + status() + else + for pkg_or_app in pkgs_or_apps + if pkg_or_app isa String + pkg_or_app = PackageSpec(pkg_or_app) + end + status(pkg_or_app) + end + end +end + +function status(pkg_or_app::Union{PackageSpec, Nothing}=nothing) + # TODO: Sort. + # TODO: Show julia version + pkg_or_app = pkg_or_app === nothing ? nothing : pkg_or_app.name + manifest = Pkg.Types.read_manifest(joinpath(APP_ENV_FOLDER, "AppManifest.toml")) + deps = Pkg.Operations.load_manifest_deps(manifest) + + is_pkg = pkg_or_app !== nothing && any(dep -> dep.name == pkg_or_app, values(manifest.deps)) + + for dep in deps + info = manifest.deps[dep.uuid] + if is_pkg && dep.name !== pkg_or_app + continue + end + if !is_pkg && pkg_or_app !== nothing + if !(pkg_or_app in keys(info.apps)) + continue + end + end + + printstyled("[", string(dep.uuid)[1:8], "] "; color = :light_black) + print_single(stdout, dep) + single_app = length(info.apps) == 1 + if !single_app + println() + else + print(":") + end + for (appname, appinfo) in info.apps + if !is_pkg && pkg_or_app !== nothing && appname !== pkg_or_app + continue + end + printstyled(" $(appname) $(appinfo.julia_command) \n", color=:green) + end + end +end + +function precompile(pkg::Union{Nothing, String}=nothing) + manifest = Pkg.Types.read_manifest(joinpath(APP_ENV_FOLDER, "AppManifest.toml")) + deps = Pkg.Operations.load_manifest_deps(manifest) + for dep in deps + # TODO: Parallel app compilation..? + info = manifest.deps[dep.uuid] + if pkg !== nothing && info.name !== pkg + continue + end + Pkg.activate(joinpath(APP_ENV_FOLDER, info.name)) do + @info "Precompiling $(info.name)..." + Pkg.precompile() + end + end +end + +function require_not_empty(pkgs, f::Symbol) + pkgs === nothing && return + isempty(pkgs) && pkgerror("app $f requires at least one package") +end + +function rm(pkgs_or_apps::Union{Vector, Nothing}) + if pkgs_or_apps === nothing + rm(nothing) + else + for pkg_or_app in pkgs_or_apps + if pkg_or_app isa String + pkg_or_app = PackageSpec(pkg_or_app) + end + rm(pkg_or_app) + end + end +end + +function rm(pkg_or_app::Union{PackageSpec, Nothing}=nothing) + pkg_or_app = pkg_or_app === nothing ? nothing : pkg_or_app.name + + require_not_empty(pkg_or_app, :rm) + + manifest = Pkg.Types.read_manifest(joinpath(APP_ENV_FOLDER, "AppManifest.toml")) + dep_idx = findfirst(dep -> dep.name == pkg_or_app, manifest.deps) + if dep_idx !== nothing + dep = manifest.deps[dep_idx] + @info "Deleted all apps for package $(dep.name)" + delete!(manifest.deps, dep.uuid) + for (appname, appinfo) in dep.apps + @info "Deleted $(appname)" + Base.rm(joinpath(JULIA_BIN_PATH, appname); force=true) + end + Base.rm(joinpath(APP_ENV_FOLDER, dep.name); recursive=true) + else + for (uuid, pkg) in manifest.deps + app_idx = findfirst(app -> app.name == pkg_or_app, pkg.apps) + if app_idx !== nothing + app = pkg.apps[app_idx] + @info "Deleted app $(app.name)" + delete!(pkg.apps, app.name) + Base.rm(joinpath(JULIA_BIN_PATH, app.name); force=true) + end + if isempty(pkg.apps) + delete!(manifest.deps, uuid) + Base.rm(joinpath(APP_ENV_FOLDER, pkg.name); recursive=true) + end + end + end + + Pkg.Types.write_manifest(manifest, APP_MANIFEST_FILE) + return +end + + + +######### +# Shims # +######### + +function generate_shims_for_apps(pkgname, apps, env) + for (_, app) in apps + generate_shim(app, pkgname; env) + end +end + +function generate_shim(app::AppInfo, pkgname; julia_executable_path::String=joinpath(Sys.BINDIR, "julia"), env=joinpath(homedir(), ".julia", "environments", "apps", pkgname)) + filename = joinpath(homedir(), ".julia", "bin", app.name * (Sys.iswindows() ? ".bat" : "")) + mkpath(dirname(filename)) + content = if Sys.iswindows() + windows_shim(pkgname, julia_executable_path, env) + else + bash_shim(pkgname, julia_executable_path, env) + end + overwrite_if_different(filename, content) + if Sys.isunix() + chmod(filename, 0o755) + end +end + + +function bash_shim(pkgname, julia_executable_path::String, env) + return """ + #!/usr/bin/env bash + + export JULIA_LOAD_PATH=$(repr(env)) + exec $julia_executable_path \\ + --startup-file=no \\ + -m $(pkgname) \\ + "\$@" + """ +end + +function windows_shim(pkgname, julia_executable_path::String, env) + return """ + @echo off + set JULIA_LOAD_PATH=$(repr(env)) + + $julia_executable_path ^ + --startup-file=no ^ + -m $(pkgname) ^ + %* + """ +end + + +################# +# PATH handling # +################# + +function add_bindir_to_path() + if Sys.iswindows() + update_windows_PATH() + else + update_unix_PATH() + end +end + +function get_shell_config_file(julia_bin_path) + home_dir = ENV["HOME"] + # Check for various shell configuration files + if occursin("/zsh", ENV["SHELL"]) + return (joinpath(home_dir, ".zshrc"), "path=('$julia_bin_path' \$path)\nexport PATH") + elseif occursin("/bash", ENV["SHELL"]) + return (joinpath(home_dir, ".bashrc"), "export PATH=\"\$PATH:$julia_bin_path\"") + elseif occursin("/fish", ENV["SHELL"]) + return (joinpath(home_dir, ".config/fish/config.fish"), "set -gx PATH \$PATH $julia_bin_path") + elseif occursin("/ksh", ENV["SHELL"]) + return (joinpath(home_dir, ".kshrc"), "export PATH=\"\$PATH:$julia_bin_path\"") + elseif occursin("/tcsh", ENV["SHELL"]) || occursin("/csh", ENV["SHELL"]) + return (joinpath(home_dir, ".tcshrc"), "setenv PATH \$PATH:$julia_bin_path") # or .cshrc + else + return (nothing, nothing) + end +end + +function update_unix_PATH() + shell_config_file, path_command = get_shell_config_file(JULIA_BIN_PATH) + if shell_config_file === nothing + @warn "Failed to insert `.julia/bin` to PATH: Failed to detect shell" + return + end + + if !isfile(shell_config_file) + @warn "Failed to insert `.julia/bin` to PATH: $(repr(shell_config_file)) does not exist." + return + end + file_contents = read(shell_config_file, String) + + # Check for the comment fence + start_fence = "# >>> julia apps initialize >>>" + end_fence = "# <<< julia apps initialize <<<" + fence_exists = occursin(start_fence, file_contents) && occursin(end_fence, file_contents) + + if !fence_exists + open(shell_config_file, "a") do file + print(file, "\n$start_fence\n\n") + print(file, "# !! Contents within this block are managed by Julia's package manager Pkg !!\n\n") + print(file, "$path_command\n\n") + print(file, "$end_fence\n\n") + end + end +end + +function update_windows_PATH() + current_path = ENV["PATH"] + occursin(JULIA_BIN_PATH, current_path) && return + new_path = "$current_path;$JULIA_BIN_PATH" + run(`setx PATH "$new_path"`) +end + +end diff --git a/src/Operations.jl b/src/Operations.jl index 8efa29c042..6ea3432ddf 100644 --- a/src/Operations.jl +++ b/src/Operations.jl @@ -1011,9 +1011,11 @@ function find_urls(registries::Vector{Registry.RegistryInstance}, uuid::UUID) end -function download_source(ctx::Context; readonly=true) - pkgs_to_install = NamedTuple{(:pkg, :urls, :path), Tuple{PackageEntry, Set{String}, String}}[] - for pkg in values(ctx.env.manifest) +download_source(ctx::Context; readonly=true) = download_source(ctx, values(ctx.env.manifest); readonly) + +function download_source(ctx::Context, pkgs; readonly=true) + pkgs_to_install = NamedTuple{(:pkg, :urls, :path), Tuple{eltype(pkgs), Set{String}, String}}[] + for pkg in pkgs tracking_registered_version(pkg, ctx.julia_version) || continue path = source_path(ctx.env.manifest_file, pkg, ctx.julia_version) path === nothing && continue @@ -1098,7 +1100,7 @@ function download_source(ctx::Context; readonly=true) fancyprint = can_fancyprint(ctx.io) try for i in 1:length(pkgs_to_install) - pkg::PackageEntry, exc_or_success, bt_or_pathurls = take!(results) + pkg::eltype(pkgs), exc_or_success, bt_or_pathurls = take!(results) exc_or_success isa Exception && pkgerror("Error when installing package $(pkg.name):\n", sprint(Base.showerror, exc_or_success, bt_or_pathurls)) success, (urls, path) = exc_or_success, bt_or_pathurls @@ -1129,7 +1131,6 @@ function download_source(ctx::Context; readonly=true) # Use LibGit2 to download any remaining packages # ################################################## for (pkg, urls, path) in missed_packages - uuid = pkg.uuid install_git(ctx.io, pkg.uuid, pkg.name, pkg.tree_hash, urls, path) readonly && set_readonly(path) vstr = if pkg.version !== nothing diff --git a/src/Pkg.jl b/src/Pkg.jl index d6260607dd..1c29d61c01 100644 --- a/src/Pkg.jl +++ b/src/Pkg.jl @@ -72,6 +72,7 @@ include("BinaryPlatforms_compat.jl") include("Artifacts.jl") include("Operations.jl") include("API.jl") +include("Apps/Apps.jl") include("REPLMode/REPLMode.jl") import .REPLMode: @pkg_str diff --git a/src/REPLMode/REPLMode.jl b/src/REPLMode/REPLMode.jl index aba9ef4dd8..2abd52e5cc 100644 --- a/src/REPLMode/REPLMode.jl +++ b/src/REPLMode/REPLMode.jl @@ -7,7 +7,7 @@ module REPLMode using Markdown, UUIDs, Dates import ..casesensitive_isdir, ..OFFLINE_MODE, ..linewrap, ..pathrepr -using ..Types, ..Operations, ..API, ..Registry, ..Resolve +using ..Types, ..Operations, ..API, ..Registry, ..Resolve, ..Apps import ..stdout_f, ..stderr_f diff --git a/src/REPLMode/argument_parsers.jl b/src/REPLMode/argument_parsers.jl index c0f284a4b0..5d909e91ba 100644 --- a/src/REPLMode/argument_parsers.jl +++ b/src/REPLMode/argument_parsers.jl @@ -204,6 +204,15 @@ function parse_registry(word::AbstractString; add=false)::RegistrySpec return registry end +# +# # Apps +# +function parse_app_add(raw_args::Vector{QString}, options) + return parse_package(raw_args, options; add_or_dev=true) +end + + + # # # Other # diff --git a/src/REPLMode/command_declarations.jl b/src/REPLMode/command_declarations.jl index cb00dfb260..9ac3ccd1f4 100644 --- a/src/REPLMode/command_declarations.jl +++ b/src/REPLMode/command_declarations.jl @@ -582,4 +582,71 @@ pkg> registry status """, ] ], #registry +"app" => CommandDeclaration[ + PSA[:name => "status", + :short_name => "st", + :api => Apps.status, + :should_splat => false, + :arg_count => 0 => Inf, + :arg_parser => parse_package, + :completions => complete_installed_apps, + :description => "show status of apps", + :help => md""" + show status of apps + """ +], +PSA[:name => "add", + :api => Apps.add, + :should_splat => false, + :arg_count => 0 => Inf, + :arg_parser => parse_app_add, + :completions => complete_add_dev, + :description => "add app", + :help => md""" + app add pkg + +Adds the apps for packages `pkg...` or apps `app...`. +``` +""", +], +PSA[:name => "remove", + :short_name => "rm", + :api => Apps.rm, + :should_splat => false, + :arg_count => 0 => Inf, + :arg_parser => parse_package, + :completions => complete_installed_apps, + :description => "remove packages from project or manifest", + :help => md""" + app [rm|remove] pkg ... + app [rm|remove] app ... + + Remove the apps for package `pkg`. + """ +], +PSA[:name => "develop", + :short_name => "dev", + :api => Apps.develop, + :should_splat => false, + :arg_count => 1 => Inf, + :arg_parser => (x,y) -> parse_package(x,y; add_or_dev=true), + :completions => complete_add_dev, + :description => "develop a package and install all the apps in it", + :help => md""" + app [dev|develop] pkg[=uuid] ... + app [dev|develop] path + +Same as `develop` but also installs all the apps in the package. +This allows one to edit their app and have the changes immediately be reflected in the app. + +**Examples** +```jl +pkg> app develop Example +pkg> app develop https://github.com/JuliaLang/Example.jl +pkg> app develop ~/mypackages/Example +pkg> app develop --local Example +``` +""" +], # app +] ] #command_declarations diff --git a/src/Types.jl b/src/Types.jl index 859b93221a..6ee6ad0777 100644 --- a/src/Types.jl +++ b/src/Types.jl @@ -179,7 +179,7 @@ function projectfile_path(env_path::String; strict=false) end function manifestfile_path(env_path::String; strict=false) - for name in Base.manifest_names + for name in (Base.manifest_names..., "AppManifest.toml") maybe_file = joinpath(env_path, name) isfile(maybe_file) && return maybe_file end @@ -233,6 +233,12 @@ end Base.:(==)(t1::Compat, t2::Compat) = t1.val == t2.val Base.hash(t::Compat, h::UInt) = hash(t.val, h) +struct AppInfo + name::String + julia_command::Union{String, Nothing} + julia_version::Union{VersionNumber, Nothing} + other::Dict{String,Any} +end Base.@kwdef mutable struct Project other::Dict{String,Any} = Dict{String,Any}() # Fields @@ -251,6 +257,7 @@ Base.@kwdef mutable struct Project exts::Dict{String,Union{Vector{String}, String}} = Dict{String,String}() extras::Dict{String,UUID} = Dict{String,UUID}() targets::Dict{String,Vector{String}} = Dict{String,Vector{String}}() + apps::Dict{String, AppInfo} = Dict{String, AppInfo}() compat::Dict{String,Compat} = Dict{String,Compat}() sources::Dict{String,Dict{String, String}} = Dict{String,Dict{String, String}}() workspace::Dict{String, Any} = Dict{String, Any}() @@ -272,6 +279,7 @@ Base.@kwdef mutable struct PackageEntry weakdeps::Dict{String,UUID} = Dict{String,UUID}() exts::Dict{String,Union{Vector{String}, String}} = Dict{String,String}() uuid::Union{Nothing, UUID} = nothing + apps::Dict{String, AppInfo} = Dict{String, AppInfo}() # used by AppManifest.toml other::Union{Dict,Nothing} = nothing end Base.:(==)(t1::PackageEntry, t2::PackageEntry) = t1.name == t2.name && @@ -284,7 +292,8 @@ Base.:(==)(t1::PackageEntry, t2::PackageEntry) = t1.name == t2.name && t1.deps == t2.deps && t1.weakdeps == t2.weakdeps && t1.exts == t2.exts && - t1.uuid == t2.uuid + t1.uuid == t2.uuid && + t1.apps == t2.apps # omits `other` Base.hash(x::PackageEntry, h::UInt) = foldr(hash, [x.name, x.version, x.path, x.entryfile, x.pinned, x.repo, x.tree_hash, x.deps, x.weakdeps, x.exts, x.uuid], init=h) # omits `other` diff --git a/src/manifest.jl b/src/manifest.jl index 2fc95023ca..7b649e844b 100644 --- a/src/manifest.jl +++ b/src/manifest.jl @@ -81,6 +81,20 @@ function read_deps(raw::Dict{String, Any})::Dict{String,UUID} return deps end +read_apps(::Nothing) = Dict{String, AppInfo}() +read_apps(::Any) = pkgerror("Expected `apps` field to be a Dict") +function read_apps(apps::Dict) + appinfos = Dict{String, AppInfo}() + for (appname, app) in apps + appinfo = AppInfo(appname::String, + app["julia_command"]::String, + VersionNumber(app["julia_version"]::String), + app) + appinfos[appinfo.name] = appinfo + end + return appinfos +end + struct Stage1 uuid::UUID entry::PackageEntry @@ -182,6 +196,7 @@ function Manifest(raw::Dict{String, Any}, f_or_io::Union{String, IO})::Manifest entry.uuid = uuid deps = read_deps(get(info::Dict, "deps", nothing)::Union{Nothing, Dict{String, Any}, Vector{String}}) weakdeps = read_deps(get(info::Dict, "weakdeps", nothing)::Union{Nothing, Dict{String, Any}, Vector{String}}) + entry.apps = read_apps(get(info::Dict, "apps", nothing)::Union{Nothing, Dict{String, Any}}) entry.exts = get(Dict{String, String}, info, "extensions") catch # TODO: Should probably not unconditionally log something @@ -306,6 +321,15 @@ function destructure(manifest::Manifest)::Dict if !isempty(entry.exts) entry!(new_entry, "extensions", entry.exts) end + + if !isempty(entry.apps) + new_entry["apps"] = Dict{String,Any}() + for (appname, appinfo) in entry.apps + julia_command = @something appinfo.julia_command joinpath(Sys.BINDIR, "julia" * (Sys.iswindows() ? ".exe" : "")) + julia_version = @something appinfo.julia_version VERSION + new_entry["apps"][appname] = Dict{String,Any}("julia_command" => julia_command, "julia_version" => julia_version) + end + end if manifest.manifest_format.major == 1 push!(get!(raw, entry.name, Dict{String,Any}[]), new_entry) elseif manifest.manifest_format.major == 2 diff --git a/src/project.jl b/src/project.jl index f7a7e83757..448c13da8c 100644 --- a/src/project.jl +++ b/src/project.jl @@ -74,6 +74,19 @@ end read_project_targets(raw, project::Project) = pkgerror("Expected `targets` section to be a key-value list") +read_project_apps(::Nothing, project::Project) = Dict{String,Any}() +function read_project_apps(raw::Dict{String,Any}, project::Project) + other = raw + appinfos = Dict{String,AppInfo}() + for (name, info) in raw + info isa Dict{String,Any} || pkgerror(""" + Expected value for app `$name` to be a dictionary. + """) + appinfos[name] = AppInfo(name, nothing, nothing, other) + end + return appinfos +end + read_project_compat(::Nothing, project::Project) = Dict{String,Compat}() function read_project_compat(raw::Dict{String,Any}, project::Project) compat = Dict{String,Compat}() @@ -196,6 +209,7 @@ function Project(raw::Dict; file=nothing) project.compat = read_project_compat(get(raw, "compat", nothing), project) project.targets = read_project_targets(get(raw, "targets", nothing), project) project.workspace = read_project_workspace(get(raw, "workspace", nothing), project) + project.apps = read_project_apps(get(raw, "apps", nothing), project) # Handle deps in both [deps] and [weakdeps] project._deps_weak = Dict(intersect(project.deps, project.weakdeps)) From c7e0ee888ebba2927be33a686074fde5a9ce25ad Mon Sep 17 00:00:00 2001 From: Kristoffer Date: Thu, 1 Feb 2024 17:35:31 +0100 Subject: [PATCH 02/12] also symlink app to `.local/bin` --- src/Apps/Apps.jl | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Apps/Apps.jl b/src/Apps/Apps.jl index b55a4f4caa..85b709424d 100644 --- a/src/Apps/Apps.jl +++ b/src/Apps/Apps.jl @@ -15,16 +15,23 @@ import Pkg.Registry const APP_ENV_FOLDER = joinpath(homedir(), ".julia", "environments", "apps") const APP_MANIFEST_FILE = joinpath(APP_ENV_FOLDER, "AppManifest.toml") const JULIA_BIN_PATH = joinpath(homedir(), ".julia", "bin") +const XDG_BIN_PATH = joinpath(homedir(), ".local", "bin") ################## # Helper Methods # ################## +function rm_julia_and_xdg_bin(name; kwargs) + Base.rm(joinpath(JULIA_BIN_PATH, name); kwargs...) + Base.rm(joinpath(XDG_BIN_PATH, name); kwargs...) +end + function handle_project_file(sourcepath) project_file = joinpath(sourcepath, "Project.toml") isfile(project_file) || error("Project file not found: $project_file") project = Pkg.Types.read_project(project_file) + @show project isempty(project.apps) && error("No apps found in Project.toml for package $(project.name) at version $(project.version)") return project end @@ -119,6 +126,7 @@ function add(pkg::PackageSpec) projectfile = joinpath(APP_ENV_FOLDER, pkg.name, "Project.toml") mkpath(dirname(projectfile)) + write_project(project, projectfile) # Move manifest if it exists here. @@ -260,7 +268,7 @@ function rm(pkg_or_app::Union{PackageSpec, Nothing}=nothing) delete!(manifest.deps, dep.uuid) for (appname, appinfo) in dep.apps @info "Deleted $(appname)" - Base.rm(joinpath(JULIA_BIN_PATH, appname); force=true) + rm_julia_and_xdg_bin(appname; force=true) end Base.rm(joinpath(APP_ENV_FOLDER, dep.name); recursive=true) else @@ -270,7 +278,7 @@ function rm(pkg_or_app::Union{PackageSpec, Nothing}=nothing) app = pkg.apps[app_idx] @info "Deleted app $(app.name)" delete!(pkg.apps, app.name) - Base.rm(joinpath(JULIA_BIN_PATH, app.name); force=true) + rm_julia_and_xdg_bin(appname; force=true) end if isempty(pkg.apps) delete!(manifest.deps, uuid) @@ -296,16 +304,22 @@ function generate_shims_for_apps(pkgname, apps, env) end function generate_shim(app::AppInfo, pkgname; julia_executable_path::String=joinpath(Sys.BINDIR, "julia"), env=joinpath(homedir(), ".julia", "environments", "apps", pkgname)) - filename = joinpath(homedir(), ".julia", "bin", app.name * (Sys.iswindows() ? ".bat" : "")) + filename = app.name * (Sys.iswindows() ? ".bat" : "") + julia_bin_filename = joinpath(JULIA_BIN_PATH, filename) mkpath(dirname(filename)) content = if Sys.iswindows() windows_shim(pkgname, julia_executable_path, env) else bash_shim(pkgname, julia_executable_path, env) end - overwrite_if_different(filename, content) + # TODO: Only overwrite if app is "controlled" by Julia? + overwrite_if_different(julia_bin_filename, content) if Sys.isunix() - chmod(filename, 0o755) + if isdir(XDG_BIN_PATH) && !isfile(joinpath(XDG_BIN_PATH, filename)) + # TODO: Verify that this symlink is in fact pointing to the correct file. + symlink(julia_bin_filename, joinpath(XDG_BIN_PATH, filename)) + end + chmod(julia_bin_filename, 0o755) end end From 1fbb0b35b0ed86fd3406a81927b40810b3def4a1 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Tue, 30 Jan 2024 11:46:43 +0100 Subject: [PATCH 03/12] wip on app support in Pkg --- src/Apps/Apps.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Apps/Apps.jl b/src/Apps/Apps.jl index 85b709424d..abee5c0b5d 100644 --- a/src/Apps/Apps.jl +++ b/src/Apps/Apps.jl @@ -31,7 +31,6 @@ function handle_project_file(sourcepath) isfile(project_file) || error("Project file not found: $project_file") project = Pkg.Types.read_project(project_file) - @show project isempty(project.apps) && error("No apps found in Project.toml for package $(project.name) at version $(project.version)") return project end @@ -126,7 +125,6 @@ function add(pkg::PackageSpec) projectfile = joinpath(APP_ENV_FOLDER, pkg.name, "Project.toml") mkpath(dirname(projectfile)) - write_project(project, projectfile) # Move manifest if it exists here. From 72f9be67637ad578aef705b7d626bc28721748f2 Mon Sep 17 00:00:00 2001 From: Kristoffer Date: Thu, 4 Jul 2024 14:01:05 +0200 Subject: [PATCH 04/12] fix from REPL extension --- src/REPLMode/command_declarations.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/REPLMode/command_declarations.jl b/src/REPLMode/command_declarations.jl index 9ac3ccd1f4..d1873eb613 100644 --- a/src/REPLMode/command_declarations.jl +++ b/src/REPLMode/command_declarations.jl @@ -589,7 +589,7 @@ pkg> registry status :should_splat => false, :arg_count => 0 => Inf, :arg_parser => parse_package, - :completions => complete_installed_apps, + :completions => get_complete_function(:complete_installed_apps), :description => "show status of apps", :help => md""" show status of apps @@ -600,7 +600,7 @@ PSA[:name => "add", :should_splat => false, :arg_count => 0 => Inf, :arg_parser => parse_app_add, - :completions => complete_add_dev, + :completions => get_complete_function(:complete_add_dev), :description => "add app", :help => md""" app add pkg @@ -615,7 +615,7 @@ PSA[:name => "remove", :should_splat => false, :arg_count => 0 => Inf, :arg_parser => parse_package, - :completions => complete_installed_apps, + :completions => get_complete_function(:complete_installed_apps), :description => "remove packages from project or manifest", :help => md""" app [rm|remove] pkg ... @@ -630,7 +630,7 @@ PSA[:name => "develop", :should_splat => false, :arg_count => 1 => Inf, :arg_parser => (x,y) -> parse_package(x,y; add_or_dev=true), - :completions => complete_add_dev, + :completions => get_complete_function(:complete_add_dev), :description => "develop a package and install all the apps in it", :help => md""" app [dev|develop] pkg[=uuid] ... From 5687881f389642b8fcdaf98446e83684409fc85c Mon Sep 17 00:00:00 2001 From: Kristoffer Date: Fri, 5 Jul 2024 16:20:04 +0200 Subject: [PATCH 05/12] fixes --- ext/REPLExt/completions.jl | 2 +- src/Apps/Apps.jl | 21 ++++++++++----------- src/Operations.jl | 7 +++---- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/ext/REPLExt/completions.jl b/ext/REPLExt/completions.jl index 71d7eb8464..146367ab09 100644 --- a/ext/REPLExt/completions.jl +++ b/ext/REPLExt/completions.jl @@ -166,7 +166,7 @@ end # TODO: Move import Pkg: Operations, Types, Apps -function complete_installed_apps(options, partial) +function complete_installed_apps(options, partial; hint) manifest = try Types.read_manifest(joinpath(Apps.APP_ENV_FOLDER, "AppManifest.toml")) catch err diff --git a/src/Apps/Apps.jl b/src/Apps/Apps.jl index abee5c0b5d..b9568ddb7f 100644 --- a/src/Apps/Apps.jl +++ b/src/Apps/Apps.jl @@ -3,7 +3,7 @@ module Apps using Pkg using Pkg.Types: AppInfo, PackageSpec, Context, EnvCache, PackageEntry, handle_repo_add!, handle_repo_develop!, write_manifest, write_project, pkgerror -using Pkg.Operations: print_single, source_path +using Pkg.Operations: print_single, source_path, update_package_add using Pkg.API: handle_package_input! using TOML, UUIDs import Pkg.Registry @@ -21,9 +21,9 @@ const XDG_BIN_PATH = joinpath(homedir(), ".local", "bin") # Helper Methods # ################## -function rm_julia_and_xdg_bin(name; kwargs) +function rm_julia_and_xdg_bin(name; kwargs...) Base.rm(joinpath(JULIA_BIN_PATH, name); kwargs...) - Base.rm(joinpath(XDG_BIN_PATH, name); kwargs...) + # Base.rm(joinpath(XDG_BIN_PATH, name); kwargs...) end function handle_project_file(sourcepath) @@ -43,6 +43,7 @@ end function overwrite_if_different(file, content) if !isfile(file) || read(file, String) != content + mkpath(dirname(file)) open(file, "w") do f write(f, content) end @@ -103,7 +104,7 @@ function add(pkg::PackageSpec) new = false if pkg.repo.source !== nothing || pkg.repo.rev !== nothing entry = Pkg.API.manifest_info(ctx.env.manifest, pkg.uuid) - pkg = Pkg.Operations.update_package_add(ctx, pkg, entry, false) + pkg = update_package_add(ctx, pkg, entry, false) new = handle_repo_add!(ctx, pkg) else pkgs = [pkg] @@ -117,7 +118,8 @@ function add(pkg::PackageSpec) sourcepath = source_path(ctx.env.manifest_file, pkg) project = handle_project_file(sourcepath) - project.path = sourcepath + # TODO: Wrong if package itself has a sourcepath? + project.entryfile = joinpath(sourcepath, "src", "$(project.name).jl") # TODO: Type stab # appdeps = get(project, "appdeps", Dict()) @@ -204,12 +206,7 @@ function status(pkg_or_app::Union{PackageSpec, Nothing}=nothing) printstyled("[", string(dep.uuid)[1:8], "] "; color = :light_black) print_single(stdout, dep) - single_app = length(info.apps) == 1 - if !single_app - println() - else - print(":") - end + println() for (appname, appinfo) in info.apps if !is_pkg && pkg_or_app !== nothing && appname !== pkg_or_app continue @@ -313,10 +310,12 @@ function generate_shim(app::AppInfo, pkgname; julia_executable_path::String=join # TODO: Only overwrite if app is "controlled" by Julia? overwrite_if_different(julia_bin_filename, content) if Sys.isunix() + #= if isdir(XDG_BIN_PATH) && !isfile(joinpath(XDG_BIN_PATH, filename)) # TODO: Verify that this symlink is in fact pointing to the correct file. symlink(julia_bin_filename, joinpath(XDG_BIN_PATH, filename)) end + =# chmod(julia_bin_filename, 0o755) end end diff --git a/src/Operations.jl b/src/Operations.jl index 6ea3432ddf..f2e6fe14bc 100644 --- a/src/Operations.jl +++ b/src/Operations.jl @@ -1483,8 +1483,8 @@ function rm(ctx::Context, pkgs::Vector{PackageSpec}; mode::PackageMode) show_update(ctx.env, ctx.registries; io=ctx.io) end -update_package_add(ctx::Context, pkg::PackageSpec, ::Nothing, source_path, source_repo, is_dep::Bool) = pkg -function update_package_add(ctx::Context, pkg::PackageSpec, entry::PackageEntry, source_path, source_repo, is_dep::Bool) +update_package_add(ctx::Context, pkg::PackageSpec, ::Nothing, is_dep::Bool) = pkg +function update_package_add(ctx::Context, pkg::PackageSpec, entry::PackageEntry, is_dep::Bool) if entry.pinned if pkg.version == VersionSpec() println(ctx.io, "`$(pkg.name)` is pinned at `v$(entry.version)`: maintaining pinned version") @@ -1628,8 +1628,7 @@ function add(ctx::Context, pkgs::Vector{PackageSpec}, new_git=Set{UUID}(); delete!(ctx.env.project.weakdeps, pkg.name) entry = manifest_info(ctx.env.manifest, pkg.uuid) is_dep = any(uuid -> uuid == pkg.uuid, [uuid for (name, uuid) in ctx.env.project.deps]) - source_path, source_repo = get_path_repo(ctx.env.project, pkg.name) - pkgs[i] = update_package_add(ctx, pkg, entry, source_path, source_repo, is_dep) + pkgs[i] = update_package_add(ctx, pkg, entry, is_dep) end names = (p.name for p in pkgs) From 1d399baf1aaa928f0d3a9b57c96757a2736c0fad Mon Sep 17 00:00:00 2001 From: Kristoffer Date: Thu, 25 Jul 2024 13:32:12 +0200 Subject: [PATCH 06/12] wip --- src/Apps/Apps.jl | 37 +++++++++++++--------------- src/REPLMode/command_declarations.jl | 8 +++--- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/Apps/Apps.jl b/src/Apps/Apps.jl index b9568ddb7f..fe298f6050 100644 --- a/src/Apps/Apps.jl +++ b/src/Apps/Apps.jl @@ -12,18 +12,18 @@ import Pkg.Registry # Constants # ############# +# Should use `DEPOT_PATH[1]` instead of `homedir()`? const APP_ENV_FOLDER = joinpath(homedir(), ".julia", "environments", "apps") const APP_MANIFEST_FILE = joinpath(APP_ENV_FOLDER, "AppManifest.toml") const JULIA_BIN_PATH = joinpath(homedir(), ".julia", "bin") -const XDG_BIN_PATH = joinpath(homedir(), ".local", "bin") + ################## # Helper Methods # ################## -function rm_julia_and_xdg_bin(name; kwargs...) +function rm_shim(name; kwargs...) Base.rm(joinpath(JULIA_BIN_PATH, name); kwargs...) - # Base.rm(joinpath(XDG_BIN_PATH, name); kwargs...) end function handle_project_file(sourcepath) @@ -44,13 +44,11 @@ end function overwrite_if_different(file, content) if !isfile(file) || read(file, String) != content mkpath(dirname(file)) - open(file, "w") do f - write(f, content) - end + write(file, content) end end -function get_latest_version_register(pkg::PackageSpec, regs) +function get_max_version_register(pkg::PackageSpec, regs) max_v = nothing tree_hash = nothing for reg in regs @@ -111,7 +109,7 @@ function add(pkg::PackageSpec) Pkg.Operations.registry_resolve!(ctx.registries, pkgs) Pkg.Operations.ensure_resolved(ctx, ctx.env.manifest, pkgs, registry=true) - pkg.version, pkg.tree_hash = get_latest_version_register(pkg, ctx.registries) + pkg.version, pkg.tree_hash = get_max_version_register(pkg, ctx.registries) new = Pkg.Operations.download_source(ctx, pkgs) end @@ -130,9 +128,9 @@ function add(pkg::PackageSpec) write_project(project, projectfile) # Move manifest if it exists here. - - Pkg.activate(joinpath(APP_ENV_FOLDER, pkg.name)) - Pkg.instantiate() + Pkg.activate(joinpath(APP_ENV_FOLDER, pkg.name)) do + Pkg.instantiate() + end if new # TODO: Call build on the package if it was freshly installed? @@ -216,6 +214,7 @@ function status(pkg_or_app::Union{PackageSpec, Nothing}=nothing) end end +#= function precompile(pkg::Union{Nothing, String}=nothing) manifest = Pkg.Types.read_manifest(joinpath(APP_ENV_FOLDER, "AppManifest.toml")) deps = Pkg.Operations.load_manifest_deps(manifest) @@ -231,6 +230,7 @@ function precompile(pkg::Union{Nothing, String}=nothing) end end end +=# function require_not_empty(pkgs, f::Symbol) pkgs === nothing && return @@ -263,7 +263,7 @@ function rm(pkg_or_app::Union{PackageSpec, Nothing}=nothing) delete!(manifest.deps, dep.uuid) for (appname, appinfo) in dep.apps @info "Deleted $(appname)" - rm_julia_and_xdg_bin(appname; force=true) + rm_shim(appname; force=true) end Base.rm(joinpath(APP_ENV_FOLDER, dep.name); recursive=true) else @@ -273,7 +273,7 @@ function rm(pkg_or_app::Union{PackageSpec, Nothing}=nothing) app = pkg.apps[app_idx] @info "Deleted app $(app.name)" delete!(pkg.apps, app.name) - rm_julia_and_xdg_bin(appname; force=true) + rm_shim(app.name; force=true) end if isempty(pkg.apps) delete!(manifest.deps, uuid) @@ -307,15 +307,8 @@ function generate_shim(app::AppInfo, pkgname; julia_executable_path::String=join else bash_shim(pkgname, julia_executable_path, env) end - # TODO: Only overwrite if app is "controlled" by Julia? overwrite_if_different(julia_bin_filename, content) if Sys.isunix() - #= - if isdir(XDG_BIN_PATH) && !isfile(joinpath(XDG_BIN_PATH, filename)) - # TODO: Verify that this symlink is in fact pointing to the correct file. - symlink(julia_bin_filename, joinpath(XDG_BIN_PATH, filename)) - end - =# chmod(julia_bin_filename, 0o755) end end @@ -346,6 +339,9 @@ function windows_shim(pkgname, julia_executable_path::String, env) end + + +#= ################# # PATH handling # ################# @@ -410,5 +406,6 @@ function update_windows_PATH() new_path = "$current_path;$JULIA_BIN_PATH" run(`setx PATH "$new_path"`) end +=# end diff --git a/src/REPLMode/command_declarations.jl b/src/REPLMode/command_declarations.jl index d1873eb613..a9d9cc3304 100644 --- a/src/REPLMode/command_declarations.jl +++ b/src/REPLMode/command_declarations.jl @@ -589,7 +589,7 @@ pkg> registry status :should_splat => false, :arg_count => 0 => Inf, :arg_parser => parse_package, - :completions => get_complete_function(:complete_installed_apps), + :completions => :complete_installed_apps, :description => "show status of apps", :help => md""" show status of apps @@ -600,7 +600,7 @@ PSA[:name => "add", :should_splat => false, :arg_count => 0 => Inf, :arg_parser => parse_app_add, - :completions => get_complete_function(:complete_add_dev), + :completions => :complete_add_dev, :description => "add app", :help => md""" app add pkg @@ -615,7 +615,7 @@ PSA[:name => "remove", :should_splat => false, :arg_count => 0 => Inf, :arg_parser => parse_package, - :completions => get_complete_function(:complete_installed_apps), + :completions => :complete_installed_apps, :description => "remove packages from project or manifest", :help => md""" app [rm|remove] pkg ... @@ -630,7 +630,7 @@ PSA[:name => "develop", :should_splat => false, :arg_count => 1 => Inf, :arg_parser => (x,y) -> parse_package(x,y; add_or_dev=true), - :completions => get_complete_function(:complete_add_dev), + :completions => :complete_add_dev, :description => "develop a package and install all the apps in it", :help => md""" app [dev|develop] pkg[=uuid] ... From 0bc346a78c8355875d7a43f8fd6ba01220f24bd2 Mon Sep 17 00:00:00 2001 From: Kristoffer Date: Thu, 25 Jul 2024 15:20:08 +0200 Subject: [PATCH 07/12] wip --- docs/src/apps.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ src/Apps/Apps.jl | 23 ++++++++++++------ 2 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 docs/src/apps.md diff --git a/docs/src/apps.md b/docs/src/apps.md new file mode 100644 index 0000000000..fc3a748966 --- /dev/null +++ b/docs/src/apps.md @@ -0,0 +1,62 @@ +# [**?.** Apps](@id Apps) + +!!! note + The app support in Pkg is currently considered experimental and some functionality and API may change. + +Apps are Julia packages that are intended to be run as a "standalone programs" (by e.g. typing the name of the app in the terminal possibly together with some arguments or flags/options). +This is in contrast to most Julia packages that are used as "libraries" and are loaded by other files or in the Julia REPL. + +## Creating a Julia app + +A Julia app is structured similar to a standard Julia library with the following additions: + +- A `@main` entry point in the package module (see the Julia help on `@main` for details) +- An `[app]` section in the `Project.toml` file listing the executable names that the package provides. + +A very simple example of an app that prints the reversed input arguments would be: + +```julia +# src/MyReverseApp.jl +module MyReverseApp + +function (@main)(ARGS) + for arg in ARGS + print(stdout, reverse(arg), " ") + end + return +end + +end # module +``` + +```toml +# Project.toml + +... + +[apps] +reverse = {} +``` + +The empty table `{}` is to allow for giving metadata about the app but it is currently unused. + +After installing this app one could run: + +``` +$ reverse some input string +emos tupni gnirts +``` + +## Installing Julia apps + +The installation of Julia apps are similar to installing julia libraries but instead of using e.g. `Pkg.add` or `pkg> add` one uses `Pkg.Apps.add` or `pkg> app add`. + +!!! note + The path `.julia/bin` has to be added to your `PATH` in order for + + + +## Other information + +- The app will currently run with the same Julia executable as was used to install the app. To update the Julia executable used run... +- diff --git a/src/Apps/Apps.jl b/src/Apps/Apps.jl index fe298f6050..231e66ade0 100644 --- a/src/Apps/Apps.jl +++ b/src/Apps/Apps.jl @@ -147,14 +147,21 @@ function develop(pkg::String) develop(PackageSpec(pkg)) end +function develop(pkg::Vector{PackageSpec}) + for p in pkg + develop(p) + end +end + function develop(pkg::PackageSpec) + if pkg.path !== nothing + pkg.path == abspath(pkg.path) + end handle_package_input!(pkg) ctx = app_context() - handle_repo_develop!(ctx, pkg, #=shared =# true) - - - project = handle_project_file(pkg.path) + sourcepath = abspath(source_path(ctx.env.manifest_file, pkg)) + project = handle_project_file(sourcepath) # Seems like the `.repo.source` field is not cleared. # At least repo-url is still in the manifest after doing a dev with a path @@ -164,9 +171,9 @@ function develop(pkg::PackageSpec) pkg.repo.source = nothing end - entry = PackageEntry(;apps = project.apps, name = pkg.name, version = project.version, tree_hash = pkg.tree_hash, path = pkg.path, repo = pkg.repo, uuid=pkg.uuid) + entry = PackageEntry(;apps = project.apps, name = pkg.name, version = project.version, tree_hash = pkg.tree_hash, path = sourcepath, repo = pkg.repo, uuid=pkg.uuid) update_app_manifest(entry) - generate_shims_for_apps(entry.name, entry.apps, entry.path) + generate_shims_for_apps(entry.name, entry.apps, sourcepath) end function status(pkgs_or_apps::Vector) @@ -265,7 +272,9 @@ function rm(pkg_or_app::Union{PackageSpec, Nothing}=nothing) @info "Deleted $(appname)" rm_shim(appname; force=true) end - Base.rm(joinpath(APP_ENV_FOLDER, dep.name); recursive=true) + if dep.path === nothing + Base.rm(joinpath(APP_ENV_FOLDER, dep.name); recursive=true) + end else for (uuid, pkg) in manifest.deps app_idx = findfirst(app -> app.name == pkg_or_app, pkg.apps) From c9418283cf58bd6d9ccdb46ea46bf9df1bb2e694 Mon Sep 17 00:00:00 2001 From: Kristoffer Date: Thu, 25 Jul 2024 16:41:20 +0200 Subject: [PATCH 08/12] update the docs a bit --- docs/src/apps.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/apps.md b/docs/src/apps.md index fc3a748966..abd0be080e 100644 --- a/docs/src/apps.md +++ b/docs/src/apps.md @@ -32,7 +32,7 @@ end # module ```toml # Project.toml -... +# standard fields here [apps] reverse = {} @@ -47,16 +47,16 @@ $ reverse some input string emos tupni gnirts ``` +directly in the terminal. + ## Installing Julia apps -The installation of Julia apps are similar to installing julia libraries but instead of using e.g. `Pkg.add` or `pkg> add` one uses `Pkg.Apps.add` or `pkg> app add`. +The installation of Julia apps are similar to installing julia libraries but instead of using e.g. `Pkg.add` or `pkg> add` one uses `Pkg.Apps.add` or `pkg> app add` (`develop` is also available). !!! note - The path `.julia/bin` has to be added to your `PATH` in order for - - + The path `.julia/bin` has to be added to your `PATH` in order for apps to be runnable after being installed. + Pkg currently does not do this for you. ## Other information -- The app will currently run with the same Julia executable as was used to install the app. To update the Julia executable used run... -- +- The app will currently run with the same Julia executable as was used to install the app. From 4e80a88a1d878789864910b7438ae9b9433f5a1d Mon Sep 17 00:00:00 2001 From: Kristoffer Date: Fri, 26 Jul 2024 15:36:13 +0200 Subject: [PATCH 09/12] update --- ext/REPLExt/completions.jl | 2 +- src/Apps/Apps.jl | 165 ++++++++++++++++++++----------------- 2 files changed, 90 insertions(+), 77 deletions(-) diff --git a/ext/REPLExt/completions.jl b/ext/REPLExt/completions.jl index 146367ab09..440192b036 100644 --- a/ext/REPLExt/completions.jl +++ b/ext/REPLExt/completions.jl @@ -168,7 +168,7 @@ end import Pkg: Operations, Types, Apps function complete_installed_apps(options, partial; hint) manifest = try - Types.read_manifest(joinpath(Apps.APP_ENV_FOLDER, "AppManifest.toml")) + Types.read_manifest(joinpath(Apps.app_env_folder(), "AppManifest.toml")) catch err err isa PkgError || rethrow() return String[] diff --git a/src/Apps/Apps.jl b/src/Apps/Apps.jl index 231e66ade0..681c8f5586 100644 --- a/src/Apps/Apps.jl +++ b/src/Apps/Apps.jl @@ -1,32 +1,25 @@ module Apps using Pkg -using Pkg.Types: AppInfo, PackageSpec, Context, EnvCache, PackageEntry, handle_repo_add!, handle_repo_develop!, write_manifest, write_project, +using Pkg.Types: AppInfo, PackageSpec, Context, EnvCache, PackageEntry, Manifest, handle_repo_add!, handle_repo_develop!, write_manifest, write_project, pkgerror using Pkg.Operations: print_single, source_path, update_package_add using Pkg.API: handle_package_input! using TOML, UUIDs import Pkg.Registry -############# -# Constants # -############# +app_env_folder() = joinpath(first(DEPOT_PATH), "environments", "apps") +app_manifest_file() = joinpath(app_env_folder(), "AppManifest.toml") +julia_bin_path() = joinpath(first(DEPOT_PATH), "bin") -# Should use `DEPOT_PATH[1]` instead of `homedir()`? -const APP_ENV_FOLDER = joinpath(homedir(), ".julia", "environments", "apps") -const APP_MANIFEST_FILE = joinpath(APP_ENV_FOLDER, "AppManifest.toml") -const JULIA_BIN_PATH = joinpath(homedir(), ".julia", "bin") +app_context() = Context(env=EnvCache(joinpath(app_env_folder(), "Project.toml"))) -################## -# Helper Methods # -################## - function rm_shim(name; kwargs...) - Base.rm(joinpath(JULIA_BIN_PATH, name); kwargs...) + Base.rm(joinpath(julia_bin_path(), name); kwargs...) end -function handle_project_file(sourcepath) +function get_project(sourcepath) project_file = joinpath(sourcepath, "Project.toml") isfile(project_file) || error("Project file not found: $project_file") @@ -35,13 +28,8 @@ function handle_project_file(sourcepath) return project end -function update_app_manifest(pkg) - manifest = Pkg.Types.read_manifest(APP_MANIFEST_FILE) - manifest.deps[pkg.uuid] = pkg - write_manifest(manifest, APP_MANIFEST_FILE) -end -function overwrite_if_different(file, content) +function overwrite_file_if_different(file, content) if !isfile(file) || read(file, String) != content mkpath(dirname(file)) write(file, content) @@ -76,7 +64,6 @@ function get_max_version_register(pkg::PackageSpec, regs) return (max_v, tree_hash) end -app_context() = Context(env=EnvCache(joinpath(APP_ENV_FOLDER, "Project.toml"))) ################## # Main Functions # @@ -84,6 +71,7 @@ app_context() = Context(env=EnvCache(joinpath(APP_ENV_FOLDER, "Project.toml"))) # TODO: Add functions similar to API that takes name, Vector{String} etc and promotes it to `Vector{PackageSpec}`.. + function add(pkg::String) pkg = PackageSpec(pkg) add(pkg) @@ -95,11 +83,15 @@ function add(pkg::Vector{PackageSpec}) end end + function add(pkg::PackageSpec) handle_package_input!(pkg) ctx = app_context() + manifest = ctx.env.manifest new = false + + # Download package if pkg.repo.source !== nothing || pkg.repo.rev !== nothing entry = Pkg.API.manifest_info(ctx.env.manifest, pkg.uuid) pkg = update_package_add(ctx, pkg, entry, false) @@ -107,42 +99,54 @@ function add(pkg::PackageSpec) else pkgs = [pkg] Pkg.Operations.registry_resolve!(ctx.registries, pkgs) - Pkg.Operations.ensure_resolved(ctx, ctx.env.manifest, pkgs, registry=true) + Pkg.Operations.ensure_resolved(ctx, manifest, pkgs, registry=true) pkg.version, pkg.tree_hash = get_max_version_register(pkg, ctx.registries) new = Pkg.Operations.download_source(ctx, pkgs) end - sourcepath = source_path(ctx.env.manifest_file, pkg) - project = handle_project_file(sourcepath) + sourcepath = source_path(manifest_file, pkg) + project = get_project(sourcepath) # TODO: Wrong if package itself has a sourcepath? - project.entryfile = joinpath(sourcepath, "src", "$(project.name).jl") - # TODO: Type stab - # appdeps = get(project, "appdeps", Dict()) - # merge!(project.deps, appdeps) + entry = PackageEntry(;apps = project.apps, name = pkg.name, version = project.version, tree_hash = pkg.tree_hash, path = pkg.path, repo = pkg.repo, uuid=pkg.uuid) + manifest.deps[pkg.uuid] = entry - projectfile = joinpath(APP_ENV_FOLDER, pkg.name, "Project.toml") - mkpath(dirname(projectfile)) - write_project(project, projectfile) + _resolve(manifest, pkg.name) + @info "For package: $(pkg.name) installed apps $(join(keys(project.apps), ","))" +end - # Move manifest if it exists here. - Pkg.activate(joinpath(APP_ENV_FOLDER, pkg.name)) do - Pkg.instantiate() - end +function _resolve(manifest::Manifest, pkgname=nothing) + for (uuid, pkg) in manifest.deps + if pkgname !== nothing && pkg.name !== pkgname + continue + end + if pkg.path == nothing + projectfile = joinpath(app_env_folder(), pkg.name, "Project.toml") + sourcepath = source_path(app_manifest_file(), pkg) + project = get_project(sourcepath) + project.entryfile = joinpath(sourcepath, "src", "$(project.name).jl") + mkpath(dirname(projectfile)) + write_project(project, projectfile) + # Move manifest if it exists here. + + Pkg.activate(joinpath(app_env_folder(), pkg.name)) do + Pkg.instantiate() + end + else + # TODO: Not hardcode Project.toml + projectfile = joinpath(source_path(app_manifest_file(), pkg), "Project.toml") + @show projectfile + end - if new - # TODO: Call build on the package if it was freshly installed? + # TODO: Julia path + generate_shims_for_apps(pkg.name, pkg.apps, dirname(projectfile), joinpath(Sys.BINDIR, "julia")) end - # Create the new package env. - entry = PackageEntry(;apps = project.apps, name = pkg.name, version = project.version, tree_hash = pkg.tree_hash, path = pkg.path, repo = pkg.repo, uuid=pkg.uuid) - update_app_manifest(entry) - generate_shims_for_apps(entry.name, entry.apps, dirname(projectfile)) + write_manifest(manifest, app_manifest_file()) end - function develop(pkg::String) develop(PackageSpec(pkg)) end @@ -161,7 +165,7 @@ function develop(pkg::PackageSpec) ctx = app_context() handle_repo_develop!(ctx, pkg, #=shared =# true) sourcepath = abspath(source_path(ctx.env.manifest_file, pkg)) - project = handle_project_file(sourcepath) + project = get_project(sourcepath) # Seems like the `.repo.source` field is not cleared. # At least repo-url is still in the manifest after doing a dev with a path @@ -171,9 +175,13 @@ function develop(pkg::PackageSpec) pkg.repo.source = nothing end + entry = PackageEntry(;apps = project.apps, name = pkg.name, version = project.version, tree_hash = pkg.tree_hash, path = sourcepath, repo = pkg.repo, uuid=pkg.uuid) - update_app_manifest(entry) - generate_shims_for_apps(entry.name, entry.apps, sourcepath) + manifest = ctx.env.manifest + manifest.deps[pkg.uuid] = entry + + _resolve(manifest, pkg.name) + @info "For package: $(pkg.name) installed apps: $(join(keys(project.apps), ","))" end function status(pkgs_or_apps::Vector) @@ -193,7 +201,7 @@ function status(pkg_or_app::Union{PackageSpec, Nothing}=nothing) # TODO: Sort. # TODO: Show julia version pkg_or_app = pkg_or_app === nothing ? nothing : pkg_or_app.name - manifest = Pkg.Types.read_manifest(joinpath(APP_ENV_FOLDER, "AppManifest.toml")) + manifest = Pkg.Types.read_manifest(joinpath(app_env_folder(), "AppManifest.toml")) deps = Pkg.Operations.load_manifest_deps(manifest) is_pkg = pkg_or_app !== nothing && any(dep -> dep.name == pkg_or_app, values(manifest.deps)) @@ -223,7 +231,7 @@ end #= function precompile(pkg::Union{Nothing, String}=nothing) - manifest = Pkg.Types.read_manifest(joinpath(APP_ENV_FOLDER, "AppManifest.toml")) + manifest = Pkg.Types.read_manifest(joinpath(app_env_folder(), "AppManifest.toml")) deps = Pkg.Operations.load_manifest_deps(manifest) for dep in deps # TODO: Parallel app compilation..? @@ -231,7 +239,7 @@ function precompile(pkg::Union{Nothing, String}=nothing) if pkg !== nothing && info.name !== pkg continue end - Pkg.activate(joinpath(APP_ENV_FOLDER, info.name)) do + Pkg.activate(joinpath(app_env_folder(), info.name)) do @info "Precompiling $(info.name)..." Pkg.precompile() end @@ -262,18 +270,18 @@ function rm(pkg_or_app::Union{PackageSpec, Nothing}=nothing) require_not_empty(pkg_or_app, :rm) - manifest = Pkg.Types.read_manifest(joinpath(APP_ENV_FOLDER, "AppManifest.toml")) + manifest = Pkg.Types.read_manifest(joinpath(app_env_folder(), "AppManifest.toml")) dep_idx = findfirst(dep -> dep.name == pkg_or_app, manifest.deps) if dep_idx !== nothing dep = manifest.deps[dep_idx] - @info "Deleted all apps for package $(dep.name)" + @info "Deleting all apps for package $(dep.name)" delete!(manifest.deps, dep.uuid) for (appname, appinfo) in dep.apps @info "Deleted $(appname)" rm_shim(appname; force=true) end if dep.path === nothing - Base.rm(joinpath(APP_ENV_FOLDER, dep.name); recursive=true) + Base.rm(joinpath(app_env_folder(), dep.name); recursive=true) end else for (uuid, pkg) in manifest.deps @@ -286,61 +294,68 @@ function rm(pkg_or_app::Union{PackageSpec, Nothing}=nothing) end if isempty(pkg.apps) delete!(manifest.deps, uuid) - Base.rm(joinpath(APP_ENV_FOLDER, pkg.name); recursive=true) + Base.rm(joinpath(app_env_folder(), pkg.name); recursive=true) end end end - Pkg.Types.write_manifest(manifest, APP_MANIFEST_FILE) + Pkg.Types.write_manifest(manifest, app_manifest_file()) return end - ######### # Shims # ######### -function generate_shims_for_apps(pkgname, apps, env) +const SHIM_COMMENT = Sys.iswindows() ? "REM " : "#" +const SHIM_VERSION = 1.0 +const SHIM_HEADER = """$SHIM_COMMENT This file is generated by the Julia package manager. + $SHIM_COMMENT Shim version: $SHIM_VERSION""" + + +function generate_shims_for_apps(pkgname, apps, env, julia) for (_, app) in apps - generate_shim(app, pkgname; env) + generate_shim(pkgname, app, env, julia) end end -function generate_shim(app::AppInfo, pkgname; julia_executable_path::String=joinpath(Sys.BINDIR, "julia"), env=joinpath(homedir(), ".julia", "environments", "apps", pkgname)) +function generate_shim(pkgname, app::AppInfo, env, julia) filename = app.name * (Sys.iswindows() ? ".bat" : "") - julia_bin_filename = joinpath(JULIA_BIN_PATH, filename) + julia_bin_filename = joinpath(julia_bin_path(), filename) mkpath(dirname(filename)) content = if Sys.iswindows() - windows_shim(pkgname, julia_executable_path, env) + windows_shim(pkgname, julia, env) else - bash_shim(pkgname, julia_executable_path, env) + bash_shim(pkgname, julia, env) end - overwrite_if_different(julia_bin_filename, content) + overwrite_file_if_different(julia_bin_filename, content) if Sys.isunix() chmod(julia_bin_filename, 0o755) end end -function bash_shim(pkgname, julia_executable_path::String, env) +function bash_shim(pkgname, julia::String, env) return """ #!/usr/bin/env bash + $SHIM_HEADER + export JULIA_LOAD_PATH=$(repr(env)) - exec $julia_executable_path \\ + exec $julia \\ --startup-file=no \\ -m $(pkgname) \\ "\$@" """ end -function windows_shim(pkgname, julia_executable_path::String, env) +function windows_shim(pkgname, julia::String, env) return """ @echo off set JULIA_LOAD_PATH=$(repr(env)) - $julia_executable_path ^ + $julia ^ --startup-file=no ^ -m $(pkgname) ^ %* @@ -348,8 +363,6 @@ function windows_shim(pkgname, julia_executable_path::String, env) end - - #= ################# # PATH handling # @@ -363,26 +376,26 @@ function add_bindir_to_path() end end -function get_shell_config_file(julia_bin_path) +function get_shell_config_file(julia_bin_path()) home_dir = ENV["HOME"] # Check for various shell configuration files if occursin("/zsh", ENV["SHELL"]) - return (joinpath(home_dir, ".zshrc"), "path=('$julia_bin_path' \$path)\nexport PATH") + return (joinpath(home_dir, ".zshrc"), "path=('$julia_bin_path()' \$path)\nexport PATH") elseif occursin("/bash", ENV["SHELL"]) - return (joinpath(home_dir, ".bashrc"), "export PATH=\"\$PATH:$julia_bin_path\"") + return (joinpath(home_dir, ".bashrc"), "export PATH=\"\$PATH:$julia_bin_path()\"") elseif occursin("/fish", ENV["SHELL"]) - return (joinpath(home_dir, ".config/fish/config.fish"), "set -gx PATH \$PATH $julia_bin_path") + return (joinpath(home_dir, ".config/fish/config.fish"), "set -gx PATH \$PATH $julia_bin_path()") elseif occursin("/ksh", ENV["SHELL"]) - return (joinpath(home_dir, ".kshrc"), "export PATH=\"\$PATH:$julia_bin_path\"") + return (joinpath(home_dir, ".kshrc"), "export PATH=\"\$PATH:$julia_bin_path()\"") elseif occursin("/tcsh", ENV["SHELL"]) || occursin("/csh", ENV["SHELL"]) - return (joinpath(home_dir, ".tcshrc"), "setenv PATH \$PATH:$julia_bin_path") # or .cshrc + return (joinpath(home_dir, ".tcshrc"), "setenv PATH \$PATH:$julia_bin_path()") # or .cshrc else return (nothing, nothing) end end function update_unix_PATH() - shell_config_file, path_command = get_shell_config_file(JULIA_BIN_PATH) + shell_config_file, path_command = get_shell_config_file(julia_bin_path()) if shell_config_file === nothing @warn "Failed to insert `.julia/bin` to PATH: Failed to detect shell" return @@ -411,8 +424,8 @@ end function update_windows_PATH() current_path = ENV["PATH"] - occursin(JULIA_BIN_PATH, current_path) && return - new_path = "$current_path;$JULIA_BIN_PATH" + occursin(julia_bin_path(), current_path) && return + new_path = "$current_path;$julia_bin_path()" run(`setx PATH "$new_path"`) end =# From 97b5d4ea6d2719f328c1e5752714663e8c4ecd75 Mon Sep 17 00:00:00 2001 From: Kristoffer Date: Fri, 26 Jul 2024 15:54:08 +0200 Subject: [PATCH 10/12] use manifest if it exists --- src/Apps/Apps.jl | 74 ++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/src/Apps/Apps.jl b/src/Apps/Apps.jl index 681c8f5586..7a6b6f3c50 100644 --- a/src/Apps/Apps.jl +++ b/src/Apps/Apps.jl @@ -2,7 +2,7 @@ module Apps using Pkg using Pkg.Types: AppInfo, PackageSpec, Context, EnvCache, PackageEntry, Manifest, handle_repo_add!, handle_repo_develop!, write_manifest, write_project, - pkgerror + pkgerror, projectfile_path, manifestfile_path using Pkg.Operations: print_single, source_path, update_package_add using Pkg.API: handle_package_input! using TOML, UUIDs @@ -20,7 +20,8 @@ function rm_shim(name; kwargs...) end function get_project(sourcepath) - project_file = joinpath(sourcepath, "Project.toml") + project_file = projectfile_path(sourcepath) + isfile(project_file) || error("Project file not found: $project_file") project = Pkg.Types.read_project(project_file) @@ -69,8 +70,43 @@ end # Main Functions # ################## -# TODO: Add functions similar to API that takes name, Vector{String} etc and promotes it to `Vector{PackageSpec}`.. +function _resolve(manifest::Manifest, pkgname=nothing) + for (uuid, pkg) in manifest.deps + if pkgname !== nothing && pkg.name !== pkgname + continue + end + if pkg.path == nothing + projectfile = joinpath(app_env_folder(), pkg.name, "Project.toml") + sourcepath = source_path(app_manifest_file(), pkg) + project = get_project(sourcepath) + project.entryfile = joinpath(sourcepath, "src", "$(project.name).jl") + mkpath(dirname(projectfile)) + write_project(project, projectfile) + package_manifest_file = manifestfile_path(sourcepath; strict=true) + if package_manifest_file !== nothing + cp(package_manifest_file, joinpath(app_env_folder(), pkg.name, basename(package_manifest_file)); force=true) + end + # Move manifest if it exists here. + + Pkg.activate(joinpath(app_env_folder(), pkg.name)) do + Pkg.instantiate() + end + else + # TODO: Not hardcode Project.toml + projectfile = projectfile_path(source_path(app_manifest_file(), pkg)) + end + + # TODO: Julia path + generate_shims_for_apps(pkg.name, pkg.apps, dirname(projectfile), joinpath(Sys.BINDIR, "julia")) + end + + write_manifest(manifest, app_manifest_file()) +end + +function _gc() + +end function add(pkg::String) pkg = PackageSpec(pkg) @@ -106,7 +142,7 @@ function add(pkg::PackageSpec) new = Pkg.Operations.download_source(ctx, pkgs) end - sourcepath = source_path(manifest_file, pkg) + sourcepath = source_path(ctx.env.manifest_file, pkg) project = get_project(sourcepath) # TODO: Wrong if package itself has a sourcepath? @@ -117,36 +153,6 @@ function add(pkg::PackageSpec) @info "For package: $(pkg.name) installed apps $(join(keys(project.apps), ","))" end -function _resolve(manifest::Manifest, pkgname=nothing) - for (uuid, pkg) in manifest.deps - if pkgname !== nothing && pkg.name !== pkgname - continue - end - if pkg.path == nothing - projectfile = joinpath(app_env_folder(), pkg.name, "Project.toml") - sourcepath = source_path(app_manifest_file(), pkg) - project = get_project(sourcepath) - project.entryfile = joinpath(sourcepath, "src", "$(project.name).jl") - mkpath(dirname(projectfile)) - write_project(project, projectfile) - # Move manifest if it exists here. - - Pkg.activate(joinpath(app_env_folder(), pkg.name)) do - Pkg.instantiate() - end - else - # TODO: Not hardcode Project.toml - projectfile = joinpath(source_path(app_manifest_file(), pkg), "Project.toml") - @show projectfile - end - - # TODO: Julia path - generate_shims_for_apps(pkg.name, pkg.apps, dirname(projectfile), joinpath(Sys.BINDIR, "julia")) - end - - write_manifest(manifest, app_manifest_file()) -end - function develop(pkg::String) develop(PackageSpec(pkg)) end From 75dcdef0138a72e466e7feea6a286862938a6975 Mon Sep 17 00:00:00 2001 From: Kristoffer Date: Fri, 26 Jul 2024 16:15:28 +0200 Subject: [PATCH 11/12] contract status --- src/Apps/Apps.jl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Apps/Apps.jl b/src/Apps/Apps.jl index 7a6b6f3c50..1fcb3a81af 100644 --- a/src/Apps/Apps.jl +++ b/src/Apps/Apps.jl @@ -104,10 +104,6 @@ function _resolve(manifest::Manifest, pkgname=nothing) write_manifest(manifest, app_manifest_file()) end -function _gc() - -end - function add(pkg::String) pkg = PackageSpec(pkg) add(pkg) @@ -205,7 +201,6 @@ end function status(pkg_or_app::Union{PackageSpec, Nothing}=nothing) # TODO: Sort. - # TODO: Show julia version pkg_or_app = pkg_or_app === nothing ? nothing : pkg_or_app.name manifest = Pkg.Types.read_manifest(joinpath(app_env_folder(), "AppManifest.toml")) deps = Pkg.Operations.load_manifest_deps(manifest) @@ -230,7 +225,8 @@ function status(pkg_or_app::Union{PackageSpec, Nothing}=nothing) if !is_pkg && pkg_or_app !== nothing && appname !== pkg_or_app continue end - printstyled(" $(appname) $(appinfo.julia_command) \n", color=:green) + julia_cmd = contractuser(appinfo.julia_command) + printstyled(" $(appname) $(julia_cmd) \n", color=:green) end end end From 177c751feea10045d51274a103ea1240359030d8 Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Thu, 7 Nov 2024 16:32:03 +0100 Subject: [PATCH 12/12] Run `mkpath` also in `write_manifest` Apps only write a manifest so can't rely on the path being created by `write_project`. Also moves the `mkpath` call for `write_project` to the method that actually does the filesystem writing with `write`. --- src/manifest.jl | 1 + src/project.jl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/manifest.jl b/src/manifest.jl index 7b649e844b..306d5ef0fa 100644 --- a/src/manifest.jl +++ b/src/manifest.jl @@ -364,6 +364,7 @@ function write_manifest(io::IO, raw_manifest::Dict) end function write_manifest(raw_manifest::Dict, manifest_file::AbstractString) str = sprint(write_manifest, raw_manifest) + mkpath(dirname(manifest_file)) write(manifest_file, str) end diff --git a/src/project.jl b/src/project.jl index 448c13da8c..5c77e7f05e 100644 --- a/src/project.jl +++ b/src/project.jl @@ -275,7 +275,6 @@ project_key_order(key::String) = something(findfirst(x -> x == key, _project_key_order), length(_project_key_order) + 1) function write_project(env::EnvCache) - mkpath(dirname(env.project_file)) write_project(env.project, env.project_file) end write_project(project::Project, project_file::AbstractString) = @@ -296,5 +295,6 @@ function write_project(io::IO, project::Dict) end function write_project(project::Dict, project_file::AbstractString) str = sprint(write_project, project) + mkpath(dirname(project_file)) write(project_file, str) end