You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

221 lines
9.0 KiB

%%% Module handling the directed graph required for the analysis
%%% of all top-level applications by the various compiler plugins.
-module(rebar_compiler_dag).
-export([init/4, prune/4, update/4, maybe_store/5, terminate/1]).
-include("rebar.hrl").
-define(DAG_VSN, 3).
-define(DAG_ROOT, "source").
-define(DAG_EXT, ".dag").
-type dag_v() :: {digraph:vertex(), term()} | 'false'.
-type dag_e() :: {digraph:vertex(), digraph:vertex()}.
-type critical_meta() :: term(). % if this changes, the DAG is invalid
-type dag_rec() :: {list(dag_v()), list(dag_e()), critical_meta()}.
-type dag() :: digraph:graph().
-record(dag, {vsn = ?DAG_VSN :: pos_integer(),
info = {[], [], []} :: dag_rec()}).
%% You should initialize one DAG per compiler module.
%% `CritMeta' is any contextual information that, if it is found to change,
%% must invalidate the DAG loaded from disk.
-spec init(file:filename_all(), atom(), string() | undefined, critical_meta()) -> dag().
init(Dir, Compiler, Label, CritMeta) ->
G = digraph:new([acyclic]),
File = dag_file(Dir, Compiler, Label),
try
restore_dag(G, File, CritMeta)
catch
_:_ ->
%% Don't mark as dirty yet to avoid creating compiler DAG files for
%% compilers that are actually never used.
?WARN("Failed to restore ~ts file. Discarding it.~n", [File]),
file:delete(File)
end,
G.
-spec prune(dag(), file:filename_all(), file:filename_all(), [file:filename_all()]) -> ok.
prune(G, SrcDirs, EbinDir, Erls) ->
%% A source file may have been renamed or deleted. Remove it from the graph
%% and remove any beam file for that source if it exists.
Vertices = digraph:vertices(G),
SrcParts = [filename:split(SrcDir) || SrcDir <- SrcDirs],
[maybe_rm_beam_and_edge(G, EbinDir, File)
|| File <- lists:sort(Vertices) -- lists:sort(Erls),
filename:extension(File) =:= ".erl",
lists:any(fun(Src) -> lists:prefix(Src, filename:split(File)) end,
SrcParts)],
ok.
%% @doc this function scans all the source files found and looks into
%% all the `InDirs' for deps (other erl or .hrl files) that are related
%% to them (by calling `CompileMod:dependencies()' on them).
%%
%% The trick here is that change detection, done with last_modified stamps,
%% takes place at the same time as the graph propagation (finding deps)
%% themselves. As such, this is a confusing mutually recursive depth-first
%% search function that relies on side-effects and precise order-of-traversal
%% to propagate file changes.
%%
%% To be replaced by a more declarative EPP-based flow.
-spec update(dag(), module(), [file:filename_all()], [file:filename_all()]) -> ok.
update(_, _, _, []) ->
ok;
update(G, Compiler, InDirs, [Source|Erls]) ->
case digraph:vertex(G, Source) of
{_, LastUpdated} ->
case filelib:last_modified(Source) of
0 ->
%% The file doesn't exist anymore,
%% erase it from the graph.
%% All the edges will be erased automatically.
digraph:del_vertex(G, Source),
mark_dirty(G),
update(G, Compiler, InDirs, Erls);
LastModified when LastUpdated < LastModified ->
add_to_dag(G, Compiler, InDirs, Source, LastModified, filename:dirname(Source)),
update(G, Compiler, InDirs, Erls);
_ ->
AltErls = digraph:out_neighbours(G, Source),
%% Deps must be explored before the module itself
update(G, Compiler, InDirs, AltErls),
Modified = is_dirty(G),
MaxModified = update_max_modified_deps(G, Source),
case Modified orelse MaxModified > LastUpdated of
true -> mark_dirty(G);
false -> ok
end,
update(G, Compiler, InDirs, Erls)
end;
false ->
add_to_dag(G, Compiler, InDirs, Source, filelib:last_modified(Source), filename:dirname(Source)),
update(G, Compiler, InDirs, Erls)
end.
maybe_store(G, Dir, Compiler, Label, CritMeta) ->
case is_dirty(G) of
true ->
clear_dirty(G),
File = dag_file(Dir, Compiler, Label),
store_dag(G, File, CritMeta);
false ->
ok
end.
terminate(G) ->
true = digraph:delete(G).
%%%%%%%%%%%%%%%
%%% PRIVATE %%%
%%%%%%%%%%%%%%%
%% @private generate the name for the DAG based on the compiler module and
%% a custom label, both of which are used to prevent various compiler runs
%% from clobbering each other. The label `undefined' is kept for a default
%% run of the compiler, to keep in line with previous versions of the file.
dag_file(Dir, CompilerMod, undefined) ->
filename:join([rebar_dir:local_cache_dir(Dir), CompilerMod,
?DAG_ROOT ++ ?DAG_EXT]);
dag_file(Dir, CompilerMod, Label) ->
filename:join([rebar_dir:local_cache_dir(Dir), CompilerMod,
?DAG_ROOT ++ "_" ++ Label ++ ?DAG_EXT]).
restore_dag(G, File, CritMeta) ->
case file:read_file(File) of
{ok, Data} ->
%% The CritMeta value is checked and if it doesn't match, we fail
%% the whole restore operation.
#dag{vsn=?DAG_VSN, info={Vs, Es, CritMeta}} = binary_to_term(Data),
[digraph:add_vertex(G, V, LastUpdated) || {V, LastUpdated} <- Vs],
[digraph:add_edge(G, V1, V2) || {_, V1, V2, _} <- Es],
ok;
{error, _Err} ->
ok
end.
store_dag(G, File, CritMeta) ->
ok = filelib:ensure_dir(File),
Vs = lists:map(fun(V) -> digraph:vertex(G, V) end, digraph:vertices(G)),
Es = lists:map(fun(E) -> digraph:edge(G, E) end, digraph:edges(G)),
Data = term_to_binary(#dag{info={Vs, Es, CritMeta}}, [{compressed, 2}]),
file:write_file(File, Data).
%% Drop a file from the digraph if it doesn't exist, and if so,
%% delete its related build artifact
maybe_rm_beam_and_edge(G, OutDir, Source) ->
%% This is NOT a double check it is the only check that the source file is actually gone
case filelib:is_regular(Source) of
true ->
%% Actually exists, don't delete
false;
false ->
Target = target_base(OutDir, Source) ++ ".beam",
?DEBUG("Source ~ts is gone, deleting previous beam file if it exists ~ts", [Source, Target]),
file:delete(Target),
digraph:del_vertex(G, Source),
mark_dirty(G),
true
end.
%% @private Return what should be the base name of an erl file, relocated to the
%% target directory. For example:
%% target_base("ebin/", "src/my_module.erl") -> "ebin/my_module"
target_base(OutDir, Source) ->
filename:join(OutDir, filename:basename(Source, ".erl")).
%% @private a file has been found to change or wasn't part of the DAG before,
%% and must be added, along with all its dependencies.
add_to_dag(G, Compiler, InDirs, Source, LastModified, SourceDir) ->
AbsIncls = Compiler:dependencies(Source, SourceDir, InDirs),
digraph:add_vertex(G, Source, LastModified),
digraph:del_edges(G, digraph:out_edges(G, Source)),
%% Deps must be explored before the module itself
[begin
update(G, Compiler, InDirs, [Incl]),
digraph:add_edge(G, Source, Incl)
end || Incl <- AbsIncls],
mark_dirty(G),
AbsIncls.
%% @private change status propagation: if the dependencies of a file have
%% been updated, mark the last_modified time for that file to be equivalent
%% to its most-recently-changed dependency; that way, nested header file
%% change stamps are propagated to the final module.
%% This is required because at some point the module is compared to its
%% associated .beam file's last-generation stamp to know if it requires
%% rebuilding.
%% The responsibility for this is however diffuse across various modules.
update_max_modified_deps(G, Source) ->
MaxModified = lists:foldl(
fun(File, Acc) ->
case digraph:vertex(G, File) of
{_, MaxModified} when MaxModified > Acc -> MaxModified;
_ -> Acc
end
end,
0,
[Source | digraph:out_neighbours(G, Source)]
),
digraph:add_vertex(G, Source, MaxModified),
MaxModified.
%% Mark the digraph as having been modified, which is required to
%% save its updated form on disk after the compiling run.
%% This uses a magic vertex to carry the dirty state. This is less
%% than ideal because listing vertices may expect filenames and
%% instead there's going to be one trick atom through it.
mark_dirty(G) ->
digraph:add_vertex(G, '$r3_dirty_bit', true),
ok.
%% Check whether the digraph has been modified and is considered dirty.
is_dirty(G) ->
case digraph:vertex(G, '$r3_dirty_bit') of
{_, Bool} -> Bool;
false -> false
end.
%% Remove the dirty status. Because the saving of a digraph on disk saves all
%% vertices, clear the flag before serializing it.
clear_dirty(G) ->
digraph:del_vertex(G, '$r3_dirty_bit').