From c5bd684f2cc48f44d84d5d2bbb1a1acbf88ef57b 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/21] 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 671e4e0f5b1..16797f94798 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -290,6 +290,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 3fcf8977c752322e9583d964d506d0549db0e701 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/21] 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 96acd4e4fcb..a63bd835bed 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 @@ -555,6 +555,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 290579a23ea..0014cbbb164 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 fd3154af684..81f7a48ce0f 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 33f630f8479..02c05dfb50b 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: @@ -143,9 +144,12 @@ defmodule Mix.Tasks.Deps.Compile do dep.manager == :rebar3 -> do_rebar3(dep, config) + dep.manager == :gleam -> + do_gleam(dep, config) + true -> Mix.shell().error( - "Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\" or \"Makefile\" " <> + "Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\", \"Makefile\" or \"gleam.toml\" " <> "(pass :compile as an option to customize compilation, set it to \"false\" to do nothing)" ) @@ -302,6 +306,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 f17cd078cc2..0f60c74d277 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 85ba7b2e27ca681de97a2934e3117fca659f53ea 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/21] 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 96a1c0b8ac5c755b2c615962c2f75d9586ceeeb1 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/21] 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 16797f94798..c07ea6c532e 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -51,6 +51,14 @@ re_import_exclude = [:re_import] end +gleam_exclude = + try do + Mix.Gleam.require!() + [] + rescue + Mix.Error -> [gleam: true] + end + Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__) CoverageRecorder.maybe_record("mix") @@ -58,7 +66,8 @@ ExUnit.start( trace: !!System.get_env("TRACE"), exclude: epmd_exclude ++ - os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude ++ re_import_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")) ) From 7e21ba0cbf5bb9b3efa9602fcbfa6f9a57b6db9a 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/21] 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 02c05dfb50b..d37e8a4615a 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: @@ -356,7 +356,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 e3c18a57983cc4e2f8b9bd181e386f1c52d27a4b 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/21] 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 0014cbbb164..7ac2bf08b06 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 3ca3c6f0fc74f768e79f841c6a2110cb1f70709a 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/21] 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 7ac2bf08b06..3e3b3f4816a 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 b142e946255d846ccea48c2d4ce9c96ff48555bd 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/21] 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 d37e8a4615a..e0902487f8e 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -356,7 +356,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 ec9e5970e4eb0d0be78e8490fd8818c6bb04510a 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/21] 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 3e3b3f4816a..2578139f70d 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 08b2a54f20aa5695b908ebd3ac8e77929d2332a6 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/21] 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 2578139f70d..a6c31b43090 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 e0902487f8e..ccd8427fb5b 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -318,7 +318,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 26203afd17d93df07a2508064aef3cf1e0e1d497 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 11/21] 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 a6c31b43090..cf4dd12b593 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 ccd8427fb5b..8825c16ecda 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -308,6 +308,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] @@ -315,58 +316,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 5ff6bc96d6e776702e5d0896f95c42671d68c397 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 12/21] Apply suggestions from code review Co-authored-by: Eksperimental --- lib/mix/lib/mix/dep/loader.ex | 8 +- lib/mix/lib/mix/gleam.ex | 92 +++++++++++----------- lib/mix/lib/mix/tasks/deps.compile.ex | 13 +-- lib/mix/lib/mix/tasks/deps.ex | 2 +- lib/mix/test/fixtures/gleam_dep/.gitignore | 11 ++- lib/mix/test/mix/gleam_test.exs | 2 +- 6 files changed, 61 insertions(+), 67 deletions(-) diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index cf4dd12b593..d37753b7ee2 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -84,7 +84,7 @@ 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/gleam 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, gleam.toml) manager = opts[:manager] || scm_manager(scm, opts) || manager || infer_manager(opts[:dest]) @@ -369,9 +369,9 @@ defmodule Mix.Dep.Loader 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") + dest = opts[:dest] + config = File.cd!(dest, fn -> Mix.Gleam.load_config(".") end) + from = Path.join(dest, "gleam.toml") deps = Enum.map(config[:deps], &to_dep(&1, from, manager, locked?)) {dep, deps} diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index 6c76ac46631..2d6125cb719 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -1,10 +1,14 @@ +# 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" def load_config(dir) do File.cd!(dir, fn -> - gleam!(["export", "package-information", "--out", "/dev/stdout"]) + gleam!(~W(export package-information --out /dev/stdout)) |> JSON.decode!() |> Map.fetch!("gleam.toml") |> parse_config() @@ -12,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 @@ -84,35 +86,31 @@ 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 - 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 diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 8825c16ecda..ee108d0f62a 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -316,18 +316,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) diff --git a/lib/mix/lib/mix/tasks/deps.ex b/lib/mix/lib/mix/tasks/deps.ex index 0f60c74d277..4972a3b91db 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 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 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 = %{ From d7f4558686f49bf82cb05aeabb7887e05ab64963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Wed, 23 Apr 2025 16:59:29 +0200 Subject: [PATCH 13/21] Add support for :application option in Compile.App --- lib/mix/lib/mix/gleam.ex | 31 +++++++++++++-------- lib/mix/lib/mix/tasks/compile.app.ex | 12 ++++++-- lib/mix/lib/mix/tasks/deps.compile.ex | 13 ++++++++- lib/mix/test/mix/gleam_test.exs | 8 +++--- lib/mix/test/mix/tasks/compile.app_test.exs | 24 ++++++++++++++++ 5 files changed, 69 insertions(+), 19 deletions(-) diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index 2d6125cb719..6468acff166 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -30,7 +30,7 @@ defmodule Mix.Gleam do deps: deps ++ dev_deps } |> maybe_gleam_version(json) - |> maybe_erlang_opts(json) + |> maybe_erlang_opts(json["erlang"]) rescue KeyError -> Mix.raise("Command \"gleam export package-information\" unexpected format: \n" <> json) @@ -45,7 +45,7 @@ defmodule Mix.Gleam do {dep, version, opts} %{"path" => path} -> - {dep, Keyword.merge(opts, path: Path.expand(path))} + {dep, Keyword.merge(opts, path: path)} %{"git" => git, "ref" => ref} -> {dep, git: git, ref: ref} @@ -67,17 +67,24 @@ defmodule Mix.Gleam do end end - 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 + defp maybe_erlang_opts(config, nil), do: config - case get_in(json, ["erlang", "extra_applications"]) do - nil -> config - extra_applications -> Map.put(config, :extra_applications, extra_applications) - end + defp maybe_erlang_opts(config, opts) do + application = + opts + |> Enum.filter(fn {_, value} -> value != nil end) + |> Enum.map(fn + {"application_start_module", module} when is_binary(module) -> + {:mod, {String.to_atom(module), []}} + + {"extra_applications", applications} when is_list(applications) -> + {:extra_applications, Enum.map(applications, &String.to_atom/1)} + + {key, value} -> + IO.warn("Gleam [erlang] option not supported\n #{key}: #{inspect(value)}") + end) + + Map.put(config, :application, application) end def require!() do diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 003b1a82edc..4bedc5f0b74 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -182,7 +182,7 @@ defmodule Mix.Tasks.Compile.App do registered: [], vsn: to_charlist(version) ] - |> merge_project_application(project) + |> merge_project_application(project, config[:application]) |> handle_extra_applications(config) |> add_compile_env(current_properties) |> add_modules(modules, compile_path) @@ -263,7 +263,7 @@ defmodule Mix.Tasks.Compile.App do end end - defp merge_project_application(best_guess, project) do + defp merge_project_application(best_guess, project, _application = nil) do if function_exported?(project, :application, 0) do project_application = project.application() @@ -279,6 +279,14 @@ defmodule Mix.Tasks.Compile.App do end end + defp merge_project_application(best_guess, _project, application) do + if not Keyword.keyword?(application) do + Mix.raise("Application configuration passed as :application should be a keyword list") + end + + Keyword.merge(best_guess, validate_properties!(application)) + end + defp validate_properties!(properties) do Enum.each(properties, fn {:description, value} -> diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index ee108d0f62a..8825c16ecda 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -316,7 +316,18 @@ defmodule Mix.Tasks.Deps.Compile do command = {"gleam", - ~w(compile-package --no-beam --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) diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index 6c855b8c350..a673fc76b20 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -53,7 +53,8 @@ defmodule Mix.GleamTest do "gleeunit" => %{"version" => ">= 1.0.0 and < 2.0.0"} }, "erlang" => %{ - "application_start_module" => "some@application" + "application_start_module" => "some@application", + "extra_applications" => ["some_app"] } } |> Mix.Gleam.parse_config() @@ -69,7 +70,8 @@ defmodule Mix.GleamTest do {:gleeunit, ">= 1.0.0 and < 2.0.0", only: :dev} ], application: [ - mod: {:some@application, []} + mod: {:some@application, []}, + extra_applications: [:some_app] ] } end @@ -103,8 +105,6 @@ defmodule Mix.GleamTest do {:description, ~c"gleam_dep"}, {:registered, []}, {:vsn, ~c"1.0.0"} - # Need to add support for :application option in Compile.App - # {:mod, {:gleam_dep@somemodule, []}} ] } ] diff --git a/lib/mix/test/mix/tasks/compile.app_test.exs b/lib/mix/test/mix/tasks/compile.app_test.exs index cf488538f40..18e5ec9d1f9 100644 --- a/lib/mix/test/mix/tasks/compile.app_test.exs +++ b/lib/mix/test/mix/tasks/compile.app_test.exs @@ -310,6 +310,30 @@ defmodule Mix.Tasks.Compile.AppTest do end) end + test "dynamic project" do + in_fixture("no_mixfile", fn -> + config = + Mix.Project.config() + |> Keyword.merge( + app: :dynamic_project, + version: "0.1.0", + application: [ + mod: {DynamicProject, []}, + applications: [:example_app, mix: :optional], + extra_applications: [:logger] + ] + ) + + Mix.ProjectStack.push(DynamicProject, config, "nofile") + Mix.Tasks.Compile.Elixir.run([]) + Mix.Tasks.Compile.App.run([]) + + properties = parse_resource_file(:dynamic_project) + assert properties[:mod] == {DynamicProject, []} + assert properties[:applications] == [:kernel, :stdlib, :elixir, :logger, :example_app, :mix] + end) + end + defp parse_resource_file(app) do {:ok, [term]} = :file.consult("_build/dev/lib/#{app}/ebin/#{app}.app") {:application, ^app, properties} = term From 449c45e791e76864ab4a22b887716e422a9755c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Tue, 29 Apr 2025 19:08:32 +0200 Subject: [PATCH 14/21] Proper handling of deeply nested and dev gleam deps Co-authored-by: selenil --- lib/mix/lib/mix/gleam.ex | 4 +-- lib/mix/lib/mix/tasks/deps.compile.ex | 31 ++++++++++++++----- lib/mix/test/fixtures/gleam_dep/.gitignore | 3 ++ lib/mix/test/fixtures/gleam_dep/gleam.toml | 1 - .../subfolder/deeper_gleam_dep/.gitignore | 14 +++++++++ .../subfolder/deeper_gleam_dep/gleam.toml | 17 ++++++++++ .../subfolder/deeper_gleam_dep/manifest.toml | 13 ++++++++ .../src/deeper_gleam_dep.gleam | 5 +++ lib/mix/test/mix/gleam_test.exs | 19 ++++++------ lib/mix/test/test_helper.exs | 12 ++++--- 10 files changed, 95 insertions(+), 24 deletions(-) create mode 100644 lib/mix/test/fixtures/subfolder/deeper_gleam_dep/.gitignore create mode 100644 lib/mix/test/fixtures/subfolder/deeper_gleam_dep/gleam.toml create mode 100644 lib/mix/test/fixtures/subfolder/deeper_gleam_dep/manifest.toml create mode 100644 lib/mix/test/fixtures/subfolder/deeper_gleam_dep/src/deeper_gleam_dep.gleam diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index 6468acff166..fac5758eeef 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -22,7 +22,7 @@ defmodule Mix.Gleam do dev_deps = Map.get(json, "dev-dependencies", %{}) - |> Enum.map(&parse_dep(&1, only: :dev)) + |> Enum.map(&parse_dep(&1, only: [:dev, :test])) %{ name: Map.fetch!(json, "name"), @@ -45,7 +45,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 8825c16ecda..8f7581ce708 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -343,7 +343,8 @@ defmodule Mix.Tasks.Deps.Compile do File.mkdir(Path.join(build, "ebin")) config = - [ + Mix.Project.deps_config() + |> Keyword.merge( app: dep.app, version: toml.version, deps: toml.deps, @@ -351,16 +352,32 @@ defmodule Mix.Tasks.Deps.Compile do 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(), + build_scm: dep.scm, deps_path: deps_path, + deps_app_path: build, erlc_paths: [src], + elixirc_paths: [src], erlc_include_path: Path.join(build, "include") - ] + ) + + env = dep.opts[:env] || :prod + old_env = Mix.env() + + try do + Mix.env(env) + Mix.ProjectStack.push(dep.app, config, "nofile") - 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") + options = ["--from-mix-deps-compile", "--no-warnings-as-errors", "--no-code-path-pruning"] + + # Somehow running just `compile` task won't work (doesn't compile the .erl files) + Mix.Task.run("compile.erlang", options) + Mix.Task.run("compile.elixir", options) + Mix.Task.run("compile.app", options) + + Mix.ProjectStack.pop() + after + Mix.env(old_env) + end end defp make_command(dep) do diff --git a/lib/mix/test/fixtures/gleam_dep/.gitignore b/lib/mix/test/fixtures/gleam_dep/.gitignore index eefc9c554fb..6d3cac8a794 100644 --- a/lib/mix/test/fixtures/gleam_dep/.gitignore +++ b/lib/mix/test/fixtures/gleam_dep/.gitignore @@ -1,6 +1,9 @@ # The directory Mix will write compiled artifacts to. /_build/ +# The directory gleam will write compiled artifacts to. +/build/ + # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump diff --git a/lib/mix/test/fixtures/gleam_dep/gleam.toml b/lib/mix/test/fixtures/gleam_dep/gleam.toml index 0a250087907..575c6b2ae11 100644 --- a/lib/mix/test/fixtures/gleam_dep/gleam.toml +++ b/lib/mix/test/fixtures/gleam_dep/gleam.toml @@ -21,4 +21,3 @@ gleeunit = ">= 1.0.0 and < 2.0.0" [erlang] extra_applications = ["ssl"] -application_start_module = "gleam_dep@somemodule" diff --git a/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/.gitignore b/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/.gitignore new file mode 100644 index 00000000000..6d3cac8a794 --- /dev/null +++ b/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/.gitignore @@ -0,0 +1,14 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# The directory gleam will write compiled artifacts to. +/build/ + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# BEAM bytecode files. +*.beam + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez diff --git a/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/gleam.toml b/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/gleam.toml new file mode 100644 index 00000000000..25adef22085 --- /dev/null +++ b/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/gleam.toml @@ -0,0 +1,17 @@ +name = "deeper_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_dep = { path = "../../gleam_dep" } +gleam_stdlib = ">= 0.59.0 and < 1.0.0" diff --git a/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/manifest.toml b/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/manifest.toml new file mode 100644 index 00000000000..32e9ea192ae --- /dev/null +++ b/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/manifest.toml @@ -0,0 +1,13 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_dep", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_otp", "gleam_stdlib"], source = "local", path = "../../gleam_dep" }, + { 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.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" }, +] + +[requirements] +gleam_dep = { path = "../../gleam_dep" } +gleam_stdlib = { version = ">= 0.59.0 and < 1.0.0" } diff --git a/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/src/deeper_gleam_dep.gleam b/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/src/deeper_gleam_dep.gleam new file mode 100644 index 00000000000..4083be191c9 --- /dev/null +++ b/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/src/deeper_gleam_dep.gleam @@ -0,0 +1,5 @@ +import gleam_dep + +pub fn main() -> Bool { + gleam_dep.main() +} diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index a673fc76b20..791066fe329 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -8,7 +8,7 @@ defmodule Mix.GleamTest do use MixTest.Case @moduletag :gleam - @compile {:no_warn_undefined, [:gleam_dep, :gleam@int]} + @compile {:no_warn_undefined, [:gleam_dep, :gleam@int, :deeper_gleam_dep]} defmodule GleamAsDep do def project do @@ -16,7 +16,7 @@ defmodule Mix.GleamTest do app: :gleam_as_dep, version: "0.1.0", deps: [ - {:gleam_dep, path: MixTest.Case.tmp_path("gleam_dep"), app: false} + {:deeper_gleam_dep, path: MixTest.Case.tmp_path("subfolder/deeper_gleam_dep")} ] ] end @@ -30,7 +30,7 @@ defmodule Mix.GleamTest do 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} + {:gleeunit, ">= 1.0.0 and < 2.0.0", only: [:dev, :test]} ] assert Enum.sort(config[:deps]) == Enum.sort(expected) @@ -46,8 +46,7 @@ defmodule Mix.GleamTest do "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"} + "gleam_stdlib" => %{"version" => ">= 0.18.0 and < 2.0.0"} }, "dev-dependencies" => %{ "gleeunit" => %{"version" => ">= 1.0.0 and < 2.0.0"} @@ -66,8 +65,7 @@ defmodule Mix.GleamTest do 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} + {:gleeunit, ">= 1.0.0 and < 2.0.0", only: [:dev, :test]} ], application: [ mod: {:some@application, []}, @@ -90,6 +88,7 @@ defmodule Mix.GleamTest do assert :gleam_dep.main() assert :gleam_dep.erl() == ~c'Hello from Collocated Erlang!' assert :gleam@int.to_string(1) == "1" + assert :deeper_gleam_dep.main() {:ok, content} = :file.consult("_build/dev/lib/gleam_dep/ebin/gleam_dep.app") @@ -100,14 +99,16 @@ defmodule Mix.GleamTest do [ {:modules, [:collocated_erlang, :gleam_dep]}, {:optional_applications, []}, - {:applications, - [:kernel, :stdlib, :elixir, :gleam_otp, :gleam_stdlib, :gleeunit]}, + {:applications, [:kernel, :stdlib, :elixir, :gleam_otp, :gleam_stdlib]}, {:description, ~c"gleam_dep"}, {:registered, []}, {:vsn, ~c"1.0.0"} ] } ] + + assert File.exists?("_build/dev/lib/deeper_gleam_dep/ebin/deeper_gleam_dep.app") + assert :ok == Mix.Tasks.Deps.Loadpaths.run([]) end) end end diff --git a/lib/mix/test/test_helper.exs b/lib/mix/test/test_helper.exs index c07ea6c532e..a077e3d64c1 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -301,12 +301,14 @@ end) ## Set up Gleam fixtures -fixture = "gleam_dep" +fixtures = ~w(gleam_dep subfolder) -source = MixTest.Case.fixture_path(fixture) -dest = MixTest.Case.tmp_path(fixture) -File.mkdir_p!(dest) -File.cp_r!(source, dest) +Enum.each(fixtures, fn fixture -> + source = MixTest.Case.fixture_path(fixture) + dest = MixTest.Case.tmp_path(fixture) + File.mkdir_p!(dest) + File.cp_r!(source, dest) +end) ## Set up Git fixtures From c7693b2d4b4547ae7dd4a9810e8414f710c1a81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Tue, 17 Jun 2025 22:52:09 +0200 Subject: [PATCH 15/21] Install gleam 1.11.1 on CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dee6c12510..b195d87ff9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,7 @@ jobs: - uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4 with: otp-version: ${{ matrix.otp_version }} + gleam-version: "1.11.1" - name: Set ERL_COMPILER_OPTIONS if: ${{ matrix.deterministic }} run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV From 8d800f8d401e3e7dc58180fd60a5d5108de459b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Mon, 30 Jun 2025 11:25:32 +0200 Subject: [PATCH 16/21] Apply code review suggestions --- lib/mix/lib/mix/tasks/compile.app.ex | 4 +++- lib/mix/lib/mix/tasks/deps.compile.ex | 12 +++++++++--- lib/mix/test/fixtures/gleam_dep/.gitignore | 2 +- .../fixtures/subfolder/deeper_gleam_dep/.gitignore | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 4bedc5f0b74..6239200593e 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -281,7 +281,9 @@ defmodule Mix.Tasks.Compile.App do defp merge_project_application(best_guess, _project, application) do if not Keyword.keyword?(application) do - Mix.raise("Application configuration passed as :application should be a keyword list") + Mix.raise( + "Application configuration passed as :application should be a keyword list, , got: #{inspect(application)}" + ) end Keyword.merge(best_guess, validate_properties!(application)) diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 8f7581ce708..9ea5a2c7551 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -342,6 +342,13 @@ defmodule Mix.Tasks.Deps.Compile do src = Path.join(build, "_gleam_artefacts") File.mkdir(Path.join(build, "ebin")) + # 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() + config = Mix.Project.deps_config() |> Keyword.merge( @@ -350,8 +357,7 @@ defmodule Mix.Tasks.Deps.Compile do 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(), + build_path: build_path, build_scm: dep.scm, deps_path: deps_path, deps_app_path: build, @@ -360,10 +366,10 @@ defmodule Mix.Tasks.Deps.Compile do erlc_include_path: Path.join(build, "include") ) - env = dep.opts[:env] || :prod old_env = Mix.env() try do + env = dep.opts[:env] || :prod Mix.env(env) Mix.ProjectStack.push(dep.app, config, "nofile") diff --git a/lib/mix/test/fixtures/gleam_dep/.gitignore b/lib/mix/test/fixtures/gleam_dep/.gitignore index 6d3cac8a794..6f6e2eb4b4d 100644 --- a/lib/mix/test/fixtures/gleam_dep/.gitignore +++ b/lib/mix/test/fixtures/gleam_dep/.gitignore @@ -1,7 +1,7 @@ # The directory Mix will write compiled artifacts to. /_build/ -# The directory gleam will write compiled artifacts to. +# The directory Gleam will write compiled artifacts to. /build/ # If the VM crashes, it generates a dump, let's ignore it too. diff --git a/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/.gitignore b/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/.gitignore index 6d3cac8a794..6f6e2eb4b4d 100644 --- a/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/.gitignore +++ b/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/.gitignore @@ -1,7 +1,7 @@ # The directory Mix will write compiled artifacts to. /_build/ -# The directory gleam will write compiled artifacts to. +# The directory Gleam will write compiled artifacts to. /build/ # If the VM crashes, it generates a dump, let's ignore it too. From 7ffa8c80ae9a2d09da4dca71e883b4c2502afba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Mon, 30 Jun 2025 13:20:26 +0200 Subject: [PATCH 17/21] Pinpoint gleam deps to avoid brittle tests --- lib/mix/test/fixtures/gleam_dep/gleam.toml | 4 ++-- lib/mix/test/fixtures/subfolder/deeper_gleam_dep/gleam.toml | 2 +- lib/mix/test/mix/gleam_test.exs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/mix/test/fixtures/gleam_dep/gleam.toml b/lib/mix/test/fixtures/gleam_dep/gleam.toml index 575c6b2ae11..c39bfc96f8f 100644 --- a/lib/mix/test/fixtures/gleam_dep/gleam.toml +++ b/lib/mix/test/fixtures/gleam_dep/gleam.toml @@ -13,8 +13,8 @@ version = "1.0.0" # 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" +gleam_stdlib = "0.59.0" +gleam_otp = "0.16.1" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/gleam.toml b/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/gleam.toml index 25adef22085..47cfc8e60de 100644 --- a/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/gleam.toml +++ b/lib/mix/test/fixtures/subfolder/deeper_gleam_dep/gleam.toml @@ -14,4 +14,4 @@ version = "1.0.0" [dependencies] gleam_dep = { path = "../../gleam_dep" } -gleam_stdlib = ">= 0.59.0 and < 1.0.0" +gleam_stdlib = "0.59.0" diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index 791066fe329..7f45ca7e43b 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -28,8 +28,8 @@ defmodule Mix.GleamTest do 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"}, + {:gleam_stdlib, "0.59.0"}, + {:gleam_otp, "0.16.1"}, {:gleeunit, ">= 1.0.0 and < 2.0.0", only: [:dev, :test]} ] From 46aa2d67edc25a3cfdf2e2038505b998809e733e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Tue, 1 Jul 2025 11:24:45 +0200 Subject: [PATCH 18/21] Fix typo Co-authored-by: Eksperimental --- lib/mix/lib/mix/tasks/compile.app.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 6239200593e..7e7ad7be9a0 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -282,7 +282,7 @@ defmodule Mix.Tasks.Compile.App do defp merge_project_application(best_guess, _project, application) do if not Keyword.keyword?(application) do Mix.raise( - "Application configuration passed as :application should be a keyword list, , got: #{inspect(application)}" + "Application configuration passed as :application should be a keyword list, got: #{inspect(application)}" ) end From 7e51726d98af23d25a2a793bbec94dd751c86aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Wed, 2 Jul 2025 13:37:19 +0200 Subject: [PATCH 19/21] Apply suggestions from code review Co-authored-by: Eksperimental --- lib/mix/lib/mix/dep.ex | 2 +- lib/mix/lib/mix/gleam.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/dep.ex b/lib/mix/lib/mix/dep.ex index a63bd835bed..6347fe0deff 100644 --- a/lib/mix/lib/mix/dep.ex +++ b/lib/mix/lib/mix/dep.ex @@ -556,7 +556,7 @@ defmodule Mix.Dep do end @doc """ - Returns `true` if dependency is a Gleam project. + Returns `true` if dependency is a Gleam project; otherwise returns `false`. """ def gleam?(%Mix.Dep{manager: manager}) do manager == :gleam diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index fac5758eeef..c703aa3cce6 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -72,7 +72,7 @@ defmodule Mix.Gleam do defp maybe_erlang_opts(config, opts) do application = opts - |> Enum.filter(fn {_, value} -> value != nil end) + |> Enum.reject(fn {_, value} -> value == nil end) |> Enum.map(fn {"application_start_module", module} when is_binary(module) -> {:mod, {String.to_atom(module), []}} From 47f3b66571592007e7593204a0699a18aec5fac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Fri, 24 Oct 2025 12:14:44 +0200 Subject: [PATCH 20/21] Fix typo in docs Co-authored-by: Hans Glimmerfors --- lib/mix/lib/mix/dep.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/dep.ex b/lib/mix/lib/mix/dep.ex index 6347fe0deff..c584676569c 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` | `:gleam' | `nil` + `:rebar3` | `:mix` | `:make` | `:gleam` | `nil` * `from` - path to the file where the dependency was defined From 2f0efe4fca4c6279497f9c5e57e5d7236601c3be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Fri, 28 Nov 2025 09:02:05 +0100 Subject: [PATCH 21/21] Update copyright comments Co-authored-by: Eksperimental --- lib/mix/lib/mix/gleam.ex | 1 - lib/mix/test/mix/gleam_test.exs | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index c703aa3cce6..c059a2bdae8 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -1,6 +1,5 @@ # 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 diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index 7f45ca7e43b..92ecb1ff816 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -1,6 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: 2021 The Elixir Team -# SPDX-FileCopyrightText: 2012 Plataformatec Code.require_file("../test_helper.exs", __DIR__)