moo 無 build
Integrating moo with build systems

Table of Contents

Introduction

One of the primary use for moo is to generate code. So, naturally it must integrate into code build systems and this note collects methods to do just that.

moo can also be used in generating a build system. See for example the wcup document which describes the use of moo to generate a software package skeleton including its build system.

Manual codegen

A reasonably simple way to "integrate" moo into a build system is to not. One may run moo manually, or partly automated via a shell script, and arrange for the generated files to be placed in the source areas and committed. Absent any apparent knowledge of moo, the software project will simply appear to have an impeccably consistent code developer.

Pros:

  • requires moo only at develop time, not build (nor run) time.
  • easy to implement
  • (manually) assures correct build dependencies
  • generated code can be placed alongside hand-made code which assists in browsing/understanding of the code base
  • building the project is decoupled from development occurring in moo itself or in templates provided by moo that are used by the project

Cons:

  • a developer can change a local template or model and forget to regenerate (though presumably, this is caught as a matter-of-course).

Build integration

Another reasonable solution is to run moo commands as part of the native build of the package. From the point of view of developing a build system, moo is seen as a compiler or possibly more accurately a pre processor.

While g++ may convert C++ files to .o object files, moo converts .j2 and .jsonnet files into .hpp, .cpp, etc files (which then themselves must go on to be further compiled. Thus, moo commands must be inserted into the build graph that the build system (Waf, CMake, etc) creates in a more novel manner than merely adding more source files.

The "pros" and "cons" are largely reversed from the manual option with the following additional "pros":

  • correct build dependencies may be assured, given a quality build system
  • generated code may be placed in or outside the source area

The rest of this document gives information to help build system developers understand how to fit moo into their system. It focuses on what input and output files with which moo may interact as most build systems use a file-based dependency graph. It ends with some Examples of integrating moo into a few of the more popular build systems (which are also understood by the author).

Inputs

moo takes input from files in a number of different ways:

models
these files may be provided in many different formats. The format used for moo examples and which is recommended is .jsonnet. But moo also supports the consumption of .json, .xml, .ini, .yaml, .csv and others. Models are provided to moo via a command line positional argument.
templates
these files conventionally have a "double extension" such as .hpp.j2. The first (.hpp) signifies the target file extension and the second (.j2) identifies the file as being marked up with Jinja2 templates. The first part is free, while some moo functionality is sensitive to the trailing .j2. Templates are provided to moo via a command line positional argument.
graft
data may be added to the model data structure by specifying its location via JSON Pointer syntax and a file that provides the value. The file will be evaluated based on its file extension and then "grafted" into the location. Grafts are performed with the moo -g <ptr>:<file> option.
TLAs
when a model in a .jsonnet file evaluates to a function, the function arguments aka "top-level arguments" (TLAs) may be provided with the moo -A var=<value> command line argument. The <value> may be code in Jsonnet syntax or a file name. If the latter, the file will be loaded (as a model is) and its resulting data will provide the <value>.

moo can be given these files via an absolute or relative path. If a relative path is given then moo will check for the file in list of given absolute "search paths". Template and model search paths are specified independently via command line arguments:

$ moo -M /path/to/models -M /path/to/more/models \
      -T /path/to/templates -T /path/to/more/templates \
      render a/relative/model.jsonnet /an/absolute/template.xyz.j2

When using the moo CLI, one may also set these search paths by setting MOO_LOAD_PATH and MOO_TEMPLATE_PATH environment variables.

$ export MOO_LOAD_PATH=/path/to/models:/path/to/more/models
$ export MOO_TEMPLATE_PATH=/path/to/templates:/path/to/more/templates
$ moo render a/relative/model.jsonnet /an/absolute/template.xyz.j2

Besides explicit files provided as moo command arguments (as above example) moo may also receive inputs via two other channels:

For example, let's prepare three models where a.jsonnet is a function and the other two are simple objects.

echo 'function(a=42) {x:a}' > buildsys/a.jsonnet
echo '{b:69}' > buildsys/b.jsonnet
echo '{c:"hello"}' > buildsys/c.jsonnet

Here is an example of grafting:

moo -g /greetings:buildsys/c.jsonnet compile buildsys/b.jsonnet
{
    "b": 69,
    "greetings": {
        "c": "hello"
    }
}

Here is an example were we inject model b via TLA where b is found via a model search path set by -M.

moo -M buildsys -A a=b.jsonnet compile buildsys/a.jsonnet
{
    "x": {
        "b": 69
    }
}

Here is an example where we supply the injected data as a literal representation:

moo -A a='{name:"moo"}' compile buildsys/a.jsonnet
{
    "x": {
        "name": "moo"
    }
}

Outputs

moo has several commands and each produce specific output. Here we give a tour of the commands relevant to a build system and their output files.

paths

When integrating moo to a build system it can be useful to query what search paths moo will use to locate a given file and moo path will report these.

It is important to know that moo builds a search path depending on the type of file to be found. For now, there are two classes of file search paths: those to locate data files (includes both model and schema files) and those to locate template files. The latter are identified by a file name ending in j2 and any other ending will be considered a data file.

In addition, for either class of files, the complete search path is composed of a builtin path and augmented with any additional path given by the user.

Some examples makes this clear. First, display the builtin search paths:

moo path j2
/home/bv/dev/moo/moo/templates
moo path jsonnet
/home/bv/dev/moo/moo/jsonnet-code

Next we show how the user may augment the search paths.

moo -M /tmp path jsonnet
/home/bv/dev/moo/moo/jsonnet-code
/tmp
moo -T $HOME/dev/moo/test path j2
/home/bv/dev/moo/moo/templates
/home/bv/dev/moo/test

resolve

Like the path command, a build system integrator may wish to test if a file is properly found without necessarily processing anything. The moo resolve command goes the extra step:

moo resolve ocpp.hpp.j2
moo resolve moo.jsonnet
moo -M $HOME/dev/moo/test resolve test-any.jsonnet
/home/bv/dev/moo/moo/templates/ocpp.hpp.j2
/home/bv/dev/moo/moo/jsonnet-code/moo.jsonnet
/home/bv/dev/moo/test/test-any.jsonnet

imports

The moo command imports is provided to help discover intermediate dependency files which have been imported through Jsonnet's or Jinja's file inclusion mechanisms. This is analogous to CPP's -M option to produce .d files for make.

Here is an example of how a build system may call moo to "scan" a Jsonnet file so that the build dependency graph may properly insert the implicit dependencies:

echo 'local a = import "b.jsonnet"; a' > buildsys/top.jsonnet
echo '{b:69}' > buildsys/b.jsonnet
moo imports buildsys/top.jsonnet
/home/bv/dev/moo/buildsys/b.jsonnet

When a model is a function, all TLAs that lack default values must be satisfied in order for evaluation to succeed. Thus all TLAs must be satisfied when the compile, render or imports commands are used.

moo -A more=less imports buildsys/topf.jsonnet
/home/bv/dev/moo/buildsys/b.jsonnet
/home/bv/dev/moo/buildsys/top.jsonnet

A Jinja template file can be scanned similarly and will be detected as such if its file name ends in .j2.

moo imports buildsys/top.txt.j2

By default, moo will print the list of imported files to standard output. The -o option to imports can be provided to output to a specific file.

compile

moo compile will take a model in Jsonnet or other format and output JSON. This takes the same main options as render (next) and so is a way test the intermediate result of grafting and/or applying TLAs or simply providing a common file type from a variety of supported input types.

A build system may choose to explicitly expose this intermediate representation but that must come at a cost of more file I/O and file production. It should only be employed if needed.

render

The main goal of moo is to generate code and this is the main command do perform that. Like compile (and imports) it prints the result to standard output but can also write to an explicit file given with the -o command option. A minimal example to show the command line is:

moo render -o output.hpp model.jsonnet template.hpp.j2

Examples

This section gives a tour of some playground style integration of moo into various build systems.

Waf

Both Waf and moo are implemented in Python and that gives the developers options in how to integrate moo into their Waf-based build system. One may either call moo as a CLI program (see section cmd) or use it as a Python module (see section mod).

cmd

The Waf project in buildsys/waf/cmd/ provides an example of integrating moo as a command line program. It uses moo render to apply a model to a simple template that merely dumps the data. The model is composed through Jsonnet import mechanism and so moo import is also as a Waf scanner to determine intermediate dependencies.

Here is the wscript file:

#!/usr/bin/env waf
'''
An example waf wscript file for exercising moo as a command
'''

from subprocess import check_output
from waflib.Utils import subst_vars

def configure(cfg):
    cfg.find_program('moo', var='MOO', mandatory=True)

def import_scanner(task):
    deps = []
    for node in task.inputs:
        cmd = "${MOO} imports %s" % node.abspath()
        cmd = subst_vars(cmd, task.env)
        #out = task.exec_command(cmd)
        out = check_output(cmd.split()).decode()
        deps += out.split("\n")

    deps = [task.generator.bld.path.find_or_declare(d) for d in deps if d]
    print(deps)
    return (deps, [])

def build(bld):
    bld(rule="${MOO} render -o ${TGT} ${SRC}",
        source="model.jsonnet dump.txt.j2",
        target="model-dump.txt",
        scan=import_scanner)

Here is the "cmd" example being exercising. We do an initial build. A second rebuild is a no-op. We then edit an intermediate file and rebuild and notice that indeed the change is noticed.

cd buildsys/waf/cmd/
waf distclean configure 
sed -i 's/fortran/FORTRAN/' sub/sub.jsonnet
echo -e "\nfirst build\n"
waf
echo -e "\nsecond build, no rebuild\n"
waf
echo -e "\nmodify intermediate dependency, see it rebuild\n"
sed -i 's/FORTRAN/fortran/' sub/sub.jsonnet
waf
'distclean' finished successfully (0.002s)
Setting top to                           : /home/bv/dev/moo/buildsys/waf/cmd 
Setting out to                           : /home/bv/dev/moo/buildsys/waf/cmd/build 
Checking for program 'moo'               : /home/bv/dev/moo/.direnv/python-venv-3.8.0/bin/moo 
'configure' finished successfully (0.003s)

first build

Waf: Entering directory `/home/bv/dev/moo/buildsys/waf/cmd/build'
[/home/bv/dev/moo/buildsys/waf/cmd/sub.jsonnet, /home/bv/dev/moo/buildsys/waf/cmd/sub/sub.jsonnet]
[1/1] Processing model-dump.txt: model.jsonnet dump.txt.j2 -> build/model-dump.txt
Waf: Leaving directory `/home/bv/dev/moo/buildsys/waf/cmd/build'
'build' finished successfully (1.572s)

second build, no rebuild

Waf: Entering directory `/home/bv/dev/moo/buildsys/waf/cmd/build'
Waf: Leaving directory `/home/bv/dev/moo/buildsys/waf/cmd/build'
'build' finished successfully (0.014s)

modify intermediate dependency, see it rebuild

Waf: Entering directory `/home/bv/dev/moo/buildsys/waf/cmd/build'
[/home/bv/dev/moo/buildsys/waf/cmd/sub.jsonnet, /home/bv/dev/moo/buildsys/waf/cmd/sub/sub.jsonnet]
[1/1] Processing model-dump.txt: model.jsonnet dump.txt.j2 -> build/model-dump.txt
Waf: Leaving directory `/home/bv/dev/moo/buildsys/waf/cmd/build'
'build' finished successfully (1.585s)

mod

The second example works the same but instead of calling moo as a CLI program, the Python module is used. In principle, this can be better performing but it does that require waf is called with a version of Python that is supported by moo.

Here is the wscript file for the "mod" example:

#!/usr/bin/env waf
'''
An example waf wscript file for exercising moo as a Python module

This should be improved by turning the moo parts into a Waf tool!
'''



import moo

def import_scanner(task):
    srcdir = task.generator.bld.path.abspath()
    deps = []
    for node in task.inputs:
        deps += moo.imports(node.abspath(), srcdir)
    deps = [task.generator.bld.path.find_or_declare(d) for d in deps if d]
    print(deps)
    return (deps, [])


def render(task):
    "${MOO} render -o ${TGT} ${SRC}",
    srcdir = task.generator.bld.path.abspath()
    out = task.outputs[0]
    model = moo.io.load(task.inputs[0].abspath())
    templ = task.inputs[1].abspath()

    # keep compatibility with moo render
    helper = moo.io.load(moo.util.resolve("moo.jsonnet"))
    out.write(moo.templates.render(templ, dict(model=model, moo=helper)))


def configure(cfg):
    pass


def build(bld):
    bld(rule=render,
        source="model.jsonnet dump.txt.j2",
        target="model-dump.txt",
        scan=import_scanner)

And, here it is being exercised:

cd buildsys/waf/mod/
waf distclean configure 
sed -i 's/fortran/FORTRAN/' sub/sub.jsonnet
echo -e "\nfirst build\n"
waf
echo -e "\nsecond build, no rebuild\n"
waf
echo -e "\nmodify intermediate dependency, see it rebuild\n"
sed -i 's/FORTRAN/fortran/' sub/sub.jsonnet
waf
'distclean' finished successfully (0.002s)
Setting top to                           : /home/bv/dev/moo/buildsys/waf/mod 
Setting out to                           : /home/bv/dev/moo/buildsys/waf/mod/build 
'configure' finished successfully (0.002s)

first build

Waf: Entering directory `/home/bv/dev/moo/buildsys/waf/mod/build'
[/home/bv/dev/moo/buildsys/waf/mod/sub.jsonnet, /home/bv/dev/moo/buildsys/waf/mod/sub/sub.jsonnet]
[1/1] Processing model-dump.txt: model.jsonnet dump.txt.j2 -> build/model-dump.txt
Waf: Leaving directory `/home/bv/dev/moo/buildsys/waf/mod/build'
'build' finished successfully (0.346s)

second build, no rebuild

Waf: Entering directory `/home/bv/dev/moo/buildsys/waf/mod/build'
Waf: Leaving directory `/home/bv/dev/moo/buildsys/waf/mod/build'
'build' finished successfully (0.005s)

modify intermediate dependency, see it rebuild

Waf: Entering directory `/home/bv/dev/moo/buildsys/waf/mod/build'
[/home/bv/dev/moo/buildsys/waf/mod/sub.jsonnet, /home/bv/dev/moo/buildsys/waf/mod/sub/sub.jsonnet]
[1/1] Processing model-dump.txt: model.jsonnet dump.txt.j2 -> build/model-dump.txt
Waf: Leaving directory `/home/bv/dev/moo/buildsys/waf/mod/build'
'build' finished successfully (0.347s)

Make

Ah, venerable make. This example currently does not capture intermediate dependencies. Contributions/fixes welcome.

The Makefile:

model-dump.txt : model.jsonnet dump.txt.j2 
        moo render -o $@ $^
clean:
        rm -f model-dump.txt

And it getting exercised:

cd buildsys/make
make clean
sed -i 's/fortran/FORTRAN/' sub/sub.jsonnet
make
echo -e '\nNext we edit the intermediate files and note no rebuild\n'
sed -i 's/FORTRAN/fortran/' sub/sub.jsonnet
make
echo -e '\nIn any case, first build gave us:\n'
ls -l model-dump.txt
cat model-dump.txt

rm -f model-dump.txt
moo render -o model-dump.txt model.jsonnet dump.txt.j2

Next we edit the intermediate files and note no rebuild

make: 'model-dump.txt' is up to date.

In any case, first build gave us:

-rw-rw-r-- 1 bv bv 93 Feb 12 10:56 model-dump.txt
sub = {'a': 1, 'b': 2, 'c': 'hello'}
subsub = {'a': 1, 'b': 2, 'c': 'hello', 'd': 'FORTRAN'}

CMake

This section describes one way to integrate moo with a CMake-based build. It shows what one must do to insert a code generator into the build graph in a way that respects intermediate dependencies using moo imports like the Waf examples.

Currently the CMake code is provided merely as an example which is found in moo.cmake. The goal is to further develop this example into proper support including:

  • [ ] install moo.cmake and other cmake files via setup.py
  • [ ] have the usual find_package(moo) do the right thing

The current example CMakeLists.txt file using the moo CMake support is:

project("moo-buildsys-cmake")
cmake_minimum_required(VERSION 3.4...3.17) # older may also work

# fixme: needs to turn into a find_package(moo)!
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR})
include(moo)

