ERL in corp app for fortage
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.
 
 
 
 
 
 

505 lines
18 KiB

  1. #!/usr/bin/env escript
  2. %% -*- erlang -*-
  3. %% vim: ts=4 sw=4 et ft=erlang
  4. %%
  5. %% Coyright 2013 - Jesse Gumm
  6. %%
  7. %% MIT License
  8. %%
  9. %% About
  10. %%
  11. %% This little script assists the Nitrogen Web Framework by simplifying the
  12. %% process of including custom elements, actions, or validators without having
  13. %% to worry about including a different header for each, or manually copying
  14. %% records from each element's header into the "records.hrl".
  15. %%
  16. %% Further, this copies the plugin static information (javascript, images, etc)
  17. %% into the site/static/plugins/<pluginname> directory. Then one doesn't need
  18. %% to copy that information for each upgrade.
  19. %%
  20. %% TO MAKE A PLUGIN WORK WITH THIS SCRIPT:
  21. %%
  22. %% 1) Make sure your plugin is able to be used as a rebar dependency
  23. %% 2) Include a file "nitrogen.plugin" in the root of the plugin directory
  24. %% 3) If you have any static resources (js, images) put them in a 'static'
  25. %% directory
  26. %% 4) If this is a custom element, action, or validator, or requires anything
  27. %% else that should be included in a header, add them to (one or more) .hrl
  28. %% files in 'include'
  29. %% 5) From your Nitrogen app's directory, include your plugin as a rebar
  30. %% dependency and run 'make'
  31. %%
  32. %% Sample Plugin Directory Structure:
  33. %%
  34. %% myplugin/
  35. %% ebin/
  36. %%
  37. %% src/
  38. %% element_myplugin.erl
  39. %% myplugin.app.src
  40. %%
  41. %% include/
  42. %% myplugin.hrl
  43. %%
  44. %% priv/
  45. %% templates/
  46. %% mytemplate.html
  47. %%
  48. %% static/
  49. %% js/
  50. %% myplugin.js
  51. %% jquery-some-plugin.js
  52. %% images/
  53. %% some_image.png
  54. %% css/
  55. %% myplugin.css
  56. %%
  57. %%
  58. %% When the plugin is processed, the following will be updated in your Nitrogen App
  59. %%
  60. %% site/
  61. %% include/
  62. %% plugins.hrl (will include a reference to all .hrl files from your plugin)
  63. %% static/
  64. %% plugins/
  65. %% myplugin/
  66. %% js/
  67. %% myplugin.js
  68. %% jquery-some-plugin.js
  69. %% images/
  70. %% some_image.png
  71. %% css/
  72. %% myplugin.css
  73. %% templates/
  74. %% plugins/
  75. %% mytemplate.html
  76. %%
  77. %% (Note: The Erlang/Nitrogen Element code is not copied, it'll be loaded just
  78. %% like any rebar dependency's code)
  79. main([]) ->
  80. io:format("Checking for Nitrogen Plugins\n"),
  81. RebarConfig = get_config("rebar.config"),
  82. PluginConfig = get_config("plugins.config"),
  83. DepDirs = proplists:get_value(deps_dir, RebarConfig, ["lib"]),
  84. {Includes,Statics,Templates} = lists:foldl(fun(Dir, {Inc, Stat, Temp}) ->
  85. {ok, FoundIncludes, FoundStatics, FoundTemplates} = get_plugins(Dir),
  86. {FoundIncludes ++ Inc, FoundStatics ++ Stat, FoundTemplates ++ Temp}
  87. end, {[],[],[]}, DepDirs),
  88. case {Includes, Statics, Templates} of
  89. {[],[],[]} ->
  90. io:format("No Nitrogen Plugins Found~n");
  91. _ ->
  92. io:format("Generating aggregate plugin header (plugins.hrl)~n"),
  93. generate_plugin_include(PluginConfig, Includes),
  94. io:format("Generating plugin static directories~n"),
  95. generate_plugin_static(PluginConfig, Statics),
  96. io:format("Generating plugin template directories~n"),
  97. generate_plugin_templates(PluginConfig, Templates),
  98. io:format("Plugin generation complete~n")
  99. end.
  100. get_plugins(DepDir) ->
  101. Files = case file:list_dir(DepDir) of
  102. {ok, F} -> F;
  103. {error, _} -> []
  104. end,
  105. {Inc,Stat,Temp} = lists:foldl(fun(X,PluginInfo={Includes,Statics,Templates}) ->
  106. PluginPath = filename:join(DepDir,X),
  107. case analyze_path(PluginPath) of
  108. undefined ->
  109. %% Not a plugin, so just continue
  110. PluginInfo;
  111. {ok, FoundIncludes, FoundStatics, FoundTemplates} ->
  112. {FoundIncludes++Includes, FoundStatics++Statics, FoundTemplates++Templates}
  113. end
  114. end,{[],[],[]},Files),
  115. {ok, Inc, Stat, Temp}.
  116. get_config(File) ->
  117. case file:consult(File) of
  118. {error, _} -> [];
  119. {ok, Config} -> Config
  120. end.
  121. analyze_path(Path) ->
  122. case is_dir_or_symlink_dir(Path) of
  123. true ->
  124. {ok, Files} = file:list_dir(Path),
  125. case lists:member("nitrogen.plugin",Files) of
  126. false ->
  127. undefined;
  128. true ->
  129. io:format("Found a Nitrogen plugin in ~p~n",[Path]),
  130. IncludeDir = filename:join(Path,include),
  131. StaticDir = filename:join(Path,static),
  132. PrivStaticDir = filename:join([Path,priv,static]),
  133. TemplateDir = filename:join([Path,priv,templates]),
  134. Includes = analyze_path_include(IncludeDir),
  135. %% Originally, the plugin spec called for statics to be
  136. %% located in the "static" dir, however, it's more
  137. %% OTP-compliant to have statics to be located in
  138. %% "priv/static", so we support both here with StaticDir
  139. %% and PrivStaticDir
  140. Statics = analyze_path_exists_only(StaticDir)
  141. ++ analyze_path_exists_only(PrivStaticDir),
  142. Templates = analyze_path_exists_only(TemplateDir),
  143. {ok, Includes, Statics, Templates}
  144. end;
  145. false -> undefined
  146. end.
  147. is_dir_or_symlink_dir(Path) ->
  148. case filelib:is_dir(Path) of
  149. true -> true;
  150. false ->
  151. case file:read_link(Path) of
  152. {ok, _} -> true;
  153. {error, _} -> false
  154. end
  155. end.
  156. analyze_path_include(Path) ->
  157. case filelib:is_dir(Path) of
  158. false -> [];
  159. true -> list_includes(Path)
  160. end.
  161. list_includes(Path) ->
  162. {ok, Files} = file:list_dir(Path),
  163. Includes = filter_non_includes(Files),
  164. [filename:join(Path,Inc) || Inc <- Includes].
  165. filter_non_includes([]) -> [];
  166. filter_non_includes([File | Files]) ->
  167. case re:run(File,"\.hrl$",[{capture,none}]) of
  168. match -> [File | filter_non_includes(Files)];
  169. nomatch -> filter_non_includes(Files)
  170. end.
  171. analyze_path_exists_only(Path) ->
  172. case filelib:is_dir(Path) of
  173. false -> [];
  174. true -> [Path]
  175. end.
  176. generate_plugin_include(Config, Includes) ->
  177. IncludeFile = proplists:get_value(plugins_hrl, Config, "site/include/plugins.hrl"),
  178. HeaderLines = ["%% Automatically Generated by do-plugins.escript",
  179. "%% Manually editing this file is not recommended."],
  180. PluginLines = [includify(I) || I <- Includes],
  181. PluginContents = string:join(HeaderLines ++ PluginLines,"\n"),
  182. FinalContents = iolist_to_binary(PluginContents),
  183. CurrentContents = get_current_include(IncludeFile),
  184. case FinalContents == CurrentContents of
  185. true -> io:format("No changes to ~p~n",[IncludeFile]);
  186. false -> file:write_file(IncludeFile,PluginContents)
  187. end.
  188. get_current_include(File) ->
  189. case file:read_file(File) of
  190. {ok, Binary} -> Binary;
  191. {error, _} -> <<>>
  192. end.
  193. includify("lib/" ++ Path) ->
  194. "-include_lib(\"" ++ Path ++ "\").";
  195. includify(Path) ->
  196. "-include(\"" ++ filename:join("..",Path) ++ "\").".
  197. generate_plugin_static(Config, Statics) ->
  198. PluginStaticBase = proplists:get_value(static_dir, Config, "site/static/plugins"),
  199. CopyMode = proplists:get_value(copy_mode, Config, copy),
  200. clear_plugin_dir(PluginStaticBase),
  201. filelib:ensure_dir(filename:join(PluginStaticBase,dummy)),
  202. [generate_plugin_copy_worker(PluginStaticBase,CopyMode,Static) || Static <- Statics].
  203. clear_plugin_dir(Dir) ->
  204. rm_rf(Dir).
  205. plugin_name_from_static_path(PluginStatic) ->
  206. PluginStaticParts = filename:split(PluginStatic),
  207. case lists:reverse(PluginStaticParts) of
  208. ["templates","priv",PluginName|_] -> PluginName;
  209. ["templates",PluginName|_] -> PluginName;
  210. ["static","priv",PluginName|_] -> PluginName;
  211. ["static",PluginName|_] -> PluginName
  212. end.
  213. generate_plugin_copy_worker(PluginBase, CopyMode, PluginStatic) ->
  214. %% Split the Plugin Static Dir into parts and extract the name of the plugin
  215. %% ie "lib/whatever/static" - the Plugin Name is "whatever"
  216. PluginName = plugin_name_from_static_path(PluginStatic),
  217. %% And we're going to copy it into our system's plugin static dir
  218. %% (ie "site/static/plugins/whatever")
  219. FinalDestination = filename:join(PluginBase,PluginName),
  220. %% And let's copy or link them.
  221. case CopyMode of
  222. link ->
  223. LinkPrefix = make_link_relative_prefix(FinalDestination),
  224. file:make_symlink(filename:join([LinkPrefix,PluginStatic]),FinalDestination);
  225. copy ->
  226. %% We want to copy the contents of the Plugin's static dir,
  227. %% So we're copying from /lib/whatever/static/*
  228. PluginSource = filename:join(PluginStatic,"*"),
  229. %% Make sure the dir exists to copy into
  230. filelib:ensure_dir(filename:join(FinalDestination,dummy)),
  231. %% And copy the directory
  232. try cp_r([PluginSource],FinalDestination)
  233. catch _:_ -> ok %% getting here usually just means that the source directory is empty
  234. end;
  235. Other ->
  236. throw({invalid_copy_mode, Other})
  237. end.
  238. generate_plugin_templates(Config, Templates) ->
  239. TemplateBase = proplists:get_value(template_dir, Config, "site/templates/plugins"),
  240. CopyMode = proplists:get_value(copy_mode, Config, copy),
  241. clear_plugin_dir(TemplateBase),
  242. filelib:ensure_dir(filename:join(TemplateBase,dummy)),
  243. [generate_plugin_copy_worker(TemplateBase, CopyMode, Template) || Template <- Templates].
  244. %% Because the symlink is relative, we need to make sure it includes the
  245. %% proper relative path prefix (ie, the right number of "../../../" to get us
  246. %% back before linking
  247. make_link_relative_prefix("./" ++ Path) ->
  248. make_link_relative_prefix(Path);
  249. make_link_relative_prefix(Path) ->
  250. Parts = filename:split(Path),
  251. Parts2 = lists:sublist(Parts, length(Parts)-1),
  252. Parts3 = [relative_path_helper(Part) || Part <- Parts2],
  253. filename:join(Parts3).
  254. relative_path_helper(".") -> ".";
  255. relative_path_helper("..") -> "";
  256. relative_path_helper(_) -> "..".
  257. %% -------------------------------------------------------------------------
  258. %% ------------- File Actions (copied mostly from Rebar) -------------------
  259. %% -------------------------------------------------------------------------
  260. -define(CONSOLE(Str, Args), io:format(Str,Args)).
  261. -define(FMT(Str,Args), lists:flatten(io_lib:format(Str,Args))).
  262. rm_rf(Target) ->
  263. case os:type() of
  264. {unix, _} ->
  265. EscTarget = escape_spaces(Target),
  266. {ok, []} = sh(?FMT("rm -rf ~s", [EscTarget]),
  267. [{use_stdout, false}, return_on_error]),
  268. ok;
  269. {win32, _} ->
  270. Filelist = filelib:wildcard(Target),
  271. Dirs = [F || F <- Filelist, filelib:is_dir(F)],
  272. Files = Filelist -- Dirs,
  273. ok = delete_each(Files),
  274. ok = delete_each_dir_win32(Dirs),
  275. ok
  276. end.
  277. delete_each([]) ->
  278. ok;
  279. delete_each([File | Rest]) ->
  280. case file:delete(File) of
  281. ok ->
  282. delete_each(Rest);
  283. {error, enoent} ->
  284. delete_each(Rest);
  285. {error, Reason} ->
  286. ExitMsg = ?FMT("Error: Failed to delete file ~s: ~p\n", [File, Reason]),
  287. exit(ExitMsg)
  288. end.
  289. delete_each_dir_win32([]) -> ok;
  290. delete_each_dir_win32([Dir | Rest]) ->
  291. {ok, []} = sh(?FMT("rd /q /s \"~s\"",
  292. [filename:nativename(Dir)]),
  293. [{use_stdout, false}, return_on_error]),
  294. delete_each_dir_win32(Rest).
  295. cp_r(Sources, Dest) ->
  296. case os:type() of
  297. {unix, _} ->
  298. EscSources = [escape_spaces(Src) || Src <- Sources],
  299. SourceStr = string:join(EscSources, " "),
  300. {ok, []} = sh(?FMT("cp -R ~s \"~s\"",
  301. [SourceStr, Dest]),
  302. [{use_stdout, false}, return_on_error]),
  303. ok;
  304. {win32, _} ->
  305. lists:foreach(fun(Src) -> ok = cp_r_win32(Src,Dest) end, Sources),
  306. ok
  307. end.
  308. %% Windows stuff
  309. xcopy_win32(Source,Dest)->
  310. {ok, R} = sh(
  311. ?FMT("xcopy \"~s\" \"~s\" /q /y /e 2> nul",
  312. [filename:nativename(Source), filename:nativename(Dest)]),
  313. [{use_stdout, false}, return_on_error]),
  314. case length(R) > 0 of
  315. %% when xcopy fails, stdout is empty and and error message is printed
  316. %% to stderr (which is redirected to nul)
  317. true -> ok;
  318. false ->
  319. {error, lists:flatten(
  320. io_lib:format("Failed to xcopy from ~s to ~s~n",
  321. [Source, Dest]))}
  322. end.
  323. cp_r_win32({true, SourceDir}, {true, DestDir}) ->
  324. %% from directory to directory
  325. SourceBase = filename:basename(SourceDir),
  326. ok = case file:make_dir(filename:join(DestDir, SourceBase)) of
  327. {error, eexist} -> ok;
  328. Other -> Other
  329. end,
  330. ok = xcopy_win32(SourceDir, filename:join(DestDir, SourceBase));
  331. cp_r_win32({false, Source} = S,{true, DestDir}) ->
  332. %% from file to directory
  333. cp_r_win32(S, {false, filename:join(DestDir, filename:basename(Source))});
  334. cp_r_win32({false, Source},{false, Dest}) ->
  335. %% from file to file
  336. {ok,_} = file:copy(Source, Dest),
  337. ok;
  338. cp_r_win32({true, SourceDir},{false,DestDir}) ->
  339. IsFile = filelib:is_file(DestDir),
  340. case IsFile of
  341. true ->
  342. %% From Directory to file? This shouldn't happen
  343. {error,[{rebar_file_utils,cp_r_win32},
  344. {directory_to_file_makes_no_sense,
  345. {source,SourceDir},{dest,DestDir}}]};
  346. false ->
  347. %% Specifying a target directory that doesn't currently exist.
  348. %% So Let's attempt to create this directory
  349. %% Will not recursively create parent directories
  350. ok = case file:make_dir(DestDir) of
  351. {error, eexist} -> ok;
  352. Other -> Other
  353. end,
  354. ok = xcopy_win32(SourceDir, DestDir)
  355. end;
  356. cp_r_win32(Source,Dest) ->
  357. Dst = {filelib:is_dir(Dest), Dest},
  358. lists:foreach(fun(Src) ->
  359. ok = cp_r_win32({filelib:is_dir(Src), Src}, Dst)
  360. end, filelib:wildcard(Source)),
  361. ok.
  362. %% Shell Command Stuff (from rebar_utils)
  363. sh(Command0, Options0) ->
  364. % ?CONSOLE("sh info:\n\tcwd: ~p\n\tcmd: ~s\n", [get_cwd(), Command0]),
  365. % ?DEBUG("\topts: ~p\n", [Options0]),
  366. DefaultOptions = [use_stdout, abort_on_error],
  367. Options = [expand_sh_flag(V)
  368. || V <- proplists:compact(Options0 ++ DefaultOptions)],
  369. ErrorHandler = proplists:get_value(error_handler, Options),
  370. OutputHandler = proplists:get_value(output_handler, Options),
  371. Command = patch_on_windows(Command0, proplists:get_value(env, Options, [])),
  372. PortSettings = proplists:get_all_values(port_settings, Options) ++
  373. [exit_status, {line, 16384}, use_stdio, stderr_to_stdout, hide],
  374. Port = open_port({spawn, Command}, PortSettings),
  375. case sh_loop(Port, OutputHandler, []) of
  376. {ok, _Output} = Ok ->
  377. Ok;
  378. {error, {_Rc, _Output}=Err} ->
  379. ErrorHandler(Command, Err)
  380. end.
  381. %%get_cwd() ->
  382. %% {ok,Dir} = file:get_cwd(),
  383. %% Dir.
  384. patch_on_windows(Cmd, Env) ->
  385. case os:type() of
  386. {win32,nt} ->
  387. "cmd /q /c "
  388. ++ lists:foldl(fun({Key, Value}, Acc) ->
  389. expand_env_variable(Acc, Key, Value)
  390. end, Cmd, Env);
  391. _ ->
  392. Cmd
  393. end.
  394. expand_env_variable(InStr, VarName, RawVarValue) ->
  395. case string:chr(InStr, $$) of
  396. 0 ->
  397. %% No variables to expand
  398. InStr;
  399. _ ->
  400. VarValue = re:replace(RawVarValue, "\\\\", "\\\\\\\\", [global]),
  401. %% Use a regex to match/replace:
  402. %% Given variable "FOO": match $FOO\s | $FOOeol | ${FOO}
  403. RegEx = io_lib:format("\\\$(~s(\\s|$)|{~s})", [VarName, VarName]),
  404. ReOpts = [global, {return, list}],
  405. re:replace(InStr, RegEx, [VarValue, "\\2"], ReOpts)
  406. end.
  407. sh_loop(Port, Fun, Acc) ->
  408. receive
  409. {Port, {data, {eol, Line}}} ->
  410. sh_loop(Port, Fun, Fun(Line ++ "\n", Acc));
  411. {Port, {data, {noeol, Line}}} ->
  412. sh_loop(Port, Fun, Fun(Line, Acc));
  413. {Port, {exit_status, 0}} ->
  414. {ok, lists:flatten(lists:reverse(Acc))};
  415. {Port, {exit_status, Rc}} ->
  416. {error, {Rc, lists:flatten(lists:reverse(Acc))}}
  417. end.
  418. escape_spaces(Str) ->
  419. re:replace(Str, " ", "\\\\ ", [global, {return, list}]).
  420. log_and_abort(Message) ->
  421. ?CONSOLE("Aborting: ~s ~n",[Message]).
  422. expand_sh_flag(return_on_error) ->
  423. {error_handler,
  424. fun(_Command, Err) ->
  425. {error, Err}
  426. end};
  427. expand_sh_flag({abort_on_error, Message}) ->
  428. {error_handler,
  429. fun(Cmd,Err) ->
  430. log_and_abort({Message,{Cmd,Err}})
  431. end};
  432. expand_sh_flag(abort_on_error) ->
  433. expand_sh_flag({abort_on_error, error});
  434. expand_sh_flag(use_stdout) ->
  435. {output_handler,
  436. fun(Line, Acc) ->
  437. ?CONSOLE("~s", [Line]),
  438. [Line | Acc]
  439. end};
  440. expand_sh_flag({use_stdout, false}) ->
  441. {output_handler,
  442. fun(Line, Acc) ->
  443. [Line | Acc]
  444. end};
  445. expand_sh_flag({cd, _CdArg} = Cd) ->
  446. {port_settings, Cd};
  447. expand_sh_flag({env, _EnvArg} = Env) ->
  448. {port_settings, Env}.