about summary refs log tree commit diff
path: root/nix/buildLisp/default.nix
blob: 0d68a2818b7d4976894e59abe9388eb5726453c2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
# 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 runCommand 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
    runCommand 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: runCommand "${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: runCommand "${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: runCommand "${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
    ;
}