Skip to content

Commit cb5e812

Browse files
josevalimgdyr
andauthored
Use System.cmd rather than System.shell for Rebar3 to avoid escaping (#13751)
Closes #13750. Co-authored-by: Michael Goodyear <[email protected]>
1 parent 99be673 commit cb5e812

File tree

5 files changed

+72
-71
lines changed

5 files changed

+72
-71
lines changed

lib/mix/lib/mix/dep/loader.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ defmodule Mix.Dep.Loader do
332332
defp rebar_dep(dep, children, manager, locked?) do
333333
%Mix.Dep{app: app, opts: opts, extra: overrides} = dep
334334

335-
if locked? and is_nil(Mix.Rebar.rebar_cmd(manager)) do
335+
if locked? and is_nil(Mix.Rebar.rebar_path(manager)) do
336336
Mix.Tasks.Local.Rebar.run(["--force"])
337337
end
338338

lib/mix/lib/mix/rebar.ex

+34-40
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,56 @@
11
defmodule Mix.Rebar do
22
@moduledoc false
33

4-
@doc """
5-
Returns the path supposed to host the local copy of `rebar`.
4+
# TODO: Remove on Elixir v1.20 because phx_new and other installers rely on it.
5+
@deprecated "Use global_rebar_path/1 instead"
6+
def global_rebar_cmd(manager) do
7+
global_rebar_path(manager)
8+
end
69

7-
The rebar3 installation is specific to the Elixir version,
8-
in order to force updates when new Elixir versions come out.
9-
"""
10-
def local_rebar_path(:rebar3) do
11-
[major, minor | _] = String.split(System.version(), ".")
12-
Path.join([Mix.Utils.mix_home(), "elixir", "#{major}-#{minor}", "rebar3"])
10+
@deprecated "Use local_rebar_path/1 instead"
11+
def local_rebar_cmd(manager) do
12+
local_rebar_path(manager)
13+
end
14+
15+
@deprecated "Use rebar_path/1 instead"
16+
def rebar_cmd(manager) do
17+
global_rebar_cmd(manager) || local_rebar_cmd(manager)
1318
end
1419

1520
@doc """
16-
Returns the path to the global copy of `rebar` defined by the
17-
environment variable `MIX_REBAR3`.
21+
Receives a Rebar executable and returns how it must be invoked.
1822
"""
19-
def global_rebar_cmd(:rebar3) do
20-
if cmd = System.get_env("MIX_REBAR3") do
21-
wrap_cmd(cmd)
23+
def rebar_args(rebar, args) do
24+
if match?({:win32, _}, :os.type()) and not String.ends_with?(rebar, ".cmd") do
25+
{"escript.exe", [rebar | args]}
26+
else
27+
{rebar, args}
2228
end
2329
end
2430

2531
@doc """
26-
Returns the path to the local copy of `rebar`, if one exists.
32+
Returns either the global or local rebar path.
2733
"""
28-
def local_rebar_cmd(manager) do
29-
cmd = local_rebar_path(manager)
30-
31-
if File.regular?(cmd) do
32-
wrap_cmd(cmd)
33-
end
34+
def rebar_path(:rebar3) do
35+
global_rebar_path(:rebar3) || local_rebar_path(:rebar3)
3436
end
3537

3638
@doc """
37-
Returns the path to the available `rebar` command.
39+
Finds the global rebar path.
3840
"""
39-
# TODO: Remove on Elixir v1.20 because phx_new and other installers rely on it.
40-
def rebar_cmd(:rebar) do
41-
Mix.shell().error("[warning] :rebar is no longer supported in Mix, falling back to :rebar3")
42-
rebar_cmd(:rebar3)
41+
def global_rebar_path(:rebar3) do
42+
System.get_env("MIX_REBAR3")
4343
end
4444

45-
def rebar_cmd(manager) do
46-
global_rebar_cmd(manager) || local_rebar_cmd(manager)
45+
@doc """
46+
Returns the path supposed to host the local copy of `rebar`.
47+
48+
The rebar3 installation is specific to the Elixir version,
49+
in order to force updates when new Elixir versions come out.
50+
"""
51+
def local_rebar_path(:rebar3) do
52+
[major, minor | _] = String.split(System.version(), ".")
53+
Path.join([Mix.Utils.mix_home(), "elixir", "#{major}-#{minor}", "rebar3"])
4754
end
4855

4956
@doc """
@@ -215,19 +222,6 @@ defmodule Mix.Rebar do
215222
end)
216223
end
217224

218-
defp wrap_cmd(rebar) do
219-
cond do
220-
not match?({:win32, _}, :os.type()) ->
221-
String.replace(rebar, " ", "\\ ")
222-
223-
String.ends_with?(rebar, ".cmd") ->
224-
"\"#{String.replace(rebar, "/", "\\")}\""
225-
226-
true ->
227-
"escript.exe \"#{rebar}\""
228-
end
229-
end
230-
231225
@doc """
232226
Applies the given overrides for app config.
233227
"""

lib/mix/lib/mix/shell.ex

+15-8
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,15 @@ defmodule Mix.Shell do
9090
end
9191

9292
@doc """
93-
Executes the given `command` as a shell command and
94-
invokes the `callback` for the streamed response.
93+
Executes the given `command` and invokes the `callback` for the streamed response.
9594
96-
This is most commonly used by shell implementations
97-
but can also be invoked directly.
95+
`command` is either a string, to be executed as a `System.shell/2` command,
96+
or a `{executable, args}` to be executed via `System.cmd/3`.
9897
99-
`callback` takes the output data of the command. Its
100-
return value is ignored.
98+
`callback` takes the output data of the command. Its return value is ignored.
99+
100+
This function is most commonly used by `Mix.Shell` implementations but can
101+
also be invoked directly.
101102
102103
## Options
103104
@@ -112,7 +113,8 @@ defmodule Mix.Shell do
112113
* `:quiet` - overrides the callback to no-op
113114
114115
"""
115-
@spec cmd(String.t(), keyword, (binary -> term)) :: exit_status :: non_neg_integer
116+
@spec cmd(String.t() | {String.t(), [String.t()]}, keyword, (binary -> term)) ::
117+
exit_status :: non_neg_integer
116118
def cmd(command, options \\ [], callback) when is_function(callback, 1) do
117119
callback =
118120
if options[:quiet] do
@@ -129,7 +131,12 @@ defmodule Mix.Shell do
129131
|> Keyword.put(:into, %Mix.Shell{callback: callback})
130132
|> Keyword.put_new(:stderr_to_stdout, use_stdio)
131133

132-
{_, status} = System.shell(command, options)
134+
{_, status} =
135+
case command do
136+
{command, args} -> System.cmd(command, args, options)
137+
command when is_binary(command) -> System.shell(command, options)
138+
end
139+
133140
status
134141
end
135142

lib/mix/lib/mix/tasks/deps.compile.ex

+21-21
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,15 @@ defmodule Mix.Tasks.Deps.Compile do
207207
{"TERM", "dumb"}
208208
]
209209

210-
cmd = "#{rebar_cmd(dep)} bare compile --paths #{escape_path(lib_path)}"
211-
do_command(dep, config, cmd, false, env)
210+
rebar = Mix.Rebar.local_rebar_path(:rebar3) || handle_rebar_not_found(dep)
211+
{exec, args} = Mix.Rebar.rebar_args(rebar, ["bare", "compile", "--paths", lib_path])
212+
213+
if Mix.shell().cmd({exec, args}, opts_for_cmd(dep, config, env)) != 0 do
214+
Mix.raise(
215+
"Could not compile dependency #{inspect(dep.app)}, \"#{Enum.join([exec | args], " ")}\" command failed. " <>
216+
deps_compile_feedback(dep.app)
217+
)
218+
end
212219

213220
# Check if we have any new symlinks after compilation
214221
for dir <- ~w(include priv src),
@@ -220,21 +227,12 @@ defmodule Mix.Tasks.Deps.Compile do
220227
true
221228
end
222229

223-
defp escape_path(path) do
224-
escape = if match?({:win32, _}, :os.type()), do: "^ ", else: "\\ "
225-
String.replace(path, " ", escape)
226-
end
227-
228230
defp rebar_config(dep) do
229231
dep.extra
230232
|> Mix.Rebar.dependency_config()
231233
|> Mix.Rebar.serialize_config()
232234
end
233235

234-
defp rebar_cmd(%Mix.Dep{manager: manager} = dep) do
235-
Mix.Rebar.rebar_cmd(manager) || handle_rebar_not_found(dep)
236-
end
237-
238236
defp handle_rebar_not_found(%Mix.Dep{app: app, manager: manager}) do
239237
shell = Mix.shell()
240238

@@ -255,12 +253,12 @@ defmodule Mix.Tasks.Deps.Compile do
255253
end
256254

257255
Mix.Tasks.Local.Rebar.run(["--force"])
258-
Mix.Rebar.local_rebar_cmd(manager) || Mix.raise("\"#{manager}\" installation failed")
256+
Mix.Rebar.local_rebar_path(manager) || Mix.raise("\"#{manager}\" installation failed")
259257
end
260258

261259
defp do_make(dep, config) do
262260
command = make_command(dep)
263-
do_command(dep, config, command, true, [{"IS_DEP", "1"}])
261+
shell_cmd!(dep, config, command, [{"IS_DEP", "1"}])
264262
build_structure(dep, config)
265263
true
266264
end
@@ -289,27 +287,29 @@ defmodule Mix.Tasks.Deps.Compile do
289287

290288
defp do_compile(%Mix.Dep{opts: opts} = dep, config) do
291289
if command = opts[:compile] do
292-
do_command(dep, config, command, true)
290+
shell_cmd!(dep, config, command)
293291
build_structure(dep, config)
294292
true
295293
else
296294
false
297295
end
298296
end
299297

300-
defp do_command(dep, config, command, print_app?, env \\ []) do
301-
%Mix.Dep{app: app, system_env: system_env, opts: opts} = dep
302-
303-
env = [{"ERL_LIBS", Path.join(config[:deps_build_path], "lib")} | system_env] ++ env
304-
305-
if Mix.shell().cmd(command, env: env, print_app: print_app?, cd: opts[:dest]) != 0 do
298+
defp shell_cmd!(%Mix.Dep{app: app} = dep, config, command, env \\ []) do
299+
if Mix.shell().cmd(command, [print_app: true] ++ opts_for_cmd(dep, config, env)) != 0 do
306300
Mix.raise(
307301
"Could not compile dependency #{inspect(app)}, \"#{command}\" command failed. " <>
308302
deps_compile_feedback(app)
309303
)
310304
end
311305

312-
true
306+
:ok
307+
end
308+
309+
defp opts_for_cmd(dep, config, env) do
310+
%Mix.Dep{system_env: system_env, opts: opts} = dep
311+
env = [{"ERL_LIBS", Path.join(config[:deps_build_path], "lib")} | system_env] ++ env
312+
[env: env, cd: opts[:dest]]
313313
end
314314

315315
defp build_structure(%Mix.Dep{opts: opts}, config) do

lib/mix/test/mix/rebar_test.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ defmodule Mix.RebarTest do
240240
in_tmp("rebar3 env with spaces", fn ->
241241
File.cp!(Mix.Rebar.local_rebar_path(:rebar3), "rebar3")
242242
System.put_env("MIX_REBAR3", Path.absname("rebar3"))
243-
assert Mix.Rebar.rebar_cmd(:rebar3) =~ " "
243+
assert Mix.Rebar.rebar_path(:rebar3) =~ " "
244244

245245
Mix.Project.push(RebarAsDep)
246246
Mix.Tasks.Deps.Get.run([])

0 commit comments

Comments
 (0)