By implementing a bundled function for an implementation, we can use a custom one for a specific implementation. This is useful for implementations like ECL where a require will be compiled as an instruction rather than importing all new symbols into a dump, so using the underlying static or shared object directly would be beneficial. overrideLisp for bundled libraries now only allows overriding the name and implementation arguments. Change-Id: I9036b29157e8daa4d86ff87d603b044373711dbf Reviewed-on: https://cl.tvl.fyi/c/depot/+/3301 Tested-by: BuildkiteCI Reviewed-by: tazjin <mail@tazj.in>
369 lines
13 KiB
Nix
369 lines
13 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
|
|
#
|
|
|
|
defaultImplementation = "sbcl";
|
|
|
|
# Generates lisp code which instructs the given lisp implementation to load
|
|
# all the given dependencies.
|
|
genLoadLispGeneric = impl: deps:
|
|
lib.concatStringsSep "\n"
|
|
(map (lib: "(load \"${lib}/${lib.lispName}.${impl.faslExt}\")")
|
|
(allDeps impl deps));
|
|
|
|
# 'genTestLispGeneric' generates a Lisp file that loads all sources and deps
|
|
# and executes expression for a given implementation description.
|
|
genTestLispGeneric = impl: { name, srcs, deps, expression }: writeText "${name}.lisp" ''
|
|
;; Dependencies
|
|
${impl.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 which
|
|
# all use the given implementation.
|
|
allDeps = impl: deps: let
|
|
# The override _should_ propagate itself recursively, as every derivation
|
|
# would only expose its actually used dependencies
|
|
deps' = builtins.map (dep: dep.overrideLisp or (lib.const dep) (_: {
|
|
implementation = impl.name;
|
|
})) deps;
|
|
in (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))
|
|
);
|
|
|
|
# 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 ? [], impl }:
|
|
let
|
|
lispNativeDeps = allNative native deps;
|
|
lispDeps = allDeps impl deps;
|
|
in runCommandNoCC name {
|
|
LD_LIBRARY_PATH = lib.makeLibraryPath lispNativeDeps;
|
|
LANG = "C.UTF-8";
|
|
} ''
|
|
echo "Running test suite ${name}"
|
|
|
|
${impl.runScript} ${
|
|
impl.genTestLisp {
|
|
inherit name srcs deps expression;
|
|
}
|
|
} | tee $out
|
|
|
|
echo "Test suite ${name} succeeded"
|
|
'';
|
|
|
|
# 'impls' is an attribute set of attribute sets which describe how to do common
|
|
# tasks when building for different Common Lisp implementations. Each
|
|
# implementation set has the following members:
|
|
#
|
|
# Required members:
|
|
#
|
|
# - runScript :: string
|
|
# Describes how to invoke the implementation from the shell, so it runs a
|
|
# lisp file as a script and exits.
|
|
# - faslExt :: string
|
|
# File extension of the implementations loadable (FASL) files.
|
|
# Implementations are free to generate native object files, but with the way
|
|
# buildLisp works it is required that we can also 'load' libraries, so
|
|
# (additionally) building a FASL or equivalent is required.
|
|
# - genLoadLisp :: [ dependency ] -> string
|
|
# Returns lisp code to 'load' the given dependencies. 'genLoadLispGeneric'
|
|
# should work for most dependencies.
|
|
# - genCompileLisp :: { name, srcs, deps } -> file
|
|
# Builds a lisp file which instructs the implementation to build a library
|
|
# from the given source files when executed. After running at least
|
|
# the file "$out/${name}.${impls.${implementation}.faslExt}" should have
|
|
# been created.
|
|
# - genDumpLisp :: { name, main, deps } -> file
|
|
# Builds a lisp file which instructs the implementation to build an
|
|
# executable which runs 'main' (and exits) where 'main' is available from
|
|
# 'deps'. The executable should be created as "$out/bin/${name}", usually
|
|
# by dumping the lisp image with the replaced toplevel function replaced.
|
|
# - genTestLisp :: { name, srcs, deps, expression } -> file
|
|
# Builds a lisp file which loads the given 'deps' and 'srcs' files and
|
|
# then evaluates 'expression'. Depending on whether 'expression' returns
|
|
# true or false, the script must exit with a zero or non-zero exit code.
|
|
# 'genTestLispGeneric' will work for most implementations.
|
|
# - lispWith :: [ dependency ] -> drv
|
|
# Builds a script (or dumped image) which when executed loads (or has
|
|
# loaded) all given dependencies. When built this should create an executable
|
|
# at "$out/bin/${implementation}".
|
|
#
|
|
# Optional members:
|
|
#
|
|
# - bundled :: string -> library
|
|
# Allows giving an implementation specific builder for a bundled library.
|
|
# This function is used as a replacement for the internal defaultBundled
|
|
# function and only needs to support one implementation. The returned derivation
|
|
# must behave like one built by 'library' (in particular have the same files
|
|
# available in "$out" and the same 'passthru' attributes), but may be built
|
|
# completely differently.
|
|
impls = lib.mapAttrs (name: v: { inherit name; } // v) {
|
|
sbcl = {
|
|
runScript = "${sbcl}/bin/sbcl --script";
|
|
faslExt = "fasl";
|
|
|
|
# 'genLoadLisp' generates Lisp code that instructs SBCL to load all
|
|
# the provided Lisp libraries.
|
|
genLoadLisp = genLoadLispGeneric impls.sbcl;
|
|
|
|
# 'genCompileLisp' generates a Lisp file that instructs SBCL to
|
|
# compile the provided list of Lisp source files to "$out/${name}.fasl".
|
|
genCompileLisp = { name, srcs, deps }: writeText "sbcl-compile.lisp" ''
|
|
;; This file compiles the specified sources into the Nix build
|
|
;; directory, creating one FASL file for each source.
|
|
(require 'sb-posix)
|
|
|
|
${impls.sbcl.genLoadLisp deps}
|
|
|
|
(defun nix-compile-lisp (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 (out-truename _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 out-truename)
|
|
|
|
;; Return pathname as a string for cat-ting it later
|
|
(namestring out-truename))))))
|
|
|
|
(let ((*compile-verbose* t)
|
|
(catted-fasl (make-pathname :type "fasl"
|
|
:directory (or (sb-posix:getenv "out")
|
|
(error "not running in a Nix build"))
|
|
:name "${name}")))
|
|
|
|
(with-open-file (file catted-fasl
|
|
:direction :output
|
|
:if-does-not-exist :create)
|
|
|
|
;; SBCL's FASL files can just be bundled together using cat
|
|
(sb-ext:run-program "cat"
|
|
(mapcar #'nix-compile-lisp
|
|
;; These forms were inserted by the Nix build:
|
|
'(${
|
|
lib.concatMapStringsSep "\n" (src: "\"${src}\"") srcs
|
|
}))
|
|
:output file :search t)))
|
|
'';
|
|
|
|
# '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 "sbcl-dump.lisp" ''
|
|
(require 'sb-posix)
|
|
|
|
${impls.sbcl.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))
|
|
'';
|
|
|
|
genTestLisp = genTestLispGeneric impls.sbcl;
|
|
|
|
lispWith = deps:
|
|
let lispDeps = filter (d: !d.lispBinary) (allDeps impls.sbcl 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" (impls.sbcl.genLoadLisp lispDeps)}"
|
|
} $@
|
|
'';
|
|
};
|
|
};
|
|
|
|
#
|
|
# Public API functions
|
|
#
|
|
|
|
# 'library' builds a list of Common Lisp files into an implementation
|
|
# specific library format, usually a single FASL file, which can then be
|
|
# loaded and built into an executable via 'program'.
|
|
library =
|
|
{ name
|
|
, implementation ? defaultImplementation
|
|
, srcs
|
|
, deps ? []
|
|
, native ? []
|
|
, tests ? null
|
|
}:
|
|
let
|
|
impl = impls."${implementation}" or
|
|
(builtins.throw "Unkown Common Lisp Implementation ${implementation}");
|
|
lispNativeDeps = (allNative native deps);
|
|
lispDeps = allDeps impl 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;
|
|
inherit impl;
|
|
}
|
|
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;
|
|
${implementation} = impl.lispWith [ self ];
|
|
};
|
|
} ''
|
|
${if ! isNull testDrv
|
|
then "echo 'Test ${testDrv} succeeded'"
|
|
else "echo 'No tests run'"}
|
|
|
|
mkdir $out
|
|
|
|
${impl.runScript} ${
|
|
impl.genCompileLisp {
|
|
inherit name srcs;
|
|
deps = lispDeps;
|
|
}
|
|
}
|
|
'');
|
|
|
|
# 'program' creates an executable, usually containing a dumped image of the
|
|
# specified sources and dependencies.
|
|
program =
|
|
{ name
|
|
, implementation ? defaultImplementation
|
|
, main ? "${name}:main"
|
|
, srcs
|
|
, deps ? []
|
|
, native ? []
|
|
, tests ? null
|
|
}:
|
|
let
|
|
impl = impls."${implementation}" or
|
|
(builtins.throw "Unkown Common Lisp Implementation ${implementation}");
|
|
lispDeps = allDeps impl deps;
|
|
libPath = lib.makeLibraryPath (allNative native lispDeps);
|
|
# overriding is used internally to propagate the implementation to use
|
|
selfLib = (makeOverridable 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;
|
|
inherit impl;
|
|
}
|
|
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;
|
|
${implementation} = impl.lispWith [ self ];
|
|
};
|
|
} ''
|
|
${if ! isNull testDrv
|
|
then "echo 'Test ${testDrv} succeeded'"
|
|
else ""}
|
|
mkdir -p $out/bin
|
|
|
|
${impl.runScript} ${
|
|
impl.genDumpLisp {
|
|
inherit name main;
|
|
deps = ([ selfLib ] ++ lispDeps);
|
|
}
|
|
}
|
|
|
|
wrapProgram $out/bin/${name} --prefix LD_LIBRARY_PATH : "${libPath}"
|
|
'');
|
|
|
|
# 'bundled' creates a "library" which makes a built-in package available,
|
|
# such as any of SBCL's sb-* packages or ASDF. By default this is done
|
|
# by calling 'require', but implementations are free to provide their
|
|
# own specific bundled function.
|
|
bundled = name:
|
|
let
|
|
# TODO(sterni): allow overriding args to underlying 'library' (e. g. srcs)
|
|
defaultBundled = implementation: name: library {
|
|
inherit name implementation;
|
|
srcs = lib.singleton (builtins.toFile "${name}.lisp" "(require '${name})");
|
|
};
|
|
|
|
bundled' =
|
|
{ implementation ? defaultImplementation
|
|
, name
|
|
}:
|
|
impls."${implementation}".bundled or (defaultBundled implementation) name;
|
|
|
|
in (makeOverridable bundled') {
|
|
inherit name;
|
|
};
|
|
|
|
in {
|
|
library = makeOverridable library;
|
|
program = makeOverridable program;
|
|
inherit bundled;
|
|
|
|
# 'sbclWith' creates an image with the specified libraries /
|
|
# programs loaded in SBCL.
|
|
sbclWith = impls.sbcl.lispWith;
|
|
}
|