Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overhaul everything. #12

Merged
merged 7 commits into from Jan 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
52 changes: 42 additions & 10 deletions README.md
@@ -1,32 +1,64 @@
rebar_prv_alpaca
Alpaca - Rebar3 Plugin
=====

rebar3 plugin for compiling [alpaca](https://github.com/alpaca-lang/alpaca) modules.
A rebar3 plugin for compiling [alpaca](https://github.com/alpaca-lang/alpaca) modules.

Alpaca modules will be compiled alongside Erlang ones. Pulling in other Alpaca
libraries as dependencies as well as performing incremental builds is also supported.

Use
---

From 0.2.8, Alpaca must be installed. Please see instructions at http://alpaca-lang.org.
Essentially, you need to download an appropriate release from
https://github.com/alpaca-lang/alpaca/releases and ensure it is saved either in a well-known
location (`/usr/lib/alpaca`, `/usr/local/lib/alpaca`, or `/opt/alpaca`) or install it
wherever you wish and set the `ALPACA_ROOT` environmental variable to the path where you
placed the Alpaca release.

Add the plugin to your rebar config:

```
{plugins, [
{rebar_prv_alpaca, ".*", {git, "https://github.com/tsloughter/rebar_prv_alpaca.git", {branch, "master"}}}
{rebar_prv_alpaca, ".*", {git, "https://github.com/alpaca/rebar_prv_alpaca.git", {branch, "master"}}}
]}.


{provider_hooks, [{post, [{compile, {alpaca, compile}}]}]}.
```


Options
---

Options can be passed to the Alpaca compiler via a `{rebar_prv_alpaca, ... }` section in your rebar.config
file. Currently, the only option supported is for default imports (i.e. functions and types that can be
implictly included in all your modules), e.g.

```
{rebar_prv_alpaca,
[ { default_imports, [ {default, list_defaults} ] } ]
}.
```
$ rebar3 alpaca shell
...
== Alpaca Shell 0.02a ==

(hint: exit with ctrl-c, run expression by terminating with ';;' or an empty line)
`default_imports` takes a list of tuples of `{Module, Function}`. In the instance above
it might live in a module that looks like this:

-> add x y = x + y ;;
-> add 10 20 ;;
-- 30
```
-module(default).
-export([list_defaults/0]).

list_defaults() ->
Funs = [{assert, <<"equal">>}],
Types = [{utils, <<"Result">>}],
{Funs, Types}.
```

The `list_defaults/0` function returns a tuple of {[FunctionRef], [TypeRef]}.
These functions and types will then be injected into each of your modules.

The intention behind this is that most of the time you will simply wish to
reference a "default" set of imports from Alpaca's own standard library (which
is a work in prgress) but that ultimately you could pull in other standard
libraries or save on explicitly importing functions and types you use
everywhere.
5 changes: 3 additions & 2 deletions rebar.config
@@ -1,3 +1,4 @@
{erl_opts, [debug_info]}.
{deps, [{alpaca, {git, "https://github.com/alpaca-lang/alpaca.git", {tag, "v0.2.7"}}},
{apcshell, {git, "https://github.com/lepoetemaudit/alpaca-repl.git", {branch, "master"}}}]}.
{deps, [{epo_runtime, {git, "git://github.com/brigadier/epo_runtime.git",
{tag, "0.3"}}}, cf
]}.
11 changes: 10 additions & 1 deletion rebar.lock
@@ -1 +1,10 @@
[].
{"1.1.0",
[{<<"cf">>,{pkg,<<"cf">>,<<"0.3.1">>},0},
{<<"epo_runtime">>,
{git,"git://github.com/brigadier/epo_runtime.git",
{ref,"a3e50e7cebb526f833757e867bbe914c1da7baa3"}},
0}]}.
[
{pkg_hash,[
{<<"cf">>, <<"5CB902239476E141EA70A740340233782D363A31EEA8AD37049561542E6CD641">>}]}
].
257 changes: 236 additions & 21 deletions src/rebar_prv_alpaca_compile.erl
@@ -1,10 +1,10 @@
-module(rebar_prv_alpaca_compile).

-export([init/1, do/1, format_error/1]).
-export([init/1, do/1, format_error/1, compile_dir/6]).

-define(PROVIDER, compile).
-define(NAMESPACE, alpaca).
-define(DEPS, [{default, lock}]).
-define(DEPS, [{default, compile}]).

%% ===================================================================
%% Public API
Expand All @@ -27,40 +27,255 @@ init(State) ->

-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
%% Locate Alpaca compiler
AlpacaPaths = [
os:getenv("ALPACA_ROOT"),
"/usr/lib/alpaca",
"/usr/local/lib/alpaca"],
AlpacaHome = get_best_path(AlpacaPaths),
code:add_path(AlpacaHome ++ "/beams"),
AlpacaModules =
[alpaca, alpaca_ast, alpaca_ast_gen, alpaca_codegen,
alpaca_compiled_po, alpaca_error_format, alpaca_exhaustiveness,
alpaca_parser, alpaca_scan, alpaca_scanner, alpaca_typer],
case code:ensure_modules_loaded(AlpacaModules) of
ok -> ok;
_ -> abort_no_alpaca()
end,

Apps = case rebar_state:current_app(State) of
undefined ->
rebar_state:project_apps(State);
AppInfo ->
[AppInfo]
end,
end,
TestsEnabled = [P || P <- rebar_state:current_profiles(State), P == test],
[begin
EBinDir = rebar_app_info:ebin_dir(AppInfo),
Opts = rebar_app_info:opts(AppInfo),
SourceDir = filename:join(rebar_app_info:dir(AppInfo), "src"),
Info = rebar_dir:src_dirs(Opts),

FoundFiles = rebar_utils:find_files(SourceDir, ".*\\.alp\$"),
Opts = case dict:find(rebar_prv_alpaca, rebar_app_info:opts(AppInfo)) of
{ok, O} -> O;
error -> []
end,
SourceFiles = rebar_utils:find_files(SourceDir, ".*\\.alp\$"),
Deps = rebar_state:all_deps(State),
LocalBeamFiles = rebar_utils:find_files(EBinDir, ".*\\.beam\$"),
DependencyBeamFiles = lists:flatmap(fun gather_beam_files/1, Deps),
compile_dir(
SourceFiles,
LocalBeamFiles,
DependencyBeamFiles,
EBinDir,
Opts,
TestsEnabled)
end || AppInfo <- Apps],
{ok, State}.

compile_dir(SourceFiles, LocalBeamFiles, DependencyBeamFiles,
EBinDir, Opts, TestsEnabled) ->
%% Get Alpaca version so we can display it when compiling source
Version = proplists:get_value(version, alpaca:compiler_info()),

%% Get User LOCALE so we can display translated error messages, if available
Locale = case string:tokens(os:getenv("LANG", "en_US"), ".") of
[L, _] -> L;
L -> L
end,

%% Validate any options passed to us
ValidOpts = is_list(Opts) andalso
lists:all(fun({Key, _}) when is_atom(Key) -> true;
(_) -> false end, Opts),

case ValidOpts of
true -> ok;
false ->
OptsError = io_lib:format("Invalid Alpaca options list: ~p", [Opts]),
throw({error, {?MODULE, OptsError}})
end,

%% initial pass - iterate over source files, extract their dependencies,
%% and figure out if (the files themselves) require compilation -
%% i.e. BEAM file missing or mismatches the source hash
FileGraph = lists:foldl(
fun(F, Graph) ->
{ok, Src} = file:read_file(F),
{Mod, ModDeps} = alpaca:list_dependencies(Src),

maps:put(Mod, {F, ModDeps, is_dirty(LocalBeamFiles, F, Src)}, Graph)
end,
maps:new(),
SourceFiles),

%% Next, we figure out which modules are dirty due to
%% modules depended upon that require recompilation
GetHasDirtyDeps = fun DirtyDeps(Mod, Map) ->
{_, ModDeps, IsDirty} = maps:get(Mod, Map, {unknown, [], false}),
case IsDirty of
true -> true;
false ->
Dirty = lists:map(fun(M) -> DirtyDeps(M, Map) end, ModDeps),
lists:any(fun(X) -> X =:= true end, Dirty)
end
end,

%% We update the file graph with the list of dirty dependencies
FileGraph2 = maps:map(
fun(_, {_, _, true} = V) -> V;
(Mod, {F, ModDeps, false}) ->
{F, ModDeps, GetHasDirtyDeps(Mod, FileGraph)}
end,
FileGraph),

%% Create a directed graph so we can topologically sort
%% the modules in order of dependency
DiGraph = digraph:new(),

%% Each 'vertex' is a module
lists:map(
fun(Mod) ->
digraph:add_vertex(DiGraph, Mod)
end,
maps:keys(FileGraph2)),

%% Each 'edge' is a dependency relationship between them
maps:map(
fun(Mod, {_, ModDeps, _}) ->
lists:map(fun(OtherMod) ->
digraph:add_edge(DiGraph, OtherMod, Mod)
end, ModDeps)
end,
FileGraph2),

%% Generate the topological ordering
Sorted = digraph_utils:topsort(DiGraph),

%% Map the final list into .beam / .alp filepaths
GatheredLocalFiles = lists:map(
fun(Mod) ->
{F, _, IsDirty} = maps:get(Mod, FileGraph2),
case IsDirty of
true -> F;
false ->
{ok, BF} = get_beam_file(F, LocalBeamFiles),
BF
end
end,
Sorted),

%% Of course, if we don't have any .alp files in the final
%% list, we don't even need to invoke compilation
case lists:all(fun(F) ->
filename:extension(F) == ".beam"
end,
GatheredLocalFiles) of
true -> ok;
false ->
CompileFiles = DependencyBeamFiles ++ GatheredLocalFiles,
rebar_api:debug("Sending files to Alpaca compiler: ~p~n", [CompileFiles]),
Sources = lists:filter(
fun(F) ->
filename:extension(F) == ".alp"
end,
GatheredLocalFiles),

AllFoundFiles = FoundFiles ++ lists:flatmap(fun gather_files/1, Deps),
rebar_api:info(
"Alpaca ~s: compiling ~s~n",
[Version,
string:join(
lists:map(fun filename:basename/1, Sources),
", ")]),

case alpaca:compile({files, AllFoundFiles}, TestsEnabled) of
{ok, Compiled} ->
CompileOpts = TestsEnabled ++
case proplists:get_value(default_imports, Opts) of
undefined -> [];
Imports -> [{default_imports, gather_imports(Imports)}]
end,

case alpaca:compile({files, CompileFiles}, CompileOpts) of
{ok, Compiled} ->
[file:write_file(filename:join(EBinDir, FileName), BeamBinary) ||
{compiled_module, ModuleName, FileName, BeamBinary} <- Compiled];
{error, Reason} ->
io:format(standard_error, "Compile error: ~s", format_error(Reason))
end
end || AppInfo <- Apps],
{compiled_module, _, FileName, BeamBinary} <- Compiled];

