diff --git a/doc/manual/expressions/builtins.xml b/doc/manual/expressions/builtins.xml index f39b393bf719..0032ba148e3e 100644 --- a/doc/manual/expressions/builtins.xml +++ b/doc/manual/expressions/builtins.xml @@ -405,6 +405,20 @@ stdenv.mkDerivation { … } + + hash + + + The SRI hash + of the output path. Please note that this can only + be used in conjunction with rev or ref. + + + The hash can be generated by running + nix to-sri --type sha256 $(nix-prefetch-url git://github.com/owner/repo --rev abcdef). + + + ref @@ -504,6 +518,28 @@ stdenv.mkDerivation { … } builtins.fetchGit { url = "ssh://git@github.com/nixos/nix.git"; ref = "master"; +} + + + + Fetching a given revision from a repo and validate its hash + + builtins.fetchGit can create a content-addressable + path to ensure that nothing changes. + + builtins.fetchGit { + url = "git://github.com/NixOS/nix"; + rev = "d1db7fa9528154284de55d37e5186ac06fcbffc9"; + hash = "sha256-B5yIPHhEm0eysJKEsO7nqxprh9vcblFxpJG11gXJus1="; +} + + + + Fetching a given tag from a repo and validate its hash + builtins.fetchGit { + url = "git://github.com/NixOS/nix"; + ref = "refs/tags/2.2.2"; + hash = "sha256-wGVMnUCo8Ms0EdXNlTEAvTBnD/CwOwoAlNCGEijOTJc="; } diff --git a/src/libexpr/primops/fetchGit.cc b/src/libexpr/primops/fetchGit.cc index 7ef3b3823983..feaa94f9f223 100644 --- a/src/libexpr/primops/fetchGit.cc +++ b/src/libexpr/primops/fetchGit.cc @@ -27,11 +27,28 @@ std::regex revRegex("^[0-9a-fA-F]{40}$"); GitInfo exportGit(ref store, const std::string & uri, std::optional ref, std::string rev, - const std::string & name) + const std::string & name, std::optional hash) { if (evalSettings.pureEval && rev == "") throw Error("in pure evaluation mode, 'fetchGit' requires a Git revision"); + Path expectedStorePath; + GitInfo gitInfo; + if (hash) { + // Disallow `hash` usage if no rev/ref is given. + if (rev == "" && !ref) { + throw Error("Cannot create a fixed-output derivation for a git repo without a git revision or ref"); + } + + expectedStorePath = store->makeFixedOutputPath(true, *hash, name); + if (store->isValidPath(expectedStorePath)) { + gitInfo.storePath = expectedStorePath; + gitInfo.rev = rev; + gitInfo.shortRev = std::string(gitInfo.rev, 0, 7); + return gitInfo; + } + } + if (!ref && rev == "" && hasPrefix(uri, "/") && pathExists(uri + "/.git")) { bool clean = true; @@ -138,7 +155,6 @@ GitInfo exportGit(ref store, const std::string & uri, } // FIXME: check whether rev is an ancestor of ref. - GitInfo gitInfo; gitInfo.rev = rev != "" ? rev : chomp(readFile(localRefFile)); gitInfo.shortRev = std::string(gitInfo.rev, 0, 7); @@ -148,20 +164,25 @@ GitInfo exportGit(ref store, const std::string & uri, Path storeLink = cacheDir + "/" + storeLinkName + ".link"; PathLocks storeLinkLock({storeLink}, fmt("waiting for lock on '%1%'...", storeLink)); // FIXME: broken - try { - auto json = nlohmann::json::parse(readFile(storeLink)); + // Only return cached git repo if no hash is set. If a hash exists, + // the proper store path would've been returned earlier, otherwise the hash + // needs to be validated. + if (!hash) { + try { + auto json = nlohmann::json::parse(readFile(storeLink)); - assert(json["name"] == name && json["rev"] == gitInfo.rev); + assert(json["name"] == name && json["rev"] == gitInfo.rev); - gitInfo.storePath = json["storePath"]; + gitInfo.storePath = json["storePath"]; - if (store->isValidPath(gitInfo.storePath)) { - gitInfo.revCount = json["revCount"]; - return gitInfo; - } + if (store->isValidPath(gitInfo.storePath)) { + gitInfo.revCount = json["revCount"]; + return gitInfo; + } - } catch (SysError & e) { - if (e.errNo != ENOENT) throw; + } catch (SysError & e) { + if (e.errNo != ENOENT) throw; + } } // FIXME: should pipe this, or find some better way to extract a @@ -173,7 +194,21 @@ GitInfo exportGit(ref store, const std::string & uri, runProgram("tar", true, { "x", "-C", tmpDir }, tar); - gitInfo.storePath = store->addToStore(name, tmpDir); + auto storePath = store->addToStore(name, tmpDir); + if (expectedStorePath != "" && storePath != expectedStorePath) { + unsigned int status = resUntrustedPath; + Hash got = hashPath(hash->type, store->toRealPath(storePath)).first; + throw Error( + status, + "hash mismatch in git repo (%s, rev %s):\n wanted: %s\n got: %s", + uri, + gitInfo.rev, + hash->to_string(SRI, true), + got.to_string(SRI, true) + ); + } + + gitInfo.storePath = storePath; gitInfo.revCount = std::stoull(runProgram("git", true, { "-C", cacheDir, "rev-list", "--count", gitInfo.rev })); @@ -194,6 +229,7 @@ static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Va std::string url; std::optional ref; std::string rev; + std::optional hash; std::string name = "source"; PathSet context; @@ -213,6 +249,8 @@ static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Va rev = state.forceStringNoCtx(*attr.value, *attr.pos); else if (n == "name") name = state.forceStringNoCtx(*attr.value, *attr.pos); + else if (n == "hash") + hash = Hash(state.forceStringNoCtx(*attr.value, *attr.pos), htUnknown); else throw EvalError("unsupported argument '%s' to 'fetchGit', at %s", attr.name, *attr.pos); } @@ -227,7 +265,7 @@ static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Va // whitelist. Ah well. state.checkURI(url); - auto gitInfo = exportGit(state.store, url, ref, rev, name); + auto gitInfo = exportGit(state.store, url, ref, rev, name, hash); state.mkAttrs(v, 8); mkString(*state.allocAttr(v, state.sOutPath), gitInfo.storePath, PathSet({gitInfo.storePath})); diff --git a/tests/fetchGit.sh b/tests/fetchGit.sh index 4c46bdf0465b..fc4a628af578 100644 --- a/tests/fetchGit.sh +++ b/tests/fetchGit.sh @@ -88,6 +88,12 @@ path3=$(nix eval --raw "(builtins.fetchGit { url = $repo; ref = \"master\"; }).o path3=$(nix eval --raw "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; }).outPath") [[ $path = $path3 ]] +path4=$(nix eval --raw "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; hash = \"sha256-Hr8g6AqANb3xqX28eu1XnjK/3ab8Gv6TJSnkb1LezG8=\"; }).outPath") +[[ $path = $path4 ]] + +nix eval --raw "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; hash = \"sha256-B5yIPHhEm0eysJKEsO7nqxprh9vcblFxpJG11gXJus1=\"; }).outPath" || status=$? +[[ "$status" = "102" ]] + # Committing should not affect the store path. git -C $repo commit -m 'Bla3' -a