diff --git a/Changelog.md b/Changelog.md index 0af8a1a..7e6a6dd 100644 --- a/Changelog.md +++ b/Changelog.md @@ -38,6 +38,10 @@ The version numbers roughly follow SemVer Possible categories: [Added, Changed, Fixed, Removed, Security, Deprecated (for soon-to-be removed features)] --> +- Remove limitation "package must be installed in active project". + Any package in the General registry (and standard library) can now be + queried from anywhere. + - This (re)introduced dependencies: Pkg (a big one; but stdlib), and UUIDS. - Dark-mode option for _all_ generated images\ (not just local SVGs; also PNGs and webapp URLs) - Pass the `mode=:dark` keyword argument to `open` and `create` for this. diff --git a/Project.toml b/Project.toml index 3fa375c..c903344 100644 --- a/Project.toml +++ b/Project.toml @@ -6,8 +6,10 @@ version = "0.4.0-dev" [deps] DefaultApplication = "3f0dd361-4fe0-5fc6-8523-80b14ec94d85" EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" +UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] DefaultApplication = "1" diff --git a/ReadMe.md b/ReadMe.md index d3ebbcd..643f5a7 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -30,19 +30,7 @@ This will open the browser to [this url][dotlink], which renders something like width=680 alt="Dependency graph of Unitful, rendered with Graphviz dot"> -
-
- - The given package (here: [Unitful][unitful]) must be installed in the currently active project for this to work. - - Note that `PkgGraph` does not have to be installed in the same project however:\ - you can switch projects _after_ `PkgGraph` has been imported (using `pkg> activate …`). - - Even easier is to install `PkgGraph` in your base environment (see [Global Install](#global-install)), - so you don't have to switch projects at all. - -
To filter out binary dependencies ([JLL packages]) or packages from the Julia standard library, you can set the keyword arguments `jll = false` and `stdlib = false`. @@ -85,7 +73,7 @@ pkg> add PkgGraph You might want to install `PkgGraph` in your base environment (e.g. `v1.8`).\ You can then use it in any project, without having to install it in that project -or having to always switch projects. +or having to switch projects.
diff --git a/docs/Manifest.toml b/docs/Manifest.toml index 4f7c516..0cddc25 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -164,7 +164,7 @@ uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" version = "1.8.0" [[deps.PkgGraph]] -deps = ["DefaultApplication", "EzXML", "TOML", "URIs"] +deps = ["DefaultApplication", "EzXML", "Pkg", "TOML", "URIs", "UUIDs"] path = ".." uuid = "f9c1b9e4-72e8-4a14-ade5-14f45fc35f11" version = "0.4.0-dev" diff --git a/docs/make.jl b/docs/make.jl index 618f270..6144212 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -22,7 +22,7 @@ DocMeta.setdocmeta!(PkgGraph, :DocTestSetup, :(using PkgGraph); recursive=true, println("") # ..including Documenter.HTML(…) construction call makedocs( source = srcmod, - modules = [PkgGraph], + # modules = [PkgGraph], # ↪ To get a warning if there are any docstrings not mentioned in the markdown. sitename = "PkgGraph.jl", # ↪ Displayed in page title and navbar. diff --git a/docs/src/ref/internals.md b/docs/src/ref/internals.md index ee07a84..fb48d1e 100644 --- a/docs/src/ref/internals.md +++ b/docs/src/ref/internals.md @@ -13,7 +13,6 @@ Internals ```@docs depgraph -packages_in_active_manifest is_jll is_in_stdlib ``` diff --git a/src/enduser.jl b/src/enduser.jl index 1aa99b2..49d2ed8 100644 --- a/src/enduser.jl +++ b/src/enduser.jl @@ -4,8 +4,6 @@ Open the browser to an image of `pkgname`'s dependency graph. -The given package must be installed in the currently active project. - See [Settings](@ref) for possible keyword arguments. """ function open(pkgname; dryrun = false, kw...) @@ -27,8 +25,6 @@ and open it with your default image viewer. Uses the external program '`dot`' (see [graphviz.org](https://graphviz.org)), which must be available on `PATH`. -The given package must be installed in the currently active project. - `fmt` is an output file format supported by dot, such as `:svg` or `:png`.\\ If `fmt` is `:svg`, the generated SVG file is post-processed, to add light and dark-mode CSS. diff --git a/src/internals/Internals.jl b/src/internals/Internals.jl index b48b7e3..ccef58e 100644 --- a/src/internals/Internals.jl +++ b/src/internals/Internals.jl @@ -11,10 +11,17 @@ can be accessed as `PkgGraph.depgraph`, e.g. module Internals using TOML +using UUIDs +include("stdlib.jl") + using Base: active_project +include("project.jl") + +using Pkg +include("registry.jl") + include("depgraph.jl") export depgraph, - packages_in_active_manifest, should_be_included, is_jll, is_in_stdlib, diff --git a/src/internals/depgraph.jl b/src/internals/depgraph.jl index 0c6da3c..bf096e7 100644 --- a/src/internals/depgraph.jl +++ b/src/internals/depgraph.jl @@ -28,23 +28,20 @@ julia> depgraph(:Test) "Test" => "Serialization" ``` """ -depgraph(pkgname; jll = true, stdlib = true) = begin +depgraph(pkgname; jll = true, stdlib = true, verbose = false) = begin rootpkg = string(pkgname) - packages = packages_in_active_manifest() - include_jll = jll - include_stdlib = stdlib - if rootpkg ∉ keys(packages) - error(""" - The given package ($pkgname) must be installed in the active project - (which is currently `$(active_project())`)""") + if is_in_project(rootpkg) + verbose && @info "Package `$rootpkg` found in active project. Using Manifest.toml" + direct_deps = direct_deps_from_project() + else + verbose && @info "Package `$rootpkg` not found in active project. Using General registry" + direct_deps = direct_deps_from_registry end deps = Vector{Pair{String, String}}() - add_deps_of(name) = begin - pkg_info = only(packages[name]) # Two packages with same name not supported. - direct_deps = get(pkg_info, "deps", []) - for dep in direct_deps - if should_be_included(dep; include_jll, include_stdlib) - push!(deps, name => dep) + add_deps_of(pkg) = begin + for dep in direct_deps(pkg) + if should_be_included(dep, include_jll=jll, include_stdlib=stdlib) + push!(deps, pkg => dep) add_deps_of(dep) end end @@ -53,40 +50,6 @@ depgraph(pkgname; jll = true, stdlib = true) = begin return unique!(deps) # Could use a SortedSet instead; but this spares a pkg load. end -manifest(proj_path) = replace(proj_path, "Project.toml" => "Manifest.toml") - -if VERSION ≥ v"1.7" - packages_in(manifest) = TOML.parsefile(manifest)["deps"] -else - packages_in(manifest) = TOML.parsefile(manifest) -end - -""" - packages_in_active_manifest() - -Read and parse the `Manifest.toml` of the active project, and return its -'deps' table (as a dictionary indexed by package names). - -Every entry in this dictionary is a list. This is for when multiple -packages would share the same name. - -## Example: - -```jldoctest; filter = r" => .*\$"m -julia> using PkgGraph.Internals - -julia> packages = packages_in_active_manifest(); - -julia> only(packages["PkgGraph"]) -Dict{String, Any} with 4 entries: - "deps" => ["DefaultApplication", "TOML", "URIs"] - "uuid" => "f9c1b9e4-72e8-4a14-ade5-14f45fc35f11" - "version" => "0.1.0" - "path" => "C:\\Users\\tfiers\\.julia\\dev\\PkgGraph" -``` -""" -packages_in_active_manifest() = packages_in(manifest(active_project())) - should_be_included(pkg; include_jll = true, include_stdlib = true) = if !include_jll && is_jll(pkg) @@ -102,26 +65,7 @@ should_be_included(pkg; include_jll = true, include_stdlib = true) = """ is_jll(pkg) = endswith(pkg, "_jll") - """ is_in_stdlib(pkg)::Bool """ -is_in_stdlib(pkg) = pkg in STDLIB - -stdlib_packages() = begin - packages = Set{String}() - for path in readdir(Sys.STDLIB; join = true) - # ↪ `join` gets us complete paths - if isdir(path) - push!(packages, pkgname(path)) - end - end - packages -end -pkgname(pkgdir) = begin - proj_file = joinpath(pkgdir, "Project.toml") - toml_dict = TOML.parsefile(proj_file) - pkgname = toml_dict["name"] -end - -const STDLIB = stdlib_packages() +is_in_stdlib(pkg) = pkg in STDLIB_NAMES diff --git a/src/internals/project.jl b/src/internals/project.jl new file mode 100644 index 0000000..eec92f5 --- /dev/null +++ b/src/internals/project.jl @@ -0,0 +1,78 @@ + +using TOML +using Base: active_project + +is_in_project(pkg, proj = active_project()) = + isfile(proj) && (name(proj) == pkg || pkg in keys(all_deps(proj))) + +name(proj_path) = name(dict(proj_path)) +dict(proj_path) = TOML.parsefile(proj_path) +name(toml::Dict) = get(toml, "name", nothing) + +all_deps(project) = begin + mani = manifest(project) + if !isfile(mani) + return Dict() + end + @static if VERSION ≥ v"1.7" + deps = TOML.parsefile(mani)["deps"] + else + deps = TOML.parsefile(mani) + end +end +manifest(project) = replace(project, "Project.toml" => "Manifest.toml") + +direct_deps_from_project(proj = active_project()) = begin + proj_dict = dict(proj) + proj_name = name(proj_dict) + all_deps_ = all_deps(proj) + direct_deps(pkgname) = + if pkgname == proj_name + get(proj_dict, "deps", []) |> keys + else + deps_with_name = all_deps_[pkgname] + check_only(deps_with_name) + dep_dict = only(deps_with_name) + get(dep_dict, "deps", []) + end + direct_deps +end +check_only(packages_with_same_name) = @assert( + length(packages_with_same_name) == 1, + """ + Different packages with same name not supported + (The offending packages:) + $packages_with_same_name + """ +) + +# The above is poop: we want dep tree of entire thing, also if no top specified +# no name. then there's multiple roots, sure (or take as name, the directory) +# + + +""" + packages_in_active_manifest() + +Read and parse the `Manifest.toml` of the given project, and return its +'deps' table (as a dictionary indexed by package names). + +Every entry in this dictionary is a list. This is for when multiple +packages would share the same name. + +## Example: + +```jldoctest; filter = r" => .*\$"m +julia> using PkgGraph.Internals + +julia> packages = packages_in_active_manifest(); + +julia> only(packages["PkgGraph"]) +Dict{String, Any} with 4 entries: + "deps" => ["DefaultApplication", "TOML", "URIs"] + "uuid" => "f9c1b9e4-72e8-4a14-ade5-14f45fc35f11" + "version" => "0.1.0" + "path" => "C:\\Users\\tfiers\\.julia\\dev\\PkgGraph" +``` +""" +packages_in_active_manifest() = all_deps(active_project()) diff --git a/src/internals/registry.jl b/src/internals/registry.jl new file mode 100644 index 0000000..d1d3f56 --- /dev/null +++ b/src/internals/registry.jl @@ -0,0 +1,50 @@ +using Pkg.Registry: reachable_registries, + uuids_from_name, + init_package_info!, + initialize_uncompressed!, + JULIA_UUID + + +const reg = first(reachable_registries()) +@assert reg.name == "General" + +name(uuid::UUID) = + if uuid in STDLIB_UUIDS + STDLIB[uuid] + elseif uuid in keys(reg.pkgs) + reg.pkgs[uuid].name + else + error() + end + +uuid(name::AbstractString) = + if name in STDLIB_NAMES + findfirst(==(name), STDLIB) + else + uuids = uuids_from_name(reg, name) + if isempty(uuids) + error("Package `$name` not found") + elseif length(uuids) > 1 + error("Multiple packages with the same name (`$name`) not supported") + else + return only(uuids) + end + end + +direct_deps_from_registry(pkg) = begin + if pkg in STDLIB_NAMES + return direct_deps_of_stdlib_pkg(pkg) + end + pkgentry = reg.pkgs[uuid(pkg)] + p = init_package_info!(pkgentry) + versions = keys(p.version_info) + v = maximum(versions) + initialize_uncompressed!(p, [v]) + vinfo = p.version_info[v] + compat_info = vinfo.uncompressed_compat + # ↪ All direct deps will be here, even if author didn't them + # [compat] (their versionspec will just be "*"). + direct_dep_uuids = collect(keys(compat_info)) + filter!(!=(JULIA_UUID), direct_dep_uuids) + return name.(direct_dep_uuids) +end diff --git a/src/internals/stdlib.jl b/src/internals/stdlib.jl new file mode 100644 index 0000000..ceeecf9 --- /dev/null +++ b/src/internals/stdlib.jl @@ -0,0 +1,26 @@ +using UUIDs +using TOML + +stdlib() = begin + packages = Dict{UUID,String}() + for path in readdir(Sys.STDLIB; join = true) + # ↪ `join` gets us complete paths + if isdir(path) + toml = proj_dict(path) + push!(packages, UUID(toml["uuid"]) => toml["name"]) + end + end + packages +end +proj_dict(pkgdir) = TOML.parsefile(proj_file(pkgdir)) +proj_file(pkgdir) = joinpath(pkgdir, "Project.toml") + +const STDLIB = stdlib() +const STDLIB_UUIDS = keys(STDLIB) +const STDLIB_NAMES = values(STDLIB) + +direct_deps_of_stdlib_pkg(name) = begin + pkgdir = joinpath(Sys.STDLIB, name) + d = proj_dict(pkgdir) + keys(get(d, "deps", [])) +end diff --git a/test/integration.jl b/test/integration.jl index 278703b..bd60773 100644 --- a/test/integration.jl +++ b/test/integration.jl @@ -7,6 +7,7 @@ using Test @test isnothing(PkgGraph.open("Test", dryrun = true)) @test isnothing(PkgGraph.create("Test", dryrun = true)) + @test isnothing(PkgGraph.create("PyPlot", dryrun = true)) end diff --git a/test/unit.jl b/test/unit.jl index 0ac0fc7..9a85c74 100644 --- a/test/unit.jl +++ b/test/unit.jl @@ -13,7 +13,7 @@ using Test if VERSION ≥ v"1.8" # Julia v1.7 does not support error string matching @test_throws( - "The given package (DinnaeExist) must be installed in the active project", + "Package `DinnaeExist` not found", depgraph("DinnaeExist") ) end