Skip to content

Commit a5e19ad

Browse files
committed
Support --output in mix deps.tree and mix app.tree, closes #15288
1 parent 983e033 commit a5e19ad

File tree

4 files changed

+154
-22
lines changed

4 files changed

+154
-22
lines changed

lib/mix/lib/mix/tasks/app.tree.ex

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,20 @@ defmodule Mix.Tasks.App.Tree do
3030
This is the default on Windows.
3131
3232
* `dot` - produces a DOT graph description of the application tree
33-
in `app_tree.dot` in the current directory.
34-
Warning: this will overwrite any previously generated file.
33+
in `app_tree.dot` in the current directory. See the documentation
34+
for the `--output` option to learn how to control where the file
35+
is written and other related details.
36+
37+
* `--output` *(since v1.20.0)* - can be used to override the location of
38+
the file created by the `dot` format. It can be set to
39+
40+
* `-` - prints the output to standard output;
41+
42+
* a path - writes the output graph to the given path
43+
44+
If the output file already exists then it will be renamed in place
45+
to have a `.bak` suffix, possibly overwriting any existing `.bak` file.
46+
If this rename fails a fatal exception will be thrown.
3547
3648
"""
3749

@@ -42,7 +54,9 @@ defmodule Mix.Tasks.App.Tree do
4254
Mix.Task.run("compile", args)
4355

4456
{app, opts} =
45-
case OptionParser.parse!(args, strict: [exclude: :keep, format: :string]) do
57+
case OptionParser.parse!(args,
58+
strict: [exclude: :keep, format: :string, output: :string]
59+
) do
4660
{opts, []} ->
4761
app =
4862
Mix.Project.config()[:app] ||
@@ -67,17 +81,23 @@ defmodule Mix.Tasks.App.Tree do
6781

6882
if opts[:format] == "dot" do
6983
root = [{app, :normal}]
70-
Mix.Utils.write_dot_graph!("app_tree.dot", "application tree", root, callback, opts)
7184

72-
"""
73-
Generated "app_tree.dot" in the current directory. To generate a PNG:
85+
file_spec =
86+
Mix.Utils.write_dot_graph!("app_tree.dot", "application tree", root, callback, opts)
7487

75-
dot -Tpng app_tree.dot -o app_tree.png
88+
if file_spec != "-" do
89+
png_file_spec = (file_spec |> Path.rootname() |> Path.basename()) <> ".png"
7690

77-
For more options see https://www.graphviz.org/.
78-
"""
79-
|> String.trim_trailing()
80-
|> Mix.shell().info()
91+
"""
92+
Generated "#{Path.relative_to_cwd(file_spec)}". To generate a PNG:
93+
94+
dot -Tpng #{inspect(file_spec)} -o #{inspect(png_file_spec)}
95+
96+
For more options see https://www.graphviz.org/.
97+
"""
98+
|> String.trim_trailing()
99+
|> Mix.shell().info()
100+
end
81101
else
82102
Mix.Utils.print_tree([{app, :normal}], callback, opts)
83103
end

lib/mix/lib/mix/tasks/deps.tree.ex

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,20 @@ defmodule Mix.Tasks.Deps.Tree do
3737
This is the default on Windows.
3838
3939
* `dot` - produces a DOT graph description of the dependency tree
40-
in `deps_tree.dot` in the current directory.
41-
Warning: this will override any previously generated file.
40+
in `deps_tree.dot` in the current directory. See the documentation
41+
for the `--output` option to learn how to control where the file
42+
is written and other related details.
43+
44+
* `--output` *(since v1.20.0)* - can be used to override the location of
45+
the file created by the `dot` format. It can be set to
46+
47+
* `-` - prints the output to standard output;
48+
49+
* a path - writes the output graph to the given path
50+
51+
If the output file already exists then it will be renamed in place
52+
to have a `.bak` suffix, possibly overwriting any existing `.bak` file.
53+
If this rename fails a fatal exception will be thrown.
4254
4355
## Examples
4456
@@ -61,7 +73,8 @@ defmodule Mix.Tasks.Deps.Tree do
6173
target: :string,
6274
exclude: :keep,
6375
umbrella_only: :boolean,
64-
format: :string
76+
format: :string,
77+
output: :string
6578
]
6679

6780
@impl true
@@ -89,17 +102,23 @@ defmodule Mix.Tasks.Deps.Tree do
89102

90103
if opts[:format] == "dot" do
91104
callback = callback(&format_dot/1, deps, opts)
92-
Mix.Utils.write_dot_graph!("deps_tree.dot", "dependency tree", [root], callback, opts)
93105

94-
"""
95-
Generated "deps_tree.dot" in the current directory. To generate a PNG:
106+
file_spec =
107+
Mix.Utils.write_dot_graph!("deps_tree.dot", "dependency tree", [root], callback, opts)
96108

97-
dot -Tpng deps_tree.dot -o deps_tree.png
109+
if file_spec != "-" do
110+
png_file_spec = (file_spec |> Path.rootname() |> Path.basename()) <> ".png"
98111

