aboutsummaryrefslogblamecommitdiffstats
path: root/src/rlx_topo.erl
blob: f8fc5ad2679e63d4ba37835a2f6f2195cf1e3a8c (plain) (tree)


















                                                                         
                                



                                                                                            
                                                        






                                                                      





                                                                         



                                                                      
                     




                                                                      











                                                                      






                                                                                



             

                                               
                                   

                                                                 


                                                                    



                                                                      


































                                                                      











                                                                        





                                                                      



                                                                            
                                                           












                                                                                              














                                                                                             




















                                                                                     
%% -*- 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.