From dd3e666c5114f81c9518e9f5ba14959ced079ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Mon, 14 Dec 2009 23:04:00 +0100 Subject: Add dependencies Makefile generation to erlc(1) and compile(3) This is useful when a project is built with Makefiles and erlc(1) instead of EMakefiles. Tracking dependencies by hand is error-prone and it becomes painful when using external application headers like EUnit's one. A dependencies Makefile will look like this: module.beam: module.erl \ /usr/local/lib/erlang/lib/eunit-2.1.4/include/eunit.hrl \ header.hrl When included in the main Makefile, 'module' will be recompiled only when needed. GCC offers the same feature and new erlc(1) options are compatible with it. More informations at: http://wiki.github.com/dumbbell/otp/dependencies-makefile --- lib/compiler/doc/src/compile.xml | 64 +++++++ lib/compiler/src/compile.erl | 194 ++++++++++++++++++++- lib/compiler/test/compile_SUITE.erl | 74 +++++++- .../test/compile_SUITE_data/simple-basic1.mk | 1 + .../test/compile_SUITE_data/simple-basic2.mk | 1 + .../test/compile_SUITE_data/simple-missing.mk | 1 + .../test/compile_SUITE_data/simple-target1.mk | 1 + .../test/compile_SUITE_data/simple-target2.mk | 1 + lib/compiler/test/compile_SUITE_data/simple.erl | 4 + 9 files changed, 338 insertions(+), 3 deletions(-) create mode 100644 lib/compiler/test/compile_SUITE_data/simple-basic1.mk create mode 100644 lib/compiler/test/compile_SUITE_data/simple-basic2.mk create mode 100644 lib/compiler/test/compile_SUITE_data/simple-missing.mk create mode 100644 lib/compiler/test/compile_SUITE_data/simple-target1.mk create mode 100644 lib/compiler/test/compile_SUITE_data/simple-target2.mk (limited to 'lib') diff --git a/lib/compiler/doc/src/compile.xml b/lib/compiler/doc/src/compile.xml index c3d65b4cb5..77f3e9b0be 100644 --- a/lib/compiler/doc/src/compile.xml +++ b/lib/compiler/doc/src/compile.xml @@ -164,6 +164,70 @@ for details.

+ makedep + +

Produce a Makefile rule to track headers dependencies. + No object file is produced. +

+

By default, this rule is written to + .Pbeam]]>. However, if the option + binary is set, nothing is written and the rule is + returned in Binary. +

+

For instance, if one has the following module: +

+ +-module(module). + +-include_lib("eunit/include/eunit.hrl"). +-include("header.hrl"). + +

Here is the Makefile rule generated by this option: +

+ +module.beam: module.erl \ + /usr/local/lib/erlang/lib/eunit/include/eunit.hrl \ + header.hrl + +
+ + {makedep_output, Output} + +

Write generated rule(s) to Output instead of the + default .Pbeam]]>. Output + can be a filename or an io_device(). To write to + stdout, use standard_io. However if binary + is set, nothing is written to Output and the + result is returned to the caller with + {ok, ModuleName, Binary}. +

+
+ + {makedep_target, Target} + +

Change the name of the rule emitted to Target. +

+
+ + makedep_quote_target + +

Characters in Target special to make(1) are quoted. +

+
+ + makedep_add_missing + +

Consider missing headers as generated files and add them to the + dependencies. +

+
+ + makedep_phony + +

Add a phony target for each dependency. +

+
+ 'P'

