I know this path has been trodden before, but for the last few days I've been so frustrated trying to move from a recursive to non-recursive makefile setup (yes, I read the wiki article
), that I came up with my own build system. I realized that 90% of all the makefiles I ever write is the same, and that making general rules for a large project is nearly impossible, or very messy looking. So I'm trying to make this system be able to handle large projects with very little, centralized configuration by allowing more "intelligent" general rulesets. As of now, this system is to be called "bake".
The first section of a bake configuration file is a set of type definitions. These definitions are used to classify different targets - information that is then used to choose the proper rule. Definitions are regular expressions (extended style, probably POSIX) that match the name of the file being classified. If multiple expressions match, the one that is most strict is chosen:
Code: Select all
source: "\.c"
object: "\.o"
header: "\.h"
binary: ""
Within this set of types, a file named "main.c" would be type source, a file named "main.o" would be type object, a file named "common.h" would be type header, and a file named "a.out" would be type binary. However, note that "libfoo.a" would also be type binary.
There are also environment variables, which work exactly the same as in make:
Code: Select all
CC = gcc
CFLAGS = -Wall -Werror -pedantic
CFLAGS += -Os
SOURCES = main.c
HEADERS = common.h
FILES = $(SOURCES) $(HEADERS)
Rules do not describe how to create a target, but instead how to convert a type of file (or multiple types of files) into a different type of file. Syntax is similar to make, but more general, and curly-brace instead of tabbed. The variables $ and $(#) (where # is a number) correspond to the produced file and argument number, respectively:
Code: Select all
# produce object from source(s)
object << source {
$(CC) $(CFLAGS) -c $1 -o $
}
# produce binary from object(s), rebuilding if headers are changed.
binary << object header {
$(LD) $(LDFLAGS) $1 -o $
}
Using these rules, bake figures out a way to produce a target from a set of files. It tries to keep things in as many files as possible at each point: in the last example, each source file would become one object file and the objects all linked together, as opposed to all the source files being compiled into one object file then linked.
To produce a target from a set of files, use this syntax:
Code: Select all
a.out - main.c common.h
# With auto-detection
a.out - "\.c" "\.h"
General rules for creating targets are also possible, implying the set of files needed to produce a target:
Code: Select all
binary - "\.c" "\.h"
all {
> a.out
}
At the end of the bake configuration file, files and targets can specify actions that must be performed before and after they are made, so that variables can be modified on a file-by file basis, and things can move around if needed. Variables modified by these scripts are only local - they do not affect the globally defined variables. Rules can also be overloaded from anywhere within. This syntax also provides a way to declare actions like "all" or "clean" or "install":
Code: Select all
all {
> a.out # The > means create target
}
main.c {
CFLAGS += -fomit-frame-pointer -O3
echo Building main.c
%% # This splits the "before" and "after" command lists
echo Built main.c
}
foo.o {
# Override source -> object rule for foo.c
object << source {
touch $
echo No object for you!
}
}
The fact that these actions are imperative means that the order of building can be preserved. However, it doesn't have to be. Actions separated by semicolons can be executed in parallel:
Code: Select all
all {
> foo.a; > bar.a
> foobar
echo Done
}
This builds foo.a and bar.a simultaneously, then foobar (which assumedly depends on them).
From the command line, bake acts like make. The name of the target or action is specified as the first argument. The configuration file will have a standard name, which is searched for, as well as an environment variable specifying a "standard" configuration file. This way, even if there is no config file in a directory, the build system can still be used provided a source directory with no special rules (just compile and link everything with no flags).
This is just a rough draft of the design, and I haven't written any code yet. Does anyone think this is practical and worthwhile to write? Any ideas for new/better features?