Skip to content

Propagate dependencies in mix tasks #1068

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 15, 2013
Merged
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
35 changes: 32 additions & 3 deletions lib/mix/lib/mix/deps.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defrecord Mix.Dep, [scm: nil, app: nil, requirement: nil, status: nil, opts: nil, project: nil] do
defrecord Mix.Dep, [ scm: nil, app: nil, requirement: nil, status: nil, opts: nil,
project: nil, deps: [] ] do
@moduledoc """
This is a record that keeps information about your project
dependencies. It keeps:
Expand Down Expand Up @@ -46,18 +47,29 @@ defmodule Mix.Deps do
Mix.Deps.Project.all
end

@doc """
Returns all dependencies depending on given dependencies.
"""
def depending(deps, all_deps // all) do
dep_names = Enum.map(deps, fn dep -> dep.app end)
parents = Enum.filter all_deps, fn dep ->
Enum.any?(dep.deps, fn child_dep -> child_dep.app in dep_names end)
end
parents ++ (if parents != [], do: depending(parents, all_deps), else: [])
end

@doc """
Receives a list of deps names and returns deps records.
Raises an error if the dependency does not exist.
"""
def by_name!(given) do
def by_name!(given, all_deps // all) do
# Ensure all apps are atoms
apps = Enum.map given, fn(app) ->
if is_binary(app), do: binary_to_atom(app), else: app
end

# We need to keep the order of all, which properly orders deps
deps = Enum.filter all, fn(dep) -> dep.app in apps end
deps = Enum.filter all_deps, fn(dep) -> dep.app in apps end

# Now we validate the given atoms
index = Mix.Dep.__index__(:app)
Expand All @@ -70,6 +82,23 @@ defmodule Mix.Deps do
deps
end

@doc """
Runs the given `fun` inside the given dependency project by
changing the current working directory and loading the given
project into the project stack.
"""
def in_dependency(Mix.Dep[app: app, opts: opts], post_config // [], fun) do
env = opts[:env] || :prod
old_env = Mix.env

try do
Mix.env(env)
Mix.Project.in_project(app, opts[:dest], post_config, fun)
after
Mix.env(old_env)
end
end

@doc """
Formats the status of a dependency.
"""
Expand Down
41 changes: 29 additions & 12 deletions lib/mix/lib/mix/deps/converger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ defmodule Mix.Deps.Converger do
main = Enum.reverse Mix.Deps.Project.all
config = [ deps_path: Path.expand(Mix.project[:deps_path]),
root_lockfile: Path.expand(Mix.project[:lockfile]) ]
all(main, [], [], main, config, callback, rest)
{ deps, rest } = all(main, [], [], main, config, callback, rest)
{ nest_deps(deps, config), rest }
end

# We traverse the tree of dependencies in a breadth-
Expand Down Expand Up @@ -115,19 +116,35 @@ defmodule Mix.Deps.Converger do
File.regular?(Path.join dep.opts[:dest], "mix.exs")
end

# Sets the `deps` field to the child dependencies of all
# given dependencies and does so recursively.
defp nest_deps(deps, config) do
Enum.map deps, fn dep ->
nest_deps(dep, deps, config)
end
end

defp nest_deps(dep, deps, config) do
if available?(dep) and mixfile?(dep) do
nested_apps = nested_names(dep, config)
sub_deps = Enum.filter(deps, fn dep -> dep.app in nested_apps end)
dep.deps Enum.map(sub_deps, nest_deps(&1, deps, config))
else
dep
end
end

defp nested_names(dep, post_config) do
Mix.Deps.in_dependency dep, post_config, fn _ ->
Mix.Deps.Project.all_names
end
end

# The dependency contains a Mixfile, so let's
# load it and retrieve its nested dependencies.
defp nested_deps(Mix.Dep[app: app, opts: opts], post_config) do
env = opts[:env] || :prod
old_env = Mix.env

try do
Mix.env(env)
Mix.Project.in_project(app, opts[:dest], post_config, fn project ->
{ project, Enum.reverse Mix.Deps.Project.all }
end)
after
Mix.env(old_env)
defp nested_deps(dep, post_config) do
Mix.Deps.in_dependency dep, post_config, fn project ->
{ project, Enum.reverse Mix.Deps.Project.all }
end
end
end
10 changes: 9 additions & 1 deletion lib/mix/lib/mix/deps/project.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ defmodule Mix.Deps.Project do
Enum.map deps, with_scm_and_status(&1, scms)
end

@doc """
Returns all application names of current project's dependencies.
"""
def all_names do
deps = Mix.project[:deps] || []
Enum.map(deps, elem(&1, 0))
end

@doc """
Receives a dependency and update its status.
"""
Expand Down Expand Up @@ -98,4 +106,4 @@ defmodule Mix.Deps.Project do
defp vsn_match?(nil, _actual), do: true
defp vsn_match?(expected, actual) when is_binary(expected), do: actual == expected
defp vsn_match?(expected, actual) when is_regex(expected), do: actual =~ expected
end
end
8 changes: 5 additions & 3 deletions lib/mix/lib/mix/tasks/deps.compile.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@ defmodule Mix.Tasks.Deps.Compile do

"""

