bconf
Build configurations, easier. bconf is a tool which purpose is to make release of a software sources easy to create and distribute.
It generates a configure
shell script,
which in turn creates a GNUmakefile
for GNU Make.
The generated configure script should be POSIX-compliant
while the configure's generated Makefile targets GNU Make,
which is available on most UNIX-like systems.
It aims to provide the following functionalities:
- Makefile-based and flexible build-system during development.
- Overridable set of rules and recipes for C software and libraries.
- Sane default set of rules and recipes for 21st century C software and libraries.
- Source release archive with a standard build method: The infamous
./configure && make install
. - Mimic the GNU
configure
script behaviour, not exactly, but enough not to traumatize package maintainers. - Enhance components installation selection. eg: if a package maintainer only wants headers, or binaries.
- Support builds on the build machine.
- Support out of source tree builds.
- Support cross compilation.
Why?
bconf is a small project born from the frustration of other alternatives.
As of the time of writing (2024), the C build ecosystem is still a gigantic mess. The current serious and portable alternatives are autotools, meson or cmake.
GNU autotools are very powerful but extremely difficult to master. Learning resources are scarce. And even if you finally tame the beast, you're now a part of a small group of programmers who are able to maintain these scripts/projects. Its main benefit is that generated artifacts are extremely portable, and thus building a source code release is always really simple.
CMake is a ninja or Makefile generator frontend. CMake is extremely configurable. Just like autotools, mastering it requires arcanes behind most humans patience. However, its main issue is lacking the ability to easily build a program on the build machine when cross compiling.
Meson is a ninja generator frontend. I won't go into details, but the benefits of ninja are only seen on huge projects when you have a stupidly powerful build machine. Meson features are as restrictive as its syntax. For simple workflows, where you just build C files, it is ok, great even, as it is one of the only alternative to support cross-compilation transparently. My first issue with meson is that when you stray from the path, creating a custom rule and having no functions in the language to generalize and factorize the build infrastructure quickly becomes a pain. My second issue is its run-time dependency on python. Meson versions need specific versions of python on the build machine to work, and we all know how managing python versions goes...
So basically, I desired the flexibility of Makefiles with the portability of autotools-generated artifacts. And that's exactly what bconf is, a configuration script with a small GNU Make infrastructure. A small and simple autotools.
Now, bconf is not a silver bullet. It is a way better and simpler alternative for small projects and sources you want to distribute. It's not meant to have optimized Makefile rules, and if the build time becomes a problem, you may want to look at alternatives. The goal of bconf is simplicity to use during development, source code releases, and source code configuration/build/install.
Compiling
bconf being built with bconf, if you build from SCM, you must first bootstrap it from the source directory, and generate the configure script. You'll need, lex, yacc and a working C compiler to do so:
make
./mkconf
Then, configure the build and remove the bootstrap artifacts. You can now build a clean version of bconf:
./configure
make clean
make
Copying
bconf is released under a BSD-3-Clause license, see the LICENSE file which should also have been redistributed with the sources.
configure.in and GNUmakefile.in templates are also redistributed under the previous license. Exception made for bconf's generated configuration/GNUmakefile files, which are redistributed under the CC0 license.
Introduction
bconf is meant to be used by different kind of actors. The main one is the software developer, who writes the rules because well, he is the one writing the software. The second role is the code owner, it designates the person who creates the source code release, and uses bconf to generate the configure script that will ship to the end-user. The last role is the package maintainer, which is a wide term for the person using the source code release created by the previous code owner. The package maintainer uses bconf when he builds the source code on his local machine (or a more complex automated infrastructure).
Software developer
The software developer is the role who will spend the most
time with bconf, because he is the one responsible of describing
how the source code is built, and what are the configuration options.
He will use bconf like a code owner in the sens that he will
use the mkconf
tool to perform local builds and iterate.
This user guide is mainly aimed at software developers.
The software developer must:
- Describe the build configuration, using the
bconf
file. - Describe the GNUmakefile targets and rules, using the
bconf.mk
file.
Code owner
The code owner is the one person (or automated infrastructure)
who performs the operation of creating a source release tarball
from the SCM.
In a manner reminiscent of autoreconf
, he must perform
the mkconf
command to create the configure
script
which will ship with the source release tarball.
The mkconf
command cannot just be typed randomly.
The code owner must choose a template set compatible with the source code.
See the chapter dedicated to template sets for more informations.
He may also specify the name and the version of the package, which
will be forwarded to bconf.mk
through the variable $(package-name)
and $(package-version)
.
Note the software developer must also perform the previous tasks for local builds,
even though the name is usually inferred from the source directory name, and the version is unused.
The code owner must:
- Specify the package name, version and description.
- Generate the
configure
script for package maintainers.
Package maintainer
The package maintainer is the one person (or automated infrastructure)
who uses the source code release created by the code owner.
Basically, the package maintainer shouldn't have to install anything
apart from GNU make. He will interact with the configure
script,
specifying options and relaying environment variables.
The configure
script in turn generates the GNUmakefile
which will rely on the bconf.mk
definitions and allow
simple usage of the command make
to compile, test, install, etc...
The package maintainer must:
- Extract the archive, patch, configure, build and install sources without installing non-POSIX utility.
- Change, enable or disable package features when calling the
configure
script.
Example
In this chapter, we will create an example project and showcase how the different roles introduced in the previous chapter come in.
Hello World
First, create a source code with our desired hello world program, hello.c
:
#include <stdio.h>
int
main(void) {
puts("Hello, world!");
return 0;
}
Next, we need to define the targets, in bconf.mk
:
hello: hello.o
host-bin+=hello
clean-up+=$(host-bin) hello.o
Now, create the configure
script:
In a shell, in the same directory as our bconf.mk
and hello.c
files:
mkconf
Finally, you can configure, compile and run the example:
./configure
make
./hello
This last step can be performed outside of the source code directory tree!
Configuration options
Now, let's introduce the reason why bconf exists: build configuration.
In the source tree, create a new file called bconf
:
config GREETING
"String to print for the hello program"
defaults "Hello, world!"
Modify the source code hello.c
:
#include <stdio.h>
int
main(void) {
puts(CONFIG_GREETING);
return 0;
}
And modify the bconf.mk
:
hello: hello.o
hello.o: CPPFLAGS+=-DCONFIG_GREETING='"$(CONFIG_GREETING)"'
host-bin+=hello
clean-up+=$(host-bin) hello.o
Don't forget to re-generate the configure
script:
mkconf
In your build directory, don't forget to clean the previous artifacts,
and run configure
once again to generate an up-to-date GNUmakefile
:
make clean
./configure --with-greeting="Hello, ${LOGNAME}\!"
make
./hello
You should now have a custom hello message due to our build configuration.
Template Sets
As explained in the introduction, the mkconf
command
might require extra arguments depending on the context and the project.
The mkconf
generates one artifact, the configure
script, a POSIX-shell script.
Yet indirectly generates two artifacts as the configure
script in turns generate
the GNUmakefile
which will be used when building the programs/artifacts.
A template set is a named pair of templates for both the configure
and GNUmakefile
files.
This document won't detail the internals of said templates nor how to write them, but the
features provided by the template sets provided by the default bconf distribution.
NOTE: bconf is still in an experimental stage. While basic features and functionalities work, syntax and features are subject to future changes and bug fixes.
configure
The configure
scripts are close to what you would find everywhere else,
they take arguments of the form --${name}=${value}
, and if an argument
has the form ${name}=${value}
, evaluates it (useful for override, eg. CC=gcc
).
While in the script, for a config entry, its name is lowercased and _
s are changed into -
s.
Three extra arguments are added to the configure
script, where ${config}
is the transformed config name:
--enable-${config}
: The variable associated to${config}
is set to1
.--with-${config}=${value}
: The variable associated to${config}
is set to${value}
.--without-${config}
,--disable-${config}
: The variable associated to${config}
is unset.
The following variables are forwarded to the GNUmakefile
:
AR
: Archiver for static libraries, ifhost
is specified, default to${host}-ar
, else toar
.CC
: C compiler, ifhost
is specified, default to${host}-gcc
, else tocc
.CPP
: C preprocessor, ifhost
is specified, default to${host}-cpp
, else tocpp
.MKDIR
: Create directories of targets, defaults tomkdir -p
.INSTALL
: Install to use during the installations, defaults toinstall
.ARFLAGS
: Flags given toAR
, defaults to-rc
.ASFLAGS
: Flags given toCC
when compiling an assembler object file, default is empty.CFLAGS
: Flags given toCC
when compiling a C object file, defaults to-fPIC -O2
.CPPFLAGS
: Flags given toCC
when compiling a C or assembler object file, default is empty.LDFLAGS
: Flags given toCC
when linking object files, default is empty.LDLIBS
: Flags given toCC
when linking object files, default is empty.
GNUmakefile
The GNUmakefile
exposes the bconf
entries in the form CONFIG_$(name)
where $(name)
is the name of the config entry as given in the bconf
file.
All GNU-make default suffixes rules are removed and replaced by bconf's rules
to avoid any conflicts. For more details, either see the associated
template set's informations or directly the generated GNUmakefile
source.
The GNUmakefile
heavily relies on the VPATH
feature of GNU-make
to transparently serve out-of-source builds, it is recommended to follow
this guide's recommandations to avoid tricky issues and best serve this use-case.
The package's name and version given during invocation of mkconf
are
available under the package-name
and package-version
variables respectively.
The absolute path to the configure
script's directory is available in srcdir
.
The absolute path to the GNUmakefile
makefile's directory is available in objdir
.
The target triple of CC
is available in the host
variable, while the
architecture of said triple should be extracted in the host-arch
variable.
Following the GNU guidelines, default installation directories are:
prefix
: Defaults to/usr/local
.exec_prefix
: Defaults to$(prefix)
.bindir
: Defaults to$(exec_prefix)/bin
.sbindir
: Defaults to$(exec_prefix)/sbin
.libexecdir
: Defaults to$(exec_prefix)/libexec
.libdir
: Defaults to$(exec_prefix)/lib
.datarootdir
: Defaults to$(prefix)/share
.datadir
: Defaults to$(datarootdir)/$(package-name)
.sysconfdir
: Defaults to$(prefix)/etc
.sharedstatedir
: Defaults to$(prefix)/com
.localstatedir
: Defaults to$(prefix)/var
.runstatedir
: Defaults to$(localstatedir)/run
.includedir
: Defaults to$(prefix)/include
.docdir
: Defaults to$(datarootdir)/doc/$(package-name)
.infodir
: Defaults to$(datarootdir)/info
.mandir
: Defaults to$(datarootdir)/man
.htmldir
: Defaults to$(docdir)/$(package-name)
.pdfdir
: Defaults to$(docdir)/$(package-name)
.
The GNUmakefile
always present the following PHONY rules:
all
: Runhost-bin
,host-sbin
,host-libexec
,host-lib
.clean
:$(RM)
everything in theclean-up
variable.install-data
: Does nothing out-of-the-box. Should be augmented for docs, manpages, etc...install-devel
: Installs static libraries. Should be augmented for headers, pkg-configs, etc...install-exec
: Installs binaries fromhost-bin
,host-sbin
,host-libexec
and shared libraries in their associated installation directories.install-exec-strip
: Same asinstall-exec
exceptINSTALL
is augmented with the-s
option.install
: Performsinstall-data
,install-devel
andinstall-exec
.install-strip
: Performsinstall-data
,install-devel
andinstall-exec-strip
.
Advanced guide
This section provides advanced examples of common operations, and how to correctly perform them using bconf to avoid issues.
Building executables
A binary executable is built in two steps:
- Compile the source code into object files.
- Link the object files into a binary executable.
The bconf rule to compile object file matches %o: %.c
, so this rule is mostly used implicitly.
The link rule, on the other hands, matches all elements of the host-bin
variable.
Thus, to declare a binary executable, define its binary executable in host-bin
and extend its dependencies to include all its object files:
hello: hello.o # hello depends on hello.o, which in turns depends on hello.c.
host-bin+=hello # Link as host binary executable.
Building libraries
Like executables, libraries are built in two steps, compiling objects, and then linking them. The compiled binary objects are the same kind of artifacts used in building executables. The difference occurs during the link operation, where different rules are applied.
The link rule matches all elements in the host-lib
variable, however, a different
rule is applied depending on whether the element matches %.a
or matches %.$(ld-so)
.
The ld-so
is a variable defined in the GNUmakefile
specifying the extension of shared
libraries (for now only so
, even on Darwin-based systems). Overriding the value of
ld-so
to a
is an implicit way to deactivate shared libraries generation.
foo-libs:=libfoo.a # libfoo supports static linking.
ifneq ($(ld-so),a) # If dynamic linking supported, add shared libfoo.
foo-libs+=libfoo.$(ld-so)
endif
$(foo-libs): foo.o # For static libfoo, and maybe shared libfoo, define dependencies.
host-lib+=$(foo-libs) # Link all libfoo variations.
Build flags, linking with a library
To specify build options, bconf
relies on overrides
and extensions of variables for specific targets:
# Expanding on the previous example:
bar-objs:=bar.o baz.o # Define all object files for a target.
$(bar-objs): CPPFLAGS+=-I$(srcdir)/include # Add headers from the source directory.
$(bar-objs): CFLAGS+=-std=c11 # Override the C standard used by the compiler when building
# Note here we override for $(bar-objs) instead of bar, that's because
# `make bar.o` wouldn't propagate the overrides from the `bar` target.
ifneq ($(CONFIG_BAR_OPTION),)
bar.o: CPPFLAGS+=-DCONFIG_BAR_OPTION='$(CONFIG_BAR_OPTION)'
# Only bar.o will receive this option during compilation.
endif
bar: $(bar-objs) # bar target objects require for linking
bar: LDFLAGS+=-Wl,-rpath='$ORIGIN/../lib' # Passing an option to the linker through the compiler driver.
bar: LDLIBS+=libfoo.$(ld-so) # bar will link with either static or shared libfoo.
host-bin+=bar # Link as host binary executable.
Cleaning artifacts
To avoid confusion between what can be cleaned and what cannot, bconf
leaves
the PHONY clean
target choice of artifacts entirely to the end user. All objects
specified in the clean-up
variable will be removed on running the clean
target.
# Expanding on the previous example:
clean-up+=$(bar-objs) $(host-bin) $(host-lib)
Compiling assembler files
Assembler files are compiled from the rule matching
all %.o: %.S
from the host-$(host-arch)
variable:
# Pre-supposing that an x86_64 assembly file asm.S exists in either $(objdir) or $(srcdir):
host-x86_64+=asm.o
# Now if $(host-arch) is x86_64, asm.S will be compiled with the `CC` compiler driver.
The uppercase .S
extension is linked to GCC (and other C compilers) interpreting
the file either as needing C preprocessing (with uppercase), or not (with a lowercase).
In the case of bconf
, uppercase is chosen as the default as a non-preprocessed file
can be compiled with the preprocessor while the other way is not always valid.
Custom rules
Just like any Makefile
, you can write rules in the bconf.mk
:
# Dependencies are resolved either from the current working directory,
# or from the $(srcdir) thanks to the $(VPATH) feature of GNU-make, always
# reference them by $^ or $< so GNU-make resolves them correctly.
.PHONY: install-mandir
# Install manpages
install-mandir: man/bar.1
$(v-e) INSTALL $(notdir $^)
$(v-a) $(INSTALL) -d -- "$(DESTDIR)$(mandir)"
$(v-a) $(INSTALL-DATA) -- $^ "$(DESTDIR)$(mandir)"
# Extend the empty install-data PHONY target to install runtime documentation.
install-data: install-mandir
The v-e
and v-a
macros work differently depending on whether
the variable V
is set to 1
or not, if V
is equal to 1
,
$(v-e)
prefixed directives are ignored and $(v-a)
prefixed ones
are printed as usual. If not, $(v-e)
echoes the following string,
and $(v-a)
executes the command silently (verbose or fancy output).