1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2009-2017. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20
21%%
22%% Purpose : Basic make facility for Common Test
23%%
24%% Compares date stamps of .erl and Object files - recompiles when
25%% necessary.
26%% Files to be checked are contained in a file 'Emakefile'
27%% If Emakefile is missing the current directory is used.
28%%
29
30-module(ct_make).
31
32-export([all/0,all/1,files/1,files/2]).
33
34-include_lib("kernel/include/file.hrl").
35
36-define(MakeOpts,[noexec,load,netload,noload]).
37
38all() ->
39    all([]).
40
41all(Options) ->
42    {MakeOpts,CompileOpts} = sort_options(Options,[],[]),
43    case read_emakefile('Emakefile',CompileOpts) of
44	Files when is_list(Files) ->
45	    do_make_files(Files,MakeOpts);
46	error ->
47	    {error,[]}
48    end.
49
50files(Fs) ->
51    files(Fs, []).
52
53files(Fs0, Options) ->
54    Fs = [filename:rootname(F,".erl") || F <- Fs0],
55    {MakeOpts,CompileOpts} = sort_options(Options,[],[]),
56    case get_opts_from_emakefile(Fs,'Emakefile',CompileOpts) of
57	Files when is_list(Files) ->
58	    do_make_files(Files,MakeOpts);
59	error ->
60	    {error,[]}
61    end.
62
63do_make_files(Fs, Opts) ->
64    process(Fs, lists:member(noexec, Opts), load_opt(Opts), []).
65
66
67sort_options([H|T],Make,Comp) ->
68    case lists:member(H,?MakeOpts) of
69	true ->
70	    sort_options(T,[H|Make],Comp);
71	false ->
72	    sort_options(T,Make,[H|Comp])
73    end;
74sort_options([],Make,Comp) ->
75    {Make,lists:reverse(Comp)}.
76
77%%% Reads the given Emakefile and returns a list of tuples: {Mods,Opts}
78%%% Mods is a list of module names (strings)
79%%% Opts is a list of options to be used when compiling Mods
80%%%
81%%% Emakefile can contain elements like this:
82%%% Mod.
83%%% {Mod,Opts}.
84%%% Mod is a module name which might include '*' as wildcard
85%%% or a list of such module names
86%%%
87%%% These elements are converted to [{ModList,OptList},...]
88%%% ModList is a list of modulenames (strings)
89read_emakefile(Emakefile,Opts) ->
90    case file:consult(Emakefile) of
91	{ok,Emake} ->
92	    transform(Emake,Opts,[],[]);
93	{error,enoent} ->
94	    %% No Emakefile found - return all modules in current
95	    %% directory and the options given at command line
96	    Mods = [filename:rootname(F) ||  F <- filelib:wildcard("*.erl")],
97	    [{Mods, Opts}];
98	{error,Other} ->
99	    io:format("make: Trouble reading 'Emakefile':~n~tp~n",[Other]),
100	    error
101    end.
102
103transform([{Mod,ModOpts}|Emake],Opts,Files,Already) ->
104    case expand(Mod,Already) of
105	[] ->
106	    transform(Emake,Opts,Files,Already);
107	Mods ->
108	    transform(Emake,Opts,[{Mods,ModOpts++Opts}|Files],Mods++Already)
109    end;
110transform([Mod|Emake],Opts,Files,Already) ->
111    case expand(Mod,Already) of
112	[] ->
113	    transform(Emake,Opts,Files,Already);
114	Mods ->
115	    transform(Emake,Opts,[{Mods,Opts}|Files],Mods++Already)
116    end;
117transform([],_Opts,Files,_Already) ->
118    lists:reverse(Files).
119
120expand(Mod,Already) when is_atom(Mod) ->
121    expand(atom_to_list(Mod),Already);
122expand(Mods,Already) when is_list(Mods), not is_integer(hd(Mods)) ->
123    lists:concat([expand(Mod,Already) || Mod <- Mods]);
124expand(Mod,Already) ->
125    case lists:member($*,Mod) of
126	true ->
127	    Fun = fun(F,Acc) ->
128			  M = filename:rootname(F),
129			  case lists:member(M,Already) of
130			      true -> Acc;
131			      false -> [M|Acc]
132			  end
133		  end,
134	    lists:foldl(Fun, [], filelib:wildcard(Mod++".erl"));
135	false ->
136	    Mod2 = filename:rootname(Mod, ".erl"),
137	    case lists:member(Mod2,Already) of
138		true -> [];
139		false -> [Mod2]
140	    end
141    end.
142
143%%% Reads the given Emakefile to see if there are any specific compile
144%%% options given for the modules.
145get_opts_from_emakefile(Mods,Emakefile,Opts) ->
146    case file:consult(Emakefile) of
147	{ok,Emake} ->
148	    Modsandopts = transform(Emake,Opts,[],[]),
149	    ModStrings = [coerce_2_list(M) || M <- Mods],
150	    get_opts_from_emakefile2(Modsandopts,ModStrings,Opts,[]);
151	{error,enoent} ->
152	    [{Mods, Opts}];
153	{error,Other} ->
154	    io:format("make: Trouble reading 'Emakefile':~n~tp~n",[Other]),
155	    error
156    end.
157
158get_opts_from_emakefile2([{MakefileMods,O}|Rest],Mods,Opts,Result) ->
159    case members(Mods,MakefileMods,[],Mods) of
160	{[],_} ->
161	    get_opts_from_emakefile2(Rest,Mods,Opts,Result);
162	{I,RestOfMods} ->
163	    get_opts_from_emakefile2(Rest,RestOfMods,Opts,[{I,O}|Result])
164    end;
165get_opts_from_emakefile2([],[],_Opts,Result) ->
166    Result;
167get_opts_from_emakefile2([],RestOfMods,Opts,Result) ->
168    [{RestOfMods,Opts}|Result].
169
170members([H|T],MakefileMods,I,Rest) ->
171    case lists:member(H,MakefileMods) of
172	true ->
173	    members(T,MakefileMods,[H|I],lists:delete(H,Rest));
174	false ->
175	    members(T,MakefileMods,I,Rest)
176    end;
177members([],_MakefileMods,I,Rest) ->
178    {I,Rest}.
179
180
181%% Any flags that are not recognised as make flags are passed directly
182%% to the compiler.
183%% So for example make:all([load,debug_info]) will make everything
184%% with the debug_info flag and load it.
185
186load_opt(Opts) ->
187    case lists:member(netload,Opts) of
188	true ->
189	    netload;
190	false ->
191	    case lists:member(load,Opts) of
192		true ->
193		    load;
194		_ ->
195		    noload
196	    end
197    end.
198
199
200process([{[],_Opts}|Rest], NoExec, Load, Result) ->
201    process(Rest, NoExec, Load, Result);
202process([{[H|T],Opts}|Rest], NoExec, Load, Result) ->
203    case recompilep(coerce_2_list(H), NoExec, Load, Opts) of
204	error ->
205	    process([{T,Opts}|Rest], NoExec, Load, [{H,error}|Result]);
206	Info ->
207	    process([{T,Opts}|Rest], NoExec, Load, [{H,Info}|Result])
208    end;
209process([], NoExec, _Load, Result) ->
210    if not NoExec ->
211	    case lists:keysearch(error, 2, Result) of
212		{value,_} ->
213		    {error,Result};
214		false ->
215		    {up_to_date,Result}
216	    end;
217       true ->
218	    Result
219    end.
220
221recompilep(File, NoExec, Load, Opts) ->
222    ObjName = lists:append(filename:basename(File),
223			   code:objfile_extension()),
224    ObjFile = case lists:keysearch(outdir,1,Opts) of
225		  {value,{outdir,OutDir}} ->
226		      filename:join(coerce_2_list(OutDir),ObjName);
227		  false ->
228		      ObjName
229	      end,
230    case exists(ObjFile) of
231	true ->
232	    recompilep1(File, NoExec, Load, Opts, ObjFile);
233	false ->
234	    recompile(File, NoExec, Load, Opts)
235    end.
236
237recompilep1(File, NoExec, Load, Opts, ObjFile) ->
238    {ok, Erl} = file:read_file_info(lists:append(File, ".erl")),
239    {ok, Obj} = file:read_file_info(ObjFile),
240    case {readable(Erl), writable(Obj)} of
241	{true, true} ->
242	    recompilep1(Erl, Obj, File, NoExec, Load, Opts);
243	_ ->
244	    error
245    end.
246
247recompilep1(#file_info{mtime=Te},
248	    #file_info{mtime=To}, File, NoExec, Load, Opts) when Te>To ->
249    recompile(File, NoExec, Load, Opts);
250recompilep1(_Erl, #file_info{mtime=To}, File, NoExec, Load, Opts) ->
251    recompile2(To, File, NoExec, Load, Opts).
252
253%% recompile2(ObjMTime, File, NoExec, Load, Opts)
254%% Check if file is of a later date than include files.
255recompile2(ObjMTime, File, NoExec, Load, Opts) ->
256    IncludePath = include_opt(Opts),
257    case check_includes(lists:append(File, ".erl"), IncludePath, ObjMTime) of
258	true ->
259	    recompile(File, NoExec, Load, Opts);
260	false ->
261	    up_to_date
262    end.
263
264include_opt([{i,Path}|Rest]) ->
265    [Path|include_opt(Rest)];
266include_opt([_First|Rest]) ->
267    include_opt(Rest);
268include_opt([]) ->
269    [].
270
271%% recompile(File, NoExec, Load, Opts)
272%% Actually recompile and load the file, depending on the flags.
273%% Where load can be netload | load | noload
274
275recompile(File, NoExec, Load, Opts) ->
276    case do_recompile(File, NoExec, Load, Opts) of
277	{ok,_} -> ok;
278	Other -> Other
279    end.
280
281do_recompile(_File, true, _Load, _Opts) ->
282    out_of_date;
283do_recompile(File, false, Load, Opts) ->
284    io:format("Recompile: ~ts\n",[File]),
285    case compile:file(File, [report_errors, report_warnings |Opts]) of
286        Ok when is_tuple(Ok), element(1,Ok)==ok ->
287            maybe_load(element(2,Ok), Load, Opts);
288        _Error ->
289            error
290    end.
291
292maybe_load(_Mod, noload, _Opts) ->
293    ok;
294maybe_load(Mod, Load, Opts) ->
295    %% We have compiled File with options Opts. Find out where the
296    %% output file went to, and load it.
297    case compile:output_generated(Opts) of
298        true ->
299            Dir = proplists:get_value(outdir,Opts,"."),
300            do_load(Dir, Mod, Load);
301        false ->
302            io:format("** Warning: No object file created - nothing loaded **~n"),
303            ok
304    end.
305
306do_load(Dir, Mod, load) ->
307    code:purge(Mod),
308    case code:load_abs(filename:join(Dir, Mod),Mod) of
309        {module,Mod} ->
310            {ok,Mod};
311        Other ->
312            Other
313    end;
314do_load(Dir, Mod, netload) ->
315    Obj = atom_to_list(Mod) ++ code:objfile_extension(),
316    Fname = filename:join(Dir, Obj),
317    case file:read_file(Fname) of
318        {ok,Bin} ->
319            rpc:eval_everywhere(code,load_binary,[Mod,Fname,Bin]),
320            {ok,Mod};
321        Other ->
322            Other
323    end.
324
325exists(File) ->
326    case file:read_file_info(File) of
327	{ok, _} ->
328	    true;
329	_ ->
330	    false
331    end.
332
333readable(#file_info{access=read_write}) -> true;
334readable(#file_info{access=read})       -> true;
335readable(_) -> false.
336
337writable(#file_info{access=read_write}) -> true;
338writable(#file_info{access=write})      -> true;
339writable(_) -> false.
340
341coerce_2_list(X) when is_atom(X) ->
342    atom_to_list(X);
343coerce_2_list(X) ->
344    X.
345
346%%% If you an include file is found with a modification
347%%% time larger than the modification time of the object
348%%% file, return true. Otherwise return false.
349check_includes(File, IncludePath, ObjMTime) ->
350    Path = [filename:dirname(File)|IncludePath],
351    case epp:open(File, Path, []) of
352	{ok, Epp} ->
353	    check_includes2(Epp, File, ObjMTime);
354	_Error ->
355	    false
356    end.
357
358check_includes2(Epp, File, ObjMTime) ->
359    A1 = erl_anno:new(1),
360    case epp:parse_erl_form(Epp) of
361	{ok, {attribute, A1, file, {File, A1}}} ->
362	    check_includes2(Epp, File, ObjMTime);
363	{ok, {attribute, A1, file, {IncFile, A1}}} ->
364	    case file:read_file_info(IncFile) of
365		{ok, #file_info{mtime=MTime}} when MTime>ObjMTime ->
366		    epp:close(Epp),
367		    true;
368		_ ->
369		    check_includes2(Epp, File, ObjMTime)
370	    end;
371	{ok, _} ->
372	    check_includes2(Epp, File, ObjMTime);
373	{eof, _} ->
374	    epp:close(Epp),
375	    false;
376	{error, _Error} ->
377	    check_includes2(Epp, File, ObjMTime);
378	{warning, _Warning} ->
379	    check_includes2(Epp, File, ObjMTime)
380    end.
381