Repository: ninenines/erlang.mk
Pull request #1020: Native Elixir support
Author: login: essen
Created: 2024-12-05T15:52:12Z | Merged: 2025-03-18T09:02:04Z

Heavily modified version of https://github.com/ninenines/erlang.mk/pull/979


Diff:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 36325fe0..0ff2e4dc 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -20,6 +20,8 @@ jobs:
       matrix:
         erlang:
           - '27'
+        elixir:
+          - '1.17'
         os:
 #          - macos-latest
           - ubuntu-latest
@@ -31,6 +33,7 @@ jobs:
           - core-compat
           # Trick GH runners into picking this slower job early.
           - ACME=1 c=core-deps
+          - core-elixir
           - core-makedep
           - core-misc
           - core-plugins
@@ -78,6 +81,7 @@ jobs:
       uses: erlef/setup-beam@v1
       with:
         otp-version: ${{ matrix.erlang }}
+        elixir-version: ${{ matrix.elixir }}
         version-type: loose
 
     - name: Setup MSYS2 (Windows)
@@ -94,6 +98,10 @@ jobs:
           gnu-netcat
           diffutils
 
+    - name: Install libsodium (Linux, Elixir)
+      if: matrix.os == 'ubuntu-latest' && matrix.suite == 'core-elixir'
+      run: sudo apt-get -y install libsodium-dev
+
     - name: Run tests (Linux)
       if: matrix.os == 'ubuntu-latest'
       run: make check c=${{ matrix.suite }} -j4 -k ${{ matrix.extra }}
@@ -143,7 +151,7 @@ jobs:
       uses: erlef/setup-beam@v1
       with:
         otp-version: ${{ matrix.erlang }}
-        elixir-version: 1.18
+        elixir-version: '1.18'
         version-type: loose
 
     - name: Setup MSYS2 (Windows)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index fd127deb..c12cbe6b 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -64,3 +64,11 @@
             future update. Erlang.mk will always assume
             that a Rebar project uses Rebar3 from that
             point forward.
+
+2025/03/18: Native support for Elixir was added. Erlang.mk
+            will now properly handle most Elixir dependencies,
+            as well as compile Elixir code in Erlang.mk projects,
+            from both src/ and lib/ directories. This allows
+            mixing Erlang and Elixir in the same project.
+            Please refer to the user guide for more information.
+            Native Elixir support is considered experimental.
diff --git a/build.config b/build.config
index 5f1c1932..27b3dc9a 100644
--- a/build.config
+++ b/build.config
@@ -15,6 +15,7 @@ core/deps
 # Core modules, continued.
 core/beam-cache
 core/erlc
+core/elixir
 core/docs
 core/rel
 core/test
diff --git a/core/core.mk b/core/core.mk
index 5d5eca00..222faebf 100644
--- a/core/core.mk
+++ b/core/core.mk
@@ -36,7 +36,7 @@ PROJECT ?= $(notdir $(CURDIR))
 PROJECT := $(strip $(PROJECT))
 
 PROJECT_VERSION ?= rolling
-PROJECT_MOD ?= $(PROJECT)_app
+PROJECT_MOD ?=
 PROJECT_ENV ?= []
 
 # Verbosity.
diff --git a/core/deps-tools.mk b/core/deps-tools.mk
index 47f3b8e8..b29bf2a5 100644
--- a/core/deps-tools.mk
+++ b/core/deps-tools.mk
@@ -78,9 +78,7 @@ endif
 ifeq ($(IS_APP)$(IS_DEP),)
 	$(verbose) sort < $(ERLANG_MK_RECURSIVE_TMP_LIST) | \
 		uniq > $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted
-	$(verbose) cmp -s $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted $@ \
-		|| mv $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted $@
-	$(verbose) rm -f $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted
+	$(verbose) mv $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted $@
 	$(verbose) rm $(ERLANG_MK_RECURSIVE_TMP_LIST)
 endif
 endif # ifneq ($(SKIP_DEPS),)
diff --git a/core/deps.mk b/core/deps.mk
index eb4685c8..d2719e5c 100644
--- a/core/deps.mk
+++ b/core/deps.mk
@@ -118,6 +118,8 @@ dep_name = $(call query_name,$(1))
 # Application directories.
 
 LOCAL_DEPS_DIRS = $(foreach a,$(LOCAL_DEPS),$(if $(wildcard $(APPS_DIR)/$a),$(APPS_DIR)/$a))
+# Elixir is handled specially as it must be built before all other deps
+# when Mix autopatching is necessary.
 ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(BUILD_DEPS) $(DEPS)),$(call query_name,$(dep))))
 
 # When we are calling an app directly we don't want to include it here
@@ -210,9 +212,11 @@ endif
 ifneq ($(SKIP_DEPS),)
 deps::
 else
-deps:: $(ALL_DEPS_DIRS) apps clean-tmp-deps.log | $(ERLANG_MK_TMP)
-ifneq ($(ALL_DEPS_DIRS),)
-	$(verbose) set -e; for dep in $(ALL_DEPS_DIRS); do \
+ALL_DEPS_DIRS_TO_BUILD = $(if $(filter-out $(DEPS_DIR)/elixir,$(ALL_DEPS_DIRS)),$(filter-out $(DEPS_DIR)/elixir,$(ALL_DEPS_DIRS)),$(ALL_DEPS_DIRS))
+
+deps:: $(ALL_DEPS_DIRS_TO_BUILD) apps clean-tmp-deps.log | $(ERLANG_MK_TMP)
+ifneq ($(ALL_DEPS_DIRS_TO_BUILD),)
+	$(verbose) set -e; for dep in $(ALL_DEPS_DIRS_TO_BUILD); do \
 		if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/deps.log; then \
 			:; \
 		else \
@@ -236,34 +240,47 @@ endif
 
 # Deps related targets.
 
-# @todo rename GNUmakefile and makefile into Makefile first, if they exist
-# While Makefile file could be GNUmakefile or makefile,
-# in practice only Makefile is needed so far.
-define dep_autopatch
+autopatch_verbose_0 = @echo " PATCH " $(subst autopatch-,,$@) "(method: $(AUTOPATCH_METHOD))";
+autopatch_verbose_2 = set -x;
+autopatch_verbose = $(autopatch_verbose_$(V))
+
+define dep_autopatch_detect
 	if [ -f $(DEPS_DIR)/$1/erlang.mk ]; then \
-		rm -rf $(DEPS_DIR)/$1/ebin/; \
-		$(call erlang,$(call dep_autopatch_appsrc.erl,$1)); \
-		$(call dep_autopatch_erlang_mk,$1); \
+		echo erlang.mk; \
+	elif [ -f $(DEPS_DIR)/$1/mix.exs -a -d $(DEPS_DIR)/$1/lib ]; then \
+		if [ "$(ELIXIR)" != "disable" ]; then \
+			echo mix; \
+		elif [ -f $(DEPS_DIR)/$1/rebar.lock -o -f $(DEPS_DIR)/$1/rebar.config ]; then \
+			echo rebar3; \
+		else \
+			exit 99; \
+		fi \
 	elif [ -f $(DEPS_DIR)/$1/Makefile ]; then \
 		if [ -f $(DEPS_DIR)/$1/rebar.lock ]; then \