moo_codegen(MODEL model.jsonnet TEMPL dump.txt.j2 CODEGEN model-dump.txt)

## We need some target to trigger the codegen.  Normally, this would
## be done by some "real" code target depending on the CODEGEN output.
##   cmake --build build/ -t DOIT
add_custom_target(DOIT ALL DEPENDS model-dump.txt)

And, here it is getting exercised:

cd buildsys/cmake
rm -rf build
echo '{a:1,b:2,c:"hello"}' > sub.jsonnet
cmake -B build -S .

echo -e '\nDo initial build\n'  
sed -i 's/fortran/FORTRAN/' sub/sub.jsonnet
cmake --build build
ls -l --full-time build/model-dump.txt

echo -e '\nNext we edit the intermediate files and notice a rebuild\n'
sed -i 's/FORTRAN/fortran/' sub/sub.jsonnet
cmake --build build
ls -l --full-time build/model-dump.txt

echo -e '\nDo do a second build, output is not rebuilt\n'  
cmake --build build
ls -l --full-time build/model-dump.txt

echo -e '\nAdd new implicit dependency, which should cause a rebuild\n'  
echo -e 'local ss = import "sub/sub2.jsonnet";\nss' > sub.jsonnet
echo '{a:1,b:2,c:"hello"}' > sub/sub2.jsonnet
cmake --build build
ls -l --full-time build/model-dump.txt

