Skip to content

Commit 05bf8e8

Browse files
committed
Give high preference to task over children aliases
1 parent 6415955 commit 05bf8e8

File tree

5 files changed

+95
-64
lines changed

5 files changed

+95
-64
lines changed

lib/mix/lib/mix.ex

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,10 @@ defmodule Mix do
238238
239239
In the example above, we have defined three aliases. One is `mix c`
240240
which is a shortcut for `mix compile`. Another is named
241-
`mix hello`, which is the equivalent to the `Mix.Tasks.Hello`
242-
we have defined in the [Mix.Task section](#module-mix-task).
243-
244-
The third is named `mix paid_task`, which runs the task `paid.task` with
245-
several arguments, including one pulled from an environment variable.
246-
Defining this as a function means that the environment variable is only
247-
evaluated when this specific task is run, not when `mix.exs` is loaded
248-
before each `mix` command.
241+
`mix hello` and the third is named `mix paid_task`, which executes
242+
the code inside a custom function to invoke the `paid.task` task
243+
with several arguments, including one pulled from an environment
244+
variable.
249245
250246
Aliases may also be lists, specifying multiple tasks to be run
251247
consecutively:
@@ -265,19 +261,13 @@ defmodule Mix do
265261
Where `&clean_extra/1` would be a function in your `mix.exs`
266262
with extra cleanup logic.
267263
268-
Arguments given to the alias will be appended to the arguments of
269-
the last task in the list. Except when overriding an existing task.
270-
In this case, the arguments will be given to the original task,
271-
in order to preserve semantics. For example, in the `:clean` alias
272-
above, the arguments given to the alias will be passed to "clean"
273-
and not to `clean_extra/1`.
274-
275-
Aliases defined in the current project do not affect its dependencies
276-
and aliases defined in dependencies are not accessible from the
277-
current project.
264+
If the alias is overriding an existing task, the arguments given
265+
to the alias will be forwarded to the original task in order to
266+
preserve semantics. Otherwise arguments given to the alias are
267+
appended to the arguments of the last task in the list.
278268
279-
Aliases can be used very powerfully to also run Elixir scripts and
280-
shell commands, for example:
269+
Another use case of aliases is to run Elixir scripts and shell
270+
commands, for example:
281271
282272
# priv/hello1.exs
283273
IO.puts("Hello One")
@@ -301,10 +291,15 @@ defmodule Mix do
301291
then `mix cmd` to execute a command line shell script. This shows how
302292
powerful aliases mixed with Mix tasks can be.
303293
304-
Mix tasks are designed to run only once. This prevents the same task
305-
from being executed multiple times. For example, if there are several tasks
306-
depending on `mix compile`, the code will be compiled once. Tasks can
307-
be executed again if they are explicitly reenabled using `Mix.Task.reenable/1`:
294+
One commit pitfall of aliases comes when trying to invoke the same task
295+
multiple times. Mix tasks are designed to run only once. This prevents
296+
the same task from being executed multiple times. For example, if there
297+
are several tasks depending on `mix compile`, the code will be compiled
298+
only once.
299+
300+
Similary, `mix format` can only be invoked once. So if you have an alias
301+
that attempts to invoke `mix format` multiple times, it won't work unless
302+
it is explicitly reenabled using `Mix.Task.reenable/1`:
308303
309304
another_alias: [
310305
"format --check-formatted priv/hello1.exs",
@@ -314,15 +309,14 @@ defmodule Mix do
314309
]
315310
316311
Some tasks are automatically reenabled though, as they are expected to
317-
be invoked multiple times. They are: `mix cmd`, `mix do`, `mix loadconfig`,
318-
`mix profile.cprof`, `mix profile.eprof`, `mix profile.fprof`, `mix run`,
319-
and `mix xref`.
320-
321-
It is worth mentioning that some tasks, such as in the case of the
322-
`mix format` command in the example above, can accept multiple files so it
323-
could be rewritten as:
324-
325-
another_alias: ["format --check-formatted priv/hello1.exs priv/hello2.exs"]
312+
be invoked multiple times, such as: `mix cmd`, `mix do`, `mix xref`, etc.
313+
314+
Finally, aliases defined in the current project do not affect its
315+
dependencies and aliases defined in dependencies are not accessible
316+
from the current project, with the exception of umbrella projects.
317+
Umbrella projects will run the aliases of its children when the
318+
umbrella project itself does not define said alias and there is no
319+
task with said name.
326320
327321
## Environment variables
328322

lib/mix/lib/mix/task.ex

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -367,43 +367,61 @@ defmodule Mix.Task do
367367
alias && Mix.TasksServer.run({:alias, task, proj}) ->
368368
run_alias(List.wrap(alias), args, proj, task, apps, :ok)
369369

370-
Mix.Project.umbrella?() && umbrella_child_alias?(task) ->
371-
Mix.ProjectStack.recur(fn ->
372-
recur(
373-
fn _ -> Mix.Project.config()[:aliases][String.to_atom(task)] && run(task, args) end,
374-
apps
375-
)
376-
end)
370+
Mix.TasksServer.get({:task, task, proj}) ->
371+
:noop
377372

378-
!Mix.TasksServer.get({:task, task, proj}) ->
379-
run_task(proj, task, args, apps)
373+
module = maybe_load_or_compile_task(proj, task) ->
374+
run_task(proj, module, task, args, apps)
375+
376+
results = Mix.Project.umbrella?() && recur_umbrella_children_alias(task, args, apps) ->
377+
results
380378

381379
true ->
382-
:noop
380+
# We could not find the task, let's raise
381+
get!(task)
383382
end
384383
end
385384

386-
defp umbrella_child_alias?(task) do
387-
umbrella_deps =
388-
Mix.Dep.Umbrella.cached()
389-
|> Enum.filter(& &1.opts[:from_umbrella])
385+
defp recur_umbrella_children_alias(task, args, apps) do
386+
alias_key = String.to_atom(task)
387+
388+
entries =
389+
Mix.ProjectStack.recur(fn ->
390+
recur(
391+
fn proj ->
392+
alias = Mix.Project.config()[:aliases][alias_key]
390393

391-
Enum.find(umbrella_deps, fn %Mix.Dep{app: app, opts: opts} ->
392-
Mix.Project.in_project(app, opts[:path], [inherit_parent_config_files: true], fn _ ->
393-
!!Mix.Project.config()[:aliases][String.to_atom(task)]
394+
cond do
395+
is_nil(alias) ->
396+
:error
397+
398+
Mix.TasksServer.run({:alias, task, proj}) ->
399+
{:ok, run_alias(List.wrap(alias), args, proj, task, nil, :ok)}
400+
401+
true ->
402+
{:ok, :noop}
403+
end
404+
end,
405+
apps
406+
)
394407
end)
395-
end)
408+
409+
case for({:ok, res} <- entries, do: res) do
410+
[] -> nil
411+
result -> result
412+
end
396413
end
397414

398-
defp run_task(proj, task, args, apps) do
415+
defp maybe_load_or_compile_task(proj, task) do
399416
# 1. If the task is available, we run it.
400417
# 2. Otherwise we compile and load dependencies
401418
# 3. Finally, we compile the current project in hope it is available.
402-
module =
403-
get_task_or_run(proj, task, fn -> run("deps.loadpaths") end) ||
404-
get_task_or_run(proj, task, fn -> run("compile", []) end) ||
405-
get!(task)
419+
get_task_or_run(proj, task, fn -> run("deps.loadpaths") end) ||
420+
get_task_or_run(proj, task, fn -> run("compile", []) end) ||
421+
get(task)
422+
end
406423

424+
defp run_task(proj, module, task, args, apps) do
407425
recursive = recursive(module)
408426

409427
cond do
@@ -477,7 +495,8 @@ defmodule Mix.Task do
477495
defp run_alias([h | t], alias_args, proj, original_task, apps, _res) when is_binary(h) do
478496
case OptionParser.split(h) do
479497
[^original_task | args] ->
480-
res = run_task(proj, original_task, args ++ alias_args, apps)
498+
module = maybe_load_or_compile_task(proj, original_task) || get!(original_task)
499+
res = run_task(proj, module, original_task, args ++ alias_args, apps)
481500
run_alias(t, [], proj, original_task, apps, res)
482501

483502
[task | args] ->

lib/mix/test/fixtures/umbrella_test/apps/bar/mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule Bar.MixProject do
99
# get accidentally swept up into the actual Mix test suite.
1010
test_pattern: "*_tests.exs",
1111
test_coverage: [ignore_modules: [Bar, ~r/Ignore/]],
12-
aliases: [mytask: ["cmd echo bar_running"]]
12+
aliases: [mytask: fn _ -> Mix.shell().info("bar_running") end]
1313
]
1414
end
1515
end

lib/mix/test/fixtures/umbrella_test/apps/foo/mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ defmodule Foo.MixProject do
88
# Choose something besides *_test.exs so that these test files don't
99
# get accidentally swept up into the actual Mix test suite.
1010
test_pattern: "*_tests.exs",
11-
aliases: [mytask: ["cmd echo foo_running"]]
11+
aliases: [mytask: fn _ -> Mix.shell().info("foo_running") end]
1212
]
1313
end
1414
end

