Skip to content

Commit c457405

Browse files
wojtekmachjosevalim
authored andcommitted
Mix.install: Add :config_path and :lockfile options (#12051)
1 parent c63cc10 commit c457405

File tree

6 files changed

+218
-38
lines changed

6 files changed

+218
-38
lines changed

lib/mix/lib/mix.ex

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -561,25 +561,31 @@ defmodule Mix do
561561
* `:elixir` - if set, ensures the current Elixir version matches the given
562562
version requirement (Default: `nil`)
563563
564+
* `:system_env` (since v1.13.0) - a list or a map of system environment variable
565+
names with respective values as binaries. The system environment is made part
566+
of the `Mix.install/2` cache, so different configurations will lead to different apps
567+
564568
* `:config` (since v1.13.0) - a keyword list of keyword lists with application
565569
configuration to be set before the apps loaded. The configuration is part of
566570
the `Mix.install/2` cache, so different configurations will lead to different
567571
apps
568572
569-
* `:system_env` (since v1.13.0) - a list or a map of system environment variable
570-
names with respective values as binaries. The system environment is made part
571-
of the `Mix.install/2` cache, so different configurations will lead to different apps
573+
* `:config_path` (since v1.14.0) - path to a configuration file. If a `runtime.exs`
574+
file exists in the same directory as the given path, it is loaded too.
575+
576+
* `:lockfile` (since v1.14.0) - path to a lockfile to be used as a basis of
577+
dependency resolution.
572578
573579
## Examples
574580
575-
To install `:decimal` and `:jason`:
581+
Installing `:decimal` and `:jason`:
576582
577583
Mix.install([
578584
:decimal,
579585
{:jason, "~> 1.0"}
580586
])
581587
582-
Using `:nx`, `:exla`, and configure the underlying applications
588+
Installing `:nx` & `:exla`, and configuring the underlying applications
583589
and environment variables:
584590
585591
Mix.install(
@@ -592,6 +598,23 @@ defmodule Mix do
592598
]
593599
)
594600
601+
Installing a Mix project as a path dependency along with its configuration
602+
and deps:
603+
604+
# $ git clone https://github.com/hexpm/hexpm /tmp/hexpm
605+
# $ cd /tmp/hexpm && mix setup
606+
607+
Mix.install(
608+
[
609+
{:hexpm, path: "/tmp/hexpm", env: :dev},
610+
],
611+
config_path: "/tmp/hexpm/config/config.exs",
612+
lockfile: "/tmp/hexpm/mix.lock"
613+
)
614+
615+
Hexpm.Repo.query!("SELECT COUNT(1) from packages")
616+
#=> ...
617+
595618
## Limitations
596619
597620
There is one limitation to `Mix.install/2`, which is actually an Elixir
@@ -659,6 +682,7 @@ defmodule Mix do
659682
end)
660683

661684
config = Keyword.get(opts, :config, [])
685+
config_path = opts[:config_path] && Path.expand(opts[:config_path])
662686
system_env = Keyword.get(opts, :system_env, [])
663687
consolidate_protocols? = Keyword.get(opts, :consolidate_protocols, true)
664688

@@ -675,18 +699,14 @@ defmodule Mix do
675699
Application.put_all_env(config, persistent: true)
676700
System.put_env(system_env)
677701

678-
installs_root =
679-
System.get_env("MIX_INSTALL_DIR") || Path.join(Mix.Utils.mix_cache(), "installs")
680-
681-
version = "elixir-#{System.version()}-erts-#{:erlang.system_info(:version)}"
682-
dir = Path.join([installs_root, version, id])
702+
install_dir = install_dir(id)
683703

684704
if opts[:verbose] do
685-
Mix.shell().info("Mix.install/2 using #{dir}")
705+
Mix.shell().info("Mix.install/2 using #{install_dir}")
686706
end
687707

688708
if force? do
689-
File.rm_rf!(dir)
709+
File.rm_rf!(install_dir)
690710
end
691711

692712
config = [
@@ -701,21 +721,51 @@ defmodule Mix do
701721
erlc_paths: ["src"],
702722
elixirc_paths: ["lib"],
703723
compilers: [],
704-
consolidate_protocols: consolidate_protocols?
724+
consolidate_protocols: consolidate_protocols?,
725+
config_path: config_path
705726
]
706727

707728
started_apps = Application.started_applications()
708729
:ok = Mix.Local.append_archives()
709730
:ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile")
710-
build_dir = Path.join(dir, "_build")
731+
build_dir = Path.join(install_dir, "_build")
732+
external_lockfile = opts[:lockfile] && Path.expand(opts[:lockfile])
711733

