Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.12.0.0 - 2026-11-01

- **`nova-cache-server reclaim-nars` lists, and with `--delete` removes, the NARs no narinfo references.** A client uploads a path's NAR before its narinfo, as upstream's `BinaryCacheStore::addToStoreCommon` does, so a NAR whose narinfo the server refuses, or whose push dies before the narinfo, stayed under `nar/` for good: the server has no route that deletes, and gains none. The subcommand runs on the host against the store directory. A narinfo names its NAR by its `URL` alone, as upstream reads it (`getFile(info->url)`), with the compression suffix part of the name. Three rules guard a run. A NAR is eligible only once it is two weeks old by its modification time (`--min-age-hours` overrides), because a NAR is unreferenced until its push sends the narinfo and nothing bounds how long a push takes; git's `gc.pruneExpire` gives unreferenced objects the same two weeks against the same race. A narinfo that cannot be read, parsed, or resolved to `nar/<file>` blocks the whole run instead of being skipped, since the NAR it names would otherwise look unreferenced. And a store holding NARs and no narinfo at all blocks it too, since one that lost its `narinfo/` directory, which the server recreates empty when it starts, looks the same as one whose every NAR is unreferenced. A run is not atomic with the server, so it belongs on a cache with no push in progress: a narinfo that arrives during a run for a NAR past the minimum age, as upstream's `nix copy` can send after skipping a NAR already present, loses that NAR. Upstream ships no garbage collection for binary caches, so there is no behavior to match beyond the URL. The library gains `NovaCache.Reclaim` (the pure selection and the store scan) and, in `NovaCache.Store`, `listNarFiles`, `listNarInfoFiles`, `removeNar` and `fileStoreAt`, a constructor that creates no directories; `time` joins the dependencies.
- **A narinfo is refused unless the NAR its `URL` names is stored and matches its `FileSize` and `FileHash`.** `PUT /<hash>.narinfo` validated the narinfo's own fields and signed it without looking at the NAR, so a NAR that arrived short or wrong sat behind a signed narinfo: clients rejected the NAR on its hash, and nova-nix's push never repaired it, because a push skips every path whose narinfo exists. Warp before 3.4.17 made the short case reachable: it ended a chunked request body at the connection's end of input as if the terminating chunk had arrived (yesodweb/wai#1129), so a `PUT /nar/<file>` cut off mid-chunk got 200 and stored a prefix. The narinfo is now refused with a 400 naming the problem unless its `URL` resolves, exactly as a GET for it is routed, to a file under `nar/` of `FileSize` bytes that hashes to `FileHash`, so an upload that went wrong fails at the narinfo. nova-nix's next push sends both again. Upstream's `nix copy` skips a NAR whose file already exists, so a wrong file under that name goes on refusing the narinfo until `reclaim-nars` removes it. A narinfo without `FileSize` or `FileHash` is refused as well, since the signature does not cover them and nothing else records which file the narinfo describes, and so is a `URL` that is not under `nar/`, such as an absolute one, which upstream can read but its `BinaryCacheStore` never writes. Upstream's `BinaryCacheStore::addToStoreCommon` and nova-nix's push both send the two fields and a relative `nar/` URL, and both upload the NAR before its narinfo, so a complete upload from either is accepted. `cacheApp` refuses requests it used to accept, which is why this is a major version. The server reads the stored NAR through once more when the narinfo arrives, in constant memory, and answers 500 if that read fails. The library gains the check's pure pieces in `NovaCache.Server` (`narUrlFileName`, `narClaim`, `judgeStoredNar`, `renderNarRefusal`).

## 0.11.1.1 - 2026-08-26

- **A symlink target with a separator serialises to its POSIX spelling on every platform.** Windows stores a symlink's reparse point with backslashes, so a target written `bin/tool` reads back `bin\tool`, and `getSymbolicLinkTarget` returned that verbatim into the NAR. NAR targets are host-independent byte strings in the POSIX spelling, so the archive of the same logical tree diverged between Windows and POSIX, and an unpack-then-recheck round-trip failed its on-disk hash comparison on Windows for any symlink whose target had more than one component. Both producers now normalise the separator to `/` when reading a target on Windows, where a backslash is only ever a separator and encodes as the single byte `0x5C` that no multi-byte UTF-8 sequence contains, so the byte-level replacement is exact. POSIX is untouched, where a backslash is a legitimate filename byte.
Expand Down
1 change: 0 additions & 1 deletion changelog.d/narinfo-requires-its-nar.md

This file was deleted.

1 change: 0 additions & 1 deletion changelog.d/reclaim-unreferenced-nars.md

This file was deleted.

2 changes: 1 addition & 1 deletion nova-cache.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ cabal-version: 3.4
-- 3.0 an in-package component name shadowed its external namesake
-- (the hazard the zstandard library's name dodges, documented there).
name: nova-cache
version: 0.11.1.1
version: 0.12.0.0
synopsis: Nix binary cache protocol: NAR, narinfo, signing and a WAI server
description:
The Nix binary cache protocol in Haskell: nix-base32, NAR serialization
Expand Down
Loading