What is the correct way to read dependency tree?

Hey, I want to parse app deps tree and generate a map.
I’m currently reading top-level deps like:

top_level_deps = Mix.Dep.loaded([]) |> Enum.filter(& &1.top_level)

I use Enum.reduce in recursive method for top_level_deps and then for dep.deps.
Everything is good until 4th level where is no deps …
For example:
App A require B that requires C that requires D - I see empty list when inspect C deps (also no hex packages here).
When I modify app B to require D and C not require D then I see all deps in my map.
What is the correct way to read dependency tree?

Steps to reproduce:
1 Create a tmp dir for apps
2 Go to tmp dir and create 4 apps like:
mix new a
mix new b
mix new c
mix new d
3 Add deps for a, b and c apps like:

defp deps do
   [{:b, path: "../b"}]
end

4 Add this task to lib/mix/tasks/ to a project:

defmodule Mix.Tasks.Reproduce do
    use Mix.Task
    def run(_) do
        app_atom = Mix.Project.config[:app]
        top_level_deps = Mix.Dep.loaded([]) |> Enum.filter(& &1.top_level)
        result = reproduce top_level_deps, app_atom
        IO.inspect Map.put_new result, app_atom, Atom.to_string(app_atom)
    end
    
    defp reproduce deps, prefix, result \\ Map.new do
        Enum.reduce deps, result, fn(dep, result) ->
            if dep.scm != Hex.SCM do # filter Hex packages here
                new_prefix = "#{prefix}_#{dep.app}"
                new_result = reproduce dep.deps, new_prefix, result
                if dep.app == :c do
                    IO.puts "No deps here !!!"
                    IO.inspect dep.deps
                end
                Map.put_new new_result, dep.app, new_prefix
            else
                result
            end
        end
    end
end

5 Run mix reproduce
Current results:

%{a: "a", b: "a_b", c: "a_b_c"}

Expected results:

%{a: "a", b: "a_b", c: "a_b_c", d: "a_b_c_d"}

Solved at: stackoverflow

1 Like

I need to add a disclaimer that Mix.Dep is not a public API. You are not supposed to be calling it and there is no guarantee the module nor the function will exist in future Elixir versions. :slight_smile:

1 Like

/me wishes that Mix some-how would not work at run-time, only at build-time, since it does not work in a release and there are occasional libraries that call into Mix at run-time, which then breaks horribly when a release is made… >.>

@josevalim: Is it available a public API that can provide me correct display of the dependency tree?
@OvermindDL1: Do you know any libraries that can provide me correct display of the dependency tree?
I understand very well that maintaining code that does not use the public API is not a good idea, so I’m open to your advice.

There are lots of dependency trees that you can grab. If you want, say, an Application supervision tree then that is doable at runtime. But if you are looking for mix dependencies, well those vanish after compile as far as I’ve seen (curious if this is otherwise though?).

I’m looking for way to get dependency tree from deps methods from mix.exs (only deps with path option specified ie. local dependencies).

You wrote:

But if you are looking for mix dependencies, well those vanish after compile (…)

so everything should be ok if I collect data inside macro and set result in module attribute.
Then i can do this (run macro) at compile time like this:

defmodule A do
	defmacro set_result do
		quote do
			@my_result :rand.uniform(10) # here I replace with my code
		end
	end
end

defmodule B do
    import A
    
    set_result # use macro from A module that sets result at compile time
    
    def my_result_inspect do
        IO.inspect @my_result # outputs: 7
    end
end

What do you think about this?

Hmm, actually that would work, build up the dep list at compile time, generate a new module with the information, should work yeah. Like an old C/C++ ‘config’ file generator. Someone should make something generic like that for Elixir. :slight_smile:

Ok, I’ll try to do it. You have already confirmed the matter of macros. What do you think about the API? Should I use the same or maybe you have a better suggestion? Do you know a library that could help me?

I believe Mix.Project.deps_paths is the only public API we provide so far.

Is it a good idea to iterate this map for each key and read info like:

path = Mix.Project.deps_paths[key]
Mix.Project.in_project key, path, fn mixfile -> IO.inspect mixfile.project end

?
Instead IO.inspect I can add Map.put_new and Mix.Project.in_project will return map with new entry.
@OvermindDL1: In that way I do not need to use macros. What do you think about it?

Give it a try? :slight_smile:

If you think this is a good enough way to “Give it a try” then everything is ok. I’m just new in Elixir and want to know if what I think is good way to do this thing ie. “good or bad practise”.
Thx for your help. I will try rewrite my code.

Honestly you are in pretty new territory from what I’ve seen, so testing things and reporting back what work, what does not, and other things you think should probably be done is the best documentation for now. :slight_smile:

2 Likes

I looked again on the things that I need. Create a tree is possible, but not need now. Anyway reading dependencies and other infos from project and it’s dependencies could be useful for service account.

Hi @Eiji I couldn’t be success to read what dependencies installed in my project and get a list of them. I tried the code you wrote, but it didn’t work, and I change to this; it just shows me mixfile module name, not the deps what were installed.

Mix.Project.in_project(:mishka_html, path, fn module ->
 "Mix project is: #{inspect(module)}"
end)

I can use MishkaHtml.MixProject.project[:deps] and see what dependencies were installed, but it doesn’t show as a tree

by the way I found this function Mix.Project.deps_apps, but it loads all deps name not separately for each project in DDD (umbrella)

1 Like

That was because it depend on private API and it has been removed.

btw. I would say that my topic wants to eat your :brain:, but its body has probably been decomposed a long time ago. :smile: That’s said I still remember this hot sunny day … There was about 30°C, no clouds on sky and the world … it was like having … freedom! :smiling_imp:

If I remember correctly at the time of writing this topic I was using 1.3.0-rc.1 release as I had lots of free time, so I was spending it on testing latest changes in release candidates and sometimes even on master brach …However I can’t blame you for necro-bumping as said Elixir version was really similar to current 1.13.1 release … :sweat_smile:

ok, jokes aside …

This is because you have a flat List of top-level dependencies for project which owns said MishkaHtml.MixProject module …

You need to combine 3 functions in recursion …

  1. Mix.Project.in_project/4 to dynamically get a project/dependency module
  2. Mix.Project.deps_path/1 to get path for dependencies
  3. MixProject.project()[:deps] to get top-level dependencies of project/dependency

Also remember that if dependency have specified path option then you need to handle it too as in such case it’s not in deps directory …

2 Likes

I am very sorry when I wanted to send this question, I didn’t think this post was for 5 years :wink:

2 Likes