049ec22943
The problem went away once again, let's see how long it'll last this time. As it turns out, CCL has a Unicode Standard conforming string implementation that doesn't allow the use of (lone) surrogate code points, requiring us to disable a test in cl-json which tested the behavior of en- and decoding of such a (technically illegal) string. Change-Id: I8bfa482934bbf94f86cecdde02d5c3d4e77950a5 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6204 Tested-by: BuildkiteCI Autosubmit: sterni <sternenseemann@systemli.org> Reviewed-by: tazjin <tazjin@tvl.su>
779 lines
30 KiB
Nix
779 lines
30 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 ecl-static ccl;
|
|
inherit (pkgs.stdenv) targetPlatform;
|
|
|
|
#
|
|
# Internal helper definitions
|
|
#
|
|
|
|
defaultImplementation = impls.sbcl;
|
|
|
|
# Many Common Lisp implementations (like ECL and CCL) will occasionally drop
|
|
# you into an interactive debugger even when executing something as a script.
|
|
# In nix builds we don't want such a situation: Any error should make the
|
|
# script exit non-zero. Luckily the ANSI standard specifies *debugger-hook*
|
|
# which is invoked before the debugger letting us just do that.
|
|
disableDebugger = writeText "disable-debugger.lisp" ''
|
|
(setf *debugger-hook*
|
|
(lambda (error hook)
|
|
(declare (ignore hook))
|
|
(format *error-output* "~%Unhandled error: ~a~%" error)
|
|
#+ccl (quit 1)
|
|
#+ecl (ext:quit 1)))
|
|
'';
|
|
|
|
# Process a list of arbitrary values which also contains “implementation
|
|
# filter sets” which describe conditonal inclusion of elements depending
|
|
# on the CL implementation used. Elements are processed in the following
|
|
# manner:
|
|
#
|
|
# * Paths, strings, derivations are left as is
|
|
# * A non-derivation attribute set is processed like this:
|
|
# 1. If it has an attribute equal to impl.name, replace with its value.
|
|
# 2. Alternatively use the value of the "default" attribute.
|
|
# 3. In all other cases delete the element from the list.
|
|
#
|
|
# This can be used to express dependencies or source files which are specific
|
|
# to certain implementations:
|
|
#
|
|
# srcs = [
|
|
# # mixable with unconditional entries
|
|
# ./package.lisp
|
|
#
|
|
# # implementation specific source files
|
|
# {
|
|
# ccl = ./impl-ccl.lisp;
|
|
# sbcl = ./impl-sbcl.lisp;
|
|
# ecl = ./impl-ecl.lisp;
|
|
# }
|
|
# ];
|
|
#
|
|
# deps = [
|
|
# # this dependency is ignored if impl.name != "sbcl"
|
|
# { sbcl = buildLisp.bundled "sb-posix"; }
|
|
#
|
|
# # only special casing for a single implementation
|
|
# {
|
|
# sbcl = buildLisp.bundled "uiop";
|
|
# default = buildLisp.bundled "asdf";
|
|
# }
|
|
# ];
|
|
implFilter = impl: xs:
|
|
let
|
|
isFilterSet = x: builtins.isAttrs x && !(lib.isDerivation x);
|
|
in
|
|
builtins.map
|
|
(
|
|
x: if isFilterSet x then x.${impl.name} or x.default else x
|
|
)
|
|
(builtins.filter
|
|
(
|
|
x: !(isFilterSet x) || x ? ${impl.name} || x ? default
|
|
)
|
|
xs);
|
|
|
|
# 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. Use implementation
|
|
# attribute created by withExtras if present, override in all other cases
|
|
# (mainly bundled).
|
|
deps' = builtins.map
|
|
(dep: dep."${impl.name}" or (dep.overrideLisp (_: {
|
|
implementation = impl;
|
|
})))
|
|
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));
|
|
};
|
|
|
|
# This is a wrapper arround 'makeOverridable' which performs its
|
|
# function, but also adds a the following additional attributes to the
|
|
# resulting derivation, namely a repl attribute which builds a `lispWith`
|
|
# derivation for the current implementation and additional attributes for
|
|
# every all implementations. So `drv.sbcl` would build the derivation
|
|
# with SBCL regardless of what was specified in the initial arguments.
|
|
withExtras = f: args:
|
|
let
|
|
drv = (makeOverridable f) args;
|
|
in
|
|
lib.fix (self:
|
|
drv.overrideLisp
|
|
(old:
|
|
let
|
|
implementation = old.implementation or defaultImplementation;
|
|
brokenOn = old.brokenOn or [ ];
|
|
targets = lib.subtractLists (brokenOn ++ [ implementation.name ])
|
|
(builtins.attrNames impls);
|
|
in
|
|
{
|
|
passthru = (old.passthru or { }) // {
|
|
repl = implementation.lispWith [ self ];
|
|
|
|
# meta is done via passthru to minimize rebuilds caused by overriding
|
|
meta = (old.passthru.meta or { }) // {
|
|
ci = (old.passthru.meta.ci or { }) // {
|
|
inherit targets;
|
|
};
|
|
};
|
|
} // builtins.listToAttrs (builtins.map
|
|
(impl: {
|
|
inherit (impl) name;
|
|
value = self.overrideLisp (_: {
|
|
implementation = impl;
|
|
});
|
|
})
|
|
(builtins.attrValues impls));
|
|
}) // {
|
|
overrideLisp = new: withExtras f (args // new args);
|
|
});
|
|
|
|
# '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 ? [ ], implementation }:
|
|
let
|
|
lispDeps = allDeps implementation (implFilter implementation deps);
|
|
lispNativeDeps = allNative native lispDeps;
|
|
filteredSrcs = implFilter implementation srcs;
|
|
in
|
|
runCommandNoCC name
|
|
{
|
|
LD_LIBRARY_PATH = lib.makeLibraryPath lispNativeDeps;
|
|
LANG = "C.UTF-8";
|
|
} ''
|
|
echo "Running test suite ${name}"
|
|
|
|
${implementation.runScript} ${
|
|
implementation.genTestLisp {
|
|
inherit name expression;
|
|
srcs = filteredSrcs;
|
|
deps = lispDeps;
|
|
}
|
|
} | 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.
|
|
# - wrapProgram :: boolean
|
|
# Whether to wrap the resulting binary / image with a wrapper script setting
|
|
# `LD_LIBRARY_PATH`.
|
|
# - 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)))
|
|
|
|
;; Tell UIOP that argv[0] will refer to running image, not the lisp impl
|
|
(when (find-package :uiop)
|
|
(eval `(setq ,(find-symbol "*IMAGE-DUMPED-P*" :uiop) :executable)))
|
|
|
|
(save-lisp-and-die outpath
|
|
:executable t
|
|
:toplevel
|
|
(lambda ()
|
|
;; Filter out everything prior to the `--` we
|
|
;; insert in the wrapper to prevent SBCL from
|
|
;; parsing arguments at startup
|
|
(setf sb-ext:*posix-argv*
|
|
(delete "--" sb-ext:*posix-argv*
|
|
:test #'string= :count 1))
|
|
(${main}))
|
|
:purify t))
|
|
'';
|
|
|
|
wrapProgram = true;
|
|
|
|
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)}"
|
|
} $@
|
|
'';
|
|
};
|
|
ecl = {
|
|
runScript = "${ecl-static}/bin/ecl --load ${disableDebugger} --shell";
|
|
faslExt = "fasc";
|
|
genLoadLisp = genLoadLispGeneric impls.ecl;
|
|
genCompileLisp = { name, srcs, deps }: writeText "ecl-compile.lisp" ''
|
|
;; This seems to be required to bring make the 'c' package available
|
|
;; early, otherwise ECL tends to fail with a read failure…
|
|
(ext:install-c-compiler)
|
|
|
|
;; Load dependencies
|
|
${impls.ecl.genLoadLisp deps}
|
|
|
|
(defun getenv-or-fail (var)
|
|
(or (ext:getenv var)
|
|
(error (format nil "Missing expected environment variable ~A" var))))
|
|
|
|
(defun nix-compile-file (srcfile &key native)
|
|
"Compile the given srcfile into a compilation unit in :out-dir using
|
|
a unique name based on srcfile as the filename which is returned after
|
|
compilation. If :native is true, create an native object file,
|
|
otherwise a byte-compile fasc file is built and immediately loaded."
|
|
|
|
(let* ((unique-name (substitute #\_ #\/ srcfile))
|
|
(out-file (make-pathname :type (if native "o" "fasc")
|
|
:directory (getenv-or-fail "NIX_BUILD_TOP")
|
|
:name unique-name)))
|
|
(multiple-value-bind (out-truename _warnings-p failure-p)
|
|
(compile-file srcfile :system-p native
|
|
:load (not native)
|
|
:output-file out-file
|
|
:verbose t :print t)
|
|
(if failure-p (ext:quit 1) out-truename))))
|
|
|
|
(let* ((out-dir (getenv-or-fail "out"))
|
|
(nix-build-dir (getenv-or-fail "NIX_BUILD_TOP"))
|
|
(srcs
|
|
;; These forms are inserted by the Nix build
|
|
'(${lib.concatMapStringsSep "\n" (src: "\"${src}\"") srcs})))
|
|
|
|
;; First, we'll byte compile loadable FASL files and load them
|
|
;; immediately. Since we are using a statically linked ECL, there's
|
|
;; no way to load native objects, so we rely on byte compilation
|
|
;; for all our loading — which is crucial in compilation of course.
|
|
(ext:install-bytecodes-compiler)
|
|
|
|
;; ECL's bytecode FASLs can just be concatenated to create a bundle
|
|
;; at least since a recent bugfix which we apply as a patch.
|
|
;; See also: https://gitlab.com/embeddable-common-lisp/ecl/-/issues/649
|
|
(let ((bundle-out (make-pathname :type "fasc" :name "${name}"
|
|
:directory out-dir)))
|
|
|
|
(with-open-file (fasc-stream bundle-out :direction :output)
|
|
(ext:run-program "cat"
|
|
(mapcar (lambda (f)
|
|
(namestring
|
|
(nix-compile-file f :native nil)))
|
|
srcs)
|
|
:output fasc-stream)))
|
|
|
|
(ext:install-c-compiler)
|
|
|
|
;; Build a (natively compiled) static archive (.a) file. We want to
|
|
;; use this for (statically) linking an executable later. The bytecode
|
|
;; dance is only required because we can't load such archives.
|
|
(c:build-static-library
|
|
(make-pathname :type "a" :name "${name}" :directory out-dir)
|
|
:lisp-files (mapcar (lambda (x)
|
|
(nix-compile-file x :native t))
|
|
srcs)))
|
|
'';
|
|
genDumpLisp = { name, main, deps }: writeText "ecl-dump.lisp" ''
|
|
(defun getenv-or-fail (var)
|
|
(or (ext:getenv var)
|
|
(error (format nil "Missing expected environment variable ~A" var))))
|
|
|
|
${impls.ecl.genLoadLisp deps}
|
|
|
|
;; makes a 'c' package available that can link executables
|
|
(ext:install-c-compiler)
|
|
|
|
(c:build-program
|
|
(merge-pathnames (make-pathname :directory '(:relative "bin")
|
|
:name "${name}")
|
|
(truename (getenv-or-fail "out")))
|
|
:epilogue-code `(progn
|
|
;; UIOP doesn't understand ECL, so we need to make it
|
|
;; aware that we are a proper executable, causing it
|
|
;; to handle argument parsing and such properly. Since
|
|
;; this needs to work even when we're not using UIOP,
|
|
;; we need to do some compile-time acrobatics.
|
|
,(when (find-package :uiop)
|
|
`(setf ,(find-symbol "*IMAGE-DUMPED-P*" :uiop) :executable))
|
|
;; Run the actual application…
|
|
(${main})
|
|
;; … and exit.
|
|
(ext:quit))
|
|
;; ECL can't remember these from its own build…
|
|
:ld-flags '("-static")
|
|
:lisp-files
|
|
;; The following forms are inserted by the Nix build
|
|
'(${
|
|
lib.concatMapStrings (dep: ''
|
|
"${dep}/${dep.lispName}.a"
|
|
'') (allDeps impls.ecl deps)
|
|
}))
|
|
'';
|
|
|
|
wrapProgram = false;
|
|
|
|
genTestLisp = genTestLispGeneric impls.ecl;
|
|
|
|
lispWith = deps:
|
|
let lispDeps = filter (d: !d.lispBinary) (allDeps impls.ecl deps);
|
|
in writeShellScriptBin "ecl" ''
|
|
exec ${ecl-static}/bin/ecl ${
|
|
lib.optionalString (deps != [])
|
|
"--load ${writeText "load.lisp" (impls.ecl.genLoadLisp lispDeps)}"
|
|
} $@
|
|
'';
|
|
|
|
bundled = name: runCommandNoCC "${name}-cllib"
|
|
{
|
|
passthru = {
|
|
lispName = name;
|
|
lispNativeDeps = [ ];
|
|
lispDeps = [ ];
|
|
lispBinary = false;
|
|
repl = impls.ecl.lispWith [ (impls.ecl.bundled name) ];
|
|
};
|
|
} ''
|
|
mkdir -p "$out"
|
|
ln -s "${ecl-static}/lib/ecl-${ecl-static.version}/${name}.${impls.ecl.faslExt}" -t "$out"
|
|
ln -s "${ecl-static}/lib/ecl-${ecl-static.version}/lib${name}.a" "$out/${name}.a"
|
|
'';
|
|
};
|
|
ccl = {
|
|
# Relatively bespoke wrapper script necessary to make CCL just™ execute
|
|
# a lisp file as a script.
|
|
runScript = pkgs.writers.writeBash "ccl" ''
|
|
# don't print intro message etc.
|
|
args=("--quiet")
|
|
|
|
# makes CCL crash on error instead of entering the debugger
|
|
args+=("--load" "${disableDebugger}")
|
|
|
|
# load files from command line in order
|
|
for f in "$@"; do
|
|
args+=("--load" "$f")
|
|
done
|
|
|
|
# Exit if everything was processed successfully
|
|
args+=("--eval" "(quit)")
|
|
|
|
exec ${ccl}/bin/ccl ''${args[@]}
|
|
'';
|
|
|
|
# See https://ccl.clozure.com/docs/ccl.html#building-definitions
|
|
faslExt =
|
|
/**/
|
|
if targetPlatform.isPower && targetPlatform.is32bit then "pfsl"
|
|
else if targetPlatform.isPower && targetPlatform.is64bit then "p64fsl"
|
|
else if targetPlatform.isx86_64 && targetPlatform.isLinux then "lx64fsl"
|
|
else if targetPlatform.isx86_32 && targetPlatform.isLinux then "lx32fsl"
|
|
else if targetPlatform.isAarch32 && targetPlatform.isLinux then "lafsl"
|
|
else if targetPlatform.isx86_32 && targetPlatform.isDarwin then "dx32fsl"
|
|
else if targetPlatform.isx86_64 && targetPlatform.isDarwin then "dx64fsl"
|
|
else if targetPlatform.isx86_64 && targetPlatform.isDarwin then "dx64fsl"
|
|
else if targetPlatform.isx86_32 && targetPlatform.isFreeBSD then "fx32fsl"
|
|
else if targetPlatform.isx86_64 && targetPlatform.isFreeBSD then "fx64fsl"
|
|
else if targetPlatform.isx86_32 && targetPlatform.isWindows then "wx32fsl"
|
|
else if targetPlatform.isx86_64 && targetPlatform.isWindows then "wx64fsl"
|
|
else builtins.throw "Don't know what FASLs are called for this platform: "
|
|
+ pkgs.stdenv.targetPlatform.system;
|
|
|
|
genLoadLisp = genLoadLispGeneric impls.ccl;
|
|
|
|
genCompileLisp = { name, srcs, deps }: writeText "ccl-compile.lisp" ''
|
|
${impls.ccl.genLoadLisp deps}
|
|
|
|
(defun getenv-or-fail (var)
|
|
(or (getenv var)
|
|
(error (format nil "Missing expected environment variable ~A" var))))
|
|
|
|
(defun nix-compile-file (srcfile)
|
|
"Trivial wrapper around COMPILE-FILE which causes CCL to exit if
|
|
compilation fails and LOADs the compiled file on success."
|
|
(let ((output (make-pathname :name (substitute #\_ #\/ srcfile)
|
|
:type "${impls.ccl.faslExt}"
|
|
:directory (getenv-or-fail "NIX_BUILD_TOP"))))
|
|
(multiple-value-bind (out-truename _warnings-p failure-p)
|
|
(compile-file srcfile :output-file output :print t :verbose t)
|
|
(declare (ignore _warnings-p))
|
|
(if failure-p (quit 1)
|
|
(progn (load out-truename) out-truename)))))
|
|
|
|
(fasl-concatenate (make-pathname :name "${name}" :type "${impls.ccl.faslExt}"
|
|
:directory (getenv-or-fail "out"))
|
|
(mapcar #'nix-compile-file
|
|
;; These forms where inserted by the Nix build
|
|
'(${
|
|
lib.concatMapStrings (src: ''
|
|
"${src}"
|
|
'') srcs
|
|
})))
|
|
'';
|
|
|
|
genDumpLisp = { name, main, deps }: writeText "ccl-dump.lisp" ''
|
|
${impls.ccl.genLoadLisp deps}
|
|
|
|
(let* ((out (or (getenv "out") (error "Not running in a Nix build")))
|
|
(bindir (concatenate 'string out "/bin/"))
|
|
(executable (make-pathname :directory bindir :name "${name}")))
|
|
|
|
;; Tell UIOP that argv[0] will refer to running image, not the lisp impl
|
|
(when (find-package :uiop)
|
|
(eval `(setf ,(find-symbol "*IMAGE-DUMPED-P*" :uiop) :executable)))
|
|
|
|
(save-application executable
|
|
:purify t
|
|
:error-handler :quit
|
|
:toplevel-function
|
|
(lambda ()
|
|
;; Filter out everything prior to the `--` we
|
|
;; insert in the wrapper to prevent SBCL from
|
|
;; parsing arguments at startup
|
|
(setf ccl:*command-line-argument-list*
|
|
(delete "--" ccl:*command-line-argument-list*
|
|
:test #'string= :count 1))
|
|
(${main}))
|
|
:mode #o755
|
|
;; TODO(sterni): use :native t on macOS
|
|
:prepend-kernel t))
|
|
'';
|
|
|
|
wrapProgram = true;
|
|
|
|
genTestLisp = genTestLispGeneric impls.ccl;
|
|
|
|
lispWith = deps:
|
|
let lispDeps = filter (d: !d.lispBinary) (allDeps impls.ccl deps);
|
|
in writeShellScriptBin "ccl" ''
|
|
export LD_LIBRARY_PATH="${lib.makeLibraryPath (allNative [] lispDeps)}"
|
|
exec ${ccl}/bin/ccl ${
|
|
lib.optionalString (deps != [])
|
|
"--load ${writeText "load.lisp" (impls.ccl.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
|
|
, brokenOn ? [ ] # TODO(sterni): make this a warning
|
|
, srcs
|
|
, deps ? [ ]
|
|
, native ? [ ]
|
|
, tests ? null
|
|
, passthru ? { }
|
|
}:
|
|
let
|
|
filteredDeps = implFilter implementation deps;
|
|
filteredSrcs = implFilter implementation srcs;
|
|
lispNativeDeps = (allNative native filteredDeps);
|
|
lispDeps = allDeps implementation filteredDeps;
|
|
testDrv =
|
|
if ! isNull tests
|
|
then
|
|
testSuite
|
|
{
|
|
name = tests.name or "${name}-test";
|
|
srcs = filteredSrcs ++ (tests.srcs or [ ]);
|
|
deps = filteredDeps ++ (tests.deps or [ ]);
|
|
expression = tests.expression;
|
|
inherit implementation;
|
|
}
|
|
else null;
|
|
in
|
|
lib.fix (self: runCommandNoCC "${name}-cllib"
|
|
{
|
|
LD_LIBRARY_PATH = lib.makeLibraryPath lispNativeDeps;
|
|
LANG = "C.UTF-8";
|
|
passthru = passthru // {
|
|
inherit lispNativeDeps lispDeps;
|
|
lispName = name;
|
|
lispBinary = false;
|
|
tests = testDrv;
|
|
};
|
|
} ''
|
|
${if ! isNull testDrv
|
|
then "echo 'Test ${testDrv} succeeded'"
|
|
else "echo 'No tests run'"}
|
|
|
|
mkdir $out
|
|
|
|
${implementation.runScript} ${
|
|
implementation.genCompileLisp {
|
|
srcs = filteredSrcs;
|
|
inherit name;
|
|
deps = lispDeps;
|
|
}
|
|
}
|
|
'');
|
|
|
|
# 'program' creates an executable, usually containing a dumped image of the
|
|
# specified sources and dependencies.
|
|
program =
|
|
{ name
|
|
, implementation ? defaultImplementation
|
|
, brokenOn ? [ ] # TODO(sterni): make this a warning
|
|
, main ? "${name}:main"
|
|
, srcs
|
|
, deps ? [ ]
|
|
, native ? [ ]
|
|
, tests ? null
|
|
, passthru ? { }
|
|
}:
|
|
let
|
|
filteredSrcs = implFilter implementation srcs;
|
|
filteredDeps = implFilter implementation deps;
|
|
lispDeps = allDeps implementation filteredDeps;
|
|
libPath = lib.makeLibraryPath (allNative native lispDeps);
|
|
# overriding is used internally to propagate the implementation to use
|
|
selfLib = (makeOverridable library) {
|
|
inherit name native brokenOn;
|
|
deps = lispDeps;
|
|
srcs = filteredSrcs;
|
|
};
|
|
testDrv =
|
|
if ! isNull tests
|
|
then
|
|
testSuite
|
|
{
|
|
name = tests.name or "${name}-test";
|
|
srcs =
|
|
(
|
|
# testSuite does run implFilter as well
|
|
filteredSrcs ++ (tests.srcs or [ ])
|
|
);
|
|
deps = filteredDeps ++ (tests.deps or [ ]);
|
|
expression = tests.expression;
|
|
inherit implementation;
|
|
}
|
|
else null;
|
|
in
|
|
lib.fix (self: runCommandNoCC "${name}"
|
|
{
|
|
nativeBuildInputs = [ makeWrapper ];
|
|
LD_LIBRARY_PATH = libPath;
|
|
LANG = "C.UTF-8";
|
|
passthru = passthru // {
|
|
lispName = name;
|
|
lispDeps = [ selfLib ];
|
|
lispNativeDeps = native;
|
|
lispBinary = true;
|
|
tests = testDrv;
|
|
};
|
|
}
|
|
(''
|
|
${if ! isNull testDrv
|
|
then "echo 'Test ${testDrv} succeeded'"
|
|
else ""}
|
|
mkdir -p $out/bin
|
|
|
|
${implementation.runScript} ${
|
|
implementation.genDumpLisp {
|
|
inherit name main;
|
|
deps = ([ selfLib ] ++ lispDeps);
|
|
}
|
|
}
|
|
'' + lib.optionalString implementation.wrapProgram ''
|
|
wrapProgram $out/bin/${name} \
|
|
--prefix LD_LIBRARY_PATH : "${libPath}" \
|
|
--add-flags "\$NIX_BUILDLISP_LISP_ARGS --"
|
|
''));
|
|
|
|
# '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
|
|
}:
|
|
implementation.bundled or (defaultBundled implementation) name;
|
|
|
|
in
|
|
(makeOverridable bundled') {
|
|
inherit name;
|
|
};
|
|
|
|
in
|
|
{
|
|
library = withExtras library;
|
|
program = withExtras program;
|
|
inherit bundled;
|
|
|
|
# 'sbclWith' creates an image with the specified libraries /
|
|
# programs loaded in SBCL.
|
|
sbclWith = impls.sbcl.lispWith;
|
|
|
|
inherit (impls)
|
|
sbcl
|
|
ecl
|
|
ccl
|
|
;
|
|
}
|