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