Skip to content

Commit cb236e7

Browse files
committed
Add Gleam integration with Mix
- Add Mix.Gleam module - Add specific gleam binary version requirement - Rely on `gleam export package-info`
1 parent 6de56db commit cb236e7

File tree

8 files changed

+245
-11
lines changed

8 files changed

+245
-11
lines changed

lib/mix/lib/mix/dep.ex

+9-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ defmodule Mix.Dep do
2727
* `top_level` - true if dependency was defined in the top-level project
2828
2929
* `manager` - the project management, possible values:
30-
`:rebar3` | `:mix` | `:make` | `nil`
30+
`:rebar3` | `:mix` | `:make` | `:gleam' | `nil`
3131
3232
* `from` - path to the file where the dependency was defined
3333
@@ -73,7 +73,7 @@ defmodule Mix.Dep do
7373
status: {:ok, String.t() | nil} | atom | tuple,
7474
opts: keyword,
7575
top_level: boolean,
76-
manager: :rebar3 | :mix | :make | nil,
76+
manager: :rebar3 | :mix | :make | :gleam | nil,
7777
from: String.t(),
7878
extra: term,
7979
system_env: keyword
@@ -535,6 +535,13 @@ defmodule Mix.Dep do
535535
manager == :make
536536
end
537537

538+
@doc """
539+
Returns `true` if dependency is a Gleam project.
540+
"""
541+
def gleam?(%Mix.Dep{manager: manager}) do
542+
manager == :gleam
543+
end
544+
538545
## Helpers
539546

540547
defp mix_env_var do

lib/mix/lib/mix/dep/converger.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ defmodule Mix.Dep.Converger do
426426
%{other | manager: sort_manager(other_manager, manager, in_upper?)}
427427
end
428428

429-
@managers [:mix, :rebar3, :make]
429+
@managers [:mix, :rebar3, :make, :gleam]
430430

431431
defp sort_manager(other_manager, manager, true) do
432432
other_manager || manager

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

+25-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
defmodule Mix.Dep.Loader do
99
@moduledoc false
1010

11-
import Mix.Dep, only: [ok?: 1, mix?: 1, rebar?: 1, make?: 1]
11+
import Mix.Dep, only: [ok?: 1, mix?: 1, rebar?: 1, make?: 1, gleam?: 1]
1212

1313
@doc """
1414
Gets all direct children of the current `Mix.Project`
@@ -84,9 +84,9 @@ defmodule Mix.Dep.Loader do
8484
def load(%Mix.Dep{manager: manager, scm: scm, opts: opts} = dep, children, locked?) do
8585
# The manager for a child dependency is set based on the following rules:
8686
# 1. Set in dependency definition
87-
# 2. From SCM, so that Hex dependencies of a rebar project can be compiled with mix
87+
# 2. From SCM, so that Hex dependencies of a rebar/gleam project can be compiled with mix
8888
# 3. From the parent dependency, used for rebar dependencies from git
89-
# 4. Inferred from files in dependency (mix.exs, rebar.config, Makefile)
89+
# 4. Inferred from files in dependency (mix.exs, rebar.config, Makefile, gleam.toml)
9090
manager = opts[:manager] || scm_manager(scm, opts) || manager || infer_manager(opts[:dest])
9191
dep = %{dep | manager: manager, status: scm_status(scm, opts)}
9292

@@ -106,6 +106,9 @@ defmodule Mix.Dep.Loader do
106106
make?(dep) ->
107107
make_dep(dep)
108108

109+
gleam?(dep) ->
110+
gleam_dep(dep, children, locked?)
111+
109112
true ->
110113
{dep, []}
111114
end
@@ -220,7 +223,7 @@ defmodule Mix.Dep.Loader do
220223

221224
# Note that we ignore Make dependencies because the
222225
# file based heuristic will always figure it out.
223-
@scm_managers ~w(mix rebar3)a
226+
@scm_managers ~w(mix rebar3 gleam)a
224227

225228
defp scm_manager(scm, opts) do
226229
managers = scm.managers(opts)
@@ -246,6 +249,9 @@ defmodule Mix.Dep.Loader do
246249
any_of?(dest, ["Makefile", "Makefile.win"]) ->
247250
:make
248251

252+
any_of?(dest, ["gleam.toml"]) ->
253+
:gleam
254+
249255
true ->
250256
nil
251257
end
@@ -361,6 +367,21 @@ defmodule Mix.Dep.Loader do
361367
{dep, []}
362368
end
363369

370+
defp gleam_dep(%Mix.Dep{opts: opts} = dep, children, locked?) do
371+
Mix.Gleam.require!()
372+
373+
deps =
374+
if children do
375+
Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?))
376+
else
377+
config = File.cd!(opts[:dest], fn -> Mix.Gleam.load_config(".") end)
378+
from = Path.join(opts[:dest], "gleam.toml")
379+
Enum.map(config[:deps], &to_dep(&1, from, _manager = nil, locked?))
380+
end
381+
382+
{%{dep | opts: Keyword.merge(opts, app: false, override: true)}, deps}
383+
end
384+
364385
defp mix_children(config, locked?, opts) do
365386
from = Mix.Project.project_file()
366387

lib/mix/lib/mix/gleam.ex

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
defmodule Mix.Gleam do
2+
# Version that introduced `gleam export package-information` command
3+
@required_gleam_version ">= 1.10.0"
4+
5+
def load_config(dir) do
6+
File.cd!(dir, fn ->
7+
gleam!(["export", "package-information", "--out", "/dev/stdout"])
8+
|> JSON.decode!()
9+
|> Map.fetch!("gleam.toml")
10+
|> parse_config()
11+
end)
12+
end
13+
14+
def parse_config(json) do
15+
try do
16+
deps =
17+
Map.get(json, "dependencies", %{})
18+
|> Enum.map(&parse_dep/1)
19+
20+
dev_deps =
21+
Map.get(json, "dev-dependencies", %{})
22+
|> Enum.map(&parse_dep(&1, only: :dev))
23+
24+
%{
25+
name: Map.fetch!(json, "name"),
26+
version: Map.fetch!(json, "version"),
27+
deps: deps ++ dev_deps
28+
}
29+
|> maybe_gleam_version(json["gleam"])
30+
rescue
31+
KeyError ->
32+
Mix.raise("Command \"gleam export package-information\" unexpected format: \n" <> json)
33+
end
34+
end
35+
36+
defp parse_dep({dep, requirement}, opts \\ []) do
37+
dep = String.to_atom(dep)
38+
39+
spec =
40+
case requirement do
41+
%{"version" => version} -> {dep, version, opts}
42+
%{"path" => path} -> {dep, Keyword.merge(opts, path: path)}
43+
end
44+
45+
case spec do
46+
{dep, version, []} -> {dep, version}
47+
spec -> spec
48+
end
49+
end
50+
51+
defp maybe_gleam_version(config, nil), do: config
52+
53+
defp maybe_gleam_version(config, version) do
54+
Map.put(config, :gleam, version)
55+
end
56+
57+
def require!() do
58+
available_version()
59+
|> Version.match?(@required_gleam_version)
60+
end
61+
62+
defp available_version do
63+
try do
64+
case gleam!(["--version"]) do
65+
"gleam " <> version -> Version.parse!(version) |> Version.to_string()
66+
output -> Mix.raise("Command \"gleam --version\" unexpected format: #{output}")
67+
end
68+
rescue
69+
e in Version.InvalidVersionError ->
70+
Mix.raise("Command \"gleam --version\" invalid version format: #{e.version}")
71+
end
72+
end
73+
74+
defp gleam!(args) do
75+
try do
76+
System.cmd("gleam", args)
77+
catch
78+
:error, :enoent ->
79+
Mix.raise(
80+
"The \"gleam\" executable is not available in your PATH. " <>
81+
"Please install it, as one of your dependencies requires it. "
82+
)
83+
else
84+
{response, 0} ->
85+
String.trim(response)
86+
87+
{response, _} when is_binary(response) ->
88+
Mix.raise("Command \"gleam #{Enum.join(args, " ")}\" failed with reason: #{response}")
89+
90+
{_, _} ->
91+
Mix.raise("Command \"gleam #{Enum.join(args, " ")}\" failed")
92+
end
93+
end
94+
end

lib/mix/lib/mix/task.compiler.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ defmodule Mix.Task.Compiler do
8080
* `:scm` - the SCM module of the dependency.
8181
8282
* `:manager` - the dependency project management, possible values:
83-
`:rebar3`, `:mix`, `:make`, `nil`.
83+
`:rebar3`, `:mix`, `:make`, `:gleam`, `nil`.
8484
8585
* `:os_pid` - the operating system PID of the process that run
8686
the compilation. The value is a string and it can be compared

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

+20-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ defmodule Mix.Tasks.Deps.Compile do
2222
* `Makefile.win`- invokes `nmake /F Makefile.win` (only on Windows)
2323
* `Makefile` - invokes `gmake` on DragonFlyBSD, FreeBSD, NetBSD, and OpenBSD,
2424
invokes `make` on any other operating system (except on Windows)
25+
* `gleam.toml` - invokes `gleam export`
2526
2627
The compilation can be customized by passing a `compile` option
2728
in the dependency:
@@ -139,9 +140,12 @@ defmodule Mix.Tasks.Deps.Compile do
139140
dep.manager == :rebar3 ->
140141
do_rebar3(dep, config)
141142

143+
dep.manager == :gleam ->
144+
do_gleam(dep, config)
145+
142146
true ->
143147
Mix.shell().error(
144-
"Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\" or \"Makefile\" " <>
148+
"Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\", \"Makefile\" or \"gleam.toml\" " <>
145149
"(pass :compile as an option to customize compilation, set it to \"false\" to do nothing)"
146150
)
147151

@@ -320,6 +324,21 @@ defmodule Mix.Tasks.Deps.Compile do
320324
true
321325
end
322326

327+
defp do_gleam(%Mix.Dep{opts: opts} = dep, config) do
328+
Mix.Gleam.require!()
329+
330+
lib = Path.join(Mix.Project.build_path(), "lib")
331+
out = opts[:build]
332+
package = opts[:dest]
333+
334+
command =
335+
{"gleam",
336+
["compile-package", "--target", "erlang", "--package", package, "--out", out, "--lib", lib]}
337+
338+
shell_cmd!(dep, config, command)
339+
Code.prepend_path(Path.join(out, "ebin"), cache: true)
340+
end
341+
323342
defp make_command(dep) do
324343
makefile_win? = makefile_win?(dep)
325344

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,10 @@ defmodule Mix.Tasks.Deps do
101101
* `:override` - if set to `true` the dependency will override any other
102102
definitions of itself by other dependencies
103103
104-
* `:manager` - Mix can also compile Rebar3 and makefile projects
104+
* `:manager` - Mix can also compile Rebar3, makefile and gleam projects
105105
and can fetch sub dependencies of Rebar3 projects. Mix will
106106
try to infer the type of project but it can be overridden with this
107-
option by setting it to `:mix`, `:rebar3`, or `:make`. In case
107+
option by setting it to `:mix`, `:rebar3`, `:make` or `:gleam`. In case
108108
there are conflicting definitions, the first manager in the list above
109109
will be picked up. For example, if a dependency is found with `:rebar3`
110110
as a manager in different part of the trees, `:rebar3` will be automatically

lib/mix/test/mix/gleam_test.exs

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: 2021 The Elixir Team
3+
# SPDX-FileCopyrightText: 2012 Plataformatec
4+
5+
Code.require_file("../test_helper.exs", __DIR__)
6+
7+
defmodule Mix.GleamTest do
8+
use MixTest.Case
9+
10+
@compile {:no_warn_undefined, [:gleam_dep, :gleam@int]}
11+
12+
defmodule GleamAsDep do
13+
def project do
14+
[
15+
app: :gleam_as_dep,
16+
version: "0.1.0",
17+
deps: [
18+
{:gleam_dep, path: MixTest.Case.tmp_path("gleam_dep"), app: false}
19+
]
20+
]
21+
end
22+
end
23+
24+
describe "load_config/1" do
25+
test "loads gleam.toml" do
26+
path = MixTest.Case.fixture_path("gleam_dep")
27+
config = Mix.Gleam.load_config(path)
28+
29+
expected = [
30+
{:gleam_stdlib, ">= 0.44.0 and < 2.0.0"},
31+
{:gleam_otp, ">= 0.16.1 and < 1.0.0"},
32+
{:gleeunit, ">= 1.0.0 and < 2.0.0", only: :dev}
33+
]
34+
35+
assert Enum.sort(config[:deps]) == Enum.sort(expected)
36+
end
37+
end
38+
39+
describe "gleam export package-information format" do
40+
test "parse_config" do
41+
config =
42+
%{
43+
"name" => "gael",
44+
"version" => "1.0.0",
45+
"gleam" => ">= 1.8.0",
46+
"dependencies" => %{
47+
"gleam_stdlib" => %{"version" => ">= 0.18.0 and < 2.0.0"},
48+
"my_other_project" => %{"path" => "../my_other_project"}
49+
},
50+
"dev-dependencies" => %{"gleeunit" => %{"version" => ">= 1.0.0 and < 2.0.0"}}
51+
}
52+
|> Mix.Gleam.parse_config()
53+
54+
assert config == %{
55+
name: "gael",
56+
version: "1.0.0",
57+
gleam: ">= 1.8.0",
58+
deps: [
59+
{:gleam_stdlib, ">= 0.18.0 and < 2.0.0"},
60+
{:my_other_project, path: "../my_other_project"},
61+
{:gleeunit, ">= 1.0.0 and < 2.0.0", only: :dev}
62+
]
63+
}
64+
end
65+
end
66+
67+
describe "integration with Mix" do
68+
test "gets and compiles dependencies" do
69+
in_tmp("get and compile dependencies", fn ->
70+
Mix.Project.push(GleamAsDep)
71+
72+
Mix.Tasks.Deps.Get.run([])
73+
assert_received {:mix_shell, :info, ["* Getting gleam_stdlib " <> _]}
74+
assert_received {:mix_shell, :info, ["* Getting gleam_otp " <> _]}
75+
assert_received {:mix_shell, :info, ["* Getting gleeunit " <> _]}
76+
77+
Mix.Tasks.Deps.Compile.run([])
78+
assert :gleam_dep.main()
79+
assert :gleam@int.to_string(1) == "1"
80+
81+
load_paths =
82+
Mix.Dep.Converger.converge([])
83+
|> Enum.map(&Mix.Dep.load_paths(&1))
84+
|> Enum.concat()
85+
86+
assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_dep/ebin"))
87+
assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_stdlib/ebin"))
88+
# Dep of a dep
89+
assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_erlang/ebin"))
90+
end)
91+
end
92+
end
93+
end

0 commit comments

Comments
 (0)