%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you 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.
%%%-------------------------------------------------------------------
%%% @author Joe Armstrong
%%% @author Eric Merritt
%%% @author Konstantin Tcepliaev
%%% @doc
%%% This is a pretty simple topological sort for erlang. It was
%%% originally written for ermake by Joe Armstrong back in '98. It
%%% has been pretty heavily modified by Eric Merritt since '06 and modified again for Relx.
%%% Konstantin Tcepliaev rewrote the algorithm in 2017.
%%%
%%% A partial order on the set S is a set of pairs {Xi,Xj} such that
%%% some relation between Xi and Xj is obeyed.
%%%
%%% A topological sort of a partial order is a sequence of elements
%%% [X1, X2, X3 ...] such that if whenever {Xi, Xj} is in the partial
%%% order i < j
%%%
%%% This particular implementation guarantees that nodes closer to
%%% the top level of the graph will be put as close as possible to
%%% the beginning of the resulting list - this ensures that dependencies
%%% are started as late as possible, and top-level apps are started
%%% as early as possible.
%%% @end
%%%-------------------------------------------------------------------
-module(rlx_topo).
-export([sort_apps/1,
format_error/1]).
-include("relx.hrl").
%%====================================================================
%% API
%%====================================================================
%% @doc This only does a topo sort on the list of applications and
%% assumes that there is only *one* version of each app in the list of
%% applications. This implies that you have already done the
%% constraint solve before you pass the list of apps here to be
%% sorted.
-spec sort_apps([rlx_app_info:t()]) ->
{ok, [rlx_app_info:t()]} |
relx:error().
sort_apps(Apps) ->
AppDeps = [{rlx_app_info:name(App),
rlx_app_info:active_deps(App) ++ rlx_app_info:library_deps(App)}
|| App <- Apps],
{AppNames, _} = lists:unzip(AppDeps),
case lists:foldl(fun iterator/2, {ok, [], AppDeps, []}, AppNames) of
{ok, Names, _, _} ->
{ok, names_to_apps(lists:reverse(Names), Apps)};
E ->
E
end.
%% @doc nicely format the error from the sort.
-spec format_error(Reason::term()) -> iolist().
format_error({cycle, App, Path}) ->
["Cycle detected in dependency graph, this must be resolved "
"before we can continue:\n",
rlx_util:indent(2),
[[erlang:atom_to_list(A), " -> "] || A <- lists:reverse(Path)],
erlang:atom_to_list(App)].
%%====================================================================
%% Internal Functions
%%====================================================================
-type name() :: AppName::atom().
-type app_dep() :: {AppName::name(), [DepName::name()]}.
-type iterator_state() :: {ok, [Acc::name()],
[Apps::app_dep()],
[Path::name()]}.
-spec iterator(name(), iterator_state() | relx:error()) ->
iterator_state() | relx:error().
iterator(App, {ok, Acc, Apps, Path}) ->
case lists:member(App, Acc) of
false ->
%% haven't seen this app yet
case lists:keytake(App, 1, Apps) of
{value, {App, Deps}, NewApps} ->
DepInit = {ok, Acc, NewApps, [App | Path]},
%% recurse over deps
case lists:foldl(fun iterator/2, DepInit, Deps) of
{ok, DepAcc, DepApps, _} ->
{ok, [App | DepAcc], DepApps, Path};
Error ->
Error
end;
false ->
%% we have visited this app before,
%% that means there's a cycle
?RLX_ERROR({cycle, App, Path})
end;
true ->
%% this app and its deps were already processed
{ok, Acc, Apps, Path}
end;
iterator(_, Error) ->
Error.
-spec names_to_apps([atom()], [rlx_app_info:t()]) -> [rlx_app_info:t()].
names_to_apps(Names, Apps) ->
[find_app_by_name(Name, Apps) || Name <- Names].
-spec find_app_by_name(atom(), [rlx_app_info:t()]) -> rlx_app_info:t().
find_app_by_name(Name, Apps) ->
{ok, App1} =
ec_lists:find(fun(App) ->
rlx_app_info:name(App) =:= Name
end, Apps),
App1.
%%====================================================================
%% Tests
%%====================================================================
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
topo_apps_cycle_test() ->
{ok, App1} = rlx_app_info:new(app1, "0.1", "/no-dir", [app2], [stdlib]),
{ok, App2} = rlx_app_info:new(app2, "0.1", "/no-dir", [app1], []),
Apps = [App1, App2],
?assertMatch({error, {_, {cycle, app1, [app2, app1]}}},
sort_apps(Apps)).
topo_apps_good_test() ->
Apps = [App ||
{ok, App} <-
[rlx_app_info:new(app1, "0.1", "/no-dir", [app2, zapp1], [stdlib, kernel]),
rlx_app_info:new(app2, "0.1", "/no-dir", [app3], []),
rlx_app_info:new(app3, "0.1", "/no-dir", [kernel], []),
rlx_app_info:new(zapp1, "0.1", "/no-dir", [app2,app3,zapp2], []),
rlx_app_info:new(stdlib, "0.1", "/no-dir", [], []),
rlx_app_info:new(kernel, "0.1", "/no-dir", [], []),
rlx_app_info:new(zapp2, "0.1", "/no-dir", [], [])]],
{ok, Sorted} = sort_apps(Apps),
?assertMatch([kernel, app3, app2, zapp2, zapp1, stdlib, app1],
[rlx_app_info:name(App) || App <- Sorted]).
topo_apps_1_test() ->
Apps = [App ||
{ok, App} <-
[rlx_app_info:new(app0, "0.1", "/no-dir", [], [stdlib, dep1, dep2, dep3]),
rlx_app_info:new(app1, "0.1", "/no-dir", [], [stdlib, kernel]),
rlx_app_info:new(dep1, "0.1", "/no-dir", [], []),
rlx_app_info:new(dep2, "0.1", "/no-dir", [], []),
rlx_app_info:new(dep3, "0.1", "/no-dir", [], []),
rlx_app_info:new(stdlib, "0.1", "/no-dir", [], []),
rlx_app_info:new(kernel, "0.1", "/no-dir", [], [])]],
{ok, Sorted} = sort_apps(Apps),
?assertMatch([stdlib, dep1, dep2, dep3, app0, kernel, app1],
[rlx_app_info:name(App) || App <- Sorted]).
topo_apps_2_test() ->
Apps = [App ||
{ok, App} <-
[rlx_app_info:new(app1, "0.1", "/no-dir", [app2, app3, app4, app5,
stdlib, kernel],
[]),
rlx_app_info:new(app2, "0.1", "/no-dir", [stdlib, kernel], []),
rlx_app_info:new(app3, "0.1", "/no-dir", [stdlib, kernel], []),
rlx_app_info:new(app4, "0.1", "/no-dir", [stdlib, kernel], []),
rlx_app_info:new(app5, "0.1", "/no-dir", [stdlib, kernel], []),
rlx_app_info:new(stdlib, "0.1", "/no-dir", [], []),
rlx_app_info:new(kernel, "0.1", "/no-dir", [], [])
]],
{ok, Sorted} = sort_apps(Apps),
?assertMatch([stdlib, kernel, app2,
app3, app4, app5, app1],
[rlx_app_info:name(App) || App <- Sorted]).
-endif.