diff options
Diffstat (limited to 'nix/buildLisp')
-rw-r--r-- | nix/buildLisp/README.md | 96 | ||||
-rw-r--r-- | nix/buildLisp/default.nix | 184 | ||||
-rw-r--r-- | nix/buildLisp/example/default.nix | 32 | ||||
-rw-r--r-- | nix/buildLisp/example/lib.lisp | 6 | ||||
-rw-r--r-- | nix/buildLisp/example/main.lisp | 7 |
5 files changed, 325 insertions, 0 deletions
diff --git a/nix/buildLisp/README.md b/nix/buildLisp/README.md new file mode 100644 index 000000000000..8e45f3479c06 --- /dev/null +++ b/nix/buildLisp/README.md @@ -0,0 +1,96 @@ +buildLisp.nix +============= + +This is a build system for Common Lisp, written in Nix. + +It aims to offer an alternative to ASDF for users who live in a +Nix-based ecosystem. This offers several advantages over ASDF: + +* Simpler (logic-less) package definitions +* Easy linking of native dependencies (from Nix) +* Composability with Nix tooling for other languages +* Effective, per-system caching strategies +* Easy overriding of dependencies and whatnot +* ... and more! + +The project is still in its early stages and some important +restrictions should be highlighted: + +* There is no separate abstraction for tests at the moment (i.e. they + are built and run as programs) +* Only SBCL is supported (though the plan is to add support for at + least ABCL and Clozure CL, and maybe make it extensible) + +## Usage + +`buildLisp` exposes four different functions: + +* `buildLisp.library`: Builds a collection of Lisp files into a library. + + | parameter | type | use | required? | + |-----------|--------------|-------------------------------|-----------| + | `name` | `string` | Name of the library | yes | + | `srcs` | `list<path>` | List of paths to source files | yes | + | `deps` | `list<drv>` | List of dependencies | no | + | `native` | `list<drv>` | List of native dependencies | no | + + The output of invoking this is a directory containing a FASL file + that is the concatenated result of all compiled sources. + +* `buildLisp.program`: Builds an executable program out of Lisp files. + + | parameter | type | use | required? | + |-----------|--------------|-------------------------------|-----------| + | `name` | `string` | Name of the program | yes | + | `srcs` | `list<path>` | List of paths to source files | yes | + | `deps` | `list<drv>` | List of dependencies | no | + | `native` | `list<drv>` | List of native dependencies | no | + | `main` | `string` | Entrypoint function | no | + + The `main` parameter should be the name of a function and defaults + to `${name}:main` (i.e. the *exported* `main` function of the + package named after the program). + + The output of invoking this is a directory containing a + `bin/${name}`. + +* `buildLisp.bundled`: Creates a virtual dependency on a built-in library. + + Certain libraries ship with Lisp implementations, for example + UIOP/ASDF are commonly included but many implementations also ship + internals (such as SBCLs various `sb-*` libraries). + + This function takes a single string argument that is the name of a + built-in library and returns a "package" that simply requires this + library. + +* `buildLisp.sbclWith`: Creates an SBCL pre-loaded with various dependencies. + + This function takes a single argument which is a list of Lisp + libraries programs or programs. It creates an SBCL that is + pre-loaded with all of that Lisp code and can be used as the host + for e.g. Sly or SLIME. + +## Example + +Using buildLisp could look like this: + +```nix +{ buildLisp, lispPkgs }: + +let libExample = buildLisp.library { + name = "lib-example"; + srcs = [ ./lib.lisp ]; + + deps = with lispPkgs; [ + (buildLisp.bundled "sb-posix") + iterate + cl-ppcre + ]; +}; +in buildLisp.program { + name = "example"; + deps = [ libExample ]; + srcs = [ ./main.lisp ]; +} +``` diff --git a/nix/buildLisp/default.nix b/nix/buildLisp/default.nix new file mode 100644 index 000000000000..0e94ed6223b7 --- /dev/null +++ b/nix/buildLisp/default.nix @@ -0,0 +1,184 @@ +# 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")) + :defaults 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) + } + )) + ''; + + # '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)); + }; + + # + # 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 ? [] }: + let + lispNativeDeps = (allNative native deps); + lispDeps = allDeps deps; + in runCommandNoCC "${name}-cllib" { + LD_LIBRARY_PATH = lib.makeLibraryPath lispNativeDeps; + LANG = "C.UTF-8"; + } '' + ${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 + '' // { + inherit lispNativeDeps lispDeps; + lispName = name; + lispBinary = false; + }; + + # 'program' creates an executable containing a dumped image of the + # specified sources and dependencies. + program = { name, main ? "${name}:main", srcs, deps ? [], native ? [] }: + let + lispDeps = allDeps deps; + libPath = lib.makeLibraryPath (allNative native lispDeps); + selfLib = library { + inherit name srcs native; + deps = lispDeps; + }; + in runCommandNoCC "${name}" { + nativeBuildInputs = [ makeWrapper ]; + LD_LIBRARY_PATH = libPath; + } '' + mkdir -p $out/bin + + ${sbcl}/bin/sbcl --script ${ + genDumpLisp name main ([ selfLib ] ++ lispDeps) + } + + wrapProgram $out/bin/${name} --prefix LD_LIBRARY_PATH : "${libPath}" + '' // { + lispName = name; + lispDeps = [ selfLib ]; + lispNativeDeps = native; + lispBinary = true; + }; + + # '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)}; + exec ${sbcl}/bin/sbcl ${lib.optionalString (deps != []) "--load ${writeText "load.lisp" (genLoadLisp lispDeps)}"} $@ + ''; +in { + library = makeOverridable library; + program = makeOverridable program; + sbclWith = makeOverridable sbclWith; + bundled = makeOverridable bundled; +} diff --git a/nix/buildLisp/example/default.nix b/nix/buildLisp/example/default.nix new file mode 100644 index 000000000000..6a518e4964a1 --- /dev/null +++ b/nix/buildLisp/example/default.nix @@ -0,0 +1,32 @@ +{ depot, ... }: + +let + inherit (depot.nix) buildLisp; + + # Example Lisp library. + # + # Currently the `name` attribute is only used for the derivation + # itself, it has no practical implications. + libExample = buildLisp.library { + name = "lib-example"; + srcs = [ + ./lib.lisp + ]; + }; + +# Example Lisp program. +# +# This builds & writes an executable for a program using the library +# above to disk. +# +# By default, buildLisp.program expects the entry point to be +# `$name:main`. This can be overridden by configuring the `main` +# attribute. +in buildLisp.program { + name = "example"; + deps = [ libExample ]; + + srcs = [ + ./main.lisp + ]; +} diff --git a/nix/buildLisp/example/lib.lisp b/nix/buildLisp/example/lib.lisp new file mode 100644 index 000000000000..e557de4ae5fd --- /dev/null +++ b/nix/buildLisp/example/lib.lisp @@ -0,0 +1,6 @@ +(defpackage lib-example + (:use :cl) + (:export :who)) +(in-package :lib-example) + +(defun who () "edef") diff --git a/nix/buildLisp/example/main.lisp b/nix/buildLisp/example/main.lisp new file mode 100644 index 000000000000..a29390cf4dba --- /dev/null +++ b/nix/buildLisp/example/main.lisp @@ -0,0 +1,7 @@ +(defpackage example + (:use :cl :lib-example) + (:export :main)) +(in-package :example) + +(defun main () + (format t "i <3 ~A~%" (who))) |