aboutsummaryrefslogtreecommitdiffstats
path: root/core/elixir.mk
blob: eb5c487599b4f57f9260b99b5e8d2d6ba11ccf08 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# 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