Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gleam support to Mix #14262

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions lib/mix/lib/mix/dep.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ defmodule Mix.Dep do
* `top_level` - true if dependency was defined in the top-level project

* `manager` - the project management, possible values:
`:rebar3` | `:mix` | `:make` | `nil`
`:rebar3` | `:mix` | `:make` | `:gleam' | `nil`

* `from` - path to the file where the dependency was defined

Expand Down Expand Up @@ -73,7 +73,7 @@ defmodule Mix.Dep do
status: {:ok, String.t() | nil} | atom | tuple,
opts: keyword,
top_level: boolean,
manager: :rebar3 | :mix | :make | nil,
manager: :rebar3 | :mix | :make | :gleam | nil,
from: String.t(),
extra: term,
system_env: keyword
Expand Down Expand Up @@ -535,6 +535,13 @@ defmodule Mix.Dep do
manager == :make
end

@doc """
Returns `true` if dependency is a Gleam project.
"""
def gleam?(%Mix.Dep{manager: manager}) do
manager == :gleam
end

## Helpers

defp mix_env_var do
Expand Down
2 changes: 1 addition & 1 deletion lib/mix/lib/mix/dep/converger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ defmodule Mix.Dep.Converger do
%{other | manager: sort_manager(other_manager, manager, in_upper?)}
end

@managers [:mix, :rebar3, :make]
@managers [:mix, :rebar3, :make, :gleam]

defp sort_manager(other_manager, manager, true) do
other_manager || manager
Expand Down
61 changes: 57 additions & 4 deletions lib/mix/lib/mix/dep/loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
defmodule Mix.Dep.Loader do
@moduledoc false

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

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

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

gleam?(dep) ->
gleam_dep(dep, children, locked?)

true ->
{dep, []}
end
Expand Down Expand Up @@ -220,7 +223,7 @@ defmodule Mix.Dep.Loader do

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

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

any_of?(dest, ["gleam.toml"]) ->
:gleam

true ->
nil
end
Expand Down Expand Up @@ -361,6 +367,53 @@ defmodule Mix.Dep.Loader do
{dep, []}
end

defp gleam_dep(%Mix.Dep{opts: opts} = dep, _children = nil, locked?) do
Mix.Gleam.require!()

config = File.cd!(opts[:dest], fn -> Mix.Gleam.load_config(".") end)
from = Path.join(opts[:dest], "gleam.toml")
deps = Enum.map(config[:deps], &to_dep(&1, from, _manager = nil, locked?))

properties =
[{:vsn, to_charlist(config[:version])}]
|> gleam_mod(config)
|> gleam_applications(config)

contents = :io_lib.format("~p.~n", [{:application, dep.app, properties}])

[opts[:build], "ebin"]
|> Path.join()
|> File.mkdir_p!()

[opts[:build], "ebin", "#{dep.app}.app"]
|> Path.join()
|> File.write!(IO.chardata_to_string(contents))

{dep, deps}
end

defp gleam_dep(%Mix.Dep{opts: opts} = dep, children, locked?) do
{dep, Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?))}
end

defp gleam_mod(properties, config) do
case config[:mod] do
nil -> properties
mod -> [{:mod, {String.to_atom(mod), []}} | properties]
end
end

defp gleam_applications(properties, config) do
case config[:extra_applications] do
nil ->
properties

applications ->
applications = Enum.map(applications, &String.to_atom/1)
[{:applications, applications} | properties]
end
end

defp mix_children(config, locked?, opts) do
from = Mix.Project.project_file()

Expand Down
118 changes: 118 additions & 0 deletions lib/mix/lib/mix/gleam.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
defmodule Mix.Gleam do
# Version that introduced `gleam export package-information` command
@required_gleam_version ">= 1.10.0"

def load_config(dir) do
File.cd!(dir, fn ->
gleam!(["export", "package-information", "--out", "/dev/stdout"])
|> JSON.decode!()
|> Map.fetch!("gleam.toml")
|> parse_config()
end)
end

def parse_config(json) do
try do
deps =
Map.get(json, "dependencies", %{})
|> Enum.map(&parse_dep/1)

dev_deps =
Map.get(json, "dev-dependencies", %{})
|> Enum.map(&parse_dep(&1, only: :dev))

%{
name: Map.fetch!(json, "name"),
version: Map.fetch!(json, "version"),
deps: deps ++ dev_deps
}
|> maybe_gleam_version(json)
|> maybe_erlang_opts(json)
rescue
KeyError ->
Mix.raise("Command \"gleam export package-information\" unexpected format: \n" <> json)
end
end

defp parse_dep({dep, requirement}, opts \\ []) do
dep = String.to_atom(dep)

spec =
case requirement do
%{"version" => version} ->
{dep, version, opts}

%{"path" => path} ->
{dep, Keyword.merge(opts, path: path)}

%{"git" => git, "ref" => ref} ->
{dep, git: git, ref: ref}

_ ->
Mix.raise("Gleam package #{dep} has unsupported requirement: #{inspect(requirement)}")
end

case spec do
{dep, version, []} -> {dep, version}
spec -> spec
end
end

defp maybe_gleam_version(config, json) do
case json["gleam"] do
nil -> config
version -> Map.put(config, :gleam, version)
end
end

defp maybe_erlang_opts(config, json) do
config =
case get_in(json, ["erlang", "application_start_module"]) do
nil -> config
mod -> Map.put(config, :mod, mod)
end

case get_in(json, ["erlang", "extra_applications"]) do
nil -> config
extra_applications -> Map.put(config, :extra_applications, extra_applications)
end
end

def require!() do
available_version()
|> Version.match?(@required_gleam_version)
end

defp available_version do
try do
case gleam!(["--version"]) do
"gleam " <> version -> Version.parse!(version) |> Version.to_string()
output -> Mix.raise("Command \"gleam --version\" unexpected format: #{output}")
end
rescue
e in Version.InvalidVersionError ->
Mix.raise("Command \"gleam --version\" invalid version format: #{e.version}")
end
end

defp gleam!(args) do
try do
System.cmd("gleam", args)
catch
:error, :enoent ->
Mix.raise(
"The \"gleam\" executable is not available in your PATH. " <>
"Please install it, as one of your dependencies requires it. "
)
else
{response, 0} ->
String.trim(response)

{response, _} when is_binary(response) ->
Mix.raise("Command \"gleam #{Enum.join(args, " ")}\" failed with reason: #{response}")

{_, _} ->
Mix.raise("Command \"gleam #{Enum.join(args, " ")}\" failed")
end
end
end
2 changes: 1 addition & 1 deletion lib/mix/lib/mix/task.compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ defmodule Mix.Task.Compiler do
* `:scm` - the SCM module of the dependency.

* `:manager` - the dependency project management, possible values:
`:rebar3`, `:mix`, `:make`, `nil`.
`:rebar3`, `:mix`, `:make`, `:gleam`, `nil`.

* `:os_pid` - the operating system PID of the process that run
the compilation. The value is a string and it can be compared
Expand Down
23 changes: 21 additions & 2 deletions lib/mix/lib/mix/tasks/deps.compile.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ defmodule Mix.Tasks.Deps.Compile do
* `Makefile.win`- invokes `nmake /F Makefile.win` (only on Windows)
* `Makefile` - invokes `gmake` on DragonFlyBSD, FreeBSD, NetBSD, and OpenBSD,
invokes `make` on any other operating system (except on Windows)
* `gleam.toml` - invokes `gleam compile-package`

The compilation can be customized by passing a `compile` option
in the dependency:
Expand Down Expand Up @@ -139,9 +140,12 @@ defmodule Mix.Tasks.Deps.Compile do
dep.manager == :rebar3 ->
do_rebar3(dep, config)

dep.manager == :gleam ->
do_gleam(dep, config)

true ->
Mix.shell().error(
"Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\" or \"Makefile\" " <>
"Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\", \"Makefile\" or \"gleam.toml\" " <>
"(pass :compile as an option to customize compilation, set it to \"false\" to do nothing)"
)

Expand Down Expand Up @@ -320,6 +324,21 @@ defmodule Mix.Tasks.Deps.Compile do
true
end

defp do_gleam(%Mix.Dep{opts: opts} = dep, config) do
Mix.Gleam.require!()

lib = Path.join(Mix.Project.build_path(), "lib")
out = opts[:build]
package = opts[:dest]

command =
{"gleam",
["compile-package", "--target", "erlang", "--package", package, "--out", out, "--lib", lib]}

shell_cmd!(dep, config, command)
Code.prepend_path(Path.join(out, "ebin"), cache: true)
end

defp make_command(dep) do
makefile_win? = makefile_win?(dep)

Expand Down Expand Up @@ -355,7 +374,7 @@ defmodule Mix.Tasks.Deps.Compile do
defp shell_cmd!(%Mix.Dep{app: app} = dep, config, command, env \\ []) do
if Mix.shell().cmd(command, [print_app: true] ++ opts_for_cmd(dep, config, env)) != 0 do
Mix.raise(
"Could not compile dependency #{inspect(app)}, \"#{command}\" command failed. " <>
"Could not compile dependency #{inspect(app)}, #{inspect(command)} command failed. " <>
deps_compile_feedback(app)
)
end
Expand Down
4 changes: 2 additions & 2 deletions lib/mix/lib/mix/tasks/deps.ex
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ defmodule Mix.Tasks.Deps do
* `:override` - if set to `true` the dependency will override any other
definitions of itself by other dependencies

* `:manager` - Mix can also compile Rebar3 and makefile projects
* `:manager` - Mix can also compile Rebar3, makefile and gleam projects
and can fetch sub dependencies of Rebar3 projects. Mix will
try to infer the type of project but it can be overridden with this
option by setting it to `:mix`, `:rebar3`, or `:make`. In case
option by setting it to `:mix`, `:rebar3`, `:make` or `:gleam`. In case
there are conflicting definitions, the first manager in the list above
will be picked up. For example, if a dependency is found with `:rebar3`
as a manager in different part of the trees, `:rebar3` will be automatically
Expand Down
4 changes: 4 additions & 0 deletions lib/mix/test/fixtures/gleam_dep/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.beam
*.ez
/build
erl_crash.dump
24 changes: 24 additions & 0 deletions lib/mix/test/fixtures/gleam_dep/gleam.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name = "gleam_dep"
version = "1.0.0"

# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
#
# description = ""
# licences = ["Apache-2.0"]
# repository = { type = "github", user = "", repo = "" }
# links = [{ title = "Website", href = "" }]
#
# For a full reference of all the available options, you can have a look at
# https://gleam.run/writing-gleam/gleam-toml/.

[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
gleam_otp = ">= 0.16.1 and < 1.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"

[erlang]
extra_applications = ["ssl"]
application_start_module = "gleam_dep@somemodule"
14 changes: 14 additions & 0 deletions lib/mix/test/fixtures/gleam_dep/manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# This file was generated by Gleam
# You typically do not need to edit this file

packages = [
{ name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" },
{ name = "gleam_otp", version = "0.16.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "50DA1539FC8E8FA09924EB36A67A2BBB0AD6B27BCDED5A7EF627057CF69D035E" },
{ name = "gleam_stdlib", version = "0.54.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "723BA61A2BAE8D67406E59DD88CEA1B3C3F266FC8D70F64BE9FEC81B4505B927" },
{ name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" },
]

[requirements]
gleam_otp = { version = ">= 0.16.1 and < 1.0.0" }
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
3 changes: 3 additions & 0 deletions lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub fn main() {
True
}
Loading