Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c5bd684
Add fixture for gleam support
Papipo Feb 13, 2025
3fcf897
Add Gleam integration with Mix
Papipo Feb 13, 2025
85ba7b2
Add support for git dependencies in gleam packages
Papipo Feb 20, 2025
96a1c0b
Exclude gleam tests if gleam is missing
Papipo Feb 20, 2025
7e21ba0
Fix deps.compile for gleam
Papipo Feb 21, 2025
e3c18a5
Add support for application_start_module
Papipo Feb 24, 2025
3ca3c6f
Handle gleam extra_applications
Papipo Feb 25, 2025
b142e94
Remove redundant quotes
Papipo Mar 26, 2025
ec9e597
Do not force `app: false` in gleam deps
Papipo Mar 31, 2025
08b2a54
Generate app file for gleam deps on compilation
Papipo Apr 2, 2025
26203af
Proper beam compilation and .app file generation
Papipo Apr 18, 2025
5ff6bc9
Apply suggestions from code review
Papipo Apr 16, 2025
d7f4558
Add support for :application option in Compile.App
Papipo Apr 23, 2025
449c45e
Proper handling of deeply nested and dev gleam deps
Papipo Apr 29, 2025
c7693b2
Install gleam 1.11.1 on CI
Papipo Jun 17, 2025
8d800f8
Apply code review suggestions
Papipo Jun 30, 2025
7ffa8c8
Pinpoint gleam deps to avoid brittle tests
Papipo Jun 30, 2025
46aa2d6
Fix typo
Papipo Jul 1, 2025
7e51726
Apply suggestions from code review
Papipo Jul 2, 2025
47f3b66
Fix typo in docs
Papipo Oct 24, 2025
2f0efe4
Update copyright comments
Papipo Nov 28, 2025
8f75b1b
Merge branch 'main' into add-gleam-compiler
Papipo Dec 4, 2025
00f852d
Merge branch 'main' into add-gleam-compiler
Papipo Feb 25, 2026
cff76d0
Update lib/mix/lib/mix/gleam.ex
Papipo Mar 5, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
with:
otp-version: ${{ matrix.otp_version }}

gleam-version: "1.11.1"
- name: Set ERL_COMPILER_OPTIONS
if: ${{ matrix.deterministic }}
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
Expand Down
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 @@ -555,6 +555,13 @@ defmodule Mix.Dep do
manager == :make
end

@doc """
Returns `true` if dependency is a Gleam project; otherwise returns `false`.
"""
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 @@ -407,7 +407,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
28 changes: 24 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, manager, locked?)

true ->
{dep, []}
end
Expand Down Expand Up @@ -227,7 +230,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 @@ -253,6 +256,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 @@ -368,6 +374,20 @@ defmodule Mix.Dep.Loader do
{dep, []}
end

defp gleam_dep(%Mix.Dep{opts: opts} = dep, _children = nil, manager, locked?) do
Mix.Gleam.require!()
dest = opts[:dest]
config = File.cd!(dest, fn -> Mix.Gleam.load_config(".") end)
from = Path.join(dest, "gleam.toml")
deps = Enum.map(config[:deps], &to_dep(&1, from, manager, locked?))

{dep, deps}
end

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

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

Expand Down
122 changes: 122 additions & 0 deletions lib/mix/lib/mix/gleam.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team

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!(~W(export package-information --out /dev/stdout))
|> JSON.decode!()
|> Map.fetch!("gleam.toml")
|> parse_config()
end)
end

def parse_config(json) 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, :test]))

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

@eksperimental eksperimental Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Mix.raise("Command \"gleam export package-information\" unexpected format: \n" <> json)
Mix.raise("Command \"gleam export package-information\" unexpected format: \n" <> inspect(json, pretty: true, limit: :infinity))

The compiler is not catching this. json is a map, but we are appending it to a string.

A test could help catch bugs like this. Adding a guard to the function detects it though.

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.expand(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, nil), do: config

defp maybe_erlang_opts(config, opts) do
application =
opts
|> Enum.reject(fn {_, value} -> value == nil end)
|> Enum.map(fn
{"application_start_module", module} when is_binary(module) ->
{:mod, {String.to_atom(module), []}}

{"extra_applications", applications} when is_list(applications) ->
{:extra_applications, Enum.map(applications, &String.to_atom/1)}

{key, value} ->
IO.warn("Gleam [erlang] option not supported\n #{key}: #{inspect(value)}")
end)

Map.put(config, :application, application)
end

def require!() do
available_version()
|> Version.match?(@required_gleam_version)
Copy link
Contributor

@eksperimental eksperimental Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requirement is not being enforced. It could return true or false and nothing will happen.

I think a test should be added to cover this.

end

defp available_version 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

defp gleam!(args) 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
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
14 changes: 12 additions & 2 deletions lib/mix/lib/mix/tasks/compile.app.ex
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ defmodule Mix.Tasks.Compile.App do
registered: [],
vsn: to_charlist(version)
]
|> merge_project_application(project)
|> merge_project_application(project, config[:application])
|> handle_extra_applications(config)
|> add_compile_env(current_properties)
|> add_modules(modules, compile_path)
Expand Down Expand Up @@ -263,7 +263,7 @@ defmodule Mix.Tasks.Compile.App do
end
end

defp merge_project_application(best_guess, project) do
defp merge_project_application(best_guess, project, _application = nil) do
if function_exported?(project, :application, 0) do
project_application = project.application()

Expand All @@ -279,6 +279,16 @@ defmodule Mix.Tasks.Compile.App do
end
end

defp merge_project_application(best_guess, _project, application) do
if not Keyword.keyword?(application) do
Mix.raise(
"Application configuration passed as :application should be a keyword list, got: #{inspect(application)}"
)
end

Keyword.merge(best_guess, validate_properties!(application))
end

defp validate_properties!(properties) do
Enum.each(properties, fn
{:description, value} ->
Expand Down
Loading