nixpkgs/pkgs/development/tools/build-managers/rebar3/rebar3-nix-bootstrap
2017-09-11 22:56:43 +02:00

300 lines
11 KiB
Erlang
Executable file

#!/usr/bin/env escript
%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
%%! -smp enable
%%% ---------------------------------------------------------------------------
%%% @doc
%%% The purpose of this command is to prepare a rebar3 project so that
%%% rebar3 understands that the dependencies are all already
%%% installed. If you want a hygienic build on nix then you must run
%%% this command before running rebar3. I suggest that you add a
%%% `Makefile` to your project and have the bootstrap command be a
%%% dependency of the build commands. See the nix documentation for
%%% more information.
%%%
%%% This command designed to have as few dependencies as possible so
%%% that it can be a dependency of root level packages like rebar3. To
%%% that end it does many things in a fairly simplistic way. That is
%%% by design.
%%%
%%% ### Assumptions
%%%
%%% This command makes the following assumptions:
%%%
%%% * It is run in a nix-shell or nix-build environment
%%% * that all dependencies have been added to the ERL_LIBS
%%% Environment Variable
-record(data, {version
, registry_only = false
, debug_info = false
, compile_ports
, erl_libs
, plugins
, root
, name
, registry_snapshot}).
-define(HEX_REGISTRY_PATH, ".cache/rebar3/hex/default/registry").
main(Args) ->
{ok, ArgData} = parse_args(Args),
{ok, RequiredData} = gather_required_data_from_the_environment(ArgData),
do_the_bootstrap(RequiredData).
%% @doc There are two modes 'registry_only' where the register is
%% created from hex and everything else.
-spec do_the_bootstrap(#data{}) -> ok.
do_the_bootstrap(RequiredData = #data{registry_only = true}) ->
ok = bootstrap_registry(RequiredData);
do_the_bootstrap(RequiredData) ->
ok = bootstrap_registry(RequiredData),
ok = bootstrap_configs(RequiredData),
ok = bootstrap_plugins(RequiredData),
ok = bootstrap_libs(RequiredData).
%% @doc
%% Argument parsing is super simple only because we want to keep the
%% dependencies minimal. For now there can be two entries on the
%% command line, "registry-only" and "debug-info"
-spec parse_args([string()]) -> #data{}.
parse_args(Args0) ->
PossibleArgs = sets:from_list(["registry-only", "debug-info"]),
Args1 = sets:from_list(Args0),
Result = sets:subtract(Args1, PossibleArgs),
case sets:to_list(Result) of
[] ->
{ok, #data{registry_only = sets:is_element("registry-only", Args1),
debug_info = sets:is_element("debug-info", Args1)}};
UnknownArgs ->
io:format("Unexpected command line arguments passed in: ~p~n",
[UnknownArgs]),
erlang:halt(120)
end.
-spec bootstrap_configs(#data{}) -> ok.
bootstrap_configs(RequiredData)->
io:format("Boostrapping app and rebar configurations~n"),
ok = if_single_app_project_update_app_src_version(RequiredData),
ok = if_compile_ports_add_pc_plugin(RequiredData),
ok = if_debug_info_add(RequiredData).
-spec bootstrap_plugins(#data{}) -> ok.
bootstrap_plugins(#data{plugins = Plugins}) ->
io:format("Bootstrapping rebar3 plugins~n"),
Target = "_build/default/plugins/",
Paths = string:tokens(Plugins, " "),
CopiableFiles =
lists:foldl(fun(Path, Acc) ->
gather_dependency(Path) ++ Acc
end, [], Paths),
lists:foreach(fun (Path) ->
ok = link_app(Path, Target)
end, CopiableFiles).
-spec bootstrap_libs(#data{}) -> ok.
bootstrap_libs(#data{erl_libs = ErlLibs}) ->
io:format("Bootstrapping dependent libraries~n"),
Target = "_build/default/lib/",
Paths = string:tokens(ErlLibs, ":"),
CopiableFiles =
lists:foldl(fun(Path, Acc) ->
gather_directory_contents(Path) ++ Acc
end, [], Paths),
lists:foreach(fun (Path) ->
ok = link_app(Path, Target)
end, CopiableFiles).
-spec gather_dependency(string()) -> [{string(), string()}].
gather_dependency(Path) ->
FullLibrary = filename:join(Path, "lib/erlang/lib/"),
case filelib:is_dir(FullLibrary) of
true ->
gather_directory_contents(FullLibrary);
false ->
[raw_hex(Path)]
end.
-spec raw_hex(string()) -> {string(), string()}.
raw_hex(Path) ->
[_, Name] = re:split(Path, "-hex-source-"),
{Path, erlang:binary_to_list(Name)}.
-spec gather_directory_contents(string()) -> [{string(), string()}].
gather_directory_contents(Path) ->
{ok, Names} = file:list_dir(Path),
lists:map(fun(AppName) ->
{filename:join(Path, AppName), fixup_app_name(AppName)}
end, Names).
%% @doc
%% Makes a symlink from the directory pointed at by Path to a
%% directory of the same name in Target. So if we had a Path of
%% {`foo/bar/baz/bash`, `baz`} and a Target of `faz/foo/foos`, the symlink
%% would be `faz/foo/foos/baz`.
-spec link_app({string(), string()}, string()) -> ok.
link_app({Path, TargetFile}, TargetDir) ->
Target = filename:join(TargetDir, TargetFile),
make_symlink(Path, Target).
-spec make_symlink(string(), string()) -> ok.
make_symlink(Path, TargetFile) ->
file:delete(TargetFile),
ok = filelib:ensure_dir(TargetFile),
io:format("Making symlink from ~s to ~s~n", [Path, TargetFile]),
ok = file:make_symlink(Path, TargetFile).
%% @doc
%% This takes an app name in the standard OTP <name>-<version> format
%% and returns just the app name. Why? Because rebar doesn't
%% respect OTP conventions in some cases.
-spec fixup_app_name(string()) -> string().
fixup_app_name(FileName) ->
case string:tokens(FileName, "-") of
[Name] -> Name;
[Name, _Version] -> Name;
[Name, _Version, _Tag] -> Name
end.
-spec bootstrap_registry(#data{}) -> ok.
bootstrap_registry(#data{registry_snapshot = RegistrySnapshot}) ->
io:format("Bootstrapping Hex Registry for Rebar~n"),
make_sure_registry_snapshot_exists(RegistrySnapshot),
filelib:ensure_dir(?HEX_REGISTRY_PATH),
ok = case filelib:is_file(?HEX_REGISTRY_PATH) of
true ->
file:delete(?HEX_REGISTRY_PATH);
false ->
ok
end,
ok = file:make_symlink(RegistrySnapshot,
?HEX_REGISTRY_PATH).
-spec make_sure_registry_snapshot_exists(string()) -> ok.
make_sure_registry_snapshot_exists(RegistrySnapshot) ->
case filelib:is_file(RegistrySnapshot) of
true ->
ok;
false ->
stderr("Registry snapshot (~s) does not exist!", [RegistrySnapshot]),
erlang:halt(1)
end.
-spec gather_required_data_from_the_environment(#data{}) -> {ok, #data{}}.
gather_required_data_from_the_environment(ArgData) ->
{ok, ArgData#data{ version = guard_env("version")
, erl_libs = get_env("ERL_LIBS", [])
, plugins = get_env("buildPlugins", [])
, root = code:root_dir()
, name = guard_env("name")
, compile_ports = nix2bool(get_env("compilePorts", ""))
, registry_snapshot = guard_env("HEX_REGISTRY_SNAPSHOT")}}.
-spec nix2bool(any()) -> boolean().
nix2bool("1") ->
true;
nix2bool("") ->
false.
get_env(Name) ->
os:getenv(Name).
get_env(Name, Def) ->
case get_env(Name) of
false -> Def;
Val -> Val
end.
-spec guard_env(string()) -> string().
guard_env(Name) ->
case get_env(Name) of
false ->
stderr("Expected Environment variable ~s! Are you sure you are "
"running in a Nix environment? Either a nix-build, "
"nix-shell, etc?~n", [Name]),
erlang:halt(1);
Variable ->
Variable
end.
%% @doc
%% If debug info is set we need to add debug info to the list of compile options
%%
-spec if_debug_info_add(#data{}) -> ok.
if_debug_info_add(#data{debug_info = true}) ->
ConfigTerms = add_debug_info(read_rebar_config()),
Text = lists:map(fun(Term) -> io_lib:format("~tp.~n", [Term]) end,
ConfigTerms),
file:write_file("rebar.config", Text);
if_debug_info_add(_) ->
ok.
-spec add_debug_info([term()]) -> [term()].
add_debug_info(Config) ->
ExistingOpts = case lists:keysearch(erl_opts, 1, Config) of
{value, {erl_opts, ExistingOptsList}} -> ExistingOptsList;
_ -> []
end,
case lists:member(debug_info, ExistingOpts) of
true ->
Config;
false ->
lists:keystore(erl_opts, 1, Config,
{erl_opts, [debug_info | ExistingOpts]})
end.
%% @doc
%% If the compile ports flag is set, rewrite the rebar config to
%% include the 'pc' plugin.
-spec if_compile_ports_add_pc_plugin(#data{}) -> ok.
if_compile_ports_add_pc_plugin(#data{compile_ports = true}) ->
ConfigTerms = add_pc_to_plugins(read_rebar_config()),
Text = lists:map(fun(Term) -> io_lib:format("~tp.~n", [Term]) end,
ConfigTerms),
file:write_file("rebar.config", Text);
if_compile_ports_add_pc_plugin(_) ->
ok.
-spec add_pc_to_plugins([term()]) -> [term()].
add_pc_to_plugins(Config) ->
PluginList = case lists:keysearch(plugins, 1, Config) of
{value, {plugins, ExistingPluginList}} -> ExistingPluginList;
_ -> []
end,
lists:keystore(plugins, 1, Config, {plugins, [pc | PluginList]}).
-spec read_rebar_config() -> [term()].
read_rebar_config() ->
case file:consult("rebar.config") of
{ok, Terms} ->
Terms;
_ ->
stderr("Unable to read rebar config!", []),
erlang:halt(1)
end.
-spec if_single_app_project_update_app_src_version(#data{}) -> ok.
if_single_app_project_update_app_src_version(#data{name = Name,
version = Version}) ->
SrcFile = filename:join("src",
lists:concat([Name, ".app.src"])),
case filelib:is_file(SrcFile) of
true ->
update_app_src_with_version(SrcFile, Version);
false ->
ok
end.
-spec update_app_src_with_version(string(), string()) -> ok.
update_app_src_with_version(SrcFile, Version) ->
{ok, [{application, Name, Details}]} = file:consult(SrcFile),
NewDetails = lists:keyreplace(vsn, 1, Details, {vsn, Version}),
ok = file:write_file(SrcFile, io_lib:fwrite("~p.\n", [{application, Name, NewDetails}])).
%% @doc
%% Write the result of the format string out to stderr.
-spec stderr(string(), [term()]) -> ok.
stderr(FormatStr, Args) ->
io:put_chars(standard_error, io_lib:format(FormatStr, Args)).