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
mooonly 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
mooitself or in templates provided bymoothat 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
mooexamples and which is recommended is.jsonnet. Butmooalso supports the consumption of.json,.xml,.ini,.yaml,.csvand others. Models are provided tomoovia 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 somemoofunctionality is sensitive to the trailing.j2. Templates are provided tomoovia 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
.jsonnetfile evaluates to a function, the function arguments aka "top-level arguments" (TLAs) may be provided with themoo -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:
[ ]installmoo.cmakeand other cmake files viasetup.py[ ]have the usualfind_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