lib/mix/test/mix/task_test.exs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ defmodule Mix.TaskTest do
172172
def project do
173173
[
174174
app: :baz,
175-
version: "0.1.0"
175+
version: "0.1.0",
176+
aliases: [help: fn _ -> raise "oops" end]
176177
]
177178
end
178179
end
@@ -186,10 +187,27 @@ defmodule Mix.TaskTest do
186187
end
187188
""")
188189

189-
assert [:ok, nil, :ok] = Mix.Task.run("mytask")
190-
191-
assert_received {:mix_shell, :run, ["foo_running" <> _]}
192-
assert_received {:mix_shell, :run, ["bar_running" <> _]}
190+
Mix.Task.run("compile")
191+
assert_received {:mix_shell, :info, ["==> foo"]}
192+
assert_received {:mix_shell, :info, ["==> bar"]}
193+
assert_received {:mix_shell, :info, ["==> baz"]}
194+
Mix.shell().flush()
195+
196+
# A child alias does not overlap an umbrella task
197+
assert Mix.Task.run("help") == :ok
198+
199+
# A missing umbrella alias can be found in children
200+
assert [:ok, :ok] = Mix.Task.run("mytask")
201+
assert_received {:mix_shell, :info, ["==> foo"]}
202+
assert_received {:mix_shell, :info, ["foo_running"]}
203+
assert_received {:mix_shell, :info, ["==> bar"]}
204+
assert_received {:mix_shell, :info, ["bar_running"]}
205+
refute_received {:mix_shell, :info, ["==> baz"]}
206+
207+
# An unknown task or alias anywhere fails
208+
assert_raise Mix.NoTaskError, fn ->
209+
Mix.Task.run("completely_unknown")
210+
end
193211
end)
194212
end)
195213
end

0 commit comments

Comments
 (0)