0298ec862e
`makeOverriddable` doesn't work for bundled sbclWith as is because it uses the `//` operator internally which doesn't work with the types `bundled` and `sbclWith` accept as arguments (string and list respectively). What's more, `bundled` already uses `makeOverridable` and allows to override the internal call to `library` via `overrideLisp`. For `sbclWith` no such mechanism exists, but this seems to be no concern for now: Using `overrideLisp` for this hasn't worked so far (and failed with a _hideous_ evaluation error), so there doesn't seem to be any real demand for this feature. Maybe a feature for another CL. Change-Id: I0b2f34c00a2143cd66dd43a6b1b2880af997ee50 Reviewed-on: https://cl.tvl.fyi/c/depot/+/3296 Tested-by: BuildkiteCI Reviewed-by: tazjin <mail@tazj.in>
260 lines
8.4 KiB
Nix
260 lines
8.4 KiB
Nix
# buildLisp provides Nix functions to build Common Lisp packages,
|
|
# targeting SBCL.
|
|
#
|
|
# buildLisp is designed to enforce conventions and do away with the
|
|
# free-for-all of existing Lisp build systems.
|
|
|
|
{ pkgs ? import <nixpkgs> {}, ... }:
|
|
|
|
let
|
|
inherit (builtins) map elemAt match filter;
|
|
inherit (pkgs) lib runCommandNoCC makeWrapper writeText writeShellScriptBin sbcl;
|
|
|
|
#
|
|
# Internal helper definitions
|
|
#
|
|
|
|
# 'genLoadLisp' generates Lisp code that instructs SBCL to load all
|
|
# the provided Lisp libraries.
|
|
genLoadLisp = deps: lib.concatStringsSep "\n"
|
|
(map (lib: "(load \"${lib}/${lib.lispName}.fasl\")") (allDeps deps));
|
|
|
|
# 'genCompileLisp' generates a Lisp file that instructs SBCL to
|
|
# compile the provided list of Lisp source files to $out.
|
|
genCompileLisp = srcs: deps: writeText "compile.lisp" ''
|
|
;; This file compiles the specified sources into the Nix build
|
|
;; directory, creating one FASL file for each source.
|
|
(require 'sb-posix)
|
|
|
|
${genLoadLisp deps}
|
|
|
|
(defun nix-compile-lisp (file srcfile)
|
|
(let ((outfile (make-pathname :type "fasl"
|
|
:directory (or (sb-posix:getenv "NIX_BUILD_TOP")
|
|
(error "not running in a Nix build"))
|
|
:name (substitute #\- #\/ srcfile))))
|
|
(multiple-value-bind (_outfile _warnings-p failure-p)
|
|
(compile-file srcfile :output-file outfile)
|
|
(if failure-p (sb-posix:exit 1)
|
|
(progn
|
|
;; For the case of multiple files belonging to the same
|
|
;; library being compiled, load them in order:
|
|
(load outfile)
|
|
|
|
;; Write them to the FASL list in the same order:
|
|
(format file "cat ~a~%" (namestring outfile)))))))
|
|
|
|
(let ((*compile-verbose* t)
|
|
;; FASL files are compiled into the working directory of the
|
|
;; build and *then* moved to the correct out location.
|
|
(pwd (sb-posix:getcwd)))
|
|
|
|
(with-open-file (file "cat_fasls"
|
|
:direction :output
|
|
:if-does-not-exist :create)
|
|
|
|
;; These forms were inserted by the Nix build:
|
|
${
|
|
lib.concatStringsSep "\n" (map (src: "(nix-compile-lisp file \"${src}\")") srcs)
|
|
}
|
|
))
|
|
'';
|
|
|
|
# 'genTestLisp' generates a Lisp file that loads all sources and deps and
|
|
# executes expression
|
|
genTestLisp = name: srcs: deps: expression: writeText "${name}.lisp" ''
|
|
;; Dependencies
|
|
${genLoadLisp deps}
|
|
|
|
;; Sources
|
|
${lib.concatStringsSep "\n" (map (src: "(load \"${src}\")") srcs)}
|
|
|
|
;; Test expression
|
|
(unless ${expression}
|
|
(exit :code 1))
|
|
'';
|
|
|
|
# 'dependsOn' determines whether Lisp library 'b' depends on 'a'.
|
|
dependsOn = a: b: builtins.elem a b.lispDeps;
|
|
|
|
# 'allDeps' flattens the list of dependencies (and their
|
|
# dependencies) into one ordered list of unique deps.
|
|
allDeps = deps: (lib.toposort dependsOn (lib.unique (
|
|
lib.flatten (deps ++ (map (d: d.lispDeps) deps))
|
|
))).result;
|
|
|
|
# 'allNative' extracts all native dependencies of a dependency list
|
|
# to ensure that library load paths are set correctly during all
|
|
# compilations and program assembly.
|
|
allNative = native: deps: lib.unique (
|
|
lib.flatten (native ++ (map (d: d.lispNativeDeps) deps))
|
|
);
|
|
|
|
# 'genDumpLisp' generates a Lisp file that instructs SBCL to dump
|
|
# the currently loaded image as an executable to $out/bin/$name.
|
|
#
|
|
# TODO(tazjin): Compression is currently unsupported because the
|
|
# SBCL in nixpkgs is, by default, not compiled with zlib support.
|
|
genDumpLisp = name: main: deps: writeText "dump.lisp" ''
|
|
(require 'sb-posix)
|
|
|
|
${genLoadLisp deps}
|
|
|
|
(let* ((bindir (concatenate 'string (sb-posix:getenv "out") "/bin"))
|
|
(outpath (make-pathname :name "${name}"
|
|
:directory bindir)))
|
|
(save-lisp-and-die outpath
|
|
:executable t
|
|
:toplevel (function ${main})
|
|
:purify t))
|
|
;;
|
|
'';
|
|
|
|
# Add an `overrideLisp` attribute to a function result that works
|
|
# similar to `overrideAttrs`, but is used specifically for the
|
|
# arguments passed to Lisp builders.
|
|
makeOverridable = f: orig: (f orig) // {
|
|
overrideLisp = new: makeOverridable f (orig // (new orig));
|
|
};
|
|
|
|
# 'testSuite' builds a Common Lisp test suite that loads all of srcs and deps,
|
|
# and then executes expression to check its result
|
|
testSuite = { name, expression, srcs, deps ? [], native ? [] }:
|
|
let
|
|
lispNativeDeps = allNative native deps;
|
|
lispDeps = allDeps deps;
|
|
in runCommandNoCC name {
|
|
LD_LIBRARY_PATH = lib.makeLibraryPath lispNativeDeps;
|
|
LANG = "C.UTF-8";
|
|
} ''
|
|
echo "Running test suite ${name}"
|
|
|
|
${sbcl}/bin/sbcl --script ${genTestLisp name srcs deps expression} \
|
|
| tee $out
|
|
|
|
echo "Test suite ${name} succeeded"
|
|
'';
|
|
|
|
#
|
|
# Public API functions
|
|
#
|
|
|
|
# 'library' builds a list of Common Lisp files into a single FASL
|
|
# which can then be loaded into SBCL.
|
|
library =
|
|
{ name
|
|
, srcs
|
|
, deps ? []
|
|
, native ? []
|
|
, tests ? null
|
|
}:
|
|
let
|
|
lispNativeDeps = (allNative native deps);
|
|
lispDeps = allDeps deps;
|
|
testDrv = if ! isNull tests
|
|
then testSuite {
|
|
name = tests.name or "${name}-test";
|
|
srcs = srcs ++ (tests.srcs or []);
|
|
deps = deps ++ (tests.deps or []);
|
|
expression = tests.expression;
|
|
}
|
|
else null;
|
|
in lib.fix (self: runCommandNoCC "${name}-cllib" {
|
|
LD_LIBRARY_PATH = lib.makeLibraryPath lispNativeDeps;
|
|
LANG = "C.UTF-8";
|
|
passthru = {
|
|
inherit lispNativeDeps lispDeps;
|
|
lispName = name;
|
|
lispBinary = false;
|
|
tests = testDrv;
|
|
sbcl = sbclWith [ self ];
|
|
};
|
|
} ''
|
|
${if ! isNull testDrv
|
|
then "echo 'Test ${testDrv} succeeded'"
|
|
else "echo 'No tests run'"}
|
|
${sbcl}/bin/sbcl --script ${genCompileLisp srcs lispDeps}
|
|
|
|
echo "Compilation finished, assembling FASL files"
|
|
|
|
# FASL files can be combined by simply concatenating them
|
|
# together, but it needs to be in the compilation order.
|
|
mkdir $out
|
|
|
|
chmod +x cat_fasls
|
|
./cat_fasls > $out/${name}.fasl
|
|
'');
|
|
|
|
# 'program' creates an executable containing a dumped image of the
|
|
# specified sources and dependencies.
|
|
program =
|
|
{ name
|
|
, main ? "${name}:main"
|
|
, srcs
|
|
, deps ? []
|
|
, native ? []
|
|
, tests ? null
|
|
}:
|
|
let
|
|
lispDeps = allDeps deps;
|
|
libPath = lib.makeLibraryPath (allNative native lispDeps);
|
|
selfLib = library {
|
|
inherit name srcs native;
|
|
deps = lispDeps;
|
|
};
|
|
testDrv = if ! isNull tests
|
|
then testSuite {
|
|
name = tests.name or "${name}-test";
|
|
srcs =
|
|
(
|
|
srcs ++ (tests.srcs or []));
|
|
deps = deps ++ (tests.deps or []);
|
|
expression = tests.expression;
|
|
}
|
|
else null;
|
|
in lib.fix (self: runCommandNoCC "${name}" {
|
|
nativeBuildInputs = [ makeWrapper ];
|
|
LD_LIBRARY_PATH = libPath;
|
|
LANG = "C.UTF-8";
|
|
passthru = {
|
|
lispName = name;
|
|
lispDeps = [ selfLib ] ++ (tests.deps or []);
|
|
lispNativeDeps = native;
|
|
lispBinary = true;
|
|
tests = testDrv;
|
|
sbcl = sbclWith [ self ];
|
|
};
|
|
} ''
|
|
${if ! isNull testDrv
|
|
then "echo 'Test ${testDrv} succeeded'"
|
|
else ""}
|
|
mkdir -p $out/bin
|
|
|
|
${sbcl}/bin/sbcl --script ${
|
|
genDumpLisp name main ([ selfLib ] ++ lispDeps)
|
|
}
|
|
|
|
wrapProgram $out/bin/${name} --prefix LD_LIBRARY_PATH : "${libPath}"
|
|
'');
|
|
|
|
# 'bundled' creates a "library" that calls 'require' on a built-in
|
|
# package, such as any of SBCL's sb-* packages.
|
|
bundled = name: (makeOverridable library) {
|
|
inherit name;
|
|
srcs = lib.singleton (builtins.toFile "${name}.lisp" "(require '${name})");
|
|
};
|
|
|
|
# 'sbclWith' creates an image with the specified libraries /
|
|
# programs loaded.
|
|
sbclWith = deps:
|
|
let lispDeps = filter (d: !d.lispBinary) (allDeps deps);
|
|
in writeShellScriptBin "sbcl" ''
|
|
export LD_LIBRARY_PATH="${lib.makeLibraryPath (allNative [] lispDeps)}"
|
|
export LANG="C.UTF-8"
|
|
exec ${sbcl}/bin/sbcl ${lib.optionalString (deps != []) "--load ${writeText "load.lisp" (genLoadLisp lispDeps)}"} $@
|
|
'';
|
|
in {
|
|
library = makeOverridable library;
|
|
program = makeOverridable program;
|
|
inherit sbclWith bundled;
|
|
}
|