%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2009-2017. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
%%
%% Purpose : Basic make facility for Common Test
%%
%% Compares date stamps of .erl and Object files - recompiles when
%% necessary.
%% Files to be checked are contained in a file 'Emakefile'
%% If Emakefile is missing the current directory is used.
%%
-module(ct_make).
-export([all/0,all/1,files/1,files/2]).
-include_lib("kernel/include/file.hrl").
-define(MakeOpts,[noexec,load,netload,noload]).
all() ->
all([]).
all(Options) ->
{MakeOpts,CompileOpts} = sort_options(Options,[],[]),
case read_emakefile('Emakefile',CompileOpts) of
Files when is_list(Files) ->
do_make_files(Files,MakeOpts);
error ->
{error,[]}
end.
files(Fs) ->
files(Fs, []).
files(Fs0, Options) ->
Fs = [filename:rootname(F,".erl") || F <- Fs0],
{MakeOpts,CompileOpts} = sort_options(Options,[],[]),
case get_opts_from_emakefile(Fs,'Emakefile',CompileOpts) of
Files when is_list(Files) ->
do_make_files(Files,MakeOpts);
error ->
{error,[]}
end.
do_make_files(Fs, Opts) ->
process(Fs, lists:member(noexec, Opts), load_opt(Opts), []).
sort_options([H|T],Make,Comp) ->
case lists:member(H,?MakeOpts) of
true ->
sort_options(T,[H|Make],Comp);
false ->
sort_options(T,Make,[H|Comp])
end;
sort_options([],Make,Comp) ->
{Make,lists:reverse(Comp)}.
%%% Reads the given Emakefile and returns a list of tuples: {Mods,Opts}
%%% Mods is a list of module names (strings)
%%% Opts is a list of options to be used when compiling Mods
%%%
%%% Emakefile can contain elements like this:
%%% Mod.
%%% {Mod,Opts}.
%%% Mod is a module name which might include '*' as wildcard
%%% or a list of such module names
%%%
%%% These elements are converted to [{ModList,OptList},...]
%%% ModList is a list of modulenames (strings)
read_emakefile(Emakefile,Opts) ->
case file:consult(Emakefile) of
{ok,Emake} ->
transform(Emake,Opts,[],[]);
{error,enoent} ->
%% No Emakefile found - return all modules in current
%% directory and the options given at command line
Mods = [filename:rootname(F) || F <- filelib:wildcard("*.erl")],
[{Mods, Opts}];
{error,Other} ->
io:format("make: Trouble reading 'Emakefile':~n~tp~n",[Other]),
error
end.
transform([{Mod,ModOpts}|Emake],Opts,Files,Already) ->
case expand(Mod,Already) of
[] ->
transform(Emake,Opts,Files,Already);
Mods ->
transform(Emake,Opts,[{Mods,ModOpts++Opts}|Files],Mods++Already)
end;
transform([Mod|Emake],Opts,Files,Already) ->
case expand(Mod,Already) of
[] ->
transform(Emake,Opts,Files,Already);
Mods ->
transform(Emake,Opts,[{Mods,Opts}|Files],Mods++Already)
end;
transform([],_Opts,Files,_Already) ->
lists:reverse(Files).
expand(Mod,Already) when is_atom(Mod) ->
expand(atom_to_list(Mod),Already);
expand(Mods,Already) when is_list(Mods), not is_integer(hd(Mods)) ->
lists:concat([expand(Mod,Already) || Mod <- Mods]);
expand(Mod,Already) ->
case lists:member($*,Mod) of
true ->
Fun = fun(F,Acc) ->
M = filename:rootname(F),
case lists:member(M,Already) of
true -> Acc;
false -> [M|Acc]
end
end,
lists:foldl(Fun, [], filelib:wildcard(Mod++".erl"));
false ->
Mod2 = filename:rootname(Mod, ".erl"),
case lists:member(Mod2,Already) of
true -> [];
false -> [Mod2]
end
end.
%%% Reads the given Emakefile to see if there are any specific compile
%%% options given for the modules.
get_opts_from_emakefile(Mods,Emakefile,Opts) ->
case file:consult(Emakefile) of
{ok,Emake} ->
Modsandopts = transform(Emake,Opts,[],[]),
ModStrings = [coerce_2_list(M) || M <- Mods],
get_opts_from_emakefile2(Modsandopts,ModStrings,Opts,[]);
{error,enoent} ->
[{Mods, Opts}];
{error,Other} ->
io:format("make: Trouble reading 'Emakefile':~n~tp~n",[Other]),
error
end.
get_opts_from_emakefile2([{MakefileMods,O}|Rest],Mods,Opts,Result) ->
case members(Mods,MakefileMods,[],Mods) of
{[],_} ->
get_opts_from_emakefile2(Rest,Mods,Opts,Result);
{I,RestOfMods} ->
get_opts_from_emakefile2(Rest,RestOfMods,Opts,[{I,O}|Result])
end;
get_opts_from_emakefile2([],[],_Opts,Result) ->
Result;
get_opts_from_emakefile2([],RestOfMods,Opts,Result) ->
[{RestOfMods,Opts}|Result].
members([H|T],MakefileMods,I,Rest) ->
case lists:member(H,MakefileMods) of
true ->
members(T,MakefileMods,[H|I],lists:delete(H,Rest));
false ->
members(T,MakefileMods,I,Rest)
end;
members([],_MakefileMods,I,Rest) ->
{I,Rest}.
%% Any flags that are not recognised as make flags are passed directly
%% to the compiler.
%% So for example make:all([load,debug_info]) will make everything
%% with the debug_info flag and load it.
load_opt(Opts) ->
case lists:member(netload,Opts) of
true ->
netload;
false ->
case lists:member(load,Opts) of
true ->
load;
_ ->
noload
end
end.
process([{[],_Opts}|Rest], NoExec, Load, Result) ->
process(Rest, NoExec, Load, Result);
process([{[H|T],Opts}|Rest], NoExec, Load, Result) ->
case recompilep(coerce_2_list(H), NoExec, Load, Opts) of
error ->
process([{T,Opts}|Rest], NoExec, Load, [{H,error}|Result]);
Info ->
process([{T,Opts}|Rest], NoExec, Load, [{H,Info}|Result])
end;
process([], NoExec, _Load, Result) ->
if not NoExec ->
case lists:keysearch(error, 2, Result) of
{value,_} ->
{error,Result};
false ->
{up_to_date,Result}
end;
true ->
Result
end.
recompilep(File, NoExec, Load, Opts) ->
ObjName = lists:append(filename:basename(File),
code:objfile_extension()),
ObjFile = case lists:keysearch(outdir,1,Opts) of
{value,{outdir,OutDir}} ->
filename:join(coerce_2_list(OutDir),ObjName);
false ->
ObjName
end,
case exists(ObjFile) of
true ->
recompilep1(File, NoExec, Load, Opts, ObjFile);
false ->
recompile(File, NoExec, Load, Opts)
end.
recompilep1(File, NoExec, Load, Opts, ObjFile) ->
{ok, Erl} = file:read_file_info(lists:append(File, ".erl")),
{ok, Obj} = file:read_file_info(ObjFile),
case {readable(Erl), writable(Obj)} of
{true, true} ->
recompilep1(Erl, Obj, File, NoExec, Load, Opts);
_ ->
error
end.
recompilep1(#file_info{mtime=Te},
#file_info{mtime=To}, File, NoExec, Load, Opts) when Te>To ->
recompile(File, NoExec, Load, Opts);
recompilep1(_Erl, #file_info{mtime=To}, File, NoExec, Load, Opts) ->
recompile2(To, File, NoExec, Load, Opts).
%% recompile2(ObjMTime, File, NoExec, Load, Opts)
%% Check if file is of a later date than include files.
recompile2(ObjMTime, File, NoExec, Load, Opts) ->
IncludePath = include_opt(Opts),
case check_includes(lists:append(File, ".erl"), IncludePath, ObjMTime) of
true ->
recompile(File, NoExec, Load, Opts);
false ->
up_to_date
end.
include_opt([{i,Path}|Rest]) ->
[Path|include_opt(Rest)];
include_opt([_First|Rest]) ->
include_opt(Rest);
include_opt([]) ->
[].
%% recompile(File, NoExec, Load, Opts)
%% Actually recompile and load the file, depending on the flags.
%% Where load can be netload | load | noload
recompile(File, NoExec, Load, Opts) ->
case do_recompile(File, NoExec, Load, Opts) of
{ok,_} -> ok;
Other -> Other
end.
do_recompile(_File, true, _Load, _Opts) ->
out_of_date;
do_recompile(File, false, noload, Opts) ->
io:format("Recompile: ~ts\n",[File]),
compile:file(File, [report_errors, report_warnings, error_summary |Opts]);
do_recompile(File, false, load, Opts) ->
io:format("Recompile: ~ts\n",[File]),
c:c(File, Opts);
do_recompile(File, false, netload, Opts) ->
io:format("Recompile: ~ts\n",[File]),
c:nc(File, Opts).
exists(File) ->
case file:read_file_info(File) of
{ok, _} ->
true;
_ ->
false
end.
readable(#file_info{access=read_write}) -> true;
readable(#file_info{access=read}) -> true;
readable(_) -> false.
writable(#file_info{access=read_write}) -> true;
writable(#file_info{access=write}) -> true;
writable(_) -> false.
coerce_2_list(X) when is_atom(X) ->
atom_to_list(X);
coerce_2_list(X) ->
X.
%%% If you an include file is found with a modification
%%% time larger than the modification time of the object
%%% file, return true. Otherwise return false.
check_includes(File, IncludePath, ObjMTime) ->
Path = [filename:dirname(File)|IncludePath],
case epp:open(File, Path, []) of
{ok, Epp} ->
check_includes2(Epp, File, ObjMTime);
_Error ->
false
end.
check_includes2(Epp, File, ObjMTime) ->
A1 = erl_anno:new(1),
case epp:parse_erl_form(Epp) of
{ok, {attribute, A1, file, {File, A1}}} ->
check_includes2(Epp, File, ObjMTime);
{ok, {attribute, A1, file, {IncFile, A1}}} ->
case file:read_file_info(IncFile) of
{ok, #file_info{mtime=MTime}} when MTime>ObjMTime ->
epp:close(Epp),
true;
_ ->
check_includes2(Epp, File, ObjMTime)
end;
{ok, _} ->
check_includes2(Epp, File, ObjMTime);
{eof, _} ->
epp:close(Epp),
false;
{error, _Error} ->
check_includes2(Epp, File, ObjMTime);
{warning, _Warning} ->
check_includes2(Epp, File, ObjMTime)
end.