Writing a Compiler Plugin¶
Craftr does not provide you with “one way to do it”. There are multiple
ways you can make Craftr generate the command you need it to. You can
hard-code the command by creating a Target
from scratch
or you can implement a Generator Function. What we do most of the time
is to implement a Compiler Class which inherits
craftr.ext.compiler._base.BaseCompiler
. It allows us to create
instances of “compiler interfaces” with different settings, which makes
these settings included in all procedures that generate targets.
Manual Targets¶
First things first though, here’s a small example how you can just manually create a target and have Craftr export that into the Ninja manifest:
from craftr import path, Target
main = Target(
command = 'gcc $in -Wall -std=c++11 -o $out',
inputs = path.glob('src/*.c'),
outputs = ['main'],
)
Notice how we specify just plain 'main'
as the output file: relative
filenames will be considered relative to the build directory! Craftr
automatically and always changes the working directory to the build
directory before executing any code.
Generator Functions¶
Given the above simple GCC example, we can make things a bit more customizable by implementing a function that generates the command and target for us.
from craftr import path, Target
def compile(sources, output, include=[], defines=[],
lib=[], libpath=[], warn='1', std='c99'):
command = ['gcc', '$in', '-W' + warn, '-std=' + std)
command += ['-I' + x for x in include]
command += ['-D' + x for x in defines]
command += ['-L' + x for x in libpath]
command += ['-l' + x for x in lib]
return Target(command, sources, [output])
main = compile(
sources = path.glob('src/*.c'),
output = 'main',
warn = 'all',
std = 'c++11'
)
Using the TargetBuilder¶
While the above example already looks nice, it still has problems, or say, complications: What will you do if you make use of some libraries and have a number of additional include directories, defines, libpaths and libs? Just concatenate them by hand?
Craftr’s solution to this problem are Framework
s. They
represent a collection of settings that can either be merged (e.g. for
things like include directories, defines, etc.) or the first available
setting can be used (e.g. for some one-off compiler option). In Craftr,
everything has frameworks. Just for example, a Target
has
a list of frameworks that have been used to generate it, thus if other
targets are created taking it as an input, they can automatically re-use
these frameworks and the user doesn’t have to manually specify the framework
yet another time.
from craftr.ext.platform import cc, ld
from craftr.ext.some_library import some_library_framework
obj = cc.compile(
sources = path.glob('src/*.c'),
frameworks = [some_library_framework]
)
bin = ld.link(
inputs = obj,
output = 'main'
# <: Note how we do not add "some_library_framework" in this call
)
Moving on to creating Target
generator functions with the
TargetBuilder
! This class handles a bunch of things,
but don’t let yourself be confused about all these internals yet. They
are here for reference:
- Evaluate a list of inputs that can consist of filenames or targets. Filenames are automatically normalized and for targets, the output files will be added to the input files and the frameworks will be included into the frameworks list.
- Include a list of frameworks passed directly to the generator function.
- Create a new
Framework
from the additional keyword arguments passed to the generator function, but this framework will not be included in the generated targets framework list! You don’t want youradditional_flags
passed tocc.compile()
also being passed toar.staticlib()
automatically :) - All frameworks will then be expanded into a single list using
expand_frameworks()
(to flatten out framework dependencies). - A
FrameworkJoin
will be created from all frameworks (including the special**kwargs
framework) to enable the generator function to read the settings.
Now, how Tracer would say it, “let’s get to it already!”. Note that I’ve
also added a language
parameter which I did not in the previous examples.
from craftr import path, Target, TargetBuilder
def compile(sources, output, frameworks=(), target_name=None, language='c', **kwargs):
builder = TargetBuilder(sources, frameworks, kwargs, name=target_name)
include = builder.merge('include')
defines = builder.merge('defines')
libpath = builder.merge('libpath')
lib = builder.merge('lib')
std = builder.get('std', 'c99')
warn = builder.get('warn', '1')
# Same code as above
command = ['gcc', '-x', language, '$in', '-W' + warn, '-std=' + std)
command += ['-I' + x for x in include]
command += ['-D' + x for x in defines]
command += ['-L' + x for x in libpath]
command += ['-l' + x for x in lib]
return builder.create_target(command, output)
# Now we can use some other Craftfiles that expose Frameworks.
# (You know, Craftr's not really popular yet so there's literally
# only my own stuff right now :P)
from craftr.ext.libs.nr_iterator import nr_iterator
from craftr.ext.libs.nr_math3d import nr_math3d
main = compile(
language = 'c++',
sources = path.glob('src/*.cpp'),
output = 'main',
frameworks = [nr_iterator, nr_math3d]
)
Using the BaseCompiler¶
It has a number of advantages, but you’re free to use a plain generator
function as shown in the previous example! There’s really not much to
be changed for using a BaseCompiler
instead:
from craftr import path, Target
from craftr.ext.compiler._base import BaseCompiler
class SimpleGCC(BaseCompiler):
def compile(self, sources, output, frameworks=(), target_name=None, language='c', **kwargs):
builder = self.builder(sources, frameworks, kwargs, name=target_name)
# ... exactly the same code as in the previous example
gcc = SimpleGCC()
main = gcc.compile(
# ...
)
However! you can now pass additional settings to the SimpleGCC()
constructor that will be taken into account as well. Note that these are
considered last after everything else (**kwargs
, frameworks list, input
target frameworks and only then the settings passed to the constructor).
Monkeypatching existing compilers¶
This is a technique that is used for instance by the maxon.c4d
extension modules which requires additional preprocessing of the
parameters passed to cxx.compile()
and ld.link()
. Since v1.1.1,
the BaseCompiler
supports hooking in after a TargetBuilder
was created for a specific method call.
def _my_link_hook(builder):
debug = builder.get('debug', options.get_bool('debug', False))
builder.setdefault('output_type', 'dll')
builder.add_framework(Framework('_my_link_hook',
defines = ['_DEBUG'] if debug else ['NDEBUG'],
), local=True)
ld = platform.ld.fork()
ld.register_hook('link', _my_link_hook)