Skip to content

libfetchers: Add an fh-resolve input scheme#577

Open
edolstra wants to merge 3 commits into
mainfrom
eelcodolstra/nix-451
Open

libfetchers: Add an fh-resolve input scheme#577
edolstra wants to merge 3 commits into
mainfrom
eelcodolstra/nix-451

Conversation

@edolstra

@edolstra edolstra commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Motivation

This adds a new fetcher fh-resolve that fetches trees from FlakeHub by resolving a flakehub org/project/version and fetching the store path of a flake output, similar to the fh resolve command. For example, the following flake input fetches the pre-built WASM plugins from the nix-wasm-rust flake:

{
  type = "fh-resolve";
  org = "DeterminateSystems";
  project = "nix-wasm-rust";
  version = "^0";
  output = "packages.x86_64-linux.default";
}

This is useful because it avoids having to evaluate the nix-wasm-rust flake, so it's a lot cheaper/faster and does not require exposing the source of the input flake.

Note that the resolved store paths are not allowed to have references, since libfetchers trees have no concept of references.

Context

Summary by CodeRabbit

  • New Features

    • Added support for resolving FlakeHub output references via the fh-resolve: input scheme.
    • Automatically resolves to substituted/prebuilt store paths and saves the resolved NAR hash for reuse.
    • Treats inputs as locked only when both storePath and narHash are available.
  • Bug Fixes

    • Strengthened validation to reliably detect expected vs resolved NAR hash mismatches, including when a storePath is already provided.
    • Improved behavior for inputs that specify a resolved storePath, ensuring consistent usage.

This fetcher resolves a flakeref like

  {
    type = "fh-resolve";
    org = "DeterminateSystems";
    project = "nix-wasm-rust";
    version = "^0";
    output = "packages.x86_64-linux.default";
  }

to a prebuilt store path by calling 'fh resolve', and substitutes that
path. This provides a convenient way for flakes to depend on prebuilt
binary artifacts (such as WASM plugins).

The lock file records the resolved store path and its NAR hash. Since
the store path is generally input-addressed, it cannot be recomputed
from the NAR hash, so Input::computeStorePath() now returns a recorded
'storePath' attribute directly. This makes the generic locked
fast-path (store reuse and substitution) operate on the resolved path,
so locked fetches never invoke 'fh'. Because a valid input-addressed
path does not imply the expected contents, the store accessor now
verifies the locked NAR hash against the path's actual NAR hash.

The resolved store paths are not allowed to have references, since
libfetchers trees have no concept of references.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds the fh-resolve fetcher scheme, which resolves FlakeHub output references through the API, records store paths and NAR hashes, and validates paths before creating accessors.

Changes

FlakeHub resolution

Layer / File(s) Summary
fh-resolve URL and input contract
src/libfetchers/fh-resolve.cc
Defines attributes, parses and serializes fh-resolve: URLs, and implements lock and fingerprint behavior.
Store path resolution and validation
src/libfetchers/fh-resolve.cc, src/libfetchers/fetchers.cc, src/libfetchers/include/nix/fetchers/fetchers.hh
Resolves paths through the FlakeHub API or reuses provided paths, checks NAR hashes and references, records resolved attributes, and creates store accessors.
Scheme registration and build integration
src/libfetchers/fh-resolve.cc, src/libfetchers/meson.build
Registers the scheme at startup and adds its source file to the nixfetchers build.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: cole-h

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant FhResolveInputScheme
  participant FileTransfer
  participant FlakeHubAPI
  participant NixStore
  Caller->>FhResolveInputScheme: Request accessor for output reference
  FhResolveInputScheme->>FileTransfer: Request output metadata
  FileTransfer->>FlakeHubAPI: Fetch output endpoint
  FlakeHubAPI-->>FileTransfer: Return store_path JSON
  FileTransfer-->>FhResolveInputScheme: Return store path
  FhResolveInputScheme->>NixStore: Query path info
  NixStore-->>FhResolveInputScheme: Return narHash and references
  FhResolveInputScheme-->>Caller: Return validated store accessor
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding the new fh-resolve input scheme.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch eelcodolstra/nix-451

Comment @coderabbitai help to get the list of available commands.

…Hash()

Assisted-by: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/libfetchers/fh-resolve.cc (1)

1-8: 📐 Maintainability & Code Quality | 🔵 Trivial

