Browse Source

初始化提交

master
SisMaker 5 years ago
parent
commit
5126992f33
13 changed files with 1998 additions and 0 deletions
  1. +3
    -0
      .gitignore
  2. +11
    -0
      Makefile
  3. +18
    -0
      README.md
  4. +212
    -0
      bootstrap
  5. +2
    -0
      bootstrap.bat
  6. +9
    -0
      ebin/erlNpc.app
  7. +9
    -0
      include/rebar.hrl
  8. +2
    -0
      rebar.config
  9. +31
    -0
      src/erlNpc.erl
  10. +230
    -0
      src/rebar.erl
  11. +211
    -0
      src/rebarConfig.erl
  12. +740
    -0
      src/rebarNpCompiler.erl
  13. +520
    -0
      src/rebarUtils.erl

+ 3
- 0
.gitignore View File

@ -15,3 +15,6 @@ deps
.rebar3
_build/
_checkouts/
.idea

+ 11
- 0
Makefile View File

@ -0,0 +1,11 @@
all:
./bootstrap.erl
debug:
./bootstrap debug
xref: debug
./xref
clean:
@rm -rf rebar .rebar ebin/*.beam

+ 18
- 0
README.md View File

@ -0,0 +1,18 @@
# Erlang nif port compiler
windows linux mac下erlang nif或者port_driver通用编译脚本
改造自 erlang-native-compiler
## Usage
default_env
1. Clone this repository
1. Run `make` in this directory
1. Copy `erlNpc` to your project "c_src" dir and commit it
1. Add these (or similar) hooks to your rebar.config:
```erlang
{pre_hooks, [{"", compile, "escript c_src/erlNpc compile"}]}.
{post_hooks, [{"", clean, "escript c_src/erlNpc clean"}]}.
```
After that enc should read your old rebar.config `port\_specs` and `port\_env` settings as expected (it is rebar2's port compiler after all...).

+ 212
- 0
bootstrap View File

@ -0,0 +1,212 @@
#!/usr/bin/env escript
%% -*- erlang -*
%%!
-include_lib("kernel/include/file.hrl").
main(Args) ->
case lists:member("--help", Args) of
true ->
usage(),
halt(0);
false ->
ok
end,
%% Get a string repr of build time
BuiltTime = getTimeStr(),
%% Get a string repr of first matching VCS changeset
VcsInfo = vcsInfo([{git, ".git", "git describe --always --tags", "git status -s"}]),
%% Check for force=1 flag to force a rebuild
case lists:member("force=1", Args) of
true ->
rm("ebin/*.beam");
false ->
case filelib:is_file("ebin/rebar.beam") of
true -> rm("ebin/rebar.beam");
false -> io:fwrite("No beam files found.~n")
end
end,
%% Add check for debug flag
DebugFlag =
case lists:member("debug", Args) of
true -> debug_info;
false -> undefined
end,
OtpInfo = string:strip(erlang:system_info(otp_release), both, $\n),
%% Types dict:dict() and digraph:digraph() have been introduced in
%% Erlang 17.
%% At the same time, their counterparts dict() and digraph() are to be
%% deprecated in Erlang 18. namespaced_types option is used to select
%% proper type name depending on the OTP version used.
%% Extract the system info of the version of OTP we use to compile rebar
%% NamespacedTypes =
%% case is_otp(OtpInfo, "^[0-9]+") of
%% true -> {d, namespaced_types};
%% false -> undefined
%% end,
%% Compile all src/*.erl to ebin
%% To not accidentally try to compile files like Mac OS X resource forks,
%% we only look for rebar source files that start with a letter.
Opts = [
DebugFlag,
{outdir, "ebin"},
{i, "include"},
{d, 'BUILD_TIME', BuiltTime},
{d, 'VCS_INFO', VcsInfo},
{d, 'OTP_INFO', OtpInfo}
],
case make:files(filelib:wildcard("src/*.erl"), Opts) of
up_to_date ->
ok;
error ->
io:format("Failed to compile erlNpc files!\n"),
halt(1)
end,
%% Make sure file:consult can parse the .app file
case file:consult("ebin/erlNpc.app") of
{ok, _} ->
ok;
{error, Reason} ->
io:format("Invalid syntax in ebin/erlNpc.app: ~p\n", [Reason]),
halt(1)
end,
%% Add ebin/ to our path
true = code:add_path("ebin"),
%% Run rebar compile to do proper .app validation etc.
%% and rebar escriptize to create the rebar script
%% RebarArgs = Args -- ["debug"], %% Avoid trying to run 'debug' command
% rebar:main(["compile"] ++ RebarArgs),
escriptize(),
%% Finally, update executable perms for our script on *nix,
%% or write out script files on win32.
case os:type() of
{unix, _} ->
[] = os:cmd("chmod u+x erlNpc"),
ok;
{win32, _} ->
writeWindowsScripts(),
ok;
_ ->
ok
end,
%% Add a helpful message
io:format(<<"Congratulations! You now have a self-contained script called"
" \"erlNpc\" in\n"
"your current working directory. "
"Place this script anywhere in your path\n"
"and you can use erlNpc to build native code for Erlang\n">>).
usage() ->
io:format(<<"Usage: bootstrap [OPTION]...~n">>),
io:format(<<" force=1 unconditional build~n">>),
io:format(<<" debug add debug information~n">>).
%% is_otp(OtpInfo, Regex) ->
%% case re:run(OtpInfo, Regex, [{capture, none}]) of
%% match -> true;
%% nomatch -> false
%% end.
rm(Path) ->
NativePath = filename:nativename(Path),
Cmd = case os:type() of
{unix, _} -> "rm -f ";
{win32, _} -> "del /q "
end,
[] = os:cmd(Cmd ++ NativePath),
ok.
getTimeStr() ->
{{Y, M, D}, {H, Min, S}} = erlang:localtime(),
lists:flatten(io_lib:format("~B_~2.10.0B_~2.10.0B ~B:~2.10.0B:~2.10.0B", [Y, M, D, H, Min, S])).
vcsInfo([]) ->
"No VCS info available.";
vcsInfo([{Id, Dir, VsnCmd, StatusCmd} | Rest]) ->
case filelib:is_dir(Dir) of
true ->
Vsn = string:strip(os:cmd(VsnCmd), both, $\n),
Status =
case string:strip(os:cmd(StatusCmd), both, $\n) of
[] ->
"";
_ ->
"-dirty"
end,
lists:concat([Id, " ", Vsn, Status]);
false ->
vcsInfo(Rest)
end.
writeWindowsScripts() ->
CmdScript =
"@echo off\r\n"
"setlocal\r\n"
"set rebarscript=%~f0\r\n"
"escript.exe \"%rebarscript:.cmd=%\" %*\r\n",
ok = file:write_file("erlNpc.cmd", CmdScript).
escriptize() ->
AppName = "erlNpc",
ScriptName = "erlNpc",
Files = loadEScriptFiles(AppName, "ebin", "*"),
{ok, {"mem", ZipBin}} = zip:create("mem", Files, [memory]),
Shebang = "#!/usr/bin/env escript\n",
Comment = "%%\n",
EmuArgs = io_lib:format("%%! -pa ~s/~s/ebin\n", [AppName, AppName]),
Script = iolist_to_binary([Shebang, Comment, EmuArgs, ZipBin]),
ok = file:write_file(ScriptName, Script),
%% Finally, update executable perms for our script
{ok, #file_info{mode = Mode}} = file:read_file_info(ScriptName),
ok = file:change_mode(ScriptName, Mode bor 8#00111).
loadEScriptFiles(AppName, Path, WildCard) ->
FileNames = filelib:wildcard(WildCard, Path),
Entries =
lists:flatmap(
fun(FN) ->
FPath = filename:join(Path, FN),
{ok, Contents} = file:read_file(FPath),
ZipPath = filename:join(AppName, FPath),
[{ZipPath, Contents} | dirEntries(ZipPath)]
end,
FileNames),
usort(Entries).
%% Given a filename, return zip archive dir entries for each sub-dir.
%% Required to work around issues fixed in OTP-10071.
dirEntries(File) ->
Dirs = dirs(File),
[{Dir ++ "/", <<>>} || Dir <- Dirs].
%% Given "foo/bar/baz", return ["foo", "foo/bar", "foo/bar/baz"].
dirs(Dir) ->
dirs1(filename:split(Dir), "", []).
dirs1([], _, Acc) ->
lists:reverse(Acc);
dirs1([H | T], "", []) ->
dirs1(T, H, [H]);
dirs1([H | T], Last, Acc) ->
Dir = filename:join(Last, H),
dirs1(T, Dir, [Dir | Acc]).
usort(List) ->
lists:ukeysort(1, lists:flatten(List)).

+ 2
- 0
bootstrap.bat View File

@ -0,0 +1,2 @@
@echo off
escript.exe bootstrap %*

+ 9
- 0
ebin/erlNpc.app View File

@ -0,0 +1,9 @@
{application,erlNpc,
[{description,"erlNpc: Erlang Native Compiler"},
{vsn,"0.1.0"},
{registered,[]},
{applications,[kernel,stdlib]},
{modules,[erlNpc,rebar,rebarConfig,rebarNpCompiler,rebarUtils]},
{licenses,["Apache 2.0"]},
{links,[]},
{env,[{log_level,warn}]}]}.

+ 9
- 0
include/rebar.hrl View File

@ -0,0 +1,9 @@
-define(FAIL, rebarUtils:abort()).
-define(ABORT(Str, Args), rebarUtils:abort(Str, Args)).
-define(INFO(Str, Args), rebar:log(info, Str, Args)).
-define(WARN(Str, Args), rebar:log(warn, Str, Args)).
-define(ERROR(Str, Args), rebar:log(error, Str, Args)).
-define(CONSOLE(Str, Args), io:format(Str, Args)).
-define(FMT(Str, Args), lists:flatten(io_lib:format(Str, Args))).

+ 2
- 0
rebar.config View File

@ -0,0 +1,2 @@
{erl_opts, [debug_info]}.
{deps, []}.

+ 31
- 0
src/erlNpc.erl View File

@ -0,0 +1,31 @@
-module(erlNpc).
-export([
main/1
]).
main(Args) ->
file:set_cwd("c_src"),
{ok, Dir} = file:get_cwd(),
io:format("erlNpc begin compile pwd:~p ~n", [Dir]),
FunCom =
fun(File) ->
case filelib:is_dir(File) == true andalso lists:nth(1, File) =/= 46 andalso filename:basename(File) =/= "include" of
true ->
{ok, CurDir} = file:get_cwd(),
io:format("erlNpc cur compile/clean: ~s, cur pwd:~p ~n", [File, CurDir]),
file:set_cwd(File),
rebar:main(Args),
file:set_cwd("..");
_ ->
ignore
end
end,
case file:list_dir(".") of
{ok, Files} ->
lists:foreach(FunCom, Files);
_Err ->
rebar:log(error, "erlNpc start compile error ~p ~n", [_Err])
end.

+ 230
- 0
src/rebar.erl View File

@ -0,0 +1,230 @@
-module(rebar).
-export([
main/1,
log/3
]).
-include("rebar.hrl").
-ifndef(BUILD_TIME).
-define(BUILD_TIME, "undefined").
-endif.
-ifndef(VCS_INFO).
-define(VCS_INFO, "undefined").
-endif.
-ifndef(OTP_INFO).
-define(OTP_INFO, "undefined").
-endif.
-define(DEFAULT_JOBS, 3).
main(Args) ->
case catch (run(Args)) of
ok ->
ok;
rebar_abort ->
rebarUtils:delayedHalt(1);
Error ->
%% Nothing should percolate up from rebar_core;
%% Dump this error to console
io:format("Uncaught error in rebar_core: ~p\n", [Error]),
rebarUtils:delayedHalt(1)
end.
log(Level, Format, Args) ->
{ok, LimitLevel} = application:get_env(erlNpc, log_level),
case levelInt(LimitLevel) >= levelInt(Level) of
true ->
io:format(destination(Level), Format, Args);
false ->
ok
end.
levelInt(info) -> 2;
levelInt(warn) -> 1;
levelInt(error) -> 0.
destination(error) -> standard_error;
destination(_) -> group_leader().
run(["help"]) ->
usage(),
help(compile);
run(["help" | RawCommands]) ->
lists:foreach(fun help/1, [list_to_atom(C) || C <- RawCommands]);
run(["version"]) ->
ok = loadRebarApp(),
%% Display vsn and build time info
version();
run(RawArgs) ->
ok = loadRebarApp(),
Args = parseArgs(RawArgs),
BaseConfig = initConfig(Args),
{BaseConfig1, Cmds} = saveOptions(BaseConfig, Args),
runAux(BaseConfig1, Cmds).
loadRebarApp() ->
%% Pre-load the rebar app so that we get default configuration
case application:load(erlNpc) of
ok ->
ok;
{error, {already_loaded,erlNpc}} ->
ok;
_ ->
rebarUtils:delayedHalt(1)
end.
help(compile) ->
rebarNpCompiler:info(help, compile);
help(clean) ->
rebarNpCompiler:info(help, clean);
help(Command) ->
?CONSOLE("erlNpc no help available for \"~p\"~n", [Command]).
parseArgs([]) ->
{[], []};
parseArgs(["-h" | _]) ->
usage(),
help(compile),
rebarUtils:delayedHalt(0);
parseArgs(["--help" | _]) ->
usage(),
help(compile),
rebarUtils:delayedHalt(0);
parseArgs(["-v" | _]) ->
version(),
rebarUtils:delayedHalt(0);
parseArgs(["--version" | _]) ->
version(),
rebarUtils:delayedHalt(0);
parseArgs(["-c", FileName | Rest]) ->
{Opts, NonOpts} = parseArgs(Rest),
{[{config, FileName} | Opts], NonOpts};
parseArgs(["--config", FileName | Rest]) ->
parseArgs(["-c", FileName | Rest]);
parseArgs([NonOpt | Rest]) ->
{Opts, NonOpts} = parseArgs(Rest),
{Opts, [NonOpt | NonOpts]}.
usage() ->
?CONSOLE("erlNpc [-hv] [-c CONFIG_FILE] COMMAND [COMMAND ...]~n~n", []).
initConfig({Options, _NonOptArgs}) ->
%% If $HOME/.rebar/config exists load and use as global config
GlobalConfigFile = filename:join([os:getenv("HOME"), ".rebar", "config"]),
GlobalConfig =
case filelib:is_regular(GlobalConfigFile) of
true ->
rebarConfig:new(GlobalConfigFile);
false ->
rebarConfig:new()
end,
%% Set the rebar config to use
GlobalConfig1 =
case proplists:get_value(config, Options) of
undefined ->
GlobalConfig;
Conf ->
rebarConfig:setGlobal(GlobalConfig, config, Conf)
end,
BaseConfig = rebarConfig:baseConfig(GlobalConfig1),
%% Keep track of how many operations we do, so we can detect bad commands
BaseConfig1 = rebarConfig:setXconf(BaseConfig, operations, 0),
%% Initialize vsn cache
rebarUtils:initVsnCache(BaseConfig1).
initConfig_1(BaseConfig) ->
%% Determine the location of the rebar executable; important for pulling
%% resources out of the escript
ScriptName = filename:absname(escript:script_name()),
BaseConfig1 = rebarConfig:setXconf(BaseConfig, escript, ScriptName),
%% Note the top-level directory for reference
AbsCwd = filename:absname(rebarUtils:getCwd()),
rebarConfig:setXconf(BaseConfig1, base_dir, AbsCwd).
runAux(BaseConfig, Commands) ->
%% Make sure crypto is running
case crypto:start() of
ok -> ok;
{error, {already_started, crypto}} -> ok
end,
%% Convert command strings to atoms
CommandAtoms = [list_to_atom(C) || C <- Commands],
BaseConfig1 = initConfig_1(BaseConfig),
%% Make sure we're an app directory
AppFile = "",
%% case rebarUtils:isAppDir() of
%% {true, AppFile0} ->
%% AppFile0;
%% false ->
%% rebarUtils:delayedHalt(1)
%% end,
% Setup our environment
BaseConfig2 = setupEnvs(BaseConfig1, [rebarNpCompiler]),
%% Process each command, resetting any state between each one
lists:foreach(
fun(Command) ->
%processCommand(Command, BaseConfig2, AppFile)""
processCommand(Command, BaseConfig2, AppFile)
end, CommandAtoms).
setupEnvs(Config, Modules) ->
lists:foldl(
fun(Module, CfgAcc) ->
Env = Module:setupEnv(CfgAcc),
rebarConfig:saveEnv(CfgAcc, Module, Env)
end, Config, Modules).
processCommand(compile, Config, AppFile) ->
rebarNpCompiler:compile(Config, AppFile);
processCommand(clean, Config, AppFile) ->
rebarNpCompiler:clean(Config, AppFile);
processCommand(Other, _, _) ->
?CONSOLE("Unknown command: ~s~n", [Other]),
rebarUtils:delayedHalt(1).
saveOptions(Config, {Options, NonOptArgs}) ->
GlobalDefines = proplists:get_all_values(defines, Options),
Config1 = rebarConfig:setXconf(Config, defines, GlobalDefines),
filterFlags(Config1, NonOptArgs, []).
%% show version information and halt
version() ->
{ok, Vsn} = application:get_key(erlNpc, vsn),
?CONSOLE("erlNpc ~s ~s ~s ~s\n", [Vsn, ?OTP_INFO, ?BUILD_TIME, ?VCS_INFO]).
%% Seperate all commands (single-words) from flags (key=value) and store
%% values into the rebar_config global storage.
filterFlags(Config, [], Commands) ->
{Config, lists:reverse(Commands)};
filterFlags(Config, [Item | Rest], Commands) ->
case string:tokens(Item, "=") of
[Command] ->
filterFlags(Config, Rest, [Command | Commands]);
[KeyStr, RawValue] ->
Key = list_to_atom(KeyStr),
Value =
case Key of
verbose ->
list_to_integer(RawValue);
_ ->
RawValue
end,
Config1 = rebarConfig:setGlobal(Config, Key, Value),
filterFlags(Config1, Rest, Commands);
Other ->
?CONSOLE("Ignoring command line argument: ~p\n", [Other]),
filterFlags(Config, Rest, Commands)
end.

+ 211
- 0
src/rebarConfig.erl View File

@ -0,0 +1,211 @@
-module(rebarConfig).
-include("rebar.hrl").
-export([
new/0,
new/1,
baseConfig/1,
consultFile/1,
get/3,
getLocal/3,
getList/3,
setGlobal/3,
getGlobal/3,
saveEnv/3,
getEnv/2,
setXconf/3,
getXconf/2,
getXconf/3
]).
-type key() :: atom().
-type rebarDict() :: dict:dict(term(), term()).
-record(config, {
dir :: file:filename(),
opts = [] :: list(),
globals = newGlobals() :: rebarDict(),
envs = newEnv() :: rebarDict(),
%% cross-directory/-command config
skipDirs = newSkipDirs() :: rebarDict(),
xconf = newXconf() :: rebarDict()
}).
-opaque config() :: #config{}.
-export_type([config/0]).
-define(DEFAULT_NAME, "rebar.config").
-spec baseConfig(config()) -> config().
baseConfig(GlobalConfig) ->
ConfName = rebarConfig:getGlobal(GlobalConfig, config, ?DEFAULT_NAME),
new(GlobalConfig, ConfName).
-spec new() -> config().
new() ->
#config{dir = rebarUtils:getCwd()}.
-spec new(file:filename() | config()) -> config().
new(ConfigFile) when is_list(ConfigFile) ->
case consultFile(ConfigFile) of
{ok, Opts} ->
#config{dir = rebarUtils:getCwd(),
opts = Opts};
Other ->
?ABORT("Failed to load ~s: ~p~n", [ConfigFile, Other])
end;
new(#config{opts = Opts0, globals = Globals, skipDirs = SkipDirs, xconf = Xconf}) ->
new(#config{opts = Opts0, globals = Globals, skipDirs = SkipDirs, xconf = Xconf},
?DEFAULT_NAME).
-spec get(config(), key(), term()) -> term().
get(Config, Key, Default) ->
proplists:get_value(Key, Config#config.opts, Default).
-spec getList(config(), key(), term()) -> term().
getList(Config, Key, Default) ->
get(Config, Key, Default).
-spec getLocal(config(), key(), term()) -> term().
getLocal(Config, Key, Default) ->
proplists:get_value(Key, localOpts(Config#config.opts, []), Default).
-spec setGlobal(config(), key(), term()) -> config().
setGlobal(Config, jobs = Key, Value) when is_list(Value) ->
setGlobal(Config, Key, list_to_integer(Value));
setGlobal(Config, jobs = Key, Value) when is_integer(Value) ->
NewGlobals = dict:store(Key, erlang:max(1, Value), Config#config.globals),
Config#config{globals = NewGlobals};
setGlobal(Config, Key, Value) ->
NewGlobals = dict:store(Key, Value, Config#config.globals),
Config#config{globals = NewGlobals}.
-spec getGlobal(config(), key(), term()) -> term().
getGlobal(Config, Key, Default) ->
case dict:find(Key, Config#config.globals) of
error ->
Default;
{ok, Value} ->
Value
end.
-spec consultFile(file:filename()) -> term().
consultFile(File) ->
case filename:extension(File) of
".script" ->
consultAndEval(removeScriptExt(File), File);
_ ->
Script = File ++ ".script",
case filelib:is_regular(Script) of
true ->
consultAndEval(File, Script);
false ->
file:consult(File)
end
end.
-spec saveEnv(config(), module(), nonempty_list()) -> config().
saveEnv(Config, Mod, Env) ->
NewEnvs = dict:store(Mod, Env, Config#config.envs),
Config#config{envs = NewEnvs}.
-spec getEnv(config(), module()) -> term().
getEnv(Config, Mod) ->
dict:fetch(Mod, Config#config.envs).
-spec setXconf(config(), term(), term()) -> config().
setXconf(Config, Key, Value) ->
NewXconf = dict:store(Key, Value, Config#config.xconf),
Config#config{xconf = NewXconf}.
-spec getXconf(config(), term()) -> term().
getXconf(Config, Key) ->
{ok, Value} = dict:find(Key, Config#config.xconf),
Value.
-spec getXconf(config(), term(), term()) -> term().
getXconf(Config, Key, Default) ->
case dict:find(Key, Config#config.xconf) of
error ->
Default;
{ok, Value} ->
Value
end.
%% ===================================================================
%% Internal functions
%% ===================================================================
-spec new(config(), file:filename()) -> config().
new(ParentConfig, ConfName) ->
%% Load terms from rebar.config, if it exists
Dir = rebarUtils:getCwd(),
ConfigFile = filename:join([Dir, ConfName]),
Opts0 = ParentConfig#config.opts,
Opts = case consultFile(ConfigFile) of
{ok, Terms} ->
%% Found a config file with some terms. We need to
%% be able to distinguish between local definitions
%% (i.e. from the file in the cwd) and inherited
%% definitions. To accomplish this, we use a marker
%% in the proplist (since order matters) between
%% the new and old defs.
Terms ++ [local] ++
[Opt || Opt <- Opts0, Opt /= local];
{error, enoent} ->
[local] ++
[Opt || Opt <- Opts0, Opt /= local];
Other ->
?ABORT("Failed to load ~s: ~p\n", [ConfigFile, Other])
end,
ParentConfig#config{dir = Dir, opts = Opts}.
-spec consultAndEval(file:filename(), file:filename()) -> {ok, term()}.
consultAndEval(File, Script) ->
ConfigData = tryConsult(File),
file:script(Script, bs([{'CONFIG', ConfigData}, {'SCRIPT', Script}])).
-spec removeScriptExt(file:filename()) -> file:filename().
removeScriptExt(F) ->
"tpircs." ++ Rev = lists:reverse(F),
lists:reverse(Rev).
-spec tryConsult(file:filename()) -> term().
tryConsult(File) ->
case file:consult(File) of
{ok, Terms} ->
Terms;
{error, enoent} ->
[];
{error, Reason} ->
?ABORT("Failed to read config file ~s: ~p~n", [File, Reason])
end.
-type bs_vars() :: [{term(), term()}].
-spec bs(bs_vars()) -> bs_vars().
bs(Vars) ->
lists:foldl(fun({K, V}, Bs) ->
erl_eval:add_binding(K, V, Bs)
end, erl_eval:new_bindings(), Vars).
-spec localOpts(list(), list()) -> list().
localOpts([], Acc) ->
lists:reverse(Acc);
localOpts([local | _Rest], Acc) ->
lists:reverse(Acc);
localOpts([Item | Rest], Acc) ->
localOpts(Rest, [Item | Acc]).
-spec newGlobals() -> rebarDict().
newGlobals() -> dict:new().
-spec newEnv() -> rebarDict().
newEnv() -> dict:new().
-spec newSkipDirs() -> rebarDict().
newSkipDirs() -> dict:new().
-spec newXconf() -> rebarDict().
newXconf() -> dict:new().

+ 740
- 0
src/rebarNpCompiler.erl View File

@ -0,0 +1,740 @@
-module(rebarNpCompiler).
-include("rebar.hrl").
-export([
compile/2,
clean/2
]).
%% for internal use only
-export([
setupEnv/1,
info/2
]).
-record(spec, {
type :: 'drv' | 'exe',
link_lang :: 'cc' | 'cxx',
target :: file:filename(),
sources = [] :: [file:filename(), ...],
objects = [] :: [file:filename(), ...],
opts = [] :: list() | []
}).
compile(Config, AppFile) ->
case getSpecs(Config, AppFile) of
[] ->
ok;
Specs ->
SharedEnv = rebarConfig:getEnv(Config, ?MODULE),
%% Compile each of the sources
NewBins = compileSources(Config, Specs, SharedEnv),
%% Make sure that the target directories exist
?INFO("Using specs ~p\n", [Specs]),
lists:foreach(
fun(#spec{target = Target}) ->
ok = filelib:ensure_dir(Target)
end, Specs),
%% Only relink if necessary, given the Target
%% and list of new binaries
lists:foreach(
fun(#spec{target = Target, objects = Bins, opts = Opts, link_lang = LinkLang}) ->
AllBins = [sets:from_list(Bins), sets:from_list(NewBins)],
Intersection = sets:intersection(AllBins),
case needsLink(Target, sets:to_list(Intersection)) of
true ->
LinkTemplate = selectLinkTemplate(LinkLang, Target),
Env = proplists:get_value(env, Opts, SharedEnv),
Cmd = expandCommand(LinkTemplate, Env, string:join(Bins, " "), Target),
rebarUtils:sh(Cmd, [{env, Env}]);
false ->
?INFO("Skipping relink of ~s\n", [Target]),
ok
end
end, Specs)
end.
clean(Config, AppFile) ->
case getSpecs(Config, AppFile) of
[] ->
ok;
Specs ->
lists:foreach(
fun(#spec{target = Target, objects = Objects}) ->
rebarUtils:deleteEach([Target]),
rebarUtils:deleteEach(Objects),
rebarUtils:deleteEach(portDeps(Objects))
end, Specs)
end,
ok.
setupEnv(Config) ->
setupEnv(Config, defaultEnv(Config)).
%% ===================================================================
%% Internal functions
%% ===================================================================
info(help, compile) ->
infoHelp("Build port sources");
info(help, clean) ->
infoHelp("Delete port build results").
infoHelp(Description) ->
?CONSOLE(
"~s.~n"
"~n"
"Valid rebar.config options:~n"
"port_specs - Erlang list of tuples of the forms~n"
" {ArchRegex, TargetFile, Sources, Options}~n"
" {ArchRegex, TargetFile, Sources}~n"
" {TargetFile, Sources}~n"
"~n"
" Examples:~n"
" ~p~n"
"~n"
"port_env - Erlang list of key/value pairs which will control~n"
" the environment when running the compiler and linker.~n"
" Variables set in the surrounding system shell are taken~n"
" into consideration when expanding port_env.~n"
"~n"
" By default, the following variables are defined:~n"
" CC - C compiler~n"
" CXX - C++ compiler~n"
" CFLAGS - C compiler~n"
" CXXFLAGS - C++ compiler~n"
" LDFLAGS - Link flags~n"
" ERL_CFLAGS - default -I paths for erts and ei~n"
" ERL_LDFLAGS - default -L and -lerl_interface -lei~n"
" DRV_CFLAGS - flags that will be used for compiling~n"
" DRV_LDFLAGS - flags that will be used for linking~n"
" EXE_CFLAGS - flags that will be used for compiling~n"
" EXE_LDFLAGS - flags that will be used for linking~n"
" ERL_EI_LIBDIR - ei library directory~n"
" DRV_CXX_TEMPLATE - C++ command template~n"
" DRV_CC_TEMPLATE - C command template~n"
" DRV_LINK_TEMPLATE - C Linker command template~n"
" DRV_LINK_CXX_TEMPLATE - C++ Linker command template~n"
" EXE_CXX_TEMPLATE - C++ command template~n"
" EXE_CC_TEMPLATE - C command template~n"
" EXE_LINK_TEMPLATE - C Linker command template~n"
" EXE_LINK_CXX_TEMPLATE - C++ Linker command template~n"
"~n"
" Note that if you wish to extend (vs. replace) these variables,~n"
" you MUST include a shell-style reference in your definition.~n"
" e.g. to extend CFLAGS, do something like:~n"
"~n"
" {port_env, [{\"CFLAGS\", \"$CFLAGS -MyOtherOptions\"}]}~n"
"~n"
" It is also possible to specify platform specific options~n"
" by specifying a triplet where the first string is a regex~n"
" that is checked against Erlang's system architecture string.~n"
" e.g. to specify a CFLAG that only applies to x86_64 on linux~n"
" do:~n"
" {port_env, [{\"x86_64.*-linux\", \"CFLAGS\",~n"
" \"$CFLAGS -X86Options\"}]}~n"
"~n"
"Cross-arch environment variables to configure toolchain:~n"
" REBAR_TARGET_ARCH to set the tool chain name to use~n"
" REBAR_TARGET_ARCH_WORDSIZE (optional - "
"if CC fails to determine word size)~n"
" fallback word size is 32~n"
" REBAR_TARGET_ARCH_VSN (optional - "
"if a special version of CC/CXX is requested)~n",
[
Description,
{port_specs, [{"priv/so_name.so", ["c_src/*.c"]},
{"linux", "priv/hello_linux", ["c_src/hello_linux.c"]},
{"linux", "priv/hello_linux", ["c_src/*.c"], [{env, []}]}]}
]).
%% set REBAR_DEPS_DIR and ERL_LIBS environment variables
defaultEnv(Config) ->
BaseDir = rebarUtils:baseDir(Config),
DepsDir0 = rebarConfig:getXconf(Config, deps_dir, "deps"),
DepsDir = filename:dirname(filename:join([BaseDir, DepsDir0, "dummy"])),
%% include rebar's DepsDir in ERL_LIBS
Separator = case os:type() of
{win32, nt} -> ";";
_ -> ":"
end,
ERL_LIBS = case os:getenv("ERL_LIBS") of
false ->
{"ERL_LIBS", DepsDir};
PrevValue ->
{"ERL_LIBS", DepsDir ++ Separator ++ PrevValue}
end,
[
{"REBAR_DEPS_DIR", DepsDir},
ERL_LIBS
].
setupEnv(Config, ExtraEnv) ->
%% Extract environment values from the config (if specified) and
%% merge with the default for this operating system. This enables
%% max flexibility for users.
DefaultEnv = filterEnv(defaultEnv(), []),
%% Get any port-specific envs; use port_env first and then fallback
%% to port_envs for compatibility
RawPortEnv = rebarConfig:getList(
Config,
port_env,
rebarConfig:getList(Config, port_envs, [])),
PortEnv = filterEnv(RawPortEnv, []),
Defines = getDefines(Config),
OverrideEnv = Defines ++ PortEnv ++ filterEnv(ExtraEnv, []),
RawEnv = applyDefaults(osEnv(), DefaultEnv) ++ OverrideEnv,
expandVarsLoop(mergeEachVar(RawEnv, [])).
getDefines(Config) ->
RawDefines = rebarConfig:getXconf(Config, defines, []),
Defines = string:join(["-D" ++ D || D <- RawDefines], " "),
[{"ERL_CFLAGS", "$ERL_CFLAGS " ++ Defines}].
replaceExtension(File, NewExt) ->
OldExt = filename:extension(File),
replaceExtension(File, OldExt, NewExt).
replaceExtension(File, OldExt, NewExt) ->
filename:rootname(File, OldExt) ++ NewExt.
%%
%% == compile and link ==
%%
compileSources(Config, Specs, SharedEnv) ->
{NewBins, Db} =
lists:foldl(
fun(#spec{sources = Sources, type = Type, opts = Opts}, Acc) ->
Env = proplists:get_value(env, Opts, SharedEnv),
compileEach(Config, Sources, Type, Env, Acc)
end, {[], []}, Specs),
%% Rewrite clang compile commands database file only if something
%% was compiled.
case NewBins of
[] ->
ok;
_ ->
{ok, ClangDbFile} = file:open("compile_commands.json", [write]),
ok = io:fwrite(ClangDbFile, "[~n", []),
lists:foreach(fun(E) -> ok = io:fwrite(ClangDbFile, E, []) end, Db),
ok = io:fwrite(ClangDbFile, "]~n", []),
ok = file:close(ClangDbFile)
end,
NewBins.
compileEach(_Config, [], _Type, _Env, {NewBins, CDB}) ->
{lists:reverse(NewBins), lists:reverse(CDB)};
compileEach(Config, [Source | Rest], Type, Env, {NewBins, CDB}) ->
Ext = filename:extension(Source),
Bin = replaceExtension(Source, Ext, ".o"),
Template = selectCompileTemplate(Type, compiler(Ext)),
Cmd = expandCommand(Template, Env, Source, Bin),
CDBEnt = cdbEntry(Source, Cmd, Rest),
NewCDB = [CDBEnt | CDB],
case needsCompile(Source, Bin) of
true ->
ShOpts = [{env, Env}, return_on_error, {use_stdout, false}],
execCompiler(Config, Source, Cmd, ShOpts),
compileEach(Config, Rest, Type, Env, {[Bin | NewBins], NewCDB});
false ->
?INFO("Skipping ~s\n", [Source]),
compileEach(Config, Rest, Type, Env, {NewBins, NewCDB})
end.
%% Generate a clang compilation db entry for Src and Cmd
cdbEntry(Src, Cmd, SrcRest) ->
%% Omit all variables from cmd, and use that as cmd in
%% CDB, because otherwise clang-* will complain about it.
CDBCmd = string:join(
lists:filter(
fun("$" ++ _) -> false;
(_) -> true
end,
string:tokens(Cmd, " ")),
" "),
Cwd = rebarUtils:getCwd(),
%% If there are more source files, make sure we end the CDB entry
%% with a comma.
Sep = case SrcRest of
[] -> "~n";
_ -> ",~n"
end,
%% CDB entry
?FMT("{ \"file\" : ~p~n"
", \"directory\" : ~p~n"
", \"command\" : ~p~n"
"}~s",
[Src, Cwd, CDBCmd, Sep]).
execCompiler(Config, Source, Cmd, ShOpts) ->
case rebarUtils:sh(Cmd, ShOpts) of
{error, {_RC, RawError}} ->
AbsSource =
case rebarUtils:processingBaseDir(Config) of
true ->
Source;
false ->
filename:absname(Source)
end,
?CONSOLE("Compiling ~s\n", [AbsSource]),
Error = re:replace(RawError, Source, AbsSource,
[{return, list}, global]),
?CONSOLE("~s", [Error]),
?FAIL;
{ok, Output} ->
?CONSOLE("Compiling ~s\n", [Source]),
?CONSOLE("~s", [Output])
end.
needsCompile(Source, Bin) ->
needsLink(Bin, [Source | binDeps(Bin)]).
%% NOTE: This relies on -MMD being passed to the compiler and returns an
%% empty list if the .d file is not available. This means header deps are
%% ignored on win32.
binDeps(Bin) ->
[DepFile] = portDeps([Bin]),
case file:read_file(DepFile) of
{ok, Deps} ->
Ds = parseBinDeps(list_to_binary(Bin), Deps),
Ds;
{error, _Err} ->
[]
end.
parseBinDeps(Bin, Deps) ->
Sz = size(Bin),
<<Bin:Sz/binary, ": ", X/binary>> = Deps,
Ds = re:split(X, "\\s*\\\\\\R\\s*|\\s+", [{return, binary}]),
[D || D <- Ds, D =/= <<>>].
needsLink(SoName, []) ->
filelib:last_modified(SoName) == 0;
needsLink(SoName, NewBins) ->
MaxLastMod = lists:max([filelib:last_modified(B) || B <- NewBins]),
case filelib:last_modified(SoName) of
0 ->
true;
Other ->
MaxLastMod >= Other
end.
%%
%% == port_specs ==
%%
getSpecs(Config, AppFile) ->
Specs =
case rebarConfig:getLocal(Config, port_specs, []) of
[] ->
%% No spec provided. Construct a spec
%% from old-school so_name and sources
[portSpecFromLegacy(Config, AppFile)];
PortSpecs ->
Filtered = filterPortSpecs(PortSpecs),
OsType = os:type(),
[getPortSpec(Config, OsType, Spec) || Spec <- Filtered]
end,
[S || S <- Specs, S#spec.sources /= []].
portSpecFromLegacy(Config, AppFile) ->
%% Get the target from the so_name variable
Target =
case rebarConfig:get(Config, so_name, undefined) of
undefined ->
%% Generate a sensible default from app file
{_, AppName} = rebarUtils:appName(Config, AppFile),
filename:join("priv", lists:concat([AppName, "_drv.so"]));
AName ->
%% Old form is available -- use it
filename:join("priv", AName)
end,
%% Get the list of source files from port_sources
Sources = portSources(rebarConfig:getList(Config, port_sources, ["c_src/*.c"])),
#spec{
type = targetType(Target),
link_lang = cc,
target = maybeSwitchExtension(os:type(), Target),
sources = Sources,
objects = portObjects(Sources)
}.
filterPortSpecs(Specs) ->
[S || S <- Specs, filterPortSpec(S)].
filterPortSpec({ArchRegex, _, _, _}) ->
rebarUtils:isArch(ArchRegex);
filterPortSpec({ArchRegex, _, _}) ->
rebarUtils:isArch(ArchRegex);
filterPortSpec({_, _}) ->
true.
getPortSpec(Config, OsType, {Target, Sources}) ->
getPortSpec(Config, OsType, {undefined, Target, Sources, []});
getPortSpec(Config, OsType, {Arch, Target, Sources}) ->
getPortSpec(Config, OsType, {Arch, Target, Sources, []});
getPortSpec(Config, OsType, {_Arch, Target, Sources, Opts}) ->
SourceFiles = portSources(Sources),
LinkLang =
case lists:any(
fun(Src) -> compiler(filename:extension(Src)) == "$CXX" end,
SourceFiles)
of
true -> cxx;
false -> cc
end,
ObjectFiles = portObjects(SourceFiles),
#spec{
type = targetType(Target),
target = maybeSwitchExtension(OsType, Target),
link_lang = LinkLang,
sources = SourceFiles,
objects = ObjectFiles,
opts = portOpts(Config, Opts)
}.
portSources(Sources) ->
lists:flatmap(fun filelib:wildcard/1, Sources).
portObjects(SourceFiles) ->
[replaceExtension(O, ".o") || O <- SourceFiles].
portDeps(SourceFiles) ->
[replaceExtension(O, ".d") || O <- SourceFiles].
portOpts(Config, Opts) ->
[portOpt(Config, O) || O <- Opts].
portOpt(Config, {env, Env}) ->
{env, setupEnv(Config, Env)};
portOpt(_Config, Opt) ->
Opt.
maybeSwitchExtension({win32, nt}, Target) ->
switchToDllOrExe(Target);
maybeSwitchExtension(_OsType, Target) ->
Target.
switchToDllOrExe(Target) ->
case filename:extension(Target) of
".so" -> filename:rootname(Target, ".so") ++ ".dll";
[] -> Target ++ ".exe";
_Other -> Target
end.
%% == port_env ==
%% Choose a compiler variable, based on a provided extension
compiler(".cc") -> "$CXX";
compiler(".cp") -> "$CXX";
compiler(".cxx") -> "$CXX";
compiler(".cpp") -> "$CXX";
compiler(".CPP") -> "$CXX";
compiler(".c++") -> "$CXX";
compiler(".C") -> "$CXX";
compiler(_) -> "$CC".
%% Given a list of {Key, Value} variables, and another list of default
%% {Key, Value} variables, return a merged list where the rule is if the
%% default is expandable expand it with the value of the variable list,
%% otherwise just return the value of the variable.
applyDefaults(Vars, Defaults) ->
dict:to_list(
dict:merge(
fun(Key, VarValue, DefaultValue) ->
case isExpandable(DefaultValue) of
true ->
rebarUtils:expandEnvVariable(DefaultValue,
Key,
VarValue);
false -> VarValue
end
end,
dict:from_list(Vars),
dict:from_list(Defaults))).
%%
%% Given a list of {Key, Value} environment variables, where Key may be defined
%% multiple times, walk the list and expand each self-reference so that we
%% end with a list of each variable singly-defined.
%%
mergeEachVar([], Vars) ->
Vars;
mergeEachVar([{Key, Value} | Rest], Vars) ->
Evalue =
case orddict:find(Key, Vars) of
error ->
%% Nothing yet defined for this key/value.
%% Expand any self-references as blank.
rebarUtils:expandEnvVariable(Value, Key, "");
{ok, Value0} ->
%% Use previous definition in expansion
rebarUtils:expandEnvVariable(Value, Key, Value0)
end,
mergeEachVar(Rest, orddict:store(Key, Evalue, Vars)).
%%
%% Give a unique list of {Key, Value} environment variables, expand each one
%% for every other key until no further expansions are possible.
%%
expandVarsLoop(Vars) ->
expandVarsLoop(Vars, [], dict:from_list(Vars), 10).
expandVarsLoop(_Pending, _Recurse, _Vars, 0) ->
?ABORT("Max. expansion reached for ENV vars!\n", []);
expandVarsLoop([], [], Vars, _Count) ->
lists:keysort(1, dict:to_list(Vars));
expandVarsLoop([], Recurse, Vars, Count) ->
expandVarsLoop(Recurse, [], Vars, Count - 1);
expandVarsLoop([{K, V} | Rest], Recurse, Vars, Count) ->
%% Identify the variables that need expansion in this value
ReOpts = [global, {capture, all_but_first, list}, unicode],
case re:run(V, "\\\${?(\\w+)}?", ReOpts) of
{match, Matches} ->
%% Identify the unique variables that need to be expanded
UniqueMatches = lists:usort([M || [M] <- Matches]),
%% For each variable, expand it and return the final
%% value. Note that if we have a bunch of unresolvable
%% variables, nothing happens and we don't bother
%% attempting further expansion
case expandKeysInValue(UniqueMatches, V, Vars) of
V ->
%% No change after expansion; move along
expandVarsLoop(Rest, Recurse, Vars, Count);
Expanded ->
%% Some expansion occurred; move to next k/v but
%% revisit this value in the next loop to check
%% for further expansion
NewVars = dict:store(K, Expanded, Vars),
expandVarsLoop(Rest, [{K, Expanded} | Recurse],
NewVars, Count)
end;
nomatch ->
%% No values in this variable need expansion; move along
expandVarsLoop(Rest, Recurse, Vars, Count)
end.
expandKeysInValue([], Value, _Vars) ->
Value;
expandKeysInValue([Key | Rest], Value, Vars) ->
NewValue =
case dict:find(Key, Vars) of
{ok, KValue} ->
rebarUtils:expandEnvVariable(Value, Key, KValue);
error ->
Value
end,
expandKeysInValue(Rest, NewValue, Vars).
expandCommand(TmplName, Env, InFiles, OutFile) ->
Cmd0 = proplists:get_value(TmplName, Env),
Cmd1 = rebarUtils:expandEnvVariable(Cmd0, "PORT_IN_FILES", InFiles),
rebarUtils:expandEnvVariable(Cmd1, "PORT_OUT_FILE", OutFile).
%%
%% Given a string, determine if it is expandable
%%
isExpandable(InStr) ->
case re:run(InStr, "\\\$", [{capture, none}]) of
match -> true;
nomatch -> false
end.
%%
%% Filter a list of env vars such that only those which match the provided
%% architecture regex (or do not have a regex) are returned.
%%
filterEnv([], Acc) ->
lists:reverse(Acc);
filterEnv([{ArchRegex, Key, Value} | Rest], Acc) ->
case rebarUtils:isArch(ArchRegex) of
true ->
filterEnv(Rest, [{Key, Value} | Acc]);
false ->
filterEnv(Rest, Acc)
end;
filterEnv([{Key, Value} | Rest], Acc) ->
filterEnv(Rest, [{Key, Value} | Acc]).
ertsDir() ->
lists:concat([code:root_dir(), "/erts-", erlang:system_info(version)]).
osEnv() ->
ReOpts = [{return, list}, {parts, 2}, unicode],
Os = [list_to_tuple(re:split(S, "=", ReOpts)) ||
S <- lists:filter(fun discardDepsVars/1, os:getenv())],
%% Drop variables without a name (win32)
[T1 || {K, _V} = T1 <- Os, K =/= []].
%%
%% To avoid having multiple repetitions of the same environment variables
%% (ERL_LIBS), avoid exporting any variables that may cause conflict with
%% those exported by the rebar_deps module (ERL_LIBS, REBAR_DEPS_DIR)
%%
discardDepsVars("ERL_LIBS=" ++ _Value) -> false;
discardDepsVars("REBAR_DEPS_DIR=" ++ _Value) -> false;
discardDepsVars(_Var) -> true.
selectCompileTemplate(drv, Compiler) ->
selectCompileDrvTemplate(Compiler);
selectCompileTemplate(exe, Compiler) ->
selectCompileExeTemplate(Compiler).
selectCompileDrvTemplate("$CC") -> "DRV_CC_TEMPLATE";
selectCompileDrvTemplate("$CXX") -> "DRV_CXX_TEMPLATE".
selectCompileExeTemplate("$CC") -> "EXE_CC_TEMPLATE";
selectCompileExeTemplate("$CXX") -> "EXE_CXX_TEMPLATE".
selectLinkTemplate(LinkLang, Target) ->
case {LinkLang, targetType(Target)} of
{cc, drv} -> "DRV_LINK_TEMPLATE";
{cxx, drv} -> "DRV_LINK_CXX_TEMPLATE";
{cc, exe} -> "EXE_LINK_TEMPLATE";
{cxx, exe} -> "EXE_LINK_CXX_TEMPLATE"
end.
targetType(Target) -> targetType_1(filename:extension(Target)).
targetType_1(".so") -> drv;
targetType_1(".dll") -> drv;
targetType_1("") -> exe;
targetType_1(".exe") -> exe.
erlInterfaceDir(Subdir) ->
case code:lib_dir(erl_interface, Subdir) of
{error, bad_name} ->
throw({error, {erl_interface, Subdir, "code:lib_dir(erl_interface)"
"is unable to find the erl_interface library."}});
Dir -> Dir
end.
defaultEnv() ->
Arch = os:getenv("REBAR_TARGET_ARCH"),
Vsn = os:getenv("REBAR_TARGET_ARCH_VSN"),
[
{"CC", getTool(Arch, Vsn, "gcc", "cc")},
{"CXX", getTool(Arch, Vsn, "g++", "c++")},
{"AR", getTool(Arch, "ar", "ar")},
{"AS", getTool(Arch, "as", "as")},
{"CPP", getTool(Arch, Vsn, "cpp", "cpp")},
{"LD", getTool(Arch, "ld", "ld")},
{"RANLIB", getTool(Arch, Vsn, "ranlib", "ranlib")},
{"STRIP", getTool(Arch, "strip", "strip")},
{"NM", getTool(Arch, "nm", "nm")},
{"OBJCOPY", getTool(Arch, "objcopy", "objcopy")},
{"OBJDUMP", getTool(Arch, "objdump", "objdump")},
{"DRV_CXX_TEMPLATE",
"$CXX -c $CXXFLAGS $DRV_CFLAGS $PORT_IN_FILES -o $PORT_OUT_FILE"},
{"DRV_CC_TEMPLATE",
"$CC -c $CFLAGS $DRV_CFLAGS $PORT_IN_FILES -o $PORT_OUT_FILE"},
{"DRV_LINK_TEMPLATE",
"$CC $PORT_IN_FILES $LDFLAGS $DRV_LDFLAGS -o $PORT_OUT_FILE"},
{"DRV_LINK_CXX_TEMPLATE",
"$CXX $PORT_IN_FILES $LDFLAGS $DRV_LDFLAGS -o $PORT_OUT_FILE"},
{"EXE_CXX_TEMPLATE",
"$CXX -c $CXXFLAGS $EXE_CFLAGS $PORT_IN_FILES -o $PORT_OUT_FILE"},
{"EXE_CC_TEMPLATE",
"$CC -c $CFLAGS $EXE_CFLAGS $PORT_IN_FILES -o $PORT_OUT_FILE"},
{"EXE_LINK_TEMPLATE",
"$CC $PORT_IN_FILES $LDFLAGS $EXE_LDFLAGS -o $PORT_OUT_FILE"},
{"EXE_LINK_CXX_TEMPLATE",
"$CXX $PORT_IN_FILES $LDFLAGS $EXE_LDFLAGS -o $PORT_OUT_FILE"},
{"DRV_CFLAGS", "-g -Wall -fPIC -MMD $ERL_CFLAGS"},
{"DRV_LDFLAGS", "-shared $ERL_LDFLAGS"},
{"EXE_CFLAGS", "-g -Wall -fPIC -MMD $ERL_CFLAGS"},
{"EXE_LDFLAGS", "$ERL_LDFLAGS"},
{"ERL_CFLAGS", lists:concat(
[
" -I\"", erlInterfaceDir(include),
"\" -I\"", filename:join(ertsDir(), "include"),
"\" "
])},
{"ERL_EI_LIBDIR", lists:concat(["\"", erlInterfaceDir(lib), "\""])},
{"ERL_LDFLAGS", " -L$ERL_EI_LIBDIR -lerl_interface -lei"},
{"ERLANG_ARCH", rebarUtils:wordsize()},
{"ERLANG_TARGET", rebarUtils:getArch()},
{"darwin", "DRV_LDFLAGS",
"-bundle -flat_namespace -undefined suppress $ERL_LDFLAGS"},
%% Solaris specific flags
{"solaris.*-64$", "CFLAGS", "-D_REENTRANT -m64 $CFLAGS"},
{"solaris.*-64$", "CXXFLAGS", "-D_REENTRANT -m64 $CXXFLAGS"},
{"solaris.*-64$", "LDFLAGS", "-m64 $LDFLAGS"},
%% OS X Leopard flags for 64-bit
{"darwin9.*-64$", "CFLAGS", "-m64 $CFLAGS"},
{"darwin9.*-64$", "CXXFLAGS", "-m64 $CXXFLAGS"},
{"darwin9.*-64$", "LDFLAGS", "-arch x86_64 -flat_namespace -undefined suppress $LDFLAGS"},
%% OS X Lion onwards flags for 64-bit
{"darwin1[0-4].*-64$", "CFLAGS", "-m64 $CFLAGS"},
{"darwin1[0-4].*-64$", "CXXFLAGS", "-m64 $CXXFLAGS"},
{"darwin1[0-4].*-64$", "LDFLAGS", "-arch x86_64 -flat_namespace -undefined suppress $LDFLAGS"},
%% OS X Snow Leopard, Lion, and Mountain Lion flags for 32-bit
{"darwin1[0-2].*-32", "CFLAGS", "-m32 $CFLAGS"},
{"darwin1[0-2].*-32", "CXXFLAGS", "-m32 $CXXFLAGS"},
{"darwin1[0-2].*-32", "LDFLAGS", "-arch i386 -flat_namespace -undefined suppress $LDFLAGS"},
%% Windows specific flags
%% add MS Visual C++ support to rebar on Windows
{"win32", "CC", "cl.exe"},
{"win32", "CXX", "cl.exe"},
{"win32", "LINKER", "link.exe"},
{"win32", "DRV_CXX_TEMPLATE",
%% DRV_* and EXE_* Templates are identical
"$CXX /c $CXXFLAGS $DRV_CFLAGS $PORT_IN_FILES /Fo$PORT_OUT_FILE"},
{"win32", "DRV_CC_TEMPLATE",
"$CC /c $CFLAGS $DRV_CFLAGS $PORT_IN_FILES /Fo$PORT_OUT_FILE"},
{"win32", "DRV_LINK_TEMPLATE",
"$LINKER $PORT_IN_FILES $LDFLAGS $DRV_LDFLAGS /OUT:$PORT_OUT_FILE"},
{"win32", "DRV_LINK_CXX_TEMPLATE",
"$LINKER $PORT_IN_FILES $LDFLAGS $DRV_LDFLAGS /OUT:$PORT_OUT_FILE"},
%% DRV_* and EXE_* Templates are identical
{"win32", "EXE_CXX_TEMPLATE",
"$CXX /c $CXXFLAGS $EXE_CFLAGS $PORT_IN_FILES /Fo$PORT_OUT_FILE"},
{"win32", "EXE_CC_TEMPLATE",
"$CC /c $CFLAGS $EXE_CFLAGS $PORT_IN_FILES /Fo$PORT_OUT_FILE"},
{"win32", "EXE_LINK_TEMPLATE",
"$LINKER $PORT_IN_FILES $LDFLAGS $EXE_LDFLAGS /OUT:$PORT_OUT_FILE"},
{"win32", "EXE_LINK_CXX_TEMPLATE",
"$LINKER $PORT_IN_FILES $LDFLAGS $EXE_LDFLAGS /OUT:$PORT_OUT_FILE"},
%% ERL_CFLAGS are ok as -I even though strictly it should be /I
{"win32", "ERL_LDFLAGS",
" /LIBPATH:$ERL_EI_LIBDIR erl_interface.lib ei.lib"},
{"win32", "DRV_CFLAGS", "/Zi /Wall $ERL_CFLAGS"},
{"win32", "DRV_LDFLAGS", "/DLL $ERL_LDFLAGS"},
%% Provide some default Windows defines for convenience
{"win32", "CFLAGS", "/Wall /DWIN32 /D_WINDOWS /D_WIN32 /DWINDOWS /Ic_src $CFLAGS"},
{"win32", "CXXFLAGS", "/Wall /DWIN32 /D_WINDOWS /D_WIN32 /DWINDOWS /Ic_src $CXXFLAGS"}
].
getTool(Arch, Tool, Default) ->
getTool(Arch, false, Tool, Default).
getTool(false, _, _, Default) -> Default;
getTool("", _, _, Default) -> Default;
getTool(Arch, false, Tool, _Default) -> Arch ++ "-" ++ Tool;
getTool(Arch, "", Tool, _Default) -> Arch ++ "-" ++ Tool;
getTool(Arch, Vsn, Tool, _Default) -> Arch ++ "-" ++ Tool ++ "-" ++ Vsn.

+ 520
- 0
src/rebarUtils.erl View File

@ -0,0 +1,520 @@
-module(rebarUtils).
-include("rebar.hrl").
-export([
getCwd/0,
isArch/1,
getArch/0,
isAppDir/0,
appName/2,
wordsize/0,
sh/2,
abort/0,
abort/2,
expandEnvVariable/3,
delayedHalt/1,
baseDir/1,
processingBaseDir/1,
initVsnCache/1,
deleteEach/1
]).
getCwd() ->
{ok, Dir} = file:get_cwd(),
filename:join([Dir]).
isArch(ArchRegex) ->
case re:run(getArch(), ArchRegex, [{capture, none}]) of
match ->
true;
nomatch ->
false
end.
%% REBAR_TARGET_ARCH, if used, should be set to the "standard"
%% target string. That is a prefix for binutils tools.
%% "x86_64-linux-gnu" or "arm-linux-gnueabi" are good candidates
%% ${REBAR_TARGET_ARCH}-gcc, ${REBAR_TARGET_ARCH}-ld ...
getArch() ->
Arch = os:getenv("REBAR_TARGET_ARCH"),
Words = wordSize(Arch),
otpRelease() ++ "-" ++ get_system_arch(Arch) ++ "-" ++ Words.
get_system_arch(Arch) when Arch =:= false; Arch =:= "" ->
erlang:system_info(system_architecture);
get_system_arch(Arch) ->
Arch.
isAppDir() ->
isAppDir(rebarUtils:getCwd()).
isAppDir(Dir) ->
SrcDir = filename:join([Dir, "src"]),
AppSrcScript = filename:join([SrcDir, "*.app.src.script"]),
AppSrc = filename:join([SrcDir, "*.app.src"]),
case {filelib:wildcard(AppSrcScript), filelib:wildcard(AppSrc)} of
{[AppSrcScriptFile], _} ->
{true, AppSrcScriptFile};
{[], [AppSrcFile]} ->
{true, AppSrcFile};
{[], []} ->
EbinDir = filename:join([Dir, "ebin"]),
App = filename:join([EbinDir, "*.app"]),
case filelib:wildcard(App) of
[AppFile] ->
{true, AppFile};
[] ->
false;
_ ->
?ERROR("More than one .app file in ~s~n", [EbinDir]),
false
end;
{_, _} ->
?ERROR("More than one .app.src file in ~s~n", [SrcDir]),
false
end.
appName(Config, AppFile) ->
case loadAppFile(Config, AppFile) of
{ok, NewConfig, AppName, _} ->
{NewConfig, AppName};
{error, Reason} ->
?ABORT("Failed to extract name from ~s: ~p\n",
[AppFile, Reason])
end.
loadAppFile(Config, Filename) ->
AppFile = {app_file, Filename},
case rebarConfig:getXconf(Config, {appfile, AppFile}, undefined) of
undefined ->
case consultAppFile(Filename) of
{ok, {application, AppName, AppData}} ->
Config1 = rebarConfig:setXconf(Config,
{appfile, AppFile},
{AppName, AppData}),
{ok, Config1, AppName, AppData};
{error, _} = Error ->
{error, {error, Error}};
Other ->
{error, {unexpected_terms, Other}}
end;
{AppName, AppData} ->
{ok, Config, AppName, AppData}
end.
consultAppFile(Filename) ->
Result = case lists:suffix(".app", Filename) of
true ->
file:consult(Filename);
false ->
rebarConfig:consultFile(Filename)
end,
case Result of
{ok, [Term]} ->
{ok, Term};
_ ->
Result
end.
wordsize() ->
wordSize(os:getenv("REBAR_TARGET_ARCH")).
%%
%% Options = [Option] -- defaults to [use_stdout, abort_on_error]
%% Option = ErrorOption | OutputOption | {cd, string()} | {env, Env}
%% ErrorOption = return_on_error | abort_on_error | {abort_on_error, string()}
%% OutputOption = use_stdout | {use_stdout, bool()}
%% Env = [{string(), Val}]
%% Val = string() | false
%%
sh(Command0, Options0) ->
?INFO("sh info:\n\tcwd: ~p\n\tcmd: ~s\n", [getCwd(), Command0]),
DefaultOptions = [use_stdout, abort_on_error],
Options = [expandShFlag(V)
|| V <- proplists:compact(Options0 ++ DefaultOptions)],
ErrorHandler = proplists:get_value(error_handler, Options),
OutputHandler = proplists:get_value(output_handler, Options),
Command = patchOnWindows(Command0, proplists:get_value(env, Options, [])),
PortSettings = proplists:get_all_values(port_settings, Options) ++
[exit_status, {line, 16384}, use_stdio, stderr_to_stdout, hide],
Port = open_port({spawn, Command}, PortSettings),
case sh_loop(Port, OutputHandler, []) of
{ok, _Output} = Ok ->
Ok;
{error, {_Rc, _Output} = Err} ->
ErrorHandler(Command, Err)
end.
-spec abort() -> no_return().
abort() ->
throw(rebar_abort).
-spec abort(string(), [term()]) -> no_return().
abort(String, Args) ->
?ERROR(String, Args),
abort().
%%
%% Given env. variable FOO we want to expand all references to
%% it in InStr. References can have two forms: $FOO and ${FOO}
%% The end of form $FOO is delimited with whitespace or eol
%%
expandEnvVariable(InStr, VarName, RawVarValue) ->
case string:chr(InStr, $$) of
0 ->
%% No variables to expand
InStr;
_ ->
ReOpts = [global, unicode, {return, list}],
VarValue = re:replace(RawVarValue, "\\\\", "\\\\\\\\", ReOpts),
%% Use a regex to match/replace:
%% Given variable "FOO", match $FOO\W | $FOOeol | ${FOO}.
RegEx = io_lib:format("\\\$(~s(\\W|$)|{~s})", [VarName, VarName]),
re:replace(InStr, RegEx, [VarValue, "\\2"], ReOpts)
end.
initVsnCache(Config) ->
initVsnCache(Config, os:getenv("REBAR_VSN_CACHE_FILE")).
initVsnCache(Config, false) ->
rebarConfig:setXconf(Config, vsn_cache, dict:new());
initVsnCache(Config, CacheFile) ->
{ok, CacheList} = file:consult(CacheFile),
CacheDict = dict:from_list(CacheList),
rebarConfig:setXconf(Config, vsn_cache, CacheDict).
-spec delayedHalt(integer()) -> no_return().
delayedHalt(Code) ->
%% Work around buffer flushing issue in erlang:halt if OTP older
%% than R15B01.
%% TODO: remove workaround once we require R15B01 or newer
%% R15B01 introduced erlang:halt/2
case erlang:is_builtin(erlang, halt, 2) of
true ->
halt(Code);
false ->
case os:type() of
{win32, nt} ->
timer:sleep(100),
halt(Code);
_ ->
halt(Code),
%% workaround to delay exit until all output is written
receive after infinity -> ok end
end
end.
deleteEach([]) ->
ok;
deleteEach([File | Rest]) ->
case file:delete(File) of
ok ->
deleteEach(Rest);
{error, enoent} ->
deleteEach(Rest);
{error, Reason} ->
?ERROR("Failed to delete file ~s: ~p\n", [File, Reason]),
?FAIL
end.
baseDir(Config) ->
rebarConfig:getXconf(Config, base_dir).
processingBaseDir(Config) ->
Cwd = rebarUtils:getCwd(),
processingBaseDir(Config, Cwd).
processingBaseDir(Config, Dir) ->
AbsDir = filename:absname(Dir),
AbsDir =:= baseDir(Config).
otpRelease() ->
case application:get_env(erlNpc, memoized_otp_release) of
{ok, Return} ->
Return;
undefined ->
Return = otpRelease_1(erlang:system_info(otp_release)),
application:set_env(erlNpc, memoized_otp_release, Return),
Return
end.
%% If OTP <= R16, otp_release is already what we want.
otpRelease_1([$R, N | _] = Rel) when is_integer(N) ->
Rel;
%% If OTP >= 17.x, erlang:system_info(otp_release) returns just the
%% major version number, we have to read the full version from
%% a file. See http://www.erlang.org/doc/system_principles/versions.html
otpRelease_1(Rel) ->
Files = [
filename:join([code:root_dir(), "releases", Rel, "OTP_VERSION"]),
filename:join([code:root_dir(), "OTP_VERSION"])
],
%% It's possible that none of the above files exist on the filesystem, in
%% which case, we're just going to rely on the provided "Rel" (which should
%% just be the value of `erlang:system_info(otp_release)`).
case readOtpVersionFiles(Files) of
undefined ->
warnMissingOtpVersionFile(Rel),
Rel;
Vsn ->
Vsn
end.
warnMissingOtpVersionFile(Rel) ->
?WARN("No OTP_VERSION file found. Using version string ~p.~n", [Rel]).
%% Try to open each file path provided, and if any of them exist on the
%% filesystem, read their contents and return the value of the first one found.
readOtpVersionFiles([]) ->
undefined;
readOtpVersionFiles([File | Rest]) ->
case file:read_file(File) of
{ok, Vsn} -> normalizeOtpVersion(Vsn);
{error, enoent} -> readOtpVersionFiles(Rest)
end.
%% Takes the Version binary as read from the OTP_VERSION file and strips any
%% trailing "**" and trailing "\n", returning the string as a list.
normalizeOtpVersion(Vsn) ->
%% It's fine to rely on the binary module here because we can
%% be sure that it's available when the otp_release string does
%% not begin with $R.
Size = byte_size(Vsn),
%% The shortest vsn string consists of at least two digits
%% followed by "\n". Therefore, it's safe to assume Size >= 3.
case binary:part(Vsn, {Size, -3}) of
<<"**\n">> ->
%% The OTP documentation mentions that a system patched
%% using the otp_patch_apply tool available to licensed
%% customers will leave a '**' suffix in the version as a
%% flag saying the system consists of application versions
%% from multiple OTP versions. We ignore this flag and
%% drop the suffix, given for all intents and purposes, we
%% cannot obtain relevant information from it as far as
%% tooling is concerned.
binary:bin_to_list(Vsn, {0, Size - 3});
_ ->
binary:bin_to_list(Vsn, {0, Size - 1})
end.
%% We do the shell variable substitution ourselves on Windows and hope that the
%% command doesn't use any other shell magic.
patchOnWindows(Cmd, Env) ->
case os:type() of
{win32, nt} ->
Cmd1 = "cmd /q /c "
++ lists:foldl(fun({Key, Value}, Acc) ->
expandEnvVariable(Acc, Key, Value)
end, Cmd, Env),
%% Remove left-over vars
re:replace(Cmd1, "\\\$\\w+|\\\${\\w+}", "",
[global, {return, list}]);
_ ->
Cmd
end.
expandShFlag(return_on_error) ->
{error_handler,
fun(_Command, Err) ->
{error, Err}
end};
expandShFlag({abort_on_error, Message}) ->
{error_handler,
logMsgAndAbort(Message)};
expandShFlag(abort_on_error) ->
{error_handler,
fun logAndAbort/2};
expandShFlag(use_stdout) ->
{output_handler,
fun(Line, Acc) ->
?CONSOLE("~s", [Line]),
[Line | Acc]
end};
expandShFlag({use_stdout, false}) ->
{output_handler,
fun(Line, Acc) ->
[Line | Acc]
end};
expandShFlag({cd, _CdArg} = Cd) ->
{port_settings, Cd};
expandShFlag({env, _EnvArg} = Env) ->
{port_settings, Env}.
-type err_handler() :: fun((string(), {integer(), string()}) -> no_return()).
-spec logMsgAndAbort(string()) -> err_handler().
logMsgAndAbort(Message) ->
fun(_Command, {_Rc, _Output}) ->
?ABORT(Message, [])
end.
-spec logAndAbort(string(), {integer(), string()}) -> no_return().
logAndAbort(Command, {Rc, Output}) ->
?ABORT("sh(~s)~n"
"failed with return code ~w and the following output:~n"
"~s~n", [Command, Rc, Output]).
sh_loop(Port, Fun, Acc) ->
receive
{Port, {data, {eol, Line}}} ->
sh_loop(Port, Fun, Fun(Line ++ "\n", Acc));
{Port, {data, {noeol, Line}}} ->
sh_loop(Port, Fun, Fun(Line, Acc));
{Port, {exit_status, 0}} ->
{ok, lists:flatten(lists:reverse(Acc))};
{Port, {exit_status, Rc}} ->
{error, {Rc, lists:flatten(lists:reverse(Acc))}}
end.
wordSize(Arch) when Arch =:= false; Arch =:= "" ->
nativeWordsize();
wordSize(Arch) ->
AllArchs = [
{"i686", "32"},
{"i386", "32"},
{"arm", "32"},
{"aarch64", "64"},
{"x86_64", "64"}
],
case matchWordSize(Arch, AllArchs) of
false ->
case crossWordSize(Arch) of
"" ->
envWordSize(os:getenv("REBAR_TARGET_ARCH_WORDSIZE"));
WordSize ->
WordSize
end;
{_, Wordsize} ->
Wordsize
end.
matchWordSize(Arch, [V = {Match, _Bits} | Vs]) ->
case re:run(Arch, Match, [{capture, none}]) of
match ->
V;
nomatch ->
matchWordSize(Arch, Vs)
end;
matchWordSize(_Arch, []) ->
false.
envWordSize(Wordsize) when Wordsize =:= false;
Wordsize =:= "" ->
?WARN("REBAR_TARGET_ARCH_WORDSIZE not set, assuming 32\n", []),
"32";
envWordSize(Wordsize) ->
case Wordsize of
"16" -> Wordsize;
"32" -> Wordsize;
"64" -> Wordsize;
_ ->
?WARN("REBAR_TARGET_ARCH_WORDSIZE bad value: ~p\n", [Wordsize]),
"32"
end.
%%
%% Find out the word size of the target by using Arch-gcc
%%
crossWordSize(Arch) ->
crossSizeof(Arch, "void*").
%%
%% Find the size of target Type using a specially crafted C file
%% that will report an error on the line of the byte size of the type.
%%
crossSizeof(Arch, Type) ->
Compiler = if Arch =:= "" -> "cc";
true -> Arch ++ "-gcc"
end,
TempFile = mkTempFile(".c"),
ok = file:write_file(TempFile,
<<"int t01 [1 - 2*(((long) (sizeof (TYPE))) == 1)];\n"
"int t02 [1 - 2*(((long) (sizeof (TYPE))) == 2)];\n"
"int t03 [1 - 2*(((long) (sizeof (TYPE))) == 3)];\n"
"int t04 [1 - 2*(((long) (sizeof (TYPE))) == 4)];\n"
"int t05 [1 - 2*(((long) (sizeof (TYPE))) == 5)];\n"
"int t06 [1 - 2*(((long) (sizeof (TYPE))) == 6)];\n"
"int t07 [1 - 2*(((long) (sizeof (TYPE))) == 7)];\n"
"int t08 [1 - 2*(((long) (sizeof (TYPE))) == 8)];\n"
"int t09 [1 - 2*(((long) (sizeof (TYPE))) == 9)];\n"
"int t10 [1 - 2*(((long) (sizeof (TYPE))) == 10)];\n"
"int t11 [1 - 2*(((long) (sizeof (TYPE))) == 11)];\n"
"int t12 [1 - 2*(((long) (sizeof (TYPE))) == 12)];\n"
"int t13 [1 - 2*(((long) (sizeof (TYPE))) == 13)];\n"
"int t14 [1 - 2*(((long) (sizeof (TYPE))) == 14)];\n"
"int t15 [1 - 2*(((long) (sizeof (TYPE))) == 15)];\n"
"int t16 [1 - 2*(((long) (sizeof (TYPE))) == 16)];\n"
>>),
Cmd = Compiler ++ " -DTYPE=\"" ++ Type ++ "\" " ++ TempFile,
ShOpts = [{use_stdout, false}, return_on_error],
{error, {_, Res}} = sh(Cmd, ShOpts),
ok = file:delete(TempFile),
case string:tokens(Res, ":") of
[_, Ln | _] ->
try list_to_integer(Ln) of
NumBytes -> integer_to_list(NumBytes * 8)
catch
error:_ ->
""
end;
_ ->
""
end.
mkTempFile(Suffix) ->
{A, B, C} = rebarNow(),
Dir = tempDir(),
File = "rebar_" ++ os:getpid() ++
integer_to_list(A) ++ "_" ++
integer_to_list(B) ++ "_" ++
integer_to_list(C) ++ Suffix,
filename:join(Dir, File).
tempDir() ->
case os:type() of
{win32, _} -> windowsTempDir();
_ -> "/tmp"
end.
windowsTempDir() ->
case os:getenv("TEMP") of
false ->
case os:getenv("TMP") of
false -> "C:/WINDOWS/TEMP";
TMP -> TMP
end;
TEMP -> TEMP
end.
rebarNow() ->
case erlang:function_exported(erlang, timestamp, 0) of
true ->
apply(erlang, timestamp, []);
false ->
%% erlang:now/0 was deprecated in 18.0. One solution to avoid the
%% deprecation warning is to use
%% -compile({nowarn_deprecated_function, [{erlang, now, 0}]}), but
%% that would raise a warning in versions older than 18.0. Calling
%% erlang:now/0 via apply/3 avoids that.
apply(erlang, now, [])
end.
nativeWordsize() ->
try erlang:system_info({wordsize, external}) of
Val ->
integer_to_list(8 * Val)
catch
error:badarg ->
integer_to_list(8 * erlang:system_info(wordsize))
end.

Loading…
Cancel
Save