-			$(call dep_autopatch2,$1); \
-		elif [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$1/Makefile` ]; then \
-			$(call dep_autopatch2,$1); \
-		elif [ 0 != `grep -ci "^[^#].*rebar" $(DEPS_DIR)/$1/Makefile` ]; then \
-			$(call dep_autopatch2,$1); \
-		elif [ -n "`find $(DEPS_DIR)/$1/ -type f -name \*.mk -not -name erlang.mk -exec grep -i "^[^#].*rebar" '{}' \;`" ]; then \
-			$(call dep_autopatch2,$1); \
-		fi \
-	else \
-		if [ ! -d $(DEPS_DIR)/$1/src/ ]; then \
-			$(call dep_autopatch_noop,$1); \
+			echo rebar3; \
+		elif [ 0 != \`grep -c "include ../\w*\.mk" $(DEPS_DIR)/$1/Makefile\` ]; then \
+			echo rebar3; \
+		elif [ 0 != \`grep -ci "^[^#].*rebar" $(DEPS_DIR)/$1/Makefile\` ]; then \
+			echo rebar3; \
+		elif [ -n "\`find $(DEPS_DIR)/$1/ -type f -name \*.mk -not -name erlang.mk -exec grep -i "^[^#].*rebar" '{}' \;\`" ]; then \
+			echo rebar3; \
 		else \
-			$(call dep_autopatch2,$1); \
+			echo noop; \
 		fi \
+	elif [ ! -d $(DEPS_DIR)/$1/src/ ]; then \
+		echo noop; \
+	else \
+		echo rebar3; \
 	fi
 endef
 
-define dep_autopatch2
+define dep_autopatch_for_erlang.mk
+	rm -rf $(DEPS_DIR)/$1/ebin/; \
+	$(call erlang,$(call dep_autopatch_appsrc.erl,$1)); \
+	$(call dep_autopatch_erlang_mk,$1)
+endef
+
+define dep_autopatch_for_rebar3
 	! test -f $(DEPS_DIR)/$1/ebin/$1.app || \
 	mv -n $(DEPS_DIR)/$1/ebin/$1.app $(DEPS_DIR)/$1/src/$1.app.src; \
 	rm -f $(DEPS_DIR)/$1/ebin/$1.app; \
@@ -279,8 +296,22 @@ define dep_autopatch2
 	fi
 endef
 
-define dep_autopatch_noop
-	printf "noop:\n" > $(DEPS_DIR)/$1/Makefile
+define dep_autopatch_for_mix
+	$(call dep_autopatch_mix,$1)
+endef
+
+define dep_autopatch_for_noop
+	test -f $(DEPS_DIR)/$1/Makefile || printf "noop:\n" > $(DEPS_DIR)/$1/Makefile
+endef
+
+define maybe_flock
+	if command -v flock >/dev/null; then \
+		flock $1 sh -c "$2"; \
+	elif command -v lockf >/dev/null; then \
+		lockf $1 sh -c "$2"; \
+	else \
+		$2; \
+	fi
 endef
 
 # Replace "include erlang.mk" with a line that will load the parent Erlang.mk
@@ -307,13 +338,7 @@ endef
 
 # We use flock/lockf when available to avoid concurrency issues.
 define dep_autopatch_fetch_rebar
-	if command -v flock >/dev/null; then \
-		flock $(ERLANG_MK_TMP)/rebar.lock sh -c "$(call dep_autopatch_fetch_rebar2)"; \
-	elif command -v lockf >/dev/null; then \
-		lockf $(ERLANG_MK_TMP)/rebar.lock sh -c "$(call dep_autopatch_fetch_rebar2)"; \
-	else \
-		$(call dep_autopatch_fetch_rebar2); \
-	fi
+	$(call maybe_flock,$(ERLANG_MK_TMP)/rebar.lock,$(call dep_autopatch_fetch_rebar2))
 endef
 
 define dep_autopatch_fetch_rebar2
@@ -397,7 +422,6 @@ define dep_autopatch_rebar.erl
 	GetHexVsn2 = fun(N, NP) ->
 		case file:consult("$(call core_native_path,$(DEPS_DIR)/$1/rebar.lock)") of
 			{ok, Lock} ->
-				io:format("~p~n", [Lock]),
 				LockPkgs = case lists:keyfind("1.2.0", 1, Lock) of
 					{_, LP} ->
 						LP;
@@ -411,10 +435,8 @@ define dep_autopatch_rebar.erl
 				end,
 				if
 					is_list(LockPkgs) ->
-						io:format("~p~n", [LockPkgs]),
 						case lists:keyfind(atom_to_binary(N, latin1), 1, LockPkgs) of
 							{_, {pkg, _, Vsn}, _} ->
-								io:format("~p~n", [Vsn]),
 								{N, {hex, NP, binary_to_list(Vsn)}};
 							_ ->
 								false
@@ -835,7 +857,7 @@ define dep_fetch_fail
 endef
 
 define dep_target
-$(DEPS_DIR)/$(call query_name,$1): | $(if $(filter hex,$(call query_fetch_method,$1)),hex-core) $(ERLANG_MK_TMP)
+$(DEPS_DIR)/$(call query_name,$1): $(if $(filter elixir,$(BUILD_DEPS) $(DEPS)),$(if $(filter-out elixir,$1),$(DEPS_DIR)/elixir/ebin/dep_built)) $(if $(filter hex,$(call query_fetch_method,$1)),$(if $(wildcard $(DEPS_DIR)/$(call query_name,$1)),,$(DEPS_DIR)/hex_core/ebin/dep_built)) | $(ERLANG_MK_TMP)
 	$(eval DEP_NAME := $(call query_name,$1))
 	$(eval DEP_STR := $(if $(filter $1,$(DEP_NAME)),$1,"$1 ($(DEP_NAME))"))
 	$(verbose) if test -d $(APPS_DIR)/$(DEP_NAME); then \
@@ -854,30 +876,44 @@ $(DEPS_DIR)/$(call query_name,$1): | $(if $(filter hex,$(call query_fetch_method
 		cd $(DEPS_DIR)/$(DEP_NAME) && ./configure; \
 	fi
 ifeq ($(filter $1,$(NO_AUTOPATCH)),)
-	$(verbose) $$(MAKE) --no-print-directory autopatch-$(DEP_NAME)
+	$(verbose) AUTOPATCH_METHOD=`$(call dep_autopatch_detect,$1)`; \
+	if [ $$$$? -eq 99 ]; then \
+		echo "Elixir is currently disabled. Please set 'ELIXIR = system' in the Makefile to enable"; \
+		exit 99; \
+	fi; \
+	$$(MAKE) --no-print-directory autopatch-$(DEP_NAME) AUTOPATCH_METHOD=$$$$AUTOPATCH_METHOD
 endif
 
 .PHONY: autopatch-$(call query_name,$1)
 
+ifeq ($1,elixir)
+autopatch-elixir::
+	$$(verbose) ln -s lib/elixir/ebin $(DEPS_DIR)/elixir/
+else
 autopatch-$(call query_name,$1)::
-	$(verbose) if [ "$1" = "elixir" -a "$(ELIXIR_PATCH)" ]; then \
-		ln -s lib/elixir/ebin $(DEPS_DIR)/elixir/; \
-	else \
-		$$(call dep_autopatch,$(call query_name,$1)) \
-	fi
+	$$(autopatch_verbose) $$(call dep_autopatch_for_$(AUTOPATCH_METHOD),$(call query_name,$1))
+endif
 endef
 
 # We automatically depend on hex_core when the project isn't already.
 $(if $(filter hex_core,$(DEPS) $(BUILD_DEPS) $(DOC_DEPS) $(REL_DEPS) $(TEST_DEPS)),,\
 	$(eval $(call dep_target,hex_core)))
 
-.PHONY: hex-core
-
-hex-core: $(DEPS_DIR)/hex_core
-	$(verbose) if [ ! -e $(DEPS_DIR)/hex_core/ebin/dep_built ]; then \
-		$(MAKE) -C $(DEPS_DIR)/hex_core IS_DEP=1; \
-		touch $(DEPS_DIR)/hex_core/ebin/dep_built; \
-	fi
+$(DEPS_DIR)/hex_core/ebin/dep_built: | $(ERLANG_MK_TMP)
+	$(verbose) $(call maybe_flock,$(ERLANG_MK_TMP)/hex_core.lock,\
+		if [ ! -e $(DEPS_DIR)/hex_core/ebin/dep_built ]; then \
+			$(MAKE) $(DEPS_DIR)/hex_core; \
+			$(MAKE) -C $(DEPS_DIR)/hex_core IS_DEP=1; \
+			touch $(DEPS_DIR)/hex_core/ebin/dep_built; \
+		fi)
+
+$(DEPS_DIR)/elixir/ebin/dep_built: | $(ERLANG_MK_TMP)
+	$(verbose) $(call maybe_flock,$(ERLANG_MK_TMP)/elixir.lock,\
+		if [ ! -e $(DEPS_DIR)/elixir/ebin/dep_built ]; then \
+			$(MAKE) $(DEPS_DIR)/elixir; \
+			$(MAKE) -C $(DEPS_DIR)/elixir; \
+			touch $(DEPS_DIR)/elixir/ebin/dep_built; \
+		fi)
 
 $(foreach dep,$(BUILD_DEPS) $(DEPS),$(eval $(call dep_target,$(dep))))
 
diff --git a/core/elixir.mk b/core/elixir.mk
new file mode 100644
index 00000000..8d1dba95
--- /dev/null
+++ b/core/elixir.mk
@@ -0,0 +1,201 @@
+# Copyright (c) 2024, Tyler Hughes <tyler@tylerhughes.dev>
+# Copyright (c) 2024, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+# Elixir is automatically enabled in all cases except when
+# an Erlang project uses an Elixir dependency. In that case
+# $(ELIXIR) must be set explicitly.
+ELIXIR ?= $(if $(filter elixir,$(BUILD_DEPS) $(DEPS)),dep,$(if $(EX_FILES),system,disable))
+export ELIXIR
+
+ifeq ($(ELIXIR),system)
+# We expect 'elixir' to be on the path.
+ELIXIR_LIBS ?= $(dir $(shell readlink -f `which elixir`))/../lib
+ELIXIR_LIBS := $(ELIXIR_LIBS)
+export ELIXIR_LIBS
+ERL_LIBS := $(ERL_LIBS):$(ELIXIR_LIBS)
+else
+ifeq ($(ELIXIR),dep)
+ERL_LIBS := $(ERL_LIBS):$(DEPS_DIR)/elixir/lib/
+endif
+endif
+
+elixirc_verbose_0 = @echo " EXC    $(words $(EX_FILES)) files";
+elixirc_verbose_2 = set -x;
+elixirc_verbose = $(elixirc_verbose_$(V))
+
+# Unfortunately this currently requires Elixir.
+# https://github.com/jelly-beam/verl is a good choice
+# for an Erlang implementation, but we already have to
+# pull hex_core and Rebar3 so adding yet another pull
+# is annoying, especially one that would be necessary
+# every time we autopatch Rebar projects. Wait and see.
+define hex_version_resolver.erl
+	HexVersionResolve = fun(Name, Req) ->
+		application:ensure_all_started(ssl),
+		application:ensure_all_started(inets),
+		Config = $(hex_config.erl),
+		case hex_repo:get_package(Config, atom_to_binary(Name)) of
+			{ok, {200, _RespHeaders, Package}} ->
+				#{releases := List} = Package,
+				{value, #{version := Version}} = lists:search(fun(#{version := Vsn}) ->
+					M = list_to_atom("Elixir.Version"),
+					F = list_to_atom("match?"),
+					M:F(Vsn, Req)
+				end, List),
+				{ok, Version};
+			{ok, {Status, _, Errors}} ->
+				{error, Status, Errors}
+		end
+	end,
+	HexVersionResolveAndPrint = fun(Name, Req) ->
+		case HexVersionResolve(Name, Req) of
+			{ok, Version} ->
+				io:format("~s", [Version]),
+				halt(0);
+			{error, Status, Errors} ->
+				io:format("Error ~b: ~0p~n", [Status, Errors]),
+				halt(77)
+		end
+	end
+endef
+
+define dep_autopatch_mix.erl
+	$(call hex_version_resolver.erl),
+	{ok, _} = application:ensure_all_started(elixir),
+	{ok, _} = application:ensure_all_started(mix),
+	MixFile = <<"$(call core_native_path,$(DEPS_DIR)/$1/mix.exs)">>,
+	{Mod, Bin} =
+		case elixir_compiler:file(MixFile, fun(_File, _LexerPid) -> ok end) of
+			[{T = {_, _}, _CheckerPid}] -> T;
+			[T = {_, _}] -> T
+		end,
+	{module, Mod} = code:load_binary(Mod, binary_to_list(MixFile), Bin),
+	Project = Mod:project(),
+	Application = try Mod:application() catch error:undef -> [] end,
+	StartMod = case lists:keyfind(mod, 1, Application) of
+		{mod, {StartMod0, _StartArgs}} ->
+			atom_to_list(StartMod0);
+		_ ->
+			""
+	end,
+	Write = fun (Text) ->
+		file:write_file("$(call core_native_path,$(DEPS_DIR)/$1/Makefile)", Text, [append])
+	end,
+	Write([
+		"PROJECT = ", atom_to_list(proplists:get_value(app, Project)), "\n"
+		"PROJECT_DESCRIPTION = ", proplists:get_value(description, Project, ""), "\n"
+		"PROJECT_VERSION = ", proplists:get_value(version, Project, ""), "\n"
+		"PROJECT_MOD = ", StartMod, "\n"
+		"define PROJECT_ENV\n",
+		io_lib:format("~p", [proplists:get_value(env, Application, [])]), "\n"
+		"endef\n\n"]),
+	ExtraApps = lists:usort([eex, elixir, logger, mix] ++ proplists:get_value(extra_applications, Application, [])),
+	Write(["LOCAL_DEPS += ", lists:join(" ", [atom_to_list(App) || App <- ExtraApps]), "\n\n"]),
+	Deps = proplists:get_value(deps, Project, []) -- [elixir_make],
+	IsRequiredProdDep = fun(Opts) ->
+		(proplists:get_value(optional, Opts) =/= true)
+		andalso
+		case proplists:get_value(only, Opts, prod) of
+			prod -> true;
+			L when is_list(L) -> lists:member(prod, L);
+			_ -> false
+		end
+	end,
+	lists:foreach(fun
+		({Name, Req}) when is_binary(Req) ->
+			{ok, Vsn} = HexVersionResolve(Name, Req),
+			Write(["DEPS += ", atom_to_list(Name), "\n"]),
+			Write(["dep_", atom_to_list(Name), " = hex ", Vsn, " ", atom_to_list(Name), "\n"]);
+		({Name, Opts}) when is_list(Opts) ->
+			Path = proplists:get_value(path, Opts),
+			case IsRequiredProdDep(Opts) of
+				true when Path =/= undefined ->
+					Write(["DEPS += ", atom_to_list(Name), "\n"]),
+					Write(["dep_", atom_to_list(Name), " = ln ", Path, "\n"]);
+				true when Path =:= undefined ->
+					Write(["DEPS += ", atom_to_list(Name), "\n"]),
+					io:format(standard_error, "Warning: No version given for ~p.", [Name]);
+				false ->
+					ok
+			end;
+		({Name, Req, Opts}) ->
+			case IsRequiredProdDep(Opts) of
+				true ->
+					{ok, Vsn} = HexVersionResolve(Name, Req),
+					Write(["DEPS += ", atom_to_list(Name), "\n"]),
+					Write(["dep_", atom_to_list(Name), " = hex ", Vsn, " ", atom_to_list(Name), "\n"]);
+				false ->
+					ok
+			end;
+		(_) ->
+			ok
+	end, Deps),
+	case lists:member(elixir_make, proplists:get_value(compilers, Project, [])) of
+		false -> 
+			ok;
+		true ->
+			Write("# https://hexdocs.pm/elixir_make/Mix.Tasks.Compile.ElixirMake.html\n"),
+			MakeVal = fun(Key, Proplist, DefaultVal, DefaultReplacement) ->
+				case proplists:get_value(Key, Proplist, DefaultVal) of
+					DefaultVal -> DefaultReplacement;
+					Value -> Value
+				end
+			end,
+			MakeMakefile = binary_to_list(MakeVal(make_makefile, Project, default, <<"Makefile">>)),
+			MakeExe = MakeVal(make_executable, Project, default, "$$\(MAKE)"),
+			MakeCwd = MakeVal(make_cwd, Project, undefined, <<".">>),
+			MakeTargets = MakeVal(make_targets, Project, [], []),
+			MakeArgs = MakeVal(make_args, Project, undefined, []),
+			case file:rename("$(DEPS_DIR)/$1/" ++ MakeMakefile, "$(DEPS_DIR)/$1/elixir_make.mk") of
+				ok -> ok;
+				Err = {error, _} ->
+					io:format(standard_error, "Failed to copy Makefile with error ~p~n", [Err]),
+					halt(90)
+			end,
+			Write(["app::\n"
+				"\t", MakeExe, " -C ", MakeCwd, " -f $(DEPS_DIR)/$1/elixir_make.mk",
+				lists:join(" ", MakeTargets),
+				lists:join(" ", MakeArgs),
+				"\n\n"]),
+			case MakeVal(make_clean, Project, nil, undefined) of
+				undefined ->
+					ok;
+				Clean ->
+					Write(["clean::\n\t", Clean, "\n\n"])
+			end
+	end,
+	Write("ERLC_OPTS = +debug_info\n\n"),
+	Write("include $$\(if $$\(ERLANG_MK_FILENAME),$$\(ERLANG_MK_FILENAME),erlang.mk)"),
+	halt()
+endef
+
+define dep_autopatch_mix
+	sed 's|\(defmodule.*do\)|\1\n  try do\n    Code.compiler_options(on_undefined_variable: :warn)\n    rescue _ -> :ok\n  end\n|g' -i $(DEPS_DIR)/$(1)/mix.exs; \
+	$(MAKE) $(DEPS_DIR)/hex_core/ebin/dep_built; \
+	MIX_ENV="$(if $(MIX_ENV),$(strip $(MIX_ENV)),prod)" \
+		$(call erlang,$(call dep_autopatch_mix.erl,$1))
+endef
+
+# We change the group leader so the Elixir io:format output
+# isn't captured as we need to either print the modules on
+# success, or print _ERROR_ on failure.
+define compile_ex.erl
+	{ok, _} = application:ensure_all_started(elixir),
+	{ok, _} = application:ensure_all_started(mix),
+	ModCode = list_to_atom("Elixir.Code"),
+	ModCode:put_compiler_option(ignore_module_conflict, true),
+	ModComp = list_to_atom("Elixir.Kernel.ParallelCompiler"),
+	ModMixProject = list_to_atom("Elixir.Mix.Project"),
+	erlang:group_leader(whereis(standard_error), self()),
+	ModMixProject:in_project($(PROJECT), ".", [], fun(_MixFile) ->
+		case ModComp:compile_to_path([$(call comma_list,$(patsubst %,<<"%">>,$1))], <<"ebin/">>) of
+			{ok, Modules, _} ->
+				lists:foreach(fun(E) -> io:format(user, "~p ", [E]) end, Modules),
+				halt(0);
+			{error, _ErroredModules, _WarnedModules} ->
+				io:format(user, "_ERROR_", []),
+				halt(1)
+		end
+	end)
+endef
diff --git a/core/erlc.mk b/core/erlc.mk
index f3b499a6..c956c4d9 100644
--- a/core/erlc.mk
+++ b/core/erlc.mk
@@ -49,7 +49,7 @@ mib_verbose_0 = @echo " MIB   " $(filter %.bin %.mib,$(?F));
 mib_verbose_2 = set -x;
 mib_verbose = $(mib_verbose_$(V))
 
-ifneq ($(wildcard src/),)
+ifneq ($(wildcard src/)$(wildcard lib/),)
 
 # Targets.
 
@@ -57,34 +57,21 @@ app:: $(if $(wildcard ebin/test),beam-cache-restore-app) deps
 	$(verbose) $(MAKE) --no-print-directory $(PROJECT).d
 	$(verbose) $(MAKE) --no-print-directory app-build
 
-ifeq ($(wildcard src/$(PROJECT_MOD).erl),)
-define app_file
-{application, '$(PROJECT)', [
-	{description, "$(PROJECT_DESCRIPTION)"},
-	{vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP),
-	{id$(comma)$(space)"$1"}$(comma))
-	{modules, [$(call comma_list,$2)]},
-	{registered, []},
-	{applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(OPTIONAL_DEPS) $(foreach dep,$(DEPS),$(call query_name,$(dep))))]},
-	{optional_applications, [$(call comma_list,$(OPTIONAL_DEPS))]},
-	{env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),)
-]}.
-endef
-else
+PROJECT_MOD := $(if $(PROJECT_MOD),$(PROJECT_MOD),$(if $(wildcard src/$(PROJECT)_app.erl),$(PROJECT)_app))
+
 define app_file
 {application, '$(PROJECT)', [
 	{description, "$(PROJECT_DESCRIPTION)"},
 	{vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP),
 	{id$(comma)$(space)"$1"}$(comma))
 	{modules, [$(call comma_list,$2)]},
-	{registered, [$(call comma_list,$(PROJECT)_sup $(PROJECT_REGISTERED))]},
+	{registered, [$(if $(PROJECT_MOD),$(call comma_list,$(if $(filter $(PROJECT_MOD),$(PROJECT)_app),$(PROJECT)_sup) $(PROJECT_REGISTERED)))]},
 	{applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(OPTIONAL_DEPS) $(foreach dep,$(DEPS),$(call query_name,$(dep))))]},
-	{optional_applications, [$(call comma_list,$(OPTIONAL_DEPS))]},
-	{mod, {$(PROJECT_MOD), []}},
+	{optional_applications, [$(call comma_list,$(OPTIONAL_DEPS))]},$(if $(PROJECT_MOD),
+	{mod$(comma)$(space){$(patsubst %,'%',$(PROJECT_MOD))$(comma)$(space)[]}}$(comma))
 	{env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),)
 ]}.
 endef
-endif
 
 app-build: ebin/$(PROJECT).app
 	$(verbose) :
@@ -96,6 +83,9 @@ ALL_SRC_FILES := $(sort $(call core_find,src/,*))
 ERL_FILES := $(filter %.erl,$(ALL_SRC_FILES))
 CORE_FILES := $(filter %.core,$(ALL_SRC_FILES))
 
+ALL_LIB_FILES := $(sort $(call core_find,lib/,*))
+EX_FILES := $(filter-out lib/mix/%,$(filter %.ex,$(ALL_SRC_FILES) $(ALL_LIB_FILES)))
+
 # ASN.1 files.
 
 ifneq ($(wildcard asn1/),)
@@ -271,21 +261,21 @@ define makedep.erl
 endef
 
 ifeq ($(if $(NO_MAKEDEP),$(wildcard $(PROJECT).d),),)
-$(PROJECT).d:: $(ERL_FILES) $(call core_find,include/,*.hrl) $(MAKEFILE_LIST)
+$(PROJECT).d:: $(ERL_FILES) $(EX_FILES) $(call core_find,include/,*.hrl) $(MAKEFILE_LIST)
 	$(makedep_verbose) $(call erlang,$(call makedep.erl,$@))
 endif
 
 ifeq ($(IS_APP)$(IS_DEP),)
-ifneq ($(words $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES)),0)
+ifneq ($(words $(ERL_FILES) $(EX_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES) $(EX_FILES)),0)
 # Rebuild everything when the Makefile changes.
 $(ERLANG_MK_TMP)/last-makefile-change: $(MAKEFILE_LIST) | $(ERLANG_MK_TMP)
 	$(verbose) if test -f $@; then \
-		touch $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES); \
+		touch $(ERL_FILES) $(EX_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES) $(EX_FILES); \
 		touch -c $(PROJECT).d; \
 	fi
 	$(verbose) touch $@
 
-$(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(ERLANG_MK_TMP)/last-makefile-change
+$(ERL_FILES) $(EX_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(ERLANG_MK_TMP)/last-makefile-change
 ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change
 endif
 endif
@@ -312,13 +302,16 @@ define validate_app_file
 	end
 endef
 
-ebin/$(PROJECT).app:: $(ERL_FILES) $(CORE_FILES) $(wildcard src/$(PROJECT).app.src)
-	$(eval FILES_TO_COMPILE := $(filter-out src/$(PROJECT).app.src,$?))
+ebin/$(PROJECT).app:: $(ERL_FILES) $(CORE_FILES) $(wildcard src/$(PROJECT).app.src) $(EX_FILES)
+	$(eval FILES_TO_COMPILE := $(filter-out $(EX_FILES) src/$(PROJECT).app.src,$?))
 	$(if $(strip $(FILES_TO_COMPILE)),$(call compile_erl,$(FILES_TO_COMPILE)))
+	$(if $(filter $(ELIXIR),disable),,$(if $(filter $?,$(EX_FILES)),$(elixirc_verbose) $(eval MODULES := $(shell $(call erlang,$(call compile_ex.erl,$(EX_FILES)))))))
+	$(eval ELIXIR_COMP_FAILED := $(if $(filter _ERROR_,$(firstword $(MODULES))),true,false))
 # Older git versions do not have the --first-parent flag. Do without in that case.
+	$(verbose) if $(ELIXIR_COMP_FAILED); then exit 1; fi
 	$(eval GITDESCRIBE := $(shell git describe --dirty --abbrev=7 --tags --always --first-parent 2>/dev/null \
 		|| git describe --dirty --abbrev=7 --tags --always 2>/dev/null || true))
-	$(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \
+	$(eval MODULES := $(MODULES) $(patsubst %,'%',$(sort $(notdir $(basename \
 		$(filter-out $(ERLC_EXCLUDE_PATHS),$(ERL_FILES) $(CORE_FILES) $(BEAM_FILES)))))))
 ifeq ($(wildcard src/$(PROJECT).app.src),)
 	$(app_verbose) printf '$(subst %,%%,$(subst $(newline),\n,$(subst ','\'',$(call app_file,$(GITDESCRIBE),$(MODULES)))))' \
diff --git a/doc/src/guide/book.asciidoc b/doc/src/guide/book.asciidoc
index 7c4ee68c..65c09b95 100644
--- a/doc/src/guide/book.asciidoc
+++ b/doc/src/guide/book.asciidoc
@@ -36,6 +36,8 @@ include::cross_compiling.asciidoc[Cross compiling]
 
 include::compat.asciidoc[Compatibility with other build tools]
 
+include::elixir.asciidoc[Elixir modules and dependencies]
+
 [[docs]]
 = Documentation
 
diff --git a/doc/src/guide/elixir.asciidoc b/doc/src/guide/elixir.asciidoc
new file mode 100644
index 00000000..b868825f
--- /dev/null
+++ b/doc/src/guide/elixir.asciidoc
@@ -0,0 +1,86 @@
+[[elixir]]
+== Elixir modules and dependencies
+
+Erlang.mk has experimental support for building Elixir
+modules as well as dependencies. In this chapter we will
+cover the details and gotchas of the Elixir support.
+
+=== Selecting Elixir
+
+By default Elixir is disabled. This is to ensure that
+there's no negative impact to normal users of Erlang.mk.
+
+Erlang.mk can use either an Elixir installed in the
+system; or use Elixir as a dependency.
+
+Elixir will be automatically enabled when Elixir is
+used as a dependency. In that case all that is required
+is to depend on Elixir:
+
+[source,make]
+----
+DEPS = elixir
+dep_elixir_commit = v1.17.3
+----
+
+Alternatively, Erlang.mk will enable the system Elixir
+installation when Elixir files are found in the top-level
+project. In that case, Elixir is assumed to be in the path.
+
+In other cases, the system Elixir installation must be
+enabled manually in order to depend on Elixir applications.
+Note that this is only required for Elixir-only applications,
+not for applications that have both Erlang and Elixir code
+(as long as you only care about the Erlang side of things).
+The `ELIXIR` variable must be defined before including
+Erlang.mk:
+
+[source,make]
+ELIXIR = system
+
+Elixir can be explicitly disabled. In that case trying to
+depend on Elixir applications will result in failure
+during autopatch, unless the Elixir application has both
+Erlang and Elixir code.
+
+[source,make]
+ELIXIR = disable
+
+=== Elixir compilation
+
+There are currently no options.
+
+=== Elixir dependencies
+
+Erlang.mk will automatically autopatch Elixir dependencies
+by running Mix on the mix.exs file and producing a Makefile
+using the generated metadata.
+
+The following is an example of depending on Elixir
+applications from an Erlang-only application:
+
+[source,make]
+----
+DEPS = jose
+dep_jose = hex 1.11.10
+
+ELIXIR = system
+
+include erlang.mk
+----
+
+=== Dialyzer
+
+Dialyzer requires Elixir to be available in order to access
+the AST of the Elixir beam files. In most cases it will just
+work. When only enabling Elixir in a sub-application, Elixir
+will not always be available, so in that case we must tell
+Dialyzer where to find Elixir libraries. This can be done
+by adding the following rules to the relevant Makefiles,
+either as a plugin or after including Erlang.mk:
+
+[source,make]
+----
+dialyze: ELIXIR_LIBS = $(dir $(shell readlink -f `which elixir`))/../lib
+dialyze: ERL_LIBS = $(APPS_DIR):$(DEPS_DIR):$(ELIXIR_LIBS)
+----
diff --git a/index/elixir.mk b/index/elixir.mk
new file mode 100644
index 00000000..86f89e79
--- /dev/null
+++ b/index/elixir.mk
@@ -0,0 +1,7 @@
+PACKAGES += elixir
+pkg_elixir_name = elixir
+pkg_elixir_description = Elixir is a dynamic, functional language for building scalable and maintainable applications.
+pkg_elixir_homepage = https://elixir-lang.org
+pkg_elixir_fetch = git
+pkg_elixir_repo = https://github.com/elixir-lang/elixir
+pkg_elixir_commit = main
diff --git a/plugins/escript.mk b/plugins/escript.mk
index 1790dcbc..4eac352f 100644
--- a/plugins/escript.mk
+++ b/plugins/escript.mk
@@ -27,17 +27,45 @@ help::
 
 # Plugin-specific targets.
 
-escript-zip:: FULL=1
-escript-zip:: deps app
+ALL_ESCRIPT_DEPS_DIRS = $(LOCAL_DEPS_DIRS) $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(DEPS)),$(call query_name,$(dep))))
+
+ESCRIPT_RUNTIME_DEPS_FILE ?= $(ERLANG_MK_TMP)/escript-deps.log
+
+escript-list-runtime-deps:
+ifeq ($(IS_DEP),)
+	$(verbose) rm -f $(ESCRIPT_RUNTIME_DEPS_FILE)
+endif
+	$(verbose) touch $(ESCRIPT_RUNTIME_DEPS_FILE)
+	$(verbose) set -e; for dep in $(ALL_ESCRIPT_DEPS_DIRS) ; do \
+		if ! grep -qs ^$$dep$$ $(ESCRIPT_RUNTIME_DEPS_FILE); then \
+			echo $$dep >> $(ESCRIPT_RUNTIME_DEPS_FILE); \
+			if grep -qs -E "^[[:blank:]]*include[[:blank:]]+(erlang\.mk|.*/erlang\.mk|.*ERLANG_MK_FILENAME.*)$$" \
+			 $$dep/GNUmakefile $$dep/makefile $$dep/Makefile; then \
+				$(MAKE) -C $$dep escript-list-runtime-deps \
+				 IS_DEP=1 \
+				 ESCRIPT_RUNTIME_DEPS_FILE=$(ESCRIPT_RUNTIME_DEPS_FILE); \
+			fi \
+		fi \
+	done
+ifeq ($(IS_DEP),)
+	$(verbose) sort < $(ESCRIPT_RUNTIME_DEPS_FILE) | uniq > $(ESCRIPT_RUNTIME_DEPS_FILE).sorted
+	$(verbose) mv $(ESCRIPT_RUNTIME_DEPS_FILE).sorted $(ESCRIPT_RUNTIME_DEPS_FILE)
+endif
+
+escript-prepare: deps app
+	$(MAKE) escript-list-runtime-deps
+
+escript-zip:: escript-prepare
 	$(verbose) mkdir -p $(dir $(abspath $(ESCRIPT_ZIP_FILE)))
 	$(verbose) rm -f $(abspath $(ESCRIPT_ZIP_FILE))
-	$(gen_verbose) cd .. && $(ESCRIPT_ZIP) $(abspath $(ESCRIPT_ZIP_FILE)) $(PROJECT)/ebin/*
+	$(gen_verbose) cd .. && $(ESCRIPT_ZIP) $(abspath $(ESCRIPT_ZIP_FILE)) $(notdir $(CURDIR))/ebin/*
 ifneq ($(DEPS),)
 	$(verbose) cd $(DEPS_DIR) && $(ESCRIPT_ZIP) $(abspath $(ESCRIPT_ZIP_FILE)) \
 		$(subst $(DEPS_DIR)/,,$(addsuffix /*,$(wildcard \
-			$(addsuffix /ebin,$(shell cat $(ERLANG_MK_TMP)/deps.log)))))
+		$(addsuffix /ebin,$(shell cat $(ESCRIPT_RUNTIME_DEPS_FILE))))))
 endif
 
+# @todo Only generate the zip file if there were changes.
 escript:: escript-zip
 	$(gen_verbose) printf "%s\n" \
 		"#!$(ESCRIPT_SHEBANG)" \
diff --git a/plugins/eunit.mk b/plugins/eunit.mk
index 211a7442..8a97688b 100644
--- a/plugins/eunit.mk
+++ b/plugins/eunit.mk
@@ -4,6 +4,11 @@
 
 .PHONY: eunit apps-eunit
 
+# Eunit can be disabled by setting this to any other value.
+EUNIT ?= system
+
+ifeq ($(EUNIT),system)
+
 # Configuration
 
 EUNIT_OPTS ?=
@@ -61,3 +66,5 @@ apps-eunit: test-build
 		exit $$eunit_retcode
 endif
 endif
+
+endif
diff --git a/plugins/hex.mk b/plugins/hex.mk
index 482a56a5..72c0db6a 100644
--- a/plugins/hex.mk
+++ b/plugins/hex.mk
@@ -19,7 +19,7 @@ define hex_user_create.erl
 endef
 
 # The $(info ) call inserts a new line after the password prompt.
-hex-user-create: hex-core
+hex-user-create: $(DEPS_DIR)/hex_core/ebin/dep_built
 	$(if $(HEX_USERNAME),,$(eval HEX_USERNAME := $(shell read -p "Username: " username; echo $$username)))
 	$(if $(HEX_PASSWORD),,$(eval HEX_PASSWORD := $(shell stty -echo; read -p "Password: " password; stty echo; echo $$password) $(info )))
 	$(if $(HEX_EMAIL),,$(eval HEX_EMAIL := $(shell read -p "Email: " email; echo $$email)))
@@ -49,7 +49,7 @@ define hex_key_add.erl
 	end
 endef
 
-hex-key-add: hex-core
+hex-key-add: $(DEPS_DIR)/hex_core/ebin/dep_built
 	$(if $(HEX_USERNAME),,$(eval HEX_USERNAME := $(shell read -p "Username: " username; echo $$username)))
 	$(if $(HEX_PASSWORD),,$(eval HEX_PASSWORD := $(shell stty -echo; read -p "Password: " password; stty echo; echo $$password) $(info )))
 	$(gen_verbose) $(call erlang,$(call hex_key_add.erl,$(HEX_USERNAME),$(HEX_PASSWORD),\
@@ -127,7 +127,7 @@ hex_tar_verbose_0 = @echo " TAR    $(notdir $(ERLANG_MK_TMP))/$(@F)";
 hex_tar_verbose_2 = set -x;
 hex_tar_verbose = $(hex_tar_verbose_$(V))
 
-$(HEX_TARBALL_OUTPUT_FILE): hex-core app
+$(HEX_TARBALL_OUTPUT_FILE): $(DEPS_DIR)/hex_core/ebin/dep_built app
 	$(hex_tar_verbose) $(call erlang,$(call hex_tarball_create.erl))
 
 hex-tarball-create: $(HEX_TARBALL_OUTPUT_FILE)
@@ -178,14 +178,14 @@ define hex_release_publish.erl
 	end
 endef
 
-hex-release-tarball: hex-core $(HEX_TARBALL_OUTPUT_FILE)
+hex-release-tarball: $(DEPS_DIR)/hex_core/ebin/dep_built $(HEX_TARBALL_OUTPUT_FILE)
 	$(verbose) $(call erlang,$(call hex_release_publish_summary.erl))
 
-hex-release-publish: hex-core hex-release-tarball
+hex-release-publish: $(DEPS_DIR)/hex_core/ebin/dep_built hex-release-tarball
 	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
 	$(gen_verbose) $(call erlang,$(call hex_release_publish.erl,$(HEX_SECRET),false))
 
-hex-release-replace: hex-core hex-release-tarball
+hex-release-replace: $(DEPS_DIR)/hex_core/ebin/dep_built hex-release-tarball
 	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
 	$(gen_verbose) $(call erlang,$(call hex_release_publish.erl,$(HEX_SECRET),true))
 
@@ -204,7 +204,7 @@ define hex_release_delete.erl
 	end
 endef
 
-hex-release-delete: hex-core
+hex-release-delete: $(DEPS_DIR)/hex_core/ebin/dep_built
 	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
 	$(gen_verbose) $(call erlang,$(call hex_release_delete.erl,$(HEX_SECRET)))
 
@@ -224,7 +224,7 @@ define hex_release_retire.erl
 	end
 endef
 
-hex-release-retire: hex-core
+hex-release-retire: $(DEPS_DIR)/hex_core/ebin/dep_built
 	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
 	$(gen_verbose) $(call erlang,$(call hex_release_retire.erl,$(HEX_SECRET),\
 		$(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION)),\
@@ -246,7 +246,7 @@ define hex_release_unretire.erl
 	end
 endef
 
-hex-release-unretire: hex-core
+hex-release-unretire: $(DEPS_DIR)/hex_core/ebin/dep_built
 	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
 	$(gen_verbose) $(call erlang,$(call hex_release_unretire.erl,$(HEX_SECRET),\
 		$(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION))))
@@ -255,7 +255,7 @@ HEX_DOCS_DOC_DIR ?= doc/
 HEX_DOCS_TARBALL_FILES ?= $(sort $(call core_find,$(HEX_DOCS_DOC_DIR),*))
 HEX_DOCS_TARBALL_OUTPUT_FILE ?= $(ERLANG_MK_TMP)/$(PROJECT)-docs.tar.gz
 
-$(HEX_DOCS_TARBALL_OUTPUT_FILE): hex-core app docs
+$(HEX_DOCS_TARBALL_OUTPUT_FILE): $(DEPS_DIR)/hex_core/ebin/dep_built app docs
 	$(hex_tar_verbose) tar czf $(HEX_DOCS_TARBALL_OUTPUT_FILE) -C $(HEX_DOCS_DOC_DIR) \
 		$(HEX_DOCS_TARBALL_FILES:$(HEX_DOCS_DOC_DIR)%=%)
 
@@ -279,7 +279,7 @@ define hex_docs_publish.erl
 	end
 endef
 
-hex-docs-publish: hex-core hex-docs-tarball-create
+hex-docs-publish: $(DEPS_DIR)/hex_core/ebin/dep_built hex-docs-tarball-create
 	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
 	$(gen_verbose) $(call erlang,$(call hex_docs_publish.erl,$(HEX_SECRET)))
 
@@ -299,7 +299,7 @@ define hex_docs_delete.erl
 	end
 endef
 
-hex-docs-delete: hex-core
+hex-docs-delete: $(DEPS_DIR)/hex_core/ebin/dep_built
 	$(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info )))
 	$(gen_verbose) $(call erlang,$(call hex_docs_delete.erl,$(HEX_SECRET),\
 		$(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION))))
diff --git a/test/core_elixir.mk b/test/core_elixir.mk
new file mode 100644
index 00000000..1ed23b6c
--- /dev/null
+++ b/test/core_elixir.mk
@@ -0,0 +1,276 @@
+# Core: Miscellaneous.
+#
+# The miscellaneous tests use the prefix "core-", not "core-misc-".
+
+CORE_ELIXIR_TARGETS = $(call list_targets,core-elixir)
+
+.PHONY: core-elixir $(CORE_ELIXIR_TARGETS)
+
+core-elixir: $(CORE_ELIXIR_TARGETS)
+
+core-elixir-compile-from-lib: init
+
+	$i "Bootstrap a new OTP library named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib $v
+
+	$i "Create Elixir source file hello.ex"
+	$t mkdir $(APP)/lib
+	$t printf "%s\n" \
+		"defmodule HelloWorld do" \
+		"  def hello do" \
+		'	IO.puts("Hello, world!")' \
+		"  end" \
+		"end" > $(APP)/lib/hello.ex
+
+	$i "Build the application"
+	$t $(MAKE) -C $(APP) $v
+
+	$i "Check that all compiled files exist"
+	$t test -f $(APP)/ebin/$(APP).app
+	$t test -f $(APP)/ebin/Elixir.HelloWorld.beam
+
+	$i "Check that the application was compiled correctly"
+	$t $(ERL) -pa $(APP)/ebin/ -pa $(APP)/deps/*/ebin -pa $(dir $(shell elixir -e 'IO.puts(:code.lib_dir(:elixir))'))/*/ebin -eval " \
+		ok = application:start($(APP)), \
+		{ok, Mods = ['Elixir.HelloWorld']} \
+			= application:get_key($(APP), modules), \
+		[{module, M} = code:load_file(M) || M <- Mods], \
+		halt()"
+
+core-elixir-compile-from-src: init
+
+	$i "Bootstrap a new OTP library named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib $v
+
+	$i "Create Elixir source file hello.ex"
+	$t printf "%s\n" \
+		"defmodule HelloWorld do" \
+		"  def hello do" \
+		'	IO.puts("Hello, world!")' \
+		"  end" \
+		"end" > $(APP)/src/hello.ex
+
+	$i "Build the application"
+	$t $(MAKE) -C $(APP) $v
+
+	$i "Check that all compiled files exist"
+	$t test -f $(APP)/ebin/$(APP).app
+	$t test -f $(APP)/ebin/Elixir.HelloWorld.beam
+
+	$i "Check that the application was compiled correctly"
+	$t $(ERL) -pa $(APP)/ebin/ -pa $(APP)/deps/*/ebin -pa $(dir $(shell elixir -e 'IO.puts(:code.lib_dir(:elixir))'))/*/ebin -eval " \
+		ok = application:start($(APP)), \
+		{ok, Mods = ['Elixir.HelloWorld']} \
+			= application:get_key($(APP), modules), \
+		[{module, M} = code:load_file(M) || M <- Mods], \
+		halt()"
+
+core-elixir-disable: init
+
+	$i "Bootstrap a new OTP library named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib $v
+
+	$i "Create Elixir source file hello.ex"
+	$t printf "%s\n" \
+		"defmodule HelloWorld do" \
+		"  def hello do" \
+		'	IO.puts("Hello, world!")' \
+		"  end" \
+		"end" > $(APP)/src/hello.ex
+
+	$i "Disable Elixir in the Makefile"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "ELIXIR = disable\n"}' $(APP)/Makefile
+
+	$i "Build the application"
+	$t $(MAKE) -C $(APP) $v
+
+	$i "Check that the Elixir file wasn't compiled"
+	$t test -f $(APP)/ebin/$(APP).app
+	$t test ! -e $(APP)/ebin/Elixir.HelloWorld.beam
+
+	$i "Check that the application was compiled correctly"
+	$t $(ERL) -pa $(APP)/ebin/ -pa $(APP)/deps/*/ebin -pa $(dir $(shell elixir -e 'IO.puts(:code.lib_dir(:elixir))'))/*/ebin -eval " \
+		ok = application:start($(APP)), \
+		{ok, Mods = []} \
+			= application:get_key($(APP), modules), \
+		halt()"
+
+core-elixir-disable-autopatch-fail: init
+
+	$i "Bootstrap a new OTP library named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib $v
+
+	$i "Add Jason to the list of dependencies"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = jason\ndep_jason = git https://github.com/michalmuskala/jason.git master\n"}' $(APP)/Makefile
+
+	$i "Disable Elixir in the Makefile"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "ELIXIR = disable\n"}' $(APP)/Makefile
+
+	$i "Building the application should fail"
+	$t ! $(MAKE) -C $(APP) $v
+
+core-elixir-disable-autopatch-rebar3: init
+
+	$i "Bootstrap a new OTP library named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib $v
+
+	$i "Add Jose to the list of dependencies"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = jose\ndep_jose = git https://github.com/potatosalad/erlang-jose main\n"}' $(APP)/Makefile
+
+	$i "Disable Elixir in the Makefile"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "ELIXIR = disable\n"}' $(APP)/Makefile
+
+	$i "Building the application should work as Jose is Rebar3-compatible"
+	$t $(MAKE) -C $(APP) $v
+
+core-elixir-from-dep: init
+
+	$i "Bootstrap a new OTP library named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib $v
+
+	$i "Add Elixir, Lager, Jason, Phoenix to the list of dependencies"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = elixir lager jason phoenix\ndep_elixir_commit = v1.17.3\ndep_lager = git https://github.com/erlang-lager/lager master\ndep_jason = git https://github.com/michalmuskala/jason.git master\ndep_phoenix = hex 1.7.2\n"}' $(APP)/Makefile
+
+	$i "Add the lager_transform parse_transform to ERLC_OPTS"
+	$t echo "ERLC_OPTS += +'{parse_transform, lager_transform}'" >> $(APP)/Makefile
+
+ifdef LEGACY
+	$i "Add Elixir, Lager, Jason and Phoenix to the applications key in the .app.src file"
+	$t perl -ni.bak -e 'print;if ($$.==7) {print "\t\telixir,\n\t\tlager,\n\t\tjason,\n\t\tphoenix,\n"}' $(APP)/src/$(APP).app.src
+endif
+
+	$i "Build the application"
+	$t $(MAKE) -C $(APP) $v
+
+	$i "Check that all dependencies were fetched and built"
+	$t test -f $(APP)/deps/elixir/ebin/dep_built
+	$t test -f $(APP)/deps/lager/ebin/dep_built
+	$t test -f $(APP)/deps/jason/ebin/dep_built
+	$t test -f $(APP)/deps/phoenix/ebin/dep_built
+
+	$i "Check that the application was compiled correctly"
+	$t cd $(APP); $(ERL) -pa ebin/ -pa deps/*/ebin -pa deps/elixir/lib/*/ebin -eval " \
+		{ok, Apps} = application:ensure_all_started('$(APP)'), \
+		true = lists:member(elixir, Apps), \
+		true = lists:member(lager, Apps), \
+		true = lists:member(jason, Apps), \
+		true = lists:member(phoenix, Apps), \
+		halt()"
+
+	$i "Check that the Jason application depends on Elixir builtins"
+	$t cd $(APP); $(ERL) -pa ebin/ -pa deps/*/ebin -pa deps/elixir/lib/*/ebin -eval " \
+		{ok, Apps} = application:ensure_all_started(jason), \
+		true = lists:member(elixir, Apps), \
+		true = lists:member(eex, Apps), \
+		true = lists:member(logger, Apps), \
+		true = lists:member(mix, Apps), \
+		halt()"
+
+core-elixir-from-system: init
+
+	$i "Bootstrap a new OTP library named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib $v
+
+	$i "Add Lager, Jason, Phoenix to the list of dependencies"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = lager jason phoenix\ndep_lager = git https://github.com/erlang-lager/lager master\ndep_jason = git https://github.com/michalmuskala/jason.git master\ndep_phoenix = hex 1.7.2\nELIXIR = system\n"}' $(APP)/Makefile
+
+	$i "Add the lager_transform parse_transform to ERLC_OPTS"
+	$t echo "ERLC_OPTS += +'{parse_transform, lager_transform}'" >> $(APP)/Makefile
+
+ifdef LEGACY
+	$i "Add Lager, Jason and Phoenix to the applications key in the .app.src file"
+	$t perl -ni.bak -e 'print;if ($$.==7) {print "\t\telixir,\n\t\tlager,\n\t\tjason,\n\t\tphoenix,\n"}' $(APP)/src/$(APP).app.src
+endif
+
+	$i "Build the application"
+	$t $(MAKE) -C $(APP) $v
+
+	$i "Check that all dependencies were fetched and built"
+	$t ! test -e $(APP)/deps/elixir
+	$t test -f $(APP)/deps/lager/ebin/dep_built
+	$t test -f $(APP)/deps/jason/ebin/dep_built
+	$t test -f $(APP)/deps/phoenix/ebin/dep_built
+
+	$i "Check that the application was compiled correctly"
+	$t cd $(APP); $(ERL) -pa ebin/ -pa deps/*/ebin -pa $(dir $(shell elixir -e 'IO.puts(:code.lib_dir(:elixir))'))/*/ebin -eval " \
+		{ok, Apps} = application:ensure_all_started('$(APP)'), \
+		true = lists:member(lager, Apps), \
+		true = lists:member(jason, Apps), \
+		true = lists:member(phoenix, Apps), \
+		halt()"
+
+	$i "Check that the Jason application depends on Elixir builtins"
+	$t cd $(APP); $(ERL) -pa ebin/ -pa deps/*/ebin -pa $(dir $(shell elixir -e 'IO.puts(:code.lib_dir(:elixir))'))/*/ebin -eval " \
+		{ok, Apps} = application:ensure_all_started(jason), \
+		true = lists:member(elixir, Apps), \
+		true = lists:member(eex, Apps), \
+		true = lists:member(logger, Apps), \
+		true = lists:member(mix, Apps), \
+		halt()"
+
+core-elixir-nif: init
+
+	$i "Bootstrap a new OTP library named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib $v
+
+	$i "Add Libsalty2 to the list of dependencies"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = libsalty2\ndep_libsalty2 = git https://github.com/Ianleeclark/libsalty2.git b11e544\nELIXIR = system\n"}' $(APP)/Makefile
+
+ifdef LEGACY
+	$i "Add Libsalty2 to the applications key in the .app.src file"
+	$t perl -ni.bak -e 'print;if ($$.==7) {print "\t\tlibsalty2,\n"}' $(APP)/src/$(APP).app.src
+endif
+
+	$i "Build the application"
+	$t $(MAKE) -C $(APP) $v
+
+	$i "Check that the application was compiled correctly"
+	$t $(ERL) -pa $(APP)/ebin/ -pa $(APP)/deps/*/ebin -pa $(dir $(shell elixir -e 'IO.puts(:code.lib_dir(:elixir))'))/*/ebin -eval " \
+		{ok, Apps} = application:ensure_all_started('$(APP)'), \
+		true = lists:member(libsalty2, Apps), \
+		halt()"
+
+core-elixir-rel: init
+
+	$i "Bootstrap a new release named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib bootstrap-rel $v
+
+	$i "Add Lager, Jason, Phoenix to the list of dependencies"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = lager jason phoenix\ndep_lager = git https://github.com/erlang-lager/lager master\ndep_jason = git https://github.com/michalmuskala/jason.git master\ndep_phoenix = hex 1.7.2\nELIXIR = system\n"}' $(APP)/Makefile
+
+	$i "Add the lager_transform parse_transform to ERLC_OPTS"
+	$t echo "ERLC_OPTS += +'{parse_transform, lager_transform}'" >> $(APP)/Makefile
+
+ifdef LEGACY
+	$i "Add Lager, Jason and Phoenix to the applications key in the .app.src file"
+	$t perl -ni.bak -e 'print;if ($$.==7) {print "\t\tlager,\n\t\tjason,\n\t\tphoenix,\n"}' $(APP)/src/$(APP).app.src
+endif
+
+	$i "Build the release"
+	$t $(MAKE) -C $(APP) $v
+
+	$i "Check that the release was built"
+	$t test -d $(APP)/_rel
+	$t test -d $(APP)/_rel/$(APP)_release
+	$t test -d $(APP)/_rel/$(APP)_release/bin
+	$t test -d $(APP)/_rel/$(APP)_release/lib
+	$t test -d $(APP)/_rel/$(APP)_release/releases
+	$t test -d $(APP)/_rel/$(APP)_release/releases/1
diff --git a/test/plugin_eunit.mk b/test/plugin_eunit.mk
index ba3b192f..bd1f0e0d 100644
--- a/test/plugin_eunit.mk
+++ b/test/plugin_eunit.mk
@@ -235,6 +235,27 @@ eunit-check: init
 	$i "Check that EUnit runs on 'make check'"
 	$t $(MAKE) -C $(APP) check | grep -c "Test passed." | grep -q 1
 
+eunit-disable: init
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Set EUNIT = disable in the Makefile"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "EUNIT = disable\n"}' $(APP)/Makefile
+
+	$i "Generate a module containing EUnit tests"
+	$t printf "%s\n" \
+		"-module($(APP))." \
+		"-ifdef(TEST)." \
+		"-include_lib(\"eunit/include/eunit.hrl\")." \
+		"ok_test() -> ok." \
+		"-endif." > $(APP)/src/$(APP).erl
+
+	$i "Check that EUnit is not run on 'make tests'"
+	$t $(MAKE) -C $(APP) tests | grep -c "Test passed." | grep -q 0
+
 eunit-erl-opts: init
 
 	$i "Bootstrap a new OTP application named $(APP)"


Reviews:
user: login: essen
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-12-16T15:28:39Z
user: login: artman41
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-24T22:16:35Z
user: login: essen
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-12-30T10:33:11Z
user: login: lhoguin
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-11T15:30:36Z
user: login: essen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T16:55:47Z
user: login: essen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T14:19:45Z
user: login: essen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T16:32:39Z
user: login: essen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T16:43:20Z
user: login: wojtekmach
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T10:42:13Z
user: login: essen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T10:44:28Z

Review comments:
diff_hunk: @@ -854,30 +901,39 @@ $(DEPS_DIR)/$(call query_name,$1): | $(if $(filter hex,$(call query_fetch_method
 		cd $(DEPS_DIR)/$(DEP_NAME) && ./configure; \
 	fi
 ifeq ($(filter $1,$(NO_AUTOPATCH)),)
-	$(verbose) $$(MAKE) --no-print-directory autopatch-$(DEP_NAME)
+	$(verbose) $$(MAKE) --no-print-directory autopatch-$(DEP_NAME) AUTOPATCH_METHOD=`$(call dep_autopatch_detect,$1)`
 endif
 
 .PHONY: autopatch-$(call query_name,$1)
 
+ifeq ($1,elixir)
+autopatch-elixir::
+	ln -s lib/elixir/ebin $(DEPS_DIR)/elixir/
path: core/deps.mk
user: login: essen
body: $(verbose)
created_at: 2024-12-16T15:28:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,145 @@
+# Copyright (c) 2024, Tyler Hughes <artman41@gmail.com>
path: core/elixir.mk
user: login: artman41
body: sorry to be awkward Essen, any chance I could get this swapped to my dev email `tyler@tylerhughes.dev`?

No worries if not, I appreciate it's a tiny thing 😅 
created_at: 2024-12-24T22:16:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,145 @@
+# Copyright (c) 2024, Tyler Hughes <artman41@gmail.com>
path: core/elixir.mk
user: login: essen
body: Will do. I think I used the email from your PR's commits. If I forget a PR updating it is of course welcome.
created_at: 2024-12-30T10:33:11Z
author_association: MEMBER
diff_hunk: @@ -854,30 +901,39 @@ $(DEPS_DIR)/$(call query_name,$1): | $(if $(filter hex,$(call query_fetch_method
 		cd $(DEPS_DIR)/$(DEP_NAME) && ./configure; \
 	fi
 ifeq ($(filter $1,$(NO_AUTOPATCH)),)
-	$(verbose) $$(MAKE) --no-print-directory autopatch-$(DEP_NAME)
+	$(verbose) $$(MAKE) --no-print-directory autopatch-$(DEP_NAME) AUTOPATCH_METHOD=`$(call dep_autopatch_detect,$1)`
 endif
 
 .PHONY: autopatch-$(call query_name,$1)
 
+ifeq ($1,elixir)
+autopatch-elixir::
+	ln -s lib/elixir/ebin $(DEPS_DIR)/elixir/
path: core/deps.mk
user: login: lhoguin
body: This still needs to be fixed.
created_at: 2025-03-11T15:30:36Z
author_association: NONE
diff_hunk: @@ -0,0 +1,161 @@
+# Copyright (c) 2024, Tyler Hughes <tyler@tylerhughes.dev>
+# Copyright (c) 2024, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+ELIXIR ?= $(if $(filter elixir,$(BUILD_DEPS) $(DEPS)),dep,system)
+export ELIXIR
+
+ifeq ($(ELIXIR),system)
+# We expect 'elixir' to be on the path.
+# @todo Only if there are EX_FILES
path: core/elixir.mk
user: login: essen
body: This should probably be resolved before merging as well.
created_at: 2025-03-11T16:55:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,161 @@
+# Copyright (c) 2024, Tyler Hughes <tyler@tylerhughes.dev>
+# Copyright (c) 2024, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+ELIXIR ?= $(if $(filter elixir,$(BUILD_DEPS) $(DEPS)),dep,system)
+export ELIXIR
+
+ifeq ($(ELIXIR),system)
+# We expect 'elixir' to be on the path.
+# @todo Only if there are EX_FILES
path: core/elixir.mk
user: login: essen
body: This can only be resolved by converting the Hex version resolver to use an Erlang equivalent of Elixir's Version.match? so I guess I am doing that now...
created_at: 2025-03-12T14:19:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,161 @@
+# Copyright (c) 2024, Tyler Hughes <tyler@tylerhughes.dev>
+# Copyright (c) 2024, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+ELIXIR ?= $(if $(filter elixir,$(BUILD_DEPS) $(DEPS)),dep,system)
+export ELIXIR
+
+ifeq ($(ELIXIR),system)
+# We expect 'elixir' to be on the path.
+# @todo Only if there are EX_FILES
path: core/elixir.mk
user: login: essen
body: It appears that https://github.com/jelly-beam/verl works just fine. It supports `>= 1.8.0 and < 3.0.0` as well, which e.g. Cowboy uses.

So it is just a matter of fetching `verl`. I guess we could have a meta project whose only purpose is to depend on both `hex_core` and `verl`; or fetch them individually depending on needs. This might make the most sense, we only need `verl` for autopatch, whereas `hex_core` is used for other Hex related functions.

Alternatively perhaps @wojtekmach would like to have `verl` a part of `hex_core` (either as a dep or vendored in) so that we have the full functionality out of the box. Just wondering.
created_at: 2025-03-12T16:32:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,161 @@
+# Copyright (c) 2024, Tyler Hughes <tyler@tylerhughes.dev>
+# Copyright (c) 2024, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+ELIXIR ?= $(if $(filter elixir,$(BUILD_DEPS) $(DEPS)),dep,system)
+export ELIXIR
+
+ifeq ($(ELIXIR),system)
+# We expect 'elixir' to be on the path.
+# @todo Only if there are EX_FILES
path: core/elixir.mk
user: login: essen
body: OK I think we can restrict the Elixir resolver to Elixir, and have an Erlang resolver for Erlang later (perhaps use what's in Rebar3 directly). So this is not blocking.

I will move the Hex resolver to elixir.mk.
created_at: 2025-03-13T16:43:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,161 @@
+# Copyright (c) 2024, Tyler Hughes <tyler@tylerhughes.dev>
+# Copyright (c) 2024, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+ELIXIR ?= $(if $(filter elixir,$(BUILD_DEPS) $(DEPS)),dep,system)
+export ELIXIR
+
+ifeq ($(ELIXIR),system)
+# We expect 'elixir' to be on the path.
+# @todo Only if there are EX_FILES
path: core/elixir.mk
user: login: wojtekmach
body: @essen agreed, I created a ticket to track it: https://github.com/hexpm/hex_core/issues/157
created_at: 2025-03-14T10:42:13Z
author_association: NONE
diff_hunk: @@ -0,0 +1,161 @@
+# Copyright (c) 2024, Tyler Hughes <tyler@tylerhughes.dev>
+# Copyright (c) 2024, Loïc Hoguin <essen@ninenines.eu>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+ELIXIR ?= $(if $(filter elixir,$(BUILD_DEPS) $(DEPS)),dep,system)
+export ELIXIR
+
+ifeq ($(ELIXIR),system)
+# We expect 'elixir' to be on the path.
+# @todo Only if there are EX_FILES
path: core/elixir.mk
user: login: essen
body: Thank you!! This will help a lot.
created_at: 2025-03-14T10:44:28Z
author_association: MEMBER

PR comments:
user: login: essen
created_at: 2024-12-10T12:20:11Z
body: We're green! Just have to do autopatch cleanup, move things around and review.
author_association: MEMBER
user: login: essen
created_at: 2024-12-11T14:37:37Z
body: Something that this doesn't currently do is skip rebuilding.
author_association: MEMBER
user: login: essen
created_at: 2024-12-13T15:26:07Z
body: Once all tests pass again in RabbitMQ this can be merged. But perhaps leave it hanging until after Christmas.
author_association: MEMBER
user: login: essen
created_at: 2024-12-16T15:05:06Z
body: This broke Ranch use within tests somehow.
author_association: MEMBER
user: login: essen
created_at: 2024-12-18T22:08:39Z
body: For eunit we need to filter out the Elixir.*.beam files.

Edit: No and it's hard to know whether eunit is used or not. Perhaps have a variable to force disable it.
author_association: MEMBER
user: login: artman41
created_at: 2025-01-09T22:50:10Z
body: I've been playing around with the branch tonight essen, found some inconsistencies when using Elixir dependencies so created a 'fixes' branch - PR is at #1021

One of the commits adds support for Elixir 1.13 which supports OTP 23 - I'm happy to remove this commit though if you'd rather not have the backwards compat
author_association: CONTRIBUTOR
user: login: essen
created_at: 2025-03-11T13:45:04Z
body: @artman41 The branch looks good as far as RabbitMQ is concerned. Please do a final check before I squash and merge. Thank you!!
author_association: MEMBER
user: login: essen
created_at: 2025-03-11T15:33:03Z
body: There's a couple comments left in this PR that are worth looking at again before merging.
author_association: MEMBER
user: login: essen
created_at: 2025-03-11T16:57:33Z
body: I think we also want to make sure that setting e.g. `ELIXIR = disable` completely disables Elixir support.
author_association: MEMBER
user: login: artman41
created_at: 2025-03-12T19:23:22Z
body: > @artman41 The branch looks good as far as RabbitMQ is concerned. Please do a final check before I squash and merge. Thank you!!

Looks good so far I think, nothing obvious I'm seeing in terms of issues 👍 
author_association: CONTRIBUTOR
user: login: essen
created_at: 2025-03-13T16:52:25Z
body: I will add a test to check that we can disable Elixir entirely.
author_association: MEMBER
user: login: essen
created_at: 2025-03-14T10:30:08Z
body: OK so I don't want a `readlink` command run every time Make is run, as that slows down everything when not needed. If there's EX_FILES we can enable Elixir. If Elixir is in deps we can use that one. But if neither is true, AND we want to use Elixir deps, then I think we must require `ELIXIR = system` to be set. That's probably rare, and OK.
author_association: MEMBER
user: login: essen
created_at: 2025-03-14T14:24:56Z
body: Most recent commit should be it, after that a squash and merge is all that's left. Plus some documentation I guess (at least explaining how to disable Elixir, or enable it for deps), and updating the changelog. I plan to get it merged Monday/Tuesday.
author_association: MEMBER
user: login: essen
created_at: 2025-03-14T14:43:43Z
body: OK there's still a problem with Elixir activation. Elixir project in RabbitMQ fails saying to enable Elixir.
author_association: MEMBER
user: login: essen
created_at: 2025-03-17T10:39:45Z
body: If Elixir is disabled we shouldn't try to autopatch Mix projects (some projects have both Erlang and Elixir so that's OK, such as `jose`).
author_association: MEMBER
user: login: essen
created_at: 2025-03-17T13:27:25Z
body: OK there's now a fallback to Rebar3 when Elixir is disabled, so `jose` can be used without Elixir enabled again.
author_association: MEMBER
user: login: essen
created_at: 2025-03-17T14:28:41Z
body: @artman41 Final commit has the beginning of a docs page.

I will merge this tomorrow if everything is OK.
author_association: MEMBER
user: login: essen
created_at: 2025-03-18T09:02:20Z
body: Merged!! Thanks!
author_association: MEMBER