From 6de56db45e5d6b297318acdeb79692678df7e235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Thu, 13 Feb 2025 09:35:12 +0100 Subject: [PATCH 01/19] Add fixture for gleam support --- lib/mix/test/fixtures/gleam_dep/.gitignore | 4 ++++ lib/mix/test/fixtures/gleam_dep/gleam.toml | 20 +++++++++++++++++++ lib/mix/test/fixtures/gleam_dep/manifest.toml | 14 +++++++++++++ .../fixtures/gleam_dep/src/gleam_dep.gleam | 3 +++ lib/mix/test/test_helper.exs | 9 +++++++++ 5 files changed, 50 insertions(+) create mode 100644 lib/mix/test/fixtures/gleam_dep/.gitignore create mode 100644 lib/mix/test/fixtures/gleam_dep/gleam.toml create mode 100644 lib/mix/test/fixtures/gleam_dep/manifest.toml create mode 100644 lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam diff --git a/lib/mix/test/fixtures/gleam_dep/.gitignore b/lib/mix/test/fixtures/gleam_dep/.gitignore new file mode 100644 index 00000000000..599be4eb929 --- /dev/null +++ b/lib/mix/test/fixtures/gleam_dep/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/lib/mix/test/fixtures/gleam_dep/gleam.toml b/lib/mix/test/fixtures/gleam_dep/gleam.toml new file mode 100644 index 00000000000..fc88f8e0f47 --- /dev/null +++ b/lib/mix/test/fixtures/gleam_dep/gleam.toml @@ -0,0 +1,20 @@ +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" diff --git a/lib/mix/test/fixtures/gleam_dep/manifest.toml b/lib/mix/test/fixtures/gleam_dep/manifest.toml new file mode 100644 index 00000000000..f7e3f2b653e --- /dev/null +++ b/lib/mix/test/fixtures/gleam_dep/manifest.toml @@ -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" } diff --git a/lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam b/lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam new file mode 100644 index 00000000000..673bfdd0147 --- /dev/null +++ b/lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam @@ -0,0 +1,3 @@ +pub fn main() { + True +} diff --git a/lib/mix/test/test_helper.exs b/lib/mix/test/test_helper.exs index 05f21bc2df9..10e7ac64b44 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -272,6 +272,15 @@ Enum.each(fixtures, fn fixture -> File.cp_r!(source, dest) end) +## Set up Gleam fixtures + +fixture = "gleam_dep" + +source = MixTest.Case.fixture_path(fixture) +dest = MixTest.Case.tmp_path(fixture) +File.mkdir_p!(dest) +File.cp_r!(source, dest) + ## Set up Git fixtures System.cmd("git", ~w[config --global user.email mix@example.com]) From cb236e777baedbab663ab26dd2e9f57881816332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Thu, 13 Feb 2025 09:42:04 +0100 Subject: [PATCH 02/19] Add Gleam integration with Mix - Add Mix.Gleam module - Add specific gleam binary version requirement - Rely on `gleam export package-info` --- lib/mix/lib/mix/dep.ex | 11 +++- lib/mix/lib/mix/dep/converger.ex | 2 +- lib/mix/lib/mix/dep/loader.ex | 29 +++++++-- lib/mix/lib/mix/gleam.ex | 94 +++++++++++++++++++++++++++ lib/mix/lib/mix/task.compiler.ex | 2 +- lib/mix/lib/mix/tasks/deps.compile.ex | 21 +++++- lib/mix/lib/mix/tasks/deps.ex | 4 +- lib/mix/test/mix/gleam_test.exs | 93 ++++++++++++++++++++++++++ 8 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 lib/mix/lib/mix/gleam.ex create mode 100644 lib/mix/test/mix/gleam_test.exs diff --git a/lib/mix/lib/mix/dep.ex b/lib/mix/lib/mix/dep.ex index 3f981351489..80acfb7ce4e 100644 --- a/lib/mix/lib/mix/dep.ex +++ b/lib/mix/lib/mix/dep.ex @@ -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 @@ -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 @@ -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 diff --git a/lib/mix/lib/mix/dep/converger.ex b/lib/mix/lib/mix/dep/converger.ex index 1d036c49822..5ae5afe7fbe 100644 --- a/lib/mix/lib/mix/dep/converger.ex +++ b/lib/mix/lib/mix/dep/converger.ex @@ -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 diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 984fa32e478..490019a8e9e 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -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` @@ -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)} @@ -106,6 +106,9 @@ defmodule Mix.Dep.Loader do make?(dep) -> make_dep(dep) + gleam?(dep) -> + gleam_dep(dep, children, locked?) + true -> {dep, []} end @@ -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) @@ -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 @@ -361,6 +367,21 @@ defmodule Mix.Dep.Loader do {dep, []} end + defp gleam_dep(%Mix.Dep{opts: opts} = dep, children, locked?) do + Mix.Gleam.require!() + + deps = + if children do + Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?)) + else + config = File.cd!(opts[:dest], fn -> Mix.Gleam.load_config(".") end) + from = Path.join(opts[:dest], "gleam.toml") + Enum.map(config[:deps], &to_dep(&1, from, _manager = nil, locked?)) + end + + {%{dep | opts: Keyword.merge(opts, app: false, override: true)}, deps} + end + defp mix_children(config, locked?, opts) do from = Mix.Project.project_file() diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex new file mode 100644 index 00000000000..270ef0ce02b --- /dev/null +++ b/lib/mix/lib/mix/gleam.ex @@ -0,0 +1,94 @@ +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["gleam"]) + 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)} + end + + case spec do + {dep, version, []} -> {dep, version} + spec -> spec + end + end + + defp maybe_gleam_version(config, nil), do: config + + defp maybe_gleam_version(config, version) do + Map.put(config, :gleam, version) + 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 diff --git a/lib/mix/lib/mix/task.compiler.ex b/lib/mix/lib/mix/task.compiler.ex index 730234afdf6..7f399d7a809 100644 --- a/lib/mix/lib/mix/task.compiler.ex +++ b/lib/mix/lib/mix/task.compiler.ex @@ -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 diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 962b7be9f3f..4967f6a7ed0 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -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 export` The compilation can be customized by passing a `compile` option in the dependency: @@ -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)" ) @@ -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) diff --git a/lib/mix/lib/mix/tasks/deps.ex b/lib/mix/lib/mix/tasks/deps.ex index 1a216730f2e..11c0018cdfa 100644 --- a/lib/mix/lib/mix/tasks/deps.ex +++ b/lib/mix/lib/mix/tasks/deps.ex @@ -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 diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs new file mode 100644 index 00000000000..aca6528358f --- /dev/null +++ b/lib/mix/test/mix/gleam_test.exs @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2021 The Elixir Team +# SPDX-FileCopyrightText: 2012 Plataformatec + +Code.require_file("../test_helper.exs", __DIR__) + +defmodule Mix.GleamTest do + use MixTest.Case + + @compile {:no_warn_undefined, [:gleam_dep, :gleam@int]} + + defmodule GleamAsDep do + def project do + [ + app: :gleam_as_dep, + version: "0.1.0", + deps: [ + {:gleam_dep, path: MixTest.Case.tmp_path("gleam_dep"), app: false} + ] + ] + end + end + + describe "load_config/1" do + test "loads gleam.toml" do + path = MixTest.Case.fixture_path("gleam_dep") + config = Mix.Gleam.load_config(path) + + expected = [ + {:gleam_stdlib, ">= 0.44.0 and < 2.0.0"}, + {:gleam_otp, ">= 0.16.1 and < 1.0.0"}, + {:gleeunit, ">= 1.0.0 and < 2.0.0", only: :dev} + ] + + assert Enum.sort(config[:deps]) == Enum.sort(expected) + end + end + + describe "gleam export package-information format" do + test "parse_config" do + config = + %{ + "name" => "gael", + "version" => "1.0.0", + "gleam" => ">= 1.8.0", + "dependencies" => %{ + "gleam_stdlib" => %{"version" => ">= 0.18.0 and < 2.0.0"}, + "my_other_project" => %{"path" => "../my_other_project"} + }, + "dev-dependencies" => %{"gleeunit" => %{"version" => ">= 1.0.0 and < 2.0.0"}} + } + |> Mix.Gleam.parse_config() + + assert config == %{ + name: "gael", + version: "1.0.0", + gleam: ">= 1.8.0", + deps: [ + {:gleam_stdlib, ">= 0.18.0 and < 2.0.0"}, + {:my_other_project, path: "../my_other_project"}, + {:gleeunit, ">= 1.0.0 and < 2.0.0", only: :dev} + ] + } + end + end + + describe "integration with Mix" do + test "gets and compiles dependencies" do + in_tmp("get and compile dependencies", fn -> + Mix.Project.push(GleamAsDep) + + Mix.Tasks.Deps.Get.run([]) + assert_received {:mix_shell, :info, ["* Getting gleam_stdlib " <> _]} + assert_received {:mix_shell, :info, ["* Getting gleam_otp " <> _]} + assert_received {:mix_shell, :info, ["* Getting gleeunit " <> _]} + + Mix.Tasks.Deps.Compile.run([]) + assert :gleam_dep.main() + assert :gleam@int.to_string(1) == "1" + + load_paths = + Mix.Dep.Converger.converge([]) + |> Enum.map(&Mix.Dep.load_paths(&1)) + |> Enum.concat() + + assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_dep/ebin")) + assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_stdlib/ebin")) + # Dep of a dep + assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_erlang/ebin")) + end) + end + end +end From a08ef986777680991c2fe01a1f37366e1f0093d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Thu, 20 Feb 2025 14:09:43 +0100 Subject: [PATCH 03/19] Add support for git dependencies in gleam packages --- lib/mix/lib/mix/gleam.ex | 13 +++++++++++-- lib/mix/test/mix/gleam_test.exs | 2 ++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index 270ef0ce02b..6cf416bf6dc 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -38,8 +38,17 @@ defmodule Mix.Gleam do spec = case requirement do - %{"version" => version} -> {dep, version, opts} - %{"path" => path} -> {dep, Keyword.merge(opts, path: path)} + %{"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 diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index aca6528358f..34548064d3d 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -44,6 +44,7 @@ defmodule Mix.GleamTest do "version" => "1.0.0", "gleam" => ">= 1.8.0", "dependencies" => %{ + "git_dep" => %{"git" => "../git_dep", "ref" => "957b83b"}, "gleam_stdlib" => %{"version" => ">= 0.18.0 and < 2.0.0"}, "my_other_project" => %{"path" => "../my_other_project"} }, @@ -56,6 +57,7 @@ defmodule Mix.GleamTest do version: "1.0.0", gleam: ">= 1.8.0", deps: [ + {:git_dep, git: "../git_dep", ref: "957b83b"}, {:gleam_stdlib, ">= 0.18.0 and < 2.0.0"}, {:my_other_project, path: "../my_other_project"}, {:gleeunit, ">= 1.0.0 and < 2.0.0", only: :dev} From 86266fb436cd5743b501f7fc3f0abbb00bdf0208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Thu, 20 Feb 2025 15:06:24 +0100 Subject: [PATCH 04/19] Exclude gleam tests if gleam is missing --- lib/mix/test/mix/gleam_test.exs | 1 + lib/mix/test/test_helper.exs | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index 34548064d3d..0db1af66f87 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -6,6 +6,7 @@ Code.require_file("../test_helper.exs", __DIR__) defmodule Mix.GleamTest do use MixTest.Case + @moduletag :gleam @compile {:no_warn_undefined, [:gleam_dep, :gleam@int]} diff --git a/lib/mix/test/test_helper.exs b/lib/mix/test/test_helper.exs index 10e7ac64b44..b6559cc6851 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -43,9 +43,18 @@ cover_exclude = [] end +gleam_exclude = + try do + Mix.Gleam.require!() + [] + rescue + Mix.Error -> [gleam: true] + end + ExUnit.start( trace: !!System.get_env("TRACE"), - exclude: epmd_exclude ++ os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude, + exclude: + epmd_exclude ++ os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude ++ gleam_exclude, include: line_include ) From 1ab8e9b97c2ee631caffa8e6f5845ed2c94a0176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Fri, 21 Feb 2025 11:43:17 +0100 Subject: [PATCH 05/19] Fix deps.compile for gleam - shell_cmd! wasn't handling tuples - Fix documentation --- lib/mix/lib/mix/tasks/deps.compile.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 4967f6a7ed0..e6f2a52a7a9 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -22,7 +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 export` + * `gleam.toml` - invokes `gleam compile-package` The compilation can be customized by passing a `compile` option in the dependency: @@ -374,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 From b09c9dcecb5b51a3e73127499e1efdc2fc4dfcc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Mon, 24 Feb 2025 22:46:32 +0100 Subject: [PATCH 06/19] Add support for application_start_module This is an optional value within [erlang] in the gleam.toml file. It will be used for the `mod` value when generating a .app file --- lib/mix/lib/mix/dep/loader.ex | 31 +++++++++++++++++++++---------- lib/mix/lib/mix/gleam.ex | 17 +++++++++++++---- lib/mix/test/mix/gleam_test.exs | 10 ++++++++-- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 490019a8e9e..3ff05a7ed5d 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -367,19 +367,30 @@ defmodule Mix.Dep.Loader do {dep, []} end - defp gleam_dep(%Mix.Dep{opts: opts} = dep, children, locked?) do + defp gleam_dep(%Mix.Dep{opts: opts} = dep, _children = nil, locked?) do Mix.Gleam.require!() - deps = - if children do - Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?)) - else - config = File.cd!(opts[:dest], fn -> Mix.Gleam.load_config(".") end) - from = Path.join(opts[:dest], "gleam.toml") - Enum.map(config[:deps], &to_dep(&1, from, _manager = nil, locked?)) - end + 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])}, + {:mod, {String.to_atom(config[:mod]), []}} + ] - {%{dep | opts: Keyword.merge(opts, app: false, override: true)}, deps} + contents = :io_lib.format("~p.~n", [{:application, dep.app, properties}]) + + [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 = %{dep | opts: Keyword.merge(opts, app: false, override: true)} + {dep, Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?))} end defp mix_children(config, locked?, opts) do diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index 6cf416bf6dc..c357163c07b 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -26,7 +26,8 @@ defmodule Mix.Gleam do version: Map.fetch!(json, "version"), deps: deps ++ dev_deps } - |> maybe_gleam_version(json["gleam"]) + |> maybe_gleam_version(json) + |> maybe_application_start_module(json) rescue KeyError -> Mix.raise("Command \"gleam export package-information\" unexpected format: \n" <> json) @@ -57,10 +58,18 @@ defmodule Mix.Gleam do end end - defp maybe_gleam_version(config, nil), do: config + defp maybe_gleam_version(config, json) do + case json["gleam"] do + nil -> config + version -> Map.put(config, :gleam, version) + end + end - defp maybe_gleam_version(config, version) do - Map.put(config, :gleam, version) + defp maybe_application_start_module(config, json) do + case get_in(json, ["erlang", "application_start_module"]) do + nil -> config + mod -> Map.put(config, :mod, mod) + end end def require!() do diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index 0db1af66f87..9cd80de431b 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -49,7 +49,12 @@ defmodule Mix.GleamTest do "gleam_stdlib" => %{"version" => ">= 0.18.0 and < 2.0.0"}, "my_other_project" => %{"path" => "../my_other_project"} }, - "dev-dependencies" => %{"gleeunit" => %{"version" => ">= 1.0.0 and < 2.0.0"}} + "dev-dependencies" => %{ + "gleeunit" => %{"version" => ">= 1.0.0 and < 2.0.0"} + }, + "erlang" => %{ + "application_start_module" => "some@application" + } } |> Mix.Gleam.parse_config() @@ -62,7 +67,8 @@ defmodule Mix.GleamTest do {:gleam_stdlib, ">= 0.18.0 and < 2.0.0"}, {:my_other_project, path: "../my_other_project"}, {:gleeunit, ">= 1.0.0 and < 2.0.0", only: :dev} - ] + ], + mod: "some@application" } end end From 360db239a3e3c9c61d78a9d4d28642d94e2b5908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Tue, 25 Feb 2025 12:23:25 +0100 Subject: [PATCH 07/19] Handle gleam extra_applications --- lib/mix/lib/mix/dep/loader.ex | 30 +++++++++++++++++++--- lib/mix/lib/mix/gleam.ex | 14 +++++++--- lib/mix/test/fixtures/gleam_dep/gleam.toml | 4 +++ lib/mix/test/mix/gleam_test.exs | 6 +++++ 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 3ff05a7ed5d..1224ee74527 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -374,13 +374,17 @@ defmodule Mix.Dep.Loader do 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])}, - {:mod, {String.to_atom(config[:mod]), []}} - ] + 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)) @@ -393,6 +397,24 @@ defmodule Mix.Dep.Loader 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() diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index c357163c07b..a5a5e81b0c7 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -27,7 +27,7 @@ defmodule Mix.Gleam do deps: deps ++ dev_deps } |> maybe_gleam_version(json) - |> maybe_application_start_module(json) + |> maybe_erlang_opts(json) rescue KeyError -> Mix.raise("Command \"gleam export package-information\" unexpected format: \n" <> json) @@ -65,10 +65,16 @@ defmodule Mix.Gleam do end end - defp maybe_application_start_module(config, json) do - case get_in(json, ["erlang", "application_start_module"]) do + 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 - mod -> Map.put(config, :mod, mod) + extra_applications -> Map.put(config, :extra_applications, extra_applications) end end diff --git a/lib/mix/test/fixtures/gleam_dep/gleam.toml b/lib/mix/test/fixtures/gleam_dep/gleam.toml index fc88f8e0f47..0a250087907 100644 --- a/lib/mix/test/fixtures/gleam_dep/gleam.toml +++ b/lib/mix/test/fixtures/gleam_dep/gleam.toml @@ -18,3 +18,7 @@ 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" diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index 9cd80de431b..c21d12042af 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -96,6 +96,12 @@ defmodule Mix.GleamTest do assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_stdlib/ebin")) # Dep of a dep assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_erlang/ebin")) + {:ok, content} = :file.consult("_build/dev/lib/gleam_dep/ebin/gleam_dep.app") + + assert content == [ + {:application, :gleam_dep, + [applications: [:ssl], mod: {:gleam_dep@somemodule, []}, vsn: ~c"1.0.0"]} + ] end) end end From f0c0a29badd754efe37ef88ebe45f9f50e7d8bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Wed, 26 Mar 2025 10:10:18 +0100 Subject: [PATCH 08/19] Remove redundant quotes --- lib/mix/lib/mix/tasks/deps.compile.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index e6f2a52a7a9..25b63b49d0a 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -374,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)}, \"#{inspect(command)}\" command failed. " <> + "Could not compile dependency #{inspect(app)}, #{inspect(command)} command failed. " <> deps_compile_feedback(app) ) end From 70861671270bf3a999cd506041adb87a2f69b87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Mon, 31 Mar 2025 11:34:46 +0200 Subject: [PATCH 09/19] Do not force `app: false` in gleam deps --- lib/mix/lib/mix/dep/loader.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 1224ee74527..fea064c7efc 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -393,7 +393,6 @@ defmodule Mix.Dep.Loader do end defp gleam_dep(%Mix.Dep{opts: opts} = dep, children, locked?) do - dep = %{dep | opts: Keyword.merge(opts, app: false, override: true)} {dep, Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?))} end From b64f23a35caa321cc942815d0e47298449a72404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Wed, 2 Apr 2025 16:35:28 +0200 Subject: [PATCH 10/19] Generate app file for gleam deps on compilation --- lib/mix/lib/mix/dep/loader.ex | 33 ------------------ lib/mix/lib/mix/gleam.ex | 2 +- lib/mix/lib/mix/tasks/deps.compile.ex | 50 ++++++++++++++++++++++++++- lib/mix/test/mix/gleam_test.exs | 24 +++++++------ 4 files changed, 63 insertions(+), 46 deletions(-) diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index fea064c7efc..2bac631f93c 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -374,21 +374,6 @@ defmodule Mix.Dep.Loader do 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 @@ -396,24 +381,6 @@ defmodule Mix.Dep.Loader 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() diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index a5a5e81b0c7..6c76ac46631 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -43,7 +43,7 @@ defmodule Mix.Gleam do {dep, version, opts} %{"path" => path} -> - {dep, Keyword.merge(opts, path: path)} + {dep, Keyword.merge(opts, path: Path.expand(path))} %{"git" => git, "ref" => ref} -> {dep, git: git, ref: ref} diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 25b63b49d0a..2a15810d0c0 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -336,7 +336,55 @@ defmodule Mix.Tasks.Deps.Compile do ["compile-package", "--target", "erlang", "--package", package, "--out", out, "--lib", lib]} shell_cmd!(dep, config, command) - Code.prepend_path(Path.join(out, "ebin"), cache: true) + + ebin = Path.join(out, "ebin") + app_file_path = Keyword.get(opts, :app, Path.join(ebin, "#{dep.app}.app")) + create_app_file = app_file_path && !File.exists?(app_file_path) + + if create_app_file do + generate_gleam_app_file(opts) + end + + Code.prepend_path(ebin, cache: true) + end + + defp gleam_extra_applications(config) do + config + |> Map.get(:extra_applications, []) + |> Enum.map(&String.to_atom/1) + end + + defp gleam_mod(config) do + case config[:mod] do + nil -> [] + mod -> {String.to_atom(mod), []} + end + end + + defp generate_gleam_app_file(opts) do + toml = File.cd!(opts[:dest], fn -> Mix.Gleam.load_config(".") end) + + module = + quote do + def project do + [ + app: unquote(toml.name) |> String.to_atom(), + version: "#{unquote(toml.version)}" + ] + end + + def application do + [ + mod: unquote(gleam_mod(toml)), + extra_applications: unquote(gleam_extra_applications(toml)) + ] + end + end + + module_name = String.to_atom("Gleam.#{toml.name}") + Module.create(module_name, module, Macro.Env.location(__ENV__)) + Mix.Project.push(module_name) + Mix.Tasks.Compile.App.run([]) end defp make_command(dep) do diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index c21d12042af..c9de5f0e7cd 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -87,20 +87,22 @@ defmodule Mix.GleamTest do assert :gleam_dep.main() assert :gleam@int.to_string(1) == "1" - load_paths = - Mix.Dep.Converger.converge([]) - |> Enum.map(&Mix.Dep.load_paths(&1)) - |> Enum.concat() - - assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_dep/ebin")) - assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_stdlib/ebin")) - # Dep of a dep - assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_erlang/ebin")) {:ok, content} = :file.consult("_build/dev/lib/gleam_dep/ebin/gleam_dep.app") assert content == [ - {:application, :gleam_dep, - [applications: [:ssl], mod: {:gleam_dep@somemodule, []}, vsn: ~c"1.0.0"]} + { + :application, + :gleam_dep, + [ + {:modules, [:gleam_dep]}, + {:optional_applications, []}, + {:applications, [:kernel, :stdlib, :elixir, :ssl]}, + {:description, ~c"gleam_dep"}, + {:registered, []}, + {:vsn, ~c"1.0.0"}, + {:mod, {:gleam_dep@somemodule, []}} + ] + } ] end) end From fe685f2ef11fa5579c8ae6c7162c78182b9930bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Wed, 16 Apr 2025 12:58:54 +0200 Subject: [PATCH 11/19] Add license and copyright Co-authored-by: Eksperimental --- lib/mix/lib/mix/gleam.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index 6c76ac46631..ad2ccbd6174 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2021 The Elixir Team +# SPDX-FileCopyrightText: 2012 Plataformatec + defmodule Mix.Gleam do # Version that introduced `gleam export package-information` command @required_gleam_version ">= 1.10.0" From b62bc647130b50a3c161c67408f2415d474c606d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Wed, 16 Apr 2025 12:59:44 +0200 Subject: [PATCH 12/19] Remove unneded try() Co-authored-by: Eksperimental --- lib/mix/lib/mix/gleam.ex | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index ad2ccbd6174..4c6ec17902b 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -16,26 +16,24 @@ defmodule Mix.Gleam do 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 + 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 defp parse_dep({dep, requirement}, opts \\ []) do From 4c57c3ad2c0d2a6c062a0aaea6acf0402534991b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Wed, 16 Apr 2025 13:00:02 +0200 Subject: [PATCH 13/19] Remove unneded try() Co-authored-by: Eksperimental --- lib/mix/lib/mix/gleam.ex | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index 4c6ec17902b..37d1f844210 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -86,15 +86,13 @@ defmodule Mix.Gleam do 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}") + 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 From fd7f25b6ed7cc0fdef5a3cfa93db568fb8e06cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Wed, 16 Apr 2025 13:00:33 +0200 Subject: [PATCH 14/19] Remove unneded try() Co-authored-by: Eksperimental --- lib/mix/lib/mix/gleam.ex | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index 37d1f844210..9ae3fb27807 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -96,23 +96,21 @@ defmodule Mix.Gleam do 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 + 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 From 220a0d72cf31dc29a6b2414b3fff0178e983b041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Wed, 16 Apr 2025 13:01:32 +0200 Subject: [PATCH 15/19] Update lib/mix/test/fixtures/gleam_dep/.gitignore Co-authored-by: Eksperimental --- lib/mix/test/fixtures/gleam_dep/.gitignore | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/mix/test/fixtures/gleam_dep/.gitignore b/lib/mix/test/fixtures/gleam_dep/.gitignore index 599be4eb929..eefc9c554fb 100644 --- a/lib/mix/test/fixtures/gleam_dep/.gitignore +++ b/lib/mix/test/fixtures/gleam_dep/.gitignore @@ -1,4 +1,11 @@ +# The directory Mix 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 -/build -erl_crash.dump From ec0320c73cf09563b386557a9b8401da4cad24a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Thu, 17 Apr 2025 13:22:57 +0200 Subject: [PATCH 16/19] Fix documentation Co-authored-by: Eksperimental --- lib/mix/lib/mix/tasks/deps.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/deps.ex b/lib/mix/lib/mix/tasks/deps.ex index 11c0018cdfa..a23867a6025 100644 --- a/lib/mix/lib/mix/tasks/deps.ex +++ b/lib/mix/lib/mix/tasks/deps.ex @@ -101,7 +101,7 @@ 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, makefile and gleam 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`, `:make` or `:gleam`. In case From 583b504e770d0a12f839a07e6dd893e79e975e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Fri, 18 Apr 2025 22:10:13 +0200 Subject: [PATCH 17/19] Proper beam compilation and .app file generation --- lib/mix/lib/mix/dep/loader.ex | 10 +-- lib/mix/lib/mix/tasks/deps.compile.ex | 86 +++++++++---------- .../gleam_dep/src/collocated_erlang.erl | 5 ++ .../fixtures/gleam_dep/src/gleam_dep.gleam | 3 + lib/mix/test/mix/gleam_test.exs | 16 ++-- 5 files changed, 63 insertions(+), 57 deletions(-) create mode 100644 lib/mix/test/fixtures/gleam_dep/src/collocated_erlang.erl diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 2bac631f93c..63187a3d3a6 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -107,7 +107,7 @@ defmodule Mix.Dep.Loader do make_dep(dep) gleam?(dep) -> - gleam_dep(dep, children, locked?) + gleam_dep(dep, children, manager, locked?) true -> {dep, []} @@ -367,18 +367,18 @@ defmodule Mix.Dep.Loader do {dep, []} end - defp gleam_dep(%Mix.Dep{opts: opts} = dep, _children = nil, locked?) do + defp gleam_dep(%Mix.Dep{opts: opts} = dep, _children = nil, manager, 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?)) + deps = Enum.map(config[:deps], &to_dep(&1, from, manager, locked?)) {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?))} + 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 diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 2a15810d0c0..57610205439 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -326,6 +326,7 @@ defmodule Mix.Tasks.Deps.Compile do defp do_gleam(%Mix.Dep{opts: opts} = dep, config) do Mix.Gleam.require!() + Mix.Project.ensure_structure() lib = Path.join(Mix.Project.build_path(), "lib") out = opts[:build] @@ -333,58 +334,51 @@ defmodule Mix.Tasks.Deps.Compile do command = {"gleam", - ["compile-package", "--target", "erlang", "--package", package, "--out", out, "--lib", lib]} + [ + "compile-package", + "--no-beam", + "--target", + "erlang", + "--package", + package, + "--out", + out, + "--lib", + lib + ]} shell_cmd!(dep, config, command) - ebin = Path.join(out, "ebin") - app_file_path = Keyword.get(opts, :app, Path.join(ebin, "#{dep.app}.app")) - create_app_file = app_file_path && !File.exists?(app_file_path) + File.cd!(package, fn -> Mix.Gleam.load_config(".") end) + |> push_gleam_project(dep, Keyword.fetch!(config, :deps_path)) - if create_app_file do - generate_gleam_app_file(opts) - end - - Code.prepend_path(ebin, cache: true) + Code.prepend_path(Path.join(out, "ebin"), cache: true) end - defp gleam_extra_applications(config) do - config - |> Map.get(:extra_applications, []) - |> Enum.map(&String.to_atom/1) - end - - defp gleam_mod(config) do - case config[:mod] do - nil -> [] - mod -> {String.to_atom(mod), []} - end - end - - defp generate_gleam_app_file(opts) do - toml = File.cd!(opts[:dest], fn -> Mix.Gleam.load_config(".") end) - - module = - quote do - def project do - [ - app: unquote(toml.name) |> String.to_atom(), - version: "#{unquote(toml.version)}" - ] - end - - def application do - [ - mod: unquote(gleam_mod(toml)), - extra_applications: unquote(gleam_extra_applications(toml)) - ] - end - end - - module_name = String.to_atom("Gleam.#{toml.name}") - Module.create(module_name, module, Macro.Env.location(__ENV__)) - Mix.Project.push(module_name) - Mix.Tasks.Compile.App.run([]) + defp push_gleam_project(toml, dep, deps_path) do + build = Path.expand(dep.opts[:build]) + src = Path.join(build, "_gleam_artefacts") + File.mkdir(Path.join(build, "ebin")) + + config = + [ + app: dep.app, + version: toml.version, + deps: toml.deps, + build_per_environment: true, + lockfile: "mix.lock", + # Remove per-environment segment from the path since ProjectStack.push below will append it + build_path: Mix.Project.build_path() |> Path.split() |> Enum.drop(-1) |> Path.join(), + deps_path: deps_path, + erlc_paths: [src], + erlc_include_path: Path.join(build, "include") + ] + + Mix.ProjectStack.pop() + Mix.ProjectStack.push(dep.app, config, "nofile") + # Somehow running just `compile` task won't work (doesn't compile the .erl files) + Mix.Task.run("compile.erlang", ["--force"]) + Mix.Task.run("compile.app") end defp make_command(dep) do diff --git a/lib/mix/test/fixtures/gleam_dep/src/collocated_erlang.erl b/lib/mix/test/fixtures/gleam_dep/src/collocated_erlang.erl new file mode 100644 index 00000000000..ea2ed915e71 --- /dev/null +++ b/lib/mix/test/fixtures/gleam_dep/src/collocated_erlang.erl @@ -0,0 +1,5 @@ +-module(collocated_erlang). +-export([hello/0]). + +hello() -> + "Hello from Collocated Erlang!". diff --git a/lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam b/lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam index 673bfdd0147..4f11d986b22 100644 --- a/lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam +++ b/lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam @@ -1,3 +1,6 @@ pub fn main() { True } + +@external(erlang, "collocated_erlang", "hello") +pub fn erl() -> String diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index c9de5f0e7cd..537e287c684 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -68,7 +68,9 @@ defmodule Mix.GleamTest do {:my_other_project, path: "../my_other_project"}, {:gleeunit, ">= 1.0.0 and < 2.0.0", only: :dev} ], - mod: "some@application" + application: [ + mod: {:some@application, []} + ] } end end @@ -81,10 +83,10 @@ defmodule Mix.GleamTest do Mix.Tasks.Deps.Get.run([]) assert_received {:mix_shell, :info, ["* Getting gleam_stdlib " <> _]} assert_received {:mix_shell, :info, ["* Getting gleam_otp " <> _]} - assert_received {:mix_shell, :info, ["* Getting gleeunit " <> _]} Mix.Tasks.Deps.Compile.run([]) assert :gleam_dep.main() + assert :gleam_dep.erl() == ~c'Hello from Collocated Erlang!' assert :gleam@int.to_string(1) == "1" {:ok, content} = :file.consult("_build/dev/lib/gleam_dep/ebin/gleam_dep.app") @@ -94,13 +96,15 @@ defmodule Mix.GleamTest do :application, :gleam_dep, [ - {:modules, [:gleam_dep]}, + {:modules, [:collocated_erlang, :gleam_dep]}, {:optional_applications, []}, - {:applications, [:kernel, :stdlib, :elixir, :ssl]}, + {:applications, + [:kernel, :stdlib, :elixir, :gleam_otp, :gleam_stdlib, :gleeunit]}, {:description, ~c"gleam_dep"}, {:registered, []}, - {:vsn, ~c"1.0.0"}, - {:mod, {:gleam_dep@somemodule, []}} + {:vsn, ~c"1.0.0"} + # Need to add support for :application option in Compile.App + # {:mod, {:gleam_dep@somemodule, []}} ] } ] From bb525641a141d3d5208b6be1e7f3fe2b8193eb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Tue, 22 Apr 2025 09:59:32 +0200 Subject: [PATCH 18/19] Use ~w sigil for command Co-authored-by: Eksperimental --- lib/mix/lib/mix/tasks/deps.compile.ex | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 57610205439..753333c528d 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -334,18 +334,7 @@ defmodule Mix.Tasks.Deps.Compile do command = {"gleam", - [ - "compile-package", - "--no-beam", - "--target", - "erlang", - "--package", - package, - "--out", - out, - "--lib", - lib - ]} + ~w(compile-package --no-beam --target erlang --package #{package} --out #{out} --lib #{lib})} shell_cmd!(dep, config, command) From 3051b90545f2a835233f8e195da6d056f1f6f806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Tue, 22 Apr 2025 13:01:48 +0200 Subject: [PATCH 19/19] Extract var Co-authored-by: Eksperimental --- lib/mix/test/mix/gleam_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index 537e287c684..6c855b8c350 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -37,7 +37,7 @@ defmodule Mix.GleamTest do end end - describe "gleam export package-information format" do + describe "Gleam export package-information format" do test "parse_config" do config = %{