This project contains build rules and toolchain for building Common Lisp code with SBCL using Bazel.
These rules are intended to be compatible with the latest released version of Bazel, currently 4.2.1.
See also the full
Stardoc generated documentation
for lisp_library, lisp_binary, and lisp_test.
The general structure of the actions for these build rules is:
LispCompile: All libraries are built in parallel, loading transitive Lisp source files (not FASL outputs) for their dependencies, producing a single combined FASL for each target'ssrcs.LispCore: All the FASLs are loaded and a core file is written withsave-lisp-and-die.LispElfinate: The core is split into a file containing assembly code representing the compiled Lisp code and an object file containing the remainder of the Lisp image.CppCompile: The assembly code is reassembled.CppLink: Everything is linked together with any C++ dependencies (fromlisp_*.cdeps) and the SBCL runtime to produce an ELF-format binary.
digraph {
subgraph cluster_dep {
LispDep [label="dep.lisp", shape="box"]
LispCompileDep [label="LispCompile", group="dep"]
DepFasl [label="dep.fasl", shape="box"]
}
subgraph cluster_consumer {
LispConsumer1 [label="consumer1.lisp", shape="box"]
LispConsumer2 [label="consumer2.lisp", shape="box"]
LispCompileConsumer1 [label="LispCompile"]
LispCompileConsumer2 [label="LispCompile"]
LispConcatFASLsConsumer [label="LispConcatFASLs"]
ConsumerFasl [label="consumer.fasl", shape="box"]
}
LispDep -> LispCompileDep
LispDep -> LispCompileConsumer1
LispDep -> LispCompileConsumer2
LispConsumer1 -> LispCompileConsumer1
LispConsumer2 -> LispCompileConsumer2
LispCompileConsumer1 -> LispConcatFASLsConsumer
LispCompileConsumer2 -> LispConcatFASLsConsumer
LispCompileDep -> DepFasl
LispConcatFASLsConsumer -> ConsumerFasl
DepFasl -> LispCore
ConsumerFasl -> LispCore
}lisp_library, lisp_binary, and lisp_test generate LispCompile actions
for each file in srcs.
Before compilation, the compilation image loads all of the srcs of the
targets' transitive deps. Note that this means lisp_library targets do not
depend on the output of their deps, so a lisp_library target and its deps
may be compiled in parallel.
If a target has multiple files in srcs, other files in srcs may also be
loaded, depending on the value of order:
"serial"(the default): Previous files insrcsare loaded in order. If multiple files must be loaded in order, it is recommended to add a comment noting this to the top of the target'ssrcsin the BUILD file, so that the order is not inadvertently changed by editors or automatic formatters. If files insrcsdo not depend on one another, settingorderto"parallel"avoids unnecessary work."parallel": No other files insrcsare loaded."multipass": All files insrcsare loaded. This can be necessary when there are circular compile-time dependencies between files, but should be avoided when possible.
Note that "parallel", "serial", and "multipass" describe dependency structure,
not the parallelization of LispCompile actions. In all cases, the compilation
actions for the files in srcs can be performed in parallel.
Prior to loading dependencies, symbols are added to *features* to represent
some things about the build:
:bazelto distinguish this from other build systems:optif--compilation_mode(-c) is set toopt:dbgif--compilation_modeis set todbg- Features specified by the
add_featuresattribute of the target and its Lisp dependencies
Dependencies are loaded with #'load-file with the optimize qualities:
speed |
debug |
safety |
space |
compilation-speed |
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 3 |
After all dependencies are loaded, each compilation action compiles a file in
srcs into a FASL file with #'compile-file, with optimize qualities depending
on the setting of --compilation_mode (-c), which defaults to fastbuild:
-c |
speed |
debug |
safety |
space |
compilation-speed |
|---|---|---|---|---|---|
fastbuild |
1 | 2 | 3 | 1 | 1 |
opt |
3 | 0 | 0 | 1 | 1 |
dbg |
1 | 3 | 3 | 1 | 1 |
When run with the coverage command (or --collect_code_coverage), the
optimize quality sb-c:store-coverage-data is also set to 3.
Default outputs and runfiles from a target's compile_data and the
compile_data of its transitive Lisp dependencies are provided as additional
inputs to the LispCompile action.
If there is at least one file in srcs, the FASLs are concatenated into
[name].fasl, which is the default output for lisp_library. If there are
multiple files in srcs, this requires an additional LispConcatFASLs action.
A lisp_library with no srcs has no default outputs.
digraph {
LispCompile1 [label="LispCompile 1"];
LispCompileMore [label="LispCompile ..."];
LispCompileN [label="LispCompile N"];
SBCLRuntime [label="SBCL C++ Runtime", shape="box"];
CDeps [label="C++ Dependencies", shape="box"];
LispCompile1 -> LispCore
LispCompileMore -> LispCore
LispCompileN -> LispCore
LispCore -> LispElfinate
LispElfinate -> CppCompile
LispElfinate -> CppLink
SBCLRuntime -> CppLink
CDeps -> CppLink
CppCompile -> CppLink
}For lisp_binary and lisp_test rules, the rule also generates an executable
output, which is the default output for those rules. By default, that executable
runs the function or code specified in the main attribute (which can be
overridden by LISP_MAIN). That defaults to cl-user::main.
Executable generation happens in several actions. First, LispCore:
- Loads all of the combined FASLs for each target in the target's transitive
deps, as well as the one for the target itself. - Binds
bazel.main::*entry-point*to the function specified inmain(or a newly-generated function which executes the code specified inmain, ifmaincontains a snippet of code instead of a single symbol name). - Generates a Lisp core file with
#'sb-ext:save-lisp-and-die, specifying the:toplevelfunctionbazel.main::restart-image. That in turn runs themainfunction by default. If the environment variableLISP_MAINis specified, it runs the function named by that (by default in thecl-userpackage) instead.LISP_MAIN=torLISP_MAIN=nilstarts the default REPL, as does setting LISP_MAIN to anything that does not name a defined function.
The core file is split up with in a LispElfinate action, then combined with
the transitive C++ dependencies (the SBCL runtime and dependencies provided to
the target and its transitive deps via cdeps) to generate an executable in a
CppLink action.
By default, the LispElfinate action splits the core file into a .s file
containing assembly for the compiled Lisp code and a .o file representing the
remainder of the Lisp core. The former is compiled with a CppCompile action,
then both are fed into CppLink.
The advantage of this approach is that the binary ends up in a standard ELF format. This allows tools which analyze the call-stack of C++ code to get human-readable stacktraces for combined C++ and Lisp code. While this obviously won't let a C++ debugger step through Lisp code, it's useful for CPU profiling and crash analysis.
The disadvantage of this approach is that the altered binary format is not
understood by save-lisp-and-die. Thus, if the allow_save_lisp attribute is
set to True, LispElfinate instead transforms the entire Lisp core into a
binary blob in a .o file plus a linker script informing the CppLink action
on how that should be linked. The result is technically ELF format, but the Lisp
portion is opaque. SBCL can operate on that Lisp portion with
save-lisp-and-die, making the executable suitable for use as a compilation
image.
The rules provide a few legacy build variables, specified with --define, which
control these rules' behavior.
LISP_COMPILATION_MODE overrides --compilation_mode (-c) for LispCompile
and LispCore actions. For example, -c opt --define=LISP_COMPILATION_MODE=fastbuild would run the build in opt mode,
except the LispCompile and LispCore actions would behave as they would in
fastbuild mode.
LISP_BUILD_FORCE runs LispCompile and LispCore actions that try to proceed
despite errors to the greatest extent possible.
VERBOSE_LISP_BUILD overrides the setting of the verbose attribute if set to
a higher value.
When build steps fail, passing
--verbose_failures
at the Bazel CLI will result in the full command-line being printed for the
failing actions. (Or
--subcommands
can be used to print all action command-lines.)
When rerunning the command for a LispCompile or LispCore action locally,
the --verbose flag can be set to 3 to provide maximum debugging output.
The --interactive flag can also be passed to the compilation image to enable
the interactive debugger, which the compilation image disables by default.
These rules
provide
LispInfo (defined in provider.bzl) to propagate information about Lisp
dependencies. LispInfo.cc_info contains a
CcInfo
providing information about transitive C++ dependencies. CcInfo is not
provided directly, because the targets instantiated by these rules cannot be
used as C++ dependencies.
These rules also provide
OutputGroupInfo
with a fasl field, containing the combined FASL file ([name].fasl) for the
target's immediate srcs, if srcs is non-empty. That is also the default
output
(DefaultInfo.files)
of lisp_library. For lisp_binary and lisp_test, the default output is the
executable (same name as the target).
If the flag --//:additional_dynamic_load_outputs is
passed, OutputGroupInfo has the following additional fields:
deps_manifestadditionally contains a plaintext representation of the target's transitive Lisp features (prefixed withfeature:) and transitive source files (prefixed withsrc:).dynamic_librarycontains a shared object file with the target's transitive C++ dependencies.
Runfiles
(DefaultInfo.default_runfiles)
are propagated from all dependencies that provide either runtime dependencies or
source files (image, deps, cdeps, data, compile_data, srcs) and
further populated from the default outputs of data and compile_data.