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/10] 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 0000000000..599be4eb92 --- /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 0000000000..fc88f8e0f4 --- /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 0000000000..f7e3f2b653 --- /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 0000000000..673bfdd014 --- /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 05f21bc2df..10e7ac64b4 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/10] 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 3f98135148..80acfb7ce4 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 1d036c4982..5ae5afe7fb 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 984fa32e47..490019a8e9 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 0000000000..270ef0ce02 --- /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 730234afdf..7f399d7a80 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 962b7be9f3..4967f6a7ed 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 1a216730f2..11c0018cdf 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 0000000000..aca6528358 --- /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/10] 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 270ef0ce02..6cf416bf6d 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 aca6528358..34548064d3 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/10] 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 34548064d3..0db1af66f8 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 10e7ac64b4..b6559cc685 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/10] 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 4967f6a7ed..e6f2a52a7a 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/10] 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 490019a8e9..3ff05a7ed5 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 6cf416bf6d..c357163c07 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 0db1af66f8..9cd80de431 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/10] 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 3ff05a7ed5..1224ee7452 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 c357163c07..a5a5e81b0c 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 fc88f8e0f4..0a25008790 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 9cd80de431..c21d12042a 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/10] 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 e6f2a52a7a..25b63b49d0 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/10] 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 1224ee7452..fea064c7ef 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/10] 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 fea064c7ef..2bac631f93 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 a5a5e81b0c..6c76ac4663 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 25b63b49d0..2a15810d0c 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 c21d12042a..c9de5f0e7c 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