1.1 Concepts

1.1.1 Project

ANVL project is a directory containing anvl.erl file, also known as the project configuration file.

anvl.erl is a regular Erlang module, with one notable exception: it must NOT contain -module attribute (and if such exists, it is ignored). That is because ANVL derives module name for the project configuration file automatically. Because of that, it’s not allowed to use anvl:Fun(...) or fun anvl:Fun/N syntax to refer to local functions. If needed, fun ?MODULE:Fun/N syntax can be used instead. Other than that, anvl.erl is compiled and loaded as a normal Erlang module.

Project configuration modules implement anvl_project behavior.

Project referred to by -d CLI argument is called the root project. It can refer to other projects, known as child projects.

1.1.2 Project Configuration

conf/0 callback of the project module returns project configuration tree.

For example:

conf() ->
  #{ plugins => [anvl_erlc]
   , erlang =>
       #{ app_paths => ["."]
        , includes => ["include", "src"]
        , ...
        }
   , [erlang, deps] => []
   , ...
   }.

Note: notation #{[foo, bar, ...] => quux} is a shortcut for #{foo => #{bar => #{... => quux ...}}}.

Groups of values can be repeated in the configuration. In this case each configuration subtree appears as an element of a list:

#{erlang =>
    #{ deps =>
         %% `erlang.deps` is a map. Children:
         [ #{ app => typerefl
            , at => "vendor/typerefl"
            }
         , #{ app => lee
            , at => "vendor/lee"
            }
         , ...
         ]
     }}

1.1.3 Project Configuration Override

conf_override/1 callback of the root project module allows to override configuration of the child projects.

The return value is a configuration patch: a list of operations setting or un-setting keys in the configuration:

config_override(ChildProject) ->
  case filename:basename(ChildProject) of
    "some_project" ->
      Key1 = [erlang, bdeps],
      Val1 = [some_app],
      Key2 = [erlang, escript, {some_escript}],
      [ {set, Key1, Val1}
      , {rm, Key2}
      , ...
      ];
    _ ->
      []
  end.

1.1.4 Plugin

A plugin is an Erlang application containing a module that implements anvl_plugin behavior.

ANVL comes with a number of built-in plugins, sufficient for bootstrapping other plugins:

ANVL core application provides the functionality that allows projects to load the plugins on demand, deals with configuration, CLI interface, documentation, etc. It also contains a number of utility functions to aid plugin development.

Plugin configuration is split into two parts:

Tool configuration that is controlled by the user running anvl command, and can be changed using CLI arguments and environment variables. Tool configuration is global.

Project configuration that is statically set by the project. ANVL keeps such configurations separate for each project.

1.1.5 Conditions and preconditions

Condition is the basic building block of ANVL plugins and projects. In a very abstract sense, ANVL condition is a specially implemented Erlang function that ensures that state of the system satisfies certain criteria, for example:

Conditions have the following properties:

  1. Condition function returns a boolean, indicating whether or not it produced side effects that changed the system. It returns false if the system state already satisfies the expectations, and no changes were made. Otherwise, it can run a subroutine that transfers the system into the expected state. When it succeeds, boolean true is returned, indicating presence of side effects.
  2. Condition throws an exception when the system can’t be transferred to the desired state for any reason.
  3. Conditions can depend on each other. Such dependencies are called preconditions.
  4. Conditions are memoized: ANVL guarantees that body of the condition is executed at most once for each unique set of arguments.

Satisfying the condition means checking whether the property already holds or running the subroutine transferring it to the desired state otherwise.

1.1.6 Example: building C code

Let us demonstrate that this rather abstract approach is sufficient to replace a traditional build system. In this example we’ll define an ANVL project compiling C code into an executable.

First, let’s declare a condition source_compiled, that is satisfied when the object file exists and is newer than the source file or after compiling the source file with GCC.

-include("anvl.hrl").

?MEMO(source_compiled, Src, Obj,
      begin
        newer(Src, Obj) andalso
          anvl_lib:exec("gcc", ["-c", "-o", Obj, Src])
      end).

This first example condition demonstrates a few things:

First, conditions are defined using MEMO macro. Its syntax is the following: ?MEMO(FunctionName, Arg1, Arg2, ..., BODY).

Second, ANVL framework don’t assume anything about the nature of conditions, it only concerns with the presence of side effects. Here, newer is a library function that compares modification time of the files (similar to make), but the user is free to implement any change detection, for example using hash comparison.

Next, let’s define a condition that ensures that the target executable is built and up-to-date:

?MEMO(executable_built,
      begin
        Executable = "build/hello",
        Sources = filelib:wildcard("c_src/*.c"),
        Objs = lists:map(fun obj_name/1, Sources),
        precondition([source_compiled(Src, obj_name(Src)) || Src <- Sources]) or
          newer(Objs, Executable) andalso
          anvl_lib:exec("gcc", ["-o", Executable | Objs])
      end).

obj_name(Src) ->
  patsubst("build/${basename}.o", Src).

Finally, setting conditions project configuration will let ANVL framework know that executable_built/0 function is a condition that can be invoked by the user:

conf() ->
  #{ conditions => [executable_built]
   }.

Now running anvl executable_built command in the project directory will satisfy the condition with the same name. (Running anvl without arguments will satisfy the first condition in the list.)

While this may look verbose at first, it opens up some possibilities. Using a full-fledged programming language (Erlang) to define conditions gives access to proper data structures and algorithms, and allows to encapsulate build rules into reusable modules with well-defined interfaces.

1.1.7 ANVL versus traditional build systems

A traditional build system is a program that automatically solves the following problems:

Typical build systems solve this problem using concepts of source, target and rule. Source and target are (usually) file names, and rule is a subroutine that takes the source files and produces the target file(s). Most build systems have a standard way of checking whether the target has to be rebuilt, based on comparison of file modification times or hashes. Dependencies between the targets are encoded in the build recipe as a graph. This way of doing things works very well as long as build system only works with files, but falls short when it’s no longer the case.

ANVL’s conditions, on the opposite, are not tied to the file system or anything concrete.

Additionally, traditional build systems usually mix several concepts together:

In contrast, ANVL presents these as independent library primitives.


JavaScript license information