Run the formatter from the Nix dev shell
./maintainers/format.sh expects pre-commit; run it as nix develop -c ./maintainers/format.sh.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libfetchers/fh-resolve.cc` around lines 1 - 8, Run the repository
formatter from the Nix development shell using nix develop -c
./maintainers/format.sh so the required pre-commit dependency is available.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/libfetchers/fetchers.cc`:
- Around line 313-327: Preserve the no-references invariant on the locked-path
fast path by adding a scheme-specific validation hook and invoking it before the
generic accessor is created in src/libfetchers/fetchers.cc:313-327. Keep direct
recorded-path reuse conditional on successful validation in
src/libfetchers/fetchers.cc:445-449, and expose the fh-resolve no-reference
check from its scheme implementation in src/libfetchers/fh-resolve.cc:186-195 so
referenced paths are rejected even when their NAR hash matches.

---

Nitpick comments:
In `@src/libfetchers/fh-resolve.cc`:
- Around line 1-8: Run the repository formatter from the Nix development shell
using nix develop -c ./maintainers/format.sh so the required pre-commit
dependency is available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a9d25a4-8543-4988-b5bf-8f5bd17cf03f

📥 Commits

Reviewing files that changed from the base of the PR and between 31bd4f6 and 69bc292.

📒 Files selected for processing (3)
  • src/libfetchers/fetchers.cc
  • src/libfetchers/fh-resolve.cc
  • src/libfetchers/meson.build

Comment on lines +313 to +327
auto narHash = store.queryPathInfo(*storePath)->narHash;

/* If the store path is input-addressed (i.e. it comes from a
`storePath` attribute rather than being computed from the
NAR hash), its validity does not imply that it has the
expected contents, so verify the NAR hash. */
if (narHash != *getNarHash())
throw Error(
(unsigned int) 102,
"NAR hash mismatch in input '%s' at '%s', expected '%s' but got '%s'",
to_string(),
store.printStorePath(*storePath),
getNarHash()->to_string(HashFormat::SRI, true),
narHash.to_string(HashFormat::SRI, true));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve the no-references invariant on the locked-path fast path.

A final fh-resolve input with a recorded storePath bypasses FhResolveInputScheme::getAccessor(), so a referenced path with a matching NAR hash is accepted despite the PR’s required no-references invariant. Validate references before returning the generic accessor, preferably through a scheme validation hook.

  • src/libfetchers/fetchers.cc#L313-L327: invoke scheme-specific validation, or reject non-empty references for fh-resolve, before creating the accessor.
  • src/libfetchers/fetchers.cc#L445-L449: keep direct recorded-path reuse conditional on that validation.
  • src/libfetchers/fh-resolve.cc#L186-L195: expose the no-reference check so the generic locked-input path can apply it.
📍 Affects 2 files
  • src/libfetchers/fetchers.cc#L313-L327 (this comment)
  • src/libfetchers/fetchers.cc#L445-L449
  • src/libfetchers/fh-resolve.cc#L186-L195
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libfetchers/fetchers.cc` around lines 313 - 327, Preserve the
no-references invariant on the locked-path fast path by adding a scheme-specific
validation hook and invoking it before the generic accessor is created in
src/libfetchers/fetchers.cc:313-327. Keep direct recorded-path reuse conditional
on successful validation in src/libfetchers/fetchers.cc:445-449, and expose the
fh-resolve no-reference check from its scheme implementation in
src/libfetchers/fh-resolve.cc:186-195 so referenced paths are rejected even when
their NAR hash matches.

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

@github-actions
github-actions Bot temporarily deployed to pull request July 24, 2026 15:12 Inactive
…irectly

Instead of shelling out to 'fh resolve', do the FlakeHub JSON query
ourselves. Credentials for api.flakehub.com are picked up from the
user's netrc file, which the curl wrapper applies automatically.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/libfetchers/fh-resolve.cc (3)

64-73: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject empty required attributes.

Attribute-based inputs can bypass the URL checks and supply empty org, project, version, or output, producing malformed API requests. Enforce non-empty values here so URL and attribute inputs share the same contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libfetchers/fh-resolve.cc` around lines 64 - 73, Update inputFromAttrs to
validate that the required org, project, version, and output attributes returned
by getStrAttr are non-empty before constructing and returning Input; reject
invalid attribute-based inputs using the same failure behavior as the URL
parsing path.

28-31: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the scheme description to match the implementation.

