Part 3 - Packaging Plugins
In parts one and two we introduced rebar plugins and looked at the basic anatomy of a plugin module. In this article we’ll be looking at how you can package plugins as external dependencies along with additional dependencies that your plugin requires. We’ll also be looking at the pre and post command hooks that plugins can utilise in order to run before (or after) a given command.
The accompanying source code can be found in the plugin-packaging branch of the main git repository. Some additional libraries are also used and links to the sources for these are provided both in the text, and in the final links section.
Packaging Requirements
If we wish to re-use a plugin across multiple projects, it makes sense to package the plugin as an independent component rather than duplicate the source code in many places. Once the plugin is packaged up and available for use across projects, users can take advantage of it by either
- installing the plugin globally (e.g., into
ERL_LIBS
) - installing the plugin as part of a standard
rebar get-deps compile
build process
We won’t be looking at the approach for (1) until later in the series, so our attention will be turned to packaging plugins in a manner that allows them to be included in a projects as one of it’s dependencies.
To demonstrate how this works we will develop two small projects, one which implements a simple build plugin, and another which uses the plugin as part of its build process. Along the way, we’ll meet a few more utility functions that can help with authoring plugins.
Introducing the bad_deps_plugin
We’re going to implement a plugin that allows us to fetch an Erlang library from github so we can use it as a dependency. Of course, rebar already allows us to do this, but there are some limitations that it places on potential dependencies - they must be packaged as an OTP application or library. Now the common approach to dealing with projects which are not packaged properly (from rebar’s point of view) in their source repository, is to fork the offending repo and fix the project structure so that rebar can handle it. This creates a bit of a maintenance headache for the person maintaining the fork, so we’re going to handle the fixing with a simple plugin.
CAVEAT!
This plugin is being developed for illustrative purposes only - we’ve chosen this particular area because it represents an interesting problem to solve using plugins. There are numerous and better ways to deal with non-standard dependencies, which we’ll discuss at the end of the article.
The badly behaving project we’re going to fix is
Kyle Kingsbury’s Implementation of a Skewed Binary Heap.
The git repository has all the Erlang sources (both the implementation and the
test code) in the top level directory, so we’ll be fetching the sources and
then rebuilding the required file and directory structure on the fly. Because
rebar is opinionated about project structure - and this is something to be
commended - we cannot use the get-deps
command to fetch the repository,
because rebar will fail after fetching the dependency because there is no
.app or .app.src file present.
We could bemoan the fact that the fetching and installation of dependencies
is coupled in this way, but instead we’ll stick to problem solving and use
rebar’s rebar_utils:sh
function to run the git clone
command by hand.
We’re not using rebar’s standard deps mechanism to get hold of our dependency, yet we’ll have to store the git repository information somewhere in our build configuration. It’s time to introduce the rebar configuration API.
Handling Custom Configuration
In order to make sure that the dependency is still required by rebar, even
though it isn’t being fetched in the usual manner, we will include an entry in
the deps
configuration section but exclude the source element of the config
tuple. Doing this makes rebar aware that the stated dependency is required, but
excludes the possibility of fetching it remotely. We will also introduce a new
configuration section, named bad_deps
in which to store the git url we need
in order to fetch the repository ourselves.
Let’s take a look at the client’s rebar config file.
1 2 3 4 5 6 7 8 |
|
Configuration data is made available to plugins in the first argument to the
command(s) they are exporting. The configuration tuples are held in an opaque
data structure, so all access should be done using the rebar_config
module.
Here’s our first stab at getting the configuration data out:
1 2 3 4 5 6 |
|
Actually running this without trying to use it in a sample project is a bit of a pain, not least because we need rebar to create the initial config for us. I often skip trying to unit test plugins, not because it is impossible - there are certainly ways to do it using eunit and/or common_test - but because in general I find it easier to write a sample project to demonstrate the plugin’s use, and then use the Rebar ReTest Plugin to do CLI-centric integration testing. There will be a full set of articles on testing plugins at a later date.
For now, we’ll just put a demo config file locally in the plugin project so that we can experiment:
1 2 3 4 5 6 |
|
And now on the command line, we can take a look….
t4@malachi:rebar-plugin-tutorial $ rebar -C demo check-config -v
DEBUG: Rebar location: "/Users/t4/bin/rebar"
DEBUG: Load global config file "/Users/t4/.rebar/config"
DEBUG: Consult config file "/Users/t4/.rebar/config"
DEBUG: Consult config file "/Users/t4/work/hyperthunk/rebar-plugin-tutorial/demo"
DEBUG: Entering /Users/t4/work/hyperthunk/rebar-plugin-tutorial
DEBUG: Available deps: []
DEBUG: Missing deps : []
INFO: Loading plugin bad_deps_plugin from src/bad_deps_plugin.erl
WARN: Missing plugins: [rebar_skip_deps]
DEBUG: Predirs: []
==> rebar-plugin-tutorial (check-config)
INFO: config = [{skewbinheap,"https://github.com/aphyr/skewbinheap.git"}]
DEBUG: Postdirs: []
t4@malachi:rebar-plugin-tutorial $
It probably didn’t go unnoticed that the config handling function we called in
our plugin’s check-config
command is called get_local - it is well worth
understanding this early on. There are two kinds of configuration that rebar
works with
- Global configuration - stored in the rebar application environment settings
- Local configuration - read from a
.config
file and passed about explicitly
Of the two, local configuration data is itself handled in two disparate ways.
As we mentioned in Part 1, each time a new directory is processed, a
new configuration set is created - either by reading the local rebar.config
file or creating an empty one. It is this local to the directory
configuration which we’re reading when we call rebar_config:get_local/3
. The
other configuration reading functions get/3
and get_list/3
do the same
thing: they search the entire config set (i.e., including any ancestors) and
return the first entry with the given key.
We must always bare in mind that it is up to the consumer of the configuration API to decide how they want to deal with the given config. They may choose one of the following functions to look up a given key
get_local
- reads the config for the current directory, or a default valueget
/get_list
- returns the first config section matching the keyget_all
- gets all (explicitly stated) values for the whole config set
Just to make sure this point is clear, we’ll look at an example. We will assume that we have both parent and child directories with the following (respective) configurations:
1 2 3 4 5 6 7 |
|
Imagine that our check-config
command was globally available to rebar and
takes the required key as key=<name>
on the command line:
t4@malachi:parent $ rebar check-config key=foo
==> child (check-config)
config key 'foo' found (get_local): "baz"
config key 'foo' found (get): "baz"
config key 'foo' found (get_all): ["baz","bar"]
==> parent (check-config)
config key 'foo' found (get_local): "bar"
config key 'foo' found (get): "bar"
config key 'foo' found (get_all): ["bar"]
t4@malachi:parent $ rebar check-config key=squish
==> child (check-config)
config key 'squish' not found (get_local)
config key 'squish' found (get): "thingumy"
config key 'squish' found (get_all): ["thingumy"]
==> parent (check-config)
config key 'squish' found (get_local): "thingumy"
config key 'squish' found (get): "thingumy"
config key 'squish' found (get_all): ["thingumy"]
t4@malachi:parent $
So as we can see, the consumer has a lot of control over the scope at which
configuration sections should be read from. For the key foo
, defined in both
the parent and child directories, we see that both get_local
and get
will
stop searching once the key is found and therefore terminate with the config
section defined by the child - in this manner, the rebar.config
in the child
sub-directory is overriding the value of foo
defined in the parent config.
For the squish
key however, we see that get_local
fails to locate any
value for the key, whilst get continues it’s search into the parent config and
locates the right value.
Using Command Hooks
We want our plugin to do its work before rebar’s internal dependency handling
mechanism starts working. As a result, we need to hook into get-deps
, and for
this we will need a pre-hook. These are nothing more than exported functions
which have the prefix pre_
in front of the command name - in our case making
for the rather odd looking export pre_get-deps
.
The fetching code is fairly simple, calculating the correct target directory and delegating to the shell to do the work.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Now in the sample project that is using this plugin, we need to fetch it from
github. Because one of our dependencies - that is, the skewbinheap library -
isn’t compatible with rebar, if we put both the plugin and the broken dep
into our config and run rebar get-deps compile
, we do see the plugin get
fetched and built, but we end up with a non-zero exit code. Personally I see
that as an indication that the build has failed, rather than a normal step
of the build process. I think this is especially important in this day and age
where tools like CI servers will report a broken build and of course you can’t
pipe failing shell commands or join them with &&
and the like.
Instead, we’ll configure the plugin as a dependency by itself in a separate
config file, and fetch and build it by itself first. After the plugin has been
successfully built, we can use it to bootstrap skewbinheap
properly for us.
By convention, I like to name rebar config files that are used for bootstrap or
initialisation by their task.
1 2 3 4 5 6 |
|
t4@malachi:rebar-plugin-tutorial $ rebar -C init.config get-deps compile
==> rebar-plugin-tutorial (get-deps)
Pulling bad_deps_plugin from {git,"git://github.com/hyperthunk/rebar-plugin-tutorial.git",
"part3-plugin"}
Initialized empty Git repository in /Users/t4/work/hyperthunk/rebar-plugin-tutorial/deps/bad_deps_plugin/.git/
Branch part3-plugin set up to track remote branch part3-plugin from origin.
==> bad_deps_plugin (get-deps)
==> bad_deps_plugin (compile)
Compiled src/bad_deps_plugin.erl
==> rebar-plugin-tutorial (compile)
t4@malachi:rebar-plugin-tutorial $
Now that the dependency is installed, we can do a test run to see if our git
command is working as expected. First the main rebar.config
needs updating,
because it needs to know about the plugin dependency in order for the plugin
to get added to the code path before the get-deps
command is executed.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Once we run get-deps
we can see clearly that our git clone
command has
indeed worked, because the directory is present. What’s interesting to note
here is that get-deps
will not return a non-zero exit status even when the
skewbinheap
dependency isn’t structured properly as an OTP application. The
compile
command however, will fail in this manner.
t4@malachi:rebar-plugin-tutorial $ rebar get-deps
==> bad_deps_plugin (pre_get-deps)
==> bad_deps_plugin (get-deps)
==> rebar-plugin-tutorial (pre_get-deps)
Initialized empty Git repository in /Users/t4/work/hyperthunk/rebar-plugin-tutorial/deps/skewbinheap/.git/
==> rebar-plugin-tutorial (get-deps)
t4@malachi:rebar-plugin-tutorial $ ls deps/
bad_deps_plugin skewbinheap
t4@malachi:rebar-plugin-tutorial $ echo $?
0
t4@malachi:rebar-plugin-tutorial $ rebar compile
==> bad_deps_plugin (compile)
==> rebar-plugin-tutorial (compile)
Dependency not available: skewbinheap-.* (undefined)
t4@malachi:rebar-plugin-tutorial $ echo $?
1
t4@malachi:rebar-plugin-tutorial $
Now the other thing to note is that the pre_get-deps
command is running for
each of the dependencies as well as the top level project. We’ve come across
this aspect of rebars build lifecycle before (in Part 2) and we can
either make the user skip_deps
or we can handle the edge case in the plugin.
There is an argument for letting a dependency handling command/plugin
continue to process directories recursively, but in practise we’ll see that
this doesn’t work without significant coding on the part of the plugin author.
For now, we’re just going to exclude anything outside of base_dir
as we did
in Part 2. To avoid writing all the tedious case is_base_dir()
boilerplate everywhere, we’re going to utilise an annotation
that I’ve written in to a
little library,
which will do the work for us. This also gives us an opportunity to look at
how plugins with their own dependencies need to be handled when they’re
included in a project.
Back in the part3-plugin branch, we’ll switch over to use the annotation. First the rebar build config needs tweaking so that we get hold of the two libraries we’re going to be using (there are links to both of them at the bottom of the page).
1 2 3 4 5 6 7 8 9 10 11 12 |
|
And now we can simply use the -base_dir
annotation anywhere in our code.
We’re going to apply it to the pre_get-deps/2
function to ensure that it does
not run for sub_dirs or dependencies.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
In order to compile the plugin with annotations support enabled,
we need to update the init.config
to meet the following requirements:
- The libraries used during the parse transform phase need to be on the code path
- The rebar annotations plugin needs to be somewhere rebar can find it
In order to locate the rebar annotations plugin at runtime, we
bootstrap the plugin_dir
to point to deps/annotations/src
, which works
nicely when you’ve got a single plugin build.
1 2 3 4 5 6 7 8 9 10 11 |
|
Notice that we’re configuring lib_dirs
to point to the deps
directory here.
This is required so that the OTP compile
module can decode the include_lib
references. Now that we’ve dealt with getting the lib_dirs
on the code path,
we are in a position where the plugin will kick in and do its work in
base_dir
only. Our build process is now two-phase, but in a later article,
we will look at how we can combine these phases into a single command.
t4@malachi:rebar-plugin-tutorial $ rebar -C init.config get-deps compile
t4@malachi:rebar-plugin-tutorial $ rebar get-deps compile
Fixing the directory structure
Now it’s time to move the files around. We’re going to provide some additional
configuration in the bad_deps
section, to specify which files are considered
production code and which are test. There is another useful method in
rebar_utils
which finds files recursively based on a regular expression and
we’ll delegate to this for moving stuff around. Let’s take a look at the new
bad_deps
configuration section then.
We won’t worry about handling .yrl
or .mib
files (or any other custom file
formats such as erlydtl templates) as our target library doesn’t have any.
1 2 3 4 5 6 |
|
The code to handle these regular expressions is fairly noddy, simply delegating
to rebar_utils
to do most of the real work. We also need to generate an
.app.src
file in the right place, so that rebar recognises the project as an
OTP library.
The full plugin code is listed below. As well as the find_files
function,
we’re also using another rebar_file_utils
function to shuffle the sources
into the right place.
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 |
|
Most of the (verbose) output has been snipped from the shell session listed below, but the relevant bits are there and the plugin is clearly doing its job properly.
t4@malachi:rebar-plugin-tutorial $ rebar get-deps -v
# <<<<<<<<<<<<<< LOTS OF OUTPUT SNIPPED! >>>>>>>>>>>>>>
==> rebar-plugin-tutorial (get-deps)
DEBUG: is_app_available, looking for App annotations with Path "/Users/t4/work/hyperthunk/rebar-plugin-tutorial/deps/annotations"
DEBUG: vcs_vsn: Unknown VCS atom in vsn field: "0.0.1"
INFO: Looking for annotations-.* ; found annotations-0.0.1 at /Users/t4/work/hyperthunk/rebar-plugin-tutorial/deps/annotations
DEBUG: is_app_available, looking for App rebar_annotations with Path "/Users/t4/work/hyperthunk/rebar-plugin-tutorial/deps/rebar_annotations"
DEBUG: vcs_vsn: Unknown VCS atom in vsn field: "1"
INFO: Looking for rebar_annotations-.* ; found rebar_annotations-1 at /Users/t4/work/hyperthunk/rebar-plugin-tutorial/deps/rebar_annotations
DEBUG: is_app_available, looking for App bad_deps_plugin with Path "/Users/t4/work/hyperthunk/rebar-plugin-tutorial/deps/bad_deps_plugin"
DEBUG: vcs_vsn: Unknown VCS atom in vsn field: "1"
INFO: Looking for bad_deps_plugin-.* ; found bad_deps_plugin-1 at /Users/t4/work/hyperthunk/rebar-plugin-tutorial/deps/bad_deps_plugin
DEBUG: is_app_available, looking for App skewbinheap with Path "/Users/t4/work/hyperthunk/rebar-plugin-tutorial/deps/skewbinheap"
DEBUG: vcs_vsn: Unknown VCS atom in vsn field: "1"
INFO: Looking for skewbinheap-.* ; found skewbinheap-1 at /Users/t4/work/hyperthunk/rebar-plugin-tutorial/deps/skewbinheap
DEBUG: Postdirs: []
t4@malachi:rebar-plugin-tutorial $ ls -la deps/skewbinheap/
total 24
drwxr-xr-x 7 t4 staff 238 6 Jan 11:42 .
drwxr-xr-x 8 t4 staff 272 6 Jan 11:42 ..
drwxr-xr-x 13 t4 staff 442 6 Jan 11:42 .git
-rw-r--r-- 1 t4 staff 58 6 Jan 11:42 .gitignore
-rw-r--r-- 1 t4 staff 737 6 Jan 11:42 README.markdown
drwxr-xr-x 4 t4 staff 136 6 Jan 11:42 src
-rw-r--r-- 1 t4 staff 1602 6 Jan 11:42 test
t4@malachi:rebar-plugin-tutorial $ ls -la deps/skewbinheap/src/
total 16
drwxr-xr-x 4 t4 staff 136 6 Jan 11:42 .
drwxr-xr-x 7 t4 staff 238 6 Jan 11:42 ..
-rw-r--r-- 1 t4 staff 101 6 Jan 11:42 skewbinheap.app.src
-rw-r--r-- 1 t4 staff 3781 6 Jan 11:42 skewbinheap.erl
t4@malachi:rebar-plugin-tutorial $
Conclusion
We’ve developed a useful little plugin here. In general, forking repositories is made very easy by modern dvcs tools, so the fork/clone-and-maintain approach is probably a better way to deal with aberrant project structures like this one.
A more general problem occurs when building foreign dependencies, some of which may not be compatible with rebar, may use another build system (such as make) or may even not contain Erlang sources at all! When dealing with these situations, I would recommend looking at the rebar_alien_plugin, which attempts to provide a framework for handling them. We will introduce that plugin in detail later on in the series, as it is effectively a configuration driven plugin compiler.
Next time…
We’ll continue next time by looking at the preprocess
and postprocess
hooks
and will introduce a new (and useful) working plugin. We will also introduce
the Rebar Plugin Manager project, which provides hooks to compensate for many
of the complexities we’ve encountered with getting the code path set up
correctly so that plugins are available.
Links
As promised, all the relevant links for this article are listed below.