712734
try do
713-
run_deps? = not File.dir?(build_dir)
714-
File.mkdir_p!(dir)
735+
first_build? = not File.dir?(build_dir)
736+
File.mkdir_p!(install_dir)
737+
738+
File.cd!(install_dir, fn ->
739+
if config_path do
740+
Mix.Task.rerun("loadconfig")
741+
end
742+
743+
cond do
744+
external_lockfile ->
745+
md5_path = Path.join(install_dir, "merge.lock.md5")
746+
747+
old_md5 =
748+
case File.read(md5_path) do
749+
{:ok, data} -> Base.decode64!(data)
750+
_ -> nil
751+
end
752+
753+
new_md5 = external_lockfile |> File.read!() |> :erlang.md5()
715754

716-
File.cd!(dir, fn ->
717-
if run_deps? do
718-
Mix.Task.rerun("deps.get")
755+
if old_md5 != new_md5 do
756+
lockfile = Path.join(install_dir, "mix.lock")
757+
old_lock = Mix.Dep.Lock.read(lockfile)
758+
new_lock = Mix.Dep.Lock.read(external_lockfile)
759+
Mix.Dep.Lock.write(lockfile, Map.merge(old_lock, new_lock))
760+
File.write!(md5_path, Base.encode64(new_md5))
761+
Mix.Task.rerun("deps.get")
762+
end
763+
764+
first_build? ->
765+
Mix.Task.rerun("deps.get")
766+
767+
true ->
768+
:ok
719769
end
720770

721771
Mix.Task.rerun("deps.loadpaths")
@@ -725,6 +775,10 @@ defmodule Mix do
725775
stop_apps(Application.started_applications() -- started_apps)
726776

727777
Mix.Task.rerun("compile")
778+
779+
if config_path do
780+
Mix.Task.rerun("app.config")
781+
end
728782
end)
729783

730784
for %{app: app, opts: opts} <- Mix.Dep.cached(),
@@ -746,6 +800,15 @@ defmodule Mix do
746800
end
747801
end
748802

803+
defp install_dir(cache_id) do
804+
install_root =
805+
System.get_env("MIX_INSTALL_DIR") ||
806+
Path.join(Mix.Utils.mix_cache(), "installs")
807+
808+
version = "elixir-#{System.version()}-erts-#{:erlang.system_info(:version)}"
809+
Path.join([install_root, version, cache_id])
810+
end
811+
749812
@doc """
750813
Returns whether `Mix.install/2` was called in the current node.
751814
"""

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ defmodule Mix.Dep.Lock do
99
Reads the lockfile, returns a map containing
1010
each app name and its current lock information.
1111
"""
12-
@spec read() :: map
13-
def read() do
14-
lockfile = lockfile()
12+
@spec read(Path.t()) :: map()
13+
def read(lockfile \\ lockfile()) do
1514
opts = [file: lockfile, warn_on_unnecessary_quotes: false]
1615

1716
with {:ok, contents} <- File.read(lockfile),
@@ -27,15 +26,15 @@ defmodule Mix.Dep.Lock do
2726
@doc """
2827
Receives a map and writes it as the latest lock.
2928
"""
30-
@spec write(map) :: :ok
31-
def write(map) do
29+
@spec write(Path.t(), map()) :: :ok
30+
def write(lockfile \\ lockfile(), map) do
3231
unless map == read() do
3332
lines =
3433
for {app, rev} <- Enum.sort(map), rev != nil do
3534
~s( "#{app}": #{inspect(rev, limit: :infinity)},\n)
3635
end
3736

38-
File.write!(lockfile(), ["%{\n", lines, "}\n"])
37+
File.write!(lockfile, ["%{\n", lines, "}\n"])
3938
Mix.Task.run("will_recompile")
4039
end
4140

lib/mix/test/mix/tasks/compile.elixir_test.exs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,10 +1638,4 @@ defmodule Mix.Tasks.Compile.ElixirTest do
16381638
assert Mix.Tasks.Compile.Elixir.run(["--no-optional-deps"]) == {:ok, []}
16391639
end)
16401640
end
1641-
1642-
defp get_git_repo_revs(repo) do
1643-
File.cd!(fixture_path(repo), fn ->
1644-
Regex.split(~r/\r?\n/, System.cmd("git", ["log", "--format=%H"]) |> elem(0))
1645-
end)
1646-
end
16471641
end

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -483,10 +483,4 @@ defmodule Mix.Tasks.DepsGitTest do
483483
Mix.ProjectStack.post_config(post_config)
484484
Mix.Project.push(name, file)
485485
end
486-
487-
defp get_git_repo_revs(repo) do
488-
File.cd!(fixture_path(repo), fn ->
489-
Regex.split(~r/\r?\n/, System.cmd("git", ["log", "--format=%H"]) |> elem(0))
490-
end)
491-
end
492486
end

lib/mix/test/mix_test.exs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ defmodule MixTest do
135135
refute Protocol.consolidated?(InstallTest.Protocol)
136136
end
137137

138-
test "config and system_env", %{tmp_dir: tmp_dir} do
138+
test ":config and :system_env", %{tmp_dir: tmp_dir} do
139139
Mix.install(
140140
[
141141
{:install_test, path: Path.join(tmp_dir, "install_test")}
@@ -153,6 +153,130 @@ defmodule MixTest do
153153
Application.delete_env(:unknown_app, :foo, persistent: true)
154154
end
155155

156+
test ":config_path", %{tmp_dir: tmp_dir} do
157+
config_path = Path.join(tmp_dir, "config.exs")
158+
159+
File.write!(config_path, """
160+
import Config
161+
config :myapp, :foo, 1
162+
""")
163+
164+
Mix.install(
165+
[
166+
{:install_test, path: Path.join(tmp_dir, "install_test")}
167+
],
168+
config_path: config_path
169+
)
170+
171+
assert Application.fetch_env!(:myapp, :foo) == 1
172+
after
173+
Application.delete_env(:myapp, :foo)
174+
end
175+
176+
test ":config_path and runtime config", %{tmp_dir: tmp_dir} do
177+
config_path = Path.join(tmp_dir, "config.exs")
178+
179+
File.write!(config_path, """
180+
import Config
181+
config :myapp, :foo, 1
182+
""")
183+
184+
File.write!(Path.join(tmp_dir, "runtime.exs"), """
185+
import Config
186+
config :myapp, :bar, 2
187+
""")
188+
189+
Mix.install(
190+
[
191+
{:install_test, path: Path.join(tmp_dir, "install_test")}
192+
],
193+
config_path: config_path
194+
)
195+
196+
assert Application.fetch_env!(:myapp, :foo) == 1
197+
assert Application.fetch_env!(:myapp, :bar) == 2
198+
after
199+
Application.delete_env(:myapp, :foo)
200+
Application.delete_env(:myapp, :bar)
201+
end
202+
203+
test ":config_path that does not exist" do
204+
assert_raise File.Error, ~r/bad.exs": no such file or directory/, fn ->
205+
Mix.install([], config_path: "bad.exs")
206+
end
207+
end
208+
209+
defmodule GitApp do
210+
def project do
211+
[
212+
app: :git_app,
213+
version: "0.1.0",
214+
deps: [
215+
{:git_repo, "0.1.0", [git: fixture_path("git_repo")]}
216+
]
217+
]
218+
end
219+
end
220+
221+
test ":lockfile with first build", %{tmp_dir: tmp_dir} do
222+
Mix.Project.push(GitApp)
223+
[_latest_rev, rev | _] = get_git_repo_revs("git_repo")
224+
lockfile = Path.join(tmp_dir, "lock")
225+
Mix.Dep.Lock.write(lockfile, %{git_repo: {:git, fixture_path("git_repo"), rev, []}})
226+
Mix.ProjectStack.pop()
227+
228+
Mix.install(
229+
[
230+
{:git_repo, git: fixture_path("git_repo")}
231+
],
232+
lockfile: lockfile,
233+
verbose: true
234+
)
235+
236+
assert_received {:mix_shell, :info, ["* Getting git_repo " <> _]}
237+
assert_received {:mix_shell, :info, ["Mix.install/2 using " <> install_dir]}
238+
assert File.read!(Path.join(install_dir, "mix.lock")) =~ rev
239+
after
240+
purge([GitRepo, GitRepo.MixProject])
241+
end
242+
243+
test ":lockfile merging", %{tmp_dir: tmp_dir} do
244+
[rev1, rev2 | _] = get_git_repo_revs("git_repo")
245+
246+
Mix.install(
247+
[
248+
{:git_repo, git: fixture_path("git_repo")}
249+
],
250+
verbose: true
251+
)
252+
253+
assert_received {:mix_shell, :info, ["* Getting git_repo " <> _]}
254+
assert_received {:mix_shell, :info, ["Mix.install/2 using " <> install_dir]}
255+
assert File.read!(Path.join(install_dir, "mix.lock")) =~ rev1
256+
257+
Mix.Project.push(GitApp)
258+
lockfile = Path.join(tmp_dir, "lock")
259+
Mix.Dep.Lock.write(lockfile, %{git_repo: {:git, fixture_path("git_repo"), rev2, []}})
260+
Mix.ProjectStack.pop()
261+
262+
Mix.install(
263+
[
264+
{:git_repo, git: fixture_path("git_repo")}
265+
],
266+
lockfile: lockfile
267+
)
268+
269+
assert File.read!(Path.join(install_dir, "mix.lock")) =~ rev1
270+
after
271+
purge([GitRepo, GitRepo.MixProject])
272+
end
273+
274+
test ":lockfile that does not exist" do
275+
assert_raise File.Error, ~r/bad": no such file or directory/, fn ->
276+
Mix.install([], lockfile: "bad")
277+
end
278+
end
279+
156280
test "installed?", %{tmp_dir: tmp_dir} do
157281
refute Mix.installed?()
158282

lib/mix/test/test_helper.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ defmodule MixTest.Case do
204204
tmp = tmp_path() |> String.to_charlist()
205205
for path <- :code.get_path(), :string.str(path, tmp) != 0, do: :code.del_path(path)
206206
end
207+
208+
def get_git_repo_revs(repo) do
209+
File.cd!(fixture_path(repo), fn ->
210+
Regex.split(~r/\r?\n/, System.cmd("git", ["log", "--format=%H"]) |> elem(0), trim: true)
211+
end)
212+
end
207213
end
208214

209215
## Set up globals

0 commit comments

Comments
 (0)