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.

272 line
9.3 KiB

  1. # Plugins #
  2. Rebar3's system is based on the concept of *[providers](https://github.com/tsloughter/providers)*. A provider has three callbacks:
  3. - `init(State) -> {ok, NewState}`, which helps set up the state required, state dependencies, etc.
  4. - `do(State) -> {ok, NewState} | {error, Error}`, which does the actual work.
  5. - `format_error(Error) -> String`, which allows to print errors when they happen, and to filter out sensitive elements from the state.
  6. A provider should also be an OTP Library application, which can be fetched as any other Erlang dependency, except for Rebar3 rather than your own system or application.
  7. This document contains the following elements:
  8. - [Using a Plugin](#using-a-plugin)
  9. - [Reference](#reference)
  10. - [Provider Interface](#provider-interface)
  11. - [List of Possible Dependencies](#list-of-possible-dependencies)
  12. - [Rebar State Manipulation](#rebar-state-manipulation)
  13. - [Tutorial](#tutorial)
  14. ## Using a Plugin ##
  15. To use the a plugin, add it to the rebar.config:
  16. ```erlang
  17. {plugins, [
  18. {plugin_name, ".*", {git, "git@host:user/name-of-plugin.git", {tag, "v1.0.0"}}}
  19. ]}.
  20. ```
  21. Then you can just call it directly:
  22. ```
  23. → rebar3 plugin_name
  24. ===> Fetching plugin_name
  25. ===> Compiling plugin_name
  26. <PLUGIN OUTPUT>
  27. ```
  28. ## Reference ##
  29. TODO
  30. ### Provider Interface ###
  31. TODO
  32. ### List of Possible Dependencies ###
  33. TODO
  34. ### Rebar State Manipulation ###
  35. TODO
  36. ## Tutorial ##
  37. ### First version ###
  38. In this tutorial, we'll show how to start from scratch, and get a basic plugin written. The plugin will be quite simple: it will look for instances of 'TODO:' lines in comments and report them as warnings. The final code for the plugin can be found on [bitbucket](https://bitbucket.org/ferd/rebar3-todo-plugin).
  39. The first step is to create a new OTP Application that will contain the plugin:
  40. → rebar3 new plugin provider_todo desc="example rebar3 plugin"
  41. ...
  42. → cd provider_todo
  43. → git init
  44. Initialized empty Git repository in /Users/ferd/code/self/provider_todo/.git/
  45. Open up the `src/provider_todo.erl` file and make sure you have the following skeleton in place:
  46. ```erlang
  47. -module(provider_todo).
  48. -behaviour(provider).
  49. -export([init/1, do/1, format_error/1]).
  50. -define(PROVIDER, todo).
  51. -define(DEPS, [app_discovery]).
  52. %% ===================================================================
  53. %% Public API
  54. %% ===================================================================
  55. -spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
  56. init(State) ->
  57. Provider = providers:create([
  58. {name, ?PROVIDER}, % The 'user friendly' name of the task
  59. {module, ?MODULE}, % The module implementation of the task
  60. {bare, true}, % The task can be run by the user, always true
  61. {deps, ?DEPS}, % The list of dependencies
  62. {example, "rebar provider_todo"}, % How to use the plugin
  63. {opts, []} % list of options understood by the plugin
  64. {short_desc, "example rebar3 plugin"},
  65. {desc, ""}
  66. ]),
  67. {ok, rebar_state:add_provider(State, Provider)}.
  68. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
  69. do(State) ->
  70. {ok, State}.
  71. -spec format_error(any()) -> iolist().
  72. format_error(Reason) ->
  73. io_lib:format("~p", [Reason]).
  74. ```
  75. This shows all the basic content needed. Note that we leave the `DEPS` macro to the value `app_discovery`, used to mean that the plugin should at least find the project's source code (excluding dependencies).
  76. In this case, we need to change very little in `init/1`. Here's the new provider description:
  77. ```erlang
  78. Provider = providers:create([
  79. {name, ?PROVIDER}, % The 'user friendly' name of the task
  80. {module, ?MODULE}, % The module implementation of the task
  81. {bare, true}, % The task can be run by the user, always true
  82. {deps, ?DEPS}, % The list of dependencies
  83. {example, "rebar todo"}, % How to use the plugin
  84. {opts, []}, % list of options understood by the plugin
  85. {short_desc, "Reports TODOs in source code"},
  86. {desc, "Scans top-level application source and find "
  87. "instances of TODO: in commented out content "
  88. "to report it to the user."}
  89. ]),
  90. ```
  91. Instead, most of the work will need to be done directly in `do/1`. We'll use the `rebar_state` module to fetch all the applications we need. This can be done by calling the `project_apps/1` function, which returns the list of the project's top-level applications.
  92. ```erlang
  93. do(State) ->
  94. lists:foreach(fun check_todo_app/1, rebar_state:project_apps(State)),
  95. {ok, State}.
  96. ```
  97. This, on a high level, means that we'll check each top-level app one at a time (there may often be more than one top-level application when working with releases)
  98. The rest is filler code specific to the plugin, in charge of reading each app path, go read code in there, and find instances of 'TODO:' in comments in the code:
  99. ```erlang
  100. check_todo_app(App) ->
  101. Path = filename:join(rebar_app_info:dir(App),"src"),
  102. Mods = find_source_files(Path),
  103. case lists:foldl(fun check_todo_mod/2, [], Mods) of
  104. [] -> ok;
  105. Instances -> display_todos(rebar_app_info:name(App), Instances)
  106. end.
  107. find_source_files(Path) ->
  108. [filename:join(Path, Mod) || Mod <- filelib:wildcard("*.erl", Path)].
  109. check_todo_mod(ModPath, Matches) ->
  110. {ok, Bin} = file:read_file(ModPath),
  111. case find_todo_lines(Bin) of
  112. [] -> Matches;
  113. Lines -> [{ModPath, Lines} | Matches]
  114. end.
  115. find_todo_lines(File) ->
  116. case re:run(File, "%+.*(TODO:.*)", [{capture, all_but_first, binary}, global, caseless]) of
  117. {match, DeepBins} -> lists:flatten(DeepBins);
  118. nomatch -> []
  119. end.
  120. display_todos(_, []) -> ok;
  121. display_todos(App, FileMatches) ->
  122. io:format("Application ~s~n",[App]),
  123. [begin
  124. io:format("\t~s~n",[Mod]),
  125. [io:format("\t ~s~n",[TODO]) || TODO <- TODOs]
  126. end || {Mod, TODOs} <- FileMatches],
  127. ok.
  128. ```
  129. Just using `io:format/2` to output is going to be fine.
  130. To test the plugin, push it to a source repository somewhere. Pick one of your projects, and add something to the rebar.config:
  131. ```erlang
  132. {plugins, [
  133. {provider_todo, ".*", {git, "git@bitbucket.org:ferd/rebar3-todo-plugin.git", {branch, "master"}}}
  134. ]}.
  135. ```
  136. Then you can just call it directly:
  137. ```
  138. → rebar3 todo
  139. ===> Fetching provider_todo
  140. ===> Compiling provider_todo
  141. Application merklet
  142. /Users/ferd/code/self/merklet/src/merklet.erl
  143. todo: consider endianness for absolute portability
  144. ```
  145. Rebar3 will download and install the plugin, and figure out when to run it. Once compiled, it can be run at any time again.
  146. ### Optionally Search Deps ###
  147. Let's extend things a bit. Maybe from time to time (when cutting a release), we'd like to make sure none of our dependencies contain 'TODO:'s either.
  148. To do this, we'll need to go parse command line arguments a bit, and change our execution model. The `?DEPS` macro will now need to specify that the `todo` provider can only run *after* dependencies have been installed:
  149. ```erlang
  150. -define(DEPS, [install_deps]).
  151. ```
  152. We can add the option to the list we use to configure the provider in `init/1`:
  153. ```erlang
  154. {opts, [ % list of options understood by the plugin
  155. {deps, $d, "deps", undefined, "also run against dependencies"}
  156. ]},
  157. ```
  158. Meaning that deps can be flagged in by using the option `-d` (or `--deps`), and if it's not defined, well, we get the default value `undefined`. The last element of the 4-tuple is documentation for the option.
  159. And then we can implement the switch to figure out what to search:
  160. ```erlang
  161. do(State) ->
  162. Apps = case discovery_type(State) of
  163. project -> rebar_state:project_apps(State);
  164. deps -> rebar_state:project_apps(State) ++ rebar_state:all_deps(State)
  165. end,
  166. lists:foreach(fun check_todo_app/1, Apps),
  167. {ok, State}.
  168. [...]
  169. discovery_type(State) ->
  170. {Args, _} = rebar_state:command_parsed_args(State),
  171. case proplists:get_value(deps, Args) of
  172. undefined -> project;
  173. _ -> deps
  174. end.
  175. ```
  176. The `deps` option is found using `rebar_state:command_parsed_args(State)`, which will return a proplist of terms on the command-line after 'todo', and will take care of validating whether the flags are accepted or not. The rest can remain the same.
  177. Push the new code for the plugin, and try it again on a project with dependencies:
  178. ```
  179. → rebar3 todo --deps
  180. ===> Fetching provider_todo
  181. ===> Compiling provider_todo
  182. ===> Fetching bootstrap
  183. ===> Fetching file_monitor
  184. ===> Fetching recon
  185. [...]
  186. Application dirmon
  187. /Users/ferd/code/self/figsync/apps/dirmon/src/dirmon_tracker.erl
  188. TODO: Peeranha should expose the UUID from a node.
  189. Application meck
  190. /Users/ferd/code/self/figsync/_deps/meck/src/meck_proc.erl
  191. TODO: What to do here?
  192. TODO: What to do here?
  193. ```
  194. Rebar3 will now go pick dependencies before running the plugin on there.
  195. you can also see that the help will be completed for you:
  196. ```
  197. → rebar3 help todo
  198. Scans top-level application source and find instances of TODO: in commented out content to report it to the user.
  199. Usage: rebar todo [-d]
  200. -d, --deps also run against dependencies
  201. ```
  202. That's it, the todo plugin is now complete! It's ready to ship and be included in other repositories.