Skip to content
Merged
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
2 changes: 1 addition & 1 deletion lib/mix/lib/mix/dep/loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ defmodule Mix.Dep.Loader do
end

defp gleam_dep(%Mix.Dep{opts: opts} = dep, _children = nil, manager, locked?) do
Mix.Gleam.require!()
Mix.Gleam.requirements!()
dest = opts[:dest]
config = File.cd!(dest, fn -> Mix.Gleam.load_config(".") end)
from = Path.join(dest, "gleam.toml")
Expand Down
147 changes: 92 additions & 55 deletions lib/mix/lib/mix/gleam.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,61 +3,77 @@

defmodule Mix.Gleam do
# Version that introduced `gleam export package-information` command
@required_gleam_version ">= 1.10.0"
@gleam_version_requirement ">= 1.10.0"

@spec load_config(Path.t()) :: config :: map()
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()
with {:ok, output} <-
gleam(~W(export package-information --out /dev/stdout)),
json <- JSON.decode!(output),
Comment thread
Papipo marked this conversation as resolved.
{:ok, gleam_toml} <- Map.fetch(json, "gleam.toml") do
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

maybe?

{{:ok, gleam_toml}, :fetch_toml} <- {Map.fetch(json, "gleam.toml"), :fetch_toml} do
        {:error, :fetch_toml} ->
          {:error, "\"gleam.toml\" key not found in \"gleam export package-information\" output"}

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.

Initially i did that, but the only function that returns :error is Map.fetch/2 so I decided to simplify it.

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

def parse_config(json) do
@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)
|> Enum.map(&parse_dep!/1)

dev_deps =
Map.get(json, "dev-dependencies", %{})
Copy link
Copy Markdown

@inoas inoas Mar 9, 2026

Choose a reason for hiding this comment

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

Blocker imho:

The gleam.toml format is now consistent. The two sausage-case fields (dev-dependencies and tag-prefix) have been replaced by snake_case versions. Files using the old names will continue to work.

Source: https://github.com/gleam-lang/gleam/blob/v1.15.0-rc1/CHANGELOG.md#build-tool

... AFAIU we must support dev_dependencies and dev-dependencies (and merge both)

|> 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)
|> Enum.map(&parse_dep!(&1, only: [:dev, :test]))

with {:ok, name} <- Map.fetch(json, "name"),
{:ok, version} <- Map.fetch(json, "version") do
config =
%{
name: name,
version: version,
deps: deps ++ dev_deps
}
|> maybe_gleam_version(json)
|> maybe_erlang_opts(json["erlang"])

{: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
dep = String.to_atom(dep)
defp parse_dep!({dep, requirement}, opts \\ []) do
String.to_atom(dep)
|> build_dep_spec(requirement, opts)
|> assert_ok_value!()
end

spec =
case requirement do
%{"version" => version} ->
{dep, version, opts}
defp build_dep_spec(dep, %{"version" => version}, []),
do: {:ok, {dep, version}}

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

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

_ ->
Mix.raise("Gleam package #{dep} has unsupported requirement: #{inspect(requirement)}")
end
defp build_dep_spec(dep, %{"git" => git, "ref" => ref}, _opts),
do: {:ok, {dep, git: git, ref: ref}}

case spec do
{dep, version, []} -> {dep, version}
spec -> spec
end
end
defp build_dep_spec(dep, requirement, _opts),
do: {:error, "Gleam package #{dep} has unsupported requirement: #{inspect(requirement)}"}

defp maybe_gleam_version(config, json) do
case json["gleam"] do
Expand Down Expand Up @@ -86,37 +102,58 @@ defmodule Mix.Gleam do
Map.put(config, :application, application)
end

def require!() do
available_version()
|> Version.match?(@required_gleam_version)
@spec requirements!() :: :ok
def requirements!() do
case fetch_gleam_version() do
{:ok, gleam_version} ->
Copy link
Copy Markdown

@inoas inoas Mar 9, 2026

Choose a reason for hiding this comment

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

elixir-lang#14262 (comment)

Do you still think a test is required, after the changes you made?
@eksperimental

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.

Well, in theory it should work, but so i theory it should have worked before the fix. Personally, since it's crucial I would test it.

if Version.match?(gleam_version, @gleam_version_requirement) do
{:ok, :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 available_version do
case gleam!(["--version"]) do
"gleam " <> version -> Version.parse!(version) |> Version.to_string()
output -> Mix.raise("Command \"gleam --version\" unexpected format: #{output}")
defp fetch_gleam_version() do
case gleam(["--version"]) do
{:ok, 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
rescue
e in Version.InvalidVersionError ->
Mix.raise("Command \"gleam --version\" invalid version format: #{e.version}")
end

defp gleam!(args) do
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. "
)
{:error,
"The \"gleam\" executable is not available in your PATH. " <>
"Please install it, as one of your dependencies requires it"}
else
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I have never seen def/catch/else syntax so far, unless I am blind it is also not mentioned here https://hexdocs.pm/elixir/try-catch-and-rescue.html But if it works, nvm.

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.

Neither have I but seems to work. Maybe it its worth documenting this

{response, 0} ->
String.trim(response)
{:ok, String.trim(response)}

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

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

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/tasks/deps.compile.ex
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ defmodule Mix.Tasks.Deps.Compile do
end

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

lib = Path.join(Mix.Project.build_path(), "lib")
Expand Down
5 changes: 3 additions & 2 deletions lib/mix/test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ re_import_exclude =

gleam_exclude =
try do
Mix.Gleam.require!()
Mix.Gleam.requirements!()
[]
rescue
Mix.Error -> [gleam: true]
Expand All @@ -69,7 +69,8 @@ ex_unit_opts =
trace: !!System.get_env("TRACE"),
exclude:
epmd_exclude ++
os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude ++ re_import_exclude ++ gleam_exclude,
os_exclude ++
git_exclude ++ line_exclude ++ cover_exclude ++ re_import_exclude ++ gleam_exclude,
include: line_include,
assert_receive_timeout: String.to_integer(System.get_env("ELIXIR_ASSERT_TIMEOUT", "300"))
] ++ maybe_seed_opt
Expand Down
Loading