This says the scheme uses fh resolve, but the implementation queries the FlakeHub API directly. The current wording can mislead users about the required dependency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libfetchers/fh-resolve.cc` around lines 28 - 31, Update the
schemeDescription() return text to describe direct FlakeHub API resolution
instead of invoking `fh resolve`, while preserving the documented FlakeHub
output reference format and avoiding any implication that the CLI dependency is
required.

76-83: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject url.authority here. fh-resolve://host/org/project/version#output still carries host in url.authority, but this code only reads the path and silently drops it. This scheme has no host component, so reject any non-empty authority instead of resolving a different input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libfetchers/fh-resolve.cc` around lines 76 - 83, Update inputFromURL to
reject URLs with a non-empty url.authority before processing pathSegments.
Preserve the existing scheme and path validation, and throw BadURL for any
authority so fh-resolve URLs cannot silently ignore a host component.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/libfetchers/fh-resolve.cc`:
- Line 169: Update the JSON parsing flow in the fetcher around the
nlohmann::json::parse call to catch parse_error, out_of_range, and type_error
exceptions, then rethrow the project’s fetcher-specific Error with descriptive
context that includes store_path and preserves the original exception details.
Keep the existing download and successful parsing behavior unchanged.

---

Outside diff comments:
In `@src/libfetchers/fh-resolve.cc`:
- Around line 64-73: Update inputFromAttrs to validate that the required org,
project, version, and output attributes returned by getStrAttr are non-empty
before constructing and returning Input; reject invalid attribute-based inputs
using the same failure behavior as the URL parsing path.
- Around line 28-31: Update the schemeDescription() return text to describe
direct FlakeHub API resolution instead of invoking `fh resolve`, while
preserving the documented FlakeHub output reference format and avoiding any
implication that the CLI dependency is required.
- Around line 76-83: Update inputFromURL to reject URLs with a non-empty
url.authority before processing pathSegments. Preserve the existing scheme and
path validation, and throw BadURL for any authority so fh-resolve URLs cannot
silently ignore a host component.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 224de90a-3138-4aa4-b65b-7cb5db409802

📥 Commits

Reviewing files that changed from the base of the PR and between 629b5d2 and f3db9e0.

📒 Files selected for processing (1)
  • src/libfetchers/fh-resolve.cc

percentEncode(getStrAttr(input.attrs, "output"))));
request.headers = {{"Accept", "application/json"}};

auto json = nlohmann::json::parse(getFileTransfer()->download(request).data);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant function and nearby context.
wc -l src/libfetchers/fh-resolve.cc
sed -n '130,220p' src/libfetchers/fh-resolve.cc

# Find other uses of download(request).data and store_path handling in this file.
rg -n 'store_path|download\(request\)\.data|nlohmann::json::parse|json\.at\(' src/libfetchers/fh-resolve.cc

Repository: DeterminateSystems/nix-src

Length of output: 3526


🏁 Script executed:

#!/bin/bash
set -euo pipefail

wc -l src/libfetchers/fh-resolve.cc
sed -n '130,220p' src/libfetchers/fh-resolve.cc
rg -n 'store_path|download\(request\)\.data|nlohmann::json::parse|json\.at\(' src/libfetchers/fh-resolve.cc

Repository: DeterminateSystems/nix-src

Length of output: 3526


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for exception wrapping around fetcher accessors and for similar JSON parsing patterns.
rg -n "try \{|catch \(.*json|catch \(const .*Error|nlohmann::json::parse|json\.at\(\"store_path\"\)" src lib

# Find the declaration/usage of getAccessorUnchecked and any generic fetcher wrappers.
rg -n "getAccessorUnchecked|getAccessor\(" src include

# Inspect nearby code in fetcher-related helpers if present.
fd -a ".*fetcher.*" src include

Repository: DeterminateSystems/nix-src

Length of output: 36041


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect fetcher exception handling and the accessor call path.
rg -n "getAccessorUnchecked|getAccessor\(|catch \(const nlohmann::json::|catch \(const Error &|catch \(const std::exception &|throw Error\(" src/libfetchers src/libstore src/libcmd src/nix

# Read the fetcher dispatch code that calls into input schemes.
sed -n '1,260p' src/libfetchers/fetchers.cc

# Inspect any nearby exception translation in fh-resolve or sibling fetchers.
sed -n '1,380p' src/libfetchers/git-lfs-fetch.cc

Repository: DeterminateSystems/nix-src

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '285,420p' src/libfetchers/fetchers.cc
printf '\n--- git-lfs-fetch ---\n'
sed -n '220,340p' src/libfetchers/git-lfs-fetch.cc
printf '\n--- github ---\n'
sed -n '430,560p' src/libfetchers/github.cc

Repository: DeterminateSystems/nix-src

Length of output: 16428


Wrap the JSON parse in fetcher-specific errors. A malformed or schema-changed response can throw nlohmann::json::parse_error, out_of_range, or type_error here and skip the usual Error context; catch those and rethrow a descriptive fetcher error around store_path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libfetchers/fh-resolve.cc` at line 169, Update the JSON parsing flow in
the fetcher around the nlohmann::json::parse call to catch parse_error,
out_of_range, and type_error exceptions, then rethrow the project’s
fetcher-specific Error with descriptive context that includes store_path and
preserves the original exception details. Keep the existing download and
successful parsing behavior unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant