aboutsummaryrefslogblamecommitdiffstats
path: root/core/elixir.mk
blob: eb5c487599b4f57f9260b99b5e8d2d6ba11ccf08 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12











                                                                                           





                                                                                 



























































































































































































                                                                                                                                                                      
# Copyright (c) 2024, Tyler Hughes <[email protected]>
# Copyright (c) 2024, Loïc Hoguin <[email protected]>
# 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_BIN ?= $(shell readlink -f `which elixir`)
ELIXIR_LIBS ?= $(abspath $(dir $(ELIXIR_BIN))/../lib)
# Fallback in case 'elixir' is a shim.
ifeq ($(wildcard $(ELIXIR_LIBS)/elixir/),)
ELIXIR_LIBS = $(abspath $(shell elixir -e 'IO.puts(:code.lib_dir(:elixir))')/../)
endif
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