import Mix.Deps, only: [ all: 0, available?: 1, by_name!: 1, compile_paths: 1,
format_dep: 1, make?: 1, mix?: 1, rebar?: 1 ]
import Mix.Deps, only: [ all: 0, available?: 1, by_name!: 2, compile_paths: 1,
depending: 2, format_dep: 1, make?: 1, mix?: 1, rebar?: 1 ]

def run(args) do
case OptionParser.parse(args) do
{ _, [] } ->
do_compile Enum.filter(all, available?(&1))
{ _, tail } ->
do_compile by_name!(tail)
all_deps = all
deps = by_name!(tail, all_deps)
do_compile(deps ++ depending(deps, all_deps))
end
end

Expand Down
7 changes: 5 additions & 2 deletions lib/mix/lib/mix/tasks/deps.update.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ defmodule Mix.Tasks.Deps.Update do
* `--no-compile` skip compilation of dependencies
"""

import Mix.Deps, only: [all: 2, available?: 1, by_name!: 1, format_dep: 1]
import Mix.Deps, only: [ all: 0, all: 2, available?: 1, by_name!: 2,
depending: 2, format_dep: 1 ]

def run(args) do
{ opts, rest } = OptionParser.parse(args, switches: [no_compile: :boolean])

if rest != [] do
deps = Enum.map by_name!(rest), check_unavailable!(&1)
all_deps = all
deps = Enum.map by_name!(rest, all_deps), check_unavailable!(&1)
deps = deps ++ depending(deps, all_deps)
{ _, acc } = Enum.map_reduce deps, init, deps_updater(&1, &2)
else
acc = all(init, deps_updater(&1, &2))
Expand Down
26 changes: 22 additions & 4 deletions lib/mix/test/mix/tasks/deps_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ defmodule Mix.Tasks.DepsTest do
assert_received { :mix_shell, :info, ["Generated git_repo.app"] }

Mix.Tasks.Deps.Update.run []
assert_received { :mix_shell, :info, ["* Updating deps_repo [path: \"custom/deps_repo\"]"] }
assert_received { :mix_shell, :info, ["* Updating deps_repo (0.1.0) [path: \"custom/deps_repo\"]"] }
end
after
purge [GitRepo, GitRepo.Mix, DepsRepo]
Expand Down Expand Up @@ -290,8 +290,9 @@ defmodule Mix.Tasks.DepsTest do

Mix.Task.clear
Mix.Tasks.Deps.Update.run []
assert_received { :mix_shell, :info, ["* Updating deps_repo [path: \"custom/deps_repo\"]"] }
assert_received { :mix_shell, :info, ["* Updating bad_deps_repo [path: \"custom/bad_deps_repo\"]"] }

assert_received { :mix_shell, :info, ["* Updating deps_repo (0.1.0) [path: \"custom/deps_repo\"]"] }
assert_received { :mix_shell, :info, ["* Updating bad_deps_repo (0.1.0) [path: \"custom/bad_deps_repo\"]"] }

Mix.Tasks.Deps.Check.run []
end
Expand All @@ -312,4 +313,21 @@ defmodule Mix.Tasks.DepsTest do
purge [GitRepo, GitRepo.Mix, DepsRepo, BadDepsRepo]
Mix.Project.pop
end
end

test "update parent dependencies" do
Mix.Project.push NestedDepsApp

in_fixture "deps_status", fn ->
Mix.Tasks.Deps.Get.run []
Mix.Task.clear
Mix.Tasks.Deps.Update.run ["git_repo"]

message = "* Updating git_repo (0.1.0) [git: \"#{fixture_path("git_repo")}\"]"
assert_received { :mix_shell, :info, [^message] }
assert_received { :mix_shell, :info, ["* Updating deps_repo (0.1.0) [path: \"custom/deps_repo\"]"] }
end
after
purge [GitRepo, GitRepo.Mix, DepsRepo]
Mix.Project.pop
end
end