From 4997e6adf0e4f96c8d863ce496af0d5c5d050a5f 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/15] 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 2acc66374ba..147ddd932b1 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -276,6 +276,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 04eede470d07de598f635a1f4690f82b692737e7 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/15] Add Gleam integration with Mix - Add Mix.Gleam module - Add specific gleam binary version requirement - Rely on `gleam export package-info` --- lib/mix/lib/mix/dep.ex | 11 +++- lib/mix/lib/mix/dep/converger.ex | 2 +- lib/mix/lib/mix/dep/loader.ex | 29 +++++++-- lib/mix/lib/mix/gleam.ex | 94 +++++++++++++++++++++++++++ lib/mix/lib/mix/task.compiler.ex | 2 +- lib/mix/lib/mix/tasks/deps.compile.ex | 21 +++++- lib/mix/lib/mix/tasks/deps.ex | 4 +- lib/mix/test/mix/gleam_test.exs | 93 ++++++++++++++++++++++++++ 8 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 lib/mix/lib/mix/gleam.ex create mode 100644 lib/mix/test/mix/gleam_test.exs diff --git a/lib/mix/lib/mix/dep.ex b/lib/mix/lib/mix/dep.ex index 3f981351489..80acfb7ce4e 100644 --- a/lib/mix/lib/mix/dep.ex +++ b/lib/mix/lib/mix/dep.ex @@ -27,7 +27,7 @@ defmodule Mix.Dep do * `top_level` - true if dependency was defined in the top-level project * `manager` - the project management, possible values: - `:rebar3` | `:mix` | `:make` | `nil` + `:rebar3` | `:mix` | `:make` | `:gleam' | `nil` * `from` - path to the file where the dependency was defined @@ -73,7 +73,7 @@ defmodule Mix.Dep do status: {:ok, String.t() | nil} | atom | tuple, opts: keyword, top_level: boolean, - manager: :rebar3 | :mix | :make | nil, + manager: :rebar3 | :mix | :make | :gleam | nil, from: String.t(), extra: term, system_env: keyword @@ -535,6 +535,13 @@ defmodule Mix.Dep do manager == :make end + @doc """ + Returns `true` if dependency is a Gleam project. + """ + def gleam?(%Mix.Dep{manager: manager}) do + manager == :gleam + end + ## Helpers defp mix_env_var do diff --git a/lib/mix/lib/mix/dep/converger.ex b/lib/mix/lib/mix/dep/converger.ex index 1d036c49822..5ae5afe7fbe 100644 --- a/lib/mix/lib/mix/dep/converger.ex +++ b/lib/mix/lib/mix/dep/converger.ex @@ -426,7 +426,7 @@ defmodule Mix.Dep.Converger do %{other | manager: sort_manager(other_manager, manager, in_upper?)} end - @managers [:mix, :rebar3, :make] + @managers [:mix, :rebar3, :make, :gleam] defp sort_manager(other_manager, manager, true) do other_manager || manager diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 984fa32e478..490019a8e9e 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -8,7 +8,7 @@ defmodule Mix.Dep.Loader do @moduledoc false - import Mix.Dep, only: [ok?: 1, mix?: 1, rebar?: 1, make?: 1] + import Mix.Dep, only: [ok?: 1, mix?: 1, rebar?: 1, make?: 1, gleam?: 1] @doc """ Gets all direct children of the current `Mix.Project` @@ -84,9 +84,9 @@ defmodule Mix.Dep.Loader do def load(%Mix.Dep{manager: manager, scm: scm, opts: opts} = dep, children, locked?) do # The manager for a child dependency is set based on the following rules: # 1. Set in dependency definition - # 2. From SCM, so that Hex dependencies of a rebar project can be compiled with mix + # 2. From SCM, so that Hex dependencies of a rebar/gleam project can be compiled with mix # 3. From the parent dependency, used for rebar dependencies from git - # 4. Inferred from files in dependency (mix.exs, rebar.config, Makefile) + # 4. Inferred from files in dependency (mix.exs, rebar.config, Makefile, gleam.toml) manager = opts[:manager] || scm_manager(scm, opts) || manager || infer_manager(opts[:dest]) dep = %{dep | manager: manager, status: scm_status(scm, opts)} @@ -106,6 +106,9 @@ defmodule Mix.Dep.Loader do make?(dep) -> make_dep(dep) + gleam?(dep) -> + gleam_dep(dep, children, locked?) + true -> {dep, []} end @@ -220,7 +223,7 @@ defmodule Mix.Dep.Loader do # Note that we ignore Make dependencies because the # file based heuristic will always figure it out. - @scm_managers ~w(mix rebar3)a + @scm_managers ~w(mix rebar3 gleam)a defp scm_manager(scm, opts) do managers = scm.managers(opts) @@ -246,6 +249,9 @@ defmodule Mix.Dep.Loader do any_of?(dest, ["Makefile", "Makefile.win"]) -> :make + any_of?(dest, ["gleam.toml"]) -> + :gleam + true -> nil end @@ -361,6 +367,21 @@ defmodule Mix.Dep.Loader do {dep, []} end + defp gleam_dep(%Mix.Dep{opts: opts} = dep, children, locked?) do + Mix.Gleam.require!() + + deps = + if children do + Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?)) + else + config = File.cd!(opts[:dest], fn -> Mix.Gleam.load_config(".") end) + from = Path.join(opts[:dest], "gleam.toml") + Enum.map(config[:deps], &to_dep(&1, from, _manager = nil, locked?)) + end + + {%{dep | opts: Keyword.merge(opts, app: false, override: true)}, deps} + end + defp mix_children(config, locked?, opts) do from = Mix.Project.project_file() diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex new file mode 100644 index 00000000000..270ef0ce02b --- /dev/null +++ b/lib/mix/lib/mix/gleam.ex @@ -0,0 +1,94 @@ +defmodule Mix.Gleam do + # Version that introduced `gleam export package-information` command + @required_gleam_version ">= 1.10.0" + + def load_config(dir) do + File.cd!(dir, fn -> + gleam!(["export", "package-information", "--out", "/dev/stdout"]) + |> JSON.decode!() + |> Map.fetch!("gleam.toml") + |> parse_config() + end) + end + + def parse_config(json) do + try do + deps = + Map.get(json, "dependencies", %{}) + |> Enum.map(&parse_dep/1) + + dev_deps = + Map.get(json, "dev-dependencies", %{}) + |> Enum.map(&parse_dep(&1, only: :dev)) + + %{ + name: Map.fetch!(json, "name"), + version: Map.fetch!(json, "version"), + deps: deps ++ dev_deps + } + |> maybe_gleam_version(json["gleam"]) + rescue + KeyError -> + Mix.raise("Command \"gleam export package-information\" unexpected format: \n" <> json) + end + end + + defp parse_dep({dep, requirement}, opts \\ []) do + dep = String.to_atom(dep) + + spec = + case requirement do + %{"version" => version} -> {dep, version, opts} + %{"path" => path} -> {dep, Keyword.merge(opts, path: path)} + end + + case spec do + {dep, version, []} -> {dep, version} + spec -> spec + end + end + + defp maybe_gleam_version(config, nil), do: config + + defp maybe_gleam_version(config, version) do + Map.put(config, :gleam, version) + end + + def require!() do + available_version() + |> Version.match?(@required_gleam_version) + end + + defp available_version do + try do + case gleam!(["--version"]) do + "gleam " <> version -> Version.parse!(version) |> Version.to_string() + output -> Mix.raise("Command \"gleam --version\" unexpected format: #{output}") + end + rescue + e in Version.InvalidVersionError -> + Mix.raise("Command \"gleam --version\" invalid version format: #{e.version}") + end + end + + defp gleam!(args) do + try do + System.cmd("gleam", args) + catch + :error, :enoent -> + Mix.raise( + "The \"gleam\" executable is not available in your PATH. " <> + "Please install it, as one of your dependencies requires it. " + ) + else + {response, 0} -> + String.trim(response) + + {response, _} when is_binary(response) -> + Mix.raise("Command \"gleam #{Enum.join(args, " ")}\" failed with reason: #{response}") + + {_, _} -> + Mix.raise("Command \"gleam #{Enum.join(args, " ")}\" failed") + end + end +end diff --git a/lib/mix/lib/mix/task.compiler.ex b/lib/mix/lib/mix/task.compiler.ex index 730234afdf6..7f399d7a809 100644 --- a/lib/mix/lib/mix/task.compiler.ex +++ b/lib/mix/lib/mix/task.compiler.ex @@ -80,7 +80,7 @@ defmodule Mix.Task.Compiler do * `:scm` - the SCM module of the dependency. * `:manager` - the dependency project management, possible values: - `:rebar3`, `:mix`, `:make`, `nil`. + `:rebar3`, `:mix`, `:make`, `:gleam`, `nil`. * `:os_pid` - the operating system PID of the process that run the compilation. The value is a string and it can be compared diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 962b7be9f3f..4967f6a7ed0 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -22,6 +22,7 @@ defmodule Mix.Tasks.Deps.Compile do * `Makefile.win`- invokes `nmake /F Makefile.win` (only on Windows) * `Makefile` - invokes `gmake` on DragonFlyBSD, FreeBSD, NetBSD, and OpenBSD, invokes `make` on any other operating system (except on Windows) + * `gleam.toml` - invokes `gleam export` The compilation can be customized by passing a `compile` option in the dependency: @@ -139,9 +140,12 @@ defmodule Mix.Tasks.Deps.Compile do dep.manager == :rebar3 -> do_rebar3(dep, config) + dep.manager == :gleam -> + do_gleam(dep, config) + true -> Mix.shell().error( - "Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\" or \"Makefile\" " <> + "Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\", \"Makefile\" or \"gleam.toml\" " <> "(pass :compile as an option to customize compilation, set it to \"false\" to do nothing)" ) @@ -320,6 +324,21 @@ defmodule Mix.Tasks.Deps.Compile do true end + defp do_gleam(%Mix.Dep{opts: opts} = dep, config) do + Mix.Gleam.require!() + + lib = Path.join(Mix.Project.build_path(), "lib") + out = opts[:build] + package = opts[:dest] + + command = + {"gleam", + ["compile-package", "--target", "erlang", "--package", package, "--out", out, "--lib", lib]} + + shell_cmd!(dep, config, command) + Code.prepend_path(Path.join(out, "ebin"), cache: true) + end + defp make_command(dep) do makefile_win? = makefile_win?(dep) diff --git a/lib/mix/lib/mix/tasks/deps.ex b/lib/mix/lib/mix/tasks/deps.ex index 1a216730f2e..11c0018cdfa 100644 --- a/lib/mix/lib/mix/tasks/deps.ex +++ b/lib/mix/lib/mix/tasks/deps.ex @@ -101,10 +101,10 @@ defmodule Mix.Tasks.Deps do * `:override` - if set to `true` the dependency will override any other definitions of itself by other dependencies - * `:manager` - Mix can also compile Rebar3 and makefile projects + * `:manager` - Mix can also compile Rebar3, makefile and gleam projects and can fetch sub dependencies of Rebar3 projects. Mix will try to infer the type of project but it can be overridden with this - option by setting it to `:mix`, `:rebar3`, or `:make`. In case + option by setting it to `:mix`, `:rebar3`, `:make` or `:gleam`. In case there are conflicting definitions, the first manager in the list above will be picked up. For example, if a dependency is found with `:rebar3` as a manager in different part of the trees, `:rebar3` will be automatically diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs new file mode 100644 index 00000000000..aca6528358f --- /dev/null +++ b/lib/mix/test/mix/gleam_test.exs @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2021 The Elixir Team +# SPDX-FileCopyrightText: 2012 Plataformatec + +Code.require_file("../test_helper.exs", __DIR__) + +defmodule Mix.GleamTest do + use MixTest.Case + + @compile {:no_warn_undefined, [:gleam_dep, :gleam@int]} + + defmodule GleamAsDep do + def project do + [ + app: :gleam_as_dep, + version: "0.1.0", + deps: [ + {:gleam_dep, path: MixTest.Case.tmp_path("gleam_dep"), app: false} + ] + ] + end + end + + describe "load_config/1" do + test "loads gleam.toml" do + path = MixTest.Case.fixture_path("gleam_dep") + config = Mix.Gleam.load_config(path) + + expected = [ + {:gleam_stdlib, ">= 0.44.0 and < 2.0.0"}, + {:gleam_otp, ">= 0.16.1 and < 1.0.0"}, + {:gleeunit, ">= 1.0.0 and < 2.0.0", only: :dev} + ] + + assert Enum.sort(config[:deps]) == Enum.sort(expected) + end + end + + describe "gleam export package-information format" do + test "parse_config" do + config = + %{ + "name" => "gael", + "version" => "1.0.0", + "gleam" => ">= 1.8.0", + "dependencies" => %{ + "gleam_stdlib" => %{"version" => ">= 0.18.0 and < 2.0.0"}, + "my_other_project" => %{"path" => "../my_other_project"} + }, + "dev-dependencies" => %{"gleeunit" => %{"version" => ">= 1.0.0 and < 2.0.0"}} + } + |> Mix.Gleam.parse_config() + + assert config == %{ + name: "gael", + version: "1.0.0", + gleam: ">= 1.8.0", + deps: [ + {:gleam_stdlib, ">= 0.18.0 and < 2.0.0"}, + {:my_other_project, path: "../my_other_project"}, + {:gleeunit, ">= 1.0.0 and < 2.0.0", only: :dev} + ] + } + end + end + + describe "integration with Mix" do + test "gets and compiles dependencies" do + in_tmp("get and compile dependencies", fn -> + Mix.Project.push(GleamAsDep) + + Mix.Tasks.Deps.Get.run([]) + assert_received {:mix_shell, :info, ["* Getting gleam_stdlib " <> _]} + assert_received {:mix_shell, :info, ["* Getting gleam_otp " <> _]} + assert_received {:mix_shell, :info, ["* Getting gleeunit " <> _]} + + Mix.Tasks.Deps.Compile.run([]) + assert :gleam_dep.main() + assert :gleam@int.to_string(1) == "1" + + load_paths = + Mix.Dep.Converger.converge([]) + |> Enum.map(&Mix.Dep.load_paths(&1)) + |> Enum.concat() + + assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_dep/ebin")) + assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_stdlib/ebin")) + # Dep of a dep + assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_erlang/ebin")) + end) + end + end +end From edf35ca9ddd51e50960947008115b1731e0511f7 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/15] 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 94c762442dff93f46ac19fee37e2cae9384fb580 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/15] 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 147ddd932b1..77ac41ff1c8 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 e81ec1e178804fefd2f5c4f40c2905fc1df410f7 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/15] Fix deps.compile for gleam - shell_cmd! wasn't handling tuples - Fix documentation --- lib/mix/lib/mix/tasks/deps.compile.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 4967f6a7ed0..e6f2a52a7a9 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -22,7 +22,7 @@ defmodule Mix.Tasks.Deps.Compile do * `Makefile.win`- invokes `nmake /F Makefile.win` (only on Windows) * `Makefile` - invokes `gmake` on DragonFlyBSD, FreeBSD, NetBSD, and OpenBSD, invokes `make` on any other operating system (except on Windows) - * `gleam.toml` - invokes `gleam export` + * `gleam.toml` - invokes `gleam compile-package` The compilation can be customized by passing a `compile` option in the dependency: @@ -374,7 +374,7 @@ defmodule Mix.Tasks.Deps.Compile do defp shell_cmd!(%Mix.Dep{app: app} = dep, config, command, env \\ []) do if Mix.shell().cmd(command, [print_app: true] ++ opts_for_cmd(dep, config, env)) != 0 do Mix.raise( - "Could not compile dependency #{inspect(app)}, \"#{command}\" command failed. " <> + "Could not compile dependency #{inspect(app)}, \"#{inspect(command)}\" command failed. " <> deps_compile_feedback(app) ) end From c595cf683ba6d0c33dd56cbd4fb5f1b8e811709d 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/15] Add support for application_start_module This is an optional value within [erlang] in the gleam.toml file. It will be used for the `mod` value when generating a .app file --- lib/mix/lib/mix/dep/loader.ex | 31 +++++++++++++++++++++---------- lib/mix/lib/mix/gleam.ex | 17 +++++++++++++---- lib/mix/test/mix/gleam_test.exs | 10 ++++++++-- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 490019a8e9e..3ff05a7ed5d 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -367,19 +367,30 @@ defmodule Mix.Dep.Loader do {dep, []} end - defp gleam_dep(%Mix.Dep{opts: opts} = dep, children, locked?) do + defp gleam_dep(%Mix.Dep{opts: opts} = dep, _children = nil, locked?) do Mix.Gleam.require!() - deps = - if children do - Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?)) - else - config = File.cd!(opts[:dest], fn -> Mix.Gleam.load_config(".") end) - from = Path.join(opts[:dest], "gleam.toml") - Enum.map(config[:deps], &to_dep(&1, from, _manager = nil, locked?)) - end + config = File.cd!(opts[:dest], fn -> Mix.Gleam.load_config(".") end) + from = Path.join(opts[:dest], "gleam.toml") + deps = Enum.map(config[:deps], &to_dep(&1, from, _manager = nil, locked?)) + + properties = [ + {:vsn, to_charlist(config[:version])}, + {:mod, {String.to_atom(config[:mod]), []}} + ] - {%{dep | opts: Keyword.merge(opts, app: false, override: true)}, deps} + contents = :io_lib.format("~p.~n", [{:application, dep.app, properties}]) + + [opts[:build], "ebin", "#{dep.app}.app"] + |> Path.join() + |> File.write!(IO.chardata_to_string(contents)) + + {dep, deps} + end + + defp gleam_dep(%Mix.Dep{opts: opts} = dep, children, locked?) do + dep = %{dep | opts: Keyword.merge(opts, app: false, override: true)} + {dep, Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?))} end defp mix_children(config, locked?, opts) do diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index 6cf416bf6dc..c357163c07b 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -26,7 +26,8 @@ defmodule Mix.Gleam do version: Map.fetch!(json, "version"), deps: deps ++ dev_deps } - |> maybe_gleam_version(json["gleam"]) + |> maybe_gleam_version(json) + |> maybe_application_start_module(json) rescue KeyError -> Mix.raise("Command \"gleam export package-information\" unexpected format: \n" <> json) @@ -57,10 +58,18 @@ defmodule Mix.Gleam do end end - defp maybe_gleam_version(config, nil), do: config + defp maybe_gleam_version(config, json) do + case json["gleam"] do + nil -> config + version -> Map.put(config, :gleam, version) + end + end - defp maybe_gleam_version(config, version) do - Map.put(config, :gleam, version) + defp maybe_application_start_module(config, json) do + case get_in(json, ["erlang", "application_start_module"]) do + nil -> config + mod -> Map.put(config, :mod, mod) + end end def require!() do diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index 0db1af66f87..9cd80de431b 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -49,7 +49,12 @@ defmodule Mix.GleamTest do "gleam_stdlib" => %{"version" => ">= 0.18.0 and < 2.0.0"}, "my_other_project" => %{"path" => "../my_other_project"} }, - "dev-dependencies" => %{"gleeunit" => %{"version" => ">= 1.0.0 and < 2.0.0"}} + "dev-dependencies" => %{ + "gleeunit" => %{"version" => ">= 1.0.0 and < 2.0.0"} + }, + "erlang" => %{ + "application_start_module" => "some@application" + } } |> Mix.Gleam.parse_config() @@ -62,7 +67,8 @@ defmodule Mix.GleamTest do {:gleam_stdlib, ">= 0.18.0 and < 2.0.0"}, {:my_other_project, path: "../my_other_project"}, {:gleeunit, ">= 1.0.0 and < 2.0.0", only: :dev} - ] + ], + mod: "some@application" } end end From 60f56254fbba40b9e2059070f8f002e8a88f2e11 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/15] Handle gleam extra_applications --- lib/mix/lib/mix/dep/loader.ex | 30 +++++++++++++++++++--- lib/mix/lib/mix/gleam.ex | 14 +++++++--- lib/mix/test/fixtures/gleam_dep/gleam.toml | 4 +++ lib/mix/test/mix/gleam_test.exs | 6 +++++ 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 3ff05a7ed5d..1224ee74527 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -374,13 +374,17 @@ defmodule Mix.Dep.Loader do from = Path.join(opts[:dest], "gleam.toml") deps = Enum.map(config[:deps], &to_dep(&1, from, _manager = nil, locked?)) - properties = [ - {:vsn, to_charlist(config[:version])}, - {:mod, {String.to_atom(config[:mod]), []}} - ] + properties = + [{:vsn, to_charlist(config[:version])}] + |> gleam_mod(config) + |> gleam_applications(config) contents = :io_lib.format("~p.~n", [{:application, dep.app, properties}]) + [opts[:build], "ebin"] + |> Path.join() + |> File.mkdir_p!() + [opts[:build], "ebin", "#{dep.app}.app"] |> Path.join() |> File.write!(IO.chardata_to_string(contents)) @@ -393,6 +397,24 @@ defmodule Mix.Dep.Loader do {dep, Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?))} end + defp gleam_mod(properties, config) do + case config[:mod] do + nil -> properties + mod -> [{:mod, {String.to_atom(mod), []}} | properties] + end + end + + defp gleam_applications(properties, config) do + case config[:extra_applications] do + nil -> + properties + + applications -> + applications = Enum.map(applications, &String.to_atom/1) + [{:applications, applications} | properties] + end + end + defp mix_children(config, locked?, opts) do from = Mix.Project.project_file() diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index c357163c07b..a5a5e81b0c7 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -27,7 +27,7 @@ defmodule Mix.Gleam do deps: deps ++ dev_deps } |> maybe_gleam_version(json) - |> maybe_application_start_module(json) + |> maybe_erlang_opts(json) rescue KeyError -> Mix.raise("Command \"gleam export package-information\" unexpected format: \n" <> json) @@ -65,10 +65,16 @@ defmodule Mix.Gleam do end end - defp maybe_application_start_module(config, json) do - case get_in(json, ["erlang", "application_start_module"]) do + defp maybe_erlang_opts(config, json) do + config = + case get_in(json, ["erlang", "application_start_module"]) do + nil -> config + mod -> Map.put(config, :mod, mod) + end + + case get_in(json, ["erlang", "extra_applications"]) do nil -> config - mod -> Map.put(config, :mod, mod) + extra_applications -> Map.put(config, :extra_applications, extra_applications) end end diff --git a/lib/mix/test/fixtures/gleam_dep/gleam.toml b/lib/mix/test/fixtures/gleam_dep/gleam.toml index fc88f8e0f47..0a250087907 100644 --- a/lib/mix/test/fixtures/gleam_dep/gleam.toml +++ b/lib/mix/test/fixtures/gleam_dep/gleam.toml @@ -18,3 +18,7 @@ gleam_otp = ">= 0.16.1 and < 1.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" + +[erlang] +extra_applications = ["ssl"] +application_start_module = "gleam_dep@somemodule" diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index 9cd80de431b..c21d12042af 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -96,6 +96,12 @@ defmodule Mix.GleamTest do assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_stdlib/ebin")) # Dep of a dep assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_erlang/ebin")) + {:ok, content} = :file.consult("_build/dev/lib/gleam_dep/ebin/gleam_dep.app") + + assert content == [ + {:application, :gleam_dep, + [applications: [:ssl], mod: {:gleam_dep@somemodule, []}, vsn: ~c"1.0.0"]} + ] end) end end From 047871e07b7974e3c87502fba94d3ad44e1aa3f0 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/15] Remove redundant quotes --- lib/mix/lib/mix/tasks/deps.compile.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index e6f2a52a7a9..25b63b49d0a 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -374,7 +374,7 @@ defmodule Mix.Tasks.Deps.Compile do defp shell_cmd!(%Mix.Dep{app: app} = dep, config, command, env \\ []) do if Mix.shell().cmd(command, [print_app: true] ++ opts_for_cmd(dep, config, env)) != 0 do Mix.raise( - "Could not compile dependency #{inspect(app)}, \"#{inspect(command)}\" command failed. " <> + "Could not compile dependency #{inspect(app)}, #{inspect(command)} command failed. " <> deps_compile_feedback(app) ) end From cf6018fd200b3308470ff00c37a1988fd2fa3e72 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/15] Do not force `app: false` in gleam deps --- lib/mix/lib/mix/dep/loader.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 1224ee74527..fea064c7efc 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -393,7 +393,6 @@ defmodule Mix.Dep.Loader do end defp gleam_dep(%Mix.Dep{opts: opts} = dep, children, locked?) do - dep = %{dep | opts: Keyword.merge(opts, app: false, override: true)} {dep, Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?))} end From 3ba141b2275a2b1869b05c83885127636d379eb1 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/15] Generate app file for gleam deps on compilation --- lib/mix/lib/mix/dep/loader.ex | 33 ------------------ lib/mix/lib/mix/gleam.ex | 2 +- lib/mix/lib/mix/tasks/deps.compile.ex | 50 ++++++++++++++++++++++++++- lib/mix/test/mix/gleam_test.exs | 24 +++++++------ 4 files changed, 63 insertions(+), 46 deletions(-) diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index fea064c7efc..2bac631f93c 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -374,21 +374,6 @@ defmodule Mix.Dep.Loader do from = Path.join(opts[:dest], "gleam.toml") deps = Enum.map(config[:deps], &to_dep(&1, from, _manager = nil, locked?)) - properties = - [{:vsn, to_charlist(config[:version])}] - |> gleam_mod(config) - |> gleam_applications(config) - - contents = :io_lib.format("~p.~n", [{:application, dep.app, properties}]) - - [opts[:build], "ebin"] - |> Path.join() - |> File.mkdir_p!() - - [opts[:build], "ebin", "#{dep.app}.app"] - |> Path.join() - |> File.write!(IO.chardata_to_string(contents)) - {dep, deps} end @@ -396,24 +381,6 @@ defmodule Mix.Dep.Loader do {dep, Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?))} end - defp gleam_mod(properties, config) do - case config[:mod] do - nil -> properties - mod -> [{:mod, {String.to_atom(mod), []}} | properties] - end - end - - defp gleam_applications(properties, config) do - case config[:extra_applications] do - nil -> - properties - - applications -> - applications = Enum.map(applications, &String.to_atom/1) - [{:applications, applications} | properties] - end - end - defp mix_children(config, locked?, opts) do from = Mix.Project.project_file() diff --git a/lib/mix/lib/mix/gleam.ex b/lib/mix/lib/mix/gleam.ex index a5a5e81b0c7..6c76ac46631 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -43,7 +43,7 @@ defmodule Mix.Gleam do {dep, version, opts} %{"path" => path} -> - {dep, Keyword.merge(opts, path: path)} + {dep, Keyword.merge(opts, path: Path.expand(path))} %{"git" => git, "ref" => ref} -> {dep, git: git, ref: ref} diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 25b63b49d0a..2a15810d0c0 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -336,7 +336,55 @@ defmodule Mix.Tasks.Deps.Compile do ["compile-package", "--target", "erlang", "--package", package, "--out", out, "--lib", lib]} shell_cmd!(dep, config, command) - Code.prepend_path(Path.join(out, "ebin"), cache: true) + + ebin = Path.join(out, "ebin") + app_file_path = Keyword.get(opts, :app, Path.join(ebin, "#{dep.app}.app")) + create_app_file = app_file_path && !File.exists?(app_file_path) + + if create_app_file do + generate_gleam_app_file(opts) + end + + Code.prepend_path(ebin, cache: true) + end + + defp gleam_extra_applications(config) do + config + |> Map.get(:extra_applications, []) + |> Enum.map(&String.to_atom/1) + end + + defp gleam_mod(config) do + case config[:mod] do + nil -> [] + mod -> {String.to_atom(mod), []} + end + end + + defp generate_gleam_app_file(opts) do + toml = File.cd!(opts[:dest], fn -> Mix.Gleam.load_config(".") end) + + module = + quote do + def project do + [ + app: unquote(toml.name) |> String.to_atom(), + version: "#{unquote(toml.version)}" + ] + end + + def application do + [ + mod: unquote(gleam_mod(toml)), + extra_applications: unquote(gleam_extra_applications(toml)) + ] + end + end + + module_name = String.to_atom("Gleam.#{toml.name}") + Module.create(module_name, module, Macro.Env.location(__ENV__)) + Mix.Project.push(module_name) + Mix.Tasks.Compile.App.run([]) end defp make_command(dep) do diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index c21d12042af..c9de5f0e7cd 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -87,20 +87,22 @@ defmodule Mix.GleamTest do assert :gleam_dep.main() assert :gleam@int.to_string(1) == "1" - load_paths = - Mix.Dep.Converger.converge([]) - |> Enum.map(&Mix.Dep.load_paths(&1)) - |> Enum.concat() - - assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_dep/ebin")) - assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_stdlib/ebin")) - # Dep of a dep - assert Enum.any?(load_paths, &String.ends_with?(&1, "gleam_erlang/ebin")) {:ok, content} = :file.consult("_build/dev/lib/gleam_dep/ebin/gleam_dep.app") assert content == [ - {:application, :gleam_dep, - [applications: [:ssl], mod: {:gleam_dep@somemodule, []}, vsn: ~c"1.0.0"]} + { + :application, + :gleam_dep, + [ + {:modules, [:gleam_dep]}, + {:optional_applications, []}, + {:applications, [:kernel, :stdlib, :elixir, :ssl]}, + {:description, ~c"gleam_dep"}, + {:registered, []}, + {:vsn, ~c"1.0.0"}, + {:mod, {:gleam_dep@somemodule, []}} + ] + } ] end) end From 6ed5fe7c9be543d9460b75b16fd750c01f6938e6 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/15] Proper beam compilation and .app file generation --- lib/mix/lib/mix/dep/loader.ex | 10 +-- lib/mix/lib/mix/tasks/deps.compile.ex | 86 +++++++++---------- .../gleam_dep/src/collocated_erlang.erl | 5 ++ .../fixtures/gleam_dep/src/gleam_dep.gleam | 3 + lib/mix/test/mix/gleam_test.exs | 16 ++-- 5 files changed, 63 insertions(+), 57 deletions(-) create mode 100644 lib/mix/test/fixtures/gleam_dep/src/collocated_erlang.erl diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 2bac631f93c..63187a3d3a6 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -107,7 +107,7 @@ defmodule Mix.Dep.Loader do make_dep(dep) gleam?(dep) -> - gleam_dep(dep, children, locked?) + gleam_dep(dep, children, manager, locked?) true -> {dep, []} @@ -367,18 +367,18 @@ defmodule Mix.Dep.Loader do {dep, []} end - defp gleam_dep(%Mix.Dep{opts: opts} = dep, _children = nil, locked?) do + defp gleam_dep(%Mix.Dep{opts: opts} = dep, _children = nil, manager, locked?) do Mix.Gleam.require!() config = File.cd!(opts[:dest], fn -> Mix.Gleam.load_config(".") end) from = Path.join(opts[:dest], "gleam.toml") - deps = Enum.map(config[:deps], &to_dep(&1, from, _manager = nil, locked?)) + deps = Enum.map(config[:deps], &to_dep(&1, from, manager, locked?)) {dep, deps} end - defp gleam_dep(%Mix.Dep{opts: opts} = dep, children, locked?) do - {dep, Enum.map(children, &to_dep(&1, opts[:dest], _manager = nil, locked?))} + defp gleam_dep(%Mix.Dep{opts: opts} = dep, children, manager, locked?) do + {dep, Enum.map(children, &to_dep(&1, opts[:dest], manager, locked?))} end defp mix_children(config, locked?, opts) do diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 2a15810d0c0..57610205439 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -326,6 +326,7 @@ defmodule Mix.Tasks.Deps.Compile do defp do_gleam(%Mix.Dep{opts: opts} = dep, config) do Mix.Gleam.require!() + Mix.Project.ensure_structure() lib = Path.join(Mix.Project.build_path(), "lib") out = opts[:build] @@ -333,58 +334,51 @@ defmodule Mix.Tasks.Deps.Compile do command = {"gleam", - ["compile-package", "--target", "erlang", "--package", package, "--out", out, "--lib", lib]} + [ + "compile-package", + "--no-beam", + "--target", + "erlang", + "--package", + package, + "--out", + out, + "--lib", + lib + ]} shell_cmd!(dep, config, command) - ebin = Path.join(out, "ebin") - app_file_path = Keyword.get(opts, :app, Path.join(ebin, "#{dep.app}.app")) - create_app_file = app_file_path && !File.exists?(app_file_path) + File.cd!(package, fn -> Mix.Gleam.load_config(".") end) + |> push_gleam_project(dep, Keyword.fetch!(config, :deps_path)) - if create_app_file do - generate_gleam_app_file(opts) - end - - Code.prepend_path(ebin, cache: true) + Code.prepend_path(Path.join(out, "ebin"), cache: true) end - defp gleam_extra_applications(config) do - config - |> Map.get(:extra_applications, []) - |> Enum.map(&String.to_atom/1) - end - - defp gleam_mod(config) do - case config[:mod] do - nil -> [] - mod -> {String.to_atom(mod), []} - end - end - - defp generate_gleam_app_file(opts) do - toml = File.cd!(opts[:dest], fn -> Mix.Gleam.load_config(".") end) - - module = - quote do - def project do - [ - app: unquote(toml.name) |> String.to_atom(), - version: "#{unquote(toml.version)}" - ] - end - - def application do - [ - mod: unquote(gleam_mod(toml)), - extra_applications: unquote(gleam_extra_applications(toml)) - ] - end - end - - module_name = String.to_atom("Gleam.#{toml.name}") - Module.create(module_name, module, Macro.Env.location(__ENV__)) - Mix.Project.push(module_name) - Mix.Tasks.Compile.App.run([]) + defp push_gleam_project(toml, dep, deps_path) do + build = Path.expand(dep.opts[:build]) + src = Path.join(build, "_gleam_artefacts") + File.mkdir(Path.join(build, "ebin")) + + config = + [ + app: dep.app, + version: toml.version, + deps: toml.deps, + build_per_environment: true, + lockfile: "mix.lock", + # Remove per-environment segment from the path since ProjectStack.push below will append it + build_path: Mix.Project.build_path() |> Path.split() |> Enum.drop(-1) |> Path.join(), + deps_path: deps_path, + erlc_paths: [src], + erlc_include_path: Path.join(build, "include") + ] + + Mix.ProjectStack.pop() + Mix.ProjectStack.push(dep.app, config, "nofile") + # Somehow running just `compile` task won't work (doesn't compile the .erl files) + Mix.Task.run("compile.erlang", ["--force"]) + Mix.Task.run("compile.app") end defp make_command(dep) do diff --git a/lib/mix/test/fixtures/gleam_dep/src/collocated_erlang.erl b/lib/mix/test/fixtures/gleam_dep/src/collocated_erlang.erl new file mode 100644 index 00000000000..ea2ed915e71 --- /dev/null +++ b/lib/mix/test/fixtures/gleam_dep/src/collocated_erlang.erl @@ -0,0 +1,5 @@ +-module(collocated_erlang). +-export([hello/0]). + +hello() -> + "Hello from Collocated Erlang!". diff --git a/lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam b/lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam index 673bfdd0147..4f11d986b22 100644 --- a/lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam +++ b/lib/mix/test/fixtures/gleam_dep/src/gleam_dep.gleam @@ -1,3 +1,6 @@ pub fn main() { True } + +@external(erlang, "collocated_erlang", "hello") +pub fn erl() -> String diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index c9de5f0e7cd..537e287c684 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -68,7 +68,9 @@ defmodule Mix.GleamTest do {:my_other_project, path: "../my_other_project"}, {:gleeunit, ">= 1.0.0 and < 2.0.0", only: :dev} ], - mod: "some@application" + application: [ + mod: {:some@application, []} + ] } end end @@ -81,10 +83,10 @@ defmodule Mix.GleamTest do Mix.Tasks.Deps.Get.run([]) assert_received {:mix_shell, :info, ["* Getting gleam_stdlib " <> _]} assert_received {:mix_shell, :info, ["* Getting gleam_otp " <> _]} - assert_received {:mix_shell, :info, ["* Getting gleeunit " <> _]} Mix.Tasks.Deps.Compile.run([]) assert :gleam_dep.main() + assert :gleam_dep.erl() == ~c'Hello from Collocated Erlang!' assert :gleam@int.to_string(1) == "1" {:ok, content} = :file.consult("_build/dev/lib/gleam_dep/ebin/gleam_dep.app") @@ -94,13 +96,15 @@ defmodule Mix.GleamTest do :application, :gleam_dep, [ - {:modules, [:gleam_dep]}, + {:modules, [:collocated_erlang, :gleam_dep]}, {:optional_applications, []}, - {:applications, [:kernel, :stdlib, :elixir, :ssl]}, + {:applications, + [:kernel, :stdlib, :elixir, :gleam_otp, :gleam_stdlib, :gleeunit]}, {:description, ~c"gleam_dep"}, {:registered, []}, - {:vsn, ~c"1.0.0"}, - {:mod, {:gleam_dep@somemodule, []}} + {:vsn, ~c"1.0.0"} + # Need to add support for :application option in Compile.App + # {:mod, {:gleam_dep@somemodule, []}} ] } ] From 8123710b2fffdf201963f468e564cfa41a3cc2d3 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/15] 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 63187a3d3a6..58759b7fb23 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 57610205439..753333c528d 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -334,18 +334,7 @@ defmodule Mix.Tasks.Deps.Compile do command = {"gleam", - [ - "compile-package", - "--no-beam", - "--target", - "erlang", - "--package", - package, - "--out", - out, - "--lib", - lib - ]} + ~w(compile-package --no-beam --target erlang --package #{package} --out #{out} --lib #{lib})} shell_cmd!(dep, config, command) diff --git a/lib/mix/lib/mix/tasks/deps.ex b/lib/mix/lib/mix/tasks/deps.ex index 11c0018cdfa..a23867a6025 100644 --- a/lib/mix/lib/mix/tasks/deps.ex +++ b/lib/mix/lib/mix/tasks/deps.ex @@ -101,7 +101,7 @@ defmodule Mix.Tasks.Deps do * `:override` - if set to `true` the dependency will override any other definitions of itself by other dependencies - * `:manager` - Mix can also compile Rebar3, makefile and gleam projects + * `:manager` - Mix can also compile Rebar3, makefile and Gleam projects and can fetch sub dependencies of Rebar3 projects. Mix will try to infer the type of project but it can be overridden with this option by setting it to `:mix`, `:rebar3`, `:make` or `:gleam`. In case 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 b75dc6140ea2bbf1c19ad48896873be0cf785f2e 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/15] 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 753333c528d..57610205439 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -334,7 +334,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 638b8a3e1b369b597d5d51df83d3e479c7ee3a61 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/15] Expand gleam deps deps paths Otherwise they are calculated from the current mix project and won't be found. --- lib/mix/lib/mix/gleam.ex | 2 +- lib/mix/lib/mix/tasks/deps.compile.ex | 1 + 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 +++++++++++++ .../deeper_gleam_dep/src/deeper_gleam_dep.gleam | 5 +++++ lib/mix/test/mix/gleam_test.exs | 11 ++++++----- lib/mix/test/test_helper.exs | 12 +++++++----- 10 files changed, 67 insertions(+), 12 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..fb871d9deaa 100644 --- a/lib/mix/lib/mix/gleam.ex +++ b/lib/mix/lib/mix/gleam.ex @@ -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 57610205439..0c23450643e 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -371,6 +371,7 @@ defmodule Mix.Tasks.Deps.Compile do build_path: Mix.Project.build_path() |> Path.split() |> Enum.drop(-1) |> Path.join(), deps_path: deps_path, erlc_paths: [src], + elixirc_paths: [src], erlc_include_path: Path.join(build, "include") ] 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..4bbbdcec7d3 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 @@ -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,7 +65,6 @@ 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} ], 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") @@ -108,6 +107,8 @@ defmodule Mix.GleamTest do ] } ] + + assert File.exists?("_build/dev/lib/deeper_gleam_dep/ebin/deeper_gleam_dep.app") end) end end diff --git a/lib/mix/test/test_helper.exs b/lib/mix/test/test_helper.exs index 77ac41ff1c8..e4f79d6cbc4 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -287,12 +287,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 2d8c285dfffcd73e744d1d64a4d83c9e67dc3fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20=C3=81lvarez?= Date: Fri, 2 May 2025 13:33:08 +0200 Subject: [PATCH 15/15] Add failing test to verify wrong loadpaths --- lib/mix/test/mix/gleam_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mix/test/mix/gleam_test.exs b/lib/mix/test/mix/gleam_test.exs index 4bbbdcec7d3..8b89977feff 100644 --- a/lib/mix/test/mix/gleam_test.exs +++ b/lib/mix/test/mix/gleam_test.exs @@ -109,6 +109,7 @@ defmodule Mix.GleamTest do ] assert File.exists?("_build/dev/lib/deeper_gleam_dep/ebin/deeper_gleam_dep.app") + assert :ok == Mix.Tasks.Deps.Loadpaths.run([]) end) end end