diff --git a/.envrc b/.envrc index 197c0d5..3550a30 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1 @@ -#!/bin/bash - use flake diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1e28bdd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +# Reusable CI of nullkomma projects, called by their push.yml. +name: nullkomma CI +on: + workflow_call: + inputs: + visibility: + description: FlakeHub visibility (public, unlisted or private); empty skips publishing. + type: string + default: "" + deploy: + description: Run `nix run .#deploy` on pushes to the default branch. + type: boolean + default: false + secrets: + CLOUDFLARE_ACCOUNT_ID: + required: false + CLOUDFLARE_API_TOKEN: + required: false +jobs: + DeterminateCI: + uses: DeterminateSystems/ci/.github/workflows/workflow.yml@main + permissions: + contents: read + id-token: write + with: + visibility: ${{ inputs.visibility }} + fail-fast: false + runner-map: | + { + "aarch64-darwin": "macos-latest", + "aarch64-linux": "ubuntu-24.04-arm", + "x86_64-linux": "ubuntu-latest" + } + flake-checker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: DeterminateSystems/flake-checker-action@v14 + with: + fail-mode: true + deploy: + if: inputs.deploy && github.event_name == 'push' && github.ref_name == github.event.repository.default_branch + needs: DeterminateCI + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v7 + - uses: DeterminateSystems/determinate-nix-action@v3 + - uses: DeterminateSystems/flakehub-cache-action@v3 + - run: nix run .#deploy + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index 759ebf6..0407e22 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -1,24 +1,13 @@ name: Maintenance on: - workflow_dispatch: schedule: - - cron: "0 0 * * 0" # runs weekly on Sunday at 01:00 + - cron: "0 0 * * 0" # weekly, Sunday 00:00 UTC + workflow_dispatch: jobs: - nix-maintenance: - name: Nix Maintenance - runs-on: ubuntu-latest + nullkomma: + uses: ./.github/workflows/maintenance.yml permissions: - id-token: "write" - contents: "write" + contents: write + id-token: write issues: write pull-requests: write - steps: - - uses: actions/checkout@v6 - - uses: DeterminateSystems/determinate-nix-action@main - - uses: DeterminateSystems/flake-checker-action@main - - uses: DeterminateSystems/update-flake-lock@main - with: - pr-title: "Update flake.lock" - pr-labels: | - dependencies - automated diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml new file mode 100644 index 0000000..fa90210 --- /dev/null +++ b/.github/workflows/maintenance.yml @@ -0,0 +1,24 @@ +# Maintenance workflow called by nullkomma's own cron.yml. +name: nullkomma maintenance +on: + workflow_call: +jobs: + update-flake-lock: + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v7 + - uses: DeterminateSystems/determinate-nix-action@v3 + - uses: DeterminateSystems/flake-checker-action@v14 + - name: Append available R snapshots + run: python3 tools/update-r-snapshots.py + - uses: DeterminateSystems/update-flake-lock@v29 + with: + pr-title: Update flake.lock + pr-labels: | + dependencies + automated diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 184cdb3..298edfe 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -1,31 +1,16 @@ name: CI on: pull_request: - workflow_dispatch: push: + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: - DeterminateCI: - uses: DeterminateSystems/ci/.github/workflows/workflow.yml@main + nullkomma: + uses: ./.github/workflows/ci.yml permissions: - id-token: "write" - contents: "read" + contents: read + id-token: write with: - visibility: public - fail-fast: false - runner-map: | - { - "aarch64-darwin": "macos-latest", - "aarch64-linux": "ubuntu-24.04-arm", - "x86_64-linux": "ubuntu-latest" - } - flake-checker: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Check Nix flake Nixpkgs inputs - uses: DeterminateSystems/flake-checker-action@v12 - with: - fail-mode: true + visibility: "public" diff --git a/.gitignore b/.gitignore index b17e0aa..983575a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,14 @@ +**/*.quarto_ipynb +/.quarto/ +/_book/ +/_site/ .direnv/ result -/.quarto/ -**/*.quarto_ipynb -_site/ +result-* .claude/ +# Initialized files +!/.envrc +!/.github/workflows/push.yml +!/.gitignore +!/.vscode/extensions.json +!/.vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 7c6bf15..7ab7367 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,8 @@ { - "recommendations": ["arrterian.nix-env-selector", "jnoortheen.nix-ide"] + "recommendations": [ + "arrterian.nix-env-selector", + "ibecker.treefmt-vscode", + "jnoortheen.nix-ide", + "quarto.quarto" + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 3266dc8..8f1d7a0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,22 +1,30 @@ { - "nixEnvSelector.nixFile": "${workspaceFolder}/flake.nix", - "nixEnvSelector.useFlakes": true, - "nixEnvSelector.suggestion": false, + "editor.defaultFormatter": "ibecker.treefmt-vscode", "nix.enableLanguageServer": true, + "nix.formatterPath": [ + "treefmt", + "--stdin", + "{file}" + ], + "nix.hiddenLanguageServerErrors": [ + "textDocument/definition", + "textDocument/completion", + "textDocument/documentHighlight" + ], "nix.serverPath": "nixd", "nix.serverSettings": { "nixd": { "formatting": { - "command": ["treefmt", "--stdin", "{file}"] + "command": [ + "treefmt", + "--stdin", + "{file}" + ] } } }, - "nix.formatterPath": ["treefmt", "--stdin", "{file}"], - "nix.hiddenLanguageServerErrors": [ - "textDocument/definition", - "textDocument/completion", - "textDocument/documentHighlight" - ], - "editor.defaultFormatter": "ibecker.treefmt-vscode", + "nixEnvSelector.nixFile": "${workspaceFolder}/flake.nix", + "nixEnvSelector.suggestion": false, + "nixEnvSelector.useFlakes": true, "notebook.defaultFormatter": "ibecker.treefmt-vscode" } diff --git a/Makefile b/Makefile deleted file mode 100644 index e96aa59..0000000 --- a/Makefile +++ /dev/null @@ -1 +0,0 @@ -include make/*.mk diff --git a/README.md b/README.md index d14b055..0e28b1a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ extra-[DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 🤌 ## Scope -nullkomma is **per-project** Nix scaffolding: flakes, templates, devshells, formatters/checks, Makefile helpers, and the CI patterns those templates ship. +nullkomma is **per-project** Nix scaffolding: devshells, formatters, linters, checks, tasks, docs, deploys, CI and editor settings, packaged as composable [flake-parts](https://flake.parts) modules. It is **not**: @@ -32,10 +32,6 @@ When making changes, adhere to [aoshima's charter](https://github.com/dataheld/a ## Installing -> [!NOTE] -> This is not another flake or development framework. -> It is much less; just the set of composable nix flakes and modules that I use for my projects. - > [!TIP] > The steps below are **system / user** prerequisites. > Project-specific software is handled by the flake once you are inside the repo. @@ -43,15 +39,22 @@ When making changes, adhere to [aoshima's charter](https://github.com/dataheld/a 1. Install Nix (the package manager). The [Determinate Nix Installer](https://github.com/DeterminateSystems/nix-installer) is recommended. -For automatic flake-shell activation, provide [direnv](https://direnv.net) and [nix-direnv](https://github.com/nix-community/nix-direnv) in your user environment (managed via [lap](https://github.com/dataheld/lap) or installed manually). +1. For automatic flake-shell activation, provide [direnv](https://direnv.net) and [nix-direnv](https://github.com/nix-community/nix-direnv) in your user environment (managed via [lap](https://github.com/dataheld/lap) or installed manually). -1. Add the necessary boilerplate files. +1. In your project, add a `flake.nix` from one of the templates (`default`, `r` or `quarto`): ```sh nix flake init --template "https://flakehub.com/f/dataheld/nullkomma/0.1.*#default" ``` -1. (one-time only) Inside the repo, run `direnv allow` +1. Initialize missing files (`.gitignore`, `.envrc`, CI workflows, editor settings): + + ```sh + git add flake.nix + nix run .#write-files + ``` + +1. (one-time only) Inside the repo, run `direnv allow`. > [!TIP] > Windows is not supported by Nix, @@ -62,14 +65,143 @@ From now on, whenever you change into the directory of your project, all the necessary dependencies etc. will be ready. The first time you enter the directory, this might take some time. -## Building +## Using + +The default aspect also exports [flake schemas](https://github.com/DeterminateSystems/flake-schemas) +for standard outputs (`packages`, `checks`, `devShells`, `apps`, etc.) and module/library outputs. +Schema-aware Nix tooling can inventory and validate these outputs; ordinary Nix can still consume the flake. +The shared `flake-schemas` input adds one lightweight node to consumer locks, with no language toolchain dependencies. -All build-targets are self-documented: +A project's entire nullkomma footprint is its `flake.nix` (and `flake.lock`): + +```nix +{ + inputs.nullkomma.url = "https://flakehub.com/f/dataheld/nullkomma/0.1.*"; + + outputs = + inputs: + inputs.nullkomma.lib.mkFlake { inherit inputs; } { + imports = [ inputs.nullkomma.flakeModules.r ]; + }; +} +``` + +`nullkomma.lib.mkFlake` is [`flake-parts.lib.mkFlake`](https://flake.parts/getting-started) with `flakeModules.default` already imported, +so everything flake-parts can do, a nullkomma project can do, too. +The project needs no other inputs: Nixpkgs, flake-parts and treefmt-nix come pinned with nullkomma. + +All tasks are self-documented: ```sh -make help +nix run ``` +Then, as usual: `nix develop`, `nix fmt`, `nix flake check`, `nix flake show`. + +### Aspects + +Each aspect is a flake-parts module in `flakeModules`. +Import only the ones a project needs; an aspect never adds anything to a project that does not import it. + +| Aspect | Adds | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `default` | devshell, [treefmt](https://github.com/numtide/treefmt-nix) (Nix, Markdown, YAML, JSON, TOML, shell, GitHub Actions), tasks (`check`, `update`, `flake-checker`, `write-files`), initial `.gitignore`, `.envrc`, CI workflow and VS Code settings | +| `r` | R with all packages from `DESCRIPTION`, [air](https://posit-dev.github.io/air/) (and optionally [jarl](https://jarl.etiennebacher.com)), `checks.r-cmd-check`, VS Code extensions | +| `quarto` | Quarto, `packages.site` (if there is a `_quarto.yml`), tasks `render` and `preview`; with `r`, R chunks run with the project's R environment | +| `cloudflare-pages` | task `deploy` of `packages.site` to [Cloudflare Pages](https://pages.cloudflare.com), run by CI on pushes to the default branch | + +### Knobs + +Options live under `nullkomma.*`, for example (the generated [option reference](reference.qmd) lists all of them, also under `perSystem`; build it with `nix build .#docs-reference`): + +```nix +inputs.nullkomma.lib.mkFlake { inherit inputs; } { + imports = with inputs.nullkomma.flakeModules; [ r quarto cloudflare-pages ]; + nullkomma = { + r.extraPackages = [ "devtools" ]; + cloudflare-pages.project = "my-site"; + github.ci.inputs.visibility = "public"; # publish to FlakeHub + }; + perSystem = + { pkgs, ... }: + { + nullkomma.devshell.packages = [ pkgs.hello ]; + treefmt.programs.typos.enable = true; + }; +} +``` + +### R dependencies and snapshots + +Keep dependencies in `DESCRIPTION`, including version constraints. Select the CRAN date +in the project's ordinary `.Rprofile`, so non-Nix users can use the same setting: + +```r +options(repos = c(CRAN = "https://packagemanager.posit.co/cran/2026-01-05")) +``` + +nullkomma reads that setting with R and selects a pinned +[rstats-on-nix/nixpkgs](https://github.com/rstats-on-nix/nixpkgs) revision from +[`lib/r-snapshots.json`](lib/r-snapshots.json). Only listed dates are supported. +This reuses nixpkgs' source hashes, dependency graph, R build recipes and curated +system libraries; nullkomma does **not** fetch and resolve arbitrary PPM packages. +The same calendar date is not a guarantee of identical PPM and nixpkgs package sets, +nor of consistency after overrides. R checks the project's version constraints +against selected versions, failing rather than silently ignoring mismatches. + +The locked nullkomma revision freezes the date-to-commit table; the snapshot is +**not an additional `flake.lock` node**. Updating nullkomma adds available dates, +but does not advance your `.Rprofile` date. Maintenance appends new dates without +changing existing pins (`python3 tools/update-r-snapshots.py`). Without a dated +profile, R follows the locked `nixpkgs-r` input (by default the main nixpkgs input). +An explicit `inputs.nullkomma.inputs.nixpkgs-r.url` remains an escape hatch when +there is no profile date. + +Pinned GitHub overrides are supported in `DESCRIPTION`, for example: + +```dcf +Imports: withr (>= 3.0.3.9000) +Remotes: r-lib/withr@d82e4bc2d69a34f044ad205210e26207bfb8f3e0 +``` + +`github::owner/repo@` and `package=owner/repo@` also work. +Existing CRAN recipes retain their system-library customizations; new packages +use `rPackages.buildRPackage`. Extra system libraries can be supplied with +`perSystem = { pkgs, ... }: { nullkomma.r.buildInputs.myPackage = [ pkgs.gdal ]; };`. +Remote runtime/build dependencies and their version constraints are read from +that source's DESCRIPTION. Native build hooks may still require a custom recipe. + +**Initial limits:** mutable refs are rejected, not automatically locked. Other +pak references (GitLab, URLs, subdirectories, local packages, `Config/Needs`), +transitive `Remotes`, dependency cycles and arbitrary constraint solving are not +supported. Full pak-compatible resolution and locking mutable refs remain open +work; this is not a universal DESCRIPTION resolver. GitHub source availability +is not guaranteed forever. Pure GitHub fetching was tested with Determinate Nix +3.21.9; older Nix versions may require additional source hashes. + +R's `read.dcf`, `tools::package_dependencies` and version parser run inside small +derivations (import from derivation). Evaluation needs a build-capable machine +for the target system and IFD enabled, and may build both bootstrap and snapshot R. +The profile executes in an isolated build directory, not the project directory: +keep its repository setting self-contained and do not depend on other files, +network access or installed packages. Ordinary R startup still loads it normally. +`nix develop` creates a writable `R_LIBS_USER` outside the store for interactive +`install.packages()`; these installations are **not Nix-reproducible** and may +shadow pinned packages. Nix checks do not use that user library. Non-Nix users +can use ordinary R/pak tooling without adopting nullkomma. + +### Initialized files + +Some files must live in the repo, so `nix run .#write-files` initializes missing +`.gitignore`, `.envrc`, `.github/workflows/push.yml`, `.vscode/*.json`, and (for R) `.Rprofile` files. +The initial R profile selects the latest shipped snapshot date; existing profiles are preserved. +Configure their initial contents in `flake.nix` (e.g. `nullkomma.gitignore`, `nullkomma.editor.vscode`). +After initialization they belong to you: edit them directly. Re-running `write-files` +preserves existing files, and there is no drift check or automatic overwrite on upgrades. +Commit them; the initial `.gitignore` re-includes these paths to override a global `core.excludesFile`. +The CI stub calls nullkomma's reusable [`ci.yml`](.github/workflows/ci.yml). +The scheduled `cron.yml` maintenance workflow exists only in nullkomma itself, not in consumers. + ## Updating ### Nix @@ -77,21 +209,20 @@ make help There are two separate aspects to updating the nix dependencies. 1. There may be newer versions available _given_ the pinning in `flake.nix`. - This can be accomplished by running `make update-flake` locally and may change the `flake.lock`. + This can be accomplished by running `nix run .#update` locally and may change the `flake.lock`. However such updates may break a project. - It is therefore recommended **to only run this in CI**, - using the periodically scheduled `nix_maintenance.yml` job. - It will automatically open pull requests if there are updates available. - Users can then inspect whether the updated project still passes all tests. + Review updates and run checks before merging. Nullkomma itself uses a scheduled + `cron.yml` workflow to open update pull requests; downstream repos do not receive this workflow. + Updates never overwrite your initialized repository files. 1. The versions pinned in `flake.nix` (and the resulting `flake.lock`) itself may be out of date. The [DeterminateSystems/flake-checker](https://github.com/DeterminateSystems/flake-checker) will fail if this is the case. It runs on every push as well as periodically. - You can also run this locally using `make check-flake-checker`. + You can also run this locally using `nix run .#flake-checker`. ### Development Shell To bring the shell you are working in up to date with the _source_ -(`nix.flake`, etc.) +(`flake.nix`, etc.) of your repository: ```sh @@ -104,6 +235,19 @@ Or if you have `nix-direnv` installed,: nix-direnv-reload ``` +## Developing + +nullkomma follows the [dendritic pattern](https://github.com/mightyiam/dendritic): +every `.nix` file under [`modules/`](modules) is a flake-parts module, imported automatically +(files and directories starting with `_` are skipped). +Files add to the exported aspects by defining `flake.flakeModules.`, so one aspect can span many files +and one file can extend several aspects; [`modules/lang/quarto/r.nix`](modules/lang/quarto/r.nix), for example, wires R into Quarto only if both are imported. + +nullkomma uses itself: +its devshell, formatter, checks and this website come from the `dev` [partition](https://flake.parts/options/flake-parts-partitions.html), +so development-only configuration is not evaluated by consumers. +The fixtures in [`tests/fixtures`](tests/fixtures) are evaluated like projects using nullkomma, and their checks, devshells and sites are part of `nix flake check`. + ## Issues `nix-`/`direnv` can be a bit chatty on launch. diff --git a/_quarto.yml b/_quarto.yml index 1206970..6ffb03d 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -1,5 +1,9 @@ project: type: website + # top-level pages only; tests/fixtures holds other Quarto projects + render: + - "*.qmd" + - "!tests/" website: title: "nullkomma" description: "Opinionated 🤓, batteries-included 🔋, extra-DRY 🤌 Nix boilerplate." @@ -7,6 +11,8 @@ website: left: - href: index.qmd text: Home + - href: reference.qmd + text: Reference format: html: theme: pulse diff --git a/flake.lock b/flake.lock index 1c315c0..82aced4 100644 --- a/flake.lock +++ b/flake.lock @@ -1,227 +1,5 @@ { "nodes": { - "crane": { - "locked": { - "lastModified": 1758758545, - "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", - "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", - "revCount": 764, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/ipetkov/crane/0.21.1/01997e40-19a9-7bc6-9dba-0585d6ed9a98/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/ipetkov/crane/0" - } - }, - "crane_2": { - "locked": { - "lastModified": 1779041105, - "narHash": "sha256-nnGD2f8OlAZT2i5OfwikJsw+ifWfiA4d6A8BWlgOXV0=", - "rev": "10e6e3cb966f7cfcc789fe5eee7a85f3188ce08b", - "revCount": 858, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/ipetkov/crane/0.23.4/019e3726-d9ea-7820-aece-59009301cab1/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/ipetkov/crane/0" - } - }, - "crane_3": { - "locked": { - "lastModified": 1745454774, - "narHash": "sha256-oLvmxOnsEKGtwczxp/CwhrfmQUG2ym24OMWowcoRhH8=", - "rev": "efd36682371678e2b6da3f108fdb5c613b3ec598", - "revCount": 729, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/ipetkov/crane/0.20.3/01966538-0f80-7363-a573-2ba9fd154399/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/ipetkov/crane/0" - } - }, - "crane_4": { - "locked": { - "lastModified": 1745454774, - "narHash": "sha256-oLvmxOnsEKGtwczxp/CwhrfmQUG2ym24OMWowcoRhH8=", - "rev": "efd36682371678e2b6da3f108fdb5c613b3ec598", - "revCount": 729, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/ipetkov/crane/0.20.3/01966538-0f80-7363-a573-2ba9fd154399/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/ipetkov/crane/0.20" - } - }, - "easy-template": { - "inputs": { - "crane": "crane_3", - "fenix": "fenix_2", - "nixpkgs": [ - "flake-checker", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1750267549, - "narHash": "sha256-febr39vSeQ1H54mIhyIB+c2p/2xdntabf4grEQIj60M=", - "rev": "6fb441f657e98f31024f19ca68b691c5b0c3fdd6", - "revCount": 5, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/easy-template/0.1.5%2Brev-6fb441f657e98f31024f19ca68b691c5b0c3fdd6/01978415-c996-7747-a25c-77ca3a4c5ed9/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/DeterminateSystems/easy-template/0" - } - }, - "fenix": { - "inputs": { - "nixpkgs": [ - "fh", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src" - }, - "locked": { - "lastModified": 1756709111, - "narHash": "sha256-xv2u5dnQpdWkrIy5TBSomr055odWtRSoECSGBzNpp3w=", - "rev": "113eba389317407992ea219d5aaced44bf6407f9", - "revCount": 2375, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/nix-community/fenix/0.1.2375%2Brev-113eba389317407992ea219d5aaced44bf6407f9/0199045f-8452-723d-b201-deb22d41c75e/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/nix-community/fenix/%3D0.1.2375" - } - }, - "fenix_2": { - "inputs": { - "nixpkgs": [ - "flake-checker", - "easy-template", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_2" - }, - "locked": { - "lastModified": 1740810935, - "narHash": "sha256-6RzWfxENGlO73jQb3uQNgOvubUFwvveeIg+PZxhAu6s=", - "rev": "f44d7c3596ff028ad9f7fcc31d1941ed585f11b3", - "revCount": 2184, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/nix-community/fenix/0.1.2184%2Brev-f44d7c3596ff028ad9f7fcc31d1941ed585f11b3/019550c8-7792-7766-8dd2-80fad5595f70/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/nix-community/fenix/0.1" - } - }, - "fenix_3": { - "inputs": { - "nixpkgs": [ - "flake-checker", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_3" - }, - "locked": { - "lastModified": 1782900513, - "narHash": "sha256-GHrsl1+ysDFgmDQtQSqWS7TiYD2Q58VlwLvnf1+crJM=", - "rev": "16810aa8f4ad89ca480b1513774e8b6f485fe368", - "revCount": 2708, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/nix-community/fenix/0.1.2708%2Brev-16810aa8f4ad89ca480b1513774e8b6f485fe368/019f1d68-8b55-7783-bed1-d5926f21ae52/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/nix-community/fenix/0.1" - } - }, - "fenix_4": { - "inputs": { - "nixpkgs": [ - "flake-iter", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_4" - }, - "locked": { - "lastModified": 1761979010, - "narHash": "sha256-isqMvjTk3jdTHN6KA/BWQvOSVe7O35OQKAZNtLK76OY=", - "rev": "3107255abfe4f2d1c3eee7a3e2f5a5eb6f2200fe", - "revCount": 2439, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/nix-community/fenix/0.1.2439%2Brev-3107255abfe4f2d1c3eee7a3e2f5a5eb6f2200fe/019a3e7f-523d-7e82-b791-0e92cf55a0a9/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/nix-community/fenix/0.1.1885" - } - }, - "fh": { - "inputs": { - "crane": "crane", - "fenix": "fenix", - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1761780713, - "narHash": "sha256-EUCV7/J9wJRroCGW5JqonFJIqcvJEBAwB7l3eWYxiSk=", - "rev": "4f001f2e1de4776f01cf22d1de815f1016a4c4c9", - "revCount": 786, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/fh/0.1.27/019a355b-4c74-769d-a9ad-232df447089d/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/DeterminateSystems/fh/0.1.%2A" - } - }, - "flake-checker": { - "inputs": { - "crane": "crane_2", - "easy-template": "easy-template", - "fenix": "fenix_3", - "nixpkgs": "nixpkgs_2" - }, - "locked": { - "lastModified": 1790188256, - "narHash": "sha256-J0RAJJdpKYgMeV8+aojCRKVkXWa4PzdaAJHLjZInB4E=", - "rev": "cddc8afc9733e3ff695df50f2be3d8d85345c9eb", - "revCount": 740, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-checker/0.2.15/01a0cf8b-8383-7b53-a0bf-5aa9bee1994a/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/DeterminateSystems/flake-checker/0.2.%2A" - } - }, - "flake-iter": { - "inputs": { - "crane": "crane_4", - "fenix": "fenix_4", - "flake-schemas": "flake-schemas", - "nixpkgs": "nixpkgs_3" - }, - "locked": { - "lastModified": 1764128805, - "narHash": "sha256-Ufi2j3Pd87IG8Agd/B4fy0MfYVZPqGa1c82id85gajE=", - "rev": "e4188aaef2c61df96c5fa2d88c764adecf69e700", - "revCount": 134, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-iter/0.1.134%2Brev-e4188aaef2c61df96c5fa2d88c764adecf69e700/019abe49-7872-7f0c-9805-2d163605f92b/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/DeterminateSystems/flake-iter/0.1.%2A" - } - }, "flake-parts": { "inputs": { "nixpkgs-lib": [ @@ -241,266 +19,62 @@ "url": "https://flakehub.com/f/hercules-ci/flake-parts/0.1.%2A" } }, - "flake-schemas": { - "locked": { - "lastModified": 1761577921, - "narHash": "sha256-eK3/xbUOrxp9fFlei09XNjqcdiHXxndzrTXp7jFpOk8=", - "rev": "47849c7625e223d36766968cc6dc23ba0e135922", - "revCount": 107, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.2.0/019a4a84-544d-7c59-b26d-e334e320c932/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/%2A" - } - }, - "flake-schemas_2": { - "locked": { - "lastModified": 1780327564, - "narHash": "sha256-HiRPtA0spK+Dkgbhz/1zW9glXxNVB+L4Rj2VYmdawb8=", - "rev": "6cc9bd98891b1fc6bb2b8cb3277df8bc72799ca6", - "revCount": 149, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.5.0/019e83cf-9af3-78b1-ac5b-70e68ad1efe1/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/0.5.%2A" - } - }, - "nix-github-actions": { - "inputs": { - "nixpkgs": [ - "nix-unit", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1737420293, - "narHash": "sha256-F1G5ifvqTpJq7fdkT34e/Jy9VCyzd5XfJ9TO8fHhJWE=", - "owner": "nix-community", - "repo": "nix-github-actions", - "rev": "f4158fa080ef4503c8f4c820967d946c2af31ec9", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nix-github-actions", - "type": "github" - } - }, - "nix-unit": { - "inputs": { - "nix-github-actions": "nix-github-actions", - "nixpkgs": [ - "nixpkgs" - ], - "treefmt-nix": "treefmt-nix" - }, - "locked": { - "lastModified": 1784020394, - "narHash": "sha256-eG+ETC+lvHDLhlKgQB6/lGfpqr56Lt3+j1pHOeb4tK4=", - "owner": "nix-community", - "repo": "nix-unit", - "rev": "3a484e17e4911ed97d56e37506f22e294c7c0ccd", - "type": "github" - }, - "original": { - "owner": "nix-community", - "ref": "v2.34.2", - "repo": "nix-unit", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1761468971, - "narHash": "sha256-vY2OLVg5ZTobdroQKQQSipSIkHlxOTrIF1fsMzPh8w8=", - "rev": "78e34d1667d32d8a0ffc3eba4591ff256e80576e", - "revCount": 811770, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2505.811770%2Brev-78e34d1667d32d8a0ffc3eba4591ff256e80576e/019a23fb-89f0-7302-a573-f6bf7dde9cf5/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/NixOS/nixpkgs/0" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1789286504, - "narHash": "sha256-eiEK7cKZORNEvX0GeF3RtNEF/JXhgf2RqSp3230q13E=", - "rev": "ef34387ddd751e1ab8857adf4676492d32eb24ec", - "revCount": 1073009, + "lastModified": 1791353474, + "narHash": "sha256-v72qr4LsSz61tQki/41nHKZKPY6NSeZavtGlAInOCng=", + "rev": "2efa67fd26b6df417c33e4603185c701f260dd83", + "revCount": 1015818, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.1073009%2Brev-ef34387ddd751e1ab8857adf4676492d32eb24ec/01a09f6e-d65c-7c23-9b52-4dcddb9c94b7/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1" - } - }, - "nixpkgs_3": { - "locked": { - "lastModified": 1763948260, - "narHash": "sha256-dY9qLD0H0zOUgU3vWacPY6Qc421BeQAfm8kBuBtPVE0=", - "rev": "1c8ba8d3f7634acac4a2094eef7c32ad9106532c", - "revCount": 813095, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2505.813095%2Brev-1c8ba8d3f7634acac4a2094eef7c32ad9106532c/019ab6d8-0005-7317-844d-5d868444249f/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/NixOS/nixpkgs/0" - } - }, - "nixpkgs_4": { - "locked": { - "lastModified": 1790920529, - "narHash": "sha256-nQyFkMR78WP6PiXKkDW2SjqzLf/spQqjRuxRGSbcV8k=", - "rev": "774debe7a0d1b496e35677ad955a1011c6ff74f3", - "revCount": 1015336, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2605.1015336%2Brev-774debe7a0d1b496e35677ad955a1011c6ff74f3/01a0fdb4-e493-7812-bab4-ef8e5029af88/source.tar.gz" + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2605.1015818%2Brev-2efa67fd26b6df417c33e4603185c701f260dd83/01a11777-646c-7ac3-9a38-941658c52e27/source.tar.gz" }, "original": { "type": "tarball", "url": "https://flakehub.com/f/NixOS/nixpkgs/0.2605.%2A" } }, - "nixpkgs_5": { - "locked": { - "lastModified": 1770107345, - "narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "4533d9293756b63904b7238acb84ac8fe4c8c2c4", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { - "fh": "fh", - "flake-checker": "flake-checker", - "flake-iter": "flake-iter", "flake-parts": "flake-parts", - "flake-schemas": "flake-schemas_2", - "nix-unit": "nix-unit", - "nixpkgs": "nixpkgs_4", - "treefmt-nix": "treefmt-nix_2" - } - }, - "rust-analyzer-src": { - "flake": false, - "locked": { - "lastModified": 1756597274, - "narHash": "sha256-wfaKRKsEVQDB7pQtAt04vRgFphkVscGRpSx3wG1l50E=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "21614ed2d3279a9aa1f15c88d293e65a98991b30", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_2": { - "flake": false, - "locked": { - "lastModified": 1740737930, - "narHash": "sha256-2AW/FJQI/i6bbRB/8HR9l9SjxjuiukJpHdMPgwApPKA=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "fe8444616679f8e50ff9696f4750df1f10e7433d", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_3": { - "flake": false, - "locked": { - "lastModified": 1782864348, - "narHash": "sha256-NVYhLbaefeIUftPlo3kS6qr0xd8eFJRodEiaHrvFKR4=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "0d381ca097a8e0375a19387874d952c0a230ac4f", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_4": { - "flake": false, - "locked": { - "lastModified": 1761894503, - "narHash": "sha256-SreGV62DEv7kLJEcOBrw2V6Kup0siT4wS3Ye8PlFTdE=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "2e2e3ebec91215078de9b754363fc9a7b0fdef13", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" + "nixpkgs": "nixpkgs", + "nixpkgs-r": [ + "nixpkgs" + ], + "treefmt-nix": "treefmt-nix", + "flake-schemas": "flake-schemas" } }, "treefmt-nix": { "inputs": { "nixpkgs": [ - "nix-unit", "nixpkgs" ] }, "locked": { - "lastModified": 1780220602, - "narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=", - "owner": "numtide", - "repo": "treefmt-nix", - "rev": "db947814a175b7ca6ded66e21383d938df01c227", - "type": "github" + "lastModified": 1791203292, + "narHash": "sha256-vuzVwvjeucSnwKwMBpyP8r5IzdrMKCHEdwTrRFwUN7I=", + "rev": "03d8ee1bcbc8f9638907bb79edb9673151d0e22b", + "revCount": 593, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/numtide/treefmt-nix/0.1.593%2Brev-03d8ee1bcbc8f9638907bb79edb9673151d0e22b/01a10d28-fe85-7ba0-9d91-820ac13e70a3/source.tar.gz" }, "original": { - "owner": "numtide", - "repo": "treefmt-nix", - "type": "github" + "type": "tarball", + "url": "https://flakehub.com/f/numtide/treefmt-nix/0.1.%2A" } }, - "treefmt-nix_2": { - "inputs": { - "nixpkgs": "nixpkgs_5" - }, + "flake-schemas": { "locked": { - "lastModified": 1786901030, - "narHash": "sha256-WSFCsDSE5ffgD2MqzkM2CYjeFiKhRF/dJUN8uedb6YE=", - "rev": "27b3b12a8e6375f28ebe122f07d230ca5459bbfa", - "revCount": 557, + "lastModified": 1780327564, + "narHash": "sha256-HiRPtA0spK+Dkgbhz/1zW9glXxNVB+L4Rj2VYmdawb8=", + "rev": "6cc9bd98891b1fc6bb2b8cb3277df8bc72799ca6", + "revCount": 149, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/numtide/treefmt-nix/0.1.557%2Brev-27b3b12a8e6375f28ebe122f07d230ca5459bbfa/01a00ba7-ea6f-7822-ab05-cbad971fc7ca/source.tar.gz" + "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.5.0/019e83cf-9af3-78b1-ac5b-70e68ad1efe1/source.tar.gz" }, "original": { "type": "tarball", - "url": "https://flakehub.com/f/numtide/treefmt-nix/0.1.%2A" + "url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/0.5.%2A" } } }, diff --git a/flake.nix b/flake.nix index 31f2a27..3057a85 100644 --- a/flake.nix +++ b/flake.nix @@ -1,158 +1,28 @@ { - description = "nullkomma"; + description = "Opinionated, batteries-included, extra-DRY Nix boilerplate"; + # Every input lands in every consumer's flake.lock, so keep this list short. + # Tools come from nixpkgs; schemas describe both our and consumers' outputs. inputs = { nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.2605.*"; - # keep-sorted start - fh.url = "https://flakehub.com/f/DeterminateSystems/fh/0.1.*"; - flake-checker.url = "https://flakehub.com/f/DeterminateSystems/flake-checker/0.2.*"; - flake-iter.url = "https://flakehub.com/f/DeterminateSystems/flake-iter/0.1.*"; - # keep-sorted end + # Knob: R version and CRAN snapshot. Free unless a consumer overrides it, e.g. + # inputs.nullkomma.inputs.nixpkgs-r.url = "github:rstats-on-nix/nixpkgs/2026-01-05"; + nixpkgs-r.follows = "nixpkgs"; flake-parts = { - inputs.nixpkgs-lib.follows = "nixpkgs"; url = "https://flakehub.com/f/hercules-ci/flake-parts/0.1.*"; + inputs.nixpkgs-lib.follows = "nixpkgs"; }; flake-schemas.url = "https://flakehub.com/f/DeterminateSystems/flake-schemas/0.5.*"; - nix-unit = { - # Pin to 2.34.x: nixpkgs 0.2605 has nixComponents_2_34, not _2_35 - url = "github:nix-community/nix-unit/v2.34.2"; - inputs = { - nixpkgs.follows = "nixpkgs"; - }; + treefmt-nix = { + url = "https://flakehub.com/f/numtide/treefmt-nix/0.1.*"; + inputs.nixpkgs.follows = "nixpkgs"; }; - treefmt-nix.url = "https://flakehub.com/f/numtide/treefmt-nix/0.1.*"; }; + # Dendritic: every file under ./modules is a flake-parts module. outputs = - { flake-parts, self, ... }@inputs: - flake-parts.lib.mkFlake { inherit inputs; } { - imports = [ - # keep-sorted start - inputs.nix-unit.modules.flake.default - inputs.treefmt-nix.flakeModule - # keep-sorted end - ]; - systems = [ - # keep-sorted start - "aarch64-darwin" - "aarch64-linux" - "x86_64-linux" - # keep-sorted end - ]; - perSystem = - { - inputs', - pkgs, - self', - ... - }: - let - # workaround for https://github.com/NixOS/nixpkgs/issues/519484 - # quarto 1.9.37 emits "syntax-highlighting" but pandoc 3.7+ expects "highlight-style" - quartoMinimal = pkgs.quartoMinimal.overrideAttrs (oldAttrs: { - postPatch = (oldAttrs.postPatch or "") + '' - substituteInPlace bin/quarto.js \ - --replace-fail "syntax-highlighting" "highlight-style" - ''; - }); - in - { - devShells.default = pkgs.mkShell { - packages = [ - # keep-sorted start - inputs'.fh.packages.default - inputs'.flake-checker.packages.default - inputs'.flake-iter.packages.default - pkgs.git - pkgs.gnumake - pkgs.nixd - quartoMinimal - self'.formatter - # keep-sorted end - ]; - }; - packages = { - docs = pkgs.stdenv.mkDerivation { - name = "nullkomma-docs"; - src = ./.; - nativeBuildInputs = [ - pkgs.writableTmpDirAsHomeHook - ]; - buildInputs = [ - quartoMinimal - pkgs.which - ]; - buildPhase = '' - make render - ''; - installPhase = '' - mkdir $out - mv _site $out - ''; - }; - }; - treefmt = { - programs = { - # keep-sorted start - actionlint.enable = true; - beautysh.enable = true; - black.enable = true; - cmake-format.enable = true; - deadnix.enable = true; - hclfmt.enable = true; - isort.enable = true; - jsonfmt.enable = true; - keep-sorted.enable = true; - mdformat.enable = true; - nixfmt.enable = true; - prettier.enable = true; - shellcheck.enable = true; - shfmt.enable = true; - sqlfluff.enable = true; - toml-sort.enable = true; - yamlfmt.enable = true; - # keep-sorted end - }; - }; - nix-unit = { - inputs = { - inherit (inputs) - nixpkgs - flake-parts - nix-unit - treefmt-nix - ; - }; - tests = { - "test example system-specific" = { - expr = "1"; - expected = "1"; - }; - }; - }; - }; - flake = { - schemas = inputs.flake-schemas.schemas; - templates = rec { - base = { - description = "nullkomma template"; - path = ./templates/base; - welcomeText = '' - Welcome to the nullkomma template - ''; - }; - default = base; - }; - tests = { - "test example system-agnostic" = { - expr = "2"; - expected = "2"; - }; - "test template welcome text" = { - expr = self.templates.default.description; - expected = "nullkomma template"; - }; - }; - }; + inputs: + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + imports = import ./lib/import-tree.nix inputs.nixpkgs.lib ./modules; }; } diff --git a/lib/description-constraints.R b/lib/description-constraints.R new file mode 100644 index 0000000..715ddcf --- /dev/null +++ b/lib/description-constraints.R @@ -0,0 +1,22 @@ +# Validate DESCRIPTION constraints against the selected package set, using R's parser. +# Usage: Rscript --vanilla description-constraints.R DESCRIPTION VERSIONS FIELD... +args <- commandArgs(trailingOnly = TRUE) +db <- read.dcf(args[[1]], fields = args[-c(1, 2)]) +versions <- read.delim(args[[2]], header = FALSE, col.names = c("name", "version"), colClasses = "character") +selected <- setNames(versions$version, versions$name) +base <- installed.packages(lib.loc = .Library, priority = "base") +selected <- c(selected, setNames(base[, "Version"], base[, "Package"]), R = as.character(getRversion())) +for (field in colnames(db)) { + if (is.na(db[1, field])) next + for (dep in tools:::.split_dependencies(db[1, field])) { + if (is.null(dep$version)) next + actual <- selected[[dep$name]] + if (is.null(actual)) stop("nullkomma.r: no selected version for ", dep$name) + matches <- do.call(dep$op, list(package_version(actual), dep$version)) + if (!isTRUE(matches)) { + stop("nullkomma.r: DESCRIPTION requires ", dep$name, " (", dep$op, " ", + dep$version, "); selected version is ", actual, ". Choose another snapshot or pinned remote.") + } + } +} +cat("ok\n") diff --git a/lib/description-deps.R b/lib/description-deps.R new file mode 100644 index 0000000..d6971c3 --- /dev/null +++ b/lib/description-deps.R @@ -0,0 +1,10 @@ +# Package names a DESCRIPTION depends on, one per line, parsed by R itself. +# Usage: Rscript description-deps.R DESCRIPTION FIELD... +args <- commandArgs(trailingOnly = TRUE) +fields <- args[-1] +db <- read.dcf(args[[1]], fields = c("Package", fields)) +db[, "Package"] <- "description" +deps <- tools::package_dependencies("description", db = db, which = fields)[["description"]] +# base packages ship with R itself +base <- rownames(installed.packages(lib.loc = .Library, priority = "base")) +writeLines(sort(setdiff(deps, c("R", base)))) diff --git a/lib/description-meta.R b/lib/description-meta.R new file mode 100644 index 0000000..3438924 --- /dev/null +++ b/lib/description-meta.R @@ -0,0 +1,4 @@ +# Package name and version of a DESCRIPTION, one per line. +# Usage: Rscript description-meta.R DESCRIPTION +db <- read.dcf(commandArgs(trailingOnly = TRUE)[[1]], fields = c("Package", "Version")) +writeLines(db[1, c("Package", "Version")]) diff --git a/lib/description-remotes.R b/lib/description-remotes.R new file mode 100644 index 0000000..d818cf6 --- /dev/null +++ b/lib/description-remotes.R @@ -0,0 +1,5 @@ +# `Remotes:` entries of a DESCRIPTION (pak/remotes syntax), one per line. +# Usage: Rscript description-remotes.R DESCRIPTION +db <- read.dcf(commandArgs(trailingOnly = TRUE)[[1]], fields = "Remotes") +remotes <- db[1, "Remotes"] +if (!is.na(remotes)) writeLines(trimws(strsplit(remotes, ",")[[1]])) diff --git a/lib/import-tree.nix b/lib/import-tree.nix new file mode 100644 index 0000000..c4ec407 --- /dev/null +++ b/lib/import-tree.nix @@ -0,0 +1,9 @@ +# Dependency-free take on vic/import-tree: list every `.nix` file below `root`, +# skipping anything whose path (relative to `root`) contains a `/_` segment. +lib: root: +let + relative = path: lib.removePrefix (toString root) (toString path); +in +builtins.filter (path: lib.hasSuffix ".nix" (toString path) && !lib.hasInfix "/_" (relative path)) ( + lib.filesystem.listFilesRecursive root +) diff --git a/lib/r-snapshots.json b/lib/r-snapshots.json new file mode 100644 index 0000000..9ae2d3d --- /dev/null +++ b/lib/r-snapshots.json @@ -0,0 +1,681 @@ +{ + "2019-03-14": "b082d27904cdff3e9d7027e6cc08f94b549ee15f", + "2019-05-05": "0cc5233098313ae1474b11d1d54e27097786a989", + "2019-05-06": "85c4d3f6c624abbef3d2f2ab126bda850467532e", + "2019-05-07": "7710cf3541174beac64c3a2c6c47984ded0bdd57", + "2019-07-22": "bcfa3c58624f02960d48181b2e5dd5bf5bc0b5f1", + "2019-12-19": "38f751a46df27f19cba826e3fc8c75edeadff2f0", + "2020-03-12": "75d4f52711abdd73ffb51722cd925c8f56952dc8", + "2020-04-27": "4c792b3812cf513f508db2f025eaf00fb4139e80", + "2020-06-07": "e6c43ddc719e448204bdf0402e9aebd74ebf3b49", + "2020-08-20": "758ac22327850ac17d24c34e044200422d428c0e", + "2020-10-30": "09637b49ec41a7e4e77b9207af1456b07e0ec9c8", + "2021-01-01": "1c9dc384f385e0e1f85aad777db857e56093a329", + "2021-02-26": "7c47254f9f10b20b18ecd516315f11cca3cfbe68", + "2021-04-01": "956f5c957922428a91c1152fe517abb4791ac28a", + "2021-05-29": "fc15ea73bceee6d3521c7efb4447321de8d688d3", + "2021-08-03": "62a4004611efed250894589ad20386a557a19f98", + "2021-10-28": "0327623aa2e237a511a7939f9ebb5e7833bb698c", + "2022-01-01": "8ba515641075bb8c9c1b8314d7c554f4c437493e", + "2022-01-16": "1210919efe86791fa4d03df9035e58e4e5dfeda2", + "2022-03-01": "1cd08a64b9016a9e117246767e7b5977f13fdb80", + "2022-04-19": "f6a6c9b078ad9e785cd16488b309a85cc6839dd5", + "2022-06-22": "064833c8f5c4a4b496e41ef5d4e8ea379a235b2f", + "2022-08-22": "fc7a39af123d87c0a69e36057f179690ce091c09", + "2022-10-20": "6df55dfda5a58c1a64170a2877f3d6b3d9436bf1", + "2022-12-20": "657db1a90f0e1ca416e0d44f70ac78d222fcc98e", + "2023-02-13": "59d5623e98797e3161c55b0bd2c1b3825bf92b70", + "2023-04-01": "a0a7258406c80099e4d8a962b95a189a379405c3", + "2023-06-15": "85a25be3fe1001ba16421eba7902aaa80090f4e6", + "2023-08-15": "20e7c4f5cecd85aace5cf1140124e769041b567d", + "2023-10-30": "794758a84ce37ce87c8679af0a871724aae5e6cf", + "2023-12-30": "512857768c81297d119c60003a7c8ee8c1ab3133", + "2024-02-29": "be5ffd8f5d8c146e150b6f8e204a75b088986b18", + "2024-04-29": "6f0e43799c20e56909cb07dcd0480a51269f13c2", + "2024-06-14": "2278316997755b298051fc245fde40f13caab9ad", + "2024-08-19": "c42455edc5cba8d30e36912ba7a20ff4b53bfe58", + "2024-10-01": "24bf799c8a845bcd82ec8179a8f2d744e63831bc", + "2024-12-14": "00294e15c7d8fe98c7b2183adccce34f969673fd", + "2024-12-15": "2b620f87f01e88abe532959654aaa8fcc5c2dac5", + "2024-12-16": "f99b21acdce727bb2dc6fa5e5ddbffac39b01c31", + "2024-12-17": "8af47b5017bdb1db6f36cc4bf2615ea8b918e50a", + "2024-12-18": "80bcd0554d2f0a69bb717df71f6c75194be03409", + "2024-12-19": "f0d39388f44fb18951dec1e18968dfd8834f17ce", + "2024-12-20": "3807d7468a171e74673229e5c09499d0f01bcbb0", + "2024-12-21": "3b82faf99f7ff61f638282ab8ce92c7ddc762d24", + "2024-12-22": "dee80e746c0ab193e31db53371c5de8d633cdc7b", + "2024-12-23": "5487fd20ccbebda1093e298ebd7cef410cab1511", + "2024-12-24": "eb639250c990c63a3a532cd6b89083d41ca53f4f", + "2024-12-25": "15cee297c2b1dcbd327f4b6ac55ffa61a4dba20d", + "2024-12-26": "86dbedd949548888021bbb2dddf49db8760559c0", + "2024-12-27": "be7dc432aab4cf82e6e1d4188aaa1650c8ba55ab", + "2024-12-28": "7ff559de2c3f5d592535af2858eab4b4018dcb9e", + "2024-12-29": "00418a317cdbbf0c812c0210d33797851144c872", + "2024-12-30": "2b8804186034ec3e08662e69da8de056c3807649", + "2024-12-31": "348a03d36a49ccde84b78da993e0ceec15dcd4d5", + "2025-01-01": "c6ff348af6b450e62b0baf3e2e0504f72c9faebe", + "2025-01-02": "c5f8ab9e7476ebc93880ee6b98a5d33ae9068db9", + "2025-01-03": "fcc02cda8b501f67b412efa69632e30515bcd91b", + "2025-01-04": "0a97f13be0a512ec44d7dde9098c6e66eb67aa0f", + "2025-01-05": "9041af49b2260bfc951fce2880ae9036df4ef8a0", + "2025-01-06": "6e9162cb4b97c97ae0fca3602bf02c026ffb9dd3", + "2025-01-07": "968bff4424ecb86bc84ff7f4c6c0ffcb066a9bd4", + "2025-01-08": "d398d28a868f809dfd07083066c9a541c5e6b812", + "2025-01-09": "d68e14ba9aa6554d86f4466a9545cc7de0c45b7c", + "2025-01-10": "2026f155fac30713da051be10d3915b01686b17f", + "2025-01-11": "e714150a101f3e4c76b7cd72891a646075eccfc8", + "2025-01-12": "efba521d5743f1da24d30d9b056c72746feb089a", + "2025-01-13": "7d3ed1c19ed6808847422c45374ee4d266238639", + "2025-01-14": "c2a805cef7cda476273266edeac5ce795b906a57", + "2025-01-15": "44c986c45fb00d560e910e52aa2a561e03a6c42d", + "2025-01-16": "9796f3e807c4a6d6c0a1b933d96e265620f472cd", + "2025-01-17": "360e351c481783dac521cfe8b338ffad17a74b30", + "2025-01-18": "97235c7fc2bfd131bb19be8c720b223b41ad3c19", + "2025-01-19": "14b266832ef0c3f656956880ee4aba0ca26aaf77", + "2025-01-20": "130ee6ef701834d10b7624a28824a5e477905546", + "2025-01-21": "881214c648d73392dc4f130f4373f7ecde23e6a0", + "2025-01-22": "8bae7adf8d994af2a994be66b2eb35fc8790269d", + "2025-01-23": "9af7e8f7f7b1d01226e238310a871dfa447bb141", + "2025-01-24": "37912d459fbbede08a12735176e62df140060ead", + "2025-01-25": "66f11255274365f0492f8f95765516601cd29e7d", + "2025-01-26": "ab077423109982fc991791769c048b0fc199f5f8", + "2025-01-27": "0d06e9fb38d7f58a5b58179e44083f0606eb5981", + "2025-01-28": "0b577ee399da1daf098bcaa8288a36c420864fd2", + "2025-01-29": "b27508d513efe874829ae4520c40090843af4cce", + "2025-01-30": "48a06f1fe66b2a5572c6de82a59f10ab91c4fd17", + "2025-01-31": "580f10d24408e42e0abeea104a38f41bc9042e18", + "2025-02-01": "c430ef24ef9a9a99cdd4534420005f838ff55876", + "2025-02-02": "e1f18bdaef13e202956427116226d35015736559", + "2025-02-03": "5b6cd7feea5316809d71348ed664c8b96c1892aa", + "2025-02-04": "a21354a186a59d8db8954da373ccdc07e248113c", + "2025-02-05": "48df87678166fcd720a732bd2f9486e661f60712", + "2025-02-06": "dc5ee1441c233c4166256f97f95a20e6e0759cc7", + "2025-02-07": "3d3bc381f381486e7be00aef1c161475749f97cc", + "2025-02-08": "82cde948b97bdd1d847e7b1a3c282cabebf1b8f7", + "2025-02-09": "919d70ccb38f545fd9674c9d3c32a70d21ce239a", + "2025-02-10": "4e08e099e4e722f368f5b601b9fa17ef85ed2937", + "2025-02-11": "008de9f701c879c5a2e9e5afcc5de84be99510b3", + "2025-02-12": "dd25a51d94624ac53f8d7a292cb18f9ed95d257f", + "2025-02-13": "da0c4c77d23771d40e173db4c737368fcc91dbec", + "2025-02-14": "fc86cf0fb5897b1bd2b6e8d550a0916e200a01e6", + "2025-02-15": "7009816b83e7a5182681ed9e0243577bf358b4dc", + "2025-02-16": "c523d845a0743ffe02426502d5de9162eb251c05", + "2025-02-17": "ac246d27046be64677d6a5b35a49aa98b84ee345", + "2025-02-18": "77092cc4cc4d1c1061032e87710a6dc2648c9fcb", + "2025-02-19": "2e83c70ad371b30dd73b7ed71ca6efa6ae7a1c62", + "2025-02-20": "87c18d4bf64fa97ef0654801da25f6d3407d2331", + "2025-02-21": "41a5e455703cba29e01ad2631b6d2f301ceded4d", + "2025-02-22": "595a81848c514f898c7390d4304fa9134a0f6dc1", + "2025-02-23": "0bb1559ba86b944a2b94e69bb51a1c481938e92d", + "2025-02-24": "cca4742831b57bf368e0a3fb72787ba92c2ef35b", + "2025-02-25": "702f9aafabdd2275678547e98dd029643bd26966", + "2025-02-26": "08a71f2ab78988611bfa607264dcfaeaf786fe8d", + "2025-02-27": "04a520a782f6730c05eed13b82b2ad7d17c215e8", + "2025-02-28": "a3cad29718ea3797d718ab5cdcf145210cff7de6", + "2025-03-01": "b3096129f2297b3a7940a4d3f9f542c8318e016a", + "2025-03-02": "7e0d0aa85ea6f80c2e188e939ce67b3758faca99", + "2025-03-03": "8eeec8e54e0c2360fec6c0d60037f1687380f643", + "2025-03-04": "b5bdbf4745c86cf8e672bbd5bc3fcf71713fb78b", + "2025-03-05": "0a511282f6e0421a5679837d6d6d910df0536112", + "2025-03-06": "f112188583cc304a5ce5223833c36927315c3663", + "2025-03-07": "789fea22b2d696a76cbdbb1d9ac7da468509e438", + "2025-03-08": "b05e8108eb75dde62330b814e549802bc7f7a74e", + "2025-03-09": "58214a5d4200750fe1fd28ff9b546967df705a45", + "2025-03-10": "2d8730eac76e8f91f2b2aa228b84f21d6ff52176", + "2025-03-11": "a2b29437766c215957d16688fafea494cdd45fde", + "2025-03-12": "75af11fedc61c290f261f846c51d8bc644142166", + "2025-03-13": "9f4b9e2c18c217354892e86c165b8c836abfbe9d", + "2025-03-14": "19b9099bc0b6fd31dbf8245e904979f403eb4db0", + "2025-03-15": "634aeb954f2b9545cba2b1fc411ddbaef28d7b42", + "2025-03-16": "4c029b477f2036bb8436dff04c63d161a8b077c2", + "2025-03-17": "1306cdfa1080f2c9bd3758890193e2e2fd61c959", + "2025-03-18": "1cacc8cc373daf0633cb4b4c4b9ed70d43394ec9", + "2025-03-19": "750c2265405d2a18a1811fa1ca5390d09ac9d858", + "2025-03-20": "13a715709e5f1b7f6a2a5558250686c80b27546d", + "2025-03-21": "bdba64efac650073879984b2b8aaac7acfd308ea", + "2025-03-22": "35baf49f34a268a0c73e57b746d991f9be1bc788", + "2025-03-23": "54aa069f2b705f9af92e77384764c599ef6b8b4b", + "2025-03-24": "1fbc36bc2f1a56362fde411c76845decb20c793d", + "2025-03-25": "2ea1404a283f5b760b917ab2b5a8d8c313806dda", + "2025-03-26": "f614ebe87dee7a1a7b9c5044307b4984e7b989ec", + "2025-03-27": "7d03e5d29afa7429a78f4f2d434c648bad185a21", + "2025-03-28": "5c829e4b03325efc25d79383986f93a1b5d0e6ee", + "2025-03-29": "518eb69296a115cafeed35ae48e63006bdaf84c4", + "2025-03-30": "41454fb5b4f76cf40a79792766a30fe5dea42a24", + "2025-03-31": "38a483f473fdfff1a16612c1b64434a4c7c1c930", + "2025-04-01": "a8ffe6150e15ee8ad13f4bca8fc96d671e17f720", + "2025-04-02": "260e347d1ed5c6fe7622744eaec181cee7466fdb", + "2025-04-03": "b360cd4ab9d5767cbfc909d231ba4bef6e105333", + "2025-04-04": "810f1eb0906365e63fd72e5ec21a0a1a4231460e", + "2025-04-05": "5fad8789624da362aa0c792598739b1ef59a2159", + "2025-04-06": "1d2d0dc201502b830fea0ab6f3d690eb6f94d791", + "2025-04-07": "d9a1cf5d71d46287f79c7a93e5160da391bab12d", + "2025-04-08": "5c070dc22d9bb4aaf8e2e07a65e4d8e06b83cec2", + "2025-04-10": "c84c9c3b239b1e2ca1f388dbb40e654eaf7e1014", + "2025-04-11": "b8d8fb3bc78d94a9c6bd1c565e89f212b363aa5b", + "2025-04-12": "dd3c5a3f0ab73278186f0dac3c7ace9047f4f9fa", + "2025-04-13": "329295bb2054fc3f0ffae0760f322fe26b4a9287", + "2025-04-14": "dfa274d8a386a7f2e74144028a55f0320353fd90", + "2025-04-15": "04500f22be72d2e0284dbce202ae8fc559afcedf", + "2025-04-16": "1a8663c24f1b53f14fa00f703758bfbcecffa1c4", + "2025-04-17": "60ec7a88e805a342eee13f377bf998fb0f2b8648", + "2025-04-18": "60387223dbc6639af2359c7dc166dcd296a74a85", + "2025-04-19": "a44e409abdc286f09b67cc1a528d6a32e0c20af6", + "2025-04-20": "c2002af1cbbd10a7b880b8aaa12ffa975a59db21", + "2025-04-21": "5c27900b47657c01292bfce11ab060718ae2dd8c", + "2025-04-22": "8110c8c501be33e6a974f858a756bd1120667669", + "2025-04-23": "3c736c8d2b64825ddf85da6a6def9fe485b81423", + "2025-04-24": "f6ed7e67fde4fe5fcdc2c2d05974cef9f4059acf", + "2025-04-25": "e1c783075ae10c18be3393f211c7723e571c9593", + "2025-04-26": "18f5d257470ec2ae96397395e8aca6f9e9506eef", + "2025-04-27": "2fb1ba8c89ce285c11229f670112ff9ec122181a", + "2025-04-28": "5a401d87c2994c0c27a7c0ee848f01f66342ccd0", + "2025-04-29": "332985c032bd22a87eb7906536c0ec3b49e25cc9", + "2025-04-30": "40b5ed374ce1b4f0fc0e77298ef3798d7b5cdb37", + "2025-05-01": "44f2af441c8a579e1d2aa00a896998fe1f993c4a", + "2025-05-02": "b53a84f37641b91483390c381a5ad6f20b5261d0", + "2025-05-03": "45209f498a598f06e401edac2bfc23622e9894c3", + "2025-05-04": "3a4a5f9239c42fae98d3e022704f45ff14139687", + "2025-05-05": "128f09e29e335c82c9d1a08cd9e1facac2914bd5", + "2025-05-06": "f53c90f865bff8852d2a728e682fa97b11dcb50b", + "2025-05-07": "592a2c4ce8566dca651459158cdb16a43601a6fb", + "2025-05-08": "a63254d3e06d1b251a2e9fbca142e6da4508d05c", + "2025-05-09": "8020721d6ee9f2719309ff8ce3cb89f9b50e33d5", + "2025-05-10": "36a3a8eceba0e3cee23d5f5ab470a7da00bbf068", + "2025-05-11": "cd9abd9cac1944fd1d8244db4122224f1540f3d5", + "2025-05-12": "1f3f779c47f0edb50dfea0deb481c835cf89e593", + "2025-05-13": "d0322bf9dcc48c386255c2396eb92e41c6ab0351", + "2025-05-14": "ad1910e516e44959362ed32defd257837975aa38", + "2025-05-15": "99d2d64ad21f88bf77fe679d8442344d26ad2bfd", + "2025-05-16": "8adac0bd319fa48270c757309ed059fcb8e33f6f", + "2025-05-17": "bcd937b6ea00800b2009a925c92d4324a8944b79", + "2025-05-18": "e6b4429720d723a294e45f94ab02e5d8e382d189", + "2025-05-19": "3ea7c68de3786f18ebcedd662afd2bb40487d065", + "2025-05-20": "19d2046a667fb9cefcdf5d55127cde4ac652421b", + "2025-05-21": "b3b48efee42e6eb69cfddbde5d32d9c18a580315", + "2025-05-22": "f88f03d1894ed8bc33543baf95d91140ca56f609", + "2025-05-23": "e6d614c81288cfb382aab507be1731109bf9d3a5", + "2025-05-24": "04bdf781743bc8d5527bebde126593a59633daba", + "2025-05-25": "e7b96e83dcd8c491ac6e29a6d7dff2c73ad35489", + "2025-05-26": "79cfbdf5ebfaff840cbf83a991b2ac591ffe7ff9", + "2025-05-27": "ddf69a834a45d3834f0c7d623f914c9f7dac5033", + "2025-05-28": "7a7b3da7047978cc626bf6aac88b92aa40cbc080", + "2025-05-29": "9c7b115f2a1d2a79cd228a242d431f2207eb72df", + "2025-05-30": "561695d4b2052b45d87e4688b4d8499866f2201d", + "2025-05-31": "e8da67b526b3a343300b18ecf052a8802647b746", + "2025-06-01": "2b671796c3d649168a036a4b34599261b566bbd3", + "2025-06-02": "308bf419a9abb48adace357a1d1b87e399b6d83c", + "2025-06-03": "af4a1921b3d06386f1194846e1f8a67ac0e7cdf0", + "2025-06-04": "4c388144942b94e9b87bf2c64b0df1e4549c3397", + "2025-06-05": "49d4f31488b4efbf5526867247f3d4e441757d56", + "2025-06-06": "cc41d60b7f0fbd62a6baf699c996ecb975597379", + "2025-06-07": "d3a57d41cb20271c3335eed116fd8df213cdf04b", + "2025-06-08": "9a043c58ecc74ae58759e4e502cee83f962d1129", + "2025-06-09": "0a8df52f2fd8f27599e88692ca9a61cf4e0ccf22", + "2025-06-10": "b1d821dd178c345c2a6d7585066ec0fc36eae594", + "2025-06-11": "b7a47b3710b52c7601c678389257c0756f57f60c", + "2025-06-12": "646b510f79360a95bfdc72305c95eb1c18b264a6", + "2025-06-13": "a84d8c4789a9af03b252bc0f87e258a0b9ebd2ae", + "2025-06-14": "263be8675361e6b4e9ad8b7251fb932897e1f712", + "2025-06-15": "21734711cb31b1e1eb585e26027e339b7dbe051c", + "2025-06-16": "aa5bb2b9225782bcf5b3e624aebf4d5798e6fb82", + "2025-06-17": "306422b4715f71036c48392958652d91c1d6c379", + "2025-06-18": "6b0766ac68738ca7cddd783baca4ec75e4333d3b", + "2025-06-19": "c08d4a64492aec916ead2c9e962208c3c9259a85", + "2025-06-20": "a28d222ec888542403e896a505a3e6ad1bf48447", + "2025-06-21": "d70f423c08ec41d667d71ef478b730bed9d0e925", + "2025-06-22": "82c1427ecb881549ef808058c48fe92c82dd9469", + "2025-06-23": "70f4b5a4ca9d6d3aad4d66e26a298d3fae7459aa", + "2025-06-24": "7aa8bdb6ebae5857e43b3235af3695e9dd0ad0fd", + "2025-06-25": "928041bd0c92328fffeefde959ee74c4170a3278", + "2025-06-26": "cc157b95cdb415314443148da1e6895148d24042", + "2025-06-27": "9a628a6a81c1ba4f293c9b20b5c7b923d9703e51", + "2025-06-28": "5dabd41a827aeb0f1d3bd8a93c377818614c818d", + "2025-06-29": "b32d209e69973369a4dfe85b612a5e6e3a52206c", + "2025-06-30": "f97dc8c4a815229388464b24ae84cd7839b891ac", + "2025-07-01": "65daf2ecd6e2149f2286dc7979d766750e105a90", + "2025-07-02": "b482cf98ca903bdaa4403808a8f7cf930645511a", + "2025-07-03": "8d0979b9639300420e0f148e6b6e9e3a1dd041da", + "2025-07-04": "37f8117c147adf0467d436675b5cecd4ae4620c5", + "2025-07-05": "4fac29e04f6f58d5d7f33b98494c2bdae3f87776", + "2025-07-06": "217c1b1d641b2128166e7f3e6c2fe2b9c3bf98a1", + "2025-07-07": "7008f582beb9a27f32718f547946638e8b4ad252", + "2025-07-08": "5cf8314e08354235fe6f59badd3afa729d0df1fe", + "2025-07-09": "cee3239a5c892ebad0bba67fb6ea64231c64cf1c", + "2025-07-10": "f89733877809763261347135df24d11687b6c847", + "2025-07-11": "8f8563fbb35479cf00033d120675c0a3e8fc7573", + "2025-07-12": "6412c72d9b4d3de45245c17d634c58a6860a8be4", + "2025-07-13": "9c732b66d145841adb3d331a1188ed3f1fe6f0a8", + "2025-07-14": "29041ec2137f49438056ea0a0114ceda07abdbd2", + "2025-07-15": "a1697baeb399571b0336e77ae85dd77d914554e2", + "2025-07-16": "8dc09ff10e2adfe2dec74bf7b23998c3abe8459c", + "2025-07-17": "f5fc9a94a3af24d07cdce61d0f93e56c822344e2", + "2025-07-18": "715796dd9cad1760a5a0b0aac9aaddfcaa8db63c", + "2025-07-19": "6aae63e9c7e4011954c6730851151dc79a3e14b8", + "2025-07-20": "2b3c4b314b132a3e72470ff4359937ced85be989", + "2025-07-21": "46ebd3b9d264a6b31cb416944285d0b097c8da5c", + "2025-07-22": "5e2df4c5150d77c2f5bf911d1ccdbfcb20d45073", + "2025-07-23": "5c65640dc13b47c8ac83bdea877a94066eed44b0", + "2025-07-24": "6506bb565270aa3f730ba429580e246a9dd267a5", + "2025-07-25": "f905e4b4f3fdc5d27d90c6d8871408b81b104990", + "2025-07-26": "6cf02de09fe5f3f8c98133720dfaa6a7f3657a2d", + "2025-07-27": "408f5cc3e379e4555e65356b8b079f668f32687d", + "2025-07-28": "1c405f2f075b004a2e5e5f0c37e66afd8accd8ad", + "2025-07-29": "240b3c0e8bff6f1be337d1a0f67218ed2eb7cbe7", + "2025-07-30": "06fc0ae474692dab80caf538e9fe77f533cf3c76", + "2025-07-31": "25a874bcb0fef801c03dea4f6388f3f307afbaf4", + "2025-08-01": "a75e918e0e38bf76d5ef584b40287fb1f1a1b641", + "2025-08-02": "6cc38572456557ac3832d1791ad819a8c8781efa", + "2025-08-03": "350fd25253ad9246924b8afd5cc77501c24ac385", + "2025-08-04": "3ab4b16e53952ecaa759672de240f12d3e2c8f6f", + "2025-08-05": "40eac87ece5d93062c3e6ded0f5e708faf36b4ff", + "2025-08-06": "4065a521f459a9d66d3c5f0203ae8b795a42556f", + "2025-08-07": "81e3d4e070d5839a3890cc2e51148664ad64f435", + "2025-08-08": "408c09a27169cef2228ff53a45538066e9b7b754", + "2025-08-09": "dca46495a556131700778f83b9a575f02a37120b", + "2025-08-10": "c5a4470afadff18782fd278c079f90a3433e9509", + "2025-08-11": "283886f4a1684f17606d57b20556b8cada39273d", + "2025-08-12": "77cf871de8b5a17ff6af5b674d4dec6ed988ed97", + "2025-08-13": "d8466114691a0f32e3f8185951f4895c62791c6b", + "2025-08-14": "08fcda9a7855e76c2ce565bd354372138ea2114c", + "2025-08-15": "8856a0ebe187dbbf20ac002f3f3fb6b920bf0efe", + "2025-08-16": "7e60844833daed0eacbad8cbff2847fef2e3b94d", + "2025-08-17": "6316ff10cb19d0dde9368be27e33f450b9b7050e", + "2025-08-18": "a232891eb48a2c5a577ee0ffc083de256c005289", + "2025-08-19": "a8f9dcf0108236faa4ca21c746e8c692004f6efa", + "2025-08-20": "e45a4965fb727779e8a257d49fe95d6e2632d7f8", + "2025-08-21": "4e573fe18525553d9e5ec3c0d31f71a76cf898e4", + "2025-08-22": "b89b0ae7f45d40e5352086b837ea025351b848ac", + "2025-08-23": "fd174c189dd1d16397260ab888c1e16017debb17", + "2025-08-24": "523edaee4f1c840450da29ca0313748f67c2e762", + "2025-08-25": "b6d5c79871ba2b1fcc7ed7d8a334dfdf8551a1bc", + "2025-08-26": "9e047312fab864da6d7702545a18508deec50d23", + "2025-08-27": "50d48a56a980ad3ecf3ad0aba43bc8e9c2520ba6", + "2025-08-28": "f5eb4b6b806af6399a2f6d191fd17a698698ba7f", + "2025-08-29": "1e37df983aba731b683e3a14e0a2c68a15b236cc", + "2025-08-30": "8d253770e3ef76f0d032961c89c49966174a0b0e", + "2025-08-31": "cb66249ddfd55226756cf3c80ee136ab5b7d539f", + "2025-09-01": "7aec61a157c33f3bc0907b161b20516232925186", + "2025-09-02": "9c8a9ba6e25e909d082332bee482c01f8df59655", + "2025-09-03": "1b9a11c354d9f4cd4fd70e142ab94430660a0753", + "2025-09-04": "e0bf6743c74318a6fc43e511ec3e7b6567e8b96c", + "2025-09-05": "7222d118ad001964b7ff2649970e5849747d4652", + "2025-09-06": "07e1139796ae5108316733a54bf9f39c9cec4d42", + "2025-09-07": "89d82d91aa7f47fc33d86a96d3377145c30408d7", + "2025-09-08": "8d97d5b8ea6cbd993bd15e593c3d46d98f53d9db", + "2025-09-09": "474f654d6856bf0cb86dbcf740d18e0e637aa30e", + "2025-09-10": "6312d00b706a74440e4222ebdb64690676e46c44", + "2025-09-11": "37a12a40ceec88b74d60c68b95feeedcb1975e7a", + "2025-09-12": "45347f2ef534d34afb8f4374eee9e614aafe1f16", + "2025-09-13": "8e2ab659dd7060a3f0a83c2732829b810f1f150f", + "2025-09-14": "f4a316c01d37707feba1e4fa72f56556b86a2009", + "2025-09-15": "a57d874ae88368e48982c5dc78482b0e0b7aff09", + "2025-09-16": "7d0db2d40303eb5c4d4aa1c5eb291e837bf420b9", + "2025-09-17": "94dc0110faab5a359badb5afbd64338932c9537a", + "2025-09-18": "af894607758e56a7c943833dc487df3a8ca5a356", + "2025-09-19": "00db8e985d427522dc0e3363fd8d844c5eb080a7", + "2025-09-20": "b3371238542f022cdeda17002f9275b87254a09b", + "2025-09-21": "e81eb3dd61f8122b6a7446ce13571b5203ce01c6", + "2025-09-22": "ca6ec973f278000cd777f77fce0b0a4e6995319e", + "2025-09-23": "eccaa635497210c65ffbdcba2586546118d200d0", + "2025-09-24": "b5c571475164b1af29aac34be36f7260dab49c25", + "2025-09-25": "749e16c4fb081287392d4a03b5d4a79af7a911dc", + "2025-09-26": "6c648ceea7ae35c8aaad6fc46084373b4f20ab0e", + "2025-09-27": "f5d495cb45443e4b1ce1cf2f3daa00deca18b5ce", + "2025-09-28": "1947f99bd98dfd870f2cbf2dc60cafc03458292a", + "2025-09-29": "8041c50d78f82ac417466898a139b7198d099bd0", + "2025-09-30": "feff8810dff177603218358dfdc127197c30be7a", + "2025-10-01": "cc531ef4142e669f831c69f1e19740650eb6425c", + "2025-10-02": "33c0a91522fab0587e87034adfa0ff9a5cab6952", + "2025-10-03": "adeb6c46fcd456f5e4cd8d49f8f244c75f157f7e", + "2025-10-04": "58e619f65c4a7b9e839c581c3ae1679bdbe8936e", + "2025-10-05": "5c252782614b133ceb46ed48c4a57c2c6ebccb5f", + "2025-10-06": "0a438ff608480a14e0af0695145c73195059cdcc", + "2025-10-07": "5750fcc375ba91257ee500ac2815d700f20d51be", + "2025-10-08": "2f7cb17eac27a698f74247e62a39027c3a864c0c", + "2025-10-09": "66ee504b50d07f224414ae088a0221d8791aa377", + "2025-10-10": "f3e59372db9e10816a2fbfe5dc360232b07e9853", + "2025-10-11": "5f2c5b896e8ebdffcf002531862f40a0b1b08878", + "2025-10-12": "517caaf739b318712ebc8f790ad24f25ac02975e", + "2025-10-13": "f6753a5c557bb3206d047d78216f5c701fc33368", + "2025-10-14": "543ccc99d2ecef7cfc86698d14959cd3f8da23f2", + "2025-10-15": "efff823ecdc869e6ea5682b9845de3c68c9cfe2f", + "2025-10-16": "592dd0d35ce70c4040095f8b0fe5f13bd6c3fce0", + "2025-10-17": "d42a99714eeea8ca28a0949324135b4e8b5d5136", + "2025-10-18": "637d5f338b56504fd729f9cd8254bb74dd244f06", + "2025-10-19": "299994ee33f11e90a2e4613e7c61d0a406d9ac4c", + "2025-10-20": "e56f723a8b453a46f8c61fb8dab648537200a017", + "2025-10-21": "e9fc88961575686645a0b3a5f0bbe4b9639511fd", + "2025-10-22": "103d21278c37888e9539df17c24aec6b969e4e68", + "2025-10-23": "f5febc0b62271a807227dc1a53f6d71ff9dbbc25", + "2025-10-24": "602fc96aa9922fcc0c8e649880c187e7ca8b5121", + "2025-10-25": "c81e5ecf132f4ae60d612897aad66bf54ee7addb", + "2025-10-26": "ed7074f451c6c70962b5673d46462e0108ce57e5", + "2025-10-27": "57c762477d5c5b1a896f08b0c883bffc26f30c56", + "2025-10-28": "c13ad7fd132995ba69bdc9103765e12c3666f54e", + "2025-10-29": "78e69633b0a78757b9365832f814a587f7dc0470", + "2025-10-30": "df58189e83470899d4795cc3e8e4b7f55b066224", + "2025-10-31": "53df7bf0f9f44029731aae5a03551fca1f539274", + "2025-11-01": "2fae29421b89d21796856b4694c36be095263d62", + "2025-11-02": "9db954b6e407ee541b304144bbd4fca2366a36cd", + "2025-11-03": "77d2d3aa8b3dea5cbe5fef8ad65f35ca2082ba73", + "2025-11-04": "7eebff29f01db641d4e6d7bb757f93fccf881161", + "2025-11-05": "5bf41d48830f51e6de971fd0dffb7c40b729923f", + "2025-11-06": "45b0a8ee20b711f7556af78ef9ca4e738659f9a0", + "2025-11-07": "d66b4999f11495984df8f46c6c5b530ff4a93a28", + "2025-11-08": "f39718ec55ad21431104a910427bbcb91bcadd71", + "2025-11-09": "83b68c3c29d60b761ca977a234c0a3c818d03263", + "2025-11-10": "b37b5950aa970ed7abe27144f2ca62e6029215e5", + "2025-11-11": "5222b9b3cece5812ff9add134db393bb36dd49b1", + "2025-11-12": "919e966e5cb990f178a62f8f96482226b26f3d60", + "2025-11-13": "12094bd9d905962a163754ab60d77fd60d87b906", + "2025-11-14": "5dc86ad37cb42056759b3f6536a82db16981547d", + "2025-11-15": "31a8a1bee22146868624bd1274836e86548e39a1", + "2025-11-16": "88d033cbc863e02516eadd3899552840fddd6b65", + "2025-11-17": "aa78f731cdd5fb5d91b45d75bf5d7bb4632f1176", + "2025-11-18": "6030b134b69457e53a1b8d7d24383d4200e168c6", + "2025-11-19": "d2a51801cbf90d367a1730adb9768fe1e7e0ef57", + "2025-11-20": "fd0bda839201bd818af9da155e996f9e66bf56f4", + "2025-11-21": "d8ea3a1796dbeb81103509215ee8386170bd0ed4", + "2025-11-22": "f349486d4aa137dcb113bbb3a87c14e7a49d372a", + "2025-11-23": "dba4f292d1fbed1987496f5699858c40f87b23ba", + "2025-11-24": "cea48d4d2606c3c6de205bf7d28d1db4f27e2473", + "2025-11-25": "66fdcdfde3a19384ce64d4dadf9cf736c3843ee1", + "2025-11-26": "559d95d878f72fc134587d7748a041f5600b95dc", + "2025-11-27": "3f6e5f579c416c19b937f722b9b17845490c8644", + "2025-11-28": "b327cd31b01155607bc1ef47fc866f488edba963", + "2025-11-29": "984c8d215a1212343b19283a35d222af9ee9d504", + "2025-11-30": "fb05a54f36e2b959af5164611f79c23cbc704acf", + "2025-12-01": "d54a7a875e10a5e2a9b9a4dcfd72a3554f3b93e2", + "2025-12-02": "d933e20fb637ea230cac4ed311ee418d2fe4ecf1", + "2025-12-03": "a8907b31efa48be65aff6291da96abdd37afc9a1", + "2025-12-04": "e7656a07d166f06c0452894cfff7336f6d1d7c1c", + "2025-12-05": "1f27989920ede5ce526d4f7a5d0b791a67b67907", + "2025-12-06": "e09cbe8895833616307f7b4c35c6661e10ab8f2d", + "2025-12-07": "0583b5b9adad75887e228a9fca14e59a3d924479", + "2025-12-08": "1cac54332c640987bfbeeb786e09e763820c18d8", + "2025-12-09": "050585da95f48b0e2c6ecf11bfeca7d43b6bdcbf", + "2025-12-10": "29198f857a3025a0bb40b71fe7ff2c5dbd0abdc1", + "2025-12-11": "6cc4e89d3fd1a484c6b77d4193c6fc3572097488", + "2025-12-13": "d76fdddde9eaf6c08c0fee4886312daa05033ccb", + "2025-12-14": "71b23a3342ac2bda17b024e05a5919f2bf913827", + "2025-12-15": "bdfc55ed37f7d1228a856be0f57876596b04477a", + "2025-12-16": "15e31916be967c884a0570781333c36b9b555490", + "2025-12-17": "991219adfbc30967aecbcfd5385c485287dd1aee", + "2025-12-18": "57cc732c800bbe02b8d37493f7b8b042486fa5f3", + "2025-12-19": "167a811e98c49fb38d03fd1e0b90c2b6f6aceeb9", + "2025-12-20": "f13e540beb3145a6d9cf5b068cf564fe6907e3a7", + "2025-12-21": "a98ffa694948c07910033323493aa117693eb53a", + "2025-12-22": "1e98a9162ff29e163b874d7b17351e3212443f8c", + "2025-12-23": "7e3195ae6bee329bd39091c80d54b16be4c19b2b", + "2025-12-24": "d618dbbc856fc320fdaae3ce9953b9f9857009a3", + "2025-12-25": "f70e31b2b968769bb839c01a697d9d4d671fb152", + "2025-12-26": "1cfe3b8664898ce0f531369ee11f89de47c8e80d", + "2025-12-27": "e29df945907785daec08a157656e38aa8bfea0d1", + "2025-12-28": "8b3fe13952a2c6ca34525732ae3b5bba8e0c0ce2", + "2025-12-29": "a96500e60c91f34666e297e76c223296d3f543b2", + "2025-12-30": "e984296baee4f42e2857f0588e3386e08e3beb84", + "2025-12-31": "ac90543a6d2f67a6b5f1cf5ecbd60859b0d185b9", + "2026-01-01": "90e7435f3684709eaab351e375ed5a2d311f2765", + "2026-01-02": "df1ab8b75d52314f337c2b2bc894bced0dbadff7", + "2026-01-03": "db1f039dbe79a734a68b240a7f828501f7f798a6", + "2026-01-04": "41186dc39dd7927baea5336ba7fffc818761386a", + "2026-01-05": "74bdafa8755c589a377669dd66b28b457e8b77ed", + "2026-01-06": "14562b0db4b37fac0d3b28d653fad5cf39ab454b", + "2026-01-07": "f46888438d868f474238aef8806c60f685e3f780", + "2026-01-08": "0b9b100324dfc4078b5bce49effb2c76e97c5bea", + "2026-01-09": "a74e3dafbcddd43d419267511cbdf3b918ef7b38", + "2026-01-10": "c62ae74503ff2a6c8d1c088e32f0edfa628c6236", + "2026-01-11": "65837719ee7530c9bf53aa1dabdf3438e16a03b0", + "2026-01-12": "1fc8f31c4620e8291dfd10fdb0ed89c207f87860", + "2026-01-13": "393aa1a91c7396249c9a750706da3a73bdc55dd0", + "2026-01-14": "858b128a3271165855d7211e878e6e48be886dfa", + "2026-01-15": "a40c71f2cdd4124f4d619ec09edfd71c734e783b", + "2026-01-16": "523ed4f67bd9ace62daea1ce7c45bed6fd09c7bd", + "2026-01-17": "fcfcc89c0e76d49624cc2d89f19d95d77c81b1d4", + "2026-01-18": "575569318261dfed4051e0046ce32de18e0bfc6e", + "2026-01-19": "927be4d4e6f643164305d6d30cf09d2f1e7d16ac", + "2026-01-20": "a35f1fa483e42ae10f212124901355e75f7c4a0c", + "2026-01-21": "f280c38eb37e07ddda18680d0c1cdff97de8df72", + "2026-01-22": "45f9775f8d416ea7ab3582e1e737c172c6a2f7a5", + "2026-01-23": "7ae4e40be933a00f6e46d93264a4b34d9edd7079", + "2026-01-24": "616e2e0fa8ec18def49d30acd94493550ddd4f36", + "2026-01-25": "9e8e5dccc512853bd42902959864ad446856ad9f", + "2026-01-26": "4cf98f12168c1974a040ea34756f8dec5cfc793e", + "2026-01-27": "908a42479e15b87661c159bf11cdd92d2d64bca4", + "2026-01-28": "fb98e286fbe4992858216e1de335f3abe4ca5337", + "2026-01-29": "2d7276c2b584aae7664a2d5d9d04c3cd2095607b", + "2026-01-30": "2a0dac9615227a0fe95434b8cecd44c09cdc0af7", + "2026-01-31": "b2cff8fe2d67f8e83f6594f15a5a362a29517353", + "2026-02-01": "d2a98c6368e16ab0999d36a3c96bf42001d200dc", + "2026-02-02": "0d3fe7afce51d2126fdccf0b717d8048b90e4781", + "2026-02-03": "897ec533e7ee144fdcd67ec02a52dc0c6af8afe4", + "2026-02-04": "45a171cbfbcab508ef8cd756eb98a528861e160b", + "2026-02-05": "b57cddcc2e8b65b124e38b743d4d358ac748a959", + "2026-02-06": "e3788efdabdbd215cb80b1797f819c0fe74a9e51", + "2026-02-07": "94e4a562e48bbb7a7ab95fc248a91c85d3742c00", + "2026-02-08": "f1117ede3aa76477e14f3397a16d10ca158b0762", + "2026-02-09": "bb780cc307273f90b0e1c0f406e1857b50c4dd72", + "2026-02-10": "a9aed2a032780438b9137cce312eb00951fbf4ce", + "2026-02-11": "c58b2746d7635d3d08bdae5154feb6b8ccffd8a0", + "2026-02-12": "c302b00ef2220d3fd6940ff41a84a6cea9f33175", + "2026-02-13": "13c02fc6503e0681bfabd7ec9f9cfc56f58a2f5c", + "2026-02-14": "0c7ac5aeffdce1c40192bdacb5c3671c049d6205", + "2026-02-15": "49e85915dfb92cd1fbef13bd8e9244af2f42492d", + "2026-02-16": "b56df64c453105d8e4ebaa0cefd7acd357685fb0", + "2026-02-17": "25061203a5762040b35f4187cffec87aa271f122", + "2026-02-18": "1c5947d544a660e811332aa45feb2138973f4a4e", + "2026-02-19": "37b49f9f0f41f7dd634782c44fa3f170bc5f6d19", + "2026-02-20": "d388f1cc13cadd37396ed379a6b15025a82868b1", + "2026-02-21": "5d48e3e2f194b2457f167f066a04d5f7d68396fa", + "2026-02-22": "189cbd2af2a8e4af6ec2ba7045a919a04e4c1666", + "2026-02-23": "75a1cd8c0ad6a48dd3f582f29079277067fc6bc4", + "2026-02-24": "580ff8b0feab4945d227f0bfa22b60373d6e79dd", + "2026-02-25": "73c2af1f4cdd6864b34b5c15d0f7651741aad9bc", + "2026-02-26": "c0e98d83114bafaa1258a384a4610885a829960a", + "2026-02-27": "6737e965da4ac2de4448896e75b5accfa3fd6ffd", + "2026-02-28": "c468eb7ee0c418f840db367df4c7f63661d89f66", + "2026-03-01": "501201aa8a1187d4589c95d7b733f0fcf6b1de6e", + "2026-03-02": "435f0b18b8ad2f7b303fda1c275383d548290a1a", + "2026-03-03": "adc83d930378c8b2f2148c26174d8e6b9919fe87", + "2026-03-04": "6213cd0308302f0b63c92e91cfc011af582999cc", + "2026-03-05": "318c351440917c104fca78bba92efb477a062ca8", + "2026-03-06": "528050327f07bd3b82c0b0a946009699b24a42d1", + "2026-03-07": "a71d50e2ac277ea06bb457c5e036877dc87d8548", + "2026-03-08": "d921bfb813a973af33bd77260fbb18d5f4d8770c", + "2026-03-09": "9287d92b1c55954bc451e4c4932635ea67f57099", + "2026-03-10": "834585d16818d7e6f1f0e86907b3a48637e15778", + "2026-03-11": "19801a2d602f870f9f4d7e54a4cfb4d3036e8d94", + "2026-03-12": "2d81d5ee029dab05497c5c249dbcd5ec1fdfdd10", + "2026-03-13": "7a6c8b79cafa1d53424ca54e93479797529c200c", + "2026-03-14": "a637f335c51bdda8758c46b7ae62c6d93d31fc5f", + "2026-03-15": "059f4e29a20263dbec0e6d256fa11acf9b786867", + "2026-03-16": "8b1d0797e62367c23b687572c02c5b5224a05651", + "2026-03-17": "2ff0e92c624f14c0d68bfb4482c3f23473c52de1", + "2026-03-18": "876fbc7dd7fb21c71026e820640648382ce887eb", + "2026-03-19": "14db294ca36c8fd2d19718ccb2b2e06ce57a8a8e", + "2026-03-20": "b6ef15429a6d9873d211919751047b3214243b44", + "2026-03-21": "46900ca7e3471f85b8ef297dc81ea8252b2c0d30", + "2026-03-22": "b4c40bc453da981b69d2604e992046d8183e209e", + "2026-03-23": "84e9247b3aa3729057090c8148e3759d9da7b9ee", + "2026-03-24": "d617b2fd6d8f71061e88007d546c4a578f0fc0f9", + "2026-03-25": "8fc4bba2737236bda073fe9cf242693d8099a425", + "2026-03-26": "e295f3110d7d453c1caa5414aabf0115e20da1d2", + "2026-03-27": "a2670573510935c4b17b49b13544cab66298f3d2", + "2026-03-28": "bf7b81d3a5e3cfe5f207dd30895cca6ea7a0f0cb", + "2026-03-29": "cf7ce30a4f9dd9e6002d8ba1ef4f58d18a6bbeba", + "2026-03-30": "d65bf3d951d474c2759ac31a1c0796e5af565f7c", + "2026-03-31": "18dc48f5b9272bb97165fbe09644b4b0cce1cae2", + "2026-04-01": "a56ae01bbee90ef177e08a78eb410ced714d1a27", + "2026-04-02": "a6762bd47f82b0e37c84cc1f3cca69d37019b15c", + "2026-04-03": "b8d3f39e7ee809289b401df04eb02ed42f8af1ba", + "2026-04-04": "e39b0ffcf35c3c1ed6006d400bad78353822a5b7", + "2026-04-05": "e38f94dbb95e209acdc50897ba99db8e9a87ef29", + "2026-04-06": "577e9cf0442e030ac9f891cb3e56af9a208c8e20", + "2026-04-07": "16897c98bb4bf0d299844f8df0915e0970908fca", + "2026-04-08": "afe5ac83d0f7dc46d01e9d71526b7a00e555ff45", + "2026-04-09": "9f360fe75af4e62a57efd435a7e6ea65424a29d1", + "2026-04-10": "ec982202a586a33aa95617ca2682c1ad54b4d974", + "2026-04-11": "ec2b6ec5f3aab5b7c56008542e720e112545d4ef", + "2026-04-12": "da69d19162bb66b4fe89132b87415c9318fdee0c", + "2026-04-13": "bc80420e1659634defd6773e07abc5af1f8daf9f", + "2026-04-14": "8e4a065f1fa8a031c59a10856ad47df82d7db5b2", + "2026-04-15": "a66bd8f34d456f107474fc4cbd8435145a067fb2", + "2026-04-22": "db90c554b7172188324b84deea35ed5975e20e44", + "2026-04-23": "41b0718ce2da05b54beff75b403fac65442bacd2", + "2026-04-24": "75fe78b6b583875264c8a328c5e3b5b0ab2f6823", + "2026-04-25": "8e5aaa3a5c5bf6b6fb1e2314bc7690071025dc4a", + "2026-04-26": "5d8ea32fd99a765421d805460642a1ba42e0abee", + "2026-04-27": "e039d2edf51bffc256dcb07f511e0cd44f925794", + "2026-04-28": "053d2f5a5590cdedae6f41a0a17c7b378e36642f", + "2026-04-29": "f26886796c62c7c11dd98d0cf8ed4179a56b8297", + "2026-04-30": "c13dcec8918c60dda3175936b6778ad2baa37d6e", + "2026-05-01": "ed938f0bb99d1f0b7afca240907e60858026d3ed", + "2026-05-02": "84ec57e2a479800c2229494dda31e3ef2a22e0cf", + "2026-05-03": "c77d89ab7a427ad55ae12a297de3df461d7d4d6a", + "2026-05-04": "00dfcf80e580d1250013b8ab7655d66363af2128", + "2026-05-05": "88b129961686e0c8324b6720c1270083675fd498", + "2026-05-06": "a7a4f06c569dcfe9be37c34e6371b80588bd72a2", + "2026-05-07": "2099b47fbd9be6960bae05e5634a76e30f1ee496", + "2026-05-08": "1407df0ee7bd30ee5c53dbd31e046af36d5e4da7", + "2026-05-09": "cde5abf8a0d43918ad82a8116e26daff9c818326", + "2026-05-10": "dc88cff597e530ee4eded9310512a5edaa08060e", + "2026-05-11": "d184c75f3a843e5f9965c298ffd4f6b0f8b8a620", + "2026-05-12": "62705e89539037ee3b6f4a42fcf24ee3772033b0", + "2026-05-13": "d49c784bd0339ff40ad7cc2b03ff8541874c598e", + "2026-05-14": "1c4604401adfcf20943532977b93f29aadf37aad", + "2026-05-15": "2c6c8b63a64713ad144163da2fb48259e6d00002", + "2026-05-16": "bce35a5a2c3a820adb55f64758cabc1edfdca657", + "2026-05-17": "38e5e7d9dfdab8c07d94a99c781ce6c1e704c932", + "2026-05-18": "f69fab53bdff2bdde0bd1f04a71954a215dd6e45", + "2026-05-19": "0d109ba1431f60f42ebe2c394ce35e53060d9de3", + "2026-05-20": "8ae9498f48b88bc60a6f9d43bff2e73d16faf262", + "2026-05-21": "d476c9484b452fac24d61a74092a1952c58dc782", + "2026-05-22": "bfe748385c26995a7c53783d10ba961089a9d253", + "2026-05-23": "0545e0ba984c6da2deec5045d27d3df2065a0d97", + "2026-05-24": "6a2bb857b0f33ada3d74e3e9029e17cbf8618a29", + "2026-05-25": "7869550ec41e7484deb68db2bab17e6139715f3e", + "2026-05-26": "31d5018c0f68984414241b52130b5c2c0555cdd7", + "2026-05-27": "256f7ec3af0389c435fb0bde1821b142673e7197", + "2026-05-28": "527d95529237fb30bbf49b639da9ac712a98c3e2", + "2026-05-29": "0b19099fc6b5540eb58d3e521bd75fed9bed0dc4", + "2026-05-30": "0a5bc1cc8b6513b10f6f6bcaa33420d4d1019924", + "2026-05-31": "6f6d05c5e1dec3e0c5fdc37d820765af4ed76b26", + "2026-06-01": "57b4973541195b177ccaed2ead36c59d399b6e2b", + "2026-06-02": "e3a21e45a9e7820e50f687eb6e9799e0b35c6590", + "2026-06-03": "a92ca28995ff8ab6ba7aa34c0fa2459ca2b94280", + "2026-06-04": "239bb232311c8d46025f9fe834607e4598677311", + "2026-06-05": "e6d7104944a20ff5f69f6452e9b1fa04a312dcd2", + "2026-06-06": "91398cb92c11454803624e7da7e224f4492643f1", + "2026-06-09": "b32caa23bf821aa58ac4edd9da52ac086b1fba7e", + "2026-06-10": "6db230008f3069bebf9fd01872ec910acad69660", + "2026-06-11": "bd73b7c7f072a13b3004074cc5940d6ee81168ff", + "2026-06-12": "362730c421a31ccf4881044c28bcd3a60f58b747", + "2026-06-13": "bed04c4e0d0253916b7b2f817df13b036c6c7267", + "2026-06-14": "71b1e3bbc250ac6cd1981a4be8410dafc9a8c056", + "2026-06-15": "3b6e838dddf8cc30d124aa7f633ef3972dffd2f6", + "2026-06-16": "02c0b7e13b2bcb5002a46c5056de9a95f1038156", + "2026-06-17": "f3bbbbb335ac3b4d4f47fdf01b8aa296a354890e", + "2026-06-18": "d8aced85be8f3fc4f1021d1cbc070f4ad8fd334e", + "2026-06-19": "53f61665141494d6ffe3a85840fb0819527b3b5c", + "2026-06-20": "22e490c1d0f1f4751b4fbdc4b41300a310c18c52", + "2026-06-21": "6fcccb3c4d8c7cd82859bf1c8021949897489010", + "2026-06-22": "f67ebce4fe712df62704669e1246de139e761824", + "2026-06-23": "15812ee6146e1486d4f47cfeee6b81e38d7f6c30", + "2026-06-24": "6b74a74ddaa3e2feebb1db82589ab96dd72dbc4b", + "2026-06-25": "d449d4e08f8efaa71d826b0389231921db8c4965", + "2026-06-26": "c3320698bcbef22f7f0efd2b1519631d46b819dd", + "2026-06-27": "1515a433764ee761a9b996b2f00f3ec608574db8", + "2026-06-28": "1bf4cf43e4e784b85273c6e59c74d33010e8e75d", + "2026-06-29": "b7d84ff8f34128212dfc6d12950b0d9f9126d5ff", + "2026-06-30": "6ad395b0875b44a12e77bbd2f2e4ace629f0d64a", + "2026-07-01": "5f1b61cc88f324887b56a356524090cdb5e642cb", + "2026-07-02": "1000ef52a09426af61ef7d660be96bcc35fd7dd0", + "2026-07-03": "88bafc1811cc5f25f38867565829f0a4a2a9dea8", + "2026-07-04": "be986b767dcf57c8a72fd68c238cbf054c3b7891", + "2026-07-05": "e69252591b9c420b57ff26e09888ad544c9219d6", + "2026-07-06": "4d1acb1406d06151cfe7423e0ebed77d07437712", + "2026-07-07": "933ec1605536aab76ab7529790f3cbcb4371482d", + "2026-07-08": "66a3edbcea94df3940e734468b8e9ce10c6f59ac", + "2026-07-09": "2da3bbb863b9e6a2ab341407ad4235bc6b8351bf", + "2026-07-10": "cd036274bf8eb86707a3f8433124c7607d1233c6", + "2026-07-11": "d0143905974223d191a7cc85f7d0f0f8467f88bc", + "2026-07-12": "f0ac6e0e0a4e2838c56133b7b1a60eb02c62f792", + "2026-07-13": "14fe283af4448ac9783f217a7de3984a9a07c63e", + "2026-07-14": "a4cc38833c6aa93ce55d881aea24b1e8f89653c9", + "2026-07-15": "477fbedbc91d989e5320b2802e6a7f3fbf3f6d54", + "2026-07-16": "6c5d0432f5d050243c8ab8b0978d77e07e5cd492", + "2026-07-27": "d74819ccdfbfb904e8b739f0e30bffdfe45269f0", + "2026-07-28": "0cf681ff6fb74f3c009c73716e61b0f1a719af03", + "2026-07-29": "6b4a041405c496d44972fcb282edb6b52a6ea0f0", + "2026-07-30": "03fde9be24bec86c2f1dcf756a2b0ffdebd2ab7c", + "2026-07-31": "2e7fc2068d05ab68e6866d9e832f6c324500554f", + "2026-08-01": "b2898ef53c5039499d6544ce11660529e00116fd", + "2026-08-02": "3522b6c1bbf27a263c2568a2388a49611e962e7e", + "2026-08-03": "c91cb4fb688a8e59ec69085527f20ca5c8583ea0", + "2026-08-04": "5af6b7d54e499d84cc0d71444daa46efceb8bd22", + "2026-08-05": "8992310bd33876ef1ca7dabe97b6402632b38e28", + "2026-08-07": "fa31c22e36072dcc6349d5914c750471d287e675", + "2026-08-08": "8fe2f9eb19577dcefdfb0cc26bb60b8ed54acaf5", + "2026-08-09": "082489e9cabe734b40eb98f36a4666e7cc8cffac", + "2026-08-10": "497750a92c29621a063e2304cf65ed094396cd0a", + "2026-08-11": "108ae0c511ff80265d29e55c99a29484534e8728", + "2026-08-12": "0449f9456aafc909c986441e103d1c7397349cd1", + "2026-08-13": "6d1d91fccc7c22b478947186dce6a54089b2777e", + "2026-08-14": "dcf7c582956b037fee4a7b4e90e03f02655c0413", + "2026-08-15": "e33ed299e61c2db1af75e30f4e1d717d76486ad2", + "2026-08-16": "5a726be4c3410ed149b28258e179bf89ee6adaaa", + "2026-08-17": "2a400ddaee18857f35cb061573033d943f77e12f", + "2026-08-18": "2bd956502fd821bdb381a6d330be0e5926a48ac9", + "2026-08-19": "3526d7a49db49d8721bde5288323e53310b4eebc", + "2026-08-20": "d29a1e608420b49f16ebd640657485811e57d99c", + "2026-08-21": "3fe1065c4f942bbf60f3e6ac9930834ebe056650", + "2026-08-22": "c78b97c4656a830ae4d49fc1d435223f47bfc239", + "2026-08-23": "62ac55629c7cebc6123b2746978f86565e74c360", + "2026-08-24": "ece897712d1575133395002d37f4a758409bc452", + "2026-08-25": "8d0db5809623c9801b5cc126aaa49ab163dc712c", + "2026-08-26": "2fee6a2add8909e01538f306edf1a264930f1470", + "2026-08-27": "a76b96c618d0dfd4471645b0f63cc0b2c27d3c7b", + "2026-08-28": "e23648a0219356d8fa9db2e6cc4b267e91aa0055", + "2026-08-29": "cb6bf41cbfa13ecfbec4c6791776bbc0ab31b4df", + "2026-08-30": "86bdf8b137e0b532755450cf484fa48fc3d807b4", + "2026-08-31": "50ca267d574237d942c130eaadd1ac3380f936ad", + "2026-09-01": "703d97f5d456295afa8105793e97c498bee6d0d1", + "2026-09-02": "dffec5f2f6526d1cf3597aa5d9667e61e1e86d6d", + "2026-09-03": "7d2b9bc1e1cef997489de4c8a14964ff96f47ce4", + "2026-09-04": "50ee41c3cb7d69c1ad064d95753e0eef1cf1d8cd", + "2026-09-05": "4e108cfac1b69d00916d78de0b694ce374f35516", + "2026-09-06": "71b17d357c564270dffd07eaca9d9d6bb20c1789", + "2026-09-07": "b5ac94066cb3ec552ea5f3f5e1c6870dce0ea46b", + "2026-09-08": "7f6b72f145ba488112d15a93c109cb3754adc86c", + "2026-09-09": "ac7d12fa9cdb3339ee058f77fcd77fce58658bdd", + "2026-09-10": "f5c14ab2e63e72a11744fc0a58ccd40820c96c2f", + "2026-09-11": "498e71d7aebb1208203bed0440501c4b03d2153e", + "2026-09-12": "4867209f283f3ac84367e363200c298108d6cee1", + "2026-09-13": "de5dbf04bf1c57568aa82cdad0b2428c5c1f7c55", + "2026-09-14": "1c27572586ec2f1b5d61a5b6aa9b6464186ce3d7", + "2026-09-15": "fe9834ea1cfa18496d55388fde3b6e33f70723bb", + "2026-09-16": "2ef961426d1fe7a8a0814121d60f0f0d54420d4e", + "2026-09-17": "8bd209d1da2fbe97f87294fa648ea1378dac325f", + "2026-09-18": "8045471215d77f62fc7796780657a0311a23a8b0", + "2026-09-19": "e97802a9d6800ccebfd4d3b99018d722a9cc715c", + "2026-09-20": "b81b20c6af9c06c21c8d7616fef8c96c3c522358", + "2026-09-21": "2745ebb1fd9e684c63187d472c4a28ec0d083974", + "2026-09-22": "aaf19c740cb1d1ff0dfd4bd290052f5aa8eafe58", + "2026-09-23": "b33a375849dd98854e7a28d604f7822aafe0aa38", + "2026-09-24": "d41aa455e6dd75b17b7878c7503cd958014d8643", + "2026-09-25": "767ea6cd2b53d2bb0de8a013fed8289b43772307", + "2026-09-26": "10e500ecdc57637cbeb4dcfb96885dfd15c3624e", + "2026-09-27": "7bcb4fe28ce521f2131753c34f7bab07f26ba3cb", + "2026-09-28": "4d174a68f6dfb7eed498e0d00c20b95c3a5289fb", + "2026-09-29": "1f04f2f07f58df7aa65c1490416f8ece6ff06ea6", + "2026-09-30": "3701aa0d3387bd9701626c7b9c6c540fca9ad4d2", + "2026-10-01": "f15434b7fb50ba504cb48338db19b21692e68a5f", + "2026-10-02": "bb5d4ba25d855322f480c85b93cd7aee2b515f61", + "2026-10-03": "1c12f31565b89513ee5e8d9cf1257987f729711a", + "2026-10-04": "893ab0d4ccc70544360d16f99e9e4d17fa4776c7", + "2026-10-05": "317840d6ef310ba04b2dead3371977b0f7cb4ce3", + "2026-10-06": "526d2daf02b1b7d1f0447ec80ab73cb07fbb2447", + "2026-10-07": "e98ef8d7beb2e3e71c1d35a9b34732986287477d", + "2026-10-08": "3c9c0fbe377d28798a2f2a1ba11578e50df9cb0b" +} diff --git a/lib/r.nix b/lib/r.nix new file mode 100644 index 0000000..8d7e7ca --- /dev/null +++ b/lib/r.nix @@ -0,0 +1,17 @@ +# Pure helpers for R projects. Parsing DESCRIPTION is left to R itself +# (see `description-deps.R`), never reimplemented here. +{ lib }: +rec { + defaultFields = [ + "Depends" + "Imports" + "LinkingTo" + "Suggests" + ]; + + # Only a cheap presence test: DCF field names start a line, continuations are indented. + hasPackageField = text: lib.any (l: lib.hasPrefix "Package:" l) (lib.splitString "\n" text); + + # nixpkgs mangles CRAN names: data.table -> data_table + attrName = lib.replaceStrings [ "." ] [ "_" ]; +} diff --git a/lib/rprofile-snapshot.R b/lib/rprofile-snapshot.R new file mode 100644 index 0000000..c67f71f --- /dev/null +++ b/lib/rprofile-snapshot.R @@ -0,0 +1,14 @@ +# The CRAN snapshot date set by a project's .Rprofile (e.g. a Posit Package +# Manager `repos` URL); prints nothing if there is none. +# Usage: R_PROFILE_USER=.Rprofile Rscript rprofile-snapshot.R +repo <- getOption("repos")[["CRAN"]] +if (!is.null(repo)) { + pattern <- "^https://packagemanager[.]posit[.]co/cran/([0-9]{4}-[0-9]{2}-[0-9]{2})/?$" + if (grepl(pattern, repo)) { + date <- sub(pattern, "\\1", repo) + if (is.na(as.Date(date))) stop("nullkomma.r: invalid PPM snapshot date") + writeLines(date) + } else if (grepl("[0-9]{4}-[0-9]{2}-[0-9]{2}", repo)) { + stop("nullkomma.r: dated CRAN repository must use https://packagemanager.posit.co/cran/YYYY-MM-DD") + } +} diff --git a/make/help.mk b/make/help.mk deleted file mode 100644 index ac228b2..0000000 --- a/make/help.mk +++ /dev/null @@ -1,55 +0,0 @@ -# from https://gist.github.com/klmr/575726c7e05d8780505a -# Inspired by -# sed script explained: -# /^##/: -# * save line in hold space -# * purge line -# * Loop: -# * append newline + line to hold space -# * go to next line -# * if line starts with doc comment, strip comment character off and loop -# * remove target prerequisites -# * append hold space (+ newline) to line -# * replace newline plus comments by `---` -# * print line -# Separate expressions are necessary because labels cannot be delimited by -# semicolon; see -.PHONY: help -help: - @echo "$$(tput bold)Available rules:$$(tput sgr0)" - @echo - @sed -n -e "/^## / { \ - h; \ - s/.*//; \ - :doc" \ - -e "H; \ - n; \ - s/^## //; \ - t doc" \ - -e "s/:.*//; \ - G; \ - s/\\n## /---/; \ - s/\\n/ /g; \ - p; \ - }" ${MAKEFILE_LIST} \ - | LC_ALL='C' sort --ignore-case \ - | awk -F '---' \ - -v ncol=$$(tput cols) \ - -v indent=19 \ - -v col_on="$$(tput setaf 6)" \ - -v col_off="$$(tput sgr0)" \ - '{ \ - printf "%s%*s%s ", col_on, -indent, $$1, col_off; \ - n = split($$2, words, " "); \ - line_length = ncol - indent; \ - for (i = 1; i <= n; i++) { \ - line_length -= length(words[i]) + 1; \ - if (line_length <= 0) { \ - line_length = ncol - indent - length(words[i]) - 1; \ - printf "\n%*s ", -indent, " "; \ - } \ - printf "%s ", words[i]; \ - } \ - printf "\n"; \ - }' \ - | more $(shell test $(shell uname) == Darwin && echo '--no-init --raw-control-chars') diff --git a/make/main.mk b/make/main.mk deleted file mode 100644 index c759e13..0000000 --- a/make/main.mk +++ /dev/null @@ -1,21 +0,0 @@ -.DELETE_ON_ERROR: - -.PHONY: all help - -.DEFAULT_GOAL := help - -## Run all targets -all: check-flake render - -all-check: - check-nix - -.PHONY: gha -## Run approximately what is run on GitHub Actions -gha: gha-push - -.PHONY: gha-push -gha-push: build-nix check-flake-checker - -.PHONY: gha-cron -gha-cron: check-flake-checker update-flake diff --git a/make/nix.mk b/make/nix.mk deleted file mode 100644 index 4dc1f03..0000000 --- a/make/nix.mk +++ /dev/null @@ -1,34 +0,0 @@ -NIX_FILES := $(wildcard *.nix) - -.PHONY: build-nix -## Build all flake outputs with derivations -build-nix: - flake-iter build - -.PHONY: check-nix -## Check all nix stuff -check-nix: check-flake check-flake-checker - -.PHONY: check-flake -# Check nix flake for bad syntax etc. -check-flake: - nix flake check --all-systems - -.PHONY: check-flake-checker -# Check nix flake for outdated dependencies, etc. -check-flake-checker: flake.lock - flake-checker - -.PHONY: update-flake -# Update flake.lock file -update-flake: - nix flake update - -# Complete flake.lock file -flake.lock: $(NIX_FILES) - nix flake lock - -.PHONY: format -# Format all files - format: - treefmt diff --git a/make/quarto.mk b/make/quarto.mk deleted file mode 100644 index e81d401..0000000 --- a/make/quarto.mk +++ /dev/null @@ -1,4 +0,0 @@ -.PHONY: render - -render: - quarto render diff --git a/modules/core/devshell.nix b/modules/core/devshell.nix new file mode 100644 index 0000000..a7ad77d --- /dev/null +++ b/modules/core/devshell.nix @@ -0,0 +1,45 @@ +{ + flake.flakeModules.default = + { flake-parts-lib, lib, ... }: + { + options.perSystem = flake-parts-lib.mkPerSystemOption { + options.nullkomma.devshell = { + packages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = "Packages on `PATH` in `devShells.default`; every aspect appends here."; + }; + env = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Environment variables set in `devShells.default`."; + }; + shellHook = lib.mkOption { + type = lib.types.lines; + default = ""; + description = "Shell code run when entering `devShells.default`."; + }; + }; + }; + config.perSystem = + { config, pkgs, ... }: + let + cfg = config.nullkomma.devshell; + in + { + config = { + nullkomma.devshell.packages = [ + config.treefmt.build.wrapper + pkgs.flake-checker + pkgs.git + pkgs.nixd + ]; + devShells.default = lib.mkDefault ( + pkgs.mkShell { + inherit (cfg) packages env shellHook; + } + ); + }; + }; + }; +} diff --git a/modules/core/direnv.nix b/modules/core/direnv.nix new file mode 100644 index 0000000..f5a7c1e --- /dev/null +++ b/modules/core/direnv.nix @@ -0,0 +1,7 @@ +{ + flake.flakeModules.default = { + perSystem.nullkomma.files.".envrc".text = '' + use flake + ''; + }; +} diff --git a/modules/core/editor.nix b/modules/core/editor.nix new file mode 100644 index 0000000..49b62ed --- /dev/null +++ b/modules/core/editor.nix @@ -0,0 +1,72 @@ +{ + flake.flakeModules.default = + { config, lib, ... }: + let + cfg = config.nullkomma.editor.vscode; + in + { + options.nullkomma.editor.vscode = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to generate `.vscode/settings.json` and `.vscode/extensions.json`."; + }; + extensions = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Recommended VS Code extensions; every aspect appends here."; + }; + settings = lib.mkOption { + type = lib.types.attrsOf lib.types.anything; + default = { }; + description = "VS Code workspace settings; every aspect merges into these."; + }; + }; + config = { + nullkomma.editor.vscode = { + extensions = [ + "arrterian.nix-env-selector" + "ibecker.treefmt-vscode" + "jnoortheen.nix-ide" + ]; + settings = lib.mapAttrs (_: lib.mkDefault) { + "nixEnvSelector.nixFile" = "\${workspaceFolder}/flake.nix"; + "nixEnvSelector.useFlakes" = true; + "nixEnvSelector.suggestion" = false; + "nix.enableLanguageServer" = true; + "nix.serverPath" = "nixd"; + "nix.serverSettings".nixd.formatting.command = [ + "treefmt" + "--stdin" + "{file}" + ]; + "nix.formatterPath" = [ + "treefmt" + "--stdin" + "{file}" + ]; + "nix.hiddenLanguageServerErrors" = [ + "textDocument/definition" + "textDocument/completion" + "textDocument/documentHighlight" + ]; + "editor.defaultFormatter" = "ibecker.treefmt-vscode"; + "notebook.defaultFormatter" = "ibecker.treefmt-vscode"; + }; + }; + perSystem = + { pkgs, ... }: + let + json = pkgs.formats.json { }; + in + { + nullkomma.files = lib.mkIf cfg.enable { + ".vscode/extensions.json".source = json.generate "vscode-extensions.json" { + recommendations = lib.sort lib.lessThan (lib.unique cfg.extensions); + }; + ".vscode/settings.json".source = json.generate "vscode-settings.json" cfg.settings; + }; + }; + }; + }; +} diff --git a/modules/core/files.nix b/modules/core/files.nix new file mode 100644 index 0000000..6e34bf8 --- /dev/null +++ b/modules/core/files.nix @@ -0,0 +1,74 @@ +# Initialize repository files once; existing files belong to the user. +{ + flake.flakeModules.default = + { flake-parts-lib, lib, ... }: + { + options.perSystem = flake-parts-lib.mkPerSystemOption ( + { pkgs, ... }: + let + fileModule = + { name, config, ... }: + { + options = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to initialize `${name}` if absent."; + }; + text = lib.mkOption { + type = lib.types.nullOr lib.types.lines; + default = null; + description = "File content; sets `source`."; + }; + source = lib.mkOption { + type = lib.types.path; + description = "Store path whose content is written to `${name}`."; + }; + }; + config.source = lib.mkIf (config.text != null) ( + pkgs.writeText "nullkomma-${lib.replaceStrings [ "/" ] [ "-" ] name}" config.text + ); + }; + in + { + options.nullkomma.files = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule fileModule); + default = { }; + description = "Files, keyed by path relative to the repo root, that nullkomma initializes if absent. Existing files are never overwritten or checked for drift."; + }; + } + ); + config.perSystem = + { config, pkgs, ... }: + let + files = lib.filterAttrs (_: f: f.enable) config.nullkomma.files; + in + { + config = { + nullkomma.tasks.write-files = { + description = "Initialize missing repository files (.gitignore, .envrc, CI stubs, …), preserving existing files"; + runtimeInputs = [ + pkgs.coreutils + pkgs.git + ]; + command = '' + root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + '' + + lib.concatStrings ( + lib.mapAttrsToList (path: f: '' + target="$root"/${lib.escapeShellArg path} + if [ -e "$target" ] || [ -L "$target" ]; then + echo "kept ${path}" + else + mkdir -p "$(dirname "$target")" + cp --no-clobber ${f.source} "$target" + chmod u+w "$target" + echo "initialized ${path}" + fi + '') files + ); + }; + }; + }; + }; +} diff --git a/modules/core/github.nix b/modules/core/github.nix new file mode 100644 index 0000000..4d3f86b --- /dev/null +++ b/modules/core/github.nix @@ -0,0 +1,83 @@ +# Thin GitHub Actions stubs that call nullkomma's reusable workflows, so CI logic +# is upgraded centrally (.github/workflows/{ci,maintenance}.yml in this repo). +{ + flake.flakeModules.default = + { config, lib, ... }: + let + cfg = config.nullkomma.github; + uses = + name: + "${cfg.workflows.source}/${name}.yml" + + lib.optionalString (cfg.workflows.ref != null) "@${cfg.workflows.ref}"; + scalar = v: if lib.isBool v then lib.boolToString v else builtins.toJSON v; + ci = '' + name: CI + on: + pull_request: + push: + workflow_dispatch: + concurrency: + group: ''${{ github.workflow }}-''${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: + nullkomma: + uses: ${uses "ci"} + permissions: + contents: read + id-token: write + '' + + lib.optionalString (cfg.ci.secrets != [ ]) ( + " secrets:\n" + + lib.concatMapStrings (s: " ${s}: \${{ secrets.${s} }}\n") (lib.unique cfg.ci.secrets) + ) + + lib.optionalString (cfg.ci.inputs != { }) ( + " with:\n" + + lib.concatStrings (lib.mapAttrsToList (k: v: " ${k}: ${scalar v}\n") cfg.ci.inputs) + ); + in + { + options.nullkomma.github = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to initialize `.github/workflows/push.yml`."; + }; + workflows = { + source = lib.mkOption { + type = lib.types.str; + default = "dataheld/nullkomma/.github/workflows"; + description = "Where the reusable workflows live (`./.github/workflows` for local ones)."; + }; + ref = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = "main"; + description = "Git ref of the reusable workflows; null for local workflows."; + }; + }; + ci = { + inputs = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.oneOf [ + lib.types.bool + lib.types.int + lib.types.str + ] + ); + default = { }; + example = { + visibility = "public"; + }; + description = "Inputs of the reusable `ci.yml` (`visibility` publishes to FlakeHub, `deploy` runs `nix run .#deploy`)."; + }; + secrets = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Repository secrets passed to the reusable `ci.yml`."; + }; + }; + }; + config.perSystem.nullkomma.files = lib.mkIf cfg.enable { + ".github/workflows/push.yml".text = ci; + }; + }; +} diff --git a/modules/core/gitignore.nix b/modules/core/gitignore.nix new file mode 100644 index 0000000..4d44d8c --- /dev/null +++ b/modules/core/gitignore.nix @@ -0,0 +1,32 @@ +{ + flake.flakeModules.default = + { config, lib, ... }: + let + ignores = lib.unique config.nullkomma.gitignore; + in + { + options.nullkomma.gitignore = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Lines of the initial `.gitignore`; every aspect appends here."; + }; + config = { + nullkomma.gitignore = [ + ".direnv/" + "result" + "result-*" + ]; + # Initialized files are re-included last, so a global excludesFile (e.g. one ignoring + # `.vscode/settings.json` or `.envrc`) can't keep them out of the flake source. + perSystem = + { config, ... }: + { + nullkomma.files.".gitignore".text = lib.concatLines ( + ignores + ++ [ "# Initialized files" ] + ++ map (path: "!/${path}") (lib.attrNames (lib.filterAttrs (_: f: f.enable) config.nullkomma.files)) + ); + }; + }; + }; +} diff --git a/modules/core/pkgs.nix b/modules/core/pkgs.nix new file mode 100644 index 0000000..2977801 --- /dev/null +++ b/modules/core/pkgs.nix @@ -0,0 +1,18 @@ +# Consumers need no nixpkgs input: nullkomma's curated pin is the default `pkgs`. +{ inputs, ... }: +{ + flake.flakeModules.default = + { lib, ... }: + { + systems = lib.mkDefault [ + "aarch64-darwin" + "aarch64-linux" + "x86_64-linux" + ]; + perSystem = + { system, ... }: + { + _module.args.pkgs = lib.mkDefault inputs.nixpkgs.legacyPackages.${system}; + }; + }; +} diff --git a/modules/core/schemas.nix b/modules/core/schemas.nix new file mode 100644 index 0000000..c1591c5 --- /dev/null +++ b/modules/core/schemas.nix @@ -0,0 +1,50 @@ +# Consumers inherit output schemas without declaring another input. +{ inputs, ... }: +{ + flake.flakeModules.default = + { ... }: + let + isModule = m: builtins.isAttrs m || builtins.isFunction m; + children = what: output: { + children = builtins.mapAttrs (_: _: { inherit what; }) output; + }; + in + { + flake.schemas = inputs.flake-schemas.schemas // { + flakeModules = { + version = 1; + doc = "Aspects as [flake-parts](https://flake.parts) modules; import them into `nullkomma.lib.mkFlake`."; + inventory = output: { + children = builtins.mapAttrs (_: module: { + what = "flake-parts module"; + evalChecks.isModule = isModule module; + }) output; + }; + }; + flakeModule = { + version = 1; + doc = "Alias of `flakeModules.default`."; + inventory = module: { + what = "flake-parts module"; + evalChecks.isModule = isModule module; + }; + }; + modules = { + version = 1; + doc = "Modules by class (`modules..`), as defined by flake-parts."; + inventory = output: { + children = builtins.mapAttrs (class: children "${class} module") output; + }; + }; + lib = { + version = 1; + doc = "Nix library: `mkFlake`, `importTree` and pure helpers per language."; + inventory = output: { + children = builtins.mapAttrs (_: v: { + what = if builtins.isFunction v then "function" else "library"; + }) output; + }; + }; + }; + }; +} diff --git a/modules/core/site.nix b/modules/core/site.nix new file mode 100644 index 0000000..3b198c0 --- /dev/null +++ b/modules/core/site.nix @@ -0,0 +1,21 @@ +# Extension point: a docs aspect (quarto, pkgdown, …) sets the site, deploy aspects read it. +{ + flake.flakeModules.default = + { flake-parts-lib, lib, ... }: + { + options.perSystem = flake-parts-lib.mkPerSystemOption { + options.nullkomma.site = lib.mkOption { + type = lib.types.nullOr lib.types.package; + default = null; + description = "Static site (a directory with `index.html`); exposed as `packages.site`."; + }; + }; + config.perSystem = + { config, ... }: + { + config.packages = lib.mkIf (config.nullkomma.site != null) { + site = config.nullkomma.site; + }; + }; + }; +} diff --git a/modules/core/tasks.nix b/modules/core/tasks.nix new file mode 100644 index 0000000..107ed5d --- /dev/null +++ b/modules/core/tasks.nix @@ -0,0 +1,88 @@ +# Named tasks become `apps.` (`nix run .#`); `nix run` lists them. +{ + flake.flakeModules.default = + { flake-parts-lib, lib, ... }: + let + taskModule = { + options = { + description = lib.mkOption { + type = lib.types.str; + description = "One-line description, shown by `nix run`."; + }; + command = lib.mkOption { + type = lib.types.lines; + description = "Bash script (checked by shellcheck); arguments are in `$@`."; + }; + runtimeInputs = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = "Packages on `PATH` while the task runs."; + }; + }; + }; + in + { + options.perSystem = flake-parts-lib.mkPerSystemOption { + options.nullkomma.tasks = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule taskModule); + default = { }; + description = "Project tasks (the Makefile replacement), exposed as `apps.`."; + }; + }; + config.perSystem = + { config, pkgs, ... }: + let + cfg = config.nullkomma.tasks; + toApp = name: task: { + type = "app"; + program = lib.getExe ( + pkgs.writeShellApplication { + inherit name; + inherit (task) runtimeInputs; + text = task.command; + } + ); + meta.description = task.description; + }; + width = lib.foldl' lib.max 0 (map lib.stringLength (lib.attrNames cfg)) + 2; + pad = s: s + lib.concatStrings (lib.genList (_: " ") (width - lib.stringLength s)); + help = pkgs.writeShellApplication { + name = "nullkomma-help"; + text = '' + cat <<'EOF' + Tasks (nix run .#): + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: t: " ${pad n}${t.description}") cfg)} + + Also: nix develop, nix fmt, nix flake check, nix flake show + EOF + ''; + }; + in + { + config = { + nullkomma.tasks = { + check = { + description = "Evaluate and build all checks (nix flake check)"; + command = ''exec nix flake check "$@"''; + }; + flake-checker = { + description = "Check flake.lock for outdated or non-upstream Nixpkgs"; + runtimeInputs = [ pkgs.flake-checker ]; + command = ''exec flake-checker "$@"''; + }; + update = { + description = "Update flake.lock (prefer the scheduled maintenance workflow)"; + command = ''exec nix flake update "$@"''; + }; + }; + apps = lib.mapAttrs toApp cfg // { + default = lib.mkDefault { + type = "app"; + program = lib.getExe help; + meta.description = "List nullkomma tasks"; + }; + }; + }; + }; + }; +} diff --git a/modules/core/treefmt.nix b/modules/core/treefmt.nix new file mode 100644 index 0000000..817f189 --- /dev/null +++ b/modules/core/treefmt.nix @@ -0,0 +1,38 @@ +{ inputs, ... }: +{ + flake.flakeModules.default = + { lib, ... }: + { + imports = [ inputs.treefmt-nix.flakeModule ]; + perSystem = + { config, ... }: + { + treefmt = { + programs = { + # keep-sorted start block=yes + actionlint.enable = lib.mkDefault true; + deadnix.enable = lib.mkDefault true; + jsonfmt.enable = lib.mkDefault true; + keep-sorted.enable = lib.mkDefault true; + mdformat = { + enable = lib.mkDefault true; + plugins = lib.mkDefault (ps: [ + ps.mdformat-footnote + ps.mdformat-frontmatter + ps.mdformat-gfm + ps.mdformat-gfm-alerts + ]); + }; + nixfmt.enable = lib.mkDefault true; + shellcheck.enable = lib.mkDefault true; + shfmt.enable = lib.mkDefault true; + toml-sort.enable = lib.mkDefault true; + yamlfmt.enable = lib.mkDefault true; + # keep-sorted end + }; + # Generated files are formatted by their generator, not by treefmt. + settings.excludes = lib.attrNames (lib.filterAttrs (_: f: f.enable) config.nullkomma.files); + }; + }; + }; +} diff --git a/modules/deploy/cloudflare-pages.nix b/modules/deploy/cloudflare-pages.nix new file mode 100644 index 0000000..87b69aa --- /dev/null +++ b/modules/deploy/cloudflare-pages.nix @@ -0,0 +1,61 @@ +# Deploys `nullkomma.site` to Cloudflare Pages: `nix run .#deploy` locally, or +# from CI on pushes to the default branch (needs the two secrets below). +{ inputs, ... }: +{ + flake.flakeModules.cloudflare-pages = + { config, lib, ... }: + let + cfg = config.nullkomma.cloudflare-pages; + in + { + imports = [ inputs.self.flakeModules.default ]; + options.nullkomma.cloudflare-pages = { + project = lib.mkOption { + type = lib.types.str; + example = "my-docs"; + description = "Cloudflare Pages project name."; + }; + branch = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Cloudflare Pages branch; null uses the project's production branch."; + }; + }; + config = { + nullkomma.github.ci = { + inputs.deploy = true; + secrets = [ + "CLOUDFLARE_ACCOUNT_ID" + "CLOUDFLARE_API_TOKEN" + ]; + }; + perSystem = + { config, pkgs, ... }: + { + nullkomma.tasks.deploy = { + description = "Deploy the site to Cloudflare Pages (needs CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID)"; + runtimeInputs = [ pkgs.wrangler ]; + command = '' + exec wrangler pages deploy ${ + if config.nullkomma.site == null then + throw "nullkomma.cloudflare-pages: no `nullkomma.site`; import a docs aspect such as flakeModules.quarto" + else + config.nullkomma.site + } ${ + lib.escapeShellArgs ( + [ + "--project-name" + cfg.project + ] + ++ lib.optionals (cfg.branch != null) [ + "--branch" + cfg.branch + ] + ) + } "$@" + ''; + }; + }; + }; + }; +} diff --git a/modules/dev/docs.nix b/modules/dev/docs.nix new file mode 100644 index 0000000..c8f1fa4 --- /dev/null +++ b/modules/dev/docs.nix @@ -0,0 +1,80 @@ +# Option reference for the exported flakeModules, built as the flake-parts +# ecosystem does it (`nixosOptionsDoc` over an evaluated flake-parts module tree) +# and added to this repo's own Quarto site as `reference.qmd`. +{ + partitions.dev.module = + { + inputs, + lib, + ... + }: + { + perSystem = + { + pkgs, + system, + ... + }: + let + evaluated = inputs.flake-parts.lib.evalFlakeModule { inherit inputs; } { + imports = lib.attrValues (lib.removeAttrs inputs.self.flakeModules [ "flakeModules" ]); + systems = [ system ]; + perSystem._module.args.pkgs = pkgs; + }; + repo = "https://github.com/dataheld/nullkomma/blob/main/"; + relative = + declaration: + lib.head (lib.splitString "," (lib.removePrefix "${toString inputs.self}/" (toString declaration))); + optionsDoc = pkgs.nixosOptionsDoc { + # Only our own namespace: documenting foreign options would need their pkgs. + options = { + flake = evaluated.options.nullkomma; + perSystem = (evaluated.options.perSystem.type.getSubOptions [ "perSystem" ]).nullkomma; + }; + transformOptions = + opt: + opt + // { + declarations = map (d: { + name = "/${relative d}"; + url = repo + relative d; + }) opt.declarations; + }; + warningsAreErrors = false; + }; + reference = pkgs.runCommand "reference.qmd" { } '' + { + printf -- '---\ntitle: "Option reference"\n---\n\n' + printf 'Options of the exported `flakeModules`. Those under `perSystem` are set inside `perSystem = { ... }: { ... }`.\n\n' + cat ${optionsDoc.optionsCommonMark} + } > $out + ''; + in + { + packages.docs-reference = reference; + checks.docs-reference = pkgs.runCommandLocal "nullkomma-docs-reference" { } '' + for option in \ + nullkomma.github.enable \ + nullkomma.r.extraPackages \ + nullkomma.r.rprofile \ + perSystem.nullkomma.r.snapshot \ + perSystem.nullkomma.r.buildInputs \ + nullkomma.quarto.root \ + nullkomma.cloudflare-pages.project \ + perSystem.nullkomma.tasks \ + perSystem.nullkomma.files \ + perSystem.nullkomma.devshell.packages \ + perSystem.nullkomma.r.package \ + perSystem.nullkomma.quarto.extraFiles + do + grep -qF "## $(echo "$option" | sed 's/\./\\./g')" ${reference} || { + echo "reference is missing option $option" >&2 + exit 1 + } + done + touch $out + ''; + nullkomma.quarto.extraFiles."reference.qmd" = reference; + }; + }; +} diff --git a/modules/dev/dogfood.nix b/modules/dev/dogfood.nix new file mode 100644 index 0000000..cf2a8d6 --- /dev/null +++ b/modules/dev/dogfood.nix @@ -0,0 +1,26 @@ +# nullkomma uses nullkomma: this repo is a Quarto site (README as index.qmd). +{ + partitions.dev.module = + { inputs, ... }: + { + imports = [ + inputs.self.flakeModules.default + inputs.self.flakeModules.quarto + ]; + nullkomma = { + github = { + workflows = { + source = "./.github/workflows"; + ref = null; + }; + ci.inputs.visibility = "public"; + }; + gitignore = [ ".claude/" ]; + }; + perSystem = + { pkgs, ... }: + { + nullkomma.devshell.packages = [ pkgs.fh ]; + }; + }; +} diff --git a/modules/dev/partition.nix b/modules/dev/partition.nix new file mode 100644 index 0000000..4f9a52f --- /dev/null +++ b/modules/dev/partition.nix @@ -0,0 +1,14 @@ +# Dogfooding and tests are evaluated only in the `dev` partition. +# Schemas come from the shared default aspect imported by dogfooding. +{ inputs, lib, ... }: +{ + imports = [ inputs.flake-parts.flakeModules.partitions ]; + partitionedAttrs = lib.genAttrs [ + "apps" + "checks" + "devShells" + "formatter" + "packages" + "schemas" + ] (_: "dev"); +} diff --git a/modules/dev/tests.nix b/modules/dev/tests.nix new file mode 100644 index 0000000..ea23e31 --- /dev/null +++ b/modules/dev/tests.nix @@ -0,0 +1,300 @@ +# Contract tests: unit tests of ./lib, plus fixtures and templates evaluated as +# consumers would (nullkomma as their only input), whose checks, devshells and +# sites become checks of nullkomma itself. +{ + partitions.dev.module = + { inputs, lib, ... }: + let + nullkomma = inputs.self; + # Like `builtins.getFlake`, but pure and with `nullkomma` pointing at this + # checkout; flake-parts' `debug` exposes the configuration to the tests. + callFlake = + src: + let + flake = import (src + "/flake.nix"); + debugged = nullkomma // { + lib = nullkomma.lib // { + mkFlake = + args: module: + nullkomma.lib.mkFlake args { + imports = [ module ]; + debug = true; + }; + }; + }; + flakeInputs = { + self = self'; + nullkomma = debugged; + }; + outputs = flake.outputs flakeInputs; + self' = outputs // { + _type = "flake"; + outPath = src; + sourceInfo.outPath = src; + inputs = flakeInputs; + inherit outputs; + }; + in + self'; + fixtures = lib.genAttrs [ "md" "r" "quarto" ] (name: callFlake ../../tests/fixtures/${name}); + templates = lib.mapAttrs (_: t: callFlake t.path) nullkomma.templates; + in + { + perSystem = + { pkgs, system, ... }: + let + perSystem = flake: flake.allSystems.${system}; + has = attrs: name: attrs.${system} ? ${name}; + importTreeRoot = ../../tests/fixtures/import-tree; + tests = { + # keep-sorted start block=yes + testConsumersExportSchemas = { + expr = lib.mapAttrs (_: flake: { + versions = map (name: flake.schemas.${name}.version) [ + "apps" + "checks" + "devShells" + "formatter" + "packages" + ]; + app = + (flake.schemas.apps.inventory flake.apps) + .children.${system}.children.write-files.evalChecks.isValidApp; + check = + (flake.schemas.checks.inventory flake.checks).children.${system}.children.treefmt.isFlakeCheck; + shell = (flake.schemas.devShells.inventory flake.devShells).children.${system}.children ? default; + module = + (flake.schemas.flakeModules.inventory { + default = nullkomma.flakeModules.default; + }).children.default.evalChecks.isModule; + }) (fixtures // lib.mapAttrs' (name: lib.nameValuePair "template-${name}") templates); + expected = + lib.genAttrs + [ + "md" + "quarto" + "r" + "template-default" + "template-quarto" + "template-r" + ] + (_: { + versions = [ + 1 + 1 + 1 + 1 + 1 + ]; + app = true; + check = true; + shell = true; + module = true; + }); + }; + testGitignoreReincludesManagedFiles = { + expr = + let + lines = lib.splitString "\n" (perSystem fixtures.r).nullkomma.files.".gitignore".text; + in + { + envrc = lib.elem "!/.envrc" lines; + vscode = lib.elem "!/.vscode/settings.json" lines; + last = lib.last (lib.init lines) == "!/.vscode/settings.json"; + }; + expected = { + envrc = true; + vscode = true; + last = true; + }; + }; + testImportTreeSkipsUnderscoresAndNonNix = { + expr = map (lib.removePrefix (toString importTreeRoot)) ( + map toString (nullkomma.lib.importTree importTreeRoot) + ); + expected = [ + "/a.nix" + "/sub/b.nix" + ]; + }; + testInitializedFilesAreUserOwned = { + expr = { + cron = (perSystem fixtures.r).nullkomma.files ? ".github/workflows/cron.yml"; + driftCheck = has fixtures.r.checks "files"; + banner = + lib.hasInfix "Generated by nullkomma" + (perSystem fixtures.r).nullkomma.files.".github/workflows/push.yml".text; + }; + expected = { + cron = false; + driftCheck = false; + banner = false; + }; + }; + testMdHasNoR = { + expr = { + air = (perSystem fixtures.md).treefmt.programs.air.enable; + options = fixtures.md.debug.options.nullkomma ? r; + check = has fixtures.md.checks "r-cmd-check"; + }; + expected = { + air = false; + options = false; + check = false; + }; + }; + testQuartoComposesWithR = { + expr = { + engines = map (p: p.name) (perSystem fixtures.quarto).nullkomma.quarto.engines; + knitr = lib.elem "knitr" fixtures.quarto.debug.nullkomma.r.extraPackages; + env = (perSystem fixtures.quarto).nullkomma.devshell.env ? QUARTO_R; + check = has fixtures.quarto.checks "r-cmd-check"; + }; + expected = { + engines = [ (perSystem fixtures.quarto).nullkomma.r.package.name ]; + knitr = true; + env = true; + check = false; + }; + }; + testQuartoDeploys = { + expr = { + site = has fixtures.quarto.packages "site"; + deploy = has fixtures.quarto.apps "deploy"; + ci = + lib.hasInfix "CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}" + (perSystem fixtures.quarto).nullkomma.files.".github/workflows/push.yml".text; + }; + expected = { + site = true; + deploy = true; + ci = true; + }; + }; + testQuartoTemplateWithoutProjectHasNoSite = { + expr = { + site = has templates.quarto.packages "site"; + tasks = lib.attrNames ( + lib.filterAttrs ( + n: _: + lib.elem n [ + "preview" + "render" + ] + ) templates.quarto.apps.${system} + ); + }; + expected = { + site = false; + tasks = [ + "preview" + "render" + ]; + }; + }; + testRDependsOnDescription = { + expr = (perSystem fixtures.r).nullkomma.r.dependencies; + expected = [ + "testthat" + "withr" + ]; + }; + testRSnapshot = { + expr = (perSystem fixtures.r).nullkomma.r.snapshot; + expected = "2026-01-05"; + }; + testRHasAirAndCheck = { + expr = { + air = (perSystem fixtures.r).treefmt.programs.air.enable; + check = has fixtures.r.checks "r-cmd-check"; + gitignore = lib.elem "*.Rcheck/" fixtures.r.debug.nullkomma.gitignore; + }; + expected = { + air = true; + check = true; + gitignore = true; + }; + }; + testRLibAttrName = { + expr = nullkomma.lib.r.attrName "data.table"; + expected = "data_table"; + }; + testTemplatesAreFixtures = { + expr = map builtins.readFile [ + ../../templates/default/flake.nix + ../../templates/r/flake.nix + ]; + expected = map builtins.readFile [ + ../../tests/fixtures/md/flake.nix + ../../tests/fixtures/r/flake.nix + ]; + }; + # keep-sorted end + }; + failures = lib.debug.runTests tests; + fixtureChecks = lib.concatMapAttrs ( + name: flake: + lib.mapAttrs' (check: lib.nameValuePair "fixture-${name}-${check}") ( + flake.checks.${system} + // { + devshell = flake.devShells.${system}.default; + } + // lib.optionalAttrs (has flake.packages "site") { + site = flake.packages.${system}.site; + } + ) + ) fixtures; + in + { + checks = fixtureChecks // { + r-parsers = pkgs.runCommandLocal "nullkomma-r-parsers" { nativeBuildInputs = [ pkgs.R ]; } '' + export HOME="$TMPDIR" + printf 'Package: example\nVersion: 0.1\nDepends: R (>= 4.0)\nImports: stats, withr (>= 3.0.0)\n' > DESCRIPTION + printf 'withr\t3.0.3\n' > versions + Rscript --vanilla ${../../lib/description-constraints.R} DESCRIPTION versions Depends Imports + printf 'withr\t2.0.0\n' > versions + if Rscript --vanilla ${../../lib/description-constraints.R} DESCRIPTION versions Imports > error 2>&1; then exit 1; fi + grep -F 'selected version is 2.0.0' error + Rscript --vanilla ${../../lib/rprofile-snapshot.R} > date + test ! -s date + cp ${../../tests/fixtures/r/.Rprofile} .Rprofile + Rscript ${../../lib/rprofile-snapshot.R} > date + grep -Fx '2026-01-05' date + touch "$out" + ''; + file-initialization = pkgs.runCommandLocal "nullkomma-file-initialization" { } '' + mkdir project + cd project + ${fixtures.r.apps.${system}.write-files.program} + test -f .github/workflows/push.yml + test -f .Rprofile + printf '# My profile\n' > .Rprofile + test ! -e .github/workflows/cron.yml + test -w .github/workflows/push.yml + if grep -q 'Generated by nullkomma' .github/workflows/push.yml; then + exit 1 + fi + printf '# My workflow\n' > .github/workflows/push.yml + ${fixtures.r.apps.${system}.write-files.program} + printf '# My workflow\n' > expected + cmp expected .github/workflows/push.yml + printf '# My profile\n' > expected + cmp expected .Rprofile + test ! -e .github/workflows/cron.yml + touch $out + ''; + unit = pkgs.runCommandLocal "nullkomma-unit-tests" { } ( + if failures == [ ] then + "touch $out" + else + '' + cat >&2 <<'EOF' + ${lib.generators.toPretty { } failures} + EOF + exit 1 + '' + ); + }; + }; + }; +} diff --git a/modules/flake-parts.nix b/modules/flake-parts.nix new file mode 100644 index 0000000..77d4f00 --- /dev/null +++ b/modules/flake-parts.nix @@ -0,0 +1,6 @@ +# Makes `flake.flakeModules.` mergeable, so several files can each +# contribute to the same exported module (and adds den-ready `flake.modules.flake`). +{ inputs, ... }: +{ + imports = [ inputs.flake-parts.flakeModules.flakeModules ]; +} diff --git a/modules/lang/quarto/editor.nix b/modules/lang/quarto/editor.nix new file mode 100644 index 0000000..9fd4330 --- /dev/null +++ b/modules/lang/quarto/editor.nix @@ -0,0 +1,3 @@ +{ + flake.flakeModules.quarto.nullkomma.editor.vscode.extensions = [ "quarto.quarto" ]; +} diff --git a/modules/lang/quarto/gitignore.nix b/modules/lang/quarto/gitignore.nix new file mode 100644 index 0000000..6a24d8f --- /dev/null +++ b/modules/lang/quarto/gitignore.nix @@ -0,0 +1,8 @@ +{ + flake.flakeModules.quarto.nullkomma.gitignore = [ + "**/*.quarto_ipynb" + "/.quarto/" + "/_book/" + "/_site/" + ]; +} diff --git a/modules/lang/quarto/package.nix b/modules/lang/quarto/package.nix new file mode 100644 index 0000000..4b6df2c --- /dev/null +++ b/modules/lang/quarto/package.nix @@ -0,0 +1,29 @@ +{ inputs, ... }: +{ + flake.flakeModules.quarto = + { flake-parts-lib, lib, ... }: + { + imports = [ inputs.self.flakeModules.default ]; + options.perSystem = flake-parts-lib.mkPerSystemOption ( + { pkgs, ... }: + { + options.nullkomma.quarto.package = lib.mkOption { + type = lib.types.package; + default = pkgs.quartoMinimal.overrideAttrs (old: { + # nixpkgs#519484: quarto 1.9.37 emits `syntax-highlighting`, pandoc 3.7+ expects `highlight-style` + postPatch = (old.postPatch or "") + '' + substituteInPlace bin/quarto.js \ + --replace-fail "syntax-highlighting" "highlight-style" + ''; + # nixpkgs#393246: quarto shells out to `which` + postFixup = (old.postFixup or "") + '' + wrapProgram $out/bin/quarto --prefix PATH : ${lib.makeBinPath [ pkgs.which ]} + ''; + }); + defaultText = lib.literalMD "`pkgs.quartoMinimal` with nullkomma's workarounds"; + description = "Quarto, without bundled R/Python; engines come from the R/Python aspects."; + }; + } + ); + }; +} diff --git a/modules/lang/quarto/r.nix b/modules/lang/quarto/r.nix new file mode 100644 index 0000000..983599f --- /dev/null +++ b/modules/lang/quarto/r.nix @@ -0,0 +1,21 @@ +# Aspects composing aspects: with flakeModules.r imported too, Quarto renders +# R chunks with the project's R environment (plus knitr and rmarkdown). +{ + flake.flakeModules.quarto = + { lib, options, ... }: + { + # `config = optionalAttrs …`, not `mkIf`: undeclared options must not even be defined. + config = lib.optionalAttrs (options.nullkomma ? r) { + nullkomma.r.extraPackages = [ + "knitr" + "rmarkdown" + ]; + perSystem = + { config, ... }: + { + nullkomma.quarto.engines = [ config.nullkomma.r.package ]; + nullkomma.devshell.env.QUARTO_R = "${config.nullkomma.r.package}/bin/R"; + }; + }; + }; +} diff --git a/modules/lang/quarto/site.nix b/modules/lang/quarto/site.nix new file mode 100644 index 0000000..e3f30a9 --- /dev/null +++ b/modules/lang/quarto/site.nix @@ -0,0 +1,90 @@ +{ + flake.flakeModules.quarto = + { + config, + flake-parts-lib, + lib, + self, + ... + }: + let + cfg = config.nullkomma.quarto; + in + { + options.nullkomma.quarto = { + root = lib.mkOption { + type = lib.types.path; + default = self; + defaultText = lib.literalExpression "self"; + description = "Quarto project directory (containing `_quarto.yml`)."; + }; + site.enable = lib.mkOption { + type = lib.types.bool; + default = builtins.pathExists (cfg.root + "/_quarto.yml"); + defaultText = lib.literalMD "whether `root` has a `_quarto.yml`"; + description = "Whether `quarto render` of `root` becomes `nullkomma.site`."; + }; + }; + options.perSystem = flake-parts-lib.mkPerSystemOption { + options.nullkomma.quarto.engines = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = "Engine environments (R, Python) on `PATH` for rendering; set by the language aspects."; + }; + options.nullkomma.quarto.extraFiles = lib.mkOption { + type = lib.types.attrsOf lib.types.path; + default = { }; + description = "Generated files (relative path to source) copied into the project before `nullkomma.site` is rendered."; + }; + }; + config.perSystem = + { config, pkgs, ... }: + let + quarto = config.nullkomma.quarto.package; + inputs = [ quarto ] ++ config.nullkomma.quarto.engines; + in + { + config = { + nullkomma = { + devshell.packages = [ quarto ]; + site = lib.mkIf cfg.site.enable ( + lib.mkDefault ( + pkgs.stdenvNoCC.mkDerivation { + name = "site"; + src = cfg.root; + nativeBuildInputs = inputs; + preBuild = lib.concatMapAttrsStringSep "" (target: source: '' + mkdir -p "$(dirname ${lib.escapeShellArg target})" + cp ${source} ${lib.escapeShellArg target} + '') config.nullkomma.quarto.extraFiles; + buildPhase = '' + runHook preBuild + export HOME="$TMPDIR" + quarto render --output-dir "$TMPDIR/site" + runHook postBuild + ''; + installPhase = '' + runHook preInstall + cp -r "$TMPDIR/site" $out + runHook postInstall + ''; + } + ) + ); + tasks = { + preview = { + description = "Live-preview the Quarto project (quarto preview)"; + runtimeInputs = inputs; + command = ''exec quarto preview "$@"''; + }; + render = { + description = "Render the Quarto project (quarto render)"; + runtimeInputs = inputs; + command = ''exec quarto render "$@"''; + }; + }; + }; + }; + }; + }; +} diff --git a/modules/lang/r/checks.nix b/modules/lang/r/checks.nix new file mode 100644 index 0000000..8388a48 --- /dev/null +++ b/modules/lang/r/checks.nix @@ -0,0 +1,83 @@ +{ + flake.flakeModules.r = + { config, lib, ... }: + let + cfg = config.nullkomma.r; + rlib = import ../../../lib/r.nix { inherit lib; }; + isPackage = cfg.description != null && rlib.hasPackageField (builtins.readFile cfg.description); + in + { + options.nullkomma.r.check = { + enable = lib.mkOption { + type = lib.types.bool; + default = isPackage; + defaultText = lib.literalMD "whether DESCRIPTION has a `Package` field"; + description = "Whether to add `checks.r-cmd-check` (`R CMD build` + `R CMD check`)."; + }; + args = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "--no-manual" + "--ignore-vignettes" + ]; + description = "Arguments to `R CMD check`."; + }; + errorOn = lib.mkOption { + type = lib.types.enum [ + "error" + "warning" + "note" + ]; + default = "warning"; + description = "Least severe `R CMD check` finding that fails the check."; + }; + }; + config.perSystem = + { config, pkgs, ... }: + let + # nullkomma's own files are not part of the R package + strip = [ + "flake.nix" + "flake.lock" + ] + ++ lib.attrNames config.nullkomma.files; + failOn = + { + error = null; + warning = "WARNING"; + note = "WARNING|NOTE"; + } + .${cfg.check.errorOn}; + in + { + # mkIf on `checks`, not on the lazy attribute, so the attribute disappears + checks = lib.mkIf cfg.check.enable { + r-cmd-check = + pkgs.runCommand "r-cmd-check" + { + src = cfg.root; + nativeBuildInputs = [ config.nullkomma.r.package ]; + _R_CHECK_CRAN_INCOMING_ = "false"; + _R_CHECK_FORCE_SUGGESTS_ = "false"; + _R_CHECK_SYSTEM_CLOCK_ = "false"; + } + '' + export HOME="$TMPDIR" + cp -r "$src" pkg + chmod -R u+w pkg + (cd pkg && rm -rf ${lib.escapeShellArgs strip}) + R CMD build --no-build-vignettes --no-manual pkg + R CMD check ${lib.escapeShellArgs cfg.check.args} ./*.tar.gz + ${lib.optionalString (failOn != null) '' + if grep -Eq '^Status: .*(${failOn})' ./*.Rcheck/00check.log; then + echo "nullkomma: R CMD check found issues at or above errorOn = ${cfg.check.errorOn}" >&2 + exit 1 + fi + ''} + mkdir -p $out + cp -r ./*.Rcheck $out/ + ''; + }; + }; + }; +} diff --git a/modules/lang/r/devshell.nix b/modules/lang/r/devshell.nix new file mode 100644 index 0000000..fb5d81f --- /dev/null +++ b/modules/lang/r/devshell.nix @@ -0,0 +1,7 @@ +{ + flake.flakeModules.r.perSystem = + { config, ... }: + { + nullkomma.devshell.packages = [ config.nullkomma.r.package ]; + }; +} diff --git a/modules/lang/r/editor.nix b/modules/lang/r/editor.nix new file mode 100644 index 0000000..e6b13fd --- /dev/null +++ b/modules/lang/r/editor.nix @@ -0,0 +1,6 @@ +{ + flake.flakeModules.r.nullkomma.editor.vscode.extensions = [ + "Posit.air-vscode" + "REditorSupport.r" + ]; +} diff --git a/modules/lang/r/gitignore.nix b/modules/lang/r/gitignore.nix new file mode 100644 index 0000000..5061b9c --- /dev/null +++ b/modules/lang/r/gitignore.nix @@ -0,0 +1,9 @@ +{ + flake.flakeModules.r.nullkomma.gitignore = [ + "*.Rcheck/" + ".RData" + ".Rhistory" + ".Rproj.user/" + ".Ruserdata" + ]; +} diff --git a/modules/lang/r/options.nix b/modules/lang/r/options.nix new file mode 100644 index 0000000..126c40c --- /dev/null +++ b/modules/lang/r/options.nix @@ -0,0 +1,57 @@ +{ inputs, ... }: +{ + flake.flakeModules.r = + { + config, + lib, + self, + ... + }: + let + rlib = import ../../../lib/r.nix { inherit lib; }; + description = config.nullkomma.r.root + "/DESCRIPTION"; + rprofile = config.nullkomma.r.root + "/.Rprofile"; + in + { + imports = [ inputs.self.flakeModules.default ]; + options.nullkomma.r = { + root = lib.mkOption { + type = lib.types.path; + default = self; + defaultText = lib.literalExpression "self"; + example = lib.literalExpression "./pkg"; + description = "R project directory (containing `DESCRIPTION`); a path literal if not the repo root."; + }; + description = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = if builtins.pathExists description then description else null; + defaultText = lib.literalExpression ''root + "/DESCRIPTION" (if it exists)''; + description = "R DESCRIPTION file: the single source of truth for R dependencies."; + }; + rprofile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = if builtins.pathExists rprofile then rprofile else null; + defaultText = lib.literalExpression ''root + "/.Rprofile" (if it exists)''; + description = '' + Project `.Rprofile`. If it sets a CRAN snapshot date, e.g. + `options(repos = c(CRAN = "https://packagemanager.posit.co/cran/2026-01-05"))`, + R and packages come from the shipped rstats-on-nix revision for that date, + not directly from PPM; equivalence of the two package sets is not guaranteed. + Plain R and tools honoring `getOption("repos")` see the PPM repository. + The profile runs during evaluation in an isolated directory: keep it self-contained. + ''; + }; + fields = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = rlib.defaultFields; + description = "DESCRIPTION fields whose packages are added to the R environment."; + }; + extraPackages = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "devtools" ]; + description = "Extra CRAN/Bioconductor package names (e.g. dev tooling not in DESCRIPTION)."; + }; + }; + }; +} diff --git a/modules/lang/r/package.nix b/modules/lang/r/package.nix new file mode 100644 index 0000000..f4b00d7 --- /dev/null +++ b/modules/lang/r/package.nix @@ -0,0 +1,249 @@ +# R and its packages come from nixpkgs. The project's `.Rprofile` may pin a CRAN +# snapshot date (a Posit Package Manager `repos` URL); then they come from the +# rstats-on-nix/nixpkgs revision of that date (see `lib/r-snapshots.json`). +# `Remotes:` in DESCRIPTION, pinned to a commit, are layered on top. +{ inputs, ... }: +{ + flake.flakeModules.r = + { + config, + flake-parts-lib, + lib, + ... + }: + let + cfg = config.nullkomma.r; + rlib = import ../../../lib/r.nix { inherit lib; }; + in + { + options.perSystem = flake-parts-lib.mkPerSystemOption { + options.nullkomma.r.snapshot = lib.mkOption { + type = lib.types.nullOr lib.types.str; + readOnly = true; + description = "CRAN snapshot date set by `nullkomma.r.rprofile`, as read by R itself; null if none."; + }; + options.nullkomma.r.dependencies = lib.mkOption { + type = lib.types.listOf lib.types.str; + readOnly = true; + description = "Package names `nullkomma.r.description` depends on in `nullkomma.r.fields`, as parsed by R itself (base packages excluded)."; + }; + options.nullkomma.r.buildInputs = lib.mkOption { + type = lib.types.attrsOf (lib.types.listOf lib.types.package); + default = { }; + example = lib.literalExpression "{ sf = [ pkgs.gdal ]; }"; + description = "System libraries to add to the build of an R package, for packages whose needs nixpkgs does not know (typically `Remotes:`)."; + }; + options.nullkomma.r.package = lib.mkOption { + type = lib.types.package; + readOnly = true; + description = "R with every package from DESCRIPTION and `nullkomma.r.extraPackages`."; + }; + }; + config.perSystem = + { system, config, ... }: + let + pcfg = config.nullkomma.r; + boot = inputs.nixpkgs-r.legacyPackages.${system}; + + # IFD: bootstrap R reads the profile; selected R reads package metadata. + runR = + pkgs: script: files: args: + builtins.readFile ( + pkgs.runCommandLocal "r-${baseNameOf script}" + ( + { + nativeBuildInputs = [ pkgs.R ]; + } + // lib.mapAttrs (name: path: pkgs.writeText name (builtins.readFile path)) files + ) + '' + export HOME="$TMPDIR" + { + ${args} + } > "$out" + '' + ); + lines = text: lib.filter (l: l != "") (lib.splitString "\n" text); + + snapshot = + if cfg.rprofile == null then + null + else + let + date = runR boot ../../../lib/rprofile-snapshot.R { rprofile = cfg.rprofile; } '' + R_PROFILE_USER="$rprofile" Rscript ${../../../lib/rprofile-snapshot.R} + ''; + in + if lines date == [ ] then null else lib.head (lines date); + snapshots = lib.importJSON ../../../lib/r-snapshots.json; + pkgsR = + if snapshot == null then + boot + else + import (builtins.fetchTree { + type = "github"; + owner = "rstats-on-nix"; + repo = "nixpkgs"; + rev = + snapshots.${snapshot} or (throw ( + "nullkomma.r: no rstats-on-nix snapshot for ${snapshot} (from ${toString cfg.rprofile}); " + + "choose a date in lib/r-snapshots.json (available range: ${lib.head (lib.attrNames snapshots)} to ${lib.last (lib.attrNames snapshots)})." + )); + }) { inherit system; }; + + describe = + src: + let + meta = lines ( + runR pkgsR ../../../lib/description-meta.R { description = "${src}/DESCRIPTION"; } '' + Rscript ${../../../lib/description-meta.R} "$description" + '' + ); + in + { + src = + if + lines ( + runR pkgsR ../../../lib/description-remotes.R { description = "${src}/DESCRIPTION"; } '' + Rscript --vanilla ${../../../lib/description-remotes.R} "$description" + '' + ) != [ ] + then + throw "nullkomma.r: transitive Remotes are not supported; declare pinned overrides in the project's DESCRIPTION." + else + src; + package = lib.elemAt meta 0; + version = lib.elemAt meta 1; + dependencies = depsOf "${src}/DESCRIPTION" [ + "Depends" + "Imports" + "LinkingTo" + ]; + }; + depsOf = + file: fields: + lines ( + runR pkgsR ../../../lib/description-deps.R { description = file; } '' + Rscript ${../../../lib/description-deps.R} "$description" ${lib.escapeShellArgs fields} + '' + ); + + dependencies = lib.optionals (cfg.description != null) (depsOf cfg.description cfg.fields); + + # Only `owner/repo@` is accepted: a commit is the one reference + # that cannot move, which is what makes the lock meaningful. + parseRemote = + entry: + let + m = builtins.match "([A-Za-z0-9.]+=)?(github::)?([^/@#=:]+)/([^/@#=:]+)@([0-9a-f]{40})" entry; + in + if m == null then + throw "nullkomma.r: Remotes entry `${entry}` must be `owner/repo@<40-character commit sha>` (GitHub, pinned to a commit)." + else + let + remote = describe ( + builtins.fetchTree { + type = "github"; + owner = lib.elemAt m 2; + repo = lib.elemAt m 3; + rev = lib.elemAt m 4; + } + ); + alias = lib.elemAt m 0; + in + if alias != null && lib.removeSuffix "=" alias != remote.package then + throw "nullkomma.r: Remotes alias `${alias}` does not match `${remote.package}` in the fetched DESCRIPTION." + else + remote; + remotes = lib.optionals (cfg.description != null) ( + map parseRemote ( + lines ( + runR pkgsR ../../../lib/description-remotes.R { description = cfg.description; } '' + Rscript ${../../../lib/description-remotes.R} "$description" + '' + ) + ) + ); + + base = pkgsR.rPackages; + lookup = + name: + final.${rlib.attrName name} + or (throw "nullkomma.r: R package `${name}` is not in `rPackages` of nixpkgs-r; pin a newer snapshot or drop it from DESCRIPTION/extraPackages."); + remoteOverrides = lib.listToAttrs ( + map ( + r: + let + attr = rlib.attrName r.package; + inputs' = map lookup r.dependencies; + in + lib.nameValuePair attr ( + if base ? ${attr} then + base.${attr}.overrideAttrs (old: { + pname = r.package; + name = "r-${r.package}-${r.version}"; + inherit (r) src version; + propagatedBuildInputs = + inputs' ++ lib.filter (p: !(p ? rCommand)) (old.propagatedBuildInputs or [ ]); + nativeBuildInputs = inputs' ++ lib.filter (p: !(p ? rCommand)) (old.nativeBuildInputs or [ ]); + }) + else + base.buildRPackage { + pname = r.package; + inherit (r) src version; + propagatedBuildInputs = inputs'; + nativeBuildInputs = inputs'; + } + ) + ) remotes + ); + systemLibraries = lib.mapAttrs' ( + name: libs: + let + attr = rlib.attrName name; + in + lib.nameValuePair attr ( + (remoteOverrides.${attr} or base.${attr}).overrideAttrs (old: { + buildInputs = (old.buildInputs or [ ]) ++ libs; + }) + ) + ) pcfg.buildInputs; + final = base.override { overrides = remoteOverrides // systemLibraries; }; + + names = lib.unique (dependencies ++ cfg.extraPackages ++ [ "languageserver" ]); + versionNames = lib.unique (names ++ lib.concatMap (r: r.dependencies) remotes); + versions = pkgsR.writeText "r-selected-versions" ( + lib.concatMapStrings (name: "${name}\t${lib.getVersion (lookup name)}\n") versionNames + ); + validate = + file: fields: + runR pkgsR ../../../lib/description-constraints.R { description = file; } '' + Rscript --vanilla ${../../../lib/description-constraints.R} "$description" ${versions} ${lib.escapeShellArgs fields} + ''; + validation = + lib.optionals (cfg.description != null) [ (validate cfg.description cfg.fields) ] + ++ map ( + r: + validate "${r.src}/DESCRIPTION" [ + "Depends" + "Imports" + "LinkingTo" + ] + ) remotes; + uniqueRemotes = lib.length remotes == lib.length (lib.unique (map (r: r.package) remotes)); + remoteOrder = lib.toposort (a: b: lib.elem a.package b.dependencies) remotes; + in + { + config.nullkomma.r = { + inherit snapshot dependencies; + package = + if !uniqueRemotes then + throw "nullkomma.r: duplicate package names in Remotes." + else if remoteOrder ? cycle then + throw "nullkomma.r: dependency cycle between Remotes." + else + builtins.deepSeq validation (pkgsR.rWrapper.override { packages = map lookup names; }); + }; + }; + }; +} diff --git a/modules/lang/r/profile.nix b/modules/lang/r/profile.nix new file mode 100644 index 0000000..e8826b3 --- /dev/null +++ b/modules/lang/r/profile.nix @@ -0,0 +1,28 @@ +{ + flake.flakeModules.r = + { + config, + lib, + self, + ... + }: + let + cfg = config.nullkomma.r; + relativeRoot = lib.removePrefix "${toString self}/" (toString cfg.root); + profile = if toString cfg.root == toString self then ".Rprofile" else "${relativeRoot}/.Rprofile"; + date = lib.last (lib.attrNames (lib.importJSON ../../../lib/r-snapshots.json)); + in + { + perSystem = { + nullkomma.files.${profile}.text = '' + options(repos = c(CRAN = "https://packagemanager.posit.co/cran/${date}")) + ''; + nullkomma.devshell.shellHook = '' + if [ -z "''${R_LIBS_USER:-}" ]; then + export R_LIBS_USER="''${XDG_DATA_HOME:-$HOME/.local/share}/nullkomma/R/$(Rscript --vanilla -e 'cat(as.character(getRversion()))')" + fi + mkdir -p "$R_LIBS_USER" + ''; + }; + }; +} diff --git a/modules/lang/r/treefmt.nix b/modules/lang/r/treefmt.nix new file mode 100644 index 0000000..0a20ea2 --- /dev/null +++ b/modules/lang/r/treefmt.nix @@ -0,0 +1,44 @@ +{ + flake.flakeModules.r = + { config, lib, ... }: + let + cfg = config.nullkomma.r.jarl; + in + { + options.nullkomma.r.jarl.enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Lint and auto-fix R code with jarl as a treefmt formatter. + Off by default: nixpkgs has no aarch64-darwin binary (upstream tests fail there), + so it is compiled from source with tests disabled on macOS. + ''; + }; + config.perSystem = + { pkgs, ... }: + { + treefmt.programs.air.enable = lib.mkDefault true; + treefmt.settings.formatter.jarl = lib.mkIf cfg.enable { + command = lib.getExe ( + if pkgs.stdenv.hostPlatform.isDarwin then + pkgs.jarl.overrideAttrs { doCheck = false; } + else + pkgs.jarl + ); + options = [ + "check" + "--fix" + "--allow-dirty" + "--allow-no-vcs" + "--output-format=concise" + ]; + includes = [ + "*.R" + "*.r" + ]; + # fix lints first, then let air format the result + priority = -1; + }; + }; + }; +} diff --git a/modules/lib.nix b/modules/lib.nix new file mode 100644 index 0000000..6eec2c3 --- /dev/null +++ b/modules/lib.nix @@ -0,0 +1,16 @@ +{ inputs, lib, ... }: +{ + flake.lib = { + importTree = import ../lib/import-tree.nix lib; + r = import ../lib/r.nix { inherit lib; }; + # `flake-parts.lib.mkFlake`, with `flakeModules.default` already imported. + mkFlake = + args: module: + inputs.flake-parts.lib.mkFlake args { + imports = [ + inputs.self.flakeModules.default + module + ]; + }; + }; +} diff --git a/modules/templates.nix b/modules/templates.nix new file mode 100644 index 0000000..8f062c9 --- /dev/null +++ b/modules/templates.nix @@ -0,0 +1,31 @@ +let + welcomeText = '' + # Welcome to nullkomma + + Next steps: + + 1. `git add flake.nix` (flakes only see tracked files) + 2. `nix run .#write-files` to initialize `.gitignore`, `.envrc`, CI stubs, … + 3. `direnv allow` (or `nix develop`) + 4. `nix run` to list all tasks + ''; +in +{ + flake.templates = { + default = { + path = ../templates/default; + description = "nullkomma project: devshell, treefmt, checks, CI"; + inherit welcomeText; + }; + r = { + path = ../templates/r; + description = "nullkomma R project: dependencies from DESCRIPTION, air, R CMD check"; + inherit welcomeText; + }; + quarto = { + path = ../templates/quarto; + description = "nullkomma Quarto project: site build, render and preview tasks"; + inherit welcomeText; + }; + }; +} diff --git a/templates/base/.envrc b/templates/base/.envrc deleted file mode 100644 index 197c0d5..0000000 --- a/templates/base/.envrc +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -use flake diff --git a/templates/base/.github/workflows/cron.yml b/templates/base/.github/workflows/cron.yml deleted file mode 100644 index 759ebf6..0000000 --- a/templates/base/.github/workflows/cron.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Maintenance -on: - workflow_dispatch: - schedule: - - cron: "0 0 * * 0" # runs weekly on Sunday at 01:00 -jobs: - nix-maintenance: - name: Nix Maintenance - runs-on: ubuntu-latest - permissions: - id-token: "write" - contents: "write" - issues: write - pull-requests: write - steps: - - uses: actions/checkout@v6 - - uses: DeterminateSystems/determinate-nix-action@main - - uses: DeterminateSystems/flake-checker-action@main - - uses: DeterminateSystems/update-flake-lock@main - with: - pr-title: "Update flake.lock" - pr-labels: | - dependencies - automated diff --git a/templates/base/.github/workflows/push.yml b/templates/base/.github/workflows/push.yml deleted file mode 100644 index ecbdeca..0000000 --- a/templates/base/.github/workflows/push.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: CI -on: - pull_request: - workflow_dispatch: - push: -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true -jobs: - DeterminateCI: - uses: DeterminateSystems/ci/.github/workflows/workflow.yml@main - permissions: - id-token: "write" - contents: "read" - with: - fail-fast: false - runner-map: | - { - "aarch64-darwin": "macos-latest", - "aarch64-linux": "ubuntu-24.04-arm", - "x86_64-linux": "ubuntu-latest" - } - flake-checker: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Check Nix flake Nixpkgs inputs - uses: DeterminateSystems/flake-checker-action@v12 - with: - fail-mode: true diff --git a/templates/base/.gitignore b/templates/base/.gitignore deleted file mode 100644 index d53e06f..0000000 --- a/templates/base/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.direnv/ -result diff --git a/templates/base/flake.nix b/templates/base/flake.nix deleted file mode 100644 index cc80efd..0000000 --- a/templates/base/flake.nix +++ /dev/null @@ -1,29 +0,0 @@ -# flake.nix -{ - inputs.treefmt-nix.url = "github:numtide/treefmt-nix"; - - outputs = - { - self, - nixpkgs, - systems, - treefmt-nix, - }: - let - # Small tool to iterate over each systems - eachSystem = f: nixpkgs.lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system}); - - # Eval the treefmt modules from ./treefmt.nix - treefmtEval = eachSystem ( - pkgs: treefmt-nix.lib.evalModule pkgs { programs.mdformat.enable = true; } - ); - in - { - # for `nix fmt` - formatter = eachSystem (pkgs: treefmtEval.${pkgs.system}.config.build.wrapper); - # for `nix flake check` - checks = eachSystem (pkgs: { - formatting = treefmtEval.${pkgs.system}.config.build.check self; - }); - }; -} diff --git a/templates/default/flake.nix b/templates/default/flake.nix new file mode 100644 index 0000000..272a0f1 --- /dev/null +++ b/templates/default/flake.nix @@ -0,0 +1,7 @@ +{ + description = "A project with batteries included by nullkomma"; + + inputs.nullkomma.url = "https://flakehub.com/f/dataheld/nullkomma/0.1.*"; + + outputs = inputs: inputs.nullkomma.lib.mkFlake { inherit inputs; } { }; +} diff --git a/templates/quarto/flake.nix b/templates/quarto/flake.nix new file mode 100644 index 0000000..cfbf96c --- /dev/null +++ b/templates/quarto/flake.nix @@ -0,0 +1,18 @@ +{ + description = "A Quarto project with batteries included by nullkomma"; + + inputs.nullkomma.url = "https://flakehub.com/f/dataheld/nullkomma/0.1.*"; + + outputs = + inputs: + inputs.nullkomma.lib.mkFlake { inherit inputs; } { + imports = [ + inputs.nullkomma.flakeModules.quarto + # R chunks? Add the R aspect, too: + # inputs.nullkomma.flakeModules.r + # Deploy the site from CI: + # inputs.nullkomma.flakeModules.cloudflare-pages + ]; + # nullkomma.cloudflare-pages.project = "my-site"; + }; +} diff --git a/templates/r/flake.nix b/templates/r/flake.nix new file mode 100644 index 0000000..8187e9e --- /dev/null +++ b/templates/r/flake.nix @@ -0,0 +1,14 @@ +{ + description = "An R project with batteries included by nullkomma"; + + inputs.nullkomma.url = "https://flakehub.com/f/dataheld/nullkomma/0.1.*"; + # CRAN date lives in .Rprofile; packages and pinned Remotes live in DESCRIPTION. + + outputs = + inputs: + inputs.nullkomma.lib.mkFlake { inherit inputs; } { + imports = [ inputs.nullkomma.flakeModules.r ]; + # R packages come from DESCRIPTION; add dev-only ones here: + # nullkomma.r.extraPackages = [ "devtools" ]; + }; +} diff --git a/tests/fixtures/import-tree/_private/c.nix b/tests/fixtures/import-tree/_private/c.nix new file mode 100644 index 0000000..ffcd441 --- /dev/null +++ b/tests/fixtures/import-tree/_private/c.nix @@ -0,0 +1 @@ +{ } diff --git a/tests/fixtures/import-tree/_skip.nix b/tests/fixtures/import-tree/_skip.nix new file mode 100644 index 0000000..ffcd441 --- /dev/null +++ b/tests/fixtures/import-tree/_skip.nix @@ -0,0 +1 @@ +{ } diff --git a/tests/fixtures/import-tree/a.nix b/tests/fixtures/import-tree/a.nix new file mode 100644 index 0000000..ffcd441 --- /dev/null +++ b/tests/fixtures/import-tree/a.nix @@ -0,0 +1 @@ +{ } diff --git a/tests/fixtures/import-tree/readme.txt b/tests/fixtures/import-tree/readme.txt new file mode 100644 index 0000000..eff7872 --- /dev/null +++ b/tests/fixtures/import-tree/readme.txt @@ -0,0 +1 @@ +not nix diff --git a/tests/fixtures/import-tree/sub/b.nix b/tests/fixtures/import-tree/sub/b.nix new file mode 100644 index 0000000..ffcd441 --- /dev/null +++ b/tests/fixtures/import-tree/sub/b.nix @@ -0,0 +1 @@ +{ } diff --git a/tests/fixtures/md/README.md b/tests/fixtures/md/README.md new file mode 100644 index 0000000..ae1dac8 --- /dev/null +++ b/tests/fixtures/md/README.md @@ -0,0 +1,3 @@ +# Fixture: a project without any language aspect + +Formatting and linting only. diff --git a/tests/fixtures/md/flake.nix b/tests/fixtures/md/flake.nix new file mode 100644 index 0000000..272a0f1 --- /dev/null +++ b/tests/fixtures/md/flake.nix @@ -0,0 +1,7 @@ +{ + description = "A project with batteries included by nullkomma"; + + inputs.nullkomma.url = "https://flakehub.com/f/dataheld/nullkomma/0.1.*"; + + outputs = inputs: inputs.nullkomma.lib.mkFlake { inherit inputs; } { }; +} diff --git a/tests/fixtures/quarto/_quarto.yml b/tests/fixtures/quarto/_quarto.yml new file mode 100644 index 0000000..2f17cbc --- /dev/null +++ b/tests/fixtures/quarto/_quarto.yml @@ -0,0 +1,4 @@ +project: + type: website +website: + title: nullkomma fixture diff --git a/tests/fixtures/quarto/flake.nix b/tests/fixtures/quarto/flake.nix new file mode 100644 index 0000000..f5387a9 --- /dev/null +++ b/tests/fixtures/quarto/flake.nix @@ -0,0 +1,16 @@ +{ + description = "Fixture: Quarto site with R chunks, deployed to Cloudflare Pages"; + + inputs.nullkomma.url = "https://flakehub.com/f/dataheld/nullkomma/0.1.*"; + + outputs = + inputs: + inputs.nullkomma.lib.mkFlake { inherit inputs; } { + imports = with inputs.nullkomma.flakeModules; [ + cloudflare-pages + quarto + r + ]; + nullkomma.cloudflare-pages.project = "nullkomma-fixture"; + }; +} diff --git a/tests/fixtures/quarto/index.qmd b/tests/fixtures/quarto/index.qmd new file mode 100644 index 0000000..0a18b66 --- /dev/null +++ b/tests/fixtures/quarto/index.qmd @@ -0,0 +1,9 @@ +--- +title: nullkomma fixture +--- + +R chunks run with the R aspect's environment: + +```{r} +1 + 1 +``` diff --git a/tests/fixtures/r/.Rprofile b/tests/fixtures/r/.Rprofile new file mode 100644 index 0000000..2f4c036 --- /dev/null +++ b/tests/fixtures/r/.Rprofile @@ -0,0 +1 @@ +options(repos = c(CRAN = "https://packagemanager.posit.co/cran/2026-01-05")) diff --git a/tests/fixtures/r/DESCRIPTION b/tests/fixtures/r/DESCRIPTION new file mode 100644 index 0000000..1ade30a --- /dev/null +++ b/tests/fixtures/r/DESCRIPTION @@ -0,0 +1,17 @@ +Package: nullkommafixture +Title: Fixture for the Nullkomma R Aspect +Version: 0.0.1 +Authors@R: person("nullkomma", email = "nullkomma@example.com", role = c("aut", "cre")) +Description: Exercises the R aspect of nullkomma: dependencies from the + DESCRIPTION file, formatting with air and R CMD check. +License: CC0 +Encoding: UTF-8 +Depends: + R (>= 4.1.0) +Imports: + stats +Suggests: + testthat (>= 3.0.0), + withr (>= 3.0.3.9000) +Remotes: r-lib/withr@d82e4bc2d69a34f044ad205210e26207bfb8f3e0 +Config/testthat/edition: 3 diff --git a/tests/fixtures/r/NAMESPACE b/tests/fixtures/r/NAMESPACE new file mode 100644 index 0000000..01843dc --- /dev/null +++ b/tests/fixtures/r/NAMESPACE @@ -0,0 +1,3 @@ +# Generated by roxygen2: do not edit by hand + +export(hello) diff --git a/tests/fixtures/r/R/hello.R b/tests/fixtures/r/R/hello.R new file mode 100644 index 0000000..3e1c375 --- /dev/null +++ b/tests/fixtures/r/R/hello.R @@ -0,0 +1,8 @@ +#' Say hello +#' +#' @param name Who to greet. +#' @return A greeting. +#' @export +hello <- function(name = "world") { + paste0("Hello, ", name, "!") +} diff --git a/tests/fixtures/r/flake.nix b/tests/fixtures/r/flake.nix new file mode 100644 index 0000000..8187e9e --- /dev/null +++ b/tests/fixtures/r/flake.nix @@ -0,0 +1,14 @@ +{ + description = "An R project with batteries included by nullkomma"; + + inputs.nullkomma.url = "https://flakehub.com/f/dataheld/nullkomma/0.1.*"; + # CRAN date lives in .Rprofile; packages and pinned Remotes live in DESCRIPTION. + + outputs = + inputs: + inputs.nullkomma.lib.mkFlake { inherit inputs; } { + imports = [ inputs.nullkomma.flakeModules.r ]; + # R packages come from DESCRIPTION; add dev-only ones here: + # nullkomma.r.extraPackages = [ "devtools" ]; + }; +} diff --git a/tests/fixtures/r/man/hello.Rd b/tests/fixtures/r/man/hello.Rd new file mode 100644 index 0000000..a10626c --- /dev/null +++ b/tests/fixtures/r/man/hello.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/hello.R +\name{hello} +\alias{hello} +\title{Say hello} +\usage{ +hello(name = "world") +} +\arguments{ +\item{name}{Who to greet.} +} +\value{ +A greeting. +} +\description{ +Say hello +} diff --git a/tests/fixtures/r/tests/testthat.R b/tests/fixtures/r/tests/testthat.R new file mode 100644 index 0000000..fe11a80 --- /dev/null +++ b/tests/fixtures/r/tests/testthat.R @@ -0,0 +1,4 @@ +library(testthat) +library(nullkommafixture) + +test_check("nullkommafixture") diff --git a/tests/fixtures/r/tests/testthat/test-hello.R b/tests/fixtures/r/tests/testthat/test-hello.R new file mode 100644 index 0000000..8b87009 --- /dev/null +++ b/tests/fixtures/r/tests/testthat/test-hello.R @@ -0,0 +1,4 @@ +test_that("hello greets", { + expect_equal(hello("you"), "Hello, you!") + expect_equal(as.character(packageVersion("withr")), "3.0.3.9000") +}) diff --git a/tools/update-r-snapshots.py b/tools/update-r-snapshots.py new file mode 100644 index 0000000..f2770c8 --- /dev/null +++ b/tools/update-r-snapshots.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Append dated rstats-on-nix revisions without changing existing pins.""" +import json +import pathlib +import re +import subprocess + +path = pathlib.Path(__file__).resolve().parents[1] / "lib/r-snapshots.json" +snapshots = json.loads(path.read_text()) +refs = subprocess.check_output( + ["git", "ls-remote", "--heads", "https://github.com/rstats-on-nix/nixpkgs.git"], + text=True, +) +for line in refs.splitlines(): + sha, ref = line.split() + date = ref.removeprefix("refs/heads/") + if re.fullmatch(r"\d{4}-\d{2}-\d{2}", date): + snapshots.setdefault(date, sha) +path.write_text(json.dumps(snapshots, indent=2, sort_keys=True) + "\n")