|
@ -1,7 +1,9 @@ |
|
|
%%% Module handling the directed graph required for the analysis |
|
|
%%% Module handling the directed graph required for the analysis |
|
|
%%% of all top-level applications by the various compiler plugins. |
|
|
%%% of all top-level applications by the various compiler plugins. |
|
|
-module(rebar_compiler_dag). |
|
|
-module(rebar_compiler_dag). |
|
|
-export([init/4, prune/4, update/4, maybe_store/5, terminate/1]). |
|
|
|
|
|
|
|
|
-export([init/4, maybe_store/5, terminate/1]). |
|
|
|
|
|
-export([prune/5, populate_sources/5, populate_deps/3, propagate_stamps/1, |
|
|
|
|
|
compile_order/2]). |
|
|
|
|
|
|
|
|
-include("rebar.hrl"). |
|
|
-include("rebar.hrl"). |
|
|
|
|
|
|
|
@ -17,7 +19,7 @@ |
|
|
-record(dag, {vsn = ?DAG_VSN :: pos_integer(), |
|
|
-record(dag, {vsn = ?DAG_VSN :: pos_integer(), |
|
|
info = {[], [], []} :: dag_rec()}). |
|
|
info = {[], [], []} :: dag_rec()}). |
|
|
|
|
|
|
|
|
%% You should initialize one DAG per compiler module. |
|
|
|
|
|
|
|
|
%% @doc You should initialize one DAG per compiler module. |
|
|
%% `CritMeta' is any contextual information that, if it is found to change, |
|
|
%% `CritMeta' is any contextual information that, if it is found to change, |
|
|
%% must invalidate the DAG loaded from disk. |
|
|
%% must invalidate the DAG loaded from disk. |
|
|
-spec init(file:filename_all(), atom(), string() | undefined, critical_meta()) -> dag(). |
|
|
-spec init(file:filename_all(), atom(), string() | undefined, critical_meta()) -> dag(). |
|
@ -35,64 +37,145 @@ init(Dir, Compiler, Label, CritMeta) -> |
|
|
end, |
|
|
end, |
|
|
G. |
|
|
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 Clear up inactive (deleted) source files from a given project. |
|
|
|
|
|
%% The file must be in one of the directories that may contain source files |
|
|
|
|
|
%% for an OTP application; source files found in the DAG `G' that lie outside |
|
|
|
|
|
%% of these directories may be used in other circumstances (i.e. options affecting |
|
|
|
|
|
%% visibility). |
|
|
|
|
|
%% Prune out files that have no corresponding sources |
|
|
|
|
|
prune(G, SrcExt, ArtifactExt, Sources, AppPaths) -> |
|
|
|
|
|
%% Collect source files that may have been removed. These files: |
|
|
|
|
|
%% * are not in Sources |
|
|
|
|
|
%% * have SrcExt |
|
|
|
|
|
%% In the process, prune header files - those don't have ArtifactExt |
|
|
|
|
|
%% extension - using side effect in is_deleted_source/5. |
|
|
|
|
|
case [Del || Del <- (digraph:vertices(G) -- Sources), |
|
|
|
|
|
is_deleted_source(G, Del, filename:extension(Del), SrcExt, ArtifactExt)] of |
|
|
|
|
|
[] -> |
|
|
|
|
|
ok; %% short circuit without sorting AppPaths |
|
|
|
|
|
Deleted -> |
|
|
|
|
|
prune_source_files(G, SrcExt, ArtifactExt, |
|
|
|
|
|
lists:sort(AppPaths), lists:sort(Deleted)) |
|
|
|
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
is_deleted_source(_G, _F, Extension, Extension, _ArtifactExt) -> |
|
|
|
|
|
%% source file |
|
|
|
|
|
true; |
|
|
|
|
|
is_deleted_source(_G, _F, Extension, _SrcExt, Extension) -> |
|
|
|
|
|
%% artifact file - skip |
|
|
|
|
|
false; |
|
|
|
|
|
is_deleted_source(G, F, _Extension, _SrcExt, _ArtifactExt) -> |
|
|
|
|
|
%% must be header file |
|
|
|
|
|
digraph:in_edges(G, F) == [] andalso maybe_rm_vertex(G, F), |
|
|
|
|
|
false. |
|
|
|
|
|
|
|
|
|
|
|
%% This can be implemented using smarter trie, but since the |
|
|
|
|
|
%% whole procedure is rare, don't bother with optimisations. |
|
|
|
|
|
%% AppDirs & Fs are sorted, and to check if File is outside of |
|
|
|
|
|
%% App, lists:prefix is checked. When the App with File in it |
|
|
|
|
|
%% exists, verify file is still there on disk. |
|
|
|
|
|
prune_source_files(_G, _SrcExt, _ArtifactExt, [], _) -> |
|
|
|
|
|
ok; |
|
|
|
|
|
prune_source_files(_G, _SrcExt, _ArtifactExt, _, []) -> |
|
|
|
|
|
ok; |
|
|
|
|
|
prune_source_files(G, SrcExt, ArtifactExt, [AppDir | AppTail], Fs) when is_atom(AppDir) -> |
|
|
|
|
|
%% dirty bit shenanigans |
|
|
|
|
|
prune_source_files(G, SrcExt, ArtifactExt, AppTail, Fs); |
|
|
|
|
|
prune_source_files(G, SrcExt, ArtifactExt, [{App, Out} | AppTail] = AppPaths, [File | FTail]) -> |
|
|
|
|
|
case lists:prefix(App, File) of |
|
|
|
|
|
true -> |
|
|
|
|
|
maybe_rm_artifact_and_edge(G, Out, SrcExt, ArtifactExt, File), |
|
|
|
|
|
prune_source_files(G, SrcExt, ArtifactExt, AppPaths, FTail); |
|
|
|
|
|
false when App < File -> |
|
|
|
|
|
prune_source_files(G, SrcExt, ArtifactExt, AppTail, [File|FTail]); |
|
|
|
|
|
false -> |
|
|
|
|
|
prune_source_files(G, SrcExt, ArtifactExt, AppPaths, FTail) |
|
|
|
|
|
end. |
|
|
|
|
|
|
|
|
%% @doc this function scans all the source files found and looks into |
|
|
%% @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(_, _, _, []) -> |
|
|
|
|
|
|
|
|
%% all the `InDirs' for deps (other source files, or files that aren't source |
|
|
|
|
|
%% but still returned by the compiler module) that are related |
|
|
|
|
|
%% to them. |
|
|
|
|
|
populate_sources(_G, _Compiler, _InDirs, [], _DepOpts) -> |
|
|
ok; |
|
|
ok; |
|
|
update(G, Compiler, InDirs, [Source|Erls]) -> |
|
|
|
|
|
|
|
|
populate_sources(G, Compiler, InDirs, [Source|Erls], DepOpts) -> |
|
|
case digraph:vertex(G, Source) of |
|
|
case digraph:vertex(G, Source) of |
|
|
{_, LastUpdated} -> |
|
|
{_, LastUpdated} -> |
|
|
case filelib:last_modified(Source) of |
|
|
case filelib:last_modified(Source) of |
|
|
0 -> |
|
|
0 -> |
|
|
%% The file doesn't exist anymore, |
|
|
|
|
|
%% erase it from the graph. |
|
|
|
|
|
%% All the edges will be erased automatically. |
|
|
|
|
|
|
|
|
%% The File doesn't exist anymore, delete |
|
|
|
|
|
%% from the graph. |
|
|
digraph:del_vertex(G, Source), |
|
|
digraph:del_vertex(G, Source), |
|
|
mark_dirty(G), |
|
|
mark_dirty(G), |
|
|
update(G, Compiler, InDirs, Erls); |
|
|
|
|
|
|
|
|
populate_sources(G, Compiler, InDirs, Erls, DepOpts); |
|
|
LastModified when LastUpdated < LastModified -> |
|
|
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) |
|
|
|
|
|
|
|
|
digraph:add_vertex(G, Source, LastModified), |
|
|
|
|
|
prepopulate_deps(G, Compiler, InDirs, Source, DepOpts, old), |
|
|
|
|
|
mark_dirty(G); |
|
|
|
|
|
_ -> % unchanged |
|
|
|
|
|
ok |
|
|
end; |
|
|
end; |
|
|
false -> |
|
|
false -> |
|
|
add_to_dag(G, Compiler, InDirs, Source, filelib:last_modified(Source), filename:dirname(Source)), |
|
|
|
|
|
update(G, Compiler, InDirs, Erls) |
|
|
|
|
|
|
|
|
LastModified = filelib:last_modified(Source), |
|
|
|
|
|
digraph:add_vertex(G, Source, LastModified), |
|
|
|
|
|
prepopulate_deps(G, Compiler, InDirs, Source, DepOpts, new), |
|
|
|
|
|
mark_dirty(G) |
|
|
|
|
|
end, |
|
|
|
|
|
populate_sources(G, Compiler, InDirs, Erls, DepOpts). |
|
|
|
|
|
|
|
|
|
|
|
%% @doc Scan all files in the digraph that are seen as dependencies, but are |
|
|
|
|
|
%% neither source files nor artifacts (i.e. header files that don't produce |
|
|
|
|
|
%% artifacts of any kind). |
|
|
|
|
|
populate_deps(G, SourceExt, ArtifactExts) -> |
|
|
|
|
|
%% deps are files that are part of the digraph, but couldn't be scanned |
|
|
|
|
|
%% because they are neither source files (`SourceExt') nor mappings |
|
|
|
|
|
%% towards build artifacts (`ArtifactExts'); they will therefore never |
|
|
|
|
|
%% be handled otherwise and need to be re-scanned for accuracy, even |
|
|
|
|
|
%% if they are not being analyzed (we assume `Compiler:deps' did that |
|
|
|
|
|
%% in depth already, and improvements should be driven at that level) |
|
|
|
|
|
IgnoredExts = [SourceExt | ArtifactExts], |
|
|
|
|
|
Vertices = digraph:vertices(G), |
|
|
|
|
|
[refresh_dep(G, File) |
|
|
|
|
|
|| File <- Vertices, |
|
|
|
|
|
Ext <- [filename:extension(File)], |
|
|
|
|
|
not lists:member(Ext, IgnoredExts)], |
|
|
|
|
|
ok. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
%% @doc Take the timestamps/diff changes and propagate them from a dep to the |
|
|
|
|
|
%% parent; given: |
|
|
|
|
|
%% A 0 -> B 1 -> C 3 -> D 2 |
|
|
|
|
|
%% then we expect to get back: |
|
|
|
|
|
%% A 3 -> B 3 -> C 3 -> D 2 |
|
|
|
|
|
%% This is going to be safe for the current run of regeneration, but also for the |
|
|
|
|
|
%% next one; unless any file in the chain has changed, the stamp won't move up |
|
|
|
|
|
%% and there won't be a reason to recompile. |
|
|
|
|
|
%% The obvious caveat to this one is that a file changing by restoring an old version |
|
|
|
|
|
%% won't be picked up, but this weakness already existed in terms of timestamps. |
|
|
|
|
|
propagate_stamps(G) -> |
|
|
|
|
|
case is_dirty(G) of |
|
|
|
|
|
false -> |
|
|
|
|
|
%% no change, no propagation to make |
|
|
|
|
|
ok; |
|
|
|
|
|
true -> |
|
|
|
|
|
%% we can use a topsort, start at the end of it (files with no deps) |
|
|
|
|
|
%% and update them all in order. By doing this, each file only needs to check |
|
|
|
|
|
%% for one level of out-neighbours to set itself to the right appropriate time. |
|
|
|
|
|
DepSort = lists:reverse(digraph_utils:topsort(G)), |
|
|
|
|
|
propagate_stamps(G, DepSort) |
|
|
end. |
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
%% @doc Return the reverse sorting order to get dep-free apps first. |
|
|
|
|
|
%% -- we would usually not need to consider the non-source files for the order to |
|
|
|
|
|
%% be complete, but using them doesn't hurt. |
|
|
|
|
|
compile_order(G, AppDefs) -> |
|
|
|
|
|
Edges = [{V1,V2} || E <- digraph:edges(G), |
|
|
|
|
|
{_,V1,V2,_} <- [digraph:edge(G, E)]], |
|
|
|
|
|
AppPaths = prepare_app_paths(AppDefs), |
|
|
|
|
|
compile_order(Edges, AppPaths, #{}). |
|
|
|
|
|
|
|
|
|
|
|
%% @doc Store the DAG on disk if it was dirty |
|
|
maybe_store(G, Dir, Compiler, Label, CritMeta) -> |
|
|
maybe_store(G, Dir, Compiler, Label, CritMeta) -> |
|
|
case is_dirty(G) of |
|
|
case is_dirty(G) of |
|
|
true -> |
|
|
true -> |
|
@ -103,6 +186,7 @@ maybe_store(G, Dir, Compiler, Label, CritMeta) -> |
|
|
ok |
|
|
ok |
|
|
end. |
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
%% Get rid of the live state for the digraph; leave disk stuff in place. |
|
|
terminate(G) -> |
|
|
terminate(G) -> |
|
|
true = digraph:delete(G). |
|
|
true = digraph:delete(G). |
|
|
|
|
|
|
|
@ -142,62 +226,168 @@ store_dag(G, File, CritMeta) -> |
|
|
|
|
|
|
|
|
%% Drop a file from the digraph if it doesn't exist, and if so, |
|
|
%% Drop a file from the digraph if it doesn't exist, and if so, |
|
|
%% delete its related build artifact |
|
|
%% delete its related build artifact |
|
|
maybe_rm_beam_and_edge(G, OutDir, Source) -> |
|
|
|
|
|
|
|
|
maybe_rm_artifact_and_edge(G, OutDir, SrcExt, Ext, Source) -> |
|
|
%% This is NOT a double check it is the only check that the source file is actually gone |
|
|
%% This is NOT a double check it is the only check that the source file is actually gone |
|
|
case filelib:is_regular(Source) of |
|
|
case filelib:is_regular(Source) of |
|
|
true -> |
|
|
true -> |
|
|
%% Actually exists, don't delete |
|
|
%% Actually exists, don't delete |
|
|
false; |
|
|
false; |
|
|
false -> |
|
|
false -> |
|
|
Target = target_base(OutDir, Source) ++ ".beam", |
|
|
|
|
|
?DEBUG("Source ~ts is gone, deleting previous beam file if it exists ~ts", [Source, Target]), |
|
|
|
|
|
|
|
|
Target = target(OutDir, Source, SrcExt, Ext), |
|
|
|
|
|
?DEBUG("Source ~ts is gone, deleting previous ~ts file if it exists ~ts", [Source, Ext, Target]), |
|
|
file:delete(Target), |
|
|
file:delete(Target), |
|
|
digraph:del_vertex(G, Source), |
|
|
digraph:del_vertex(G, Source), |
|
|
mark_dirty(G), |
|
|
mark_dirty(G), |
|
|
true |
|
|
true |
|
|
end. |
|
|
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) -> |
|
|
|
|
|
|
|
|
maybe_rm_vertex(G, Source) -> |
|
|
|
|
|
case filelib:is_regular(Source) of |
|
|
|
|
|
true -> |
|
|
|
|
|
exists; |
|
|
|
|
|
false -> |
|
|
|
|
|
digraph:del_vertex(G, Source), |
|
|
|
|
|
mark_dirty(G) |
|
|
|
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
%% Add dependencies of a given file to the DAG. If the file is not found yet, |
|
|
|
|
|
%% mark its timestamp to 0, which means we have no info on it. |
|
|
|
|
|
%% Source files will be covered at a later point in their own scan, and |
|
|
|
|
|
%% non-source files are going to be covered by `populate_deps/3'. |
|
|
|
|
|
prepopulate_deps(G, Compiler, InDirs, Source, DepOpts, Status) -> |
|
|
|
|
|
SourceDir = filename:dirname(Source), |
|
|
|
|
|
AbsIncls = case erlang:function_exported(Compiler, dependencies, 4) of |
|
|
|
|
|
false -> |
|
|
|
|
|
Compiler:dependencies(Source, SourceDir, InDirs); |
|
|
|
|
|
true -> |
|
|
|
|
|
Compiler:dependencies(Source, SourceDir, InDirs, DepOpts) |
|
|
|
|
|
end, |
|
|
|
|
|
%% the file hasn't been visited yet; set it to existing, but with |
|
|
|
|
|
%% a last modified value that's null so it gets updated to something new. |
|
|
|
|
|
[digraph:add_vertex(G, Src, 0) || Src <- AbsIncls, |
|
|
|
|
|
digraph:vertex(G, Src) =:= false], |
|
|
|
|
|
%% drop edges from deps that aren't included! |
|
|
|
|
|
[digraph:del_edge(G, Edge) || Status == old, |
|
|
|
|
|
Edge <- digraph:out_edges(G, Source), |
|
|
|
|
|
{_, _Src, Path, _} <- [digraph:edge(G, Edge)], |
|
|
|
|
|
not lists:member(Path, AbsIncls)], |
|
|
|
|
|
%% Add the rest |
|
|
|
|
|
[digraph:add_edge(G, Source, Incl) || Incl <- AbsIncls], |
|
|
|
|
|
ok. |
|
|
|
|
|
|
|
|
|
|
|
%% check that a dep file is up to date |
|
|
|
|
|
refresh_dep(G, File) -> |
|
|
|
|
|
{_, LastUpdated} = digraph:vertex(G, File), |
|
|
|
|
|
case filelib:last_modified(File) of |
|
|
|
|
|
0 -> |
|
|
|
|
|
%% Gone! Erase from the graph |
|
|
|
|
|
digraph:del_vertex(G, File), |
|
|
|
|
|
mark_dirty(G); |
|
|
|
|
|
LastModified when LastUpdated < LastModified -> |
|
|
|
|
|
digraph:add_vertex(G, File, LastModified), |
|
|
|
|
|
mark_dirty(G); |
|
|
|
|
|
_ -> |
|
|
|
|
|
% unchanged |
|
|
|
|
|
ok |
|
|
|
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
%% Do the actual propagation of all files; the files are expected to be |
|
|
|
|
|
%% in a topological order such that we don't need to go more than a level |
|
|
|
|
|
%% deep in what we search. |
|
|
|
|
|
propagate_stamps(_G, []) -> |
|
|
|
|
|
ok; |
|
|
|
|
|
propagate_stamps(G, [File|Files]) -> |
|
|
|
|
|
Stamps = [element(2, digraph:vertex(G, F)) |
|
|
|
|
|
|| F <- digraph:out_neighbours(G, File)], |
|
|
|
|
|
case Stamps of |
|
|
|
|
|
[] -> |
|
|
|
|
|
ok; |
|
|
|
|
|
_ -> |
|
|
|
|
|
Max = lists:max(Stamps), |
|
|
case digraph:vertex(G, File) of |
|
|
case digraph:vertex(G, File) of |
|
|
{_, MaxModified} when MaxModified > Acc -> MaxModified; |
|
|
|
|
|
_ -> Acc |
|
|
|
|
|
|
|
|
{_, Smaller} when Smaller < Max -> |
|
|
|
|
|
digraph:add_vertex(G, File, Max); |
|
|
|
|
|
_ -> |
|
|
|
|
|
ok |
|
|
|
|
|
end |
|
|
|
|
|
end, |
|
|
|
|
|
propagate_stamps(G, Files). |
|
|
|
|
|
|
|
|
|
|
|
%% Do the actual reversal; be aware that only working from the edges |
|
|
|
|
|
%% may omit files, so we have to add all non-dependant apps manually |
|
|
|
|
|
%% to make sure we don't drop em. Since they have no deps, they're |
|
|
|
|
|
%% safer to put first (and compile first) |
|
|
|
|
|
compile_order([], AppPaths, AppDeps) -> |
|
|
|
|
|
%% use a digraph so we don't reimplement topsort by hand. |
|
|
|
|
|
G = digraph:new([acyclic]), % ignore cycles and hope it works |
|
|
|
|
|
Tups = maps:keys(AppDeps), |
|
|
|
|
|
{Va,Vb} = lists:unzip(Tups), |
|
|
|
|
|
[digraph:add_vertex(G, V) || V <- Va], |
|
|
|
|
|
[digraph:add_vertex(G, V) || V <- Vb], |
|
|
|
|
|
[digraph:add_edge(G, V1, V2) || {V1, V2} <- Tups], |
|
|
|
|
|
Sorted = lists:reverse(digraph_utils:topsort(G)), |
|
|
|
|
|
digraph:delete(G), |
|
|
|
|
|
Standalone = [Name || {_, Name} <- AppPaths] -- Sorted, |
|
|
|
|
|
Standalone ++ Sorted; |
|
|
|
|
|
compile_order([{P1,P2}|T], AppPaths, AppDeps) -> |
|
|
|
|
|
%% Assume most dependencies are between files of the same app |
|
|
|
|
|
%% so ask to see if it's the same before doing a deeper check: |
|
|
|
|
|
case find_app(P1, AppPaths) of |
|
|
|
|
|
not_found -> % system lib probably! not in the repo |
|
|
|
|
|
compile_order(T, AppPaths, AppDeps); |
|
|
|
|
|
{P1App, P1Path} -> |
|
|
|
|
|
case find_cached_app(P2, {P1App, P1Path}, AppPaths) of |
|
|
|
|
|
{P2App, _} when P2App =/= P1App -> |
|
|
|
|
|
compile_order(T, AppPaths, AppDeps#{{P1App,P2App} => true}); |
|
|
|
|
|
_ -> |
|
|
|
|
|
compile_order(T, AppPaths, AppDeps) |
|
|
end |
|
|
end |
|
|
end, |
|
|
|
|
|
0, |
|
|
|
|
|
[Source | digraph:out_neighbours(G, Source)] |
|
|
|
|
|
), |
|
|
|
|
|
digraph:add_vertex(G, Source, MaxModified), |
|
|
|
|
|
MaxModified. |
|
|
|
|
|
|
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
%% Swap app name with paths in the order, and sort there; this lets us |
|
|
|
|
|
%% bail out early in a search where a file won't be found. |
|
|
|
|
|
prepare_app_paths(AppPaths) -> |
|
|
|
|
|
lists:sort([{filename:split(Path), Name} || {Name, Path} <- AppPaths]). |
|
|
|
|
|
|
|
|
|
|
|
%% Look for the app to which the path belongs; needed to |
|
|
|
|
|
%% go from an edge between files in the DAG to building |
|
|
|
|
|
%% app-related orderings |
|
|
|
|
|
find_app(Path, AppPaths) -> |
|
|
|
|
|
find_app_(filename:split(Path), AppPaths). |
|
|
|
|
|
|
|
|
|
|
|
%% A cached search for the app to which a path belongs; |
|
|
|
|
|
%% the assumption is that sorted edges and common relationships |
|
|
|
|
|
%% are going to be between local files within an app most |
|
|
|
|
|
%% of the time; so we first look for the same path as a |
|
|
|
|
|
%% prior match to avoid searching _all_ potential candidates. |
|
|
|
|
|
%% If it doesn't work, go for the normal search. |
|
|
|
|
|
find_cached_app(Path, {Name, AppPath}, AppPaths) -> |
|
|
|
|
|
Split = filename:split(Path), |
|
|
|
|
|
case find_app_(Split, [{AppPath, Name}]) of |
|
|
|
|
|
not_found -> find_app_(Split, AppPaths); |
|
|
|
|
|
LastEntry -> LastEntry |
|
|
|
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
%% Do the actual recursive search |
|
|
|
|
|
find_app_(_Path, []) -> |
|
|
|
|
|
not_found; |
|
|
|
|
|
find_app_(Path, [{AppPath, AppName}|Rest]) -> |
|
|
|
|
|
case lists:prefix(AppPath, Path) of |
|
|
|
|
|
true -> |
|
|
|
|
|
{AppName, AppPath}; |
|
|
|
|
|
false when AppPath > Path -> |
|
|
|
|
|
not_found; |
|
|
|
|
|
false -> |
|
|
|
|
|
find_app_(Path, Rest) |
|
|
|
|
|
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", ".erl", ".beam") -> "ebin/my_module.beam" |
|
|
|
|
|
target(OutDir, Source, SrcExt, Ext) -> |
|
|
|
|
|
filename:join(OutDir, filename:basename(Source, SrcExt) ++ Ext). |
|
|
|
|
|
|
|
|
%% Mark the digraph as having been modified, which is required to |
|
|
%% Mark the digraph as having been modified, which is required to |
|
|
%% save its updated form on disk after the compiling run. |
|
|
%% save its updated form on disk after the compiling run. |
|
|