Manual¶
This is the Fbuild manual, which is aiming at being the authorative guide to using Fbuild in your projects. Note that this is still a work-in-progress!
Basics: Getting Fbuild, C Compilation, and Builders¶
Getting Started¶
This manual is for the most recent version of Fbuild from Git. In order to get it, just run:
$ git clone https://github.com/felix-lang/fbuild.git
$ cd fbuild
$ python3 setup.py install
You can also use Fbuild without installing it by running the fbuild-light
script.
Compiling C¶
Let’s start with a simple build script: it just builds a “Hello, world!” program written in C. Here’s the C code:
#include <stdio.h>
int main() {
puts("Hello, world!");
return 0;
}
and the Fbuild build script:
from fbuild.builders.c import guess
def build(ctx):
builder = guess.static(ctx)
builder.build_exe('hello', ['hello.c'])
It’s pretty simple. First, guess is imported. That object will let us create a
new builder for building C programs (we’ll get to builders in a moment). Then, the
script defines a build function that takes an object of type
fbuild.context.Context (ctx). That object is kind of like the “build
engine.” The next line calls guess.static, and the line after calls the
build_exe method. It can take several keyword arguments, but the only two
positional ones are the output file (hello) and a list of source files
(['hello.c']). Let’s run it:
ryan@DevPC-LX:~/stuff/fbuild/playground/doc$ fbuild
determining platform : {'linux', 'posix'}
looking for clang : ok /home/ryan/stuff/Downloads/clang+llvm-3.8.0-x86_64-linux-gnu-ubuntu-14.04/bin/clang
checking clang : ok
looking for ar : ok /usr/bin/ar
looking for ranlib : ok /usr/bin/ranlib
checking if clang can make objects : ok
checking if clang can make libraries : ok
checking if clang can make exes : ok
checking if clang can link lib to exe : ok
* clang : hello.c -> build/obj/hello/hello.o
* clang : build/obj/hello/hello.o -> build/hello
ryan@DevPC-LX:~/stuff/fbuild/playground/doc$
That just:
- Detected my C compiler and the associated utilities (
arandranlib). - Tested it.
- Built our program.
Builders¶
In Fbuild, a builder is an object that…builds stuff. In the last example, the
builder could build C executables and libraries. Most of Fbuild revolves around
builders, and all of them are located within fbuild.builders.
Rewind: The Core of Fbuild¶
Graphs and Caching¶
Let’s take a step back for a moment. How does all this stuff even work??
Conventional build systems usually will function by first building a DAG (directed acyclic graph) consisting of all the inputs/outputs as nodes and the commands to create the outputs as edges. Although this allows some nice features (such as implicit parallelism and cool progress bars), it doesn’t allow for some important things. In particular, if you’ve ever use Make, you know how painful it can be to read dependencies from a file. In addition, you can’t decide a target’s outputs at run time, and some DAG-based build systems can be either hard to extend, hard to use for simple projects, or brutally low-level.
Fbuild doesn’t use a DAG. It doesn’t even use a graph at all. Instead, it uses something much more powerful: caching. The core idea is that all your build rules are really just functions.
Any attempt to explain this any more would likely fail, so I’ll just show an
example. Put this in a new fbuildroot.py:
import fbuild.db
@fbuild.db.caches
def myfunc(ctx, name):
print('Hello, %s!' % name)
def build(ctx):
myfunc(ctx, 'Fbuild world')
I’ll explain fbuild.db.caches in a moment, but for now, note that any function
that you use it on must take a context object as its first argument.
When the script is run, the output is what one would expect:
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw$ fbuild
Hello, Fbuild world!
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw$
However, watch what happens if you run it again:
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw$ fbuild
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw$
Nothing was shown! But why?
fbuild.db.caches will cache (or memoize, if you’re more familiar with that
term) the given function. That means that, when the function is called, Fbuild
will save its arguments and the result into a database on disk (by default, it’s
located in build/fbuild.db). If the function is called again, then, instead of
running it, Fbuild will just return the previous result. This is more obvious
with a slightly different example:
import fbuild.db
@fbuild.db.caches
def myfunc(ctx, name):
print('Hello, %s!' % name)
return 'myfunc was called'
def build(ctx):
message = myfunc(ctx, 'Fbuild world')
print(message)
If you run it, this happens:
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw$ fbuild
Hello, Fbuild world!
myfunc was called
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw$
Note that the database didn’t need to be deleted; Fbuild will automatically re-run a function if its contents have changed.
Watch what happens if you run it again:
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw$ fbuild
myfunc was called
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw$
When myfunc was called the first time, it’s return value (
'myfunc was called') was saved into the database. On the second run, Fbuild
saw that myfunc hadn’t changed and was being called with the same arguments,
so it just returned the original return value.
You may be wondering what this has to do with build systems. Well, in Fbuild,
almost every internal function is cached like this. Remember guess_static? If
you run that script again, the C compiler won’t be re-configured. Fbuild cached
the result of calling guess_static and loaded it back up from the database.
Dependencies¶
All this is really cool, but it doesn’t seem that practical at the moment. Build systems don’t just configure builders; they also…well, build stuff. Caching seems useless for solving this problem, right!
Wrong! Fbuild has several function annotations that you can use to help with this. Take a look at this build script:
import fbuild.db
@fbuild.db.caches
def build_a_file(ctx, src: fbuild.db.SRC):
print('This is supposed to build the file %s...' % src)
def build(ctx):
build_a_file(ctx, 'myfile')
I’ll explain the details in a moment; for now, just know that build_a_file is
supposed to do something with its input argument myfile. Let’s run it:
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$ fbuild
Traceback (most recent call last):
File "/media/ryan/stuff/anaconda/bin/fbuild", line 9, in <module>
load_entry_point('fbuild==0.2', 'console_scripts', 'fbuild')()
File "/media/ryan/stuff/fbuild/lib/fbuild/main.py", line 179, in main
result = build(ctx)
File "/media/ryan/stuff/fbuild/lib/fbuild/main.py", line 104, in build
target.function(ctx)
File "/media/ryan/stuff/fbuild/playground/doc-rw-dep/fbuildroot.py", line 8, in build
build_a_file(ctx, 'myfile')
File "/media/ryan/stuff/fbuild/lib/fbuild/db/__init__.py", line 121, in __call__
result, srcs, dsts = self.call(*args, **kwargs)
File "/media/ryan/stuff/fbuild/lib/fbuild/db/__init__.py", line 125, in call
return ctx.db.call(self.function, ctx, *args, **kwargs)
File "/media/ryan/stuff/fbuild/lib/fbuild/db/database.py", line 101, in call
dsts)
File "/media/ryan/stuff/fbuild/lib/fbuild/rpc.py", line 68, in call
raise result.result
File "/media/ryan/stuff/fbuild/lib/fbuild/rpc.py", line 112, in _process
result.result = self._handler(*args, **kwargs)
File "/media/ryan/stuff/fbuild/lib/fbuild/db/database.py", line 24, in handle_rpc
return method(*args, **kwargs)
File "/media/ryan/stuff/fbuild/lib/fbuild/db/backend.py", line 42, in prepare
call_file_digests = self.check_call_files(call_id, srcs)
File "/media/ryan/stuff/fbuild/lib/fbuild/db/backend.py", line 143, in check_call_files
d, file_id, file_digest = self.check_call_file(call_id, file_name)
File "/media/ryan/stuff/fbuild/lib/fbuild/db/backend.py", line 165, in check_call_file
dirty, file_id, mtime, digest = self.add_file(file_name)
File "/media/ryan/stuff/fbuild/lib/fbuild/db/backend.py", line 249, in add_file
file_mtime = file_path.getmtime()
File "/media/ryan/stuff/fbuild/lib/fbuild/path.py", line 224, in getmtime
return os.path.getmtime(self)
File "/media/ryan/stuff/anaconda/lib/python3.4/genericpath.py", line 55, in getmtime
return os.stat(filename).st_mtime
FileNotFoundError: [Errno 2] No such file or directory: Path('myfile')
Whoops! I forgot to create myfile:
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$ touch myfile
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$ fbuild
This is supposed to build the file myfile...
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$
As usual, let’s also run it again:
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$ fbuild
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$
Nothing happened! This is caching at work again.
Now try adding something to myfile and running it again:
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$ echo 1234 > myfile
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$ fbuild
This is supposed to build the file myfile...
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$
build_a_file is run again! Look back at these two lines in fbuildroot.py:
@fbuild.db.caches
def build_a_file(ctx, src: fbuild.db.SRC):
I already explained how fbuild.db.caches works. However, the new addition is
the function annotation fbuild.db.SRC. This works with fbuild.db.caches to
allow for dependency resolution.
When you annotate a function argument with fbuild.db.SRC, you’re telling
fbuild.db.caches that the argument is a source file. As already stated, if
you change build_a_file or change any of its arguments, it will be re-run.
In addition, if you change the contents of any source file, the function will
also be re-run. Because I changed the contents of myfile, Fbuild re-ran
build_a_file.
Remember build_exe? This is how it works. Although the function itself is
somewhat complex, at it’s core, it uses a similar method to this.
You can also create functions that take multiple sources:
import fbuild.db
@fbuild.db.caches
def build_a_file(ctx, first_source: fbuild.db.SRC, other_sources: fbuild.db.SRCS):
print('Do something with %s and %s...' % (first_source, other_sources))
def build(ctx):
build_a_file(ctx, 'myfile1', ['myfile2', 'myfile3'])
As you might expect by now, fbuild.db.SRCS takes a list of source files, not
just one.
Nevertheless, this is only part of the equation. A build system usually needs to also keep track of its output files. Unlike other example scripts, this is actually not just a toy; it’s actually a quite useful function:
import fbuild.db, shutil, io
@fbuild.db.caches
def merge_files(ctx, srcs: fbuild.db.SRCS, dst: fbuild.db.DST):
print('Merging files...')
result = io.StringIO()
for src in srcs:
with open(src) as f:
shutil.copyfileobj(f, result)
result.seek(0)
with open(dst, 'w') as f:
shutil.copyfileobj(result, f)
def build(ctx):
merge_files(ctx, ['input1', 'input2'], 'output')
The details of merge_files don’t really matter as much as the function
annotations. Note that another annotation was added: fbuild.db.DST, which
annotates the destination parameter. The results of running it are like you’d
expect:
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$ echo 1 > input1
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$ echo 2 > input2
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$ fbuild
Merging files...
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$ cat output
1
2
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-dep$
As before, any changes to input1 or input2 will cause output to be
re-built.
This isn’t quite enough, however, but before I go to the next topic, there’s one more basic thing that needs to be covered: paths.
Path Objects¶
Remember the error message when I forgot to create myfile? It mentioned that
the missing file was Path('myfile'). The Path here is for Fbuild’s path
objects. I won’t go over every single detail, but path objects (defined in
fbuild.path) are…well, path objects. The class fbuild.path.Path is a
subclass of str, so it supports all the normal operations of str, and you
can pass it to any normal Python function expecting a string, However, path
objects also have a bunch of methods useful for file system/path manipulation.
For thorough documentation on all the methods, check out `lib/fbuild/path.py <
https://github.com/felix-lang/fbuild/blob/master/lib/fbuild/path.py>`_ in the
source code. Here I’ll mention just one capability of paths: in order to join
them, you can use /. For instance, Path('src') / 'dst' returns
Path('src/dst') on Posix and Path('src\\dst') on Windows.
Rule Destinations and Cached Objects¶
Back on topic: recall the very first Fbuild script in the tutorial:
from fbuild.builders.c import guess
def build(ctx):
builder = guess.static(ctx)
builder.build_exe('hello', ['hello.c'])
See builder.build_exe? That function actually returns a value: the full path
to the resulting executable. The reason is that, usually, the developer doesn’t
care where the executable is stored or what extension it has, but they may very
well want to know where it’s located. To handle this case, Fbuild supports
annotating the function’s return value as a destination. For example:
from fbuild.path import Path
import fbuild.db, shutil
@fbuild.db.caches
def do_something(ctx, src: fbuild.db.SRC) -> fbuild.db.DST:
src = Path(src)
dst = ctx.buildroot / src.replaceext('.out')
print('Copying %s to %s...' % (src, dst))
src.copy(dst)
return dst
def build(ctx):
do_something(ctx, 'x.in')
Let’s run it:
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-out$ echo 123 > x.in
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-out$ fbuild
Copying x.in to build/x.out...
ryan@DevPC-LX:~/stuff/fbuild/playground/doc-rw-out$
This script has a lot of new stuff! It uses the Path objects mentioned in the
previous section. In particular:
- This is the first example script to use
ctx.buildroot, which is aPaththat points to the output directory. In this case, it’sbuild. Path.replaceextreplaces the given file extension, e.g.Path('x.in').replaceext('.out')results inPath('x.out').Path.copycopies the given file.Path(src).copy(dst)`is equivalent toshutil.copy(src, dst).- Most importantly,
do_somethingreturns the resulting output file. This will cause Fbuild to place it in the database.
The entirety of Fbuild, including the C builder that I first showed, consists of what I’ve just shown here, with three exceptions:
- In
fbuild.db, there’s a very important class:
fbuild.db.PersistentObject. If you want to contain any cached functions within a class, the class must derive fromPersistentObject, and the cached functions should instead usecachemethod(see below). Note that the default constructor for objects derived fromPersistentObjecttakes a context object as its argument. If you define a custom__init__, you need to take a context object and assign it toself.ctx. Example:class MyObject(fbuild.db.PersistentObject): def __init__(self, ctx): self.ctx = ctx def build(ctx): obj = MyObject(ctx)
fbuild.db.cachemethodis equivalent tofbuild.db.caches, but it is
instead designed to annotate methods that are in a subclass of
PersistentObject. In addition, methods annotated withcachemethoddon’t need to be passed a context argument. Example:class MyObject(fbuild.db.PersistentObject): def __init__(self, ctx): self.ctx = ctx @fbuild.db.cachemethod def myfunc(self, msg): print('Message:', msg) def build(ctx): obj = MyObject(ctx) obj.myfunc('Hello, world!')
- Sometimes, you may not want to return a whole object. For this case, Fbuild
providesfbuild.record.Record. ARecordis basically adict, except that you can also set and get keys via attributes. For example,my_record.ais equivalent tomy_record['a'].
Many examples of this are in the Fbuild source.
Back to a Higher Level: Logging and Running External Commands¶
Logging¶
Of course, a build system is mostly useless without being able to run external commands. First, I need to mention an important concept of Fbuild that I’ve glossed over thus far: logging.
Notice that, in all the above examples, print was used to print information.
Technically, you’re not supposed to do this! In order to handle this, Fbuild
provides ctx.logger. Here’s a basic example:
def build(ctx):
ctx.logger.log('This will be written to the log file: build/fbuild.log.',
verbose=1)
ctx.logger.log('This will be written to the console.')
ctx.logger.log('This will be written to the console in red.', color='red')
ctx.logger.log('This will be written to the console in a color designated for '
'compiling files.', color='compile')
ctx.logger.log('And for linking files!', color='link')
ctx.logger.check('this is used when configuring various things in Fbuild')
ctx.logger.passed()
ctx.logger.check('you can also give custom messages and colors', color='blue')
ctx.logger.passed('it worked!')
ctx.logger.check('things can also fail')
ctx.logger.failed('dang it!')
and here’s the output:
Executing Shell Commands¶
Now comes executing shell commands! Every context object has a method for this:
execute. Here’s the definition from the source code:
def execute(self, cmd, msg1=None, msg2=None, *,
color=None,
quieter=0,
stdout_quieter=None,
stderr_quieter=None,
input=None,
stdin=None,
stdout=fbuild.subprocess.PIPE,
stderr=fbuild.subprocess.PIPE,
timeout=None,
env=None,
runtime_libpaths=None,
ignore_error=False,
**kwargs):
That’s a lot of arguments! I’ll break them down one by one:
cmdis the command to run. Although there are some edge cases, in general, this should be a list, such as['clang', '-o', 'x', 'x.c'].msg1,msg2, andcolorwill be explained in the example below.quieteris the same as thequieterargument withlogger.log; it determines whether or notmsg1andmsg2will be displayed or just sent to the log file. In addition, this will be the default value fostdout_quieterandstderr_quieterif they areNone.stdout_quieterandstderr_quieterare the same thing asquieter, except they are for whether or not the output of the command will be shown.inputis a byte string to be sent to the command’s standard input.stdinis ignored ifinputis truthy; otherwise, it will be thestdinargument passed tosubprocess.Popen.stdoutandstderrare passed tosubprocess.Popen.timeoutis the maximum number of seconds to wait for the command to finish before killing it. If you pass a falsy value, it will never kill the command.envis a dictionary of environment variables to pass to the function; ifNone, then the current environment inos.environwill be passed.runtime_libpathsis a list of strings to be added to the platform’s DLL/ shared library search path.ignore_errorwill determine whether or not anfbuild.ExecutionErroris thrown if the command fails.kwargsis just passed on tosubprocess.Popen.
In addition, it will return a tuple (stdout, stderr), where both stdout
and stderr are byte strings.
That’s a lot to take in at once, so here are some examples of using execute:
def build(ctx):
ctx.execute(['echo', '123']) # Run `echo 123` and print the output.
# Run echo, but print the message "running echo" first, with NO NEWLINE.
ctx.execute(['echo', 'Echoed text here!'], msg1='running echo')
# Run echo, but print the message "running echo: 123" first.
ctx.execute(['echo', 'Echoed text here!'], msg1='running echo', msg2='123')
# This is the color of `msg2`.
ctx.execute(['echo', '123'], msg1='running echo', msg2='123', color='compile')
# This would normally be an error, but ignore_error is True.
ctx.execute(['printf'], ignore_error=True)
ctx.execute(['echo', '123'], stdout_quieter=1) # Don't print the output.
# msg1 and msg2 will still be printed.
ctx.execute(['echo', '123'], msg1='running echo', msg2='123', stdout_quieter=1)
ctx.execute(['cat'], input=b'Input here!') # Send some text to stdin.
# This will throw an error because running `sleep` timed out.
ctx.execute(['sleep', '1'], msg1='running sleep', msg2='1', timeout=0.5)
And the output:
Note that execute is not cached!
Configuration: Locating Programs and Defining Command-line Options¶
Finding Programs¶
A frequently needed capability of a build system is to locate a program. For
instance, you may want to find the awk executable on the system. For this,
Fbuild has fbuild.builders.find_program. It works like this:
from fbuild.builders import find_program
def build(ctx):
awk = find_program(ctx, ['awk', 'gawk'])
print(awk)
It takes two arguments: the context object and a list of programs to search for.
The return value is the first program it found. If none are found, it will throw
an exception of type fbuild.ConfigFailed.
In addition, find_program is cached, so it won’t re-run every single time you
run Fbuild.
Command-line Options¶
Other common build system feature is the ability for the user to pass information to the build system in some way, usually either environment variables or command-line arguments. Unfortunately, both are often implemented in odd ways, like weird execution environments (Make and CMake are the worst offenders here) or even defining arguments in a totally different file.
Fbuild takes the easy route: since your build rules are in one function, your arguments can be defined in another. Instead of using some weird, arcane, home-grown, undocumented module for this, Fbuild uses the `argparse module < https://docs.python.org/3/library/argparse.html>`_.
Here’s a simple example of command-line arguments in Fbuild:
def arguments(parser):
group = parser.add_argument_group('config options')
group.add_argument('--build-mode', help='Define the desired build mode',
choices=['debug', 'release'])
group.add_argument('--mini', help='Attempt to minify the built code',
action='store_true', dest='minify')
group.add_argument('--arch', help='The target build architecture', default='x64')
group.add_options((
make_option('--build-mode', help='Define the desired build mode',
choices=['debug', 'release']),
make_option('--mini', help='Attempt to minify the built code',
action='store_true', dest='minify'),
make_option('--arch', help='The target build architecture', default='x64'),
))
def build(ctx):
print('Build mode:', ctx.options.build_mode)
print('Minify?', ctx.options.minify)
print('Arch:', ctx.options.arch)
The example is mostly self-explanatory; if you have any questions, consult the argparse documentation.
Advanced Core Topics: Adding External Dependencies and Installing Files¶
External Dependencies¶
Let’s say you’re creating your own programming language called Qux. When you run it, it looks kind of like this:
ryan@DevPC-LX:~$ qux myfile.qux myfile.out
Qux version 0.0.0
Building myfile.qux...
NOTE: myfile.qux imports myotherfile.qux!
Building myotherfile.qux...
Successfully built myfile.out!
ryan@DevPC-LX:~$
Take this simple rule for building Qux programs:
from fbuild.builders import find_program
from fbuild.path import Path
import fbuild.db
class QuxBuilder(fbuild.db.PersistentObject):
def __init__(self, ctx):
self.ctx = ctx
self.qux = find_program(ctx, ['qux'])
@fbuild.db.cachemethod
def build(self, src: fbuild.db.SRC) -> fbuild.db.DST:
dst = self.ctx.buildroot / Path(src).replaceext('.out')
self.ctx.execute([self.qux, src, dst])
return dst
def build(ctx):
qux = QuxBuilder(ctx)
qux.build('myfile.qux')
This sort of works. Remember, myfile.qux depends on myotherfile.qux, but
Fbuild doesn’t know that. Therefore, if you edit myotherfile.qux,
myfile.out won’t get rebuilt.
For this purpose, Fbuild has ctx.db.add_external_dependencies_to_call:
from fbuild.builders import find_program
from fbuild.path import Path
import fbuild.db, re
class QuxBuilder(fbuild.db.PersistentObject):
def __init__(self, ctx):
self.ctx = ctx
self.qux = find_program(ctx, ['qux'])
@fbuild.db.cachemethod
def build(self, src: fbuild.db.SRC) -> fbuild.db.DST:
dst = self.ctx.buildroot / Path(src).replaceext('.out')
stdout, stderr = self.ctx.execute([self.qux, src, dst])
regex = re.compile(r'Building (.*)...$')
for line in stdout.decode('ascii').splitlines():
m = regex.match(line)
if m:
self.ctx.db.add_external_dependencies_to_call(srcs=[m.group(1)])
return dst
def build(ctx):
qux = QuxBuilder(ctx)
qux.build('myfile.qux')
This is quite a bit more complex than the last example! If you’re unfamiliar with
Python’s re module, this may look
confusing. All the regex is going is locating all the files that Qux is building.
The important part is the call to
self.ctx.db.add_external_dependencies_to_call, which takes two keyword
arguments: srcs and dsts. These add extra dependencies/outputs to the
build rule while it’s still executing. Now, if you edit myotherfile.qux,
then myfile.out will be rebuild!
Installing Files¶
When your application is built, you probably want some way to install it onto the
user’s system. Fbuild has this covered with ctx.install. It’s defined like
this:
def install(self, path, target, *, rename=None, perms=None):
The path is the path to install of the file to install, and target is a
subdirectory of the installation prefix to install into. For instance,
ctx.install('somehwere/my-file', 'share/my-app') will copy somewhere/my-file
to $TARGET/share/my-app/my-file.
The installation target (shown above as $TARGET) is defined as
ctx.install_destdir / ctx.install_prefix. install_destdir is / by default,
and install_prefix is /usr. (This separation is done to ensure that packaging
apps using Fbuild for Unix systems can be done in an autoconf-compatible way.) Both of
these can be set inside your application; for example, you can assign them in build
using values passed on the command line:
def arguments(parser):
group = parser.add_argument_group('config options')
group.add_argument('--destdir', help='Set the installation destdir', default='/')
group.add_argument('--prefix', help='Set the installation prefix', default='usr')
def build(ctx):
ctx.install_destdir = ctx.options.destdir
ctx.install_prefix = ctx.options.prefix
# Continue as normal.
If you want to change the file name (e.g. my-new-file instead), you can pass that
as the rename parameter, e.g.
ctx.install('somewhere/my-file', 'share/my-app', 'my-new-file').
perms can be used to assign custom permissions to the target file. By default, it
will use the same permissions as the original file.
By default, nothing is installed yet. ctx.install just marks the file for
installation. Later on, when the user runs fbuild install, Fbuild will run the
build script, then install any files that were marked for installation.
Bonus points: if you use a Linux system that has polkit available (which is basically
most modern Linux distros), you’ll never need to prefix your install commands with
sudo. Fbuild will automatically use polkit to ask for escalated privileges to allow
installation.
One more note: Fbuild provides two hooks, pre_install and post_install, that can be used to have code run before and after installation:
def pre_install(ctx):
print('Going to install...')
def post_install(ctx):
print('Done installing!')
Platforms¶
Within fbuild.builders is a module that you will likely rarely use, but it
is important to the implementation of various builders: platform. It defines
a couple of interesting functions, but the most important one is
guess_platform.
guess_platform returns a set of strings that aims at identifying the
platform that Fbuild is running on. The following are possible members of the set
posix: This is a Posix system.linux: This is a Linux system.solaris: This is a Solaris system.sunos: This is a Sun Solaris system.cygwin: This is a Windows system, but Fbuild is being run within Cygwin.mingw: This is a Windows system, but Fbuild is being run within MinGW.windows: This is a Windows system.win32: This is a 32-bit Windows system.win64: This is a 64-bit Windows system.bsd: This is a BSD system.freebsd: This is a FreeBSD system.openbsd: This is a OpenBSD system.netbsd: This is a NetBSD system.darwin,osx: This is a Mac OS X system.
Some examples of sets returned by guess_platform would include
{'windows', 'win32'} and {'posix', 'linux'}.
Building C/C++ Files¶
A major portion of any build system usually revolves around C and C++. As a result, Fbuild provides a large amount of tools to help with this. Note that this section of the manual is not aimed at documenting every single feature, but it focuses on the important parts.
Builders, and the guess api¶
Let’s take a look at this example:
from fbuild.builders.c import guess
def build(ctx):
builder = guess.static(ctx)
lib = builder.build_lib('mylib', ['mylib.c'])
builder.build_exe('hello', ['hello.c'], libs=[lib])
Now that you know how builders are defined, I can start convering building C code. The
magic here is all in the function guess.static. The guess object has two members:
- guess.static: Returns a builder that can be used to build normal executables via
build_exeand static libraries viabuild_lib. - guess.shared: Returns a builder that can be used to build position-independent
executables (
-fPIC) viabuild_exeand and shared libraries viabuild_lib.
In this example, guess.static is used, so it returns a builder for building “normal”
executables and static libraries.
If you want both a static and a shared builder, you can use guess(...):
from fbuild.builders.c import guess
def build(ctx);
builders = guess(ctx)
static = builders.static.build_lib('mylib-static', ['mylib.c'])
shared = builders.shared.build_lib('mylib-shared', ['mylib.c'])
TODO
TODO¶
Document fbuild.config and more C/C++ compilation stuff.