default.nixnix · 436 linesall
{ lib, ... }:
{
  imports = [
    ./examples.nix
    ./bench-tools.nix
  ];

  perSystem =
    {
      config,
      pkgs,
      inputs',
      ...
    }:
    let
      inherit (config.legacyPackages) d-toolchain;

      # `ci` execs a D compiler at runtime to build the examples, so it lands in
      # ci's runtime closure (and every consumer's — pre-commit devShell, lint
      # CI). Prefer DMD on x86_64-linux for `--example-files` / `--test`: no
      # LLVM backend, so ~half LDC's closure. DMD only targets
      # x86_64/i686-linux + x86_64-darwin; keep LDC elsewhere. `--wasm`
      # (via `--test-extracted --require-toolchain`) still needs LDC +
      # wasm-ld + a runtime, so those go on PATH even when DMD is the
      # default compiler.
      ciCompiler = if pkgs.stdenv.hostPlatform.system == "x86_64-linux" then pkgs.dmd else pkgs.ldc;
    in
    {
      # Sources retained by ci's runtime closure and published with all-desktop.
      # Keep example-only dependencies out of normal application builds.
      packages.dub-sources = pkgs.importDubLock {
        pname = "sparkles";
        version = "sources";
        lock.dependencies =
          (lib.importJSON ../dub-lock.json).dependencies // (lib.importJSON ../ci-dub-lock.json).dependencies;
      };

      packages.ci = config.legacyPackages.buildSparklesApp (finalAttrs: {
        pname = "ci";
        version = "0.1.0";

        # `--audit-fences` classifies fence labels against the grammar bundle, so
        # `ci` now links sparkles:syntax -> sparkles:tree-sitter, whose ImportC
        # shim `#include`s <tree_sitter/api.h>. That header is found through
        # pkg-config (dub#3085 feeds `--cflags` to ImportC), so the resolver has
        # to be present at *build* time — without it the build dies on
        # "tree_sitter/api.h: No such file or directory", which also breaks the
        # dev shell, since it depends on this package.
        nativeBuildInputs = [ pkgs.pkg-config ];

        # Runtime deps subtracted from buildSparklesApp's default scrub set:
        # - `ciCompiler` — shells out via PATH (see postFixup)
        # - `pkgs.ldc` / `pkgs.lld` / `pkgs.nodejs` — `--test-extracted --wasm`
        # - `pkgs.curl.out` — `--ci-stats` uses std.net.curl; Phobos bakes an
        #   absolute libcurl path
        # - `pkgs.tree-sitter` — `libs "tree-sitter"` in libs/tree-sitter
        buildInputs = [
          ciCompiler
          pkgs.ldc
          pkgs.lld
          pkgs.nodejs
          pkgs.coreutils
          pkgs.curl.out
          pkgs.tree-sitter
        ];

        # ci shells out to `dub --single` at runtime to compile examples, so it
        # needs a D compiler + `dub` + git on PATH (via wrapProgram below).
        # gitMinimal avoids the second CPython that full git's git-p4 shebang pulls.
        postFixup =
          let
            path = lib.makeBinPath [
              ciCompiler
              pkgs.ldc
              pkgs.lld
              pkgs.nodejs
              pkgs.dub
              pkgs.coreutils
              pkgs.gitMinimal
            ];
            # Render `--set NAME VALUE` triples for wrapProgram from the toolchain
            # env (non-empty on darwin: CC/CXX/SDKROOT/MACOSX_DEPLOYMENT_TARGET).
            # Not lib.cli.toCommandLine*: its option-spec model renders `--flag
            # value` pairs, not wrapProgram's three-token `--set KEY VALUE` form.
            setEnv = lib.escapeShellArgs (
              lib.concatLists (
                lib.mapAttrsToList (name: value: [
                  "--set"
                  name
                  value
                ]) d-toolchain.env
              )
            );
            # The cpu-pmu research probes (docs/research/cpu-pmu/examples) link
            # C libraries via `libs "dw" "elf"` / `libs "pfm"`. Inside the
            # devshell the shellHook exports these paths (nix/shells); carry
            # them in the wrapper too so `nix run .#ci -- --example-files`
            # links them outside any shell.
            exampleLibPath = lib.optionalString pkgs.stdenv.hostPlatform.isLinux (
              lib.makeSearchPath "lib" [
                pkgs.elfutils.out
                pkgs.libpfm
              ]
            );
            exampleLibArgs = lib.optionalString (
              exampleLibPath != ""
            ) "--prefix LIBRARY_PATH : ${exampleLibPath} --prefix LD_LIBRARY_PATH : ${exampleLibPath}";
          in
          # Best-effort bump of NOFILE: dub/ldc can open many files in parallel
          # builds. Redirect stderr and '|| true' so that on environments where
          # the hard cap is below nofileLimit (some CI runners, restricted
          # sandboxes) the wrapper does not abort under makeWrapper --run's set
          # -e semantics — we just fall back to the inherited limit.
          ''
            wrapProgram $out/bin/${finalAttrs.pname} \
              --prefix PATH : ${path} \
              --set-default SPARKLES_DUB_SOURCES ${config.packages.dub-sources} \
              ${setEnv} \
              ${exampleLibArgs} \
              --run 'ulimit -n ${toString d-toolchain.nofileLimit} 2>/dev/null || true'
          '';

        meta = {
          description = ''
            Repository CI helper for markdown examples, standalone examples, and
            markdown link maintenance
          '';
          mainProgram = finalAttrs.pname;
        };
      });

      # The same `ci` binary with nothing on PATH — for the subcommands that
      # only talk to an HTTP API and never exec a compiler.
      #
      # `packages.ci` is 2.4 GiB, and essentially none of it is linked: the
      # wrapper above puts dmd (487 MiB), a second dmd (525), ldc-binary (543),
      # lld (629), two llvm-libs (587 + 525), nodejs (253) and gcc+wrapper (645)
      # on PATH so `--example-files` can compile markdown examples. That is
      # right for the job that runs examples and wrong for one that files a
      # check run: `main-checks.yml` sits on the merge path, so a two-gigabyte
      # substitution there is paid on every merge to buy nothing.
      #
      # So: no `postFixup`, and `buildInputs` narrowed to the two genuine
      # runtime references — libcurl, whose absolute path Phobos bakes into
      # `std.net.curl`, and tree-sitter, which `--audit-fences` links. Everything
      # else returns to the scrub set it was exempted from.
      #
      # It builds `apps/ci` under its own name, so the three attributes
      # `buildSparklesApp` derives from `pname` are supplied explicitly: the
      # source closure, the unpacked directory to build in, and the binary dub
      # emits (`build/ci`) versus the one installed (`bin/ci-minimal`).
      packages.ci-minimal = config.legacyPackages.buildSparklesApp (finalAttrs: {
        pname = "ci-minimal";
        version = "0.1.0";

        sourceDirs = config.legacyPackages.sparklesSources.srcClosure "apps/ci";
        sourceRoot = "${finalAttrs.src.name}/apps/ci";
        installPhase = ''
          install -Dm755 build/ci $out/bin/${finalAttrs.pname}
        '';

        nativeBuildInputs = [ pkgs.pkg-config ];

        buildInputs = [
          pkgs.curl.out
          pkgs.tree-sitter
        ];

        meta = {
          description = ''
            Repository CI helper, unwrapped: the API-driven subcommands
            (--mirror-checks, --report-link-rot, --ci-stats) with no compiler
            toolchain in its closure
          '';
          mainProgram = finalAttrs.pname;
        };
      });

      # Reuse the existing harness, with no foreign-oracle libraries or curl.
      # Foreign dependencies live in a separate opt-in recipe so DUB never
      # resolves them here; the shared builder provisions root manifests.
      packages.text-conformance = config.legacyPackages.buildSparklesApp (finalAttrs: {
        pname = "text-conformance";
        version = "0.1.0";
        sourceDirs = [
          "libs/base/tools/text-conformance/src"
        ]
        ++ config.legacyPackages.sparklesSources.libsClosure [
          "base"
          "core-cli"
        ];
        sourceRoot = "${finalAttrs.src.name}/libs/base/tools/text-conformance";
        dubBuildFlags = [ "--config=offline" ];
        meta = {
          description = "Offline manifest-authenticated owned Unicode conformance harness";
          mainProgram = finalAttrs.pname;
        };
      });

      # Provision every authenticated input before the sandboxed offline gate.
      # Only the reviewed manifest belongs to the source tree; fetchurl checks
      # each upstream file's exact SHA-256 before linkFarm preserves its path.
      packages.unicode-conformance-data =
        let
          manifestPath = ../../libs/base/tools/unicode/manifest.json;
          manifest = builtins.fromJSON (builtins.readFile manifestPath);
        in
        pkgs.linkFarm "unicode-conformance-data" (
          [
            {
              name = "manifest.json";
              path = manifestPath;
            }
          ]
          ++ map (artifact: {
            name = "${manifest.release}/${artifact.path}";
            path = pkgs.fetchurl {
              inherit (artifact) url sha256;
            };
          }) manifest.artifacts
        );

      # Ordinary sandboxed derivation: test execution has no network access,
      # and the offline executable has no downloader compiled in. D verifies
      # the manifest identity against the generated tables and each input hash.
      checks.text-conformance = pkgs.runCommand "text-conformance-official" { } ''
        set -o pipefail
        ${lib.getExe config.packages.text-conformance} \
          --layers 0,1,2,11,12,13,14,15,16 --no-network \
          --manifest ${config.packages.unicode-conformance-data}/manifest.json \
          --ucd-dir ${config.packages.unicode-conformance-data}/18.0.0 \
          | tee "$out"
      '';

      # `buildSparklesApp` derives the source closure from apps/terminal/dub.sdl
      # (transitively: base, core-cli, ghostty, math, and the test-runner
      # shim+impl) and supplies the shared dub plumbing, so only the raylib +
      # libghostty-vt build inputs and the fontconfig runtime wrapper remain.
      packages.terminal = config.legacyPackages.buildSparklesApp (finalAttrs: {
        pname = "terminal";
        version = "0.1.0";

        nativeBuildInputs = [ pkgs.pkg-config ];

        buildInputs = [
          # The document viewer (`TDV`, sparkles:doc-view) highlights through
          # sparkles:syntax.
          pkgs.tree-sitter
          pkgs.raylib
          inputs'.ghostty.packages.libghostty-vt
          inputs'.ghostty.packages.libghostty-vt.dev
        ];

        env = d-toolchain.env;

        # The credits page (`TPG15`), beside the executable: the document and
        # its staged licence texts, the files the docs site renders (`TPG16`).
        installPhase = ''
          install -Dm755 build/${finalAttrs.pname} $out/bin/${finalAttrs.pname}
          mkdir -p $out/share/sparkles-terminal
          cp -r ${config.legacyPackages.sparklesCredits.bundle} $out/share/sparkles-terminal/credits
        '';

        # The version and commit `logBuildInfo` reports (`TPG2`): the stamp
        # lands in the `stringImportPaths "views"` apps/terminal/dub.sdl names.
        preBuild = ''
          chmod -R u+w "$NIX_BUILD_TOP"
          mkdir -p views
          cp ${
            config.legacyPackages.mkBuildStamp {
              inherit (finalAttrs) version;
              components."libghostty-vt" = inputs'.ghostty.packages.libghostty-vt.version;
            }
          }/sparkles-build-stamp views/
        '';

        # The terminal shells out to `fc-match` (fontconfig) at runtime to
        # resolve fonts (see apps/terminal/src/app.d). Under `nix run` PATH is
        # the ambient user environment, so wrap the binary to guarantee
        # fontconfig is reachable instead of relying on the user's PATH.
        postFixup = ''
          wrapProgram $out/bin/${finalAttrs.pname} \
            --set-default SPARKLES_TS_GRAMMAR_PATH ${config.packages.ts-grammars} \
            --prefix PATH : ${lib.makeBinPath [ pkgs.fontconfig ]}
        '';

        meta = {
          description = "A minimal terminal emulator using libghostty-vt";
          mainProgram = finalAttrs.pname;
        };
      });

      # CPU benchmark harness for the terminal. Pure D + core-cli (it only spawns
      # terminal binaries handed to it and reads /proc), so no raylib/ghostty
      # build inputs and no runtime wrapper are needed — just the default
      # `buildSparklesApp` closure (core-cli → base, math, test-runner shim+impl).
      packages.terminal-benchmark = config.legacyPackages.buildSparklesApp (finalAttrs: {
        pname = "terminal-benchmark";
        version = "0.1.0";

        meta = {
          description = "CPU/throughput benchmark harness for the sparkles:terminal emulator";
          mainProgram = finalAttrs.pname;
        };
      });

      # The sparkles:ui catalog. It reaches raylib through `sparkles:ui-app`'s
      # `full` configuration — the gallery itself names no backend — so the
      # build inputs are the window's, not the application's. libghostty-vt is
      # the Terminal page's: it embeds `sparkles:terminal-view` (TVW7).
      # `fc-match` is on the same footing as the terminal's: the font set
      # shells out to it at runtime, and under `nix run` PATH is the ambient
      # user environment.
      packages.ui-gallery = config.legacyPackages.buildSparklesApp (finalAttrs: {
        pname = "ui-gallery";
        version = "0.1.0";

        nativeBuildInputs = [ pkgs.pkg-config ];
        buildInputs = [
          pkgs.raylib
          inputs'.ghostty.packages.libghostty-vt
          inputs'.ghostty.packages.libghostty-vt.dev
        ];

        env = d-toolchain.env;

        postFixup = ''
          wrapProgram $out/bin/${finalAttrs.pname} \
            --prefix PATH : ${lib.makeBinPath [ pkgs.fontconfig ]}
        '';

        meta = {
          description = "A browsable catalog of the sparkles:ui toolkit, in a terminal or a window";
          mainProgram = finalAttrs.pname;
        };
      });

      apps.ci = {
        type = "app";
        program = lib.getExe config.packages.ci;
      };

      apps.ci-minimal = {
        type = "app";
        program = lib.getExe config.packages.ci-minimal;
      };

      packages.release = config.legacyPackages.buildSparklesApp (finalAttrs: {
        pname = "release";
        version = "0.1.0";

        # Deliberately UNWRAPPED. `release` shells out to git, `gh`, `ci` and —
        # for the publishing stages — fdroidserver, a JDK, rclone and
        # bundletool. Bundling all of that would put a Python and JVM toolchain
        # into the closure of a tool most often run to create a tag, and CI
        # would download it just to build an APK.
        #
        # So the lean package is the binary alone and takes its tools from
        # PATH, which the dev shell (and CI's own steps) already provide. When
        # it needs something it cannot find, the stage-aware check in
        # `sparkles.release.store.tools` names the missing program and what it
        # was for, rather than failing three steps in.
        #
        # `release-full` below is the same binary with everything pinned, for
        # the workstation that actually publishes.
        meta = {
          description = ''
            Cut a sparkles release: scan tags, summarize commits, suggest a bump,
            write notes, tag and publish
          '';
          mainProgram = finalAttrs.pname;
        };
      });

      # The publishing configuration: `release` with every external program it
      # can invoke pinned by this repository rather than resolved from PATH.
      #
      # This is what signs and publishes, so "whatever version happens to be
      # installed" is the wrong answer for all of them — an fdroidserver or
      # apksigner picked up from the environment would decide the bytes users
      # install.
      packages.release-full = pkgs.symlinkJoin {
        name = "release-full-${config.packages.release.version}";
        paths = [ config.packages.release ];
        nativeBuildInputs = [ pkgs.makeWrapper ];
        postBuild =
          let
            path = lib.makeBinPath [
              pkgs.gitMinimal
              pkgs.gh
              config.packages.ci
              pkgs.fdroidserver
              # fdroidserver shells out to keytool, jarsigner and jar, and the
              # nixpkgs package wraps only apksigner onto PATH — `fdroid
              # update` fails outright without a JDK.
              pkgs.jdk_headless
              pkgs.apksigner
              pkgs.rclone
              # Builds the App Bundle for the Play channel.
              pkgs.bundletool
            ];
          in
          ''
            wrapProgram $out/bin/release --prefix PATH : ${path}
          '';
        meta = {
          description = "release, with every publishing tool pinned (signing workstation)";
          mainProgram = "release";
        };
      };

      apps.release = {
        type = "app";
        program = lib.getExe config.packages.release;
      };
      # `nix run .#release-full` — the publishing configuration.
      apps.release-full = {
        type = "app";
        program = lib.getExe config.packages.release-full;
      };
      apps.terminal = {
        type = "app";
        program = lib.getExe config.packages.terminal;
      };

      apps.terminal-benchmark = {
        type = "app";
        program = lib.getExe config.packages.terminal-benchmark;
      };

      apps.ui-gallery = {
        type = "app";
        program = lib.getExe config.packages.ui-gallery;
      };
    };
}