You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

342 lines
13 KiB

  1. %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*-
  2. %% ex: ts=4 sw=4 et
  3. %% -------------------------------------------------------------------
  4. %%
  5. %% rebar: Erlang Build Tools
  6. %%
  7. %% Copyright (c) 2009 Dave Smith (dizzyd@dizzyd.com)
  8. %%
  9. %% Permission is hereby granted, free of charge, to any person obtaining a copy
  10. %% of this software and associated documentation files (the "Software"), to deal
  11. %% in the Software without restriction, including without limitation the rights
  12. %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. %% copies of the Software, and to permit persons to whom the Software is
  14. %% furnished to do so, subject to the following conditions:
  15. %%
  16. %% The above copyright notice and this permission notice shall be included in
  17. %% all copies or substantial portions of the Software.
  18. %%
  19. %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. %% THE SOFTWARE.
  26. %% -------------------------------------------------------------------
  27. -module(rebar_reltool).
  28. -export([generate/2,
  29. clean/2]).
  30. -include("rebar.hrl").
  31. -include_lib("reltool/src/reltool.hrl").
  32. -include_lib("kernel/include/file.hrl").
  33. %% ===================================================================
  34. %% Public API
  35. %% ===================================================================
  36. generate(Config, ReltoolFile) ->
  37. %% Make sure we have decent version of reltool available
  38. check_vsn(),
  39. %% Load the reltool configuration from the file
  40. ReltoolConfig = load_config(ReltoolFile),
  41. %% Spin up reltool server and load our config into it
  42. {ok, Server} = reltool:start_server([sys_tuple(ReltoolConfig)]),
  43. %% Do some validation of the reltool configuration; error messages out of
  44. %% reltool are still pretty cryptic
  45. validate_rel_apps(Server, sys_tuple(ReltoolConfig)),
  46. %% Finally, run reltool
  47. case catch(run_reltool(Server, Config, ReltoolConfig)) of
  48. ok ->
  49. ok;
  50. {error, failed} ->
  51. ?FAIL;
  52. Other2 ->
  53. ?ERROR("Unexpected error: ~p\n", [Other2]),
  54. ?FAIL
  55. end.
  56. clean(_Config, ReltoolFile) ->
  57. ReltoolConfig = load_config(ReltoolFile),
  58. TargetDir = target_dir(ReltoolConfig),
  59. rebar_file_utils:rm_rf(TargetDir),
  60. rebar_file_utils:delete_each(["reltool.spec"]).
  61. %% ===================================================================
  62. %% Internal functions
  63. %% ===================================================================
  64. check_vsn() ->
  65. case code:lib_dir(reltool) of
  66. {error, bad_name} ->
  67. ?ABORT("Reltool support requires the reltool application to be installed!", []);
  68. Path ->
  69. ReltoolVsn = filename:basename(Path),
  70. case ReltoolVsn < "reltool-0.5.2" of
  71. true ->
  72. ?ABORT("Reltool support requires at least reltool-0.5.2; this VM is using ~s\n",
  73. [ReltoolVsn]);
  74. false ->
  75. ok
  76. end
  77. end.
  78. %%
  79. %% Load terms from reltool.config
  80. %%
  81. load_config(ReltoolFile) ->
  82. case file:consult(ReltoolFile) of
  83. {ok, Terms} ->
  84. Terms;
  85. Other ->
  86. ?ABORT("Failed to load expected config from ~s: ~p\n", [ReltoolFile, Other])
  87. end.
  88. %%
  89. %% Look for the {sys, [...]} tuple in the reltool.config file. Without this present, we
  90. %% can't run reltool.
  91. %%
  92. sys_tuple(ReltoolConfig) ->
  93. case lists:keyfind(sys, 1, ReltoolConfig) of
  94. {sys, _} = SysTuple ->
  95. SysTuple;
  96. false ->
  97. ?ABORT("Failed to find {sys, [...]} tuple in reltool.config.", [])
  98. end.
  99. %%
  100. %% Look for {target_dir, TargetDir} in the reltool config file; if none is
  101. %% found, use the name of the release as the default target directory.
  102. %%
  103. target_dir(ReltoolConfig) ->
  104. case rebar_config:get_global(target_dir, undefined) of
  105. undefined ->
  106. case lists:keyfind(target_dir, 1, ReltoolConfig) of
  107. {target_dir, TargetDir} ->
  108. filename:absname(TargetDir);
  109. false ->
  110. {sys, SysInfo} = sys_tuple(ReltoolConfig),
  111. case lists:keyfind(rel, 1, SysInfo) of
  112. {rel, Name, _Vsn, _Apps} ->
  113. filename:absname(Name);
  114. false ->
  115. filename:absname("target")
  116. end
  117. end;
  118. TargetDir ->
  119. filename:absname(TargetDir)
  120. end.
  121. %%
  122. %% Look for overlay_vars file reference. The user can override this from the
  123. %% command line (i.e. globals), so we check there first and then fall back to
  124. %% what is present in the reltool.config file
  125. %%
  126. overlay_vars(ReltoolConfig) ->
  127. case rebar_config:get_global(overlay_vars, undefined) of
  128. undefined ->
  129. case lists:keyfind(overlay_vars, 1, ReltoolConfig) of
  130. {overlay_vars, File} ->
  131. File;
  132. false ->
  133. undefined
  134. end;
  135. File ->
  136. File
  137. end.
  138. validate_rel_apps(ReltoolServer, {sys, ReltoolConfig}) ->
  139. case lists:keyfind(rel, 1, ReltoolConfig) of
  140. false ->
  141. ok;
  142. {rel, _Name, _Vsn, Apps} ->
  143. %% Identify all the apps that do NOT exist, based on what's available
  144. %% from the reltool server
  145. Missing = lists:sort([App || App <- Apps,
  146. app_exists(App, ReltoolServer) == false]),
  147. case Missing of
  148. [] ->
  149. ok;
  150. _ ->
  151. ?ABORT("Apps in {rel, ...} section not found by reltool: ~p\n", [Missing])
  152. end;
  153. Rel ->
  154. %% Invalid release format!
  155. ?ABORT("Invalid {rel, ...} section in reltools.config: ~p\n", [Rel])
  156. end.
  157. app_exists(App, Server) when is_atom(App) ->
  158. case reltool_server:get_app(Server, App) of
  159. {ok, _} ->
  160. true;
  161. _ ->
  162. false
  163. end;
  164. app_exists(AppTuple, Server) when is_tuple(AppTuple) ->
  165. app_exists(element(1, AppTuple), Server).
  166. run_reltool(Server, _Config, ReltoolConfig) ->
  167. case reltool:get_target_spec(Server) of
  168. {ok, Spec} ->
  169. %% Pull the target dir and make sure it exists
  170. TargetDir = target_dir(ReltoolConfig),
  171. mk_target_dir(TargetDir),
  172. %% Dump the spec, if necessary
  173. dump_spec(Spec),
  174. %% Have reltool actually run
  175. case reltool:eval_target_spec(Spec, code:root_dir(), TargetDir) of
  176. ok ->
  177. ok;
  178. {error, Reason} ->
  179. ?ABORT("Failed to generate target from spec: ~p\n", [Reason])
  180. end,
  181. %% Initialize overlay vars with some basics (that can get overwritten)
  182. OverlayVars0 = [{erts_vsn, "erts-" ++ erlang:system_info(version)}],
  183. %% Load up any variables specified by overlay_vars
  184. OverlayVars = case overlay_vars(ReltoolConfig) of
  185. undefined ->
  186. dict:from_list(OverlayVars0);
  187. File ->
  188. case file:consult(File) of
  189. {ok, Terms} ->
  190. dict:from_list(OverlayVars0 ++ Terms);
  191. {error, Reason2} ->
  192. ?ABORT("Unable to load overlay_vars from ~s: ~p\n",
  193. [File, Reason2])
  194. end
  195. end,
  196. %% Finally, overlay the files specified by the overlay section
  197. case lists:keysearch(overlay, 1, ReltoolConfig) of
  198. {value, {overlay, Overlay}} when is_list(Overlay) ->
  199. execute_overlay(Overlay, OverlayVars, rebar_utils:get_cwd(), TargetDir);
  200. {value, _} ->
  201. ?ABORT("{overlay, [...]} entry in reltool.config must be a list.\n", []);
  202. false ->
  203. ?INFO("No {overlay, [...]} found in reltool.config.\n", [])
  204. end;
  205. {error, Reason} ->
  206. ?ABORT("Unable to generate spec: ~s\n", [Reason])
  207. end.
  208. mk_target_dir(TargetDir) ->
  209. case file:make_dir(TargetDir) of
  210. ok ->
  211. ok;
  212. {error, eexist} ->
  213. %% Output directory already exists; if force=1, wipe it out
  214. case rebar_config:get_global(force, "0") of
  215. "1" ->
  216. rebar_file_utils:rm_rf(TargetDir),
  217. ok = file:make_dir(TargetDir);
  218. _ ->
  219. ?ERROR("Release target directory ~p already exists!\n", [TargetDir]),
  220. ?FAIL
  221. end
  222. end.
  223. dump_spec(Spec) ->
  224. case rebar_config:get_global(dump_spec, "0") of
  225. "1" ->
  226. SpecBin = list_to_binary(io_lib:print(Spec, 1, 120, -1)),
  227. ok = file:write_file("reltool.spec", SpecBin);
  228. _ ->
  229. ok
  230. end.
  231. execute_overlay([], _Vars, _BaseDir, _TargetDir) ->
  232. ok;
  233. execute_overlay([{mkdir, Out} | Rest], Vars, BaseDir, TargetDir) ->
  234. OutFile = render(filename:join([TargetDir, Out, "dummy"]), Vars),
  235. ok = filelib:ensure_dir(OutFile),
  236. ?DEBUG("Created dir ~s\n", [filename:dirname(OutFile)]),
  237. execute_overlay(Rest, Vars, BaseDir, TargetDir);
  238. execute_overlay([{copy, In} | Rest], _Vars, BaseDir, TargetDir) ->
  239. execute_overlay([{copy, In, ""} | Rest], _Vars, BaseDir, TargetDir);
  240. execute_overlay([{copy, In, Out} | Rest], Vars, BaseDir, TargetDir) ->
  241. InFile = render(filename:join(BaseDir, In), Vars),
  242. OutFile = render(filename:join(TargetDir, Out), Vars),
  243. case filelib:is_dir(InFile) of
  244. true ->
  245. ok;
  246. false ->
  247. ok = filelib:ensure_dir(OutFile)
  248. end,
  249. rebar_file_utils:cp_r([InFile], OutFile),
  250. execute_overlay(Rest, Vars, BaseDir, TargetDir);
  251. execute_overlay([{template, In, Out} | Rest], Vars, BaseDir, TargetDir) ->
  252. InFile = render(filename:join(BaseDir, In), Vars),
  253. {ok, InFileData} = file:read_file(InFile),
  254. OutFile = render(filename:join(TargetDir, Out), Vars),
  255. ok = filelib:ensure_dir(OutFile),
  256. case file:write_file(OutFile, render(InFileData, Vars)) of
  257. ok ->
  258. ok = apply_file_info(InFile, OutFile),
  259. ?DEBUG("Templated ~p\n", [OutFile]),
  260. execute_overlay(Rest, Vars, BaseDir, TargetDir);
  261. {error, Reason} ->
  262. ?ABORT("Failed to template ~p: ~p\n", [OutFile, Reason])
  263. end;
  264. execute_overlay([{create, Out, Contents} | Rest], Vars, BaseDir, TargetDir) ->
  265. OutFile = render(filename:join(TargetDir, Out), Vars),
  266. ok = filelib:ensure_dir(OutFile),
  267. case file:write_file(OutFile, Contents) of
  268. ok ->
  269. ?DEBUG("Created ~p\n", [OutFile]),
  270. execute_overlay(Rest, Vars, BaseDir, TargetDir);
  271. {error, Reason} ->
  272. ?ABORT("Failed to create ~p: ~p\n", [OutFile, Reason])
  273. end;
  274. execute_overlay([{replace, Out, Regex, Replacement} | Rest],
  275. Vars, BaseDir, TargetDir) ->
  276. execute_overlay([{replace, Out, Regex, Replacement, []} | Rest], Vars, BaseDir, TargetDir);
  277. execute_overlay([{replace, Out, Regex, Replacement, Opts} | Rest],
  278. Vars, BaseDir, TargetDir) ->
  279. Filename = render(filename:join(TargetDir, Out), Vars),
  280. {ok, OrigData} = file:read_file(Filename),
  281. Data = re:replace(OrigData, Regex, Replacement, [global, {return, binary}] ++ Opts),
  282. case file:write_file(Filename, Data) of
  283. ok ->
  284. ?DEBUG("Edited ~s: s/~s/~s/\n", [Filename, Regex, Replacement]),
  285. execute_overlay(Rest, Vars, BaseDir, TargetDir);
  286. {error, Reason} ->
  287. ?ABORT("Failed to edit ~p: ~p\n", [Filename, Reason])
  288. end;
  289. execute_overlay([Other | _Rest], _Vars, _BaseDir, _TargetDir) ->
  290. {error, {unsupported_operation, Other}}.
  291. %%
  292. %% Render a binary to a string, using mustache and the specified context
  293. %%
  294. render(Bin, Context) ->
  295. ReOpts = [global, {return, list}],
  296. %% Be sure to escape any double-quotes before rendering...
  297. Str0 = re:replace(Bin, "\\\\", "\\\\\\", ReOpts),
  298. Str1 = re:replace(Str0, "\"", "\\\\\"", ReOpts),
  299. mustache:render(Str1, Context).
  300. apply_file_info(InFile, OutFile) ->
  301. {ok, FileInfo} = file:read_file_info(InFile),
  302. ok = file:write_file_info(OutFile, FileInfo).