From 748142838cd11ce5cd532dd598d220cab8424f75 Mon Sep 17 00:00:00 2001 From: Geoff Cant Date: Fri, 13 Nov 2015 13:18:09 -0800 Subject: Feature: rebar shell [--script ] Adds the ability to run an escript before starting the apps and interactive shell for a project. This is intended to improve the local development experience for projects by providing an easy way to run companion services (mock rest APIs, databases etc) that the project relies on. This patch also adds {shell, Defaults} to the rebar config file so that a project can supply default values for many of the new or improved 'rebar3 shell' options: * {apps, OTPApps} * {script_file, EscriptFileName} * {config, ConfigFileName} The order of option precedence is command line, rebar.config, relx. --- src/rebar_prv_shell.erl | 160 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 131 insertions(+), 29 deletions(-) diff --git a/src/rebar_prv_shell.erl b/src/rebar_prv_shell.erl index 4cf1e04..d93a21f 100644 --- a/src/rebar_prv_shell.erl +++ b/src/rebar_prv_shell.erl @@ -56,12 +56,23 @@ init(State) -> {short_desc, "Run shell with project apps and deps in path."}, {desc, info()}, {opts, [{config, undefined, "config", string, - "Path to the config file to use. Defaults to the " - "sys_config defined for relx, if present."}, + "Path to the config file to use. Defaults to " + "{shell, [{config, File}]} and then the relx " + "sys.config file if not specified."}, {name, undefined, "name", atom, "Gives a long name to the node."}, {sname, undefined, "sname", atom, - "Gives a short name to the node."}]} + "Gives a short name to the node."}, + {script_file, undefined, "script", string, + "Path to an escript file to run before " + "starting the project apps. Defaults to " + "rebar.config {shell, [{script_file, File}]} " + "if not specified."}, + {apps, undefined, "apps", string, + "A list of apps to boot before starting the " + "shell. (E.g. --apps app1,app2,app3) Defaults " + "to rebar.config {shell, [{apps, Apps}]} or " + "relx apps if not specified."}]} ]) ), {ok, State1}. @@ -86,6 +97,7 @@ shell(State) -> setup_name(State), setup_paths(State), setup_shell(), + maybe_run_script(State), %% apps must be started after the change in shell because otherwise %% their application masters never gets the new group leader (held in %% their internal state) @@ -134,6 +146,51 @@ setup_paths(State) -> %% add project app test paths ok = add_test_paths(State). +maybe_run_script(State) -> + case first_value([fun find_script_option/1, + fun find_script_rebar/1], State) of + no_value -> + ?DEBUG("No script_file specified.", []), + ok; + "none" -> + ?DEBUG("Shell script execution skipped (--script none).", []), + ok; + RelFile -> + File = filename:absname(RelFile), + try run_script_file(File) + catch + C:E -> + ?ABORT("Couldn't run shell escript ~p - ~p:~p~nStack: ~p", + [File, C, E, erlang:get_stacktrace()]) + end + end. + +-spec find_script_option(rebar_state:t()) -> no_value | list(). +find_script_option(State) -> + {Opts, _} = rebar_state:command_parsed_args(State), + debug_get_value(script_file, Opts, no_value, + "Found script file from command line option."). + +-spec find_script_rebar(rebar_state:t()) -> no_value | list(). +find_script_rebar(State) -> + Config = rebar_state:get(State, shell, []), + %% Either a string, or undefined + debug_get_value(script_file, Config, no_value, + "Found script file from rebar config file."). + +run_script_file(File) -> + ?DEBUG("Extracting escript from ~p", [File]), + {ok, Script} = escript:extract(File, [compile_source]), + Beam = proplists:get_value(source, Script), + Mod = proplists:get_value(module, beam_lib:info(Beam)), + ?DEBUG("Compiled escript as ~p", [Mod]), + FakeFile = "/fake_path/" ++ atom_to_list(Mod), + {module, Mod} = code:load_binary(Mod, FakeFile, Beam), + ?DEBUG("Evaling ~p:main([]).", [Mod]), + Result = Mod:main([]), + ?DEBUG("Result: ~p", [Result]), + Result. + maybe_boot_apps(State) -> case find_apps_to_boot(State) of undefined -> @@ -172,17 +229,42 @@ check_epmd(_) -> find_apps_to_boot(State) -> %% Try the shell_apps option - case rebar_state:get(State, shell_apps, undefined) of - undefined -> - %% Get to the relx tuple instead - case lists:keyfind(release, 1, rebar_state:get(State, relx, [])) of - {_, _, Apps} -> Apps; - false -> undefined - end; + case first_value([fun find_apps_option/1, + fun find_apps_rebar/1, + fun find_apps_relx/1], State) of + no_value -> + undefined; Apps -> Apps end. +-spec find_apps_option(rebar_state:t()) -> no_value | [atom()]. +find_apps_option(State) -> + {Opts, _} = rebar_state:command_parsed_args(State), + case debug_get_value(apps, Opts, no_value, + "Found shell apps from command line option.") of + no_value -> no_value; + AppsStr -> + [ list_to_atom(AppStr) + || AppStr <- string:tokens(AppsStr, " ,:") ] + end. + +-spec find_apps_rebar(rebar_state:t()) -> no_value | list(). +find_apps_rebar(State) -> + ShellOpts = rebar_state:get(State, shell, []), + debug_get_value(apps, ShellOpts, no_value, + "Found shell opts from command line option."). + +-spec find_apps_relx(rebar_state:t()) -> no_value | list(). +find_apps_relx(State) -> + case lists:keyfind(release, 1, rebar_state:get(State, relx, [])) of + {_, _, Apps} -> + ?DEBUG("Found shell apps from relx.", []), + Apps; + false -> + no_value + end. + load_apps(Apps) -> [case application:load(App) of ok -> @@ -261,31 +343,51 @@ add_test_paths(State) -> % First try the --config flag, then try the relx sys_config -spec find_config(rebar_state:t()) -> [tuple()] | no_config. find_config(State) -> - case find_config_option(State) of - no_config -> - find_config_relx(State); - Result -> - Result + case first_value([fun find_config_option/1, + fun find_config_rebar/1, + fun find_config_relx/1], State) of + no_value -> + no_config; + Filename when is_list(Filename) -> + consult_config(State, Filename) + end. + +-spec first_value([Fun], State) -> no_value | Value when + Value :: any(), + State :: rebar_state:t(), + Fun :: fun ((State) -> no_value | Value). +first_value([], _) -> no_value; +first_value([Fun | Rest], State) -> + case Fun(State) of + no_value -> + first_value(Rest, State); + Value -> + Value end. --spec find_config_option(rebar_state:t()) -> [tuple()] | no_config. +debug_get_value(Key, List, Default, Description) -> + case proplists:get_value(Key, List, Default) of + Default -> Default; + Value -> + ?DEBUG(Description, []), + Value + end. + +-spec find_config_option(rebar_state:t()) -> Filename::list() | no_value. find_config_option(State) -> {Opts, _} = rebar_state:command_parsed_args(State), - case proplists:get_value(config, Opts) of - undefined -> - no_config; - Filename -> - consult_config(State, Filename) - end. + debug_get_value(config, Opts, no_value, + "Found config from command line option."). --spec find_config_relx(rebar_state:t()) -> [tuple()] | no_config. +-spec find_config_rebar(rebar_state:t()) -> [tuple()] | no_value. +find_config_rebar(State) -> + debug_get_value(config, rebar_state:get(State, shell, []), no_value, + "Found config from rebar config file."). + +-spec find_config_relx(rebar_state:t()) -> [tuple()] | no_value. find_config_relx(State) -> - case proplists:get_value(sys_config, rebar_state:get(State, relx, [])) of - undefined -> - no_config; - Filename -> - consult_config(State, Filename) - end. + debug_get_value(sys_config, rebar_state:get(State, relx, []), no_value, + "Found config from relx."). -spec consult_config(rebar_state:t(), string()) -> [tuple()]. consult_config(State, Filename) -> -- cgit v1.1