{ok, State}.
{error, _} = E ->
Error = alpaca_error_format:fmt(E, Locale),
throw({error, {?MODULE, Error}})
end
end.

get_beam_file(Filename, BeamFiles) ->
[ModuleName, _] = string:tokens(filename:basename(Filename), "."),
BeamName = "alpaca_" ++ ModuleName ++ ".beam",
BFS = lists:filter(
fun(F) ->
filename:basename(F) == BeamName
end, BeamFiles),
case BFS of
[BF] -> {ok, BF};
[] -> {error, no_file};
_ -> {error, duplicate_filename, BFS}
end.

is_dirty(BeamFiles, Filename, SrcText) ->
case get_beam_file(Filename, BeamFiles) of
{ok, BeamMod} ->
%% We have a BEAM File, compare the hashes
SrcHash = alpaca:hash_source(unicode:characters_to_list(SrcText)),
BeamHash = alpaca:retrieve_hash(BeamMod),
case {SrcHash, BeamHash} of
%% Hashes match, unchanged
{_S, _S} -> false;
%% Hashes differ, it has changed
_ -> true
end;

{error, no_file} ->
%% No BEAM file, compile the source file
true
end.

gather_files(AppInfo) ->
SourceDir = filename:join(rebar_app_info:dir(AppInfo), "src"),
rebar_utils:find_files(SourceDir, ".*\\.alp\$").