Produces a listing of the parsed code after preprocessing diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl index 26da3ecad2..38603a76a1 100644 --- a/lib/compiler/src/compile.erl +++ b/lib/compiler/src/compile.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1996-2010. All Rights Reserved. +%% Copyright Ericsson AB 1996-2011. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -205,6 +205,9 @@ format_error(write_error) -> format_error({rename,From,To,Error}) -> io_lib:format("failed to rename ~s to ~s: ~s", [From,To,file:format_error(Error)]); +format_error({delete,File,Error}) -> + io_lib:format("failed to delete file ~s: ~s", + [File,file:format_error(Error)]); format_error({delete_temp,File,Error}) -> io_lib:format("failed to delete temporary file ~s: ~s", [File,file:format_error(Error)]); @@ -435,6 +438,8 @@ passes(file, Opts) -> %% file will be Ext. (Ext should not contain %% a period.) No more passes will be run. %% +%% done End compilation at this point. +%% %% {done,Ext} End compilation at this point. Produce a listing %% as with {listing,Ext}, unless 'binary' is %% specified, in which case the current @@ -468,6 +473,8 @@ select_passes([{src_listing,Ext}|_], _Opts) -> [{listing,fun (St) -> src_listing(Ext, St) end}]; select_passes([{listing,Ext}|_], _Opts) -> [{listing,fun (St) -> listing(Ext, St) end}]; +select_passes([done|_], _Opts) -> + []; select_passes([{done,Ext}|_], Opts) -> select_passes([{unless,binary,{listing,Ext}}], Opts); select_passes([{iff,Flag,Pass}|Ps], Opts) -> @@ -550,6 +557,13 @@ select_list_passes_1([], _, Acc) -> standard_passes() -> [?pass(transform_module), + + {iff,makedep,[ + ?pass(makedep), + {unless,binary,?pass(makedep_output)} + ]}, + {iff,makedep,done}, + {iff,'dpp',{listing,"pp"}}, ?pass(lint_module), {iff,'P',{src_listing,"P"}}, @@ -901,6 +915,184 @@ core_lint_module(St) -> errors=St#compile.errors ++ Es}} end. +makedep(#compile{code=Code,options=Opts}=St) -> + Ifile = St#compile.ifile, + Ofile = St#compile.ofile, + + %% Get the target of the Makefile rule. + Target0 = + case proplists:get_value(makedep_target, Opts) of + undefined -> + %% The target is derived from the output filename: possibly + %% remove the current working directory to obtain a relative + %% path. + shorten_filename(Ofile); + T -> + %% The caller specified one. + T + end, + + %% Quote the target is the called asked for this. + Target1 = case proplists:get_value(makedep_quote_target, Opts) of + true -> + %% For now, only "$" is replaced by "$$". + Fun = fun + ($$) -> "$$"; + (C) -> C + end, + map(Fun, Target0); + _ -> + Target0 + end, + Target = Target1 ++ ":", + + %% List the dependencies (includes) for this target. + {MainRule,PhonyRules} = makedep_add_headers( + Ifile, % The input file name. + Code, % The parsed source. + [], % The list of dependencies already added. + length(Target), % The current line length. + Target, % The target. + "", % Phony targets. + Opts), + + %% Prepare the content of the Makefile. For instance: + %% hello.erl: hello.hrl common.hrl + %% + %% Or if phony targets are enabled: + %% hello.erl: hello.hrl common.hrl + %% + %% hello.hrl: + %% + %% common.hrl: + Makefile = case proplists:get_value(makedep_phony, Opts) of + true -> MainRule ++ PhonyRules; + _ -> MainRule + end, + {ok,St#compile{code=iolist_to_binary([Makefile,"\n"])}}. + +makedep_add_headers(Ifile, [{attribute,_,file,{File,_}}|Rest], + Included, LineLen, MainTarget, Phony, Opts) -> + %% The header "File" exists, add it to the dependencies. + {Included1,LineLen1,MainTarget1,Phony1} = + makedep_add_header(Ifile, Included, LineLen, MainTarget, Phony, File), + makedep_add_headers(Ifile, Rest, Included1, LineLen1, + MainTarget1, Phony1, Opts); +makedep_add_headers(Ifile, [{error,{_,epp,{include,file,File}}}|Rest], + Included, LineLen, MainTarget, Phony, Opts) -> + %% The header "File" doesn't exist, do we add it to the dependencies? + case proplists:get_value(makedep_add_missing, Opts) of + true -> + {Included1,LineLen1,MainTarget1,Phony1} = + makedep_add_header(Ifile, Included, LineLen, MainTarget, + Phony, File), + makedep_add_headers(Ifile, Rest, Included1, LineLen1, + MainTarget1, Phony1, Opts); + _ -> + makedep_add_headers(Ifile, Rest, Included, LineLen, + MainTarget, Phony, Opts) + end; +makedep_add_headers(Ifile, [_|Rest], Included, LineLen, + MainTarget, Phony, Opts) -> + makedep_add_headers(Ifile, Rest, Included, + LineLen, MainTarget, Phony, Opts); +makedep_add_headers(_Ifile, [], _Included, _LineLen, + MainTarget, Phony, _Opts) -> + {MainTarget,Phony}. + +makedep_add_header(Ifile, Included, LineLen, MainTarget, Phony, File) -> + case member(File, Included) of + true -> + %% This file was already listed in the dependencies, skip it. + {Included,LineLen,MainTarget,Phony}; + false -> + Included1 = [File|Included], + + %% Remove "./" in front of the dependency filename. + File1 = case File of + "./" ++ File0 -> File0; + _ -> File + end, + + %% Prepare the phony target name. + Phony1 = case File of + Ifile -> Phony; + _ -> Phony ++ "\n\n" ++ File1 ++ ":" + end, + + %% Add the file to the dependencies. Lines longer than 76 columns + %% are splitted. + if + LineLen + 1 + length(File1) > 76 -> + LineLen1 = 2 + length(File1), + MainTarget1 = MainTarget ++ " \\\n " ++ File1, + {Included1,LineLen1,MainTarget1,Phony1}; + true -> + LineLen1 = LineLen + 1 + length(File1), + MainTarget1 = MainTarget ++ " " ++ File1, + {Included1,LineLen1,MainTarget1,Phony1} + end + end. + +makedep_output(#compile{code=Code,options=Opts,ofile=Ofile}=St) -> + %% Write this Makefile (Code) to the selected output. + %% If no output is specified, the default is to write to a file named after + %% the output file. + Output0 = case proplists:get_value(makedep_output, Opts) of + undefined -> + %% Prepare the default filename. + outfile(filename:basename(Ofile, ".beam"), "Pbeam", Opts); + O -> + O + end, + + %% If the caller specified an io_device(), there's nothing to do. If he + %% specified a filename, we must create it. Furthermore, this created file + %% must be closed before returning. + Ret = case Output0 of + _ when is_list(Output0) -> + case file:delete(Output0) of + Ret2 when Ret2 =:= ok; Ret2 =:= {error,enoent} -> + case file:open(Output0, [write]) of + {ok,IODev} -> + {ok,IODev,true}; + {error,Reason2} -> + {error,open,Reason2} + end; + {error,Reason1} -> + {error,delete,Reason1} + end; + _ -> + {ok,Output0,false} + end, + + case Ret of + {ok,Output1,CloseOutput} -> + try + %% Write the Makefile. + io:fwrite(Output1, "~s", [Code]), + %% Close the file if relevant. + if + CloseOutput -> file:close(Output1); + true -> ok + end, + {ok,St} + catch + exit:_ -> + %% Couldn't write to output Makefile. + Err = {St#compile.ifile,[{none,?MODULE,write_error}]}, + {error,St#compile{errors=St#compile.errors++[Err]}} + end; + {error,open,Reason} -> + %% Couldn't open output Makefile. + Err = {St#compile.ifile,[{none,?MODULE,{open,Reason}}]}, + {error,St#compile{errors=St#compile.errors++[Err]}}; + {error,delete,Reason} -> + %% Couldn't open output Makefile. + Err = {St#compile.ifile,[{none,?MODULE,{delete,Output0,Reason}}]}, + {error,St#compile{errors=St#compile.errors++[Err]}} + end. + %% expand_module(State) -> State' %% Do the common preprocessing of the input forms. diff --git a/lib/compiler/test/compile_SUITE.erl b/lib/compiler/test/compile_SUITE.erl index e1cc5dafb5..cf0f352979 100644 --- a/lib/compiler/test/compile_SUITE.erl +++ b/lib/compiler/test/compile_SUITE.erl @@ -25,7 +25,7 @@ -export([all/1, app_test/1, file_1/1, module_mismatch/1, big_file/1, outdir/1, - binary/1, cond_and_ifdef/1, listings/1, listings_big/1, + binary/1, makedep/1, cond_and_ifdef/1, listings/1, listings_big/1, other_output/1, package_forms/1, encrypted_abstr/1, bad_record_use/1, bad_record_use1/1, bad_record_use2/1, strict_record/1, missing_testheap/1, cover/1, env/1, core/1, asm/1]). @@ -40,7 +40,7 @@ all(suite) -> test_lib:recompile(?MODULE), [app_test, - file_1, module_mismatch, big_file, outdir, binary, + file_1, module_mismatch, big_file, outdir, binary, makedep, cond_and_ifdef, listings, listings_big, other_output, package_forms, encrypted_abstr, @@ -132,6 +132,76 @@ binary(Config) when is_list(Config) -> ?line test_server:timetrap_cancel(Dog), ok. +%% Tests that the dependencies-Makefile-related options work. + +makedep(Config) when is_list(Config) -> + ?line Dog = test_server:timetrap(test_server:seconds(60)), + ?line {Simple,Target} = files(Config, "makedep"), + ?line DataDir = ?config(data_dir, Config), + ?line SimpleRootname = filename:rootname(Simple), + ?line IncludeDir = filename:join(filename:dirname(Simple), "include"), + ?line IncludeOptions = [ + {d,need_foo}, + {d,foo_value,42}, + {d,include_generated}, + {i,IncludeDir} + ], + %% Basic rule. + ?line BasicMf1Name = SimpleRootname ++ "-basic1.mk", + ?line {ok,BasicMf1} = file:read_file(BasicMf1Name), + ?line {ok,_,Mf1} = compile:file(Simple, [binary,makedep]), + ?line BasicMf1 = makedep_canonicalize_result(Mf1, DataDir), + %% Basic rule with one existing header. + ?line BasicMf2Name = SimpleRootname ++ "-basic2.mk", + ?line {ok,BasicMf2} = file:read_file(BasicMf2Name), + ?line {ok,_,Mf2} = compile:file(Simple, [binary,makedep|IncludeOptions]), + ?line BasicMf2 = makedep_canonicalize_result(Mf2, DataDir), + %% Rule with one existing header and one missing header. + ?line MissingMfName = SimpleRootname ++ "-missing.mk", + ?line {ok,MissingMf} = file:read_file(MissingMfName), + ?line {ok,_,Mf3} = compile:file(Simple, + [binary,makedep,makedep_add_missing|IncludeOptions]), + ?line MissingMf = makedep_canonicalize_result(Mf3, DataDir), + %% Rule with modified target. + ?line TargetMf1Name = SimpleRootname ++ "-target1.mk", + ?line {ok,TargetMf1} = file:read_file(TargetMf1Name), + ?line {ok,_,Mf4} = compile:file(Simple, + [binary,makedep,{makedep_target,"$target"}|IncludeOptions]), + ?line TargetMf1 = makedep_modify_target( + makedep_canonicalize_result(Mf4, DataDir), "$$target"), + %% Rule with quoted modified target. + ?line TargetMf2Name = SimpleRootname ++ "-target2.mk", + ?line {ok,TargetMf2} = file:read_file(TargetMf2Name), + ?line {ok,_,Mf5} = compile:file(Simple, + [binary,makedep,{makedep_target,"$target"},makedep_quote_target| + IncludeOptions]), + ?line TargetMf2 = makedep_modify_target( + makedep_canonicalize_result(Mf5, DataDir), "$$target"), + %% Basic rule written to some file. + ?line {ok,_} = compile:file(Simple, + [makedep,{makedep_output,Target}|IncludeOptions]), + ?line {ok,Mf6} = file:read_file(Target), + ?line BasicMf2 = makedep_canonicalize_result(Mf6, DataDir), + + ?line ok = file:delete(Target), + ?line ok = file:del_dir(filename:dirname(Target)), + ?line test_server:timetrap_cancel(Dog), + ok. + +makedep_canonicalize_result(Mf, DataDir) -> + Mf0 = binary_to_list(Mf), + %% Replace the Datadir by "$(srcdir)". + Mf1 = re:replace(Mf0, DataDir, "$(srcdir)/", + [global,multiline,{return,list}]), + %% Long lines are splitted, put back everything on one line. + Mf2 = re:replace(Mf1, "\\\\\n ", "", [global,multiline,{return,list}]), + list_to_binary(Mf2). + +makedep_modify_target(Mf, Target) -> + Mf0 = binary_to_list(Mf), + Mf1 = re:replace(Mf0, Target, "$target", [{return,list}]), + list_to_binary(Mf1). + %% Tests that conditional compilation, defining values, including files work. cond_and_ifdef(Config) when is_list(Config) -> diff --git a/lib/compiler/test/compile_SUITE_data/simple-basic1.mk b/lib/compiler/test/compile_SUITE_data/simple-basic1.mk new file mode 100644 index 0000000000..4073fa82d0 --- /dev/null +++ b/lib/compiler/test/compile_SUITE_data/simple-basic1.mk @@ -0,0 +1 @@ +simple.beam: $(srcdir)/simple.erl diff --git a/lib/compiler/test/compile_SUITE_data/simple-basic2.mk b/lib/compiler/test/compile_SUITE_data/simple-basic2.mk new file mode 100644 index 0000000000..761d1d9582 --- /dev/null +++ b/lib/compiler/test/compile_SUITE_data/simple-basic2.mk @@ -0,0 +1 @@ +simple.beam: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl diff --git a/lib/compiler/test/compile_SUITE_data/simple-missing.mk b/lib/compiler/test/compile_SUITE_data/simple-missing.mk new file mode 100644 index 0000000000..b13d44ec36 --- /dev/null +++ b/lib/compiler/test/compile_SUITE_data/simple-missing.mk @@ -0,0 +1 @@ +simple.beam: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl generated.hrl diff --git a/lib/compiler/test/compile_SUITE_data/simple-target1.mk b/lib/compiler/test/compile_SUITE_data/simple-target1.mk new file mode 100644 index 0000000000..dd9fa0d6e5 --- /dev/null +++ b/lib/compiler/test/compile_SUITE_data/simple-target1.mk @@ -0,0 +1 @@ +$target: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl diff --git a/lib/compiler/test/compile_SUITE_data/simple-target2.mk b/lib/compiler/test/compile_SUITE_data/simple-target2.mk new file mode 100644 index 0000000000..a5fc6f461d --- /dev/null +++ b/lib/compiler/test/compile_SUITE_data/simple-target2.mk @@ -0,0 +1 @@ +$$target: $(srcdir)/simple.erl $(srcdir)/include/simple.hrl diff --git a/lib/compiler/test/compile_SUITE_data/simple.erl b/lib/compiler/test/compile_SUITE_data/simple.erl index 2021056388..8e98852417 100644 --- a/lib/compiler/test/compile_SUITE_data/simple.erl +++ b/lib/compiler/test/compile_SUITE_data/simple.erl @@ -37,3 +37,7 @@ foo() -> {?included_value, ?foo_value}. -endif. + +-ifdef(include_generated). +-include("generated.hrl"). +-endif. -- cgit v1.2.3