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 to 1.
  • --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, if host is specified, default to ${host}-ar, else to ar.
  • CC: C compiler, if host is specified, default to ${host}-gcc, else to cc.
  • CPP: C preprocessor, if host is specified, default to ${host}-cpp, else to cpp.
  • MKDIR: Create directories of targets, defaults to mkdir -p.
  • INSTALL: Install to use during the installations, defaults to install.
  • ARFLAGS: Flags given to AR, defaults to -rc.
  • ASFLAGS: Flags given to CC when compiling an assembler object file, default is empty.
  • CFLAGS: Flags given to CC when compiling a C object file, defaults to -fPIC -O2.
  • CPPFLAGS: Flags given to CC when compiling a C or assembler object file, default is empty.
  • LDFLAGS: Flags given to CC when linking object files, default is empty.
  • LDLIBS: Flags given to CC 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: Run host-bin, host-sbin, host-libexec, host-lib.
  • clean: $(RM) everything in the clean-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 from host-bin, host-sbin, host-libexec and shared libraries in their associated installation directories.
  • install-exec-strip: Same as install-exec except INSTALL is augmented with the -s option.
  • install: Performs install-data, install-devel and install-exec.
  • install-strip: Performs install-data, install-devel and install-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).