oxalica / nil

NIx Language server, an incremental analysis assistant for writing in Nix.
Apache License 2.0
1.28k stars 39 forks source link

Always fails to fetch non-flake file inputs #113

Closed MathiasSven closed 9 months ago

MathiasSven commented 9 months ago

Consider this flake:

{
  inputs.bar = {
    url = "https://gist.githubusercontent.com/MathiasSven/44045168334c4601c4cae21d60d71491/raw/b6d47272b67d0d13bf495ba03b3a987bdf962986/gistfile1.txt";
    flake = false;
  };

  outputs = { bar, ... }: { inherit bar; foo = import bar; };
}

Here the gist is just a placeholder for a file, any file would suffice for the example.

Every time you enter a flake.nix with a non-flake file input such as this one, the LSP will ask you:

Some flake inputs are not available. Fetch them now?
You can enable autoArchive in lsp configuration.
(1)Fetch, (2)Ignore missing ones:

If you try to fetch it, it will give you this warning:

[coc.nvim] Failed to archiving flake: command succeeded but some paths are still missing: [("bar", Path("/nix/store/n1gjjsb5bwdy96gzp1fj78jhh2dxh5j1-source"))]

# Notice it says "/nix/store/n1gjjsb5bwdy96gzp1fj78jhh2dxh5j1-source"

And there is no way to just permanently ignore that input specifically, so the message/error will always appear.

Partial expiation of the issue, if anyone can fill in the gap I would appreciate it as I wasn't able to figure out why:

The problem here is that nil uses a (derivation { ... ; outputHash = HASH }).outPath in order to determine where to check for the input source path, however in this case this doesn't work, observe:

nix eval .#bar.narHash
"sha256-MrqkB8frUDtvEopnDXs8/t0nZxgqYtJkTZeAQ8B7RVM="
# This is the hash that is present in the flake.lock file, and the one nil fills inside the outputHash to get the path

nil eval .#bar.outPath
"/nix/store/dr9hbdf6mdbvbq83napciyfir2xk1m24-source"
# This is where the file is actually stored, not where nil thought it would be, ie: "/nix/store/n1gjjsb5bwdy96gzp1fj78jhh2dxh5j1-source" as shown above

There seems to be some indirection with how either the path or the hash get computed for a file input in flakes. Changing the strategy does not yield the correct path:

nix eval --expr '(derivation { 
  name = "source"; 
  system = "dummy"; 
  builder = "dummy"; 
  outputHashMode = "recursive"; 
  outputHashAlgo = null;
  outputHash = "sha256-MrqkB8frUDtvEopnDXs8/t0nZxgqYtJkTZeAQ8B7RVM="; }).outPath'
# "/nix/store/n1gjjsb5bwdy96gzp1fj78jhh2dxh5j1-source"
# This is how nil gets to the incorrect path

nix eval --expr '(derivation { 
  name = "source"; 
  system = "dummy"; 
  builder = "dummy"; 
  outputHashMode = "flat"; 
  outputHashAlgo = null;
  outputHash = "sha256-MrqkB8frUDtvEopnDXs8/t0nZxgqYtJkTZeAQ8B7RVM="; }).outPath'
# "/nix/store/lsxl020awachf1zjl1k0si4vzvnmhhkh-source"
# Using "flat" does not give the right path either

I think the main problem stems from this issue:

url="https://gist.githubusercontent.com/MathiasSven/44045168334c4601c4cae21d60d71491/raw/b6d47272b67d0d13bf495ba03b3a987bdf962986/gistfile1.txt"

curl $url > source.str

nix hash path source.str
# sha256-MrqkB8frUDtvEopnDXs8/t0nZxgqYtJkTZeAQ8B7RVM=
## The true hash that you see in flake.lock

nix hash file source.str
# sha256-KwOAjXZIRfei7ZuJhSao5Ui5rxJqHRYUQ7M8LJvxOF4=

nix eval --expr '(derivation { 
  name = "source"; 
  system = "dummy"; 
  builder = "dummy"; 
  outputHashMode = "flat"; 
  outputHashAlgo = null;
  outputHash = "sha256-KwOAjXZIRfei7ZuJhSao5Ui5rxJqHRYUQ7M8LJvxOF4="; }).outPath'
# "/nix/store/dr9hbdf6mdbvbq83napciyfir2xk1m24-source"

The correct path! So the hash that the flake.lock stores is the result of doing a nix hash file on the content, but the location in which the file is stored is done from the hash you would get out of doing nix hash path. nix-prefetch-url also imitates this behaviour:

nix-prefetch-url $url --print-path --name source
# 0piqy6djqg5k8ca1c7ba2apvjj75m0k8b2cvxnigfia8fs6q00rb
# /nix/store/dr9hbdf6mdbvbq83napciyfir2xk1m24-source

nix hash to-sri --type sha256 0piqy6djqg5k8ca1c7ba2apvjj75m0k8b2cvxnigfia8fs6q00rb
# sha256-KwOAjXZIRfei7ZuJhSao5Ui5rxJqHRYUQ7M8LJvxOF4=
## The hash that leads to the correct path, but not the one shown in the flake.lock

Anyway, I have little experience with rust, and I am not sure if the .as_ref().unwrap() bellow will result in exceptions in some cases, but this is the patch I have added to my installation of nil, from the little testing I have done, there seems to be no issues so far.

diff --git a/crates/nix-interop/src/flake_lock.rs b/crates/nix-interop/src/flake_lock.rs
index ccadd4d..48e032f 100644
--- a/crates/nix-interop/src/flake_lock.rs
+++ b/crates/nix-interop/src/flake_lock.rs
@@ -38,7 +38,7 @@ pub async fn resolve_flake_locked_inputs(

     // Ignore the root node which is unlocked. This happens in cycle flake inputs.
     // TODO: Should come up a way to correctly handle it in database.
-    inputs.retain(|&(_, node)| !std::ptr::eq(node, root_node));
+    inputs.retain(|&(_, node)| !std::ptr::eq(node, root_node) && !node.locked.as_ref().unwrap()._type.eq("file"));

     let hashes = inputs
         .iter()
@@ -184,6 +184,7 @@ enum FlakeInput {
 #[serde(rename_all = "camelCase")]
 struct LockedFlakeRef {
     nar_hash: String,
+    _type: String,
     // ...
 }
oxalica commented 9 months ago

So the hash that the flake.lock stores is the result of doing a nix hash file on the content, but the location in which the file is stored is done from the hash you would get out of doing nix hash path. nix-prefetch-url also imitates this behaviour:

This is unfortunately. So the store path is a plain content hash instead of a tree hash (nar hash). But since only narHash is stored in flake.lock, there's no way to recover the plain hash and store path before its existence. I don't think there's even a way for Nix itself to get the store path given only the flake.lock without a full database scan: the hash (narHash) column of table ValidPath is not even indexed. I'll check the source code of Nix for what's going on.