TODOs
While everyone is welcome to download, experiment with and submit patches for Fabrique, you probably shouldn’t consider it “ready for prime time” until the following TODO items have been resolved:
Interface types
Currently, we can define name-to-value aggregates (“structures”), either directly or by importing another Fabrique file. These aggregates have types, but they’re not easily described by other code (e.g., a function) that expects to receive a certain aggregate type. What we want is to be able to express:
# Information required to link against a library.
struct LibraryConfig
{
definitions:list[string];
flags:list[string];
libraries:list[string];
include_directories:list[file];
library_directories:list[file];
};
compile = function(srcs:list[file], config:list[LibraryConfig]) # ...
libfoo = import('libfoo.fab');
libbar = import('libbar.fab');
obj = compile(files(a.c b.c), [ libfoo.config libbar.config ]);
Plugins
Many build configuration systems run arbitrary shell scrips and parse their outputs to get configuration data. This makes repeatability difficult. Fabrique will use a different approach, with two sources of configuration data: static, descriptive Fabrique files and dynamic but repeatable plugins.
In the first approach, descriptive Fabrique files are installed alongside the packages they describe. This approach is one of the most useful intellectual contributions that CMake has made to the field (even if its execution is poor). For instance, /usr/local/share/fabrique/libfoo.fab
might contain:
config = function(feature_a:bool, feature_b = false): LibraryConfig
{
need_common_dependency = (feature_a or feature_b);
struct
{
definitions =
if (need_common_dependency) [ 'USING_DEP_FOO' ] else []
+ if (feature_a) [ 'FEATURE_A' ] else []
+ if (feature_b) [ 'FEATURE_B' ] else [ 'NO_FEATURE_B' ]
;
flags:list[string] = [];
libraries = [ 'foobase' ]
+ if (need_common_dependency) [ 'foocommon' ] else []
+ if (feature_a) [ 'foo_a' ] else []
+ if (feature_b) [ 'foo_b' ] else []
;
include_directories = [ file('/usr/local/include/libfoo') ];
library_directories = [ file('/usr/local/lib') ];
}
}
Some configuration, however, needs to be dynamic: it depends on properties of the host as it is currently running. We also need general-purpose tools to work around software that doesn’t provide static Fabrique-style configuration. For these situations, we need plugins.
In Fabrique, build configuration will be done with the assistance of compiled plugins. These plugins will be dynamically-linked libraries that Fabrique can load, but their outputs will be recorded so that we can later “play back” a configuration run, possibly for debugging on another machine. Plugins that we see an immediate need for include:
which
: find the firstfoo
on the current$PATH
sysctl
: on UNIX platforms, get a (typed) value for a given namepkg_config
: wrap thepkg-config
tool for packages that don’t supply Fabrique files
Standard library
Before it’s widely useful, Fabrique needs a richer standard library. For instance, a compiler driver function might want to take a list of warnings concatenated from several libraries, eliminate duplicates, search for logical inconsistencies (foo
and no-foo
) and transform the result into some compiler-specific representation. This should all be achievable with standard-library list operations.
Other minor things
These things would make Fabrique nicer to work with, but may not be prerequisites to getting useful work done.
Constant expressions
It would be nice to have a concept of statically-evaluable expressions, possibly together with an explicit constexpr
qualifier, so that we could use expressions rather than just constants in import
statements:
foo = import(join_path(some_directory, base_name + '.fab'));
Filenames
We might need to provide direct access to generated files’ names:
tex_basename = texfile.name[-3:];
auxfile = tex_basename + '.aux';
output_pdf = tex_basename + '.pdf';
More warnings
We should warn about more potentially-unsafe behaviours:
- variables with embedded variable references
- the output order of variables is undefined and can be significant (e.g. in Ninja)
- generating files in
buildroot
with the same name as input files insrcroot
- targets with the same names as files (e.g.,
foo = link(objs, output = file('foo'));
)
Objects
It might be nice to make aggregates into more complete objects through a Python- or Go-like approach to methods:
foo = struct
{
bar = function(self, x:int, y:int) # ...
};
foo.bar(1, 2);
Then maybe inheritance? Mixins could be useful (e.g., a Clang
object is a CCompiler
and also incorporates the ProducesDependencyFiles
mixin).