From 134e450a8c0b7bd4c4c26fdb76e298214e8c35e0 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Sat, 25 Jul 2026 22:43:12 -0400 Subject: [PATCH] perf(nix): use npins for faster commands --- docs/dev/ralph/nix-eval-perf/README.md | 114 +++++++ flake.lock | 288 ------------------ flake.nix | 90 +++--- justfile | 3 +- nix/chrome-devtools/shell.nix | 18 +- nix/diagrams.nix | 52 ++++ nix/modules/flake-parts/devshell.nix | 19 -- nix/modules/flake-parts/diagrams.nix | 91 ------ nix/modules/flake-parts/extra/default.nix | 5 - .../flake-parts/extra/small-closure.nix | 65 ---- nix/modules/flake-parts/haskell.nix | 106 ------- nix/modules/flake-parts/nixpkgs.nix | 15 - nix/modules/flake-parts/pre-commit.nix | 36 --- nix/modules/flake-parts/toplevel.nix | 16 - nix/modules/flake-parts/website.nix | 20 -- nix/pre-commit.nix | 48 +++ nix/project.nix | 85 ++++++ nix/system.nix | 68 +++++ npins/default.nix | 249 +++++++++++++++ npins/sources.json | 189 ++++++++++++ tests/shell.nix | 19 +- 21 files changed, 869 insertions(+), 727 deletions(-) create mode 100644 docs/dev/ralph/nix-eval-perf/README.md delete mode 100644 flake.lock create mode 100644 nix/diagrams.nix delete mode 100644 nix/modules/flake-parts/devshell.nix delete mode 100644 nix/modules/flake-parts/diagrams.nix delete mode 100644 nix/modules/flake-parts/extra/default.nix delete mode 100644 nix/modules/flake-parts/extra/small-closure.nix delete mode 100644 nix/modules/flake-parts/haskell.nix delete mode 100644 nix/modules/flake-parts/nixpkgs.nix delete mode 100644 nix/modules/flake-parts/pre-commit.nix delete mode 100644 nix/modules/flake-parts/toplevel.nix delete mode 100644 nix/modules/flake-parts/website.nix create mode 100644 nix/pre-commit.nix create mode 100644 nix/project.nix create mode 100644 nix/system.nix create mode 100644 npins/default.nix create mode 100644 npins/sources.json diff --git a/docs/dev/ralph/nix-eval-perf/README.md b/docs/dev/ralph/nix-eval-perf/README.md new file mode 100644 index 000000000..4be796d35 --- /dev/null +++ b/docs/dev/ralph/nix-eval-perf/README.md @@ -0,0 +1,114 @@ +# Ralph: Nix command latency + +Iterative, measurement-driven reduction of the time developers wait for +Emanote's Nix developer shell and app. The shipped change follows the same +zero-input-flake design used by Kolu: the user-facing flake contains no flake +inputs, while npins supplies the same pinned revisions to standalone Nix +libraries. + +## Goal + +Reduce the warm latency of `nix develop` and `nix run` without changing the +Emanote package, app, docs, checks, or public flake modules. Cold evaluation is +measured as a guardrail, but the primary metric is the repeated command latency +felt during normal development. + +## Methodology + +Measurements were taken on `x86_64-linux` with Nix 2.34.7. Each result is the +median of five wall-clock runs. The baseline is `master` at +`9d0103737216e5f36f3642eadb31e39dcb9c95f1`; the final measurements use the +staged working tree. + +| Mode | Method | +| --- | --- | +| warm | Run the command once to populate Nix's evaluation cache, then measure five invocations | +| no eval cache | Add `--no-eval-cache` to each invocation | +| fresh client cache | Give each invocation a new `XDG_CACHE_HOME`; the Nix store remains populated | + +The measured commands are: + +```sh +nix develop --quiet -c true +nix run --quiet . -- --help +``` + +Fresh-client-cache runs include source resolution and may contact remotes. They +are not empty-store builds. + +## Before and after + +| Command | Mode | Baseline samples (s) | Final samples (s) | Baseline median | Final median | Delta | +| --- | --- | --- | --- | ---: | ---: | ---: | +| `nix develop` | warm | 0.28, 0.28, 0.28, 0.28, 0.29 | 0.22, 0.22, 0.23, 0.23, 0.23 | 0.28s | **0.23s** | **-17.9%** | +| `nix develop` | no eval cache | 2.67, 2.76, 2.77, 2.78, 3.13 | 2.52, 2.53, 2.58, 2.58, 2.62 | 2.77s | **2.58s** | **-6.9%** | +| `nix develop` | fresh client cache | 2.98, 3.06, 3.08, 3.22, 3.25 | 2.80, 2.83, 2.87, 2.87, 3.06 | 3.08s | **2.87s** | **-6.8%** | +| `nix run` | warm | 0.14, 0.15, 0.15, 0.15, 0.16 | 0.10, 0.11, 0.11, 0.11, 0.11 | 0.15s | **0.11s** | **-26.7%** | +| `nix run` | no eval cache | 1.63, 1.64, 1.68, 1.68, 1.70 | 1.76, 1.76, 1.78, 1.79, 1.82 | 1.68s | 1.78s | +6.0% | +| `nix run` | fresh client cache | 1.94, 1.96, 1.97, 1.99, 2.03 | 1.98, 1.99, 1.99, 2.00, 2.04 | 1.97s | 1.99s | +1.0% | + +The warm path improves substantially for both target commands. Developer-shell +cold evaluation also improves. The standalone `haskell-flake` path costs about +100 ms more when forcing app evaluation without Nix's evaluation cache; this is +retained because the normal app path is 40 ms faster and fresh-client-cache +latency is effectively unchanged. + +## Shipped design + +1. The root `flake.nix` has no inputs. `flake.lock` is replaced by npins while + retaining every dependency revision. +2. `haskell-flake` is evaluated through its documented standalone + `nix/lib.nix` API, without flake-parts or nixos-unified. +3. Runtime outputs disable `haskell-flake`'s development shell. The full + Haskell shell remains available to `nix develop`. +4. Formatting hooks move to `nix develop .#fmt`, so ordinary shell entry does + not evaluate git-hooks and fourmolu. `just fmt` selects that shell. +5. The diagram, Playwright, and Chrome DevTools shells consume the npins sources + directly. The public Emanote-site flake-parts module remains an output for + downstream users; it is not involved in evaluating this repository's root + flake. + +Update all pins with `npins update`, or update one pin with, for example, +`npins update nixpkgs-latest`. + +## Optimization log + +| Cycle | Mutation | Result | Decision | +| --- | --- | --- | --- | +| 1 | Put an npins compatibility layer under the existing nixos-unified/flake-parts graph | warm develop 0.34s; warm run 0.24s | Rejected: retained the expensive module graph and regressed both targets | +| 2 | Use the standalone `haskell-flake` library | warm develop 0.29s; warm run 0.14s | Retained as the simpler foundation | +| 3 | Load npins sources through nixpkgs fetchers | no-cache develop 3.07s; run 1.79s | Rejected: no improvement over the standard npins loader | +| 4 | Replace git-hooks evaluation with a checked-in pre-commit config and static packages | warm develop 0.41s; no-cache develop 3.16s | Rejected: regressed shell entry | +| 5 | Generate and check in `emanote/cabal.nix` | no-cache develop 3.32s; run 1.79s | Rejected: no improvement | +| 6 | Move formatting hooks to `.#fmt` | warm develop 0.22-0.23s | Retained: 18-21% shell improvement | +| 7 | Disable the Haskell dev shell for runtime-only outputs | warm run 0.10-0.11s | Retained: 27-33% app improvement | + +Three consecutive speculative changes failed the 3% improvement threshold, so +the loop stopped after isolating the two output-specific wins. + +## Compatibility checks + +- Output names remain `packages.{default,emanote,docs}`, + `apps.{default,emanote,docs}`, `devShells.{default,diagrams}`, and the existing + checks. `devShells.fmt` is additive. +- The evaluated `apps.x86_64-linux.default.program` store path is unchanged from + the baseline. +- `homeManagerModule`, the downstream `flakeModule`, the template, and the + Omnix CI contract remain exported. +- The pin migration preserves the revisions previously recorded in + `flake.lock`. + +## Reproduce locally + +Warm each command once, then time five runs: + +```sh +nix develop --quiet -c true +for _ in 1 2 3 4 5; do time nix develop --quiet -c true; done + +nix run --quiet . -- --help +for _ in 1 2 3 4 5; do time nix run --quiet . -- --help; done +``` + +Use `--no-eval-cache` for forced evaluation. For the fresh-client-cache metric, +set `XDG_CACHE_HOME` to a newly created temporary directory for each run. diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 4a8627bca..000000000 --- a/flake.lock +++ /dev/null @@ -1,288 +0,0 @@ -{ - "nodes": { - "commonmark-simple": { - "flake": false, - "locked": { - "lastModified": 1755566927, - "narHash": "sha256-2WE5SjwgwjKdsHxDRfQIb2WRrVYoNPiCIyG4HZd8Znk=", - "owner": "srid", - "repo": "commonmark-simple", - "rev": "81dc7a66b2490a701bfc87e6e892307665e6336e", - "type": "github" - }, - "original": { - "owner": "srid", - "ref": "0.2.0.0", - "repo": "commonmark-simple", - "type": "github" - } - }, - "commonmark-wikilink": { - "flake": false, - "locked": { - "lastModified": 1777254858, - "narHash": "sha256-dxjsU2SGG2txShwdXCl18ND18Y5Qbqt3J/FlrKhrkp4=", - "owner": "srid", - "repo": "commonmark-wikilink", - "rev": "e47ba496ceaf68ec119e56eb22670ae017b35a42", - "type": "github" - }, - "original": { - "owner": "srid", - "ref": "master", - "repo": "commonmark-wikilink", - "type": "github" - } - }, - "dpella-jsonrpc": { - "flake": false, - "locked": { - "lastModified": 1771256709, - "narHash": "sha256-gdBzrXo/tm9Xcs35yYiZkf9OI3S/FEV/5eGY183ywm4=", - "owner": "dpella", - "repo": "jsonrpc", - "rev": "0a708eb6c2744e1d69822d1cb90e9e352455a51b", - "type": "github" - }, - "original": { - "owner": "dpella", - "repo": "jsonrpc", - "rev": "0a708eb6c2744e1d69822d1cb90e9e352455a51b", - "type": "github" - } - }, - "dpella-mcp": { - "flake": false, - "locked": { - "lastModified": 1771505470, - "narHash": "sha256-wfh76/wO6j5JCIB++vfZn4LaFz1KAdy7hBjaFIpW5ZI=", - "owner": "dpella", - "repo": "mcp", - "rev": "52d13472d23ec11b9f6109f0fbf5159e9fda93da", - "type": "github" - }, - "original": { - "owner": "dpella", - "repo": "mcp", - "rev": "52d13472d23ec11b9f6109f0fbf5159e9fda93da", - "type": "github" - } - }, - "ema": { - "flake": false, - "locked": { - "lastModified": 1777214989, - "narHash": "sha256-bzypHo+Z/fLJRK1hTEuxCzK/ybxJixDrfML8M6cfDtk=", - "owner": "srid", - "repo": "ema", - "rev": "87da4f3c678b78bc3ee96225910a3932b3e8b6f9", - "type": "github" - }, - "original": { - "owner": "srid", - "repo": "ema", - "type": "github" - } - }, - "emanote-template": { - "flake": false, - "locked": { - "lastModified": 1745726040, - "narHash": "sha256-cLL9ylGe1OoN/KSX7+FRW3j8rm5iPTCleOJGSdOfdE0=", - "owner": "srid", - "repo": "emanote-template", - "rev": "1475b182fc136fc5da3a3b3e2b9be90795af19bb", - "type": "github" - }, - "original": { - "owner": "srid", - "repo": "emanote-template", - "type": "github" - } - }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1751413152, - "narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "77826244401ea9de6e3bac47c2db46005e1f30b5", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "fourmolu-nix": { - "locked": { - "lastModified": 1707266073, - "narHash": "sha256-tCFzZQJicDdYjnuJiNK4hiiRAH7c2wQzMhOCdUMbVKE=", - "owner": "jedimahdi", - "repo": "fourmolu-nix", - "rev": "717f5a91b0d7b97b1be7ecc3a0fd42d37ffe1c9b", - "type": "github" - }, - "original": { - "owner": "jedimahdi", - "repo": "fourmolu-nix", - "type": "github" - } - }, - "git-hooks": { - "flake": false, - "locked": { - "lastModified": 1748731907, - "narHash": "sha256-KVgK2PB1h5RNhHJzGn090XcW1i9Mq0FVh6qTVKsg2RU=", - "owner": "bmrips", - "repo": "git-hooks.nix", - "rev": "2749fc9197fd12231746ef685225eaf85fe087fd", - "type": "github" - }, - "original": { - "owner": "bmrips", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "haskell-flake": { - "locked": { - "lastModified": 1776783293, - "narHash": "sha256-Na95Y2awqZsLhFNfBNbLj0hk4zyE3eKUROB2o9Qdqi8=", - "owner": "srid", - "repo": "haskell-flake", - "rev": "f52ac89b2232dd50e5d1110416ebc5bbb09265bd", - "type": "github" - }, - "original": { - "owner": "srid", - "repo": "haskell-flake", - "type": "github" - } - }, - "heist-extra": { - "flake": false, - "locked": { - "lastModified": 1777548733, - "narHash": "sha256-YefkrB6jip7AV9ssRLngjSbp9xHE/xcSgfQMzDjpga0=", - "owner": "srid", - "repo": "heist-extra", - "rev": "85c433921629d0de9374bc5deaa866ec830748c2", - "type": "github" - }, - "original": { - "owner": "srid", - "repo": "heist-extra", - "type": "github" - } - }, - "lvar": { - "flake": false, - "locked": { - "lastModified": 1753226155, - "narHash": "sha256-Uwg5s0SlxLp83bXHA+6TIQV+L+lu8lwMpBdNuiuk7hg=", - "owner": "srid", - "repo": "lvar", - "rev": "cd110d4823ee7b8bbbf115a47c79bc304c3309cd", - "type": "github" - }, - "original": { - "owner": "srid", - "ref": "0.2.0.0", - "repo": "lvar", - "type": "github" - } - }, - "nixos-unified": { - "locked": { - "lastModified": 1751174231, - "narHash": "sha256-OLPo3ZI/gKH0C6P6l2W9RYm1ow/Jl4qBrasQ3rjAA0E=", - "owner": "srid", - "repo": "nixos-unified", - "rev": "05eb3d59d3b48460ea01c419702d4fc0c3210805", - "type": "github" - }, - "original": { - "owner": "srid", - "repo": "nixos-unified", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1752900028, - "narHash": "sha256-dPALCtmik9Wr14MGqVXm+OQcv7vhPBXcWNIOThGnB/Q=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "6b4955211758ba47fac850c040a27f23b9b4008f", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-latest": { - "locked": { - "lastModified": 1776949667, - "narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "commonmark-simple": "commonmark-simple", - "commonmark-wikilink": "commonmark-wikilink", - "dpella-jsonrpc": "dpella-jsonrpc", - "dpella-mcp": "dpella-mcp", - "ema": "ema", - "emanote-template": "emanote-template", - "flake-parts": "flake-parts", - "fourmolu-nix": "fourmolu-nix", - "git-hooks": "git-hooks", - "haskell-flake": "haskell-flake", - "heist-extra": "heist-extra", - "lvar": "lvar", - "nixos-unified": "nixos-unified", - "nixpkgs": "nixpkgs", - "nixpkgs-latest": "nixpkgs-latest", - "unionmount": "unionmount" - } - }, - "unionmount": { - "flake": false, - "locked": { - "lastModified": 1778504504, - "narHash": "sha256-SyJUttW4blD+1OW8yCMWndN1OxVP/pSJRmboIE9bQDM=", - "owner": "srid", - "repo": "unionmount", - "rev": "7b631ebe44d1b9e3e6bc39c9acb97645ddf8b9ba", - "type": "github" - }, - "original": { - "owner": "srid", - "repo": "unionmount", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix index cd4d0ab82..85195e929 100644 --- a/flake.nix +++ b/flake.nix @@ -4,44 +4,60 @@ extra-substituters = "https://cache.nixos.asia/oss"; extra-trusted-public-keys = "oss:KO872wNJkCDgmGN3xy9dT89WAhvv13EiKncTtHDItVU="; }; - inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; - # Independent pin tracking unstable HEAD, used by tests/shell.nix - # for playwright-driver. Decoupled from `nixpkgs` so the Haskell - # build's pin and the Playwright pin update on different cadences. - nixpkgs-latest.url = "github:nixos/nixpkgs/nixpkgs-unstable"; - flake-parts.url = "github:hercules-ci/flake-parts"; - flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; - haskell-flake.url = "github:srid/haskell-flake"; - fourmolu-nix.url = "github:jedimahdi/fourmolu-nix"; - git-hooks.url = "github:bmrips/git-hooks.nix"; - git-hooks.flake = false; - nixos-unified.url = "github:srid/nixos-unified"; - # These are not (necessarily) upstreamed to nixpkgs, yet. - ema.url = "github:srid/ema"; - ema.flake = false; - lvar.url = "github:srid/lvar/0.2.0.0"; - lvar.flake = false; - heist-extra.url = "github:srid/heist-extra"; - heist-extra.flake = false; - unionmount.url = "github:srid/unionmount"; - unionmount.flake = false; - commonmark-simple.url = "github:srid/commonmark-simple/0.2.0.0"; - commonmark-simple.flake = false; - commonmark-wikilink.url = "github:srid/commonmark-wikilink/master"; - commonmark-wikilink.flake = false; + # Keep this user-facing flake input-free. Nix verifies every flake input on + # each invocation; the same revisions are pinned by npins and consumed through + # the standalone libraries exposed by haskell-flake and the development tools. + outputs = { self }: + let + sources = import ./npins; + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + eachSystem = f: builtins.listToAttrs (map + (system: { + name = system; + value = f system; + }) + systems); + systemOutputs = eachSystem (system: import ./nix/system.nix { + inherit sources system; + root = ./.; + }); + outputFor = name: builtins.mapAttrs (_: output: output.${name}) systemOutputs; + in + { + packages = outputFor "packages"; + apps = outputFor "apps"; + devShells = outputFor "devShells"; + checks = outputFor "checks"; - emanote-template.url = "github:srid/emanote-template"; - emanote-template.flake = false; + homeManagerModule = { lib, pkgs, ... }: { + imports = [ ./nix/modules/home/emanote.nix ]; + services.emanote.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.default; + }; + flakeModule = ./nix/modules/flake-parts/flake-module; + templates.default = { + description = "A simple flake.nix template for emanote notebooks"; + path = builtins.path { + name = "emanote-template"; + path = sources.emanote-template; + filter = path: _: baseNameOf path == "flake.nix"; + }; + }; - # dpella/mcp is newer on GitHub than in nixpkgs' all-cabal-hashes, - # so pin the source directly. mcp-server/ and mcp-types/ are subdirs. - dpella-mcp.url = "github:dpella/mcp/52d13472d23ec11b9f6109f0fbf5159e9fda93da"; - dpella-mcp.flake = false; - dpella-jsonrpc.url = "github:dpella/jsonrpc/0a708eb6c2744e1d69822d1cb90e9e352455a51b"; - dpella-jsonrpc.flake = false; - }; - outputs = inputs: - inputs.nixos-unified.lib.mkFlake { inherit inputs; root = ./.; }; + # Retain the Omnix CI contract previously declared by the flake-parts + # small-closure module. Its system list is intentionally empty today. + om.ci.default.emanote = { + dir = "."; + steps.custom.closure-size = { + type = "app"; + name = "check-closure-size"; + systems = [ ]; + }; + }; + }; } diff --git a/justfile b/justfile index 1e464495a..c2adde219 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,6 @@ notebook := "../docs" nix_shell := if env('IN_NIX_SHELL', '') != '' { '' } else { 'nix develop -c' } +nix_fmt_shell := 'nix develop .#fmt -c' # List available recipes default: @@ -16,7 +17,7 @@ repl *ARGS: # Autoformat the project tree fmt: - {{nix_shell}} pre-commit run --all-files + {{nix_fmt_shell}} pre-commit run --all-files # Run the app using ghcid (with auto-reload / recompile) # To run against a custom notebook: diff --git a/nix/chrome-devtools/shell.nix b/nix/chrome-devtools/shell.nix index 53c568738..ec96f7d71 100644 --- a/nix/chrome-devtools/shell.nix +++ b/nix/chrome-devtools/shell.nix @@ -6,21 +6,11 @@ # server and defers Chrome provisioning to the consumer. Emanote is the # consumer here, so Chromium is pinned and wired into --executable-path. # -# nixpkgs is pulled straight from the top-level flake.lock so this shell -# stays consistent with the rest of the build without a second pin to -# maintain. +# nixpkgs is pulled straight from the top-level npins sources so this shell +# stays consistent with the rest of the build without a second pin to maintain. let - lock = builtins.fromJSON (builtins.readFile ../../flake.lock); - nixpkgsLocked = lock.nodes.nixpkgs.locked; - pkgs = import - (builtins.fetchTree { - type = "github"; - owner = nixpkgsLocked.owner; - repo = nixpkgsLocked.repo; - rev = nixpkgsLocked.rev; - narHash = nixpkgsLocked.narHash; - }) - { }; + sources = import ../../npins; + pkgs = import sources.nixpkgs { }; mcpVersion = "0.21.0"; # nixpkgs ships no Chromium derivation for Darwin (its meta.platforms # is Linux-only), so referencing pkgs.chromium fails eval on macOS. diff --git a/nix/diagrams.nix b/nix/diagrams.nix new file mode 100644 index 000000000..f1d483f81 --- /dev/null +++ b/nix/diagrams.nix @@ -0,0 +1,52 @@ +{ pkgs }: +let + inherit (pkgs) lib; + diagramsCetzVersion = "0.3.4"; + versionAttr = version: "cetz_" + lib.replaceStrings [ "." ] [ "_" ] version; + diagramEngineBins = [ pkgs.d2 pkgs.typst ]; + diagramsTypstPackageRoot = + let + # nixpkgs stores each Typst package below lib/typst-packages, while + # TYPST_PACKAGE_PATH expects /preview//. + typstPackages = [ + pkgs.typstPackages.${versionAttr diagramsCetzVersion} + pkgs.typstPackages.oxifmt_0_2_1 + ]; + in + pkgs.runCommand "emanote-typst-packages" { } '' + mkdir -p $out/preview + ${lib.concatMapStringsSep "\n" (package: '' + for entry in ${package}/lib/typst-packages/*/*; do + name=$(basename "$(dirname "$entry")") + version=$(basename "$entry") + mkdir -p "$out/preview/$name" + ln -s "$entry" "$out/preview/$name/$version" + done + '') typstPackages} + ''; + devShell = pkgs.mkShell { + name = "emanote-diagrams"; + packages = diagramEngineBins; + TYPST_PACKAGE_PATH = diagramsTypstPackageRoot; + EMANOTE_CETZ_VERSION = diagramsCetzVersion; + }; + check = pkgs.runCommand "diagrams-typst-offline" + { + nativeBuildInputs = [ pkgs.typst ]; + TYPST_PACKAGE_PATH = diagramsTypstPackageRoot; + } + '' + cat > smoke.typ <//`, which Typst can't read - # directly — its `TYPST_PACKAGE_PATH` resolver wants - # `/preview///`. The runCommand below - # reshapes the layout via symlinks. A proper fix is in nixpkgs: - # either expose a `typstPackages.makeOfflineRoot` helper, or - # ship each package at the `preview/`-shaped path so consumers - # can `symlinkJoin` directly. - # - # HACK: transitive `@preview/…` imports are listed manually - # because typst-packages propagate via `propagatedBuildInputs` - # but we need a path-shaped layout, not a build-input closure. - # cetz 0.3.4 imports `@preview/oxifmt:0.2.1` from `src/util.typ`; - # when bumping `diagramsCetzVersion`, grep the new release's - # source for `@preview/...` and update this list. The proper - # fix would be a closure walker that reads each package's - # `typst.toml`/sources and recursively pulls every - # `@preview/:` it imports. - typstPackages = [ - pkgs.typstPackages.${versionAttr diagramsCetzVersion} - pkgs.typstPackages.oxifmt_0_2_1 # transitive dep of cetz 0.3.4 - ]; - in - pkgs.runCommand "emanote-typst-packages" { } '' - mkdir -p $out/preview - ${lib.concatMapStringsSep "\n" (p: '' - for entry in ${p}/lib/typst-packages/*/*; do - name=$(basename "$(dirname "$entry")") - version=$(basename "$entry") - mkdir -p "$out/preview/$name" - ln -s "$entry" "$out/preview/$name/$version" - done - '') typstPackages} - ''; - in - { - # A self-contained shell that carries the diagram engines on PATH - # plus the env vars the bundled `lua-filters/diagram.lua` reads - # (`TYPST_PACKAGE_PATH`, `EMANOTE_CETZ_VERSION`). `devshell.nix` - # picks this up via `inputsFrom`; `haskell.nix` reads the raw - # values off `passthru` for `wrapProgram`. Adding a new engine - # edits this file alone. - devShells.diagrams = pkgs.mkShell { - name = "emanote-diagrams"; - packages = diagramEngineBins; - TYPST_PACKAGE_PATH = diagramsTypstPackageRoot; - EMANOTE_CETZ_VERSION = diagramsCetzVersion; - passthru = { - inherit diagramsCetzVersion diagramEngineBins diagramsTypstPackageRoot; - }; - }; - # Compile a minimal cetz document against the offline package - # root. A missing transitive `@preview/…` import surfaces here - # rather than at note-render time as a `Pandoc Lua filter error` - # banner. - checks.diagrams-typst-offline = pkgs.runCommand "diagrams-typst-offline" - { - nativeBuildInputs = [ pkgs.typst ]; - TYPST_PACKAGE_PATH = diagramsTypstPackageRoot; - } '' - cat > smoke.typ < - # "x86_64-linux" - - # Disabling on macOS due to upstream bug: - # - https://github.com/NixOS/nixpkgs/issues/318013 - # - https://github.com/NixOS/nixpkgs/pull/304352 - # - Related: https://github.com/NixOS/nix/pull/10877 - # "aarch64-darwin" - ]; -in -{ - perSystem = { pkgs, lib, config, system, ... }: lib.mkIf (lib.elem system allowedSystems) { - haskellProjects.default = { - settings = { - emanote = { name, pkgs, self, super, ... }: { - separateBinOutput = false; # removeReferencesTo.nix doesn't work otherwise - justStaticExecutables = true; - removeReferencesTo = [ - self.ghc - self.pandoc - self.pandoc-types - self.warp - ]; - }; - }; - }; - - apps.check-closure-size = rec { - inherit (program) meta; - program = pkgs.writeShellApplication { - name = "emanote-check-closure-size"; - runtimeInputs = [ pkgs.jq pkgs.bc pkgs.nix ]; - meta.description = "Check that emanote's nix closure size remains reasonably small"; - text = '' - MAX_CLOSURE_SIZE=$(echo "600 * 1000000" | bc) - CLOSURE_SIZE=$(nix path-info --json -S .#default | jq 'first(.[])'.closureSize) - echo "Emanote closure size: $CLOSURE_SIZE" - echo " Max closure size: $MAX_CLOSURE_SIZE" - if [ "$CLOSURE_SIZE" -gt "$MAX_CLOSURE_SIZE" ]; then - echo "ERROR: Emanote's nix closure size has increased" - exit 3 - else - echo "OK: Emanote's nix closure size is within limits" - fi - ''; - }; - }; - }; - - flake.om.ci.default.emanote = { - dir = "."; - steps.custom = { - closure-size = { - type = "app"; - name = "check-closure-size"; - systems = allowedSystems; - }; - }; - }; -} diff --git a/nix/modules/flake-parts/haskell.nix b/nix/modules/flake-parts/haskell.nix deleted file mode 100644 index a059a11f7..000000000 --- a/nix/modules/flake-parts/haskell.nix +++ /dev/null @@ -1,106 +0,0 @@ -{ root, inputs, ... }: -{ - imports = [ - inputs.haskell-flake.flakeModule - ]; - - perSystem = { pkgs, lib, config, system, ... }: { - # haskell-flake configuration - haskellProjects.default = { - projectFlakeName = "emanote"; - - # Whitelist to avoid unnecessary rebuilds of Nix package. - projectRoot = builtins.toString (lib.fileset.toSource { - inherit root; - fileset = lib.fileset.unions [ - (root + /emanote) - (root + /cabal.project) - ]; - }); - - devShell.tools = hp: { - inherit (pkgs) - stork - tailwindcss_4; - }; - autoWire = [ "packages" "apps" "checks" ]; - packages = { - unionmount.source = inputs.unionmount; - commonmark-simple.source = inputs.commonmark-simple; - commonmark-wikilink.source = inputs.commonmark-wikilink; - fsnotify.source = "0.4.1.0"; # Not in nixpkgs, yet. - ghcid.source = "0.8.8"; - heist-extra.source = inputs.heist-extra; - - # MCP (Model Context Protocol) server library. - # Pulled from GitHub because 0.3.1.0 isn't in the pinned - # all-cabal-hashes snapshot and mcp-types isn't packaged in nixpkgs. - mcp.source = inputs.dpella-mcp + /mcp-server; - mcp-types.source = inputs.dpella-mcp + /mcp-types; - # jsonrpc is an mcp-types dependency missing from nixpkgs. - jsonrpc.source = inputs.dpella-jsonrpc; - - ema.source = inputs.ema + /ema; - ema-generics.source = inputs.ema + /ema-generics; - ema-extra.source = inputs.ema + /ema-extra; - lvar.source = inputs.lvar; - }; - - settings = { - # Haskell packages in nixpkgs are often broken in many ways; ergo, - # it is our responsibility to fix them here. - fsnotify.check = false; - heist.broken = false; - ixset-typed.broken = false; - ixset-typed.jailbreak = true; - pandoc-link-context.broken = false; - pandoc-link-context.jailbreak = true; - tagtree.broken = false; - tagtree.jailbreak = true; - unionmount.check = !pkgs.stdenv.isDarwin; # garnix: Slow M1 builder - emanote = { name, pkgs, self, super, ... }: - let - # https://github.com/NixOS/cabal2nix/issues/608 - addMeta = pkg: pkg.overrideAttrs (oldAttrs: { - meta = (oldAttrs.meta or { }) // { - longDescription = '' - Emanote is a tool for generating a structured view of your - plain-text notes on the web, as a statically generated - website as well as a local live server. - - For editing notes, you can use any text editor of your - choice including the likes of Obsidian. - ''; - }; - }); - # Wrap the binary with the diagram-engine binaries and the - # offline Typst package cache the bundled `diagram.lua` - # filter needs. The engine set, package root, and cetz - # version all hang off `devShells.diagrams.passthru` in - # `nix/modules/flake-parts/diagrams.nix` — adding a new - # engine edits one file, not this one. `--set-default` lets - # a user with their own typst-package cache override - # Emanote's. - diagrams = config.devShells.diagrams.passthru; - wrapDiagramEngines = pkg: pkg.overrideAttrs (oldAttrs: { - nativeBuildInputs = (oldAttrs.nativeBuildInputs or [ ]) ++ [ pkgs.makeWrapper ]; - postInstall = (oldAttrs.postInstall or "") + '' - wrapProgram $out/bin/emanote \ - --prefix PATH : ${lib.makeBinPath diagrams.diagramEngineBins} \ - --set-default TYPST_PACKAGE_PATH ${diagrams.diagramsTypstPackageRoot} \ - --set-default EMANOTE_CETZ_VERSION ${diagrams.diagramsCetzVersion} - ''; - }); - in - { - check = false; - extraBuildDepends = [ pkgs.stork pkgs.tailwindcss_4 ]; - custom = pkg: wrapDiagramEngines (addMeta pkg); - }; - }; - }; - - packages.default = config.packages.emanote; - apps.default = config.apps.emanote; - }; -} diff --git a/nix/modules/flake-parts/nixpkgs.nix b/nix/modules/flake-parts/nixpkgs.nix deleted file mode 100644 index deb215e44..000000000 --- a/nix/modules/flake-parts/nixpkgs.nix +++ /dev/null @@ -1,15 +0,0 @@ -{ inputs, ... }: -{ - perSystem = { pkgs, lib, config, system, ... }: { - _module.args = import inputs.nixpkgs { - inherit system; - overlays = [ - (self: super: { - # Stork is marked as broken on intel mac, but it does work. - # Unfortunately we cannot test this code PATH due to lack of CI for intel mac (#335). - stork = if system == "x86_64-darwin" then super.stork.overrideAttrs (_oa: { meta.broken = false; }) else super.stork; - }) - ]; - }; - }; -} diff --git a/nix/modules/flake-parts/pre-commit.nix b/nix/modules/flake-parts/pre-commit.nix deleted file mode 100644 index c4db9b6d3..000000000 --- a/nix/modules/flake-parts/pre-commit.nix +++ /dev/null @@ -1,36 +0,0 @@ -{ inputs, ... }: -{ - imports = [ - (inputs.git-hooks + /flake-module.nix) - inputs.fourmolu-nix.flakeModule - ]; - perSystem = { config, ... }: { - pre-commit.settings = { - hooks = { - nixpkgs-fmt.enable = true; - fourmolu = { - enable = true; - package = config.fourmolu.wrapper; - excludes = [ "vira\.hs" ]; - }; - hlint = { - enable = true; - excludes = [ "vira\.h" ]; - }; - cabal-fmt.enable = true; - }; - }; - - fourmolu.settings = { - indentation = 2; - comma-style = "leading"; - record-brace-space = true; - indent-wheres = true; - import-export-style = "diff-friendly"; - respectful = true; - haddock-style = "multi-line"; - newlines-between-decls = 1; - extensions = [ "ImportQualifiedPost" ]; - }; - }; -} diff --git a/nix/modules/flake-parts/toplevel.nix b/nix/modules/flake-parts/toplevel.nix deleted file mode 100644 index a5243937c..000000000 --- a/nix/modules/flake-parts/toplevel.nix +++ /dev/null @@ -1,16 +0,0 @@ -{ root, inputs, self, ... }: -{ - flake = { - homeManagerModule = { lib, pkgs, ... }: { - imports = [ - (root + /nix/modules/home/emanote.nix) - ]; - services.emanote.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.default; - }; - flakeModule = (root + /nix/modules/flake-parts/flake-module); - templates.default = { - description = "A simple flake.nix template for emanote notebooks"; - path = builtins.path { path = inputs.emanote-template; filter = path: _: baseNameOf path == "flake.nix"; }; - }; - }; -} diff --git a/nix/modules/flake-parts/website.nix b/nix/modules/flake-parts/website.nix deleted file mode 100644 index af9b2374c..000000000 --- a/nix/modules/flake-parts/website.nix +++ /dev/null @@ -1,20 +0,0 @@ -{ root, ... }: -{ - perSystem = { pkgs, lib, config, system, ... }: { - emanote = { - sites = { - "docs" = { - package = config.packages.default; - layers = [{ path = (root + /docs); pathString = "./docs"; }]; - allowBrokenInternalLinks = true; # A couple, by design, in markdown.md - allowBrokenLuaFilters = true; # `lua-filters/writing-filters.md` demos the error path - extraConfig = { - template = { - urlStrategy = "pretty"; - }; - }; - }; - }; - }; - }; -} diff --git a/nix/pre-commit.nix b/nix/pre-commit.nix new file mode 100644 index 000000000..7ef5b9868 --- /dev/null +++ b/nix/pre-commit.nix @@ -0,0 +1,48 @@ +{ pkgs, root, sources }: +let + fourmolu = (import "${sources.fourmolu-nix}/lib").mkWrapper pkgs { + settings = { + indentation = 2; + comma-style = "leading"; + record-brace-space = true; + indent-wheres = true; + import-export-style = "diff-friendly"; + respectful = true; + haddock-style = "multi-line"; + newlines-between-decls = 1; + extensions = [ "ImportQualifiedPost" ]; + }; + }; + tools = import "${sources.git-hooks}/nix/call-tools.nix" pkgs; + run = pkgs.callPackage "${sources.git-hooks}/nix/run.nix" { + inherit pkgs tools; + isFlakes = true; + gitignore-nix-src = { }; + }; + check = run { + src = root; + hooks = { + nixpkgs-fmt = { + enable = true; + excludes = [ "npins/default\\.nix" ]; + }; + fourmolu = { + enable = true; + package = fourmolu; + excludes = [ "vira\\.hs" ]; + }; + hlint = { + enable = true; + excludes = [ "vira\\.h" ]; + }; + cabal-fmt.enable = true; + }; + }; + devShell = pkgs.mkShell { + inherit (check) shellHook; + packages = check.enabledPackages; + }; +in +{ + inherit check devShell; +} diff --git a/nix/project.nix b/nix/project.nix new file mode 100644 index 000000000..67745953c --- /dev/null +++ b/nix/project.nix @@ -0,0 +1,85 @@ +{ diagrams, pkgs, root, sources, withDevShell ? true }: +let + inherit (pkgs) lib; + projectRoot = builtins.toString (lib.fileset.toSource { + inherit root; + fileset = lib.fileset.unions [ + (root + /emanote) + (root + /cabal.project) + ]; + }); +in +(import "${sources.haskell-flake}/nix/lib.nix" { inherit pkgs; }).evalHaskellProject { + inherit projectRoot; + modules = [{ + devShell = + if withDevShell then { + tools = _hp: { + inherit (pkgs) stork tailwindcss_4; + }; + } else { + enable = false; + }; + packages = { + unionmount.source = sources.unionmount; + commonmark-simple.source = sources.commonmark-simple; + commonmark-wikilink.source = sources.commonmark-wikilink; + fsnotify.source = "0.4.1.0"; + ghcid.source = "0.8.8"; + heist-extra.source = sources.heist-extra; + mcp.source = sources.dpella-mcp + /mcp-server; + mcp-types.source = sources.dpella-mcp + /mcp-types; + jsonrpc.source = sources.dpella-jsonrpc; + ema.source = sources.ema + /ema; + ema-generics.source = sources.ema + /ema-generics; + ema-extra.source = sources.ema + /ema-extra; + lvar.source = sources.lvar; + }; + settings = { + fsnotify.check = false; + heist.broken = false; + ixset-typed = { + broken = false; + jailbreak = true; + }; + pandoc-link-context = { + broken = false; + jailbreak = true; + }; + tagtree = { + broken = false; + jailbreak = true; + }; + unionmount.check = !pkgs.stdenv.isDarwin; + emanote = { ... }: + let + addMeta = package: package.overrideAttrs (old: { + meta = (old.meta or { }) // { + longDescription = '' + Emanote is a tool for generating a structured view of your + plain-text notes on the web, as a statically generated + website as well as a local live server. + + For editing notes, you can use any text editor of your + choice including the likes of Obsidian. + ''; + }; + }); + wrapDiagramEngines = package: package.overrideAttrs (old: { + nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ pkgs.makeWrapper ]; + postInstall = (old.postInstall or "") + '' + wrapProgram $out/bin/emanote \ + --prefix PATH : ${lib.makeBinPath diagrams.diagramEngineBins} \ + --set-default TYPST_PACKAGE_PATH ${diagrams.diagramsTypstPackageRoot} \ + --set-default EMANOTE_CETZ_VERSION ${diagrams.diagramsCetzVersion} + ''; + }); + in + { + check = false; + extraBuildDepends = [ pkgs.stork pkgs.tailwindcss_4 ]; + custom = package: wrapDiagramEngines (addMeta package); + }; + }; + }]; +} diff --git a/nix/system.nix b/nix/system.nix new file mode 100644 index 000000000..59cb92533 --- /dev/null +++ b/nix/system.nix @@ -0,0 +1,68 @@ +{ root, sources, system }: +let + pkgs = import sources.nixpkgs { + inherit system; + overlays = [ + (_final: previous: { + stork = if system == "x86_64-darwin" then previous.stork.overrideAttrs (_: { meta.broken = false; }) else previous.stork; + }) + ]; + }; + inherit (pkgs) lib; + diagrams = import ./diagrams.nix { inherit pkgs; }; + runtimeProject = import ./project.nix { + inherit diagrams pkgs root sources; + withDevShell = false; + }; + devProject = import ./project.nix { inherit diagrams pkgs root sources; }; + preCommit = import ./pre-commit.nix { inherit pkgs root sources; }; + emanote = runtimeProject.packages.emanote.package; + docs = (lib.evalModules { + modules = [ + ./modules/flake-parts/flake-module/site + { + package = emanote; + layers = [{ path = root + /docs; pathString = "./docs"; }]; + allowBrokenInternalLinks = true; + allowBrokenLuaFilters = true; + extraConfig.template.urlStrategy = "pretty"; + } + ]; + specialArgs = { + inherit pkgs; + name = "docs"; + inputs' = { }; + }; + }).config.outputs; +in +{ + packages = { + default = emanote; + inherit emanote; + docs = docs.package; + }; + apps = { + default = runtimeProject.apps.emanote; + emanote = runtimeProject.apps.emanote; + docs = docs.app // { program = lib.getExe docs.app.program; }; + }; + devShells = { + default = lib.addMetaAttrs { description = "Emanote development environment"; } + (pkgs.mkShell { + name = "emanote-dev"; + inputsFrom = [ devProject.devShell diagrams.devShell ]; + packages = [ pkgs.just ]; + }); + diagrams = diagrams.devShell; + fmt = lib.addMetaAttrs { description = "Emanote formatting environment"; } + (pkgs.mkShell { + name = "emanote-fmt"; + inputsFrom = [ preCommit.devShell ]; + }); + }; + checks = { + diagrams-typst-offline = diagrams.check; + docs = docs.check; + pre-commit = preCommit.check; + }; +} diff --git a/npins/default.nix b/npins/default.nix new file mode 100644 index 000000000..884fc8cc5 --- /dev/null +++ b/npins/default.nix @@ -0,0 +1,249 @@ +/* + This file is provided under the MIT licence: + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +# Generated by npins. Do not modify; will be overwritten regularly +let + # Backwards-compatibly make something that previously didn't take any arguments take some + # The function must return an attrset, and will unfortunately be eagerly evaluated + # Same thing, but it catches eval errors on the default argument so that one may still call it with other arguments + mkFunctor = + fn: + let + e = builtins.tryEval (fn { }); + in + (if e.success then e.value else { error = fn { }; }) // { __functor = _self: fn; }; + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 + range = + first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 + stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 + stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); + concatStrings = builtins.concatStringsSep ""; + + # If the environment variable NPINS_OVERRIDE_${name} is set, then use + # the path directly as opposed to the fetched source. + # (Taken from Niv for compatibility) + mayOverride = + name: path: + let + envVarName = "NPINS_OVERRIDE_${saneName}"; + saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; + ersatz = builtins.getEnv envVarName; + in + if ersatz == "" then + path + else + # this turns the string into an actual Nix path (for both absolute and + # relative paths) + builtins.trace "Overriding path of \"${name}\" with \"${ersatz}\" due to set \"${envVarName}\"" ( + if builtins.substring 0 1 ersatz == "/" then + /. + ersatz + else + /. + builtins.getEnv "PWD" + "/${ersatz}" + ); + + mkSource = + name: spec: + { + pkgs ? null, + }: + assert spec ? type; + let + # Unify across builtin and pkgs fetchers. + # `fetchGit` requires a wrapper because of slight API differences. + fetchers = + if pkgs == null then + { + inherit (builtins) fetchTarball fetchurl; + # For some fucking reason, fetchGit has a different signature than the other builtin fetchers … + fetchGit = args: (builtins.fetchGit args).outPath; + } + else + { + fetchTarball = + { + url, + sha256, + }: + pkgs.fetchzip { + inherit url sha256; + extension = "tar"; + }; + inherit (pkgs) fetchurl; + fetchGit = + { + url, + submodules, + rev, + name, + narHash, + }: + pkgs.fetchgit { + inherit url rev name; + fetchSubmodules = submodules; + hash = narHash; + }; + }; + + # Dispatch to the correct code path based on the type + path = + if spec.type == "Git" then + mkGitSource fetchers spec + else if spec.type == "GitRelease" then + mkGitSource fetchers spec + else if spec.type == "PyPi" then + mkPyPiSource fetchers spec + else if spec.type == "Channel" then + mkChannelSource fetchers spec + else if spec.type == "Tarball" then + mkTarballSource fetchers spec + else if spec.type == "Container" then + mkContainerSource pkgs spec + else + builtins.throw "Unknown source type ${spec.type}"; + in + spec // { outPath = mayOverride name path; }; + + mkGitSource = + { + fetchTarball, + fetchGit, + ... + }: + { + repository, + revision, + url ? null, + submodules, + hash, + ... + }: + assert repository ? type; + # At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository + # In the latter case, there we will always be an url to the tarball + if url != null && !submodules then + fetchTarball { + inherit url; + sha256 = hash; + } + else + let + url = + if repository.type == "Git" then + repository.url + else if repository.type == "GitHub" then + "https://github.com/${repository.owner}/${repository.repo}.git" + else if repository.type == "GitLab" then + "${repository.server}/${repository.repo_path}.git" + else if repository.type == "Forgejo" then + "${repository.server}/${repository.owner}/${repository.repo}.git" + else + throw "Unrecognized repository type ${repository.type}"; + urlToName = + url: rev: + let + matched = builtins.match "^.*/([^/]*)(\\.git)?$" url; + + short = builtins.substring 0 7 rev; + + appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else ""; + in + "${if matched == null then "source" else builtins.head matched}${appendShort}"; + name = urlToName url revision; + in + fetchGit { + rev = revision; + narHash = hash; + + inherit name submodules url; + }; + + mkPyPiSource = + { fetchurl, ... }: + { + url, + hash, + ... + }: + fetchurl { + inherit url; + sha256 = hash; + }; + + mkChannelSource = + { fetchTarball, ... }: + { + url, + hash, + ... + }: + fetchTarball { + inherit url; + sha256 = hash; + }; + + mkTarballSource = + { fetchTarball, ... }: + { + url, + locked_url ? url, + hash, + ... + }: + fetchTarball { + url = locked_url; + sha256 = hash; + }; + + mkContainerSource = + pkgs: + { + image_name, + image_tag, + image_digest, + ... + }: + if pkgs == null then + builtins.throw "container sources require passing in a Nixpkgs value: https://github.com/andir/npins/blob/master/README.md#using-the-nixpkgs-fetchers" + else + pkgs.dockerTools.pullImage { + imageName = image_name; + imageDigest = image_digest; + finalImageTag = image_tag; + }; +in +mkFunctor ( + { + input ? ./sources.json, + }: + let + data = + if builtins.isPath input then + # while `readFile` will throw an error anyways if the path doesn't exist, + # we still need to check beforehand because *our* error can be caught but not the one from the builtin + # *piegames sighs* + if builtins.pathExists input then + builtins.fromJSON (builtins.readFile input) + else + throw "Input path ${toString input} does not exist" + else if builtins.isAttrs input then + input + else + throw "Unsupported input type ${builtins.typeOf input}, must be a path or an attrset"; + version = data.version; + in + if version == 7 then + builtins.mapAttrs (name: spec: mkFunctor (mkSource name spec)) data.pins + else + throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`" +) diff --git a/npins/sources.json b/npins/sources.json new file mode 100644 index 000000000..dbf2f9fc9 --- /dev/null +++ b/npins/sources.json @@ -0,0 +1,189 @@ +{ + "pins": { + "commonmark-simple": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "srid", + "repo": "commonmark-simple" + }, + "branch": "master", + "submodules": false, + "revision": "81dc7a66b2490a701bfc87e6e892307665e6336e", + "url": "https://github.com/srid/commonmark-simple/archive/81dc7a66b2490a701bfc87e6e892307665e6336e.tar.gz", + "hash": "sha256-2WE5SjwgwjKdsHxDRfQIb2WRrVYoNPiCIyG4HZd8Znk=", + "frozen": true + }, + "commonmark-wikilink": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "srid", + "repo": "commonmark-wikilink" + }, + "branch": "master", + "submodules": false, + "revision": "e47ba496ceaf68ec119e56eb22670ae017b35a42", + "url": "https://github.com/srid/commonmark-wikilink/archive/e47ba496ceaf68ec119e56eb22670ae017b35a42.tar.gz", + "hash": "sha256-dxjsU2SGG2txShwdXCl18ND18Y5Qbqt3J/FlrKhrkp4=" + }, + "dpella-jsonrpc": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "dpella", + "repo": "jsonrpc" + }, + "branch": "main", + "submodules": false, + "revision": "0a708eb6c2744e1d69822d1cb90e9e352455a51b", + "url": "https://github.com/dpella/jsonrpc/archive/0a708eb6c2744e1d69822d1cb90e9e352455a51b.tar.gz", + "hash": "sha256-gdBzrXo/tm9Xcs35yYiZkf9OI3S/FEV/5eGY183ywm4=" + }, + "dpella-mcp": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "dpella", + "repo": "mcp" + }, + "branch": "main", + "submodules": false, + "revision": "52d13472d23ec11b9f6109f0fbf5159e9fda93da", + "url": "https://github.com/dpella/mcp/archive/52d13472d23ec11b9f6109f0fbf5159e9fda93da.tar.gz", + "hash": "sha256-wfh76/wO6j5JCIB++vfZn4LaFz1KAdy7hBjaFIpW5ZI=" + }, + "ema": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "srid", + "repo": "ema" + }, + "branch": "master", + "submodules": false, + "revision": "87da4f3c678b78bc3ee96225910a3932b3e8b6f9", + "url": "https://github.com/srid/ema/archive/87da4f3c678b78bc3ee96225910a3932b3e8b6f9.tar.gz", + "hash": "sha256-bzypHo+Z/fLJRK1hTEuxCzK/ybxJixDrfML8M6cfDtk=" + }, + "emanote-template": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "srid", + "repo": "emanote-template" + }, + "branch": "master", + "submodules": false, + "revision": "1475b182fc136fc5da3a3b3e2b9be90795af19bb", + "url": "https://github.com/srid/emanote-template/archive/1475b182fc136fc5da3a3b3e2b9be90795af19bb.tar.gz", + "hash": "sha256-cLL9ylGe1OoN/KSX7+FRW3j8rm5iPTCleOJGSdOfdE0=" + }, + "fourmolu-nix": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "jedimahdi", + "repo": "fourmolu-nix" + }, + "branch": "main", + "submodules": false, + "revision": "717f5a91b0d7b97b1be7ecc3a0fd42d37ffe1c9b", + "url": "https://github.com/jedimahdi/fourmolu-nix/archive/717f5a91b0d7b97b1be7ecc3a0fd42d37ffe1c9b.tar.gz", + "hash": "sha256-tCFzZQJicDdYjnuJiNK4hiiRAH7c2wQzMhOCdUMbVKE=" + }, + "git-hooks": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "bmrips", + "repo": "git-hooks.nix" + }, + "branch": "master", + "submodules": false, + "revision": "2749fc9197fd12231746ef685225eaf85fe087fd", + "url": "https://github.com/bmrips/git-hooks.nix/archive/2749fc9197fd12231746ef685225eaf85fe087fd.tar.gz", + "hash": "sha256-KVgK2PB1h5RNhHJzGn090XcW1i9Mq0FVh6qTVKsg2RU=" + }, + "haskell-flake": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "srid", + "repo": "haskell-flake" + }, + "branch": "master", + "submodules": false, + "revision": "f52ac89b2232dd50e5d1110416ebc5bbb09265bd", + "url": "https://github.com/srid/haskell-flake/archive/f52ac89b2232dd50e5d1110416ebc5bbb09265bd.tar.gz", + "hash": "sha256-Na95Y2awqZsLhFNfBNbLj0hk4zyE3eKUROB2o9Qdqi8=" + }, + "heist-extra": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "srid", + "repo": "heist-extra" + }, + "branch": "master", + "submodules": false, + "revision": "85c433921629d0de9374bc5deaa866ec830748c2", + "url": "https://github.com/srid/heist-extra/archive/85c433921629d0de9374bc5deaa866ec830748c2.tar.gz", + "hash": "sha256-YefkrB6jip7AV9ssRLngjSbp9xHE/xcSgfQMzDjpga0=" + }, + "lvar": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "srid", + "repo": "lvar" + }, + "branch": "master", + "submodules": false, + "revision": "cd110d4823ee7b8bbbf115a47c79bc304c3309cd", + "url": "https://github.com/srid/lvar/archive/cd110d4823ee7b8bbbf115a47c79bc304c3309cd.tar.gz", + "hash": "sha256-Uwg5s0SlxLp83bXHA+6TIQV+L+lu8lwMpBdNuiuk7hg=", + "frozen": true + }, + "nixpkgs": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "nixos", + "repo": "nixpkgs" + }, + "branch": "nixpkgs-unstable", + "submodules": false, + "revision": "6b4955211758ba47fac850c040a27f23b9b4008f", + "url": "https://github.com/nixos/nixpkgs/archive/6b4955211758ba47fac850c040a27f23b9b4008f.tar.gz", + "hash": "sha256-dPALCtmik9Wr14MGqVXm+OQcv7vhPBXcWNIOThGnB/Q=" + }, + "nixpkgs-latest": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "nixos", + "repo": "nixpkgs" + }, + "branch": "nixpkgs-unstable", + "submodules": false, + "revision": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", + "url": "https://github.com/nixos/nixpkgs/archive/01fbdeef22b76df85ea168fbfe1bfd9e63681b30.tar.gz", + "hash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=" + }, + "unionmount": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "srid", + "repo": "unionmount" + }, + "branch": "master", + "submodules": false, + "revision": "7b631ebe44d1b9e3e6bc39c9acb97645ddf8b9ba", + "url": "https://github.com/srid/unionmount/archive/7b631ebe44d1b9e3e6bc39c9acb97645ddf8b9ba.tar.gz", + "hash": "sha256-SyJUttW4blD+1OW8yCMWndN1OxVP/pSJRmboIE9bQDM=" + } + }, + "version": 7 +} diff --git a/tests/shell.nix b/tests/shell.nix index 304ef1e44..dcd76102e 100644 --- a/tests/shell.nix +++ b/tests/shell.nix @@ -5,26 +5,17 @@ # `npx playwright install --with-deps` (which shells out to # `sudo apt-get`) cannot work. # -# nixpkgs is read from the parent flake's `nixpkgs-latest` input — -# kept independent from the main `nixpkgs` input (which the Haskell +# nixpkgs is read from the parent project's `nixpkgs-latest` npins source — +# kept independent from the main `nixpkgs` source (which the Haskell # build pins) so the Playwright pin can move on its own cadence. The # constraint: this rev's `playwright-driver` version must match the # `playwright` version in `tests/package.json`. Diverging the two # raises "browser revision X not found at " -# at launch — bump both together via `nix flake update nixpkgs-latest` +# at launch — bump both together via `npins update nixpkgs-latest` # plus a matching npm bump. let - lock = builtins.fromJSON (builtins.readFile ../flake.lock); - nixpkgsLocked = lock.nodes.nixpkgs-latest.locked; - pkgs = import - (builtins.fetchTree { - type = "github"; - owner = nixpkgsLocked.owner; - repo = nixpkgsLocked.repo; - rev = nixpkgsLocked.rev; - narHash = nixpkgsLocked.narHash; - }) - { }; + sources = import ../npins; + pkgs = import sources.nixpkgs-latest { }; in pkgs.mkShell { packages = [ pkgs.nodejs ];