Skip to content

Commit f32ede4

Browse files
author
José Valim
committed
Ensure umbrella is recompiled when a child dependency changes
1 parent 1dc9193 commit f32ede4

File tree

13 files changed

+110
-156
lines changed

13 files changed

+110
-156
lines changed

lib/mix/lib/mix/dep.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ defmodule Mix.Dep do
3131
* `:app` - The application name
3232
* `:dest` - The destination path for the dependency
3333
* `:lock` - The lock information retrieved from mix.lock
34+
* `:build` - The build path for the dependency
3435
3536
"""
36-
defstruct scm: nil, app: nil, requirement: nil, status: nil, opts: nil,
37-
deps: [], top_level: false, extra: nil, manager: nil, from: nil
37+
defstruct scm: nil, app: nil, requirement: nil, status: nil, opts: [],
38+
deps: [], top_level: false, extra: [], manager: nil, from: nil
3839

3940
@doc """
4041
Returns all children dependencies for the current project,

lib/mix/lib/mix/dep/fetcher.ex

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ defmodule Mix.Dep.Fetcher do
9191

9292
# Note we only retrieve the parent dependencies of the updated
9393
# deps if all dependencies are available. This is because if a
94-
# dependency is missing, it could be a children of the parent
95-
# (aka a sibling) which would make parent compilation fail.
94+
# dependency is missing, it could directly affect one of the
95+
# dependencies we are trying to compile, causing the whole thing
96+
# to fail.
9697
#
9798
# If there is any other dependency that is not ok, we include
9899
# it for compilation too, this is our best to try to solve the
@@ -107,15 +108,17 @@ defmodule Mix.Dep.Fetcher do
107108
lock = Dict.merge(old_lock, new_lock)
108109
Mix.Dep.Lock.write(lock)
109110

110-
require_compilation(deps)
111+
mark_as_fetched(deps)
111112
{apps, all_deps}
112113
end
113114

114-
defp require_compilation(deps) do
115-
envs = Path.wildcard("_build/*/lib")
116-
117-
for %Mix.Dep{app: app} <- deps, env <- envs do
118-
File.touch Path.join [env, Atom.to_string(app), ".compile"]
115+
defp mark_as_fetched(deps) do
116+
# If the dependency is fetchable, we are going to write a .fetch
117+
# file to it. Each build, regardless of the environment and location,
118+
# will compared against this .fetch file to know if the depednency
119+
# needs recompiling.
120+
for %Mix.Dep{scm: scm, opts: opts} <- deps, scm.fetchable? do
121+
File.touch Path.join opts[:dest], ".fetch"
119122
end
120123
end
121124

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,22 +245,27 @@ defmodule Mix.Dep.Loader do
245245

246246
defp validate_app(%Mix.Dep{opts: opts, requirement: req, app: app, status: status} = dep) do
247247
opts_app = opts[:app]
248-
build = opts[:build]
249248

250249
cond do
251250
not ok?(status) ->
252251
dep
253-
File.exists?(Path.join(opts[:build], ".compile")) ->
252+
recently_fetched?(dep) ->
254253
%{dep | status: :compile}
255254
opts_app == false ->
256255
dep
257256
true ->
258-
path = if is_binary(opts_app), do: opts_app, else: "ebin/#{app}.app"
259-
path = Path.expand(path, build)
257+
path = if is_binary(opts_app), do: opts_app, else: "ebin/#{app}.app"
258+
path = Path.expand(path, opts[:build])
260259
%{dep | status: app_status(path, app, req)}
261260
end
262261
end
263262

263+
defp recently_fetched?(%Mix.Dep{opts: opts, scm: scm}) do
264+
scm.fetchable? &&
265+
Mix.Utils.stale?([Path.join(opts[:dest], ".fetch")],
266+
[Path.join(opts[:build], ".compile.lock")])
267+
end
268+
264269
defp app_status(app_path, app, req) do
265270
case :file.consult(app_path) do
266271
{:ok, [{:application, ^app, config}]} ->

lib/mix/lib/mix/dep/lock.ex

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ defmodule Mix.Dep.Lock do
5656
case File.read(lockfile) do
5757
{:ok, info} ->
5858
{value, _binding} = Code.eval_string(info)
59-
# TODO: Remove Enum.into() once apps migrate to new lock
60-
Enum.into(value || [], %{})
59+
value || %{}
6160
{:error, _} ->
6261
%{}
6362
end

lib/mix/lib/mix/project_stack.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ defmodule Mix.ProjectStack do
7373
[h|t] ->
7474
h = %{h | io_done: true}
7575
t = Enum.map(t, &%{&1 | io_done: false})
76-
{true, %{state | stack: [h|t]}}
76+
{has_app?(h), %{state | stack: [h|t]}}
7777
end
7878
end
7979
end
@@ -151,6 +151,10 @@ defmodule Mix.ProjectStack do
151151
{name, config, file}
152152
end
153153

154+
defp has_app?(%{config: config}) do
155+
config[:app]
156+
end
157+
154158
defp get_and_update(fun) do
155159
Agent.get_and_update __MODULE__, fun, @timeout
156160
end

lib/mix/lib/mix/scm.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ defmodule Mix.SCM do
1212
Returns a boolean if the dependency can be fetched
1313
or it is meant to be previously available in the
1414
filesystem.
15+
16+
Local dependencies (i.e. non fetchable ones) are automatically
17+
recompiled every time the parent project is compiled.
1518
"""
1619
defcallback fetchable? :: boolean
1720

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ defmodule Mix.Tasks.Deps.Compile do
2424
"""
2525

2626
import Mix.Dep, only: [loaded: 1, available?: 1, loaded_by_name: 2,
27-
format_dep: 1, make?: 1, mix?: 1, rebar?: 1]
27+
format_dep: 1, make?: 1, mix?: 1, rebar?: 1]
2828

2929
def run(args) do
3030
Mix.Project.get! # Require the project to be available
@@ -43,7 +43,7 @@ defmodule Mix.Tasks.Deps.Compile do
4343
config = Mix.Project.deps_config
4444