99-
For more options see https://www.graphviz.org/.
100-
"""
101-
|> String.trim_trailing()
102-
|> Mix.shell().info()
112+
"""
113+
Generated "#{Path.relative_to_cwd(file_spec)}". To generate a PNG:
114+
115+
dot -Tpng #{inspect(file_spec)} -o #{inspect(png_file_spec)}
116+
117+
For more options see https://www.graphviz.org/.
118+
"""
119+
|> String.trim_trailing()
120+
|> Mix.shell().info()
121+
end
103122
else
104123
callback = callback(&format_tree/1, deps, opts)
105124
Mix.Utils.print_tree([root], callback, opts)

lib/mix/test/mix/tasks/app.tree_test.exs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Code.require_file("../../test_helper.exs", __DIR__)
77
defmodule Mix.Tasks.App.TreeTest do
88
use MixTest.Case
99

10+
import ExUnit.CaptureIO
11+
1012
defmodule AppDepsSample do
1113
def project do
1214
[app: :test, version: "0.1.0"]
@@ -137,6 +139,54 @@ defmodule Mix.Tasks.App.TreeTest do
137139
end)
138140
end
139141

142+
@expected_dot """
143+
digraph "application tree" {
144+
"test"
145+
"test" -> "app_deps_sample"
146+
"app_deps_sample" -> "app_deps2_sample"
147+
"app_deps2_sample" -> "app_deps4_sample" [label="(included)"]
148+
"app_deps_sample" -> "app_deps3_sample"
149+
"test" -> "elixir"
150+
"test" -> "logger"
151+
"logger" -> "elixir"
152+
}
153+
"""
154+
155+
@tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample]
156+
test "writes the dot graph to a custom file via --output", context do
157+
in_tmp(context.test, fn ->
158+
Mix.Project.push(AppDepsSample)
159+
160+
load_apps()
161+
Mix.Tasks.App.Tree.run(["--format", "dot", "--output", "custom.dot"])
162+
163+
assert File.read!("custom.dot") == @expected_dot
164+
refute File.exists?("app_tree.dot")
165+
166+
File.write!("custom.dot", "previous")
167+
Mix.Tasks.App.Tree.run(["--format", "dot", "--output", "custom.dot"])
168+
assert File.read!("custom.dot") == @expected_dot
169+
assert File.read!("custom.dot.bak") == "previous"
170+
end)
171+
end
172+
173+
@tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample]
174+
test "writes the dot graph to stdout via --output -", context do
175+
in_tmp(context.test, fn ->
176+
Mix.Project.push(AppDepsSample)
177+
178+
load_apps()
179+
180+
output =
181+
capture_io(fn ->
182+
Mix.Tasks.App.Tree.run(["--format", "dot", "--output", "-"])
183+
end)
184+
185+
assert output == @expected_dot
186+
refute File.exists?("app_tree.dot")
187+
end)
188+
end
189+
140190
defp load_apps(optional_apps \\ []) do
141191
:ok = :application.load({:application, :app_deps4_sample, [vsn: ~c"1.0.0", env: []]})
142192
:ok = :application.load({:application, :app_deps3_sample, [vsn: ~c"1.0.0", env: []]})

lib/mix/test/mix/tasks/deps.tree_test.exs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Code.require_file("../../test_helper.exs", __DIR__)
77
defmodule Mix.Tasks.Deps.TreeTest do
88
use MixTest.Case
99

10+
import ExUnit.CaptureIO
11+
1012
defmodule ConvergedDepsApp do
1113
def project do
1214
[
@@ -164,4 +166,45 @@ defmodule Mix.Tasks.Deps.TreeTest do
164166
after
165167
purge([DepsOnGitRepo.MixProject, GitRepo.MixProject])
166168
end
169+
170+
@expected_dot """
171+
digraph "dependency tree" {
172+
"sample"
173+
"sample" -> "deps_on_git_repo" [label="0.2.0"]
174+
"sample" -> "git_repo" [label=">= 0.1.0"]
175+
}
176+
"""
177+
178+
test "writes the dot graph to a custom file via --output", context do
179+
in_tmp(context.test, fn ->
180+
Mix.Project.push(ConvergedDepsApp)
181+
182+
Mix.Tasks.Deps.Tree.run(["--format", "dot", "--output", "custom.dot"])
183+
assert File.read!("custom.dot") == @expected_dot
184+
refute File.exists?("deps_tree.dot")
185+
186+
File.write!("custom.dot", "previous")
187+
Mix.Tasks.Deps.Tree.run(["--format", "dot", "--output", "custom.dot"])
188+
assert File.read!("custom.dot") == @expected_dot
189+
assert File.read!("custom.dot.bak") == "previous"
190+
end)
191+
after
192+
purge([DepsOnGitRepo.MixProject, GitRepo.MixProject])
193+
end
194+
195+
test "writes the dot graph to stdout via --output -", context do
196+
in_tmp(context.test, fn ->
197+
Mix.Project.push(ConvergedDepsApp)
198+
199+
output =
200+
capture_io(fn ->
201+
Mix.Tasks.Deps.Tree.run(["--format", "dot", "--output", "-"])
202+
end)
203+
204+
assert output == @expected_dot
205+
refute File.exists?("deps_tree.dot")
206+
end)
207+
after
208+
purge([DepsOnGitRepo.MixProject, GitRepo.MixProject])
209+
end
167210
end

0 commit comments

Comments
 (0)