Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 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
d8bd1ff
Refactor `Mix.Gleam`
eksperimental Feb 27, 2026
cff76d0
Update lib/mix/lib/mix/gleam.ex
Papipo Mar 5, 2026
6762814
Merge remote-tracking branch 'eks/Papipo/add-gleam-compiler+' into ad…
Papipo Mar 13, 2026
356afbe
Couple of tiny fixes
Papipo Mar 13, 2026
0152025
Add support for gleam dev_dependencies
Papipo Mar 25, 2026
c5bafdc
Merge branch 'main' into add-gleam-compiler
Papipo Apr 14, 2026
d6670ee
Fix bad merge artifact
Papipo Apr 14, 2026
e35e0c8
Format test_helper
Papipo Apr 14, 2026
e631277
Merge branch 'elixir-lang:main' into add-gleam-compiler
Papipo Aug 31, 2026
e91cb8f
fix: remove unneeded gleam stuff
Papipo Aug 31, 2026
65d7ad7
add --prod flag to gleam compile-package
Papipo Aug 31, 2026
871e32b
fix: dev deps are no longer required
Papipo Aug 31, 2026
0b10bc4
update gleam tests
Papipo Sep 1, 2026
9aeaacd
fix: tures. Yes, fixed the fixtures.
Papipo Sep 1, 2026
33380fd
rely on gleam command errors for package requirements
Papipo Sep 1, 2026
3ca2f5d
remove application option
Papipo Sep 1, 2026
6cbae73
fix stranded tests
Papipo Sep 1, 2026
847d27d
adjust package-information command to latest version
Papipo Sep 2, 2026
28dfeb0
switch gleam compile-package flag to --src-only
Papipo Sep 22, 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 @@ -49,7 +49,7 @@ jobs:
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
otp-version: ${{ matrix.otp_version }}

gleam-version: "1.11.1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Papipo this could be increased now or whenever there is movement on the PR again, @lpil which version would we chose here? how would we keep this updated? And to what version (latest major stable minus one plus patches?)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Testing all versions seems to much. I guess it would make sense to just keep it up to date.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Don't forget to keep this up to date!

- 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 @@ -74,7 +74,7 @@ defmodule Mix.Dep do
opts: keyword,
deps: [t],
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 @@ -571,6 +571,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 @@ -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 @@ -228,7 +231,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 @@ -254,6 +257,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 @@ -369,6 +375,20 @@ defmodule Mix.Dep.Loader do
{dep, []}
end

defp gleam_dep(%Mix.Dep{opts: opts} = dep, _children = nil, manager, locked?) do
Mix.Gleam.requirements!()
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
127 changes: 127 additions & 0 deletions lib/mix/lib/mix/gleam.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team

defmodule Mix.Gleam do
# Version that introduced generating an .app file
@gleam_version_requirement ">= 1.19.0"

@spec load_config(Path.t()) :: config :: map()
def load_config(dir) do
File.cd!(dir, fn ->
with {:ok, output} <-
gleam(~W(export package-information)),
json <- JSON.decode!(output),
{:ok, gleam_toml} <- Map.fetch(json, "gleam.toml") do
parse_config(gleam_toml)
else
:error ->
{:error, "\"gleam.toml\" key not found in \"gleam export package-information\" output"}

{:error, message} ->
{:error, message}
end
|> assert_ok_value!()
end)
end

@spec parse_config(map()) :: {:ok, config :: map()} | {:error, message :: binary()}
def parse_config(json) when is_map(json) do
deps =
Map.get(json, "dependencies", %{})
|> Enum.map(&parse_dep!/1)

with {:ok, name} <- Map.fetch(json, "name"),
{:ok, version} <- Map.fetch(json, "version") do
config =
%{
name: name,
version: version,
deps: deps
}

{:ok, config}
else
:error ->
{:error,
"Command \"gleam export package-information\" unexpected format: \n" <>
inspect(json, pretty: true, limit: :infinity)}
end
end

defp parse_dep!({dep, requirement}, opts \\ []) do
String.to_atom(dep)
|> build_dep_spec(requirement, opts)
|> assert_ok_value!()
end

defp build_dep_spec(dep, %{"version" => version}, []),
do: {:ok, {dep, version}}

defp build_dep_spec(dep, %{"version" => version}, opts),
do: {:ok, {dep, version, opts}}

defp build_dep_spec(dep, %{"path" => path}, opts),
do: {:ok, {dep, Keyword.merge(opts, path: Path.expand(path))}}

defp build_dep_spec(dep, %{"git" => git, "ref" => ref}, _opts),
do: {:ok, {dep, git: git, ref: ref}}

defp build_dep_spec(dep, requirement, _opts),
do: {:error, "Gleam package #{dep} has unsupported requirement: #{inspect(requirement)}"}

@spec requirements!() :: :ok
def requirements!() do
case fetch_gleam_version() do
{:ok, gleam_version} ->
if Version.match?(gleam_version, @gleam_version_requirement) do
:ok
else
{:error,
"Current Gleam version does not meet minimum requirements " <>
"#{@gleam_version_requirement}, got: #{gleam_version}"}
end

{:error, message} ->
{:error, message}
end
|> assert_ok_value!()
end

defp fetch_gleam_version() do
case gleam(["--version"]) do
{:ok, "gleam " <> version} ->
case Version.parse(version) do
{:ok, parsed_version} ->
{:ok, Version.to_string(parsed_version)}

:error ->
{:error, "Command \"gleam --version\" invalid version format: #{version}"}
end

{:error, output} ->
{:error, "Command \"gleam --version\" unexpected format: #{output}"}
end
end

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

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

{_, _} ->
{:error, "Command \"gleam #{Enum.join(args, " ")}\" failed"}
end

defp assert_ok_value!(:ok), do: :ok
defp assert_ok_value!({:ok, term}), do: term
defp assert_ok_value!({:error, message}) when is_binary(message), do: Mix.raise(message)
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
42 changes: 40 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 @@ -143,9 +144,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 @@ -310,6 +314,40 @@ defmodule Mix.Tasks.Deps.Compile do
true
end

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

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

args = [
"compile-package",
"--src-only",
"--target",
"erlang",
"--package",
package,
"--out",
out,
"--lib",
lib
]

case System.cmd("gleam", args, [stderr_to_stdout: true] ++ opts_for_cmd(dep, config, [])) do
{_output, 0} ->
:ok

{output, _} ->
Mix.raise(
"Could not compile dependency #{inspect(dep.app)}, `gleam compile-package` command failed with reason: #{output}"
)
end

Code.prepend_path(Path.join(out, "ebin"), cache: true)
end

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

Expand Down Expand Up @@ -345,7 +383,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
14 changes: 14 additions & 0 deletions lib/mix/test/fixtures/gleam_dep/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# The directory Mix will write compiled artifacts to.
/_build/

# The directory Gleam will write compiled artifacts to.
/build/

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# BEAM bytecode files.
*.beam

# Also ignore archive artifacts (built via "mix archive.build").
*.ez
23 changes: 23 additions & 0 deletions lib/mix/test/fixtures/gleam_dep/gleam.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name = "gleam_dep"
version = "1.0.0"
description = "GleamDep description"
# 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.59.0"
gleam_otp = "0.16.1"

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

[erlang]
extra_applications = ["ssl"]
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" }
Loading