Skip to content

Commit 3dc3f4e

Browse files
authored
Add MIX_OS_DEPS_COMPILE_PARTITION_COUNT for concurrent deps compilation (#14340)
1 parent e0c016c commit 3dc3f4e

File tree

10 files changed

+443
-103
lines changed

10 files changed

+443
-103
lines changed

lib/mix/lib/mix.ex

+16-10
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,11 @@ defmodule Mix do
359359
* `MIX_OS_CONCURRENCY_LOCK` - when set to `0` or `false`, disables mix compilation locking.
360360
While not recommended, this may be necessary in cases where hard links or TCP sockets are
361361
not available. When opting for this behaviour, make sure to not start concurrent compilations
362-
of the same project.
362+
of the same project
363+
364+
* `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` - when set to a number greater than 1, it enables
365+
compilation of dependencies over multiple operating system processes. See `mix help deps.compile`
366+
for more information
363367
364368
* `MIX_PATH` - appends extra code paths
365369
@@ -420,7 +424,7 @@ defmodule Mix do
420424
421425
"""
422426

423-
@mix_install_project __MODULE__.InstallProject
427+
@mix_install_project Mix.InstallProject
424428
@mix_install_app :mix_install
425429
@mix_install_app_string Atom.to_string(@mix_install_app)
426430

@@ -905,9 +909,7 @@ defmodule Mix do
905909

906910
case Mix.State.get(:installed) do
907911
nil ->
908-
Application.put_all_env(config, persistent: true)
909912
System.put_env(system_env)
910-
911913
install_project_dir = install_project_dir(id)
912914

913915
if Keyword.fetch!(opts, :verbose) do
@@ -924,10 +926,14 @@ defmodule Mix do
924926
config_path: config_path
925927
]
926928

927-
config = install_project_config(dynamic_config)
929+
:ok =
930+
Mix.ProjectStack.push(
931+
@mix_install_project,
932+
[compile_config: config] ++ install_project_config(dynamic_config),
933+
"nofile"
934+
)
928935

929936
started_apps = Application.started_applications()
930-
:ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile")
931937
build_dir = Path.join(install_project_dir, "_build")
932938
external_lockfile = expand_path(opts[:lockfile], deps, :lockfile, "mix.lock")
933939

@@ -944,9 +950,9 @@ defmodule Mix do
944950
File.mkdir_p!(install_project_dir)
945951

946952
File.cd!(install_project_dir, fn ->
947-
if config_path do
948-
Mix.Task.rerun("loadconfig")
949-
end
953+
# This steps need to be mirror in mix deps.partition
954+
Application.put_all_env(config, persistent: true)
955+
Mix.Task.rerun("loadconfig")
950956

951957
cond do
952958
external_lockfile ->
@@ -1079,7 +1085,7 @@ defmodule Mix do
10791085

10801086
defp install_project_config(dynamic_config) do
10811087
[
1082-
version: "0.1.0",
1088+
version: "1.0.0",
10831089
build_per_environment: true,
10841090
build_path: "_build",
10851091
lockfile: "mix.lock",

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

+85-61
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ defmodule Mix.Tasks.Deps.Compile do
3535
recompiled without propagating those changes upstream. To ensure
3636
`b` is included in the compilation step, pass `--include-children`.
3737
38+
## Compiling dependencies across multiple OS processes
39+
40+
If you set the environment variable `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`
41+
to a number greater than 1, Mix will start multiple operating system
42+
processes to compile your dependencies concurrently.
43+
44+
While Mix and Rebar compile all files within a given project in parallel,
45+
enabling this environment variable can still yield useful gains in several
46+
cases, such as when compiling dependencies with native code, dependencies
47+
that must download assets, or dependencies where the compilation time is not
48+
evenly distributed (for example, one file takes much longer to compile than
49+
all others).
50+
51+
While most configuration in Mix is done via command line flags, this particular
52+
environment variable exists because the best number will vary per machine
53+
(and often per project too). The environment variable also makes it more accessible
54+
to enable concurrent compilation in CI and also during `Mix.install/2` commands.
55+
3856
## Command line options
3957
4058
* `--force` - force compilation of deps
@@ -57,7 +75,6 @@ defmodule Mix.Tasks.Deps.Compile do
5775
end
5876

5977
Mix.Project.get!()
60-
6178
config = Mix.Project.config()
6279

6380
Mix.Project.with_build_lock(config, fn ->
@@ -75,86 +92,82 @@ defmodule Mix.Tasks.Deps.Compile do
7592

7693
@doc false
7794
def compile(deps, options \\ []) do
78-
shell = Mix.shell()
79-
config = Mix.Project.deps_config()
8095
Mix.Task.run("deps.precompile")
96+
force? = Keyword.get(options, :force, false)
8197

82-
compiled =
98+
deps =
8399
deps
84100
|> reject_umbrella_children(options)
85101
|> reject_local_deps(options)
86-
|> Enum.map(fn %Mix.Dep{app: app, status: status, opts: opts, scm: scm} = dep ->
87-
check_unavailable!(app, scm, status)
88-
maybe_clean(dep, options)
89102

90-
compiled? =
91-
cond do
92-
not is_nil(opts[:compile]) ->
93-
do_compile(dep, config)
103+
count = System.get_env("MIX_OS_DEPS_COMPILE_PARTITION_COUNT", "0") |> String.to_integer()
94104

95-
Mix.Dep.mix?(dep) ->
96-
do_mix(dep, config)
105+
compiled? =
106+
if count > 1 and length(deps) > 1 do
107+
Mix.shell().info("mix deps.compile running across #{count} OS processes")
108+
Mix.Tasks.Deps.Partition.server(deps, count, force?)
109+
else
110+
config = Mix.Project.deps_config()
111+
true in Enum.map(deps, &compile_single(&1, force?, config))
112+
end
97113

98-
Mix.Dep.make?(dep) ->
99-
do_make(dep, config)
114+
if compiled?, do: Mix.Task.run("will_recompile"), else: :ok
115+
end
100116

101-
dep.manager == :rebar3 ->
102-
do_rebar3(dep, config)
117+
@doc false
118+
def compile_single(%Mix.Dep{} = dep, force?, config) do
119+
%{app: app, status: status, opts: opts, scm: scm} = dep
120+
check_unavailable!(app, scm, status)
103121

104-
true ->
105-
shell.error(
106-
"Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\" or \"Makefile\" " <>
107-
"(pass :compile as an option to customize compilation, set it to \"false\" to do nothing)"
108-
)
122+
# If a dependency was marked as fetched or with an out of date lock
123+
# or missing the app file, we always compile it from scratch.
124+
if force? or Mix.Dep.compilable?(dep) do
125+
File.rm_rf!(Path.join([Mix.Project.build_path(), "lib", Atom.to_string(dep.app)]))
126+
end
109127

110-
false
111-
end
128+
compiled? =
129+
cond do
130+
not is_nil(opts[:compile]) ->
131+
do_compile(dep, config)
112132

113-
if compiled? do
114-
build_path = Mix.Project.build_path(config)
133+
Mix.Dep.mix?(dep) ->
134+
do_mix(dep, config)
115135

116-
lazy_message = fn ->
117-
info = %{
118-
app: dep.app,
119-
scm: dep.scm,
120-
manager: dep.manager,
121-
os_pid: System.pid()
122-
}
136+
Mix.Dep.make?(dep) ->
137+
do_make(dep, config)
123138

124-
{:dep_compiled, info}
125-
end
139+
dep.manager == :rebar3 ->
140+
do_rebar3(dep, config)
126141

127-
Mix.Sync.PubSub.broadcast(build_path, lazy_message)
128-
end
142+
true ->
143+
Mix.shell().error(
144+
"Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\" or \"Makefile\" " <>
145+
"(pass :compile as an option to customize compilation, set it to \"false\" to do nothing)"
146+
)
129147

130-
# We should touch fetchable dependencies even if they
131-
# did not compile otherwise they will always be marked
132-
# as stale, even when there is nothing to do.
133-
fetchable? = touch_fetchable(scm, opts[:build])
148+
false
149+
end
134150

135-
compiled? and fetchable?
151+
if compiled? do
152+
config
153+
|> Mix.Project.build_path()
154+
|> Mix.Sync.PubSub.broadcast(fn ->
155+
info = %{
156+
app: dep.app,
157+
scm: dep.scm,
158+
manager: dep.manager,
159+
os_pid: System.pid()
160+
}
161+
162+
{:dep_compiled, info}
136163
end)
137-
138-
if true in compiled, do: Mix.Task.run("will_recompile"), else: :ok
139-
end
140-
141-
defp maybe_clean(dep, opts) do
142-
# If a dependency was marked as fetched or with an out of date lock
143-
# or missing the app file, we always compile it from scratch.
144-
if Keyword.get(opts, :force, false) or Mix.Dep.compilable?(dep) do
145-
File.rm_rf!(Path.join([Mix.Project.build_path(), "lib", Atom.to_string(dep.app)]))
146164
end
147-
end
148165

149-
defp touch_fetchable(scm, path) do
150-
if scm.fetchable?() do
151-
path = Path.join(path, ".mix")
152-
File.mkdir_p!(path)
153-
File.touch!(Path.join(path, "compile.fetch"))
154-
true
155-
else
156-
false
157-
end
166+
# We should touch fetchable dependencies even if they
167+
# did not compile otherwise they will always be marked
168+
# as stale, even when there is nothing to do.
169+
fetchable? = touch_fetchable(scm, opts[:build])
170+
compiled? and fetchable?
158171
end
159172

160173
defp check_unavailable!(app, scm, {:unavailable, path}) do
@@ -176,6 +189,17 @@ defmodule Mix.Tasks.Deps.Compile do
176189
:ok
177190
end
178191

192+
defp touch_fetchable(scm, path) do
193+
if scm.fetchable?() do
194+
path = Path.join(path, ".mix")
195+
File.mkdir_p!(path)
196+
File.touch!(Path.join(path, "compile.fetch"))
197+
true
198+
else
199+
false
200+
end
201+
end
202+
179203
defp do_mix(dep, _config) do
180204
Mix.Dep.in_dependency(dep, fn _ ->
181205
config = Mix.Project.config()

lib/mix/lib/mix/tasks/deps.loadpaths.ex

+1-10
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,7 @@ defmodule Mix.Tasks.Deps.Loadpaths do
139139
defp partition([dep | deps], not_ok, compile) do
140140
cond do
141141
Mix.Dep.compilable?(dep) or (Mix.Dep.ok?(dep) and local?(dep)) ->
142-
if from_umbrella?(dep) do
143-
partition(deps, not_ok, compile)
144-
else
145-
partition(deps, not_ok, [dep | compile])
146-
end
142+
partition(deps, not_ok, [dep | compile])
147143

148144
Mix.Dep.ok?(dep) ->
149145
partition(deps, not_ok, compile)
@@ -163,11 +159,6 @@ defmodule Mix.Tasks.Deps.Loadpaths do
163159
|> Mix.Dep.filter_by_name(Mix.Dep.load_and_cache())
164160
end
165161

166-
# Those are compiled by umbrella.
167-
defp from_umbrella?(dep) do
168-
dep.opts[:from_umbrella]
169-
end
170-
171162
# Every local dependency (i.e. that are not fetchable)
172163
# are automatically recompiled if they are ok.
173164
defp local?(dep) do

0 commit comments

Comments
 (0)