mirror of
https://github.com/privatevoid-net/nix-super.git
synced 2024-11-22 05:56:15 +02:00
Backport libfetchers from the flakes branch
This provides a pluggable mechanism for defining new fetchers. It adds
a builtin function 'fetchTree' that generalizes existing fetchers like
'fetchGit', 'fetchMercurial' and 'fetchTarball'. 'fetchTree' takes a
set of attributes, e.g.
fetchTree {
type = "git";
url = "https://example.org/repo.git";
ref = "some-branch";
rev = "abcdef...";
}
The existing fetchers are just wrappers around this. Note that the
input attributes to fetchTree are the same as flake input
specifications and flake lock file entries.
All fetchers share a common cache stored in
~/.cache/nix/fetcher-cache-v1.sqlite. This replaces the ad hoc caching
mechanisms in fetchGit and download.cc (e.g. ~/.cache/nix/{tarballs,git-revs*}).
This also adds support for Git worktrees (c169ea5904
).
This commit is contained in:
parent
ebb20a5356
commit
462421d345
36 changed files with 2199 additions and 647 deletions
1
Makefile
1
Makefile
|
@ -4,6 +4,7 @@ makefiles = \
|
||||||
nix-rust/local.mk \
|
nix-rust/local.mk \
|
||||||
src/libutil/local.mk \
|
src/libutil/local.mk \
|
||||||
src/libstore/local.mk \
|
src/libstore/local.mk \
|
||||||
|
src/libfetchers/local.mk \
|
||||||
src/libmain/local.mk \
|
src/libmain/local.mk \
|
||||||
src/libexpr/local.mk \
|
src/libexpr/local.mk \
|
||||||
src/nix/local.mk \
|
src/nix/local.mk \
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
#include "download.hh"
|
#include "download.hh"
|
||||||
#include "util.hh"
|
#include "util.hh"
|
||||||
#include "eval.hh"
|
#include "eval.hh"
|
||||||
|
#include "fetchers.hh"
|
||||||
|
#include "store-api.hh"
|
||||||
|
|
||||||
namespace nix {
|
namespace nix {
|
||||||
|
|
||||||
|
@ -46,9 +48,9 @@ Bindings * MixEvalArgs::getAutoArgs(EvalState & state)
|
||||||
Path lookupFileArg(EvalState & state, string s)
|
Path lookupFileArg(EvalState & state, string s)
|
||||||
{
|
{
|
||||||
if (isUri(s)) {
|
if (isUri(s)) {
|
||||||
CachedDownloadRequest request(s);
|
return state.store->toRealPath(
|
||||||
request.unpack = true;
|
fetchers::downloadTarball(
|
||||||
return getDownloader()->downloadCached(state.store, request).path;
|
state.store, resolveUri(s), "source", false).storePath);
|
||||||
} else if (s.size() > 2 && s.at(0) == '<' && s.at(s.size() - 1) == '>') {
|
} else if (s.size() > 2 && s.at(0) == '<' && s.at(s.size() - 1) == '>') {
|
||||||
Path p = s.substr(1, s.size() - 2);
|
Path p = s.substr(1, s.size() - 2);
|
||||||
return state.findFile(p);
|
return state.findFile(p);
|
||||||
|
|
|
@ -6,9 +6,9 @@ libexpr_DIR := $(d)
|
||||||
|
|
||||||
libexpr_SOURCES := $(wildcard $(d)/*.cc) $(wildcard $(d)/primops/*.cc) $(d)/lexer-tab.cc $(d)/parser-tab.cc
|
libexpr_SOURCES := $(wildcard $(d)/*.cc) $(wildcard $(d)/primops/*.cc) $(d)/lexer-tab.cc $(d)/parser-tab.cc
|
||||||
|
|
||||||
libexpr_CXXFLAGS += -I src/libutil -I src/libstore -I src/libmain -I src/libexpr
|
libexpr_CXXFLAGS += -I src/libutil -I src/libstore -I src/libfetchers -I src/libmain -I src/libexpr
|
||||||
|
|
||||||
libexpr_LIBS = libutil libstore libnixrust
|
libexpr_LIBS = libutil libstore libfetchers libnixrust
|
||||||
|
|
||||||
libexpr_LDFLAGS =
|
libexpr_LDFLAGS =
|
||||||
ifneq ($(OS), FreeBSD)
|
ifneq ($(OS), FreeBSD)
|
||||||
|
|
|
@ -545,6 +545,7 @@ formal
|
||||||
|
|
||||||
#include "eval.hh"
|
#include "eval.hh"
|
||||||
#include "download.hh"
|
#include "download.hh"
|
||||||
|
#include "fetchers.hh"
|
||||||
#include "store-api.hh"
|
#include "store-api.hh"
|
||||||
|
|
||||||
|
|
||||||
|
@ -687,9 +688,8 @@ std::pair<bool, std::string> EvalState::resolveSearchPathElem(const SearchPathEl
|
||||||
|
|
||||||
if (isUri(elem.second)) {
|
if (isUri(elem.second)) {
|
||||||
try {
|
try {
|
||||||
CachedDownloadRequest request(elem.second);
|
res = { true, store->toRealPath(fetchers::downloadTarball(
|
||||||
request.unpack = true;
|
store, resolveUri(elem.second), "source", false).storePath) };
|
||||||
res = { true, getDownloader()->downloadCached(store, request).path };
|
|
||||||
} catch (DownloadError & e) {
|
} catch (DownloadError & e) {
|
||||||
printError(format("warning: Nix search path entry '%1%' cannot be downloaded, ignoring") % elem.second);
|
printError(format("warning: Nix search path entry '%1%' cannot be downloaded, ignoring") % elem.second);
|
||||||
res = { false, "" };
|
res = { false, "" };
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#include "archive.hh"
|
#include "archive.hh"
|
||||||
#include "derivations.hh"
|
#include "derivations.hh"
|
||||||
#include "download.hh"
|
|
||||||
#include "eval-inline.hh"
|
#include "eval-inline.hh"
|
||||||
#include "eval.hh"
|
#include "eval.hh"
|
||||||
#include "globals.hh"
|
#include "globals.hh"
|
||||||
|
@ -2045,68 +2044,6 @@ static void prim_splitVersion(EvalState & state, const Pos & pos, Value * * args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*************************************************************
|
|
||||||
* Networking
|
|
||||||
*************************************************************/
|
|
||||||
|
|
||||||
|
|
||||||
void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v,
|
|
||||||
const string & who, bool unpack, const std::string & defaultName)
|
|
||||||
{
|
|
||||||
CachedDownloadRequest request("");
|
|
||||||
request.unpack = unpack;
|
|
||||||
request.name = defaultName;
|
|
||||||
|
|
||||||
state.forceValue(*args[0]);
|
|
||||||
|
|
||||||
if (args[0]->type == tAttrs) {
|
|
||||||
|
|
||||||
state.forceAttrs(*args[0], pos);
|
|
||||||
|
|
||||||
for (auto & attr : *args[0]->attrs) {
|
|
||||||
string n(attr.name);
|
|
||||||
if (n == "url")
|
|
||||||
request.uri = state.forceStringNoCtx(*attr.value, *attr.pos);
|
|
||||||
else if (n == "sha256")
|
|
||||||
request.expectedHash = Hash(state.forceStringNoCtx(*attr.value, *attr.pos), htSHA256);
|
|
||||||
else if (n == "name")
|
|
||||||
request.name = state.forceStringNoCtx(*attr.value, *attr.pos);
|
|
||||||
else
|
|
||||||
throw EvalError(format("unsupported argument '%1%' to '%2%', at %3%") % attr.name % who % attr.pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.uri.empty())
|
|
||||||
throw EvalError(format("'url' argument required, at %1%") % pos);
|
|
||||||
|
|
||||||
} else
|
|
||||||
request.uri = state.forceStringNoCtx(*args[0], pos);
|
|
||||||
|
|
||||||
state.checkURI(request.uri);
|
|
||||||
|
|
||||||
if (evalSettings.pureEval && !request.expectedHash)
|
|
||||||
throw Error("in pure evaluation mode, '%s' requires a 'sha256' argument", who);
|
|
||||||
|
|
||||||
auto res = getDownloader()->downloadCached(state.store, request);
|
|
||||||
|
|
||||||
if (state.allowedPaths)
|
|
||||||
state.allowedPaths->insert(res.path);
|
|
||||||
|
|
||||||
mkString(v, res.storePath, PathSet({res.storePath}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void prim_fetchurl(EvalState & state, const Pos & pos, Value * * args, Value & v)
|
|
||||||
{
|
|
||||||
fetch(state, pos, args, v, "fetchurl", false, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void prim_fetchTarball(EvalState & state, const Pos & pos, Value * * args, Value & v)
|
|
||||||
{
|
|
||||||
fetch(state, pos, args, v, "fetchTarball", true, "source");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*************************************************************
|
/*************************************************************
|
||||||
* Primop registration
|
* Primop registration
|
||||||
*************************************************************/
|
*************************************************************/
|
||||||
|
@ -2289,10 +2226,6 @@ void EvalState::createBaseEnv()
|
||||||
addPrimOp("derivationStrict", 1, prim_derivationStrict);
|
addPrimOp("derivationStrict", 1, prim_derivationStrict);
|
||||||
addPrimOp("placeholder", 1, prim_placeholder);
|
addPrimOp("placeholder", 1, prim_placeholder);
|
||||||
|
|
||||||
// Networking
|
|
||||||
addPrimOp("__fetchurl", 1, prim_fetchurl);
|
|
||||||
addPrimOp("fetchTarball", 1, prim_fetchTarball);
|
|
||||||
|
|
||||||
/* Add a wrapper around the derivation primop that computes the
|
/* Add a wrapper around the derivation primop that computes the
|
||||||
`drvPath' and `outPath' attributes lazily. */
|
`drvPath' and `outPath' attributes lazily. */
|
||||||
string path = canonPath(settings.nixDataDir + "/nix/corepkgs/derivation.nix", true);
|
string path = canonPath(settings.nixDataDir + "/nix/corepkgs/derivation.nix", true);
|
||||||
|
|
|
@ -20,6 +20,7 @@ struct RegisterPrimOp
|
||||||
them. */
|
them. */
|
||||||
/* Load a ValueInitializer from a DSO and return whatever it initializes */
|
/* Load a ValueInitializer from a DSO and return whatever it initializes */
|
||||||
void prim_importNative(EvalState & state, const Pos & pos, Value * * args, Value & v);
|
void prim_importNative(EvalState & state, const Pos & pos, Value * * args, Value & v);
|
||||||
|
|
||||||
/* Execute a program and parse its output */
|
/* Execute a program and parse its output */
|
||||||
void prim_exec(EvalState & state, const Pos & pos, Value * * args, Value & v);
|
void prim_exec(EvalState & state, const Pos & pos, Value * * args, Value & v);
|
||||||
|
|
||||||
|
|
|
@ -1,202 +1,17 @@
|
||||||
#include "primops.hh"
|
#include "primops.hh"
|
||||||
#include "eval-inline.hh"
|
#include "eval-inline.hh"
|
||||||
#include "download.hh"
|
|
||||||
#include "store-api.hh"
|
#include "store-api.hh"
|
||||||
#include "pathlocks.hh"
|
|
||||||
#include "hash.hh"
|
#include "hash.hh"
|
||||||
#include "tarfile.hh"
|
#include "fetchers.hh"
|
||||||
|
#include "url.hh"
|
||||||
#include <sys/time.h>
|
|
||||||
|
|
||||||
#include <regex>
|
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
|
||||||
|
|
||||||
using namespace std::string_literals;
|
|
||||||
|
|
||||||
namespace nix {
|
namespace nix {
|
||||||
|
|
||||||
struct GitInfo
|
|
||||||
{
|
|
||||||
Path storePath;
|
|
||||||
std::string rev;
|
|
||||||
std::string shortRev;
|
|
||||||
uint64_t revCount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::regex revRegex("^[0-9a-fA-F]{40}$");
|
|
||||||
|
|
||||||
GitInfo exportGit(ref<Store> store, const std::string & uri,
|
|
||||||
std::optional<std::string> ref, std::string rev,
|
|
||||||
const std::string & name)
|
|
||||||
{
|
|
||||||
if (evalSettings.pureEval && rev == "")
|
|
||||||
throw Error("in pure evaluation mode, 'fetchGit' requires a Git revision");
|
|
||||||
|
|
||||||
if (!ref && rev == "" && hasPrefix(uri, "/") && pathExists(uri + "/.git")) {
|
|
||||||
|
|
||||||
bool clean = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
runProgram("git", true, { "-C", uri, "diff-index", "--quiet", "HEAD", "--" });
|
|
||||||
} catch (ExecError & e) {
|
|
||||||
if (!WIFEXITED(e.status) || WEXITSTATUS(e.status) != 1) throw;
|
|
||||||
clean = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!clean) {
|
|
||||||
|
|
||||||
/* This is an unclean working tree. So copy all tracked files. */
|
|
||||||
GitInfo gitInfo;
|
|
||||||
gitInfo.rev = "0000000000000000000000000000000000000000";
|
|
||||||
gitInfo.shortRev = std::string(gitInfo.rev, 0, 7);
|
|
||||||
|
|
||||||
auto files = tokenizeString<std::set<std::string>>(
|
|
||||||
runProgram("git", true, { "-C", uri, "ls-files", "-z" }), "\0"s);
|
|
||||||
|
|
||||||
PathFilter filter = [&](const Path & p) -> bool {
|
|
||||||
assert(hasPrefix(p, uri));
|
|
||||||
std::string file(p, uri.size() + 1);
|
|
||||||
|
|
||||||
auto st = lstat(p);
|
|
||||||
|
|
||||||
if (S_ISDIR(st.st_mode)) {
|
|
||||||
auto prefix = file + "/";
|
|
||||||
auto i = files.lower_bound(prefix);
|
|
||||||
return i != files.end() && hasPrefix(*i, prefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
return files.count(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
gitInfo.storePath = store->printStorePath(store->addToStore("source", uri, true, htSHA256, filter));
|
|
||||||
|
|
||||||
return gitInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
// clean working tree, but no ref or rev specified. Use 'HEAD'.
|
|
||||||
rev = chomp(runProgram("git", true, { "-C", uri, "rev-parse", "HEAD" }));
|
|
||||||
ref = "HEAD"s;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ref) ref = "HEAD"s;
|
|
||||||
|
|
||||||
if (rev != "" && !std::regex_match(rev, revRegex))
|
|
||||||
throw Error("invalid Git revision '%s'", rev);
|
|
||||||
|
|
||||||
deletePath(getCacheDir() + "/nix/git");
|
|
||||||
|
|
||||||
Path cacheDir = getCacheDir() + "/nix/gitv2/" + hashString(htSHA256, uri).to_string(Base32, false);
|
|
||||||
|
|
||||||
if (!pathExists(cacheDir)) {
|
|
||||||
createDirs(dirOf(cacheDir));
|
|
||||||
runProgram("git", true, { "init", "--bare", cacheDir });
|
|
||||||
}
|
|
||||||
|
|
||||||
Path localRefFile;
|
|
||||||
if (ref->compare(0, 5, "refs/") == 0)
|
|
||||||
localRefFile = cacheDir + "/" + *ref;
|
|
||||||
else
|
|
||||||
localRefFile = cacheDir + "/refs/heads/" + *ref;
|
|
||||||
|
|
||||||
bool doFetch;
|
|
||||||
time_t now = time(0);
|
|
||||||
/* If a rev was specified, we need to fetch if it's not in the
|
|
||||||
repo. */
|
|
||||||
if (rev != "") {
|
|
||||||
try {
|
|
||||||
runProgram("git", true, { "-C", cacheDir, "cat-file", "-e", rev });
|
|
||||||
doFetch = false;
|
|
||||||
} catch (ExecError & e) {
|
|
||||||
if (WIFEXITED(e.status)) {
|
|
||||||
doFetch = true;
|
|
||||||
} else {
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/* If the local ref is older than ‘tarball-ttl’ seconds, do a
|
|
||||||
git fetch to update the local ref to the remote ref. */
|
|
||||||
struct stat st;
|
|
||||||
doFetch = stat(localRefFile.c_str(), &st) != 0 ||
|
|
||||||
(uint64_t) st.st_mtime + settings.tarballTtl <= (uint64_t) now;
|
|
||||||
}
|
|
||||||
if (doFetch)
|
|
||||||
{
|
|
||||||
Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching Git repository '%s'", uri));
|
|
||||||
|
|
||||||
// FIXME: git stderr messes up our progress indicator, so
|
|
||||||
// we're using --quiet for now. Should process its stderr.
|
|
||||||
runProgram("git", true, { "-C", cacheDir, "fetch", "--quiet", "--force", "--", uri, fmt("%s:%s", *ref, *ref) });
|
|
||||||
|
|
||||||
struct timeval times[2];
|
|
||||||
times[0].tv_sec = now;
|
|
||||||
times[0].tv_usec = 0;
|
|
||||||
times[1].tv_sec = now;
|
|
||||||
times[1].tv_usec = 0;
|
|
||||||
|
|
||||||
utimes(localRefFile.c_str(), times);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
printTalkative("using revision %s of repo '%s'", gitInfo.rev, uri);
|
|
||||||
|
|
||||||
std::string storeLinkName = hashString(htSHA512, name + std::string("\0"s) + gitInfo.rev).to_string(Base32, false);
|
|
||||||
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));
|
|
||||||
|
|
||||||
assert(json["name"] == name && json["rev"] == gitInfo.rev);
|
|
||||||
|
|
||||||
gitInfo.storePath = json["storePath"];
|
|
||||||
|
|
||||||
if (store->isValidPath(store->parseStorePath(gitInfo.storePath))) {
|
|
||||||
gitInfo.revCount = json["revCount"];
|
|
||||||
return gitInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (SysError & e) {
|
|
||||||
if (e.errNo != ENOENT) throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto source = sinkToSource([&](Sink & sink) {
|
|
||||||
RunOptions gitOptions("git", { "-C", cacheDir, "archive", gitInfo.rev });
|
|
||||||
gitOptions.standardOut = &sink;
|
|
||||||
runProgram2(gitOptions);
|
|
||||||
});
|
|
||||||
|
|
||||||
Path tmpDir = createTempDir();
|
|
||||||
AutoDelete delTmpDir(tmpDir, true);
|
|
||||||
|
|
||||||
unpackTarfile(*source, tmpDir);
|
|
||||||
|
|
||||||
gitInfo.storePath = store->printStorePath(store->addToStore(name, tmpDir));
|
|
||||||
|
|
||||||
gitInfo.revCount = std::stoull(runProgram("git", true, { "-C", cacheDir, "rev-list", "--count", gitInfo.rev }));
|
|
||||||
|
|
||||||
nlohmann::json json;
|
|
||||||
json["storePath"] = gitInfo.storePath;
|
|
||||||
json["uri"] = uri;
|
|
||||||
json["name"] = name;
|
|
||||||
json["rev"] = gitInfo.rev;
|
|
||||||
json["revCount"] = gitInfo.revCount;
|
|
||||||
|
|
||||||
writeFile(storeLink, json.dump());
|
|
||||||
|
|
||||||
return gitInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Value & v)
|
static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Value & v)
|
||||||
{
|
{
|
||||||
std::string url;
|
std::string url;
|
||||||
std::optional<std::string> ref;
|
std::optional<std::string> ref;
|
||||||
std::string rev;
|
std::optional<Hash> rev;
|
||||||
std::string name = "source";
|
std::string name = "source";
|
||||||
PathSet context;
|
PathSet context;
|
||||||
|
|
||||||
|
@ -213,7 +28,7 @@ static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Va
|
||||||
else if (n == "ref")
|
else if (n == "ref")
|
||||||
ref = state.forceStringNoCtx(*attr.value, *attr.pos);
|
ref = state.forceStringNoCtx(*attr.value, *attr.pos);
|
||||||
else if (n == "rev")
|
else if (n == "rev")
|
||||||
rev = state.forceStringNoCtx(*attr.value, *attr.pos);
|
rev = Hash(state.forceStringNoCtx(*attr.value, *attr.pos), htSHA1);
|
||||||
else if (n == "name")
|
else if (n == "name")
|
||||||
name = state.forceStringNoCtx(*attr.value, *attr.pos);
|
name = state.forceStringNoCtx(*attr.value, *attr.pos);
|
||||||
else
|
else
|
||||||
|
@ -230,17 +45,35 @@ static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Va
|
||||||
// whitelist. Ah well.
|
// whitelist. Ah well.
|
||||||
state.checkURI(url);
|
state.checkURI(url);
|
||||||
|
|
||||||
auto gitInfo = exportGit(state.store, url, ref, rev, name);
|
if (evalSettings.pureEval && !rev)
|
||||||
|
throw Error("in pure evaluation mode, 'fetchGit' requires a Git revision");
|
||||||
|
|
||||||
|
auto parsedUrl = parseURL(
|
||||||
|
url.find("://") != std::string::npos
|
||||||
|
? "git+" + url
|
||||||
|
: "git+file://" + url);
|
||||||
|
if (ref) parsedUrl.query.insert_or_assign("ref", *ref);
|
||||||
|
if (rev) parsedUrl.query.insert_or_assign("rev", rev->gitRev());
|
||||||
|
// FIXME: use name
|
||||||
|
auto input = fetchers::inputFromURL(parsedUrl);
|
||||||
|
|
||||||
|
auto [tree, input2] = input->fetchTree(state.store);
|
||||||
|
|
||||||
state.mkAttrs(v, 8);
|
state.mkAttrs(v, 8);
|
||||||
mkString(*state.allocAttr(v, state.sOutPath), gitInfo.storePath, PathSet({gitInfo.storePath}));
|
auto storePath = state.store->printStorePath(tree.storePath);
|
||||||
mkString(*state.allocAttr(v, state.symbols.create("rev")), gitInfo.rev);
|
mkString(*state.allocAttr(v, state.sOutPath), storePath, PathSet({storePath}));
|
||||||
mkString(*state.allocAttr(v, state.symbols.create("shortRev")), gitInfo.shortRev);
|
// Backward compatibility: set 'rev' to
|
||||||
mkInt(*state.allocAttr(v, state.symbols.create("revCount")), gitInfo.revCount);
|
// 0000000000000000000000000000000000000000 for a dirty tree.
|
||||||
|
auto rev2 = input2->getRev().value_or(Hash(htSHA1));
|
||||||
|
mkString(*state.allocAttr(v, state.symbols.create("rev")), rev2.gitRev());
|
||||||
|
mkString(*state.allocAttr(v, state.symbols.create("shortRev")), rev2.gitShortRev());
|
||||||
|
// Backward compatibility: set 'revCount' to 0 for a dirty tree.
|
||||||
|
mkInt(*state.allocAttr(v, state.symbols.create("revCount")),
|
||||||
|
tree.info.revCount.value_or(0));
|
||||||
v.attrs->sort();
|
v.attrs->sort();
|
||||||
|
|
||||||
if (state.allowedPaths)
|
if (state.allowedPaths)
|
||||||
state.allowedPaths->insert(state.store->toRealPath(gitInfo.storePath));
|
state.allowedPaths->insert(tree.actualPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
static RegisterPrimOp r("fetchGit", 1, prim_fetchGit);
|
static RegisterPrimOp r("fetchGit", 1, prim_fetchGit);
|
||||||
|
|
|
@ -1,174 +1,18 @@
|
||||||
#include "primops.hh"
|
#include "primops.hh"
|
||||||
#include "eval-inline.hh"
|
#include "eval-inline.hh"
|
||||||
#include "download.hh"
|
|
||||||
#include "store-api.hh"
|
#include "store-api.hh"
|
||||||
#include "pathlocks.hh"
|
#include "fetchers.hh"
|
||||||
|
#include "url.hh"
|
||||||
#include <sys/time.h>
|
|
||||||
|
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
|
||||||
|
|
||||||
using namespace std::string_literals;
|
|
||||||
|
|
||||||
namespace nix {
|
namespace nix {
|
||||||
|
|
||||||
struct HgInfo
|
|
||||||
{
|
|
||||||
Path storePath;
|
|
||||||
std::string branch;
|
|
||||||
std::string rev;
|
|
||||||
uint64_t revCount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::regex commitHashRegex("^[0-9a-fA-F]{40}$");
|
|
||||||
|
|
||||||
HgInfo exportMercurial(ref<Store> store, const std::string & uri,
|
|
||||||
std::string rev, const std::string & name)
|
|
||||||
{
|
|
||||||
if (evalSettings.pureEval && rev == "")
|
|
||||||
throw Error("in pure evaluation mode, 'fetchMercurial' requires a Mercurial revision");
|
|
||||||
|
|
||||||
if (rev == "" && hasPrefix(uri, "/") && pathExists(uri + "/.hg")) {
|
|
||||||
|
|
||||||
bool clean = runProgram("hg", true, { "status", "-R", uri, "--modified", "--added", "--removed" }) == "";
|
|
||||||
|
|
||||||
if (!clean) {
|
|
||||||
|
|
||||||
/* This is an unclean working tree. So copy all tracked
|
|
||||||
files. */
|
|
||||||
|
|
||||||
printTalkative("copying unclean Mercurial working tree '%s'", uri);
|
|
||||||
|
|
||||||
HgInfo hgInfo;
|
|
||||||
hgInfo.rev = "0000000000000000000000000000000000000000";
|
|
||||||
hgInfo.branch = chomp(runProgram("hg", true, { "branch", "-R", uri }));
|
|
||||||
|
|
||||||
auto files = tokenizeString<std::set<std::string>>(
|
|
||||||
runProgram("hg", true, { "status", "-R", uri, "--clean", "--modified", "--added", "--no-status", "--print0" }), "\0"s);
|
|
||||||
|
|
||||||
PathFilter filter = [&](const Path & p) -> bool {
|
|
||||||
assert(hasPrefix(p, uri));
|
|
||||||
std::string file(p, uri.size() + 1);
|
|
||||||
|
|
||||||
auto st = lstat(p);
|
|
||||||
|
|
||||||
if (S_ISDIR(st.st_mode)) {
|
|
||||||
auto prefix = file + "/";
|
|
||||||
auto i = files.lower_bound(prefix);
|
|
||||||
return i != files.end() && hasPrefix(*i, prefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
return files.count(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
hgInfo.storePath = store->printStorePath(store->addToStore("source", uri, true, htSHA256, filter));
|
|
||||||
|
|
||||||
return hgInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rev == "") rev = "default";
|
|
||||||
|
|
||||||
Path cacheDir = fmt("%s/nix/hg/%s", getCacheDir(), hashString(htSHA256, uri).to_string(Base32, false));
|
|
||||||
|
|
||||||
Path stampFile = fmt("%s/.hg/%s.stamp", cacheDir, hashString(htSHA512, rev).to_string(Base32, false));
|
|
||||||
|
|
||||||
/* If we haven't pulled this repo less than ‘tarball-ttl’ seconds,
|
|
||||||
do so now. */
|
|
||||||
time_t now = time(0);
|
|
||||||
struct stat st;
|
|
||||||
if (stat(stampFile.c_str(), &st) != 0 ||
|
|
||||||
(uint64_t) st.st_mtime + settings.tarballTtl <= (uint64_t) now)
|
|
||||||
{
|
|
||||||
/* Except that if this is a commit hash that we already have,
|
|
||||||
we don't have to pull again. */
|
|
||||||
if (!(std::regex_match(rev, commitHashRegex)
|
|
||||||
&& pathExists(cacheDir)
|
|
||||||
&& runProgram(
|
|
||||||
RunOptions("hg", { "log", "-R", cacheDir, "-r", rev, "--template", "1" })
|
|
||||||
.killStderr(true)).second == "1"))
|
|
||||||
{
|
|
||||||
Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching Mercurial repository '%s'", uri));
|
|
||||||
|
|
||||||
if (pathExists(cacheDir)) {
|
|
||||||
try {
|
|
||||||
runProgram("hg", true, { "pull", "-R", cacheDir, "--", uri });
|
|
||||||
}
|
|
||||||
catch (ExecError & e) {
|
|
||||||
string transJournal = cacheDir + "/.hg/store/journal";
|
|
||||||
/* hg throws "abandoned transaction" error only if this file exists */
|
|
||||||
if (pathExists(transJournal)) {
|
|
||||||
runProgram("hg", true, { "recover", "-R", cacheDir });
|
|
||||||
runProgram("hg", true, { "pull", "-R", cacheDir, "--", uri });
|
|
||||||
} else {
|
|
||||||
throw ExecError(e.status, fmt("'hg pull' %s", statusToString(e.status)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
createDirs(dirOf(cacheDir));
|
|
||||||
runProgram("hg", true, { "clone", "--noupdate", "--", uri, cacheDir });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFile(stampFile, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto tokens = tokenizeString<std::vector<std::string>>(
|
|
||||||
runProgram("hg", true, { "log", "-R", cacheDir, "-r", rev, "--template", "{node} {rev} {branch}" }));
|
|
||||||
assert(tokens.size() == 3);
|
|
||||||
|
|
||||||
HgInfo hgInfo;
|
|
||||||
hgInfo.rev = tokens[0];
|
|
||||||
hgInfo.revCount = std::stoull(tokens[1]);
|
|
||||||
hgInfo.branch = tokens[2];
|
|
||||||
|
|
||||||
std::string storeLinkName = hashString(htSHA512, name + std::string("\0"s) + hgInfo.rev).to_string(Base32, false);
|
|
||||||
Path storeLink = fmt("%s/.hg/%s.link", cacheDir, storeLinkName);
|
|
||||||
|
|
||||||
try {
|
|
||||||
auto json = nlohmann::json::parse(readFile(storeLink));
|
|
||||||
|
|
||||||
assert(json["name"] == name && json["rev"] == hgInfo.rev);
|
|
||||||
|
|
||||||
hgInfo.storePath = json["storePath"];
|
|
||||||
|
|
||||||
if (store->isValidPath(store->parseStorePath(hgInfo.storePath))) {
|
|
||||||
printTalkative("using cached Mercurial store path '%s'", hgInfo.storePath);
|
|
||||||
return hgInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (SysError & e) {
|
|
||||||
if (e.errNo != ENOENT) throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
Path tmpDir = createTempDir();
|
|
||||||
AutoDelete delTmpDir(tmpDir, true);
|
|
||||||
|
|
||||||
runProgram("hg", true, { "archive", "-R", cacheDir, "-r", rev, tmpDir });
|
|
||||||
|
|
||||||
deletePath(tmpDir + "/.hg_archival.txt");
|
|
||||||
|
|
||||||
hgInfo.storePath = store->printStorePath(store->addToStore(name, tmpDir));
|
|
||||||
|
|
||||||
nlohmann::json json;
|
|
||||||
json["storePath"] = hgInfo.storePath;
|
|
||||||
json["uri"] = uri;
|
|
||||||
json["name"] = name;
|
|
||||||
json["branch"] = hgInfo.branch;
|
|
||||||
json["rev"] = hgInfo.rev;
|
|
||||||
json["revCount"] = hgInfo.revCount;
|
|
||||||
|
|
||||||
writeFile(storeLink, json.dump());
|
|
||||||
|
|
||||||
return hgInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void prim_fetchMercurial(EvalState & state, const Pos & pos, Value * * args, Value & v)
|
static void prim_fetchMercurial(EvalState & state, const Pos & pos, Value * * args, Value & v)
|
||||||
{
|
{
|
||||||
std::string url;
|
std::string url;
|
||||||
std::string rev;
|
std::optional<Hash> rev;
|
||||||
|
std::optional<std::string> ref;
|
||||||
std::string name = "source";
|
std::string name = "source";
|
||||||
PathSet context;
|
PathSet context;
|
||||||
|
|
||||||
|
@ -182,8 +26,15 @@ static void prim_fetchMercurial(EvalState & state, const Pos & pos, Value * * ar
|
||||||
string n(attr.name);
|
string n(attr.name);
|
||||||
if (n == "url")
|
if (n == "url")
|
||||||
url = state.coerceToString(*attr.pos, *attr.value, context, false, false);
|
url = state.coerceToString(*attr.pos, *attr.value, context, false, false);
|
||||||
else if (n == "rev")
|
else if (n == "rev") {
|
||||||
rev = state.forceStringNoCtx(*attr.value, *attr.pos);
|
// Ugly: unlike fetchGit, here the "rev" attribute can
|
||||||
|
// be both a revision or a branch/tag name.
|
||||||
|
auto value = state.forceStringNoCtx(*attr.value, *attr.pos);
|
||||||
|
if (std::regex_match(value, revRegex))
|
||||||
|
rev = Hash(value, htSHA1);
|
||||||
|
else
|
||||||
|
ref = value;
|
||||||
|
}
|
||||||
else if (n == "name")
|
else if (n == "name")
|
||||||
name = state.forceStringNoCtx(*attr.value, *attr.pos);
|
name = state.forceStringNoCtx(*attr.value, *attr.pos);
|
||||||
else
|
else
|
||||||
|
@ -200,18 +51,36 @@ static void prim_fetchMercurial(EvalState & state, const Pos & pos, Value * * ar
|
||||||
// whitelist. Ah well.
|
// whitelist. Ah well.
|
||||||
state.checkURI(url);
|
state.checkURI(url);
|
||||||
|
|
||||||
auto hgInfo = exportMercurial(state.store, url, rev, name);
|
if (evalSettings.pureEval && !rev)
|
||||||
|
throw Error("in pure evaluation mode, 'fetchMercurial' requires a Mercurial revision");
|
||||||
|
|
||||||
|
auto parsedUrl = parseURL(
|
||||||
|
url.find("://") != std::string::npos
|
||||||
|
? "hg+" + url
|
||||||
|
: "hg+file://" + url);
|
||||||
|
if (rev) parsedUrl.query.insert_or_assign("rev", rev->gitRev());
|
||||||
|
if (ref) parsedUrl.query.insert_or_assign("ref", *ref);
|
||||||
|
// FIXME: use name
|
||||||
|
auto input = fetchers::inputFromURL(parsedUrl);
|
||||||
|
|
||||||
|
auto [tree, input2] = input->fetchTree(state.store);
|
||||||
|
|
||||||
state.mkAttrs(v, 8);
|
state.mkAttrs(v, 8);
|
||||||
mkString(*state.allocAttr(v, state.sOutPath), hgInfo.storePath, PathSet({hgInfo.storePath}));
|
auto storePath = state.store->printStorePath(tree.storePath);
|
||||||
mkString(*state.allocAttr(v, state.symbols.create("branch")), hgInfo.branch);
|
mkString(*state.allocAttr(v, state.sOutPath), storePath, PathSet({storePath}));
|
||||||
mkString(*state.allocAttr(v, state.symbols.create("rev")), hgInfo.rev);
|
if (input2->getRef())
|
||||||
mkString(*state.allocAttr(v, state.symbols.create("shortRev")), std::string(hgInfo.rev, 0, 12));
|
mkString(*state.allocAttr(v, state.symbols.create("branch")), *input2->getRef());
|
||||||
mkInt(*state.allocAttr(v, state.symbols.create("revCount")), hgInfo.revCount);
|
// Backward compatibility: set 'rev' to
|
||||||
|
// 0000000000000000000000000000000000000000 for a dirty tree.
|
||||||
|
auto rev2 = input2->getRev().value_or(Hash(htSHA1));
|
||||||
|
mkString(*state.allocAttr(v, state.symbols.create("rev")), rev2.gitRev());
|
||||||
|
mkString(*state.allocAttr(v, state.symbols.create("shortRev")), std::string(rev2.gitRev(), 0, 12));
|
||||||
|
if (tree.info.revCount)
|
||||||
|
mkInt(*state.allocAttr(v, state.symbols.create("revCount")), *tree.info.revCount);
|
||||||
v.attrs->sort();
|
v.attrs->sort();
|
||||||
|
|
||||||
if (state.allowedPaths)
|
if (state.allowedPaths)
|
||||||
state.allowedPaths->insert(state.store->toRealPath(hgInfo.storePath));
|
state.allowedPaths->insert(tree.actualPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
static RegisterPrimOp r("fetchMercurial", 1, prim_fetchMercurial);
|
static RegisterPrimOp r("fetchMercurial", 1, prim_fetchMercurial);
|
||||||
|
|
165
src/libexpr/primops/fetchTree.cc
Normal file
165
src/libexpr/primops/fetchTree.cc
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
#include "primops.hh"
|
||||||
|
#include "eval-inline.hh"
|
||||||
|
#include "store-api.hh"
|
||||||
|
#include "fetchers.hh"
|
||||||
|
#include "download.hh"
|
||||||
|
|
||||||
|
#include <ctime>
|
||||||
|
#include <iomanip>
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
void emitTreeAttrs(
|
||||||
|
EvalState & state,
|
||||||
|
const fetchers::Tree & tree,
|
||||||
|
std::shared_ptr<const fetchers::Input> input,
|
||||||
|
Value & v)
|
||||||
|
{
|
||||||
|
state.mkAttrs(v, 8);
|
||||||
|
|
||||||
|
auto storePath = state.store->printStorePath(tree.storePath);
|
||||||
|
|
||||||
|
mkString(*state.allocAttr(v, state.sOutPath), storePath, PathSet({storePath}));
|
||||||
|
|
||||||
|
assert(tree.info.narHash);
|
||||||
|
mkString(*state.allocAttr(v, state.symbols.create("narHash")),
|
||||||
|
tree.info.narHash.to_string(SRI));
|
||||||
|
|
||||||
|
if (input->getRev()) {
|
||||||
|
mkString(*state.allocAttr(v, state.symbols.create("rev")), input->getRev()->gitRev());
|
||||||
|
mkString(*state.allocAttr(v, state.symbols.create("shortRev")), input->getRev()->gitShortRev());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tree.info.revCount)
|
||||||
|
mkInt(*state.allocAttr(v, state.symbols.create("revCount")), *tree.info.revCount);
|
||||||
|
|
||||||
|
if (tree.info.lastModified)
|
||||||
|
mkString(*state.allocAttr(v, state.symbols.create("lastModified")),
|
||||||
|
fmt("%s", std::put_time(std::gmtime(&*tree.info.lastModified), "%Y%m%d%H%M%S")));
|
||||||
|
|
||||||
|
v.attrs->sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void prim_fetchTree(EvalState & state, const Pos & pos, Value * * args, Value & v)
|
||||||
|
{
|
||||||
|
settings.requireExperimentalFeature("flakes");
|
||||||
|
|
||||||
|
std::shared_ptr<const fetchers::Input> input;
|
||||||
|
PathSet context;
|
||||||
|
|
||||||
|
state.forceValue(*args[0]);
|
||||||
|
|
||||||
|
if (args[0]->type == tAttrs) {
|
||||||
|
state.forceAttrs(*args[0], pos);
|
||||||
|
|
||||||
|
fetchers::Attrs attrs;
|
||||||
|
|
||||||
|
for (auto & attr : *args[0]->attrs) {
|
||||||
|
state.forceValue(*attr.value);
|
||||||
|
if (attr.value->type == tString)
|
||||||
|
attrs.emplace(attr.name, attr.value->string.s);
|
||||||
|
else if (attr.value->type == tBool)
|
||||||
|
attrs.emplace(attr.name, attr.value->boolean);
|
||||||
|
else
|
||||||
|
throw TypeError("fetchTree argument '%s' is %s while a string or Boolean is expected",
|
||||||
|
attr.name, showType(*attr.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!attrs.count("type"))
|
||||||
|
throw Error("attribute 'type' is missing in call to 'fetchTree', at %s", pos);
|
||||||
|
|
||||||
|
input = fetchers::inputFromAttrs(attrs);
|
||||||
|
} else
|
||||||
|
input = fetchers::inputFromURL(state.coerceToString(pos, *args[0], context, false, false));
|
||||||
|
|
||||||
|
if (evalSettings.pureEval && !input->isImmutable())
|
||||||
|
throw Error("in pure evaluation mode, 'fetchTree' requires an immutable input");
|
||||||
|
|
||||||
|
// FIXME: use fetchOrSubstituteTree
|
||||||
|
auto [tree, input2] = input->fetchTree(state.store);
|
||||||
|
|
||||||
|
if (state.allowedPaths)
|
||||||
|
state.allowedPaths->insert(tree.actualPath);
|
||||||
|
|
||||||
|
emitTreeAttrs(state, tree, input2, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
static RegisterPrimOp r("fetchTree", 1, prim_fetchTree);
|
||||||
|
|
||||||
|
static void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v,
|
||||||
|
const string & who, bool unpack, std::string name)
|
||||||
|
{
|
||||||
|
std::optional<std::string> url;
|
||||||
|
std::optional<Hash> expectedHash;
|
||||||
|
|
||||||
|
state.forceValue(*args[0]);
|
||||||
|
|
||||||
|
if (args[0]->type == tAttrs) {
|
||||||
|
|
||||||
|
state.forceAttrs(*args[0], pos);
|
||||||
|
|
||||||
|
for (auto & attr : *args[0]->attrs) {
|
||||||
|
string n(attr.name);
|
||||||
|
if (n == "url")
|
||||||
|
url = state.forceStringNoCtx(*attr.value, *attr.pos);
|
||||||
|
else if (n == "sha256")
|
||||||
|
expectedHash = Hash(state.forceStringNoCtx(*attr.value, *attr.pos), htSHA256);
|
||||||
|
else if (n == "name")
|
||||||
|
name = state.forceStringNoCtx(*attr.value, *attr.pos);
|
||||||
|
else
|
||||||
|
throw EvalError("unsupported argument '%s' to '%s', at %s",
|
||||||
|
attr.name, who, attr.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url)
|
||||||
|
throw EvalError("'url' argument required, at %s", pos);
|
||||||
|
|
||||||
|
} else
|
||||||
|
url = state.forceStringNoCtx(*args[0], pos);
|
||||||
|
|
||||||
|
url = resolveUri(*url);
|
||||||
|
|
||||||
|
state.checkURI(*url);
|
||||||
|
|
||||||
|
if (name == "")
|
||||||
|
name = baseNameOf(*url);
|
||||||
|
|
||||||
|
if (evalSettings.pureEval && !expectedHash)
|
||||||
|
throw Error("in pure evaluation mode, '%s' requires a 'sha256' argument", who);
|
||||||
|
|
||||||
|
auto storePath =
|
||||||
|
unpack
|
||||||
|
? fetchers::downloadTarball(state.store, *url, name, (bool) expectedHash).storePath
|
||||||
|
: fetchers::downloadFile(state.store, *url, name, (bool) expectedHash).storePath;
|
||||||
|
|
||||||
|
auto path = state.store->toRealPath(storePath);
|
||||||
|
|
||||||
|
if (expectedHash) {
|
||||||
|
auto hash = unpack
|
||||||
|
? state.store->queryPathInfo(storePath)->narHash
|
||||||
|
: hashFile(htSHA256, path);
|
||||||
|
if (hash != *expectedHash)
|
||||||
|
throw Error((unsigned int) 102, "hash mismatch in file downloaded from '%s':\n wanted: %s\n got: %s",
|
||||||
|
*url, expectedHash->to_string(), hash.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.allowedPaths)
|
||||||
|
state.allowedPaths->insert(path);
|
||||||
|
|
||||||
|
mkString(v, path, PathSet({path}));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void prim_fetchurl(EvalState & state, const Pos & pos, Value * * args, Value & v)
|
||||||
|
{
|
||||||
|
fetch(state, pos, args, v, "fetchurl", false, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void prim_fetchTarball(EvalState & state, const Pos & pos, Value * * args, Value & v)
|
||||||
|
{
|
||||||
|
fetch(state, pos, args, v, "fetchTarball", true, "source");
|
||||||
|
}
|
||||||
|
|
||||||
|
static RegisterPrimOp r2("__fetchurl", 1, prim_fetchurl);
|
||||||
|
static RegisterPrimOp r3("fetchTarball", 1, prim_fetchTarball);
|
||||||
|
|
||||||
|
}
|
92
src/libfetchers/attrs.cc
Normal file
92
src/libfetchers/attrs.cc
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
#include "attrs.hh"
|
||||||
|
#include "fetchers.hh"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
namespace nix::fetchers {
|
||||||
|
|
||||||
|
Attrs jsonToAttrs(const nlohmann::json & json)
|
||||||
|
{
|
||||||
|
Attrs attrs;
|
||||||
|
|
||||||
|
for (auto & i : json.items()) {
|
||||||
|
if (i.value().is_number())
|
||||||
|
attrs.emplace(i.key(), i.value().get<int64_t>());
|
||||||
|
else if (i.value().is_string())
|
||||||
|
attrs.emplace(i.key(), i.value().get<std::string>());
|
||||||
|
else if (i.value().is_boolean())
|
||||||
|
attrs.emplace(i.key(), i.value().get<bool>());
|
||||||
|
else
|
||||||
|
throw Error("unsupported input attribute type in lock file");
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json attrsToJson(const Attrs & attrs)
|
||||||
|
{
|
||||||
|
nlohmann::json json;
|
||||||
|
for (auto & attr : attrs) {
|
||||||
|
if (auto v = std::get_if<int64_t>(&attr.second)) {
|
||||||
|
json[attr.first] = *v;
|
||||||
|
} else if (auto v = std::get_if<std::string>(&attr.second)) {
|
||||||
|
json[attr.first] = *v;
|
||||||
|
} else if (auto v = std::get_if<Explicit<bool>>(&attr.second)) {
|
||||||
|
json[attr.first] = v->t;
|
||||||
|
} else abort();
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> maybeGetStrAttr(const Attrs & attrs, const std::string & name)
|
||||||
|
{
|
||||||
|
auto i = attrs.find(name);
|
||||||
|
if (i == attrs.end()) return {};
|
||||||
|
if (auto v = std::get_if<std::string>(&i->second))
|
||||||
|
return *v;
|
||||||
|
throw Error("input attribute '%s' is not a string %s", name, attrsToJson(attrs).dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getStrAttr(const Attrs & attrs, const std::string & name)
|
||||||
|
{
|
||||||
|
auto s = maybeGetStrAttr(attrs, name);
|
||||||
|
if (!s)
|
||||||
|
throw Error("input attribute '%s' is missing", name);
|
||||||
|
return *s;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<int64_t> maybeGetIntAttr(const Attrs & attrs, const std::string & name)
|
||||||
|
{
|
||||||
|
auto i = attrs.find(name);
|
||||||
|
if (i == attrs.end()) return {};
|
||||||
|
if (auto v = std::get_if<int64_t>(&i->second))
|
||||||
|
return *v;
|
||||||
|
throw Error("input attribute '%s' is not an integer", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t getIntAttr(const Attrs & attrs, const std::string & name)
|
||||||
|
{
|
||||||
|
auto s = maybeGetIntAttr(attrs, name);
|
||||||
|
if (!s)
|
||||||
|
throw Error("input attribute '%s' is missing", name);
|
||||||
|
return *s;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<bool> maybeGetBoolAttr(const Attrs & attrs, const std::string & name)
|
||||||
|
{
|
||||||
|
auto i = attrs.find(name);
|
||||||
|
if (i == attrs.end()) return {};
|
||||||
|
if (auto v = std::get_if<int64_t>(&i->second))
|
||||||
|
return *v;
|
||||||
|
throw Error("input attribute '%s' is not a Boolean", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getBoolAttr(const Attrs & attrs, const std::string & name)
|
||||||
|
{
|
||||||
|
auto s = maybeGetBoolAttr(attrs, name);
|
||||||
|
if (!s)
|
||||||
|
throw Error("input attribute '%s' is missing", name);
|
||||||
|
return *s;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
37
src/libfetchers/attrs.hh
Normal file
37
src/libfetchers/attrs.hh
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "types.hh"
|
||||||
|
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
#include <nlohmann/json_fwd.hpp>
|
||||||
|
|
||||||
|
namespace nix::fetchers {
|
||||||
|
|
||||||
|
/* Wrap bools to prevent string literals (i.e. 'char *') from being
|
||||||
|
cast to a bool in Attr. */
|
||||||
|
template<typename T>
|
||||||
|
struct Explicit {
|
||||||
|
T t;
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef std::variant<std::string, int64_t, Explicit<bool>> Attr;
|
||||||
|
typedef std::map<std::string, Attr> Attrs;
|
||||||
|
|
||||||
|
Attrs jsonToAttrs(const nlohmann::json & json);
|
||||||
|
|
||||||
|
nlohmann::json attrsToJson(const Attrs & attrs);
|
||||||
|
|
||||||
|
std::optional<std::string> maybeGetStrAttr(const Attrs & attrs, const std::string & name);
|
||||||
|
|
||||||
|
std::string getStrAttr(const Attrs & attrs, const std::string & name);
|
||||||
|
|
||||||
|
std::optional<int64_t> maybeGetIntAttr(const Attrs & attrs, const std::string & name);
|
||||||
|
|
||||||
|
int64_t getIntAttr(const Attrs & attrs, const std::string & name);
|
||||||
|
|
||||||
|
std::optional<bool> maybeGetBoolAttr(const Attrs & attrs, const std::string & name);
|
||||||
|
|
||||||
|
bool getBoolAttr(const Attrs & attrs, const std::string & name);
|
||||||
|
|
||||||
|
}
|
121
src/libfetchers/cache.cc
Normal file
121
src/libfetchers/cache.cc
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
#include "cache.hh"
|
||||||
|
#include "sqlite.hh"
|
||||||
|
#include "sync.hh"
|
||||||
|
#include "store-api.hh"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
namespace nix::fetchers {
|
||||||
|
|
||||||
|
static const char * schema = R"sql(
|
||||||
|
|
||||||
|
create table if not exists Cache (
|
||||||
|
input text not null,
|
||||||
|
info text not null,
|
||||||
|
path text not null,
|
||||||
|
immutable integer not null,
|
||||||
|
timestamp integer not null,
|
||||||
|
primary key (input)
|
||||||
|
);
|
||||||
|
)sql";
|
||||||
|
|
||||||
|
struct CacheImpl : Cache
|
||||||
|
{
|
||||||
|
struct State
|
||||||
|
{
|
||||||
|
SQLite db;
|
||||||
|
SQLiteStmt add, lookup;
|
||||||
|
};
|
||||||
|
|
||||||
|
Sync<State> _state;
|
||||||
|
|
||||||
|
CacheImpl()
|
||||||
|
{
|
||||||
|
auto state(_state.lock());
|
||||||
|
|
||||||
|
auto dbPath = getCacheDir() + "/nix/fetcher-cache-v1.sqlite";
|
||||||
|
createDirs(dirOf(dbPath));
|
||||||
|
|
||||||
|
state->db = SQLite(dbPath);
|
||||||
|
state->db.isCache();
|
||||||
|
state->db.exec(schema);
|
||||||
|
|
||||||
|
state->add.create(state->db,
|
||||||
|
"insert or replace into Cache(input, info, path, immutable, timestamp) values (?, ?, ?, ?, ?)");
|
||||||
|
|
||||||
|
state->lookup.create(state->db,
|
||||||
|
"select info, path, immutable, timestamp from Cache where input = ?");
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(
|
||||||
|
ref<Store> store,
|
||||||
|
const Attrs & inAttrs,
|
||||||
|
const Attrs & infoAttrs,
|
||||||
|
const StorePath & storePath,
|
||||||
|
bool immutable) override
|
||||||
|
{
|
||||||
|
_state.lock()->add.use()
|
||||||
|
(attrsToJson(inAttrs).dump())
|
||||||
|
(attrsToJson(infoAttrs).dump())
|
||||||
|
(store->printStorePath(storePath))
|
||||||
|
(immutable)
|
||||||
|
(time(0)).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::pair<Attrs, StorePath>> lookup(
|
||||||
|
ref<Store> store,
|
||||||
|
const Attrs & inAttrs) override
|
||||||
|
{
|
||||||
|
if (auto res = lookupExpired(store, inAttrs)) {
|
||||||
|
if (!res->expired)
|
||||||
|
return std::make_pair(std::move(res->infoAttrs), std::move(res->storePath));
|
||||||
|
debug("ignoring expired cache entry '%s'",
|
||||||
|
attrsToJson(inAttrs).dump());
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Result> lookupExpired(
|
||||||
|
ref<Store> store,
|
||||||
|
const Attrs & inAttrs) override
|
||||||
|
{
|
||||||
|
auto state(_state.lock());
|
||||||
|
|
||||||
|
auto inAttrsJson = attrsToJson(inAttrs).dump();
|
||||||
|
|
||||||
|
auto stmt(state->lookup.use()(inAttrsJson));
|
||||||
|
if (!stmt.next()) {
|
||||||
|
debug("did not find cache entry for '%s'", inAttrsJson);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto infoJson = stmt.getStr(0);
|
||||||
|
auto storePath = store->parseStorePath(stmt.getStr(1));
|
||||||
|
auto immutable = stmt.getInt(2) != 0;
|
||||||
|
auto timestamp = stmt.getInt(3);
|
||||||
|
|
||||||
|
store->addTempRoot(storePath);
|
||||||
|
if (!store->isValidPath(storePath)) {
|
||||||
|
// FIXME: we could try to substitute 'storePath'.
|
||||||
|
debug("ignoring disappeared cache entry '%s'", inAttrsJson);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("using cache entry '%s' -> '%s', '%s'",
|
||||||
|
inAttrsJson, infoJson, store->printStorePath(storePath));
|
||||||
|
|
||||||
|
return Result {
|
||||||
|
.expired = !immutable && (settings.tarballTtl.get() == 0 || timestamp + settings.tarballTtl < time(0)),
|
||||||
|
.infoAttrs = jsonToAttrs(nlohmann::json::parse(infoJson)),
|
||||||
|
.storePath = std::move(storePath)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ref<Cache> getCache()
|
||||||
|
{
|
||||||
|
static auto cache = std::make_shared<CacheImpl>();
|
||||||
|
return ref<Cache>(cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
34
src/libfetchers/cache.hh
Normal file
34
src/libfetchers/cache.hh
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fetchers.hh"
|
||||||
|
|
||||||
|
namespace nix::fetchers {
|
||||||
|
|
||||||
|
struct Cache
|
||||||
|
{
|
||||||
|
virtual void add(
|
||||||
|
ref<Store> store,
|
||||||
|
const Attrs & inAttrs,
|
||||||
|
const Attrs & infoAttrs,
|
||||||
|
const StorePath & storePath,
|
||||||
|
bool immutable) = 0;
|
||||||
|
|
||||||
|
virtual std::optional<std::pair<Attrs, StorePath>> lookup(
|
||||||
|
ref<Store> store,
|
||||||
|
const Attrs & inAttrs) = 0;
|
||||||
|
|
||||||
|
struct Result
|
||||||
|
{
|
||||||
|
bool expired = false;
|
||||||
|
Attrs infoAttrs;
|
||||||
|
StorePath storePath;
|
||||||
|
};
|
||||||
|
|
||||||
|
virtual std::optional<Result> lookupExpired(
|
||||||
|
ref<Store> store,
|
||||||
|
const Attrs & inAttrs) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
ref<Cache> getCache();
|
||||||
|
|
||||||
|
}
|
73
src/libfetchers/fetchers.cc
Normal file
73
src/libfetchers/fetchers.cc
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
#include "fetchers.hh"
|
||||||
|
#include "store-api.hh"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
namespace nix::fetchers {
|
||||||
|
|
||||||
|
std::unique_ptr<std::vector<std::unique_ptr<InputScheme>>> inputSchemes = nullptr;
|
||||||
|
|
||||||
|
void registerInputScheme(std::unique_ptr<InputScheme> && inputScheme)
|
||||||
|
{
|
||||||
|
if (!inputSchemes) inputSchemes = std::make_unique<std::vector<std::unique_ptr<InputScheme>>>();
|
||||||
|
inputSchemes->push_back(std::move(inputScheme));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Input> inputFromURL(const ParsedURL & url)
|
||||||
|
{
|
||||||
|
for (auto & inputScheme : *inputSchemes) {
|
||||||
|
auto res = inputScheme->inputFromURL(url);
|
||||||
|
if (res) return res;
|
||||||
|
}
|
||||||
|
throw Error("input '%s' is unsupported", url.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Input> inputFromURL(const std::string & url)
|
||||||
|
{
|
||||||
|
return inputFromURL(parseURL(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Input> inputFromAttrs(const Attrs & attrs)
|
||||||
|
{
|
||||||
|
for (auto & inputScheme : *inputSchemes) {
|
||||||
|
auto res = inputScheme->inputFromAttrs(attrs);
|
||||||
|
if (res) {
|
||||||
|
if (auto narHash = maybeGetStrAttr(attrs, "narHash"))
|
||||||
|
// FIXME: require SRI hash.
|
||||||
|
res->narHash = Hash(*narHash);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw Error("input '%s' is unsupported", attrsToJson(attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
Attrs Input::toAttrs() const
|
||||||
|
{
|
||||||
|
auto attrs = toAttrsInternal();
|
||||||
|
if (narHash)
|
||||||
|
attrs.emplace("narHash", narHash->to_string(SRI));
|
||||||
|
attrs.emplace("type", type());
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<Tree, std::shared_ptr<const Input>> Input::fetchTree(ref<Store> store) const
|
||||||
|
{
|
||||||
|
auto [tree, input] = fetchTreeInternal(store);
|
||||||
|
|
||||||
|
if (tree.actualPath == "")
|
||||||
|
tree.actualPath = store->toRealPath(tree.storePath);
|
||||||
|
|
||||||
|
if (!tree.info.narHash)
|
||||||
|
tree.info.narHash = store->queryPathInfo(tree.storePath)->narHash;
|
||||||
|
|
||||||
|
if (input->narHash)
|
||||||
|
assert(input->narHash == tree.info.narHash);
|
||||||
|
|
||||||
|
if (narHash && narHash != input->narHash)
|
||||||
|
throw Error("NAR hash mismatch in input '%s' (%s), expected '%s', got '%s'",
|
||||||
|
to_string(), tree.actualPath, narHash->to_string(SRI), input->narHash->to_string(SRI));
|
||||||
|
|
||||||
|
return {std::move(tree), input};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
103
src/libfetchers/fetchers.hh
Normal file
103
src/libfetchers/fetchers.hh
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "types.hh"
|
||||||
|
#include "hash.hh"
|
||||||
|
#include "path.hh"
|
||||||
|
#include "tree-info.hh"
|
||||||
|
#include "attrs.hh"
|
||||||
|
#include "url.hh"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace nix { class Store; }
|
||||||
|
|
||||||
|
namespace nix::fetchers {
|
||||||
|
|
||||||
|
struct Input;
|
||||||
|
|
||||||
|
struct Tree
|
||||||
|
{
|
||||||
|
Path actualPath;
|
||||||
|
StorePath storePath;
|
||||||
|
TreeInfo info;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Input : std::enable_shared_from_this<Input>
|
||||||
|
{
|
||||||
|
std::optional<Hash> narHash; // FIXME: implement
|
||||||
|
|
||||||
|
virtual std::string type() const = 0;
|
||||||
|
|
||||||
|
virtual ~Input() { }
|
||||||
|
|
||||||
|
virtual bool operator ==(const Input & other) const { return false; }
|
||||||
|
|
||||||
|
/* Check whether this is a "direct" input, that is, not
|
||||||
|
one that goes through a registry. */
|
||||||
|
virtual bool isDirect() const { return true; }
|
||||||
|
|
||||||
|
/* Check whether this is an "immutable" input, that is,
|
||||||
|
one that contains a commit hash or content hash. */
|
||||||
|
virtual bool isImmutable() const { return (bool) narHash; }
|
||||||
|
|
||||||
|
virtual bool contains(const Input & other) const { return false; }
|
||||||
|
|
||||||
|
virtual std::optional<std::string> getRef() const { return {}; }
|
||||||
|
|
||||||
|
virtual std::optional<Hash> getRev() const { return {}; }
|
||||||
|
|
||||||
|
virtual ParsedURL toURL() const = 0;
|
||||||
|
|
||||||
|
std::string to_string() const
|
||||||
|
{
|
||||||
|
return toURL().to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
Attrs toAttrs() const;
|
||||||
|
|
||||||
|
std::pair<Tree, std::shared_ptr<const Input>> fetchTree(ref<Store> store) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
virtual std::pair<Tree, std::shared_ptr<const Input>> fetchTreeInternal(ref<Store> store) const = 0;
|
||||||
|
|
||||||
|
virtual Attrs toAttrsInternal() const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputScheme
|
||||||
|
{
|
||||||
|
virtual ~InputScheme() { }
|
||||||
|
|
||||||
|
virtual std::unique_ptr<Input> inputFromURL(const ParsedURL & url) = 0;
|
||||||
|
|
||||||
|
virtual std::unique_ptr<Input> inputFromAttrs(const Attrs & attrs) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<Input> inputFromURL(const ParsedURL & url);
|
||||||
|
|
||||||
|
std::unique_ptr<Input> inputFromURL(const std::string & url);
|
||||||
|
|
||||||
|
std::unique_ptr<Input> inputFromAttrs(const Attrs & attrs);
|
||||||
|
|
||||||
|
void registerInputScheme(std::unique_ptr<InputScheme> && fetcher);
|
||||||
|
|
||||||
|
struct DownloadFileResult
|
||||||
|
{
|
||||||
|
StorePath storePath;
|
||||||
|
std::string etag;
|
||||||
|
std::string effectiveUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
DownloadFileResult downloadFile(
|
||||||
|
ref<Store> store,
|
||||||
|
const std::string & url,
|
||||||
|
const std::string & name,
|
||||||
|
bool immutable);
|
||||||
|
|
||||||
|
Tree downloadTarball(
|
||||||
|
ref<Store> store,
|
||||||
|
const std::string & url,
|
||||||
|
const std::string & name,
|
||||||
|
bool immutable);
|
||||||
|
|
||||||
|
}
|
401
src/libfetchers/git.cc
Normal file
401
src/libfetchers/git.cc
Normal file
|
@ -0,0 +1,401 @@
|
||||||
|
#include "fetchers.hh"
|
||||||
|
#include "cache.hh"
|
||||||
|
#include "globals.hh"
|
||||||
|
#include "tarfile.hh"
|
||||||
|
#include "store-api.hh"
|
||||||
|
|
||||||
|
#include <sys/time.h>
|
||||||
|
|
||||||
|
using namespace std::string_literals;
|
||||||
|
|
||||||
|
namespace nix::fetchers {
|
||||||
|
|
||||||
|
static std::string readHead(const Path & path)
|
||||||
|
{
|
||||||
|
return chomp(runProgram("git", true, { "-C", path, "rev-parse", "--abbrev-ref", "HEAD" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GitInput : Input
|
||||||
|
{
|
||||||
|
ParsedURL url;
|
||||||
|
std::optional<std::string> ref;
|
||||||
|
std::optional<Hash> rev;
|
||||||
|
bool shallow = false;
|
||||||
|
|
||||||
|
GitInput(const ParsedURL & url) : url(url)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
std::string type() const override { return "git"; }
|
||||||
|
|
||||||
|
bool operator ==(const Input & other) const override
|
||||||
|
{
|
||||||
|
auto other2 = dynamic_cast<const GitInput *>(&other);
|
||||||
|
return
|
||||||
|
other2
|
||||||
|
&& url == other2->url
|
||||||
|
&& rev == other2->rev
|
||||||
|
&& ref == other2->ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isImmutable() const override
|
||||||
|
{
|
||||||
|
return (bool) rev;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> getRef() const override { return ref; }
|
||||||
|
|
||||||
|
std::optional<Hash> getRev() const override { return rev; }
|
||||||
|
|
||||||
|
ParsedURL toURL() const override
|
||||||
|
{
|
||||||
|
ParsedURL url2(url);
|
||||||
|
if (url2.scheme != "git") url2.scheme = "git+" + url2.scheme;
|
||||||
|
if (rev) url2.query.insert_or_assign("rev", rev->gitRev());
|
||||||
|
if (ref) url2.query.insert_or_assign("ref", *ref);
|
||||||
|
if (shallow) url2.query.insert_or_assign("shallow", "1");
|
||||||
|
return url2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Attrs toAttrsInternal() const override
|
||||||
|
{
|
||||||
|
Attrs attrs;
|
||||||
|
attrs.emplace("url", url.to_string());
|
||||||
|
if (ref)
|
||||||
|
attrs.emplace("ref", *ref);
|
||||||
|
if (rev)
|
||||||
|
attrs.emplace("rev", rev->gitRev());
|
||||||
|
if (shallow)
|
||||||
|
attrs.emplace("shallow", true);
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<bool, std::string> getActualUrl() const
|
||||||
|
{
|
||||||
|
// Don't clone file:// URIs (but otherwise treat them the
|
||||||
|
// same as remote URIs, i.e. don't use the working tree or
|
||||||
|
// HEAD).
|
||||||
|
static bool forceHttp = getEnv("_NIX_FORCE_HTTP") == "1"; // for testing
|
||||||
|
bool isLocal = url.scheme == "file" && !forceHttp;
|
||||||
|
return {isLocal, isLocal ? url.path : url.base};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<Tree, std::shared_ptr<const Input>> fetchTreeInternal(nix::ref<Store> store) const override
|
||||||
|
{
|
||||||
|
auto name = "source";
|
||||||
|
|
||||||
|
auto input = std::make_shared<GitInput>(*this);
|
||||||
|
|
||||||
|
assert(!rev || rev->type == htSHA1);
|
||||||
|
|
||||||
|
auto cacheType = shallow ? "git-shallow" : "git";
|
||||||
|
|
||||||
|
auto getImmutableAttrs = [&]()
|
||||||
|
{
|
||||||
|
return Attrs({
|
||||||
|
{"type", cacheType},
|
||||||
|
{"name", name},
|
||||||
|
{"rev", input->rev->gitRev()},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
auto makeResult = [&](const Attrs & infoAttrs, StorePath && storePath)
|
||||||
|
-> std::pair<Tree, std::shared_ptr<const Input>>
|
||||||
|
{
|
||||||
|
assert(input->rev);
|
||||||
|
assert(!rev || rev == input->rev);
|
||||||
|
return {
|
||||||
|
Tree {
|
||||||
|
.actualPath = store->toRealPath(storePath),
|
||||||
|
.storePath = std::move(storePath),
|
||||||
|
.info = TreeInfo {
|
||||||
|
.revCount = shallow ? std::nullopt : std::optional(getIntAttr(infoAttrs, "revCount")),
|
||||||
|
.lastModified = getIntAttr(infoAttrs, "lastModified"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rev) {
|
||||||
|
if (auto res = getCache()->lookup(store, getImmutableAttrs()))
|
||||||
|
return makeResult(res->first, std::move(res->second));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [isLocal, actualUrl_] = getActualUrl();
|
||||||
|
auto actualUrl = actualUrl_; // work around clang bug
|
||||||
|
|
||||||
|
// If this is a local directory and no ref or revision is
|
||||||
|
// given, then allow the use of an unclean working tree.
|
||||||
|
if (!input->ref && !input->rev && isLocal) {
|
||||||
|
bool clean = false;
|
||||||
|
|
||||||
|
/* Check whether this repo has any commits. There are
|
||||||
|
probably better ways to do this. */
|
||||||
|
auto gitDir = actualUrl + "/.git";
|
||||||
|
auto commonGitDir = chomp(runProgram(
|
||||||
|
"git",
|
||||||
|
true,
|
||||||
|
{ "-C", actualUrl, "rev-parse", "--git-common-dir" }
|
||||||
|
));
|
||||||
|
if (commonGitDir != ".git")
|
||||||
|
gitDir = commonGitDir;
|
||||||
|
|
||||||
|
bool haveCommits = !readDirectory(gitDir + "/refs/heads").empty();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (haveCommits) {
|
||||||
|
runProgram("git", true, { "-C", actualUrl, "diff-index", "--quiet", "HEAD", "--" });
|
||||||
|
clean = true;
|
||||||
|
}
|
||||||
|
} catch (ExecError & e) {
|
||||||
|
if (!WIFEXITED(e.status) || WEXITSTATUS(e.status) != 1) throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clean) {
|
||||||
|
|
||||||
|
/* This is an unclean working tree. So copy all tracked files. */
|
||||||
|
|
||||||
|
if (!settings.allowDirty)
|
||||||
|
throw Error("Git tree '%s' is dirty", actualUrl);
|
||||||
|
|
||||||
|
if (settings.warnDirty)
|
||||||
|
warn("Git tree '%s' is dirty", actualUrl);
|
||||||
|
|
||||||
|
auto files = tokenizeString<std::set<std::string>>(
|
||||||
|
runProgram("git", true, { "-C", actualUrl, "ls-files", "-z" }), "\0"s);
|
||||||
|
|
||||||
|
PathFilter filter = [&](const Path & p) -> bool {
|
||||||
|
assert(hasPrefix(p, actualUrl));
|
||||||
|
std::string file(p, actualUrl.size() + 1);
|
||||||
|
|
||||||
|
auto st = lstat(p);
|
||||||
|
|
||||||
|
if (S_ISDIR(st.st_mode)) {
|
||||||
|
auto prefix = file + "/";
|
||||||
|
auto i = files.lower_bound(prefix);
|
||||||
|
return i != files.end() && hasPrefix(*i, prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
return files.count(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto storePath = store->addToStore("source", actualUrl, true, htSHA256, filter);
|
||||||
|
|
||||||
|
auto tree = Tree {
|
||||||
|
.actualPath = store->printStorePath(storePath),
|
||||||
|
.storePath = std::move(storePath),
|
||||||
|
.info = TreeInfo {
|
||||||
|
// FIXME: maybe we should use the timestamp of the last
|
||||||
|
// modified dirty file?
|
||||||
|
.lastModified = haveCommits ? std::stoull(runProgram("git", true, { "-C", actualUrl, "log", "-1", "--format=%ct", "HEAD" })) : 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {std::move(tree), input};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input->ref) input->ref = isLocal ? readHead(actualUrl) : "master";
|
||||||
|
|
||||||
|
Attrs mutableAttrs({
|
||||||
|
{"type", cacheType},
|
||||||
|
{"name", name},
|
||||||
|
{"url", actualUrl},
|
||||||
|
{"ref", *input->ref},
|
||||||
|
});
|
||||||
|
|
||||||
|
Path repoDir;
|
||||||
|
|
||||||
|
if (isLocal) {
|
||||||
|
|
||||||
|
if (!input->rev)
|
||||||
|
input->rev = Hash(chomp(runProgram("git", true, { "-C", actualUrl, "rev-parse", *input->ref })), htSHA1);
|
||||||
|
|
||||||
|
repoDir = actualUrl;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (auto res = getCache()->lookup(store, mutableAttrs)) {
|
||||||
|
auto rev2 = Hash(getStrAttr(res->first, "rev"), htSHA1);
|
||||||
|
if (!rev || rev == rev2) {
|
||||||
|
input->rev = rev2;
|
||||||
|
return makeResult(res->first, std::move(res->second));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Path cacheDir = getCacheDir() + "/nix/gitv3/" + hashString(htSHA256, actualUrl).to_string(Base32, false);
|
||||||
|
repoDir = cacheDir;
|
||||||
|
|
||||||
|
if (!pathExists(cacheDir)) {
|
||||||
|
createDirs(dirOf(cacheDir));
|
||||||
|
runProgram("git", true, { "init", "--bare", repoDir });
|
||||||
|
}
|
||||||
|
|
||||||
|
Path localRefFile =
|
||||||
|
input->ref->compare(0, 5, "refs/") == 0
|
||||||
|
? cacheDir + "/" + *input->ref
|
||||||
|
: cacheDir + "/refs/heads/" + *input->ref;
|
||||||
|
|
||||||
|
bool doFetch;
|
||||||
|
time_t now = time(0);
|
||||||
|
|
||||||
|
/* If a rev was specified, we need to fetch if it's not in the
|
||||||
|
repo. */
|
||||||
|
if (input->rev) {
|
||||||
|
try {
|
||||||
|
runProgram("git", true, { "-C", repoDir, "cat-file", "-e", input->rev->gitRev() });
|
||||||
|
doFetch = false;
|
||||||
|
} catch (ExecError & e) {
|
||||||
|
if (WIFEXITED(e.status)) {
|
||||||
|
doFetch = true;
|
||||||
|
} else {
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* If the local ref is older than ‘tarball-ttl’ seconds, do a
|
||||||
|
git fetch to update the local ref to the remote ref. */
|
||||||
|
struct stat st;
|
||||||
|
doFetch = stat(localRefFile.c_str(), &st) != 0 ||
|
||||||
|
(uint64_t) st.st_mtime + settings.tarballTtl <= (uint64_t) now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doFetch) {
|
||||||
|
Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching Git repository '%s'", actualUrl));
|
||||||
|
|
||||||
|
// FIXME: git stderr messes up our progress indicator, so
|
||||||
|
// we're using --quiet for now. Should process its stderr.
|
||||||
|
try {
|
||||||
|
runProgram("git", true, { "-C", repoDir, "fetch", "--quiet", "--force", "--", actualUrl, fmt("%s:%s", *input->ref, *input->ref) });
|
||||||
|
} catch (Error & e) {
|
||||||
|
if (!pathExists(localRefFile)) throw;
|
||||||
|
warn("could not update local clone of Git repository '%s'; continuing with the most recent version", actualUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct timeval times[2];
|
||||||
|
times[0].tv_sec = now;
|
||||||
|
times[0].tv_usec = 0;
|
||||||
|
times[1].tv_sec = now;
|
||||||
|
times[1].tv_usec = 0;
|
||||||
|
|
||||||
|
utimes(localRefFile.c_str(), times);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input->rev)
|
||||||
|
input->rev = Hash(chomp(readFile(localRefFile)), htSHA1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isShallow = chomp(runProgram("git", true, { "-C", repoDir, "rev-parse", "--is-shallow-repository" })) == "true";
|
||||||
|
|
||||||
|
if (isShallow && !shallow)
|
||||||
|
throw Error("'%s' is a shallow Git repository, but a non-shallow repository is needed", actualUrl);
|
||||||
|
|
||||||
|
// FIXME: check whether rev is an ancestor of ref.
|
||||||
|
|
||||||
|
printTalkative("using revision %s of repo '%s'", input->rev->gitRev(), actualUrl);
|
||||||
|
|
||||||
|
/* Now that we know the ref, check again whether we have it in
|
||||||
|
the store. */
|
||||||
|
if (auto res = getCache()->lookup(store, getImmutableAttrs()))
|
||||||
|
return makeResult(res->first, std::move(res->second));
|
||||||
|
|
||||||
|
// FIXME: should pipe this, or find some better way to extract a
|
||||||
|
// revision.
|
||||||
|
auto source = sinkToSource([&](Sink & sink) {
|
||||||
|
RunOptions gitOptions("git", { "-C", repoDir, "archive", input->rev->gitRev() });
|
||||||
|
gitOptions.standardOut = &sink;
|
||||||
|
runProgram2(gitOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
Path tmpDir = createTempDir();
|
||||||
|
AutoDelete delTmpDir(tmpDir, true);
|
||||||
|
|
||||||
|
unpackTarfile(*source, tmpDir);
|
||||||
|
|
||||||
|
auto storePath = store->addToStore(name, tmpDir);
|
||||||
|
|
||||||
|
auto lastModified = std::stoull(runProgram("git", true, { "-C", repoDir, "log", "-1", "--format=%ct", input->rev->gitRev() }));
|
||||||
|
|
||||||
|
Attrs infoAttrs({
|
||||||
|
{"rev", input->rev->gitRev()},
|
||||||
|
{"lastModified", lastModified},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shallow)
|
||||||
|
infoAttrs.insert_or_assign("revCount",
|
||||||
|
std::stoull(runProgram("git", true, { "-C", repoDir, "rev-list", "--count", input->rev->gitRev() })));
|
||||||
|
|
||||||
|
if (!this->rev)
|
||||||
|
getCache()->add(
|
||||||
|
store,
|
||||||
|
mutableAttrs,
|
||||||
|
infoAttrs,
|
||||||
|
storePath,
|
||||||
|
false);
|
||||||
|
|
||||||
|
getCache()->add(
|
||||||
|
store,
|
||||||
|
getImmutableAttrs(),
|
||||||
|
infoAttrs,
|
||||||
|
storePath,
|
||||||
|
true);
|
||||||
|
|
||||||
|
return makeResult(infoAttrs, std::move(storePath));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GitInputScheme : InputScheme
|
||||||
|
{
|
||||||
|
std::unique_ptr<Input> inputFromURL(const ParsedURL & url) override
|
||||||
|
{
|
||||||
|
if (url.scheme != "git" &&
|
||||||
|
url.scheme != "git+http" &&
|
||||||
|
url.scheme != "git+https" &&
|
||||||
|
url.scheme != "git+ssh" &&
|
||||||
|
url.scheme != "git+file") return nullptr;
|
||||||
|
|
||||||
|
auto url2(url);
|
||||||
|
if (hasPrefix(url2.scheme, "git+")) url2.scheme = std::string(url2.scheme, 4);
|
||||||
|
url2.query.clear();
|
||||||
|
|
||||||
|
Attrs attrs;
|
||||||
|
attrs.emplace("type", "git");
|
||||||
|
|
||||||
|
for (auto &[name, value] : url.query) {
|
||||||
|
if (name == "rev" || name == "ref")
|
||||||
|
attrs.emplace(name, value);
|
||||||
|
else
|
||||||
|
url2.query.emplace(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs.emplace("url", url2.to_string());
|
||||||
|
|
||||||
|
return inputFromAttrs(attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Input> inputFromAttrs(const Attrs & attrs) override
|
||||||
|
{
|
||||||
|
if (maybeGetStrAttr(attrs, "type") != "git") return {};
|
||||||
|
|
||||||
|
for (auto & [name, value] : attrs)
|
||||||
|
if (name != "type" && name != "url" && name != "ref" && name != "rev" && name != "shallow")
|
||||||
|
throw Error("unsupported Git input attribute '%s'", name);
|
||||||
|
|
||||||
|
auto input = std::make_unique<GitInput>(parseURL(getStrAttr(attrs, "url")));
|
||||||
|
if (auto ref = maybeGetStrAttr(attrs, "ref")) {
|
||||||
|
if (!std::regex_match(*ref, refRegex))
|
||||||
|
throw BadURL("invalid Git branch/tag name '%s'", *ref);
|
||||||
|
input->ref = *ref;
|
||||||
|
}
|
||||||
|
if (auto rev = maybeGetStrAttr(attrs, "rev"))
|
||||||
|
input->rev = Hash(*rev, htSHA1);
|
||||||
|
|
||||||
|
input->shallow = maybeGetBoolAttr(attrs, "shallow").value_or(false);
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static auto r1 = OnStartup([] { registerInputScheme(std::make_unique<GitInputScheme>()); });
|
||||||
|
|
||||||
|
}
|
195
src/libfetchers/github.cc
Normal file
195
src/libfetchers/github.cc
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
#include "download.hh"
|
||||||
|
#include "cache.hh"
|
||||||
|
#include "fetchers.hh"
|
||||||
|
#include "globals.hh"
|
||||||
|
#include "store-api.hh"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
namespace nix::fetchers {
|
||||||
|
|
||||||
|
std::regex ownerRegex("[a-zA-Z][a-zA-Z0-9_-]*", std::regex::ECMAScript);
|
||||||
|
std::regex repoRegex("[a-zA-Z][a-zA-Z0-9_-]*", std::regex::ECMAScript);
|
||||||
|
|
||||||
|
struct GitHubInput : Input
|
||||||
|
{
|
||||||
|
std::string owner;
|
||||||
|
std::string repo;
|
||||||
|
std::optional<std::string> ref;
|
||||||
|
std::optional<Hash> rev;
|
||||||
|
|
||||||
|
std::string type() const override { return "github"; }
|
||||||
|
|
||||||
|
bool operator ==(const Input & other) const override
|
||||||
|
{
|
||||||
|
auto other2 = dynamic_cast<const GitHubInput *>(&other);
|
||||||
|
return
|
||||||
|
other2
|
||||||
|
&& owner == other2->owner
|
||||||
|
&& repo == other2->repo
|
||||||
|
&& rev == other2->rev
|
||||||
|
&& ref == other2->ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isImmutable() const override
|
||||||
|
{
|
||||||
|
return (bool) rev;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> getRef() const override { return ref; }
|
||||||
|
|
||||||
|
std::optional<Hash> getRev() const override { return rev; }
|
||||||
|
|
||||||
|
ParsedURL toURL() const override
|
||||||
|
{
|
||||||
|
auto path = owner + "/" + repo;
|
||||||
|
assert(!(ref && rev));
|
||||||
|
if (ref) path += "/" + *ref;
|
||||||
|
if (rev) path += "/" + rev->to_string(Base16, false);
|
||||||
|
return ParsedURL {
|
||||||
|
.scheme = "github",
|
||||||
|
.path = path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Attrs toAttrsInternal() const override
|
||||||
|
{
|
||||||
|
Attrs attrs;
|
||||||
|
attrs.emplace("owner", owner);
|
||||||
|
attrs.emplace("repo", repo);
|
||||||
|
if (ref)
|
||||||
|
attrs.emplace("ref", *ref);
|
||||||
|
if (rev)
|
||||||
|
attrs.emplace("rev", rev->gitRev());
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<Tree, std::shared_ptr<const Input>> fetchTreeInternal(nix::ref<Store> store) const override
|
||||||
|
{
|
||||||
|
auto rev = this->rev;
|
||||||
|
auto ref = this->ref.value_or("master");
|
||||||
|
|
||||||
|
if (!rev) {
|
||||||
|
auto url = fmt("https://api.github.com/repos/%s/%s/commits/%s",
|
||||||
|
owner, repo, ref);
|
||||||
|
auto json = nlohmann::json::parse(
|
||||||
|
readFile(
|
||||||
|
store->toRealPath(
|
||||||
|
downloadFile(store, url, "source", false).storePath)));
|
||||||
|
rev = Hash(json["sha"], htSHA1);
|
||||||
|
debug("HEAD revision for '%s' is %s", url, rev->gitRev());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto input = std::make_shared<GitHubInput>(*this);
|
||||||
|
input->ref = {};
|
||||||
|
input->rev = *rev;
|
||||||
|
|
||||||
|
Attrs immutableAttrs({
|
||||||
|
{"type", "git-tarball"},
|
||||||
|
{"rev", rev->gitRev()},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (auto res = getCache()->lookup(store, immutableAttrs)) {
|
||||||
|
return {
|
||||||
|
Tree{
|
||||||
|
.actualPath = store->toRealPath(res->second),
|
||||||
|
.storePath = std::move(res->second),
|
||||||
|
.info = TreeInfo {
|
||||||
|
.lastModified = getIntAttr(res->first, "lastModified"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: use regular /archive URLs instead? api.github.com
|
||||||
|
// might have stricter rate limits.
|
||||||
|
|
||||||
|
auto url = fmt("https://api.github.com/repos/%s/%s/tarball/%s",
|
||||||
|
owner, repo, rev->to_string(Base16, false));
|
||||||
|
|
||||||
|
std::string accessToken = settings.githubAccessToken.get();
|
||||||
|
if (accessToken != "")
|
||||||
|
url += "?access_token=" + accessToken;
|
||||||
|
|
||||||
|
auto tree = downloadTarball(store, url, "source", true);
|
||||||
|
|
||||||
|
getCache()->add(
|
||||||
|
store,
|
||||||
|
immutableAttrs,
|
||||||
|
{
|
||||||
|
{"rev", rev->gitRev()},
|
||||||
|
{"lastModified", *tree.info.lastModified}
|
||||||
|
},
|
||||||
|
tree.storePath,
|
||||||
|
true);
|
||||||
|
|
||||||
|
return {std::move(tree), input};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GitHubInputScheme : InputScheme
|
||||||
|
{
|
||||||
|
std::unique_ptr<Input> inputFromURL(const ParsedURL & url) override
|
||||||
|
{
|
||||||
|
if (url.scheme != "github") return nullptr;
|
||||||
|
|
||||||
|
auto path = tokenizeString<std::vector<std::string>>(url.path, "/");
|
||||||
|
auto input = std::make_unique<GitHubInput>();
|
||||||
|
|
||||||
|
if (path.size() == 2) {
|
||||||
|
} else if (path.size() == 3) {
|
||||||
|
if (std::regex_match(path[2], revRegex))
|
||||||
|
input->rev = Hash(path[2], htSHA1);
|
||||||
|
else if (std::regex_match(path[2], refRegex))
|
||||||
|
input->ref = path[2];
|
||||||
|
else
|
||||||
|
throw BadURL("in GitHub URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[2]);
|
||||||
|
} else
|
||||||
|
throw BadURL("GitHub URL '%s' is invalid", url.url);
|
||||||
|
|
||||||
|
for (auto &[name, value] : url.query) {
|
||||||
|
if (name == "rev") {
|
||||||
|
if (input->rev)
|
||||||
|
throw BadURL("GitHub URL '%s' contains multiple commit hashes", url.url);
|
||||||
|
input->rev = Hash(value, htSHA1);
|
||||||
|
}
|
||||||
|
else if (name == "ref") {
|
||||||
|
if (!std::regex_match(value, refRegex))
|
||||||
|
throw BadURL("GitHub URL '%s' contains an invalid branch/tag name", url.url);
|
||||||
|
if (input->ref)
|
||||||
|
throw BadURL("GitHub URL '%s' contains multiple branch/tag names", url.url);
|
||||||
|
input->ref = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input->ref && input->rev)
|
||||||
|
throw BadURL("GitHub URL '%s' contains both a commit hash and a branch/tag name", url.url);
|
||||||
|
|
||||||
|
input->owner = path[0];
|
||||||
|
input->repo = path[1];
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Input> inputFromAttrs(const Attrs & attrs) override
|
||||||
|
{
|
||||||
|
if (maybeGetStrAttr(attrs, "type") != "github") return {};
|
||||||
|
|
||||||
|
for (auto & [name, value] : attrs)
|
||||||
|
if (name != "type" && name != "owner" && name != "repo" && name != "ref" && name != "rev")
|
||||||
|
throw Error("unsupported GitHub input attribute '%s'", name);
|
||||||
|
|
||||||
|
auto input = std::make_unique<GitHubInput>();
|
||||||
|
input->owner = getStrAttr(attrs, "owner");
|
||||||
|
input->repo = getStrAttr(attrs, "repo");
|
||||||
|
input->ref = maybeGetStrAttr(attrs, "ref");
|
||||||
|
if (auto rev = maybeGetStrAttr(attrs, "rev"))
|
||||||
|
input->rev = Hash(*rev, htSHA1);
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static auto r1 = OnStartup([] { registerInputScheme(std::make_unique<GitHubInputScheme>()); });
|
||||||
|
|
||||||
|
}
|
11
src/libfetchers/local.mk
Normal file
11
src/libfetchers/local.mk
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
libraries += libfetchers
|
||||||
|
|
||||||
|
libfetchers_NAME = libnixfetchers
|
||||||
|
|
||||||
|
libfetchers_DIR := $(d)
|
||||||
|
|
||||||
|
libfetchers_SOURCES := $(wildcard $(d)/*.cc)
|
||||||
|
|
||||||
|
libfetchers_CXXFLAGS += -I src/libutil -I src/libstore
|
||||||
|
|
||||||
|
libfetchers_LIBS = libutil libstore libnixrust
|
303
src/libfetchers/mercurial.cc
Normal file
303
src/libfetchers/mercurial.cc
Normal file
|
@ -0,0 +1,303 @@
|
||||||
|
#include "fetchers.hh"
|
||||||
|
#include "cache.hh"
|
||||||
|
#include "globals.hh"
|
||||||
|
#include "tarfile.hh"
|
||||||
|
#include "store-api.hh"
|
||||||
|
|
||||||
|
#include <sys/time.h>
|
||||||
|
|
||||||
|
using namespace std::string_literals;
|
||||||
|
|
||||||
|
namespace nix::fetchers {
|
||||||
|
|
||||||
|
struct MercurialInput : Input
|
||||||
|
{
|
||||||
|
ParsedURL url;
|
||||||
|
std::optional<std::string> ref;
|
||||||
|
std::optional<Hash> rev;
|
||||||
|
|
||||||
|
MercurialInput(const ParsedURL & url) : url(url)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
std::string type() const override { return "hg"; }
|
||||||
|
|
||||||
|
bool operator ==(const Input & other) const override
|
||||||
|
{
|
||||||
|
auto other2 = dynamic_cast<const MercurialInput *>(&other);
|
||||||
|
return
|
||||||
|
other2
|
||||||
|
&& url == other2->url
|
||||||
|
&& rev == other2->rev
|
||||||
|
&& ref == other2->ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isImmutable() const override
|
||||||
|
{
|
||||||
|
return (bool) rev;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> getRef() const override { return ref; }
|
||||||
|
|
||||||
|
std::optional<Hash> getRev() const override { return rev; }
|
||||||
|
|
||||||
|
ParsedURL toURL() const override
|
||||||
|
{
|
||||||
|
ParsedURL url2(url);
|
||||||
|
url2.scheme = "hg+" + url2.scheme;
|
||||||
|
if (rev) url2.query.insert_or_assign("rev", rev->gitRev());
|
||||||
|
if (ref) url2.query.insert_or_assign("ref", *ref);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
Attrs toAttrsInternal() const override
|
||||||
|
{
|
||||||
|
Attrs attrs;
|
||||||
|
attrs.emplace("url", url.to_string());
|
||||||
|
if (ref)
|
||||||
|
attrs.emplace("ref", *ref);
|
||||||
|
if (rev)
|
||||||
|
attrs.emplace("rev", rev->gitRev());
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<bool, std::string> getActualUrl() const
|
||||||
|
{
|
||||||
|
bool isLocal = url.scheme == "file";
|
||||||
|
return {isLocal, isLocal ? url.path : url.base};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<Tree, std::shared_ptr<const Input>> fetchTreeInternal(nix::ref<Store> store) const override
|
||||||
|
{
|
||||||
|
auto name = "source";
|
||||||
|
|
||||||
|
auto input = std::make_shared<MercurialInput>(*this);
|
||||||
|
|
||||||
|
auto [isLocal, actualUrl_] = getActualUrl();
|
||||||
|
auto actualUrl = actualUrl_; // work around clang bug
|
||||||
|
|
||||||
|
// FIXME: return lastModified.
|
||||||
|
|
||||||
|
// FIXME: don't clone local repositories.
|
||||||
|
|
||||||
|
if (!input->ref && !input->rev && isLocal && pathExists(actualUrl + "/.hg")) {
|
||||||
|
|
||||||
|
bool clean = runProgram("hg", true, { "status", "-R", actualUrl, "--modified", "--added", "--removed" }) == "";
|
||||||
|
|
||||||
|
if (!clean) {
|
||||||
|
|
||||||
|
/* This is an unclean working tree. So copy all tracked
|
||||||
|
files. */
|
||||||
|
|
||||||
|
if (!settings.allowDirty)
|
||||||
|
throw Error("Mercurial tree '%s' is unclean", actualUrl);
|
||||||
|
|
||||||
|
if (settings.warnDirty)
|
||||||
|
warn("Mercurial tree '%s' is unclean", actualUrl);
|
||||||
|
|
||||||
|
input->ref = chomp(runProgram("hg", true, { "branch", "-R", actualUrl }));
|
||||||
|
|
||||||
|
auto files = tokenizeString<std::set<std::string>>(
|
||||||
|
runProgram("hg", true, { "status", "-R", actualUrl, "--clean", "--modified", "--added", "--no-status", "--print0" }), "\0"s);
|
||||||
|
|
||||||
|
PathFilter filter = [&](const Path & p) -> bool {
|
||||||
|
assert(hasPrefix(p, actualUrl));
|
||||||
|
std::string file(p, actualUrl.size() + 1);
|
||||||
|
|
||||||
|
auto st = lstat(p);
|
||||||
|
|
||||||
|
if (S_ISDIR(st.st_mode)) {
|
||||||
|
auto prefix = file + "/";
|
||||||
|
auto i = files.lower_bound(prefix);
|
||||||
|
return i != files.end() && hasPrefix(*i, prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
return files.count(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto storePath = store->addToStore("source", actualUrl, true, htSHA256, filter);
|
||||||
|
|
||||||
|
return {Tree {
|
||||||
|
.actualPath = store->printStorePath(storePath),
|
||||||
|
.storePath = std::move(storePath),
|
||||||
|
}, input};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input->ref) input->ref = "default";
|
||||||
|
|
||||||
|
auto getImmutableAttrs = [&]()
|
||||||
|
{
|
||||||
|
return Attrs({
|
||||||
|
{"type", "hg"},
|
||||||
|
{"name", name},
|
||||||
|
{"rev", input->rev->gitRev()},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
auto makeResult = [&](const Attrs & infoAttrs, StorePath && storePath)
|
||||||
|
-> std::pair<Tree, std::shared_ptr<const Input>>
|
||||||
|
{
|
||||||
|
assert(input->rev);
|
||||||
|
assert(!rev || rev == input->rev);
|
||||||
|
return {
|
||||||
|
Tree{
|
||||||
|
.actualPath = store->toRealPath(storePath),
|
||||||
|
.storePath = std::move(storePath),
|
||||||
|
.info = TreeInfo {
|
||||||
|
.revCount = getIntAttr(infoAttrs, "revCount"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (input->rev) {
|
||||||
|
if (auto res = getCache()->lookup(store, getImmutableAttrs()))
|
||||||
|
return makeResult(res->first, std::move(res->second));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(input->rev || input->ref);
|
||||||
|
auto revOrRef = input->rev ? input->rev->gitRev() : *input->ref;
|
||||||
|
|
||||||
|
Attrs mutableAttrs({
|
||||||
|
{"type", "hg"},
|
||||||
|
{"name", name},
|
||||||
|
{"url", actualUrl},
|
||||||
|
{"ref", *input->ref},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (auto res = getCache()->lookup(store, mutableAttrs)) {
|
||||||
|
auto rev2 = Hash(getStrAttr(res->first, "rev"), htSHA1);
|
||||||
|
if (!rev || rev == rev2) {
|
||||||
|
input->rev = rev2;
|
||||||
|
return makeResult(res->first, std::move(res->second));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Path cacheDir = fmt("%s/nix/hg/%s", getCacheDir(), hashString(htSHA256, actualUrl).to_string(Base32, false));
|
||||||
|
|
||||||
|
/* If this is a commit hash that we already have, we don't
|
||||||
|
have to pull again. */
|
||||||
|
if (!(input->rev
|
||||||
|
&& pathExists(cacheDir)
|
||||||
|
&& runProgram(
|
||||||
|
RunOptions("hg", { "log", "-R", cacheDir, "-r", input->rev->gitRev(), "--template", "1" })
|
||||||
|
.killStderr(true)).second == "1"))
|
||||||
|
{
|
||||||
|
Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching Mercurial repository '%s'", actualUrl));
|
||||||
|
|
||||||
|
if (pathExists(cacheDir)) {
|
||||||
|
try {
|
||||||
|
runProgram("hg", true, { "pull", "-R", cacheDir, "--", actualUrl });
|
||||||
|
}
|
||||||
|
catch (ExecError & e) {
|
||||||
|
string transJournal = cacheDir + "/.hg/store/journal";
|
||||||
|
/* hg throws "abandoned transaction" error only if this file exists */
|
||||||
|
if (pathExists(transJournal)) {
|
||||||
|
runProgram("hg", true, { "recover", "-R", cacheDir });
|
||||||
|
runProgram("hg", true, { "pull", "-R", cacheDir, "--", actualUrl });
|
||||||
|
} else {
|
||||||
|
throw ExecError(e.status, fmt("'hg pull' %s", statusToString(e.status)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
createDirs(dirOf(cacheDir));
|
||||||
|
runProgram("hg", true, { "clone", "--noupdate", "--", actualUrl, cacheDir });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto tokens = tokenizeString<std::vector<std::string>>(
|
||||||
|
runProgram("hg", true, { "log", "-R", cacheDir, "-r", revOrRef, "--template", "{node} {rev} {branch}" }));
|
||||||
|
assert(tokens.size() == 3);
|
||||||
|
|
||||||
|
input->rev = Hash(tokens[0], htSHA1);
|
||||||
|
auto revCount = std::stoull(tokens[1]);
|
||||||
|
input->ref = tokens[2];
|
||||||
|
|
||||||
|
if (auto res = getCache()->lookup(store, getImmutableAttrs()))
|
||||||
|
return makeResult(res->first, std::move(res->second));
|
||||||
|
|
||||||
|
Path tmpDir = createTempDir();
|
||||||
|
AutoDelete delTmpDir(tmpDir, true);
|
||||||
|
|
||||||
|
runProgram("hg", true, { "archive", "-R", cacheDir, "-r", input->rev->gitRev(), tmpDir });
|
||||||
|
|
||||||
|
deletePath(tmpDir + "/.hg_archival.txt");
|
||||||
|
|
||||||
|
auto storePath = store->addToStore(name, tmpDir);
|
||||||
|
|
||||||
|
Attrs infoAttrs({
|
||||||
|
{"rev", input->rev->gitRev()},
|
||||||
|
{"revCount", (int64_t) revCount},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this->rev)
|
||||||
|
getCache()->add(
|
||||||
|
store,
|
||||||
|
mutableAttrs,
|
||||||
|
infoAttrs,
|
||||||
|
storePath,
|
||||||
|
false);
|
||||||
|
|
||||||
|
getCache()->add(
|
||||||
|
store,
|
||||||
|
getImmutableAttrs(),
|
||||||
|
infoAttrs,
|
||||||
|
storePath,
|
||||||
|
true);
|
||||||
|
|
||||||
|
return makeResult(infoAttrs, std::move(storePath));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MercurialInputScheme : InputScheme
|
||||||
|
{
|
||||||
|
std::unique_ptr<Input> inputFromURL(const ParsedURL & url) override
|
||||||
|
{
|
||||||
|
if (url.scheme != "hg+http" &&
|
||||||
|
url.scheme != "hg+https" &&
|
||||||
|
url.scheme != "hg+ssh" &&
|
||||||
|
url.scheme != "hg+file") return nullptr;
|
||||||
|
|
||||||
|
auto url2(url);
|
||||||
|
url2.scheme = std::string(url2.scheme, 3);
|
||||||
|
url2.query.clear();
|
||||||
|
|
||||||
|
Attrs attrs;
|
||||||
|
attrs.emplace("type", "hg");
|
||||||
|
|
||||||
|
for (auto &[name, value] : url.query) {
|
||||||
|
if (name == "rev" || name == "ref")
|
||||||
|
attrs.emplace(name, value);
|
||||||
|
else
|
||||||
|
url2.query.emplace(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs.emplace("url", url2.to_string());
|
||||||
|
|
||||||
|
return inputFromAttrs(attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Input> inputFromAttrs(const Attrs & attrs) override
|
||||||
|
{
|
||||||
|
if (maybeGetStrAttr(attrs, "type") != "hg") return {};
|
||||||
|
|
||||||
|
for (auto & [name, value] : attrs)
|
||||||
|
if (name != "type" && name != "url" && name != "ref" && name != "rev")
|
||||||
|
throw Error("unsupported Mercurial input attribute '%s'", name);
|
||||||
|
|
||||||
|
auto input = std::make_unique<MercurialInput>(parseURL(getStrAttr(attrs, "url")));
|
||||||
|
if (auto ref = maybeGetStrAttr(attrs, "ref")) {
|
||||||
|
if (!std::regex_match(*ref, refRegex))
|
||||||
|
throw BadURL("invalid Mercurial branch/tag name '%s'", *ref);
|
||||||
|
input->ref = *ref;
|
||||||
|
}
|
||||||
|
if (auto rev = maybeGetStrAttr(attrs, "rev"))
|
||||||
|
input->rev = Hash(*rev, htSHA1);
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static auto r1 = OnStartup([] { registerInputScheme(std::make_unique<MercurialInputScheme>()); });
|
||||||
|
|
||||||
|
}
|
277
src/libfetchers/tarball.cc
Normal file
277
src/libfetchers/tarball.cc
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
#include "fetchers.hh"
|
||||||
|
#include "cache.hh"
|
||||||
|
#include "download.hh"
|
||||||
|
#include "globals.hh"
|
||||||
|
#include "store-api.hh"
|
||||||
|
#include "archive.hh"
|
||||||
|
#include "tarfile.hh"
|
||||||
|
|
||||||
|
namespace nix::fetchers {
|
||||||
|
|
||||||
|
DownloadFileResult downloadFile(
|
||||||
|
ref<Store> store,
|
||||||
|
const std::string & url,
|
||||||
|
const std::string & name,
|
||||||
|
bool immutable)
|
||||||
|
{
|
||||||
|
// FIXME: check store
|
||||||
|
|
||||||
|
Attrs inAttrs({
|
||||||
|
{"type", "file"},
|
||||||
|
{"url", url},
|
||||||
|
{"name", name},
|
||||||
|
});
|
||||||
|
|
||||||
|
auto cached = getCache()->lookupExpired(store, inAttrs);
|
||||||
|
|
||||||
|
auto useCached = [&]() -> DownloadFileResult
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
.storePath = std::move(cached->storePath),
|
||||||
|
.etag = getStrAttr(cached->infoAttrs, "etag"),
|
||||||
|
.effectiveUrl = getStrAttr(cached->infoAttrs, "url")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cached && !cached->expired)
|
||||||
|
return useCached();
|
||||||
|
|
||||||
|
DownloadRequest request(url);
|
||||||
|
if (cached)
|
||||||
|
request.expectedETag = getStrAttr(cached->infoAttrs, "etag");
|
||||||
|
DownloadResult res;
|
||||||
|
try {
|
||||||
|
res = getDownloader()->download(request);
|
||||||
|
} catch (DownloadError & e) {
|
||||||
|
if (cached) {
|
||||||
|
warn("%s; using cached version", e.msg());
|
||||||
|
return useCached();
|
||||||
|
} else
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: write to temporary file.
|
||||||
|
|
||||||
|
Attrs infoAttrs({
|
||||||
|
{"etag", res.etag},
|
||||||
|
{"url", res.effectiveUri},
|
||||||
|
});
|
||||||
|
|
||||||
|
std::optional<StorePath> storePath;
|
||||||
|
|
||||||
|
if (res.cached) {
|
||||||
|
assert(cached);
|
||||||
|
assert(request.expectedETag == res.etag);
|
||||||
|
storePath = std::move(cached->storePath);
|
||||||
|
} else {
|
||||||
|
StringSink sink;
|
||||||
|
dumpString(*res.data, sink);
|
||||||
|
auto hash = hashString(htSHA256, *res.data);
|
||||||
|
ValidPathInfo info(store->makeFixedOutputPath(false, hash, name));
|
||||||
|
info.narHash = hashString(htSHA256, *sink.s);
|
||||||
|
info.narSize = sink.s->size();
|
||||||
|
info.ca = makeFixedOutputCA(false, hash);
|
||||||
|
store->addToStore(info, sink.s, NoRepair, NoCheckSigs);
|
||||||
|
storePath = std::move(info.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCache()->add(
|
||||||
|
store,
|
||||||
|
inAttrs,
|
||||||
|
infoAttrs,
|
||||||
|
*storePath,
|
||||||
|
immutable);
|
||||||
|
|
||||||
|
if (url != res.effectiveUri)
|
||||||
|
getCache()->add(
|
||||||
|
store,
|
||||||
|
{
|
||||||
|
{"type", "file"},
|
||||||
|
{"url", res.effectiveUri},
|
||||||
|
{"name", name},
|
||||||
|
},
|
||||||
|
infoAttrs,
|
||||||
|
*storePath,
|
||||||
|
immutable);
|
||||||
|
|
||||||
|
return {
|
||||||
|
.storePath = std::move(*storePath),
|
||||||
|
.etag = res.etag,
|
||||||
|
.effectiveUrl = res.effectiveUri,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Tree downloadTarball(
|
||||||
|
ref<Store> store,
|
||||||
|
const std::string & url,
|
||||||
|
const std::string & name,
|
||||||
|
bool immutable)
|
||||||
|
{
|
||||||
|
Attrs inAttrs({
|
||||||
|
{"type", "tarball"},
|
||||||
|
{"url", url},
|
||||||
|
{"name", name},
|
||||||
|
});
|
||||||
|
|
||||||
|
auto cached = getCache()->lookupExpired(store, inAttrs);
|
||||||
|
|
||||||
|
if (cached && !cached->expired)
|
||||||
|
return Tree {
|
||||||
|
.actualPath = store->toRealPath(cached->storePath),
|
||||||
|
.storePath = std::move(cached->storePath),
|
||||||
|
.info = TreeInfo {
|
||||||
|
.lastModified = getIntAttr(cached->infoAttrs, "lastModified"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
auto res = downloadFile(store, url, name, immutable);
|
||||||
|
|
||||||
|
std::optional<StorePath> unpackedStorePath;
|
||||||
|
time_t lastModified;
|
||||||
|
|
||||||
|
if (cached && res.etag != "" && getStrAttr(cached->infoAttrs, "etag") == res.etag) {
|
||||||
|
unpackedStorePath = std::move(cached->storePath);
|
||||||
|
lastModified = getIntAttr(cached->infoAttrs, "lastModified");
|
||||||
|
} else {
|
||||||
|
Path tmpDir = createTempDir();
|
||||||
|
AutoDelete autoDelete(tmpDir, true);
|
||||||
|
unpackTarfile(store->toRealPath(res.storePath), tmpDir);
|
||||||
|
auto members = readDirectory(tmpDir);
|
||||||
|
if (members.size() != 1)
|
||||||
|
throw nix::Error("tarball '%s' contains an unexpected number of top-level files", url);
|
||||||
|
auto topDir = tmpDir + "/" + members.begin()->name;
|
||||||
|
lastModified = lstat(topDir).st_mtime;
|
||||||
|
unpackedStorePath = store->addToStore(name, topDir, true, htSHA256, defaultPathFilter, NoRepair);
|
||||||
|
}
|
||||||
|
|
||||||
|
Attrs infoAttrs({
|
||||||
|
{"lastModified", lastModified},
|
||||||
|
{"etag", res.etag},
|
||||||
|
});
|
||||||
|
|
||||||
|
getCache()->add(
|
||||||
|
store,
|
||||||
|
inAttrs,
|
||||||
|
infoAttrs,
|
||||||
|
*unpackedStorePath,
|
||||||
|
immutable);
|
||||||
|
|
||||||
|
return Tree {
|
||||||
|
.actualPath = store->toRealPath(*unpackedStorePath),
|
||||||
|
.storePath = std::move(*unpackedStorePath),
|
||||||
|
.info = TreeInfo {
|
||||||
|
.lastModified = lastModified,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TarballInput : Input
|
||||||
|
{
|
||||||
|
ParsedURL url;
|
||||||
|
std::optional<Hash> hash;
|
||||||
|
|
||||||
|
TarballInput(const ParsedURL & url) : url(url)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
std::string type() const override { return "tarball"; }
|
||||||
|
|
||||||
|
bool operator ==(const Input & other) const override
|
||||||
|
{
|
||||||
|
auto other2 = dynamic_cast<const TarballInput *>(&other);
|
||||||
|
return
|
||||||
|
other2
|
||||||
|
&& to_string() == other2->to_string()
|
||||||
|
&& hash == other2->hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isImmutable() const override
|
||||||
|
{
|
||||||
|
return hash || narHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParsedURL toURL() const override
|
||||||
|
{
|
||||||
|
auto url2(url);
|
||||||
|
// NAR hashes are preferred over file hashes since tar/zip files
|
||||||
|
// don't have a canonical representation.
|
||||||
|
if (narHash)
|
||||||
|
url2.query.insert_or_assign("narHash", narHash->to_string(SRI));
|
||||||
|
else if (hash)
|
||||||
|
url2.query.insert_or_assign("hash", hash->to_string(SRI));
|
||||||
|
return url2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Attrs toAttrsInternal() const override
|
||||||
|
{
|
||||||
|
Attrs attrs;
|
||||||
|
attrs.emplace("url", url.to_string());
|
||||||
|
if (narHash)
|
||||||
|
attrs.emplace("narHash", narHash->to_string(SRI));
|
||||||
|
else if (hash)
|
||||||
|
attrs.emplace("hash", hash->to_string(SRI));
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<Tree, std::shared_ptr<const Input>> fetchTreeInternal(nix::ref<Store> store) const override
|
||||||
|
{
|
||||||
|
auto tree = downloadTarball(store, url.to_string(), "source", false);
|
||||||
|
|
||||||
|
auto input = std::make_shared<TarballInput>(*this);
|
||||||
|
input->narHash = store->queryPathInfo(tree.storePath)->narHash;
|
||||||
|
|
||||||
|
return {std::move(tree), input};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TarballInputScheme : InputScheme
|
||||||
|
{
|
||||||
|
std::unique_ptr<Input> inputFromURL(const ParsedURL & url) override
|
||||||
|
{
|
||||||
|
if (url.scheme != "file" && url.scheme != "http" && url.scheme != "https") return nullptr;
|
||||||
|
|
||||||
|
if (!hasSuffix(url.path, ".zip")
|
||||||
|
&& !hasSuffix(url.path, ".tar")
|
||||||
|
&& !hasSuffix(url.path, ".tar.gz")
|
||||||
|
&& !hasSuffix(url.path, ".tar.xz")
|
||||||
|
&& !hasSuffix(url.path, ".tar.bz2"))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
auto input = std::make_unique<TarballInput>(url);
|
||||||
|
|
||||||
|
auto hash = input->url.query.find("hash");
|
||||||
|
if (hash != input->url.query.end()) {
|
||||||
|
// FIXME: require SRI hash.
|
||||||
|
input->hash = Hash(hash->second);
|
||||||
|
input->url.query.erase(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto narHash = input->url.query.find("narHash");
|
||||||
|
if (narHash != input->url.query.end()) {
|
||||||
|
// FIXME: require SRI hash.
|
||||||
|
input->narHash = Hash(narHash->second);
|
||||||
|
input->url.query.erase(narHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Input> inputFromAttrs(const Attrs & attrs) override
|
||||||
|
{
|
||||||
|
if (maybeGetStrAttr(attrs, "type") != "tarball") return {};
|
||||||
|
|
||||||
|
for (auto & [name, value] : attrs)
|
||||||
|
if (name != "type" && name != "url" && name != "hash" && name != "narHash")
|
||||||
|
throw Error("unsupported tarball input attribute '%s'", name);
|
||||||
|
|
||||||
|
auto input = std::make_unique<TarballInput>(parseURL(getStrAttr(attrs, "url")));
|
||||||
|
if (auto hash = maybeGetStrAttr(attrs, "hash"))
|
||||||
|
// FIXME: require SRI hash.
|
||||||
|
input->hash = Hash(*hash);
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static auto r1 = OnStartup([] { registerInputScheme(std::make_unique<TarballInputScheme>()); });
|
||||||
|
|
||||||
|
}
|
14
src/libfetchers/tree-info.cc
Normal file
14
src/libfetchers/tree-info.cc
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#include "tree-info.hh"
|
||||||
|
#include "store-api.hh"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
namespace nix::fetchers {
|
||||||
|
|
||||||
|
StorePath TreeInfo::computeStorePath(Store & store) const
|
||||||
|
{
|
||||||
|
assert(narHash);
|
||||||
|
return store.makeFixedOutputPath(true, narHash, "source");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
29
src/libfetchers/tree-info.hh
Normal file
29
src/libfetchers/tree-info.hh
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "path.hh"
|
||||||
|
#include "hash.hh"
|
||||||
|
|
||||||
|
#include <nlohmann/json_fwd.hpp>
|
||||||
|
|
||||||
|
namespace nix { class Store; }
|
||||||
|
|
||||||
|
namespace nix::fetchers {
|
||||||
|
|
||||||
|
struct TreeInfo
|
||||||
|
{
|
||||||
|
Hash narHash;
|
||||||
|
std::optional<uint64_t> revCount;
|
||||||
|
std::optional<time_t> lastModified;
|
||||||
|
|
||||||
|
bool operator ==(const TreeInfo & other) const
|
||||||
|
{
|
||||||
|
return
|
||||||
|
narHash == other.narHash
|
||||||
|
&& revCount == other.revCount
|
||||||
|
&& lastModified == other.lastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
StorePath computeStorePath(Store & store) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -378,7 +378,7 @@ Hash hashDerivationModulo(Store & store, const Derivation & drv, bool maskOutput
|
||||||
if (h == drvHashes.end()) {
|
if (h == drvHashes.end()) {
|
||||||
assert(store.isValidPath(i.first));
|
assert(store.isValidPath(i.first));
|
||||||
h = drvHashes.insert_or_assign(i.first.clone(), hashDerivationModulo(store,
|
h = drvHashes.insert_or_assign(i.first.clone(), hashDerivationModulo(store,
|
||||||
readDerivation(store, store.toRealPath(store.printStorePath(i.first))), false)).first;
|
readDerivation(store, store.toRealPath(i.first)), false)).first;
|
||||||
}
|
}
|
||||||
inputs2.insert_or_assign(h->second.to_string(Base16, false), i.second);
|
inputs2.insert_or_assign(h->second.to_string(Base16, false), i.second);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,10 +35,6 @@ DownloadSettings downloadSettings;
|
||||||
|
|
||||||
static GlobalConfig::Register r1(&downloadSettings);
|
static GlobalConfig::Register r1(&downloadSettings);
|
||||||
|
|
||||||
CachedDownloadRequest::CachedDownloadRequest(const std::string & uri)
|
|
||||||
: uri(uri), ttl(settings.tarballTtl)
|
|
||||||
{ }
|
|
||||||
|
|
||||||
std::string resolveUri(const std::string & uri)
|
std::string resolveUri(const std::string & uri)
|
||||||
{
|
{
|
||||||
if (uri.compare(0, 8, "channel:") == 0)
|
if (uri.compare(0, 8, "channel:") == 0)
|
||||||
|
@ -802,139 +798,6 @@ void Downloader::download(DownloadRequest && request, Sink & sink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CachedDownloadResult Downloader::downloadCached(
|
|
||||||
ref<Store> store, const CachedDownloadRequest & request)
|
|
||||||
{
|
|
||||||
auto url = resolveUri(request.uri);
|
|
||||||
|
|
||||||
auto name = request.name;
|
|
||||||
if (name == "") {
|
|
||||||
auto p = url.rfind('/');
|
|
||||||
if (p != string::npos) name = string(url, p + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<StorePath> expectedStorePath;
|
|
||||||
if (request.expectedHash) {
|
|
||||||
expectedStorePath = store->makeFixedOutputPath(request.unpack, request.expectedHash, name);
|
|
||||||
if (store->isValidPath(*expectedStorePath)) {
|
|
||||||
CachedDownloadResult result;
|
|
||||||
result.storePath = store->printStorePath(*expectedStorePath);
|
|
||||||
result.path = store->toRealPath(result.storePath);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Path cacheDir = getCacheDir() + "/nix/tarballs";
|
|
||||||
createDirs(cacheDir);
|
|
||||||
|
|
||||||
string urlHash = hashString(htSHA256, name + std::string("\0"s) + url).to_string(Base32, false);
|
|
||||||
|
|
||||||
Path dataFile = cacheDir + "/" + urlHash + ".info";
|
|
||||||
Path fileLink = cacheDir + "/" + urlHash + "-file";
|
|
||||||
|
|
||||||
PathLocks lock({fileLink}, fmt("waiting for lock on '%1%'...", fileLink));
|
|
||||||
|
|
||||||
std::optional<StorePath> storePath;
|
|
||||||
|
|
||||||
string expectedETag;
|
|
||||||
|
|
||||||
bool skip = false;
|
|
||||||
|
|
||||||
CachedDownloadResult result;
|
|
||||||
|
|
||||||
if (pathExists(fileLink) && pathExists(dataFile)) {
|
|
||||||
storePath = store->parseStorePath(readLink(fileLink));
|
|
||||||
// FIXME
|
|
||||||
store->addTempRoot(*storePath);
|
|
||||||
if (store->isValidPath(*storePath)) {
|
|
||||||
auto ss = tokenizeString<vector<string>>(readFile(dataFile), "\n");
|
|
||||||
if (ss.size() >= 3 && ss[0] == url) {
|
|
||||||
time_t lastChecked;
|
|
||||||
if (string2Int(ss[2], lastChecked) && (uint64_t) lastChecked + request.ttl >= (uint64_t) time(0)) {
|
|
||||||
skip = true;
|
|
||||||
result.effectiveUri = request.uri;
|
|
||||||
result.etag = ss[1];
|
|
||||||
} else if (!ss[1].empty()) {
|
|
||||||
debug(format("verifying previous ETag '%1%'") % ss[1]);
|
|
||||||
expectedETag = ss[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
storePath.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skip) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
DownloadRequest request2(url);
|
|
||||||
request2.expectedETag = expectedETag;
|
|
||||||
auto res = download(request2);
|
|
||||||
result.effectiveUri = res.effectiveUri;
|
|
||||||
result.etag = res.etag;
|
|
||||||
|
|
||||||
if (!res.cached) {
|
|
||||||
StringSink sink;
|
|
||||||
dumpString(*res.data, sink);
|
|
||||||
Hash hash = hashString(request.expectedHash ? request.expectedHash.type : htSHA256, *res.data);
|
|
||||||
ValidPathInfo info(store->makeFixedOutputPath(false, hash, name));
|
|
||||||
info.narHash = hashString(htSHA256, *sink.s);
|
|
||||||
info.narSize = sink.s->size();
|
|
||||||
info.ca = makeFixedOutputCA(false, hash);
|
|
||||||
store->addToStore(info, sink.s, NoRepair, NoCheckSigs);
|
|
||||||
storePath = info.path.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(storePath);
|
|
||||||
replaceSymlink(store->printStorePath(*storePath), fileLink);
|
|
||||||
|
|
||||||
writeFile(dataFile, url + "\n" + res.etag + "\n" + std::to_string(time(0)) + "\n");
|
|
||||||
} catch (DownloadError & e) {
|
|
||||||
if (!storePath) throw;
|
|
||||||
warn("warning: %s; using cached result", e.msg());
|
|
||||||
result.etag = expectedETag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.unpack) {
|
|
||||||
Path unpackedLink = cacheDir + "/" + ((std::string) storePath->to_string()) + "-unpacked";
|
|
||||||
PathLocks lock2({unpackedLink}, fmt("waiting for lock on '%1%'...", unpackedLink));
|
|
||||||
std::optional<StorePath> unpackedStorePath;
|
|
||||||
if (pathExists(unpackedLink)) {
|
|
||||||
unpackedStorePath = store->parseStorePath(readLink(unpackedLink));
|
|
||||||
// FIXME
|
|
||||||
store->addTempRoot(*unpackedStorePath);
|
|
||||||
if (!store->isValidPath(*unpackedStorePath))
|
|
||||||
unpackedStorePath.reset();
|
|
||||||
}
|
|
||||||
if (!unpackedStorePath) {
|
|
||||||
printInfo("unpacking '%s'...", url);
|
|
||||||
Path tmpDir = createTempDir();
|
|
||||||
AutoDelete autoDelete(tmpDir, true);
|
|
||||||
unpackTarfile(store->toRealPath(store->printStorePath(*storePath)), tmpDir);
|
|
||||||
auto members = readDirectory(tmpDir);
|
|
||||||
if (members.size() != 1)
|
|
||||||
throw nix::Error("tarball '%s' contains an unexpected number of top-level files", url);
|
|
||||||
auto topDir = tmpDir + "/" + members.begin()->name;
|
|
||||||
unpackedStorePath = store->addToStore(name, topDir, true, htSHA256, defaultPathFilter, NoRepair);
|
|
||||||
}
|
|
||||||
replaceSymlink(store->printStorePath(*unpackedStorePath), unpackedLink);
|
|
||||||
storePath = std::move(*unpackedStorePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expectedStorePath && *storePath != *expectedStorePath) {
|
|
||||||
unsigned int statusCode = 102;
|
|
||||||
Hash gotHash = request.unpack
|
|
||||||
? hashPath(request.expectedHash.type, store->toRealPath(store->printStorePath(*storePath))).first
|
|
||||||
: hashFile(request.expectedHash.type, store->toRealPath(store->printStorePath(*storePath)));
|
|
||||||
throw nix::Error(statusCode, "hash mismatch in file downloaded from '%s':\n wanted: %s\n got: %s",
|
|
||||||
url, request.expectedHash.to_string(), gotHash.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
result.storePath = store->printStorePath(*storePath);
|
|
||||||
result.path = store->toRealPath(result.storePath);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
bool isUri(const string & s)
|
bool isUri(const string & s)
|
||||||
{
|
{
|
||||||
|
|
|
@ -65,28 +65,6 @@ struct DownloadResult
|
||||||
uint64_t bodySize = 0;
|
uint64_t bodySize = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CachedDownloadRequest
|
|
||||||
{
|
|
||||||
std::string uri;
|
|
||||||
bool unpack = false;
|
|
||||||
std::string name;
|
|
||||||
Hash expectedHash;
|
|
||||||
unsigned int ttl;
|
|
||||||
|
|
||||||
CachedDownloadRequest(const std::string & uri);
|
|
||||||
CachedDownloadRequest() = delete;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CachedDownloadResult
|
|
||||||
{
|
|
||||||
// Note: 'storePath' may be different from 'path' when using a
|
|
||||||
// chroot store.
|
|
||||||
Path storePath;
|
|
||||||
Path path;
|
|
||||||
std::optional<std::string> etag;
|
|
||||||
std::string effectiveUri;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Store;
|
class Store;
|
||||||
|
|
||||||
struct Downloader
|
struct Downloader
|
||||||
|
@ -108,12 +86,6 @@ struct Downloader
|
||||||
invoked on the thread of the caller. */
|
invoked on the thread of the caller. */
|
||||||
void download(DownloadRequest && request, Sink & sink);
|
void download(DownloadRequest && request, Sink & sink);
|
||||||
|
|
||||||
/* Check if the specified file is already in ~/.cache/nix/tarballs
|
|
||||||
and is more recent than ‘tarball-ttl’ seconds. Otherwise,
|
|
||||||
use the recorded ETag to verify if the server has a more
|
|
||||||
recent version, and if so, download it to the Nix store. */
|
|
||||||
CachedDownloadResult downloadCached(ref<Store> store, const CachedDownloadRequest & request);
|
|
||||||
|
|
||||||
enum Error { NotFound, Forbidden, Misc, Transient, Interrupted };
|
enum Error { NotFound, Forbidden, Misc, Transient, Interrupted };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -135,4 +107,7 @@ public:
|
||||||
|
|
||||||
bool isUri(const string & s);
|
bool isUri(const string & s);
|
||||||
|
|
||||||
|
/* Resolve deprecated 'channel:<foo>' URLs. */
|
||||||
|
std::string resolveUri(const std::string & uri);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -351,12 +351,21 @@ public:
|
||||||
Setting<Paths> pluginFiles{this, {}, "plugin-files",
|
Setting<Paths> pluginFiles{this, {}, "plugin-files",
|
||||||
"Plugins to dynamically load at nix initialization time."};
|
"Plugins to dynamically load at nix initialization time."};
|
||||||
|
|
||||||
|
Setting<std::string> githubAccessToken{this, "", "github-access-token",
|
||||||
|
"GitHub access token to get access to GitHub data through the GitHub API for github:<..> flakes."};
|
||||||
|
|
||||||
Setting<Strings> experimentalFeatures{this, {}, "experimental-features",
|
Setting<Strings> experimentalFeatures{this, {}, "experimental-features",
|
||||||
"Experimental Nix features to enable."};
|
"Experimental Nix features to enable."};
|
||||||
|
|
||||||
bool isExperimentalFeatureEnabled(const std::string & name);
|
bool isExperimentalFeatureEnabled(const std::string & name);
|
||||||
|
|
||||||
void requireExperimentalFeature(const std::string & name);
|
void requireExperimentalFeature(const std::string & name);
|
||||||
|
|
||||||
|
Setting<bool> allowDirty{this, true, "allow-dirty",
|
||||||
|
"Whether to allow dirty Git/Mercurial trees."};
|
||||||
|
|
||||||
|
Setting<bool> warnDirty{this, true, "warn-dirty",
|
||||||
|
"Whether to warn about dirty Git/Mercurial trees."};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#include "thread-pool.hh"
|
#include "thread-pool.hh"
|
||||||
#include "json.hh"
|
#include "json.hh"
|
||||||
#include "derivations.hh"
|
#include "derivations.hh"
|
||||||
|
#include "url.hh"
|
||||||
|
|
||||||
#include <future>
|
#include <future>
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ Path Store::followLinksToStore(std::string_view _path) const
|
||||||
path = absPath(target, dirOf(path));
|
path = absPath(target, dirOf(path));
|
||||||
}
|
}
|
||||||
if (!isInStore(path))
|
if (!isInStore(path))
|
||||||
throw Error(format("path '%1%' is not in the Nix store") % path);
|
throw NotInStore("path '%1%' is not in the Nix store", path);
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -866,27 +867,7 @@ std::pair<std::string, Store::Params> splitUriAndParams(const std::string & uri_
|
||||||
Store::Params params;
|
Store::Params params;
|
||||||
auto q = uri.find('?');
|
auto q = uri.find('?');
|
||||||
if (q != std::string::npos) {
|
if (q != std::string::npos) {
|
||||||
for (auto s : tokenizeString<Strings>(uri.substr(q + 1), "&")) {
|
params = decodeQuery(uri.substr(q + 1));
|
||||||
auto e = s.find('=');
|
|
||||||
if (e != std::string::npos) {
|
|
||||||
auto value = s.substr(e + 1);
|
|
||||||
std::string decoded;
|
|
||||||
for (size_t i = 0; i < value.size(); ) {
|
|
||||||
if (value[i] == '%') {
|
|
||||||
if (i + 2 >= value.size())
|
|
||||||
throw Error("invalid URI parameter '%s'", value);
|
|
||||||
try {
|
|
||||||
decoded += std::stoul(std::string(value, i + 1, 2), 0, 16);
|
|
||||||
i += 3;
|
|
||||||
} catch (...) {
|
|
||||||
throw Error("invalid URI parameter '%s'", value);
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
decoded += value[i++];
|
|
||||||
}
|
|
||||||
params[s.substr(0, e)] = decoded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uri = uri_.substr(0, q);
|
uri = uri_.substr(0, q);
|
||||||
}
|
}
|
||||||
return {uri, params};
|
return {uri, params};
|
||||||
|
|
|
@ -28,6 +28,7 @@ MakeError(InvalidPath, Error);
|
||||||
MakeError(Unsupported, Error);
|
MakeError(Unsupported, Error);
|
||||||
MakeError(SubstituteGone, Error);
|
MakeError(SubstituteGone, Error);
|
||||||
MakeError(SubstituterDisabled, Error);
|
MakeError(SubstituterDisabled, Error);
|
||||||
|
MakeError(NotInStore, Error);
|
||||||
|
|
||||||
|
|
||||||
struct BasicDerivation;
|
struct BasicDerivation;
|
||||||
|
|
|
@ -157,4 +157,12 @@ typedef list<Path> Paths;
|
||||||
typedef set<Path> PathSet;
|
typedef set<Path> PathSet;
|
||||||
|
|
||||||
|
|
||||||
|
/* Helper class to run code at startup. */
|
||||||
|
template<typename T>
|
||||||
|
struct OnStartup
|
||||||
|
{
|
||||||
|
OnStartup(T && t) { t(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
137
src/libutil/url.cc
Normal file
137
src/libutil/url.cc
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
#include "url.hh"
|
||||||
|
#include "util.hh"
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
std::regex refRegex(refRegexS, std::regex::ECMAScript);
|
||||||
|
std::regex revRegex(revRegexS, std::regex::ECMAScript);
|
||||||
|
std::regex flakeIdRegex(flakeIdRegexS, std::regex::ECMAScript);
|
||||||
|
|
||||||
|
ParsedURL parseURL(const std::string & url)
|
||||||
|
{
|
||||||
|
static std::regex uriRegex(
|
||||||
|
"((" + schemeRegex + "):"
|
||||||
|
+ "(?:(?://(" + authorityRegex + ")(" + absPathRegex + "))|(/?" + pathRegex + ")))"
|
||||||
|
+ "(?:\\?(" + queryRegex + "))?"
|
||||||
|
+ "(?:#(" + queryRegex + "))?",
|
||||||
|
std::regex::ECMAScript);
|
||||||
|
|
||||||
|
std::smatch match;
|
||||||
|
|
||||||
|
if (std::regex_match(url, match, uriRegex)) {
|
||||||
|
auto & base = match[1];
|
||||||
|
std::string scheme = match[2];
|
||||||
|
auto authority = match[3].matched
|
||||||
|
? std::optional<std::string>(match[3]) : std::nullopt;
|
||||||
|
std::string path = match[4].matched ? match[4] : match[5];
|
||||||
|
auto & query = match[6];
|
||||||
|
auto & fragment = match[7];
|
||||||
|
|
||||||
|
auto isFile = scheme.find("file") != std::string::npos;
|
||||||
|
|
||||||
|
if (authority && *authority != "" && isFile)
|
||||||
|
throw Error("file:// URL '%s' has unexpected authority '%s'",
|
||||||
|
url, *authority);
|
||||||
|
|
||||||
|
if (isFile && path.empty())
|
||||||
|
path = "/";
|
||||||
|
|
||||||
|
return ParsedURL{
|
||||||
|
.url = url,
|
||||||
|
.base = base,
|
||||||
|
.scheme = scheme,
|
||||||
|
.authority = authority,
|
||||||
|
.path = path,
|
||||||
|
.query = decodeQuery(query),
|
||||||
|
.fragment = percentDecode(std::string(fragment))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
throw BadURL("'%s' is not a valid URL", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string percentDecode(std::string_view in)
|
||||||
|
{
|
||||||
|
std::string decoded;
|
||||||
|
for (size_t i = 0; i < in.size(); ) {
|
||||||
|
if (in[i] == '%') {
|
||||||
|
if (i + 2 >= in.size())
|
||||||
|
throw BadURL("invalid URI parameter '%s'", in);
|
||||||
|
try {
|
||||||
|
decoded += std::stoul(std::string(in, i + 1, 2), 0, 16);
|
||||||
|
i += 3;
|
||||||
|
} catch (...) {
|
||||||
|
throw BadURL("invalid URI parameter '%s'", in);
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
decoded += in[i++];
|
||||||
|
}
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::string, std::string> decodeQuery(const std::string & query)
|
||||||
|
{
|
||||||
|
std::map<std::string, std::string> result;
|
||||||
|
|
||||||
|
for (auto s : tokenizeString<Strings>(query, "&")) {
|
||||||
|
auto e = s.find('=');
|
||||||
|
if (e != std::string::npos)
|
||||||
|
result.emplace(
|
||||||
|
s.substr(0, e),
|
||||||
|
percentDecode(std::string_view(s).substr(e + 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string percentEncode(std::string_view s)
|
||||||
|
{
|
||||||
|
std::string res;
|
||||||
|
for (auto & c : s)
|
||||||
|
if ((c >= 'a' && c <= 'z')
|
||||||
|
|| (c >= 'A' && c <= 'Z')
|
||||||
|
|| (c >= '0' && c <= '9')
|
||||||
|
|| strchr("-._~!$&'()*+,;=:@", c))
|
||||||
|
res += c;
|
||||||
|
else
|
||||||
|
res += fmt("%%%02x", (unsigned int) c);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string encodeQuery(const std::map<std::string, std::string> & ss)
|
||||||
|
{
|
||||||
|
std::string res;
|
||||||
|
bool first = true;
|
||||||
|
for (auto & [name, value] : ss) {
|
||||||
|
if (!first) res += '&';
|
||||||
|
first = false;
|
||||||
|
res += percentEncode(name);
|
||||||
|
res += '=';
|
||||||
|
res += percentEncode(value);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ParsedURL::to_string() const
|
||||||
|
{
|
||||||
|
return
|
||||||
|
scheme
|
||||||
|
+ ":"
|
||||||
|
+ (authority ? "//" + *authority : "")
|
||||||
|
+ path
|
||||||
|
+ (query.empty() ? "" : "?" + encodeQuery(query))
|
||||||
|
+ (fragment.empty() ? "" : "#" + percentEncode(fragment));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ParsedURL::operator ==(const ParsedURL & other) const
|
||||||
|
{
|
||||||
|
return
|
||||||
|
scheme == other.scheme
|
||||||
|
&& authority == other.authority
|
||||||
|
&& path == other.path
|
||||||
|
&& query == other.query
|
||||||
|
&& fragment == other.fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
62
src/libutil/url.hh
Normal file
62
src/libutil/url.hh
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "types.hh"
|
||||||
|
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
struct ParsedURL
|
||||||
|
{
|
||||||
|
std::string url;
|
||||||
|
std::string base; // URL without query/fragment
|
||||||
|
std::string scheme;
|
||||||
|
std::optional<std::string> authority;
|
||||||
|
std::string path;
|
||||||
|
std::map<std::string, std::string> query;
|
||||||
|
std::string fragment;
|
||||||
|
|
||||||
|
std::string to_string() const;
|
||||||
|
|
||||||
|
bool operator ==(const ParsedURL & other) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
MakeError(BadURL, Error);
|
||||||
|
|
||||||
|
std::string percentDecode(std::string_view in);
|
||||||
|
|
||||||
|
std::map<std::string, std::string> decodeQuery(const std::string & query);
|
||||||
|
|
||||||
|
ParsedURL parseURL(const std::string & url);
|
||||||
|
|
||||||
|
// URI stuff.
|
||||||
|
const static std::string pctEncoded = "(?:%[0-9a-fA-F][0-9a-fA-F])";
|
||||||
|
const static std::string schemeRegex = "(?:[a-z+]+)";
|
||||||
|
const static std::string ipv6AddressRegex = "(?:\\[[0-9a-fA-F:]+\\])";
|
||||||
|
const static std::string unreservedRegex = "(?:[a-zA-Z0-9-._~])";
|
||||||
|
const static std::string subdelimsRegex = "(?:[!$&'\"()*+,;=])";
|
||||||
|
const static std::string hostnameRegex = "(?:(?:" + unreservedRegex + "|" + pctEncoded + "|" + subdelimsRegex + ")*)";
|
||||||
|
const static std::string hostRegex = "(?:" + ipv6AddressRegex + "|" + hostnameRegex + ")";
|
||||||
|
const static std::string userRegex = "(?:(?:" + unreservedRegex + "|" + pctEncoded + "|" + subdelimsRegex + "|:)*)";
|
||||||
|
const static std::string authorityRegex = "(?:" + userRegex + "@)?" + hostRegex + "(?::[0-9]+)?";
|
||||||
|
const static std::string pcharRegex = "(?:" + unreservedRegex + "|" + pctEncoded + "|" + subdelimsRegex + "|[:@])";
|
||||||
|
const static std::string queryRegex = "(?:" + pcharRegex + "|[/? \"])*";
|
||||||
|
const static std::string segmentRegex = "(?:" + pcharRegex + "+)";
|
||||||
|
const static std::string absPathRegex = "(?:(?:/" + segmentRegex + ")*/?)";
|
||||||
|
const static std::string pathRegex = "(?:" + segmentRegex + "(?:/" + segmentRegex + ")*/?)";
|
||||||
|
|
||||||
|
// A Git ref (i.e. branch or tag name).
|
||||||
|
const static std::string refRegexS = "[a-zA-Z0-9][a-zA-Z0-9_.-]*"; // FIXME: check
|
||||||
|
extern std::regex refRegex;
|
||||||
|
|
||||||
|
// A Git revision (a SHA-1 commit hash).
|
||||||
|
const static std::string revRegexS = "[0-9a-fA-F]{40}";
|
||||||
|
extern std::regex revRegex;
|
||||||
|
|
||||||
|
// A ref or revision, or a ref followed by a revision.
|
||||||
|
const static std::string refAndOrRevRegex = "(?:(" + revRegexS + ")|(?:(" + refRegexS + ")(?:/(" + revRegexS + "))?))";
|
||||||
|
|
||||||
|
const static std::string flakeIdRegexS = "[a-zA-Z][a-zA-Z0-9_-]*";
|
||||||
|
extern std::regex flakeIdRegex;
|
||||||
|
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
#include "download.hh"
|
#include "download.hh"
|
||||||
#include "store-api.hh"
|
#include "store-api.hh"
|
||||||
#include "../nix/legacy.hh"
|
#include "../nix/legacy.hh"
|
||||||
|
#include "fetchers.hh"
|
||||||
|
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
@ -86,12 +87,9 @@ static void update(const StringSet & channelNames)
|
||||||
// We want to download the url to a file to see if it's a tarball while also checking if we
|
// We want to download the url to a file to see if it's a tarball while also checking if we
|
||||||
// got redirected in the process, so that we can grab the various parts of a nix channel
|
// got redirected in the process, so that we can grab the various parts of a nix channel
|
||||||
// definition from a consistent location if the redirect changes mid-download.
|
// definition from a consistent location if the redirect changes mid-download.
|
||||||
CachedDownloadRequest request(url);
|
auto result = fetchers::downloadFile(store, url, std::string(baseNameOf(url)), false);
|
||||||
request.ttl = 0;
|
auto filename = store->toRealPath(result.storePath);
|
||||||
auto dl = getDownloader();
|
url = result.effectiveUrl;
|
||||||
auto result = dl->downloadCached(store, request);
|
|
||||||
auto filename = result.path;
|
|
||||||
url = chomp(result.effectiveUri);
|
|
||||||
|
|
||||||
// If the URL contains a version number, append it to the name
|
// If the URL contains a version number, append it to the name
|
||||||
// attribute (so that "nix-env -q" on the channels profile
|
// attribute (so that "nix-env -q" on the channels profile
|
||||||
|
@ -114,11 +112,10 @@ static void update(const StringSet & channelNames)
|
||||||
if (!unpacked) {
|
if (!unpacked) {
|
||||||
// Download the channel tarball.
|
// Download the channel tarball.
|
||||||
try {
|
try {
|
||||||
filename = dl->downloadCached(store, CachedDownloadRequest(url + "/nixexprs.tar.xz")).path;
|
filename = store->toRealPath(fetchers::downloadFile(store, url + "/nixexprs.tar.xz", "nixexprs.tar.xz", false).storePath);
|
||||||
} catch (DownloadError & e) {
|
} catch (DownloadError & e) {
|
||||||
filename = dl->downloadCached(store, CachedDownloadRequest(url + "/nixexprs.tar.bz2")).path;
|
filename = store->toRealPath(fetchers::downloadFile(store, url + "/nixexprs.tar.bz2", "nixexprs.tar.bz2", false).storePath);
|
||||||
}
|
}
|
||||||
chomp(filename);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regardless of where it came from, add the expression representing this channel to accumulated expression
|
// Regardless of where it came from, add the expression representing this channel to accumulated expression
|
||||||
|
@ -185,6 +182,8 @@ static int _main(int argc, char ** argv)
|
||||||
} else if (*arg == "--rollback") {
|
} else if (*arg == "--rollback") {
|
||||||
cmd = cRollback;
|
cmd = cRollback;
|
||||||
} else {
|
} else {
|
||||||
|
if (hasPrefix(*arg, "-"))
|
||||||
|
throw UsageError("unsupported argument '%s'", *arg);
|
||||||
args.push_back(std::move(*arg));
|
args.push_back(std::move(*arg));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -15,9 +15,9 @@ nix_SOURCES := \
|
||||||
$(wildcard src/nix-prefetch-url/*.cc) \
|
$(wildcard src/nix-prefetch-url/*.cc) \
|
||||||
$(wildcard src/nix-store/*.cc) \
|
$(wildcard src/nix-store/*.cc) \
|
||||||
|
|
||||||
nix_CXXFLAGS += -I src/libutil -I src/libstore -I src/libexpr -I src/libmain
|
nix_CXXFLAGS += -I src/libutil -I src/libstore -I src/libfetchers -I src/libexpr -I src/libmain
|
||||||
|
|
||||||
nix_LIBS = libexpr libmain libstore libutil libnixrust
|
nix_LIBS = libexpr libmain libfetchers libstore libutil libnixrust
|
||||||
|
|
||||||
nix_LDFLAGS = -pthread $(SODIUM_LIBS) $(EDITLINE_LIBS) $(BOOST_LDFLAGS) -lboost_context -lboost_thread -lboost_system
|
nix_LDFLAGS = -pthread $(SODIUM_LIBS) $(EDITLINE_LIBS) $(BOOST_LDFLAGS) -lboost_context -lboost_thread -lboost_system
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ repo=$TEST_ROOT/git
|
||||||
|
|
||||||
export _NIX_FORCE_HTTP=1
|
export _NIX_FORCE_HTTP=1
|
||||||
|
|
||||||
rm -rf $repo ${repo}-tmp $TEST_HOME/.cache/nix/gitv2
|
rm -rf $repo ${repo}-tmp $TEST_HOME/.cache/nix $TEST_ROOT/worktree $TEST_ROOT/shallow
|
||||||
|
|
||||||
git init $repo
|
git init $repo
|
||||||
git -C $repo config user.email "foobar@example.com"
|
git -C $repo config user.email "foobar@example.com"
|
||||||
|
@ -25,8 +25,16 @@ rev1=$(git -C $repo rev-parse HEAD)
|
||||||
|
|
||||||
echo world > $repo/hello
|
echo world > $repo/hello
|
||||||
git -C $repo commit -m 'Bla2' -a
|
git -C $repo commit -m 'Bla2' -a
|
||||||
|
git -C $repo worktree add $TEST_ROOT/worktree
|
||||||
|
echo hello >> $TEST_ROOT/worktree/hello
|
||||||
rev2=$(git -C $repo rev-parse HEAD)
|
rev2=$(git -C $repo rev-parse HEAD)
|
||||||
|
|
||||||
|
# Fetch a worktree
|
||||||
|
unset _NIX_FORCE_HTTP
|
||||||
|
path0=$(nix eval --raw "(builtins.fetchGit file://$TEST_ROOT/worktree).outPath")
|
||||||
|
export _NIX_FORCE_HTTP=1
|
||||||
|
[[ $(tail -n 1 $path0/hello) = "hello" ]]
|
||||||
|
|
||||||
# Fetch the default branch.
|
# Fetch the default branch.
|
||||||
path=$(nix eval --raw "(builtins.fetchGit file://$repo).outPath")
|
path=$(nix eval --raw "(builtins.fetchGit file://$repo).outPath")
|
||||||
[[ $(cat $path/hello) = world ]]
|
[[ $(cat $path/hello) = world ]]
|
||||||
|
@ -50,9 +58,6 @@ path2=$(nix eval --raw "(builtins.fetchGit file://$repo).outPath")
|
||||||
[[ $(nix eval "(builtins.fetchGit file://$repo).revCount") = 2 ]]
|
[[ $(nix eval "(builtins.fetchGit file://$repo).revCount") = 2 ]]
|
||||||
[[ $(nix eval --raw "(builtins.fetchGit file://$repo).rev") = $rev2 ]]
|
[[ $(nix eval --raw "(builtins.fetchGit file://$repo).rev") = $rev2 ]]
|
||||||
|
|
||||||
# But with TTL 0, it should fail.
|
|
||||||
(! nix eval --tarball-ttl 0 "(builtins.fetchGit file://$repo)" -vvvvv)
|
|
||||||
|
|
||||||
# Fetching with a explicit hash should succeed.
|
# Fetching with a explicit hash should succeed.
|
||||||
path2=$(nix eval --tarball-ttl 0 --raw "(builtins.fetchGit { url = file://$repo; rev = \"$rev2\"; }).outPath")
|
path2=$(nix eval --tarball-ttl 0 --raw "(builtins.fetchGit { url = file://$repo; rev = \"$rev2\"; }).outPath")
|
||||||
[[ $path = $path2 ]]
|
[[ $path = $path2 ]]
|
||||||
|
@ -74,6 +79,7 @@ echo bar > $repo/dir2/bar
|
||||||
git -C $repo add dir1/foo
|
git -C $repo add dir1/foo
|
||||||
git -C $repo rm hello
|
git -C $repo rm hello
|
||||||
|
|
||||||
|
unset _NIX_FORCE_HTTP
|
||||||
path2=$(nix eval --raw "(builtins.fetchGit $repo).outPath")
|
path2=$(nix eval --raw "(builtins.fetchGit $repo).outPath")
|
||||||
[ ! -e $path2/hello ]
|
[ ! -e $path2/hello ]
|
||||||
[ ! -e $path2/bar ]
|
[ ! -e $path2/bar ]
|
||||||
|
@ -110,9 +116,9 @@ path=$(nix eval --raw "(builtins.fetchGit file://$repo).outPath")
|
||||||
git -C $repo checkout $rev2 -b dev
|
git -C $repo checkout $rev2 -b dev
|
||||||
echo dev > $repo/hello
|
echo dev > $repo/hello
|
||||||
|
|
||||||
# File URI uses 'master' unless specified otherwise
|
# File URI uses dirty tree unless specified otherwise
|
||||||
path2=$(nix eval --raw "(builtins.fetchGit file://$repo).outPath")
|
path2=$(nix eval --raw "(builtins.fetchGit file://$repo).outPath")
|
||||||
[[ $path = $path2 ]]
|
[ $(cat $path2/hello) = dev ]
|
||||||
|
|
||||||
# Using local path with branch other than 'master' should work when clean or dirty
|
# Using local path with branch other than 'master' should work when clean or dirty
|
||||||
path3=$(nix eval --raw "(builtins.fetchGit $repo).outPath")
|
path3=$(nix eval --raw "(builtins.fetchGit $repo).outPath")
|
||||||
|
@ -131,9 +137,9 @@ path5=$(nix eval --raw "(builtins.fetchGit { url = $repo; ref = \"dev\"; }).outP
|
||||||
|
|
||||||
|
|
||||||
# Nuke the cache
|
# Nuke the cache
|
||||||
rm -rf $TEST_HOME/.cache/nix/gitv2
|
rm -rf $TEST_HOME/.cache/nix
|
||||||
|
|
||||||
# Try again, but without 'git' on PATH
|
# Try again, but without 'git' on PATH. This should fail.
|
||||||
NIX=$(command -v nix)
|
NIX=$(command -v nix)
|
||||||
# This should fail
|
# This should fail
|
||||||
(! PATH= $NIX eval --raw "(builtins.fetchGit { url = $repo; ref = \"dev\"; }).outPath" )
|
(! PATH= $NIX eval --raw "(builtins.fetchGit { url = $repo; ref = \"dev\"; }).outPath" )
|
||||||
|
@ -141,3 +147,13 @@ NIX=$(command -v nix)
|
||||||
# Try again, with 'git' available. This should work.
|
# Try again, with 'git' available. This should work.
|
||||||
path5=$(nix eval --raw "(builtins.fetchGit { url = $repo; ref = \"dev\"; }).outPath")
|
path5=$(nix eval --raw "(builtins.fetchGit { url = $repo; ref = \"dev\"; }).outPath")
|
||||||
[[ $path3 = $path5 ]]
|
[[ $path3 = $path5 ]]
|
||||||
|
|
||||||
|
# Fetching a shallow repo shouldn't work by default, because we can't
|
||||||
|
# return a revCount.
|
||||||
|
git clone --depth 1 file://$repo $TEST_ROOT/shallow
|
||||||
|
(! nix eval --raw "(builtins.fetchGit { url = $TEST_ROOT/shallow; ref = \"dev\"; }).outPath")
|
||||||
|
|
||||||
|
# But you can request a shallow clone, which won't return a revCount.
|
||||||
|
path6=$(nix eval --raw "(builtins.fetchTree { type = \"git\"; url = \"file://$TEST_ROOT/shallow\"; ref = \"dev\"; shallow = true; }).outPath")
|
||||||
|
[[ $path3 = $path6 ]]
|
||||||
|
[[ $(nix eval "(builtins.fetchTree { type = \"git\"; url = \"file://$TEST_ROOT/shallow\"; ref = \"dev\"; shallow = true; }).revCount or 123") == 123 ]]
|
||||||
|
|
|
@ -17,7 +17,7 @@ cat > "$NIX_CONF_DIR"/nix.conf <<EOF
|
||||||
build-users-group =
|
build-users-group =
|
||||||
keep-derivations = false
|
keep-derivations = false
|
||||||
sandbox = false
|
sandbox = false
|
||||||
experimental-features = nix-command
|
experimental-features = nix-command flakes
|
||||||
include nix.conf.extra
|
include nix.conf.extra
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ mkdir -p $tarroot
|
||||||
cp dependencies.nix $tarroot/default.nix
|
cp dependencies.nix $tarroot/default.nix
|
||||||
cp config.nix dependencies.builder*.sh $tarroot/
|
cp config.nix dependencies.builder*.sh $tarroot/
|
||||||
|
|
||||||
|
hash=$(nix hash-path $tarroot)
|
||||||
|
|
||||||
test_tarball() {
|
test_tarball() {
|
||||||
local ext="$1"
|
local ext="$1"
|
||||||
local compressor="$2"
|
local compressor="$2"
|
||||||
|
@ -25,6 +27,11 @@ test_tarball() {
|
||||||
|
|
||||||
nix-build -o $TEST_ROOT/result -E "import (fetchTarball file://$tarball)"
|
nix-build -o $TEST_ROOT/result -E "import (fetchTarball file://$tarball)"
|
||||||
|
|
||||||
|
nix-build --experimental-features flakes -o $TEST_ROOT/result -E "import (fetchTree file://$tarball)"
|
||||||
|
nix-build --experimental-features flakes -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; })"
|
||||||
|
nix-build --experimental-features flakes -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"$hash\"; })"
|
||||||
|
nix-build --experimental-features flakes -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"sha256-xdKv2pq/IiwLSnBBJXW8hNowI4MrdZfW+SYqDQs7Tzc=\"; })" 2>&1 | grep 'NAR hash mismatch in input'
|
||||||
|
|
||||||
nix-instantiate --eval -E '1 + 2' -I fnord=file://no-such-tarball.tar$ext
|
nix-instantiate --eval -E '1 + 2' -I fnord=file://no-such-tarball.tar$ext
|
||||||
nix-instantiate --eval -E 'with <fnord/xyzzy>; 1 + 2' -I fnord=file://no-such-tarball$ext
|
nix-instantiate --eval -E 'with <fnord/xyzzy>; 1 + 2' -I fnord=file://no-such-tarball$ext
|
||||||
(! nix-instantiate --eval -E '<fnord/xyzzy> 1' -I fnord=file://no-such-tarball$ext)
|
(! nix-instantiate --eval -E '<fnord/xyzzy> 1' -I fnord=file://no-such-tarball$ext)
|
||||||
|
|
Loading…
Reference in a new issue