From 2311bc5cbe06dcf2cb03d9b0a93392e1d9095bbb Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 12 Sep 2012 16:27:41 -0500 Subject: support OTP App discovery in the system --- src/rcl_app_info.erl | 134 ++++++++++++++++++++++++++++++++++++ src/rcl_prv_discover.erl | 175 +++++++++++++++++++++++++++++++++++++++++++++++ src/rcl_state.erl | 120 +++++++++++++++++++++++++++----- 3 files changed, 413 insertions(+), 16 deletions(-) create mode 100644 src/rcl_app_info.erl create mode 100644 src/rcl_prv_discover.erl (limited to 'src') diff --git a/src/rcl_app_info.erl b/src/rcl_app_info.erl new file mode 100644 index 0000000..eaf3302 --- /dev/null +++ b/src/rcl_app_info.erl @@ -0,0 +1,134 @@ +%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- +%%% 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 Eric Merritt +%%% @copyright (C) 2012 Erlware, LLC. +%%% +%%% @doc This module represents useful, relevant information about an +%%% application. The relevant information is. +%%% +%%% +%%% +-module(rcl_app_info). + +-export([new/0, + new/5, + name/1, + name/2, + vsn/1, + vsn/2, + dir/1, + dir/2, + active_deps/1, + active_deps/2, + library_deps/1, + library_deps/2]). + +-export_type([t/0]). + +-record(app_info_t, {name :: atom(), + vsn :: string(), + dir :: file:name(), + active_deps :: [atom()], + library_deps :: [atom()]}). + +%%============================================================================ +%% types +%%============================================================================ +-opaque t() :: record(app_info_t). + +%%============================================================================ +%% API +%% ============================================================================ +%% @doc Build a new, empty, app info value. This is not of a lot of use and you +%% probably wont be doing this much. +-spec new() -> t(). +new() -> + #app_info_t{}. + +%% @doc build a complete version of the app info with all fields set. +-spec new(atom(), string(), file:name(), [atom()], [atom()]) -> t(). +new(AppName, Vsn, Dir, ActiveDeps, LibraryDeps) + when erlang:is_atom(AppName), + erlang:is_list(Vsn), + erlang:is_list(Dir), + erlang:is_list(ActiveDeps), + erlang:is_list(LibraryDeps) -> + #app_info_t{name=AppName, vsn=Vsn, dir=Dir, + active_deps=ActiveDeps, + library_deps=LibraryDeps}. + +-spec name(t()) -> atom(). +name(#app_info_t{name=Name}) -> + Name. + +-spec name(t(), atom()) -> t(). +name(AppInfo=#app_info_t{}, AppName) + when erlang:is_atom(AppName) -> + AppInfo#app_info_t{name=AppName}. + +-spec vsn(t()) -> string(). +vsn(#app_info_t{vsn=Vsn}) -> + Vsn. + +-spec vsn(t(), string()) -> t(). +vsn(AppInfo=#app_info_t{}, AppVsn) + when erlang:is_list(AppVsn) -> + AppInfo#app_info_t{vsn=AppVsn}. + +-spec dir(t()) -> file:name(). +dir(#app_info_t{dir=Dir}) -> + Dir. +-spec dir(t(), file:name()) -> t(). +dir(AppInfo=#app_info_t{}, Dir) -> + AppInfo#app_info_t{dir=Dir}. + +-spec active_deps(t()) -> [atom()]. +active_deps(#app_info_t{active_deps=Deps}) -> + Deps. +-spec active_deps(t(), [atom()]) -> t(). +active_deps(AppInfo=#app_info_t{}, ActiveDeps) + when erlang:is_list(ActiveDeps) -> + AppInfo#app_info_t{active_deps=ActiveDeps}. + +-spec library_deps(t()) -> [atom()]. +library_deps(#app_info_t{library_deps=Deps}) -> + Deps. + +-spec library_deps(t(), [atom()]) -> t(). +library_deps(AppInfo=#app_info_t{}, LibraryDeps) + when erlang:is_list(LibraryDeps) -> + AppInfo#app_info_t{library_deps=LibraryDeps}. + + + + + +%%%=================================================================== +%%% Test Functions +%%%=================================================================== diff --git a/src/rcl_prv_discover.erl b/src/rcl_prv_discover.erl new file mode 100644 index 0000000..e9a6dea --- /dev/null +++ b/src/rcl_prv_discover.erl @@ -0,0 +1,175 @@ +%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- +%%% 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 Eric Merritt +%%% @copyright (C) 2012 Erlware, LLC. +%%% +%%% @doc This provider uses the lib_dir setting of the state. It searches the +%%% Lib Dirs looking for all OTP Applications that are available. When it finds +%%% those OTP Applications it loads the information about them and adds them to +%%% the state of available apps. This implements the rcl_provider behaviour. +-module(rcl_prv_discover). + +-behaviour(rcl_provider). + +-export([init/1, + do/1, + format/1]). + +%%============================================================================ +%% API +%%============================================================================ +-spec init(rcl_state:t()) -> {ok, rcl_state:t()}. +init(State) -> + {ok, State}. + +-spec do(rcl_state:t()) -> {error, Reason::term()} | {ok, rcl_state:t()}. +do(State) -> + LibDirs = rcl_state:lib_dirs(State), + AppMeta = lists:flatten(ec_plists:map(fun discover_dir/1, LibDirs)), + Errors = [case El of + {error, Ret} -> Ret; + _ -> El + end + || El <- AppMeta, + case El of + {error, _} -> + true; + _ -> + false + end], + + lists:filter(fun({error, _}) -> true; + (_) -> false + end, AppMeta), + case Errors of + [] -> + {ok, rcl_state:available_apps(State, lists:flatten(AppMeta))}; + _ -> + {error, Errors} + end. + +-spec format({error, [ErrorDetail::term()]}) -> iolist(). +format({error, ErrorDetails}) -> + [[format_detail(ErrorDetail), "\n"] || ErrorDetail <- ErrorDetails]. + +%%%=================================================================== +%%% Internal Functions +%%%=================================================================== +-spec format_detail(ErrorDetail::term()) -> iolist(). +format_detail({accessing, File, eaccess}) -> + io_lib:format("permission denied accessing file ~s", [File]); +format_detail({accessing, File, Type}) -> + io_lib:format("error (~p) accessing file ~s", [Type, File]); +format_detail({error, {no_beam_files, EbinDir}}) -> + io_lib:format("no beam files found in directory ~s", [EbinDir]); +format_detail({error, {not_a_directory, EbinDir}}) -> + io_lib:format("~s is not a directory when it should be a directory", [EbinDir]); +format_detail({error, {unable_to_load_app, AppDir, _}}) -> + io_lib:format("Unable to load the application metadata from ~s", [AppDir]); +format_detail({error, {invalid_app_file, File}}) -> + io_lib:format("Application metadata file exists but is malformed: ~s", + [File]); +format_detail({error, {unversioned_app, AppDir, _AppName}}) -> + io_lib:format("Application metadata exists but version is not available: ~s", + [AppDir]). + +-spec discover_dir(file:name()) -> + [rcl_app_info:t() | {error, Reason::term()}] | + rcl_app_info:t() | {error, Reason::term()}. +discover_dir(File) -> + case filelib:is_dir(File) of + true -> + case file:list_dir(File) of + {error, Reason} -> + {error, {accessing, File, Reason}}; + {ok, List} -> + ec_plists:map(fun discover_dir/1, [filename:join([File, Dir]) || Dir <- List]) + end; + false -> + is_valid_otp_app(File) + end. + +-spec is_valid_otp_app(file:name()) -> [rcl_app_info:t() | {error, Reason::term()} | []]. +is_valid_otp_app(File) -> + %% Is this an ebin dir? + EbinDir = filename:dirname(File), + case filename:basename(EbinDir) of + "ebin" -> + case lists:suffix(".app", File) of + true -> + has_at_least_one_beam(EbinDir, File); + false -> + [] + end; + _ -> + [] + end. + +-spec has_at_least_one_beam(file:name(), file:filename()) -> + rcl_app_info:t() | {error, Reason::term()}. +has_at_least_one_beam(EbinDir, File) -> + case file:list_dir(EbinDir) of + {ok, List} -> + case lists:any(fun(NFile) -> lists:suffix(".beam", NFile) end, List) of + true -> + gather_application_info(EbinDir, File); + false -> + {error, {no_beam_files, EbinDir}} + end; + _ -> + {error, {not_a_directory, EbinDir}} + end. + +-spec gather_application_info(file:name(), file:filename()) -> + rcl_app_info:t() | {error, Reason::term()}. +gather_application_info(EbinDir, File) -> + AppDir = filename:dirname(EbinDir), + case file:consult(File) of + {ok, [{application, AppName, AppDetail}]} -> + get_vsn(AppDir, AppName, AppDetail); + {error, Reason} -> + {error, {unable_to_load_app, AppDir, Reason}}; + _ -> + {error, {invalid_app_file, File}} + end. + +-spec get_vsn(file:name(), atom(), proplists:proplist()) -> + rcl_app_info:t() | {error, Reason::term()}. +get_vsn(AppDir, AppName, AppDetail) -> + case proplists:get_value(vsn, AppDetail) of + undefined -> + {error, {unversioned_app, AppDir, AppName}}; + AppVsn -> + get_deps(AppDir, AppName, AppVsn, AppDetail) + end. + +-spec get_deps(file:name(), atom(), string(), proplists:proplist()) -> + rcl_app_info:t(). +get_deps(AppDir, AppName, AppVsn, AppDetail) -> + ActiveApps = proplists:get_value(applications, AppDetail, []), + LibraryApps = proplists:get_value(included_applications, AppDetail, []), + rcl_app_info:new(AppName, AppVsn, AppDir, ActiveApps, LibraryApps). + +%%%=================================================================== +%%% Test Functions +%%%=================================================================== + +-ifndef(NOTEST). +-include_lib("eunit/include/eunit.hrl"). + +-endif. diff --git a/src/rcl_state.erl b/src/rcl_state.erl index 1b25ce3..c399b42 100644 --- a/src/rcl_state.erl +++ b/src/rcl_state.erl @@ -29,24 +29,47 @@ lib_dirs/1, goals/1, config_files/1, + providers/1, + providers/2, + add_release/2, + get_release/3, + releases/1, + default_release/1, + default_release/3, + available_apps/1, + available_apps/2, format/1, format/2]). + -export_type([t/0, + app_descriptor/0, + releases/0, cmd_args/0]). --record(?MODULE, {log :: rcl_log:t(), +-record(state_t, {log :: rcl_log:t(), output_dir :: file:name(), lib_dirs=[] :: [file:name()], config_files=[] :: [file:filename()], - goals=[] :: [depsolver:constraint()]}). + goals=[] :: [depsolver:constraint()], + providers = [] :: [rcl_provider:t()], + available_apps = [] :: [app_descriptor()], + default_release :: {rcl_release:name(), rcl_release:vsn()}, + releases :: ec_dictionary:dictionary({ReleaseName::atom(), + ReleaseVsn::string()}, + rcl_release:t())}). %%============================================================================ %% types %%============================================================================ +-type app_descriptor() :: {rcl_release:app_name(), rcl_release:app_vsn(), file:name()}. + +-type releases() :: ec_dictionary:dictionary({rcl_release:name(), + rcl_release:vsn()}, + rcl_release:t()). -type cmd_args() :: proplists:proplist(). --opaque t() :: record(?MODULE). +-opaque t() :: record(state_t). %%============================================================================ %% API @@ -54,40 +77,84 @@ %% @doc Create a new 'log level' for the system -spec new(proplists:proplist(), [file:filename()]) -> t(). new(PropList, Targets) when erlang:is_list(PropList) -> - #?MODULE{log = proplists:get_value(log, PropList, rcl_log:new(error)), - output_dir=proplists:get_value(output_dir, PropList, ""), - lib_dirs=proplists:get_value(lib_dirs, PropList, []), - config_files=Targets, - goals=proplists:get_value(goals, PropList, [])}. + State0 = + #state_t{log = proplists:get_value(log, PropList, rcl_log:new(error)), + output_dir=proplists:get_value(output_dir, PropList, ""), + lib_dirs=get_lib_dirs(proplists:get_value(lib_dirs, PropList, [])), + config_files=Targets, + goals=proplists:get_value(goals, PropList, []), + providers = [], + releases=ec_dictionary:new(ec_dict)}, + create_logic_providers(State0). %% @doc get the current log state for the system -spec log(t()) -> rcl_log:t(). -log(#?MODULE{log=LogState}) -> +log(#state_t{log=LogState}) -> LogState. -spec output_dir(t()) -> file:name(). -output_dir(#?MODULE{output_dir=OutDir}) -> +output_dir(#state_t{output_dir=OutDir}) -> OutDir. -spec lib_dirs(t()) -> [file:name()]. -lib_dirs(#?MODULE{lib_dirs=LibDir}) -> +lib_dirs(#state_t{lib_dirs=LibDir}) -> LibDir. -spec goals(t()) -> [depsolver:constraints()]. -goals(#?MODULE{goals=TS}) -> +goals(#state_t{goals=TS}) -> TS. -spec config_files(t()) -> [file:filename()]. -config_files(#?MODULE{config_files=ConfigFiles}) -> +config_files(#state_t{config_files=ConfigFiles}) -> ConfigFiles. +-spec providers(t()) -> [rcl_provider:t()]. +providers(#state_t{providers=Providers}) -> + Providers. + +-spec providers(t(), [rcl_provider:t()]) -> t(). +providers(M, NewProviders) -> + M#state_t{providers=NewProviders}. + +-spec add_release(t(), rcl_release:t()) -> t(). +add_release(M=#state_t{releases=Releases}, Release) -> + M#state_t{releases=ec_dictionary:add({rcl_release:name(Release), + rcl_release:vsn(Release)}, + Release, + Releases)}. + +-spec get_release(t(), rcl_release:name(), rcl_release:vsn()) -> rcl_release:t(). +get_release(#state_t{releases=Releases}, Name, Vsn) -> + ec_dictionary:get({Name, Vsn}, Releases). + +-spec releases(t()) -> releases(). +releases(#state_t{releases=Releases}) -> + Releases. + +-spec default_release(t()) -> {rcl_release:name(), rcl_release:vsn()}. +default_release(#state_t{default_release=Def}) -> + Def. + +-spec default_release(t(), rcl_release:name(), rcl_release:vsn()) -> t(). +default_release(M, Name, Vsn) -> + M#state_t{default_release={Name, Vsn}}. + +-spec available_apps(t()) -> [app_descriptor()]. +available_apps(#state_t{available_apps=Apps}) -> + Apps. + +-spec available_apps(t(), [app_descriptor()]) -> t(). +available_apps(M, NewApps) -> + M#state_t{available_apps=NewApps}. + -spec format(t()) -> iolist(). format(Mod) -> format(Mod, 0). -spec format(t(), non_neg_integer()) -> iolist(). -format(#?MODULE{log=LogState, output_dir=OutDir, lib_dirs=LibDirs, - goals=Goals, config_files=ConfigFiles}, +format(#state_t{log=LogState, output_dir=OutDir, lib_dirs=LibDirs, + goals=Goals, config_files=ConfigFiles, + providers=Providers}, Indent) -> [rcl_util:indent(Indent), <<"state:\n">>, @@ -98,7 +165,28 @@ format(#?MODULE{log=LogState, output_dir=OutDir, lib_dirs=LibDirs, [[rcl_util:indent(Indent + 2), depsolver:format_constraint(Goal), ",\n"] || Goal <- Goals], rcl_util:indent(Indent + 1), "output_dir: ", OutDir, "\n", rcl_util:indent(Indent + 1), "lib_dirs: \n", - [[rcl_util:indent(Indent + 2), LibDir, ",\n"] || LibDir <- LibDirs]]. + [[rcl_util:indent(Indent + 2), LibDir, ",\n"] || LibDir <- LibDirs], + rcl_util:indent(Indent + 1), "providers: \n", + [[rcl_util:indent(Indent + 2), rcl_provider:format(Provider), ",\n"] || Provider <- Providers]]. + +%%%=================================================================== +%%% Internal Functions +%%%=================================================================== +-spec get_lib_dirs([file:name()]) -> [file:name()]. +get_lib_dirs(CmdDirs) -> + case os:getenv("ERL_LIBS") of + false -> + CmdDirs; + EnvString -> + [Lib || Lib <- re:split(EnvString, ":|;"), + filelib:is_dir(Lib)] ++ CmdDirs + end. + +-spec create_logic_providers(t()) -> t(). +create_logic_providers(State0) -> + {ConfigProvider, {ok, State1}} = rcl_provider:new(rcl_prv_config, State0), + {DiscoveryProvider, {ok, State2}} = rcl_provider:new(rcl_prv_discover, State1), + State2#state_t{providers=[ConfigProvider, DiscoveryProvider]}. %%%=================================================================== %%% Test Functions -- cgit v1.2.3