echo -e '\nUpdate new implicit dependency, which should cause a rebuild\n'  
echo '{a:1,b:2,c:"see me"}' > sub/sub2.jsonnet
cmake --build build
ls -l --full-time build/model-dump.txt

-- The C compiler identification is GNU 7.5.0
-- The CXX compiler identification is GNU 7.5.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc - works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ - works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/bv/dev/moo/buildsys/cmake/build

Do initial build

Scanning dependencies of target DOIT
[100%] generate code model-dump.txt
[100%] Built target DOIT
-rw-rw-r-- 1 bv bv 93 2021-02-12 10:56:29.982843529 -0500 build/model-dump.txt

Next we edit the intermediate files and notice a rebuild

[100%] generate code model-dump.txt
[100%] Built target DOIT
-rw-rw-r-- 1 bv bv 93 2021-02-12 10:56:30.694823972 -0500 build/model-dump.txt

Do do a second build, output is not rebuilt

[100%] Built target DOIT
-rw-rw-r-- 1 bv bv 93 2021-02-12 10:56:30.694823972 -0500 build/model-dump.txt

Add new implicit dependency, which should cause a rebuild

[100%] generate code model-dump.txt
[100%] Built target DOIT
-rw-rw-r-- 1 bv bv 93 2021-02-12 10:56:31.442803424 -0500 build/model-dump.txt

Update new implicit dependency, which should cause a rebuild

[100%] Built target DOIT
-rw-rw-r-- 1 bv bv 93 2021-02-12 10:56:31.442803424 -0500 build/model-dump.txt

Author: Brett Viren

Created: 2021-02-12 Fri 10:56

Validate