Machines on which LANG is misconfigured have trouble with SBCL loading files that contain characters in certain encodings. This overrides whichever local LANG (if any) is set. Change-Id: Ic4341a01c4393e7f697de6cecc58dea4f2d85987 Reviewed-on: https://cl.tvl.fyi/c/depot/+/2076 Tested-by: BuildkiteCI Reviewed-by: glittershark <grfn@gws.fyi>
		
			
				
	
	
		
			259 lines
		
	
	
	
		
			8.4 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			259 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";
 | 
						|
    } ''
 | 
						|
      ${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
 | 
						|
    '' // {
 | 
						|
      inherit lispNativeDeps lispDeps;
 | 
						|
      lispName = name;
 | 
						|
      lispBinary = false;
 | 
						|
      tests = testDrv;
 | 
						|
      sbcl = sbclWith [ self ];
 | 
						|
    });
 | 
						|
 | 
						|
  # '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";
 | 
						|
    } ''
 | 
						|
      ${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}"
 | 
						|
    '' // {
 | 
						|
      lispName = name;
 | 
						|
      lispDeps = [ selfLib ] ++ (tests.deps or []);
 | 
						|
      lispNativeDeps = native;
 | 
						|
      lispBinary = true;
 | 
						|
      tests = testDrv;
 | 
						|
      sbcl = sbclWith [ self ];
 | 
						|
    });
 | 
						|
 | 
						|
  # '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;
 | 
						|
  sbclWith = makeOverridable sbclWith;
 | 
						|
  bundled = makeOverridable bundled;
 | 
						|
}
 |