Custom Dep Resources

Resources Compatible with versions 3.7.0 and Above

Starting with version 3.7.0, Rebar3 published a new API for custom resources, which gives access to the project’s local configuration to enable more powerful custom dependency formats. They can use contextual information from the current build to customize how dependencies might be fetched.

🚧The new Interface is not backwards compatible

This new interface is unknown and unsupported in versions prior to 3.7.0. If you are writing libraries that should work with all rebar3 copies, skip to the next section, where resources compatible with all rebar3 versions are documented. Old interfaces however are still compatible with all versions and no support for existing project has been broken in adding the new API.

The new callback API is defined as follows:

  1. %% Type declarations
  2. -type resource() :: #resource{}. % an opaque record generated by an API call described below
  3. -type source() :: {type(), location(), ref()} | {type(), location(), ref(), binary()}.
  4. -type type() :: atom().
  5. -type location() :: string().
  6. -type ref() :: any().
  7. -type resource_state() :: term().
  8. %% and the following callbacks
  9. -callback init(type(), rebar_state:t()) -> {ok, resource()}.
  10. -callback lock(rebar_app_info:t(), resource_state()) -> source().
  11. -callback download(file:filename_all(), rebar_app_info:t(), resource_state(), rebar_state:t()) ->
  12. ok | {error, any()}.
  13. -callback needs_update(rebar_app_info:t(), resource_state()) -> boolean().
  14. -callback make_vsn(rebar_app_info:t(), resource_state()) ->
  15. {plain, string()} | {error, string()}.

The callbacks allow the resource plugin to have access to the rebar_state:t() data structure, which lets you access and manipulate the rebar3 state, find application state, and generally work with the rebar_state, rebar_app_info, rebar_dir, and the new rebar_paths modules.

An example of a plugin making use of this functionality is rebar3_path_deps. Rebar3’s own hex package resource uses this API.

A resource plugin is initialized the same way as any other plugin would:

  1. -module(my_rebar_plugin).
  2. -export([init/1]).
  3. -spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
  4. init(State) ->
  5. {ok, rebar_state:add_resource(State, {Tag, Module})}.

Where Tag stands for the type in the deps configuration value (git, hg, etc.), and Module is the callback module.

The callback module may look as follows:

  1. -module(my_rebar_plugin_resource).
  2. -export([init/2,
  3. lock/2,
  4. download/4, download/3,
  5. needs_update/2,
  6. make_vsn/1]).
  7. %% Initialize the custom dep resource plugin
  8. init(Type, _RebarState) ->
  9. CustomState = #{},
  10. Resource = rebar_resource_v2:new(
  11. Type, % type tag such as 'git' or 'hg'
  12. ?MODULE, % this callback module
  13. CustomState % anything you want to carry around for next calls
  14. ),
  15. {ok, Resource}.
  16. lock(AppInfo, CustomState) ->
  17. %% Extract info such as {Type, ResourcePath, ...} as declared
  18. %% in rebar.config
  19. SourceTuple = rebar_app_info:source(AppInfo)),
  20. %% Annotate and modify the source tuple to make it absolutely
  21. %% and indeniably unambiguous (for example, with git this means
  22. %% transforming a branch name into an immutable ref)
  23. ...
  24. %% Return the unambiguous source tuple
  25. ModifiedSource.
  26. download(TmpDir, AppInfo, CustomState, RebarState) ->
  27. %% Extract info such as {Type, ResourcePath, ...} as declared
  28. %% in rebar.config
  29. SourceTuple = rebar_app_info:source(AppInfo)),
  30. %% Download the resource defined by SourceTuple, which should be
  31. %% an OTP application or library, into TmpDir
  32. ...
  33. ok.
  34. make_vsn(Dir, ResourceState) ->
  35. %% Extract a version number from the application. This is useful
  36. %% when defining the version in the .app.src file as `{version, Type}',
  37. %% which means it should be derived from the build information. For
  38. %% the `git' resource, this means looking for the last tag and adding
  39. %% commit-specific information
  40. ...
  41. {plain, "0.1.2"}.
  42. needs_update(AppInfo, ResourceState) ->
  43. %% Extract the Source tuple if needed
  44. SourceTuple = rebar_app_info:source(AppInfo),
  45. %% Base version in the current file
  46. OriginalVsn = rebar_app_info:original_vsn(AppInfo)
  47. %% Check if the copy in the current install matches
  48. %% the defined value in the source tuple. On a conflict,
  49. %% return `true', otherwise `false'
  50. ...,
  51. Bool.

Resources Compatible with all versions

Prior to version 3.7.0, the dependency resource framework was a bit more restricted. It had to essentially work context-free, with only the deps information from the rebar.config and rebar.lock to work from. It was not possible to have any information relative to the project configuration, which essentially restricted what could be done by each resource.

These custom resources are still supported in Rebar3 versions higher than 3.7.0, and so if you have users running older build, we recommend that you develop this kind of resources only.

Each dependency resource must implement the rebar_resource behaviour.

  1. -module(rebar_resource).
  2. -export_type([resource/0
  3. ,type/0
  4. ,location/0
  5. ,ref/0]).
  6. -type resource() :: {type(), location(), ref()}.
  7. -type type() :: atom().
  8. -type location() :: string().
  9. -type ref() :: any().
  10. -callback lock(file:filename_all(), tuple()) ->
  11. rebar_resource:resource().
  12. -callback download(file:filename_all(), tuple(), rebar_state:t()) ->
  13. {tarball, file:filename_all()} | {ok, any()} | {error, any()}.
  14. -callback needs_update(file:filename_all(), tuple()) ->
  15. boolean().
  16. -callback make_vsn(file:filename_all()) ->
  17. {plain, string()} | {error, string()}.

Included with rebar3 are rebar_git_resource, rebar_hg_resource and rebar_pkg_resource.

A custom resource can be included the same way as a plugin. An example of this can be seen in Kelly McLaughlin’s rebar3_tidy_deps resource:

  1. -module(rebar_tidy_deps).
  2. -export([init/1]).
  3. -spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
  4. init(State) ->
  5. {ok, rebar_state:add_resource(State, {github, rebar_github_resource})}.

This resource rebar_github_resource which implements the rebar3 resource behaviour is added to the list of resources available in rebar_state. Adding the repo as a plugin to rebar.config allows this resource to be used:

  1. {mydep, {github, "kellymclauglin/mydep.git", {tag, "1.0.1"}}}.
  2. {plugins, [
  3. {rebar_tidy_deps, ".*", {git, "https://github.com/kellymclaughlin/rebar3-tidy-deps-plugin.git", {tag, "0.0.2"}}}
  4. ]}.

Writing a Plugin working with both versions

If you want to write a custom resource plugin that works with both versions, you can dynamically detect arguments to provide backwards-compatible functionality. In the example below, the new API disregards all new information and just plugs itself back in the old API:

  1. -module(my_rebar_plugin_resource).
  2. -export([init/2,
  3. lock/2,
  4. download/4, download/3,
  5. needs_update/2,
  6. make_vsn/1]).
  7. init(Type, _RebarState) ->
  8. CustomState = #{},
  9. Resource = rebar_resource_v2:new(Type, ?MODULE, CustomState),
  10. {ok, Resource}.
  11. %% Old API
  12. lock(Dir, Source) when is_tuple(Source) ->
  13. lock_(Dir, Source);
  14. %% New API
  15. lock(AppInfo, _ResourceState) ->
  16. %% extract info for dir extract info for source
  17. lock_(rebar_app_info:dir(AppInfo), rebar_app_info:source(AppInfo)).
  18. %% Function handling normalized case
  19. lock_(Dir, Path) ->
  20. ...
  21. %% Old Version
  22. download(TmpDir, SourceTuple, RebarState) ->
  23. download_(TmpDir, SourceTuple, State).
  24. %% New Version
  25. download(TmpDir, AppInfo, _ResourceState, RebarState) ->
  26. %% extract source tuple
  27. download_(TmpDir, rebar_app_info:source(AppInfo), RebarState).
  28. %% Function handling normalized case
  29. download_(TmpDir, {MyTag, ...}, _State) ->
  30. ...
  31. %% Old version
  32. make_vsn(Dir) ->
  33. ...
  34. %% New version
  35. make_vsn(Dir, _ResourceState) ->
  36. make_vsn(Dir).
  37. %% Old Version
  38. needs_update(Dir, {MyTag, Path, _}) ->
  39. needs_update_(Dir, {MyTag, Path});
  40. %% New Version
  41. needs_update(AppInfo, _) ->
  42. needs_update_(rebar_app_info:dir(AppInfo), rebar_app_info:source(AppInfo)).
  43. %% Function handling normalized case
  44. needs_update_(Dir, {Tag, Path}) ->
  45. ...

Note that if you resource really needs the new API to work, backwards compatibility will be difficult to achieve since whenever it will be called, it won’t have all the information of the new API.

This approach is mostly useful when you can provide an acceptable (even if degraded) user experience with the old API.