From b17109ebad02112783ea90cbec7f1de380e07db6 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/17] 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 e6bce9251b0..cf159d286e0 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -280,6 +280,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 74f0147dd2d23539329344c5193f8f67737de52a 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/17] 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 1dd953b4cd9..e69ec79fc57 100644 --- a/lib/mix/lib/mix/dep.ex +++ b/lib/mix/lib/mix/dep.ex @@ -27,7 +27,7 @@ defmodule Mix.Dep do * `top_level` - true if dependency was defined in the top-level project * `manager` - the project management, possible values: - `:rebar3` | `:mix` | `:make` | `nil` + `:rebar3` | `:mix` | `:make` | `:gleam' | `nil` * `from` - path to the file where the dependency was defined @@ -73,7 +73,7 @@ defmodule Mix.Dep do status: {:ok, String.t() | nil} | atom | tuple, opts: keyword, top_level: boolean, - manager: :rebar3 | :mix | :make | nil, + manager: :rebar3 | :mix | :make | :gleam | nil, from: String.t(), extra: term, system_env: keyword @@ -535,6 +535,13 @@ defmodule Mix.Dep do manager == :make end + @doc """ + Returns `true` if dependency is a Gleam project. + """ + def gleam?(%Mix.Dep{manager: manager}) do + manager == :gleam + end + ## Helpers defp mix_env_var do diff --git a/lib/mix/lib/mix/dep/converger.ex b/lib/mix/lib/mix/dep/converger.ex index 1d036c49822..5ae5afe7fbe 100644 --- a/lib/mix/lib/mix/dep/converger.ex +++ b/lib/mix/lib/mix/dep/converger.ex @@ -426,7 +426,7 @@ defmodule Mix.Dep.Converger do %{other | manager: sort_manager(other_manager, manager, in_upper?)} end - @managers [:mix, :rebar3, :make] + @managers [:mix, :rebar3, :make, :gleam] defp sort_manager(other_manager, manager, true) do other_manager || manager diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 41ac29bca89..d148d27c684 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 ded63073b04..b65bac82a2c 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 2e91535b199..0bc4787714c 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)" ) @@ -292,6 +296,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 ce7080eac42..557f54bc285 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 aaf83bae2e6cbf8d9d642f7892d24fb5bcf8d2d4 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/17] 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 c104e094032e0bbb47b6e0a5f562687e25ca0956 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/17] 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 cf159d286e0..2ca4647dff3 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -43,12 +43,21 @@ cover_exclude = [] 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") 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, assert_receive_timeout: String.to_integer(System.get_env("ELIXIR_ASSERT_TIMEOUT", "300")) ) From e04ae6b4eed4b6203f0bd9b4bc7090031ee2fa40 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/17] 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 0bc4787714c..2b1ad37554b 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: @@ -346,7 +346,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 005f283aafe09c70498321e392f8d5d35ad2a7ef 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/17] 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 d148d27c684..c4c55da5ca6 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 e3c15596a6ea6c4ea57dbd4129a21e99e523e237 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/17] 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 c4c55da5ca6..943919fefa4 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 2d74c1384c5e215401a9e1b0fc8d8cc2b41f396f 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/17] 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 2b1ad37554b..e5f649efcb4 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -346,7 +346,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 cca9cd96d4e34587bd60ab28df3cbb6e66b3ee72 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/17] 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 943919fefa4..3097591e8b4 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 700def012b8276ca4e748c4b629870bba26c411b 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/17] 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 3097591e8b4..e9249e8a205 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 e5f649efcb4..1b614a88803 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -308,7 +308,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 9e218eeffdaf5bbde3b7ee97f4a72ab6e7fb8e2d 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/17] 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 e9249e8a205..e94acb2cc25 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 1b614a88803..0500dba117e 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -298,6 +298,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] @@ -305,58 +306,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 16ec11eb477f9f7a3bfcbc44e8cb0693a737dc99 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/17] 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 e94acb2cc25..d85e99f38b5 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 0500dba117e..b8d25f0f188 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -306,18 +306,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 557f54bc285..346fce3dda9 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 519922a9200eb5d190c9da2f8b75e858a70fd183 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/17] 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 92b272e04b2..279e55f0fb5 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) @@ -252,7 +252,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() @@ -268,6 +268,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 b8d25f0f188..0500dba117e 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -306,7 +306,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 9a8926412f5..9c9ed0b71a6 100644 --- a/lib/mix/test/mix/tasks/compile.app_test.exs +++ b/lib/mix/test/mix/tasks/compile.app_test.exs @@ -283,6 +283,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 5727bd6551b0fa7c078f65555d1dc2eaf1e310a7 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/17] 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 0500dba117e..66088d9e55e 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -333,7 +333,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, @@ -341,16 +342,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 2ca4647dff3..574335a3a35 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -291,12 +291,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 01d54c5f5508a27bc7705c69d3437c077697c1ee 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/17] 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 420a2c7e1e4..9fe11d5b335 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,7 @@ jobs: - uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0 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 da217a19f4c9e2e2a61865cc9d8899fae26582f9 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/17] 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 279e55f0fb5..312bb043a2c 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -270,7 +270,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 66088d9e55e..6ec275cc522 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -332,6 +332,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( @@ -340,8 +347,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, @@ -350,10 +356,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 21e949c3527521c35c903916ca744faa242dd2e1 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/17] 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]} ]