gather_beam_files(Dep) ->
EbinDir = rebar_app_info:ebin_dir(Dep),
PossibleAlpacaFiles = rebar_utils:find_files(EbinDir, "alpaca_.*\\.beam\$"),
lists:filter(fun(Mod) -> case alpaca:retrieve_hash(Mod) of
undefined -> false;
_ -> true
end
end, PossibleAlpacaFiles).

gather_imports(Imports) ->
ImportList = [begin
code:ensure_modules_loaded([Mod]),
case erlang:function_exported(Mod, Fun, 0) of
true ->
erlang:apply(Mod, Fun, []);

false ->
Error = io_lib:format(
"Default imports: '~p:~p/0' doesn't exist", [Mod, Fun]),
throw({error, {?MODULE, Error}})
end
end|| {Mod, Fun} <- Imports],
{Funs, Types} = lists:unzip(ImportList),
{lists:flatten(Funs), lists:flatten(Types)}.

-spec format_error(any()) -> iolist().
format_error(Reason) ->
io_lib:format("~p", [Reason]).
io_lib:format("Alpaca compile error: \n\033[0m~s", [Reason]).


get_best_path([]) ->
"/opt/alpaca";
get_best_path([Path | Rest]) ->
case filelib:is_dir(Path) of
true -> Path;
false -> get_best_path(Rest)
end.

abort_no_alpaca() ->
io:put_chars(
standard_error, "Error: Cannot find Alpaca. Please follow"
" instructions at http://alpaca-lang.org\n"),
halt(1, []).