4545
compiled =
46-
Enum.map(deps, fn %Mix.Dep{app: app, status: status, opts: opts} = dep ->
46+
Enum.map(deps, fn %Mix.Dep{app: app, status: status, opts: opts, scm: scm} = dep ->
4747
check_unavailable!(app, status)
4848

4949
compiled = cond do
@@ -61,7 +61,7 @@ defmodule Mix.Tasks.Deps.Compile do
6161
end
6262

6363
unless mix?(dep), do: build_structure(dep, config)
64-
File.rm(Path.join(opts[:build], ".compile"))
64+
if scm.fetchable?, do: Mix.Dep.Lock.touch(opts[:build])
6565
compiled
6666
end)
6767

@@ -70,8 +70,8 @@ defmodule Mix.Tasks.Deps.Compile do
7070

7171
# All available dependencies can be compiled
7272
# except for umbrella applications.
73-
defp compilable?(%Mix.Dep{manager: manager, extra: extra} = dep) do
74-
available?(dep) and (manager != :mix or !extra[:umbrella?])
73+
defp compilable?(%Mix.Dep{extra: extra} = dep) do
74+
available?(dep) and !extra[:umbrella?]
7575
end
7676

7777
defp check_unavailable!(app, {:unavailable, _}) do

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,16 @@ defmodule Mix.Tasks.Deps do
3232
Mix also supports git and path dependencies:
3333
3434
{:foobar, git: "https://github.com/elixir-lang/foobar.git", tag: "0.1"}
35+
{:foobar, path: "path/to/foobar"}
3536
36-
And also umbrella applications:
37+
And also in umbrella dependencies:
3738
3839
{:myapp, in_umbrella: true}
3940
41+
Path and in umbrella dependencies are automatically recompiled by
42+
the parent project whenever they change. While fetchable dependencies
43+
like git are recompiled only when fetched/updated.
44+
4045
The dependencies versions are expected to follow Semantic Versioning
4146
and the requirements must be specified as defined in the `Version`
4247
module.
@@ -74,7 +79,7 @@ defmodule Mix.Tasks.Deps do
7479
7580
This task lists all dependencies in the following format:
7681
77-
* APP VERSION (SCM)
82+
APP VERSION (SCM)
7883
[locked at REF]
7984
STATUS
8085

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

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,15 @@ defmodule Mix.Tasks.DepsGitTest do
4141
end
4242
end
4343

44-
test "get, update and clean git repos with compilation" do
44+
test "gets and updates git repos with compilation" do
4545
Mix.Project.push GitApp
4646

4747
in_fixture "no_mixfile", fn ->
4848
Mix.Tasks.Deps.Get.run []
4949
message = "* Getting git_repo (#{fixture_path("git_repo")})"
5050
assert_received {:mix_shell, :info, [^message]}
51-
assert File.read!("mix.lock") =~ ~r/"git_repo": {:git, #{inspect fixture_path("git_repo")}, "[a-f0-9]+", \[\]}/
51+
assert File.read!("mix.lock") =~
52+
~r/"git_repo": {:git, #{inspect fixture_path("git_repo")}, "[a-f0-9]+", \[\]}/
5253

5354
Mix.Tasks.Deps.Update.run ["--all"]
5455
message = "* Updating git_repo (#{fixture_path("git_repo")})"
@@ -67,7 +68,7 @@ defmodule Mix.Tasks.DepsGitTest do
6768
end
6869
end
6970

70-
test "gets many levels deep dependencies" do
71+
test "gets and updates many levels deep dependencies" do
7172
Mix.Project.push DepsOnGitApp
7273

7374
in_fixture "no_mixfile", fn ->
@@ -80,64 +81,56 @@ defmodule Mix.Tasks.DepsGitTest do
8081
assert_received {:mix_shell, :info, [^message]}
8182

8283
assert File.exists?("deps/deps_on_git_repo/mix.exs")
84+
assert File.rm("deps/deps_on_git_repo/.fetch") == :ok
8385
assert File.exists?("deps/git_repo/mix.exs")
84-
end
85-
after
86-
purge [GitRepo, GitRepo.Mix]
87-
end
88-
89-
test "checks if repo information changes" do
90-
Mix.Project.push GitApp
91-
92-
in_fixture "no_mixfile", fn ->
93-
Mix.Tasks.Deps.Get.run []
94-
message = "* Getting git_repo (#{fixture_path("git_repo")})"
95-
assert_received {:mix_shell, :info, [^message]}
96-
97-
# We can compile just fine
98-
Mix.Task.clear
99-
Mix.Tasks.Run.run ["-e", "1+2"]
100-
assert_received {:mix_shell, :info, ["==> git_repo"]}
101-
102-
# Now let's add a submodules option
103-
Mix.Project.pop
104-
Mix.Project.push GitSubmodulesApp
86+
assert File.rm("deps/git_repo/.fetch") == :ok
10587

106-
# We should fail because the lock diverged
107-
Mix.Task.clear
108-
assert_raise Mix.Error, fn ->
109-
Mix.Tasks.Run.run ["1+2"]
110-
end
88+
Mix.Tasks.Deps.Update.run ["deps_on_git_repo"]
89+
assert File.exists?("deps/deps_on_git_repo/.fetch")
90+
assert File.exists?("deps/git_repo/.fetch")
11191
end
11292
after
11393
purge [GitRepo, GitRepo.Mix]
11494
end
11595

116-
test "recompiles the project when a deps change" do
96+
test "recompiles the project when a dep is fetched" do
11797
Mix.Project.push GitApp
11898

11999
in_fixture "no_mixfile", fn ->
120100
Mix.Tasks.Deps.Get.run []
121-
message = "* Getting git_repo (#{fixture_path("git_repo")})"
122-
assert File.exists?("_build/dev/lib/git_app/.compile.lock")
123-
assert_received {:mix_shell, :info, [^message]}
101+
assert File.exists?("deps/git_repo/.fetch")
124102

125103
# We can compile just fine
126-
Mix.Task.clear
127104
Mix.Tasks.Compile.run []
105+
assert_received {:mix_shell, :info, ["Compiled lib/git_repo.ex"]}
106+
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
128107

129-
# Notify a deps changed
108+
# Clear up to prepare for the update
109+
File.rm("_build/dev/lib/git_repo/ebin/Elixir.GitRepo.beam")
110+
File.rm("_build/dev/lib/git_repo/.compile.elixir")
111+
File.rm("deps/git_repo/.fetch")
112+
Mix.Task.clear
130113
Mix.shell.flush
131-
File.touch!("_build/dev/lib/git_app/.compile.lock", {{2020, 4, 17}, {14, 0, 0}})
114+
purge [A, B, C, GitRepo]
132115

133-
# We are forced to recompile
134-
purge [A, B, C]
135-
Mix.Task.clear
116+
# Update will mark the update required
117+
Mix.Tasks.Deps.Update.run ["git_repo"]
118+
assert File.exists?("deps/git_repo/.fetch")
119+
ensure_touched("deps/git_repo/.fetch") # Ensure timestamp differs
120+
121+
# mix deps.compile is required...
122+
Mix.Tasks.Deps.run []
123+
msg = " the dependency build is outdated, please run `mix deps.compile`"
124+
assert_received {:mix_shell, :info, [^msg]}
125+
126+
# But also ran automatically
136127
Mix.Tasks.Compile.run []
128+
assert_received {:mix_shell, :info, ["Compiled lib/git_repo.ex"]}
137129
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
130+
:ok
138131
end
139132
after
140-
purge [GitRepo, GitRepo.Mix]
133+
purge [A, B, C, GitRepo, GitRepo.Mix]
141134
end
142135

143136
test "all up to date dependencies" do
@@ -233,6 +226,10 @@ defmodule Mix.Tasks.DepsGitTest do
233226
# Update the project configuration. It should force an update.
234227
refresh deps: [{:git_repo, "0.1.0", git: fixture_path("git_repo"), ref: last}]
235228

229+
Mix.Tasks.Deps.run []
230+
msg = " lock outdated: the lock is outdated compared to the options in your mixfile"
231+
assert_received {:mix_shell, :info, [^msg]}
232+
236233
# Check an update was triggered
237234
Mix.Tasks.Deps.Get.run []
238235
assert File.read!("mix.lock") =~ last
@@ -258,7 +255,7 @@ defmodule Mix.Tasks.DepsGitTest do
258255
end
259256
end
260257

261-
test "does not load bad mix files on get" do
258+
test "does not load bad mixfiles on get" do
262259
Mix.Project.push GitApp
263260
[last, _, bad|_] = get_git_repo_revs
264261

@@ -274,7 +271,7 @@ defmodule Mix.Tasks.DepsGitTest do
274271
purge [GitRepo, GitRepo.Mix]
275272
end
276273

277-
test "does not load bad mix files on update" do
274+
test "does not load bad mixfiles on update" do
278275
Mix.Project.push GitApp
279276
[last, _, bad|_] = get_git_repo_revs
280277

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,12 @@ defmodule Mix.Tasks.DepsPathTest do
1515
end
1616
end
1717

18-
test "marks for compilation across environments on get/update" do
18+
test "does not mark for compilation on get/update" do
1919
Mix.Project.push DepsApp
2020

2121
in_fixture "deps_status", fn ->
22-
File.mkdir_p!("_build/dev/lib/raw_repo")
23-
File.mkdir_p!("_build/test/lib/raw_repo")
24-
2522
Mix.Tasks.Deps.Get.run ["--all"]
26-
assert File.exists?("_build/dev/lib/raw_repo/.compile")
27-
assert File.exists?("_build/test/lib/raw_repo/.compile")
28-
29-
Mix.Tasks.Run.run ["-e", "Mix.shell.info RawRepo.hello"]
30-
assert_received {:mix_shell, :info, ["==> raw_repo"]}
31-
assert_received {:mix_shell, :info, ["world"]}
23+
refute File.exists?("custom/raw_repo/.fetch")
3224
end
3325
end
3426

0 commit comments

Comments
 (0)