`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;
 | |
| }
 |