Remove TreeInfo

The attributes previously stored in TreeInfo (narHash, revCount,
lastModified) are now stored in Input. This makes it less arbitrary
what attributes are stored where.

As a result, the lock file format has changed. An entry like

    "info": {
      "lastModified": 1585405475,
      "narHash": "sha256-bESW0n4KgPmZ0luxvwJ+UyATrC6iIltVCsGdLiphVeE="
    },
    "locked": {
      "owner": "NixOS",
      "repo": "nixpkgs",
      "rev": "b88ff468e9850410070d4e0ccd68c7011f15b2be",
      "type": "github"
    },

is now stored as

    "locked": {
      "owner": "NixOS",
      "repo": "nixpkgs",
      "rev": "b88ff468e9850410070d4e0ccd68c7011f15b2be",
      "type": "github",
      "lastModified": 1585405475,
      "narHash": "sha256-bESW0n4KgPmZ0luxvwJ+UyATrC6iIltVCsGdLiphVeE="
    },

The 'Input' class is now a dumb set of attributes. All the fetcher
implementations subclass InputScheme, not Input. This simplifies the
API.

Also, fix substitution of flake inputs. This was broken since lazy
flake fetching started using fetchTree internally.
This commit is contained in:
Eelco Dolstra 2020-05-30 00:44:11 +02:00
parent 5633c0975b
commit 950b46821f
30 changed files with 963 additions and 1145 deletions

View file

@ -76,7 +76,7 @@ Path lookupFileArg(EvalState & state, string s)
if (isUri(s)) { if (isUri(s)) {
return state.store->toRealPath( return state.store->toRealPath(
fetchers::downloadTarball( fetchers::downloadTarball(
state.store, resolveUri(s), "source", false).storePath); state.store, resolveUri(s), "source", false).first.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);

View file

@ -11,7 +11,7 @@ let
sourceInfo = sourceInfo =
if key == lockFile.root if key == lockFile.root
then rootSrc then rootSrc
else fetchTree ({ inherit (node.info) narHash; } // removeAttrs node.locked ["dir"]); else fetchTree (node.info or {} // removeAttrs node.locked ["dir"]);
subdir = if key == lockFile.root then rootSubdir else node.locked.dir or ""; subdir = if key == lockFile.root then rootSubdir else node.locked.dir or "";
flake = import (sourceInfo + (if subdir != "" then "/" else "") + subdir + "/flake.nix"); flake = import (sourceInfo + (if subdir != "" then "/" else "") + subdir + "/flake.nix");
inputs = builtins.mapAttrs (inputName: key: allNodes.${key}) (node.inputs or {}); inputs = builtins.mapAttrs (inputName: key: allNodes.${key}) (node.inputs or {});

View file

@ -19,7 +19,7 @@ static FlakeRef maybeLookupFlake(
const FlakeRef & flakeRef, const FlakeRef & flakeRef,
bool allowLookup) bool allowLookup)
{ {
if (!flakeRef.input->isDirect()) { if (!flakeRef.input.isDirect()) {
if (allowLookup) if (allowLookup)
return flakeRef.resolve(store); return flakeRef.resolve(store);
else else
@ -49,16 +49,15 @@ static FlakeRef lookupInFlakeCache(
static std::tuple<fetchers::Tree, FlakeRef, FlakeRef> fetchOrSubstituteTree( static std::tuple<fetchers::Tree, FlakeRef, FlakeRef> fetchOrSubstituteTree(
EvalState & state, EvalState & state,
const FlakeRef & originalRef, const FlakeRef & originalRef,
std::optional<TreeInfo> treeInfo,
bool allowLookup, bool allowLookup,
FlakeCache & flakeCache) FlakeCache & flakeCache)
{ {
/* The tree may already be in the Nix store, or it could be /* The tree may already be in the Nix store, or it could be
substituted (which is often faster than fetching from the substituted (which is often faster than fetching from the
original source). So check that. */ original source). So check that. */
if (treeInfo && originalRef.input->isDirect() && originalRef.input->isImmutable()) { if (originalRef.input.isDirect() && originalRef.input.isImmutable() && originalRef.input.hasAllInfo()) {
try { try {
auto storePath = treeInfo->computeStorePath(*state.store); auto storePath = originalRef.input.computeStorePath(*state.store);
state.store->ensurePath(storePath); state.store->ensurePath(storePath);
@ -74,7 +73,6 @@ static std::tuple<fetchers::Tree, FlakeRef, FlakeRef> fetchOrSubstituteTree(
Tree { Tree {
.actualPath = actualPath, .actualPath = actualPath,
.storePath = std::move(storePath), .storePath = std::move(storePath),
.info = *treeInfo,
}, },
originalRef, originalRef,
originalRef originalRef
@ -99,8 +97,7 @@ static std::tuple<fetchers::Tree, FlakeRef, FlakeRef> fetchOrSubstituteTree(
if (state.allowedPaths) if (state.allowedPaths)
state.allowedPaths->insert(tree.actualPath); state.allowedPaths->insert(tree.actualPath);
if (treeInfo) assert(!originalRef.input.getNarHash() || tree.storePath == originalRef.input.computeStorePath(*state.store));
assert(tree.storePath == treeInfo->computeStorePath(*state.store));
return {std::move(tree), resolvedRef, lockedRef}; return {std::move(tree), resolvedRef, lockedRef};
} }
@ -202,12 +199,11 @@ static std::map<FlakeId, FlakeInput> parseFlakeInputs(
static Flake getFlake( static Flake getFlake(
EvalState & state, EvalState & state,
const FlakeRef & originalRef, const FlakeRef & originalRef,
std::optional<TreeInfo> treeInfo,
bool allowLookup, bool allowLookup,
FlakeCache & flakeCache) FlakeCache & flakeCache)
{ {
auto [sourceInfo, resolvedRef, lockedRef] = fetchOrSubstituteTree( auto [sourceInfo, resolvedRef, lockedRef] = fetchOrSubstituteTree(
state, originalRef, treeInfo, allowLookup, flakeCache); state, originalRef, allowLookup, flakeCache);
// Guard against symlink attacks. // Guard against symlink attacks.
auto flakeFile = canonPath(sourceInfo.actualPath + "/" + lockedRef.subdir + "/flake.nix"); auto flakeFile = canonPath(sourceInfo.actualPath + "/" + lockedRef.subdir + "/flake.nix");
@ -278,7 +274,7 @@ static Flake getFlake(
Flake getFlake(EvalState & state, const FlakeRef & originalRef, bool allowLookup) Flake getFlake(EvalState & state, const FlakeRef & originalRef, bool allowLookup)
{ {
FlakeCache flakeCache; FlakeCache flakeCache;
return getFlake(state, originalRef, {}, allowLookup, flakeCache); return getFlake(state, originalRef, allowLookup, flakeCache);
} }
/* Compute an in-memory lock file for the specified top-level flake, /* Compute an in-memory lock file for the specified top-level flake,
@ -292,7 +288,7 @@ LockedFlake lockFlake(
FlakeCache flakeCache; FlakeCache flakeCache;
auto flake = getFlake(state, topRef, {}, lockFlags.useRegistries, flakeCache); auto flake = getFlake(state, topRef, lockFlags.useRegistries, flakeCache);
// FIXME: symlink attack // FIXME: symlink attack
auto oldLockFile = LockFile::read( auto oldLockFile = LockFile::read(
@ -393,7 +389,7 @@ LockedFlake lockFlake(
didn't change and there is no override from a didn't change and there is no override from a
higher level flake. */ higher level flake. */
auto childNode = std::make_shared<LockedNode>( auto childNode = std::make_shared<LockedNode>(
oldLock->lockedRef, oldLock->originalRef, oldLock->info, oldLock->isFlake); oldLock->lockedRef, oldLock->originalRef, oldLock->isFlake);
node->inputs.insert_or_assign(id, childNode); node->inputs.insert_or_assign(id, childNode);
@ -409,7 +405,7 @@ LockedFlake lockFlake(
if (hasChildUpdate) { if (hasChildUpdate) {
auto inputFlake = getFlake( auto inputFlake = getFlake(
state, oldLock->lockedRef, oldLock->info, false, flakeCache); state, oldLock->lockedRef, false, flakeCache);
computeLocks(inputFlake.inputs, childNode, inputPath, oldLock); computeLocks(inputFlake.inputs, childNode, inputPath, oldLock);
} else { } else {
/* No need to fetch this flake, we can be /* No need to fetch this flake, we can be
@ -440,11 +436,11 @@ LockedFlake lockFlake(
/* We need to create a new lock file entry. So fetch /* We need to create a new lock file entry. So fetch
this input. */ this input. */
if (!lockFlags.allowMutable && !input.ref.input->isImmutable()) if (!lockFlags.allowMutable && !input.ref.input.isImmutable())
throw Error("cannot update flake input '%s' in pure mode", inputPathS); throw Error("cannot update flake input '%s' in pure mode", inputPathS);
if (input.isFlake) { if (input.isFlake) {
auto inputFlake = getFlake(state, input.ref, {}, lockFlags.useRegistries, flakeCache); auto inputFlake = getFlake(state, input.ref, lockFlags.useRegistries, flakeCache);
/* Note: in case of an --override-input, we use /* Note: in case of an --override-input, we use
the *original* ref (input2.ref) for the the *original* ref (input2.ref) for the
@ -454,7 +450,7 @@ LockedFlake lockFlake(
file. That is, overrides are sticky unless you file. That is, overrides are sticky unless you
use --no-write-lock-file. */ use --no-write-lock-file. */
auto childNode = std::make_shared<LockedNode>( auto childNode = std::make_shared<LockedNode>(
inputFlake.lockedRef, input2.ref, inputFlake.sourceInfo->info); inputFlake.lockedRef, input2.ref);
node->inputs.insert_or_assign(id, childNode); node->inputs.insert_or_assign(id, childNode);
@ -479,9 +475,9 @@ LockedFlake lockFlake(
else { else {
auto [sourceInfo, resolvedRef, lockedRef] = fetchOrSubstituteTree( auto [sourceInfo, resolvedRef, lockedRef] = fetchOrSubstituteTree(
state, input.ref, {}, lockFlags.useRegistries, flakeCache); state, input.ref, lockFlags.useRegistries, flakeCache);
node->inputs.insert_or_assign(id, node->inputs.insert_or_assign(id,
std::make_shared<LockedNode>(lockedRef, input.ref, sourceInfo.info, false)); std::make_shared<LockedNode>(lockedRef, input.ref, false));
} }
} }
} }
@ -534,7 +530,7 @@ LockedFlake lockFlake(
printInfo("inputs of flake '%s' changed:\n%s", topRef, chomp(diff)); printInfo("inputs of flake '%s' changed:\n%s", topRef, chomp(diff));
if (lockFlags.writeLockFile) { if (lockFlags.writeLockFile) {
if (auto sourcePath = topRef.input->getSourcePath()) { if (auto sourcePath = topRef.input.getSourcePath()) {
if (!newLockFile.isImmutable()) { if (!newLockFile.isImmutable()) {
if (settings.warnDirty) if (settings.warnDirty)
warn("will not write lock file of flake '%s' because it has a mutable input", topRef); warn("will not write lock file of flake '%s' because it has a mutable input", topRef);
@ -555,7 +551,7 @@ LockedFlake lockFlake(
newLockFile.write(path); newLockFile.write(path);
topRef.input->markChangedFile( topRef.input.markChangedFile(
(topRef.subdir == "" ? "" : topRef.subdir + "/") + "flake.lock", (topRef.subdir == "" ? "" : topRef.subdir + "/") + "flake.lock",
lockFlags.commitLockFile lockFlags.commitLockFile
? std::optional<std::string>(fmt("%s: %s\n\nFlake input changes:\n\n%s", ? std::optional<std::string>(fmt("%s: %s\n\nFlake input changes:\n\n%s",
@ -567,19 +563,19 @@ LockedFlake lockFlake(
also just clear the 'rev' field... */ also just clear the 'rev' field... */
auto prevLockedRef = flake.lockedRef; auto prevLockedRef = flake.lockedRef;
FlakeCache dummyCache; FlakeCache dummyCache;
flake = getFlake(state, topRef, {}, lockFlags.useRegistries, dummyCache); flake = getFlake(state, topRef, lockFlags.useRegistries, dummyCache);
if (lockFlags.commitLockFile && if (lockFlags.commitLockFile &&
flake.lockedRef.input->getRev() && flake.lockedRef.input.getRev() &&
prevLockedRef.input->getRev() != flake.lockedRef.input->getRev()) prevLockedRef.input.getRev() != flake.lockedRef.input.getRev())
warn("committed new revision '%s'", flake.lockedRef.input->getRev()->gitRev()); warn("committed new revision '%s'", flake.lockedRef.input.getRev()->gitRev());
/* Make sure that we picked up the change, /* Make sure that we picked up the change,
i.e. the tree should usually be dirty i.e. the tree should usually be dirty
now. Corner case: we could have reverted from a now. Corner case: we could have reverted from a
dirty to a clean tree! */ dirty to a clean tree! */
if (flake.lockedRef.input == prevLockedRef.input if (flake.lockedRef.input == prevLockedRef.input
&& !flake.lockedRef.input->isImmutable()) && !flake.lockedRef.input.isImmutable())
throw Error("'%s' did not change after I updated its 'flake.lock' file; is 'flake.lock' under version control?", flake.originalRef); throw Error("'%s' did not change after I updated its 'flake.lock' file; is 'flake.lock' under version control?", flake.originalRef);
} }
} else } else
@ -625,7 +621,7 @@ static void prim_getFlake(EvalState & state, const Pos & pos, Value * * args, Va
{ {
auto flakeRefS = state.forceStringNoCtx(*args[0], pos); auto flakeRefS = state.forceStringNoCtx(*args[0], pos);
auto flakeRef = parseFlakeRef(flakeRefS, {}, true); auto flakeRef = parseFlakeRef(flakeRefS, {}, true);
if (evalSettings.pureEval && !flakeRef.input->isImmutable()) if (evalSettings.pureEval && !flakeRef.input.isImmutable())
throw Error("cannot call 'getFlake' on mutable flake reference '%s', at %s (use --impure to override)", flakeRefS, pos); throw Error("cannot call 'getFlake' on mutable flake reference '%s', at %s (use --impure to override)", flakeRefS, pos);
callFlake(state, callFlake(state,
@ -650,8 +646,8 @@ Fingerprint LockedFlake::getFingerprint() const
return hashString(htSHA256, return hashString(htSHA256,
fmt("%s;%d;%d;%s", fmt("%s;%d;%d;%s",
flake.sourceInfo->storePath.to_string(), flake.sourceInfo->storePath.to_string(),
flake.sourceInfo->info.revCount.value_or(0), flake.lockedRef.input.getRevCount().value_or(0),
flake.sourceInfo->info.lastModified.value_or(0), flake.lockedRef.input.getLastModified().value_or(0),
lockFile)); lockFile));
} }

View file

@ -104,7 +104,7 @@ void callFlake(
void emitTreeAttrs( void emitTreeAttrs(
EvalState & state, EvalState & state,
const fetchers::Tree & tree, const fetchers::Tree & tree,
std::shared_ptr<const fetchers::Input> input, const fetchers::Input & input,
Value & v); Value & v);
} }

View file

@ -15,7 +15,7 @@ const static std::string subDirRegex = subDirElemRegex + "(?:/" + subDirElemRege
std::string FlakeRef::to_string() const std::string FlakeRef::to_string() const
{ {
auto url = input->toURL(); auto url = input.toURL();
if (subdir != "") if (subdir != "")
url.query.insert_or_assign("dir", subdir); url.query.insert_or_assign("dir", subdir);
return url.to_string(); return url.to_string();
@ -23,7 +23,7 @@ std::string FlakeRef::to_string() const
fetchers::Attrs FlakeRef::toAttrs() const fetchers::Attrs FlakeRef::toAttrs() const
{ {
auto attrs = input->toAttrs(); auto attrs = input.toAttrs();
if (subdir != "") if (subdir != "")
attrs.emplace("dir", subdir); attrs.emplace("dir", subdir);
return attrs; return attrs;
@ -37,13 +37,13 @@ std::ostream & operator << (std::ostream & str, const FlakeRef & flakeRef)
bool FlakeRef::operator ==(const FlakeRef & other) const bool FlakeRef::operator ==(const FlakeRef & other) const
{ {
return *input == *other.input && subdir == other.subdir; return input == other.input && subdir == other.subdir;
} }
FlakeRef FlakeRef::resolve(ref<Store> store) const FlakeRef FlakeRef::resolve(ref<Store> store) const
{ {
auto [input2, extraAttrs] = lookupInRegistries(store, input); auto [input2, extraAttrs] = lookupInRegistries(store, input);
return FlakeRef(input2, fetchers::maybeGetStrAttr(extraAttrs, "dir").value_or(subdir)); return FlakeRef(std::move(input2), fetchers::maybeGetStrAttr(extraAttrs, "dir").value_or(subdir));
} }
FlakeRef parseFlakeRef( FlakeRef parseFlakeRef(
@ -98,7 +98,7 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
}; };
return std::make_pair( return std::make_pair(
FlakeRef(inputFromURL(parsedURL), ""), FlakeRef(Input::fromURL(parsedURL), ""),
percentDecode(std::string(match[6]))); percentDecode(std::string(match[6])));
} }
@ -143,7 +143,7 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
} }
return std::make_pair( return std::make_pair(
FlakeRef(inputFromURL(parsedURL), get(parsedURL.query, "dir").value_or("")), FlakeRef(Input::fromURL(parsedURL), get(parsedURL.query, "dir").value_or("")),
fragment); fragment);
} }
@ -155,7 +155,7 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
attrs.insert_or_assign("type", "path"); attrs.insert_or_assign("type", "path");
attrs.insert_or_assign("path", path); attrs.insert_or_assign("path", path);
return std::make_pair(FlakeRef(inputFromAttrs(attrs), ""), fragment); return std::make_pair(FlakeRef(Input::fromAttrs(std::move(attrs)), ""), fragment);
} }
else { else {
@ -163,7 +163,7 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
std::string fragment; std::string fragment;
std::swap(fragment, parsedURL.fragment); std::swap(fragment, parsedURL.fragment);
return std::make_pair( return std::make_pair(
FlakeRef(inputFromURL(parsedURL), get(parsedURL.query, "dir").value_or("")), FlakeRef(Input::fromURL(parsedURL), get(parsedURL.query, "dir").value_or("")),
fragment); fragment);
} }
} }
@ -183,14 +183,14 @@ FlakeRef FlakeRef::fromAttrs(const fetchers::Attrs & attrs)
auto attrs2(attrs); auto attrs2(attrs);
attrs2.erase("dir"); attrs2.erase("dir");
return FlakeRef( return FlakeRef(
fetchers::inputFromAttrs(attrs2), fetchers::Input::fromAttrs(std::move(attrs2)),
fetchers::maybeGetStrAttr(attrs, "dir").value_or("")); fetchers::maybeGetStrAttr(attrs, "dir").value_or(""));
} }
std::pair<fetchers::Tree, FlakeRef> FlakeRef::fetchTree(ref<Store> store) const std::pair<fetchers::Tree, FlakeRef> FlakeRef::fetchTree(ref<Store> store) const
{ {
auto [tree, lockedInput] = input->fetchTree(store); auto [tree, lockedInput] = input.fetch(store);
return {std::move(tree), FlakeRef(lockedInput, subdir)}; return {std::move(tree), FlakeRef(std::move(lockedInput), subdir)};
} }
} }

View file

@ -14,17 +14,15 @@ typedef std::string FlakeId;
struct FlakeRef struct FlakeRef
{ {
std::shared_ptr<const fetchers::Input> input; fetchers::Input input;
Path subdir; Path subdir;
bool operator==(const FlakeRef & other) const; bool operator==(const FlakeRef & other) const;
FlakeRef(const std::shared_ptr<const fetchers::Input> & input, const Path & subdir) FlakeRef(fetchers::Input && input, const Path & subdir)
: input(input), subdir(subdir) : input(std::move(input)), subdir(subdir)
{ { }
assert(input);
}
// FIXME: change to operator <<. // FIXME: change to operator <<.
std::string to_string() const; std::string to_string() const;

View file

@ -5,35 +5,40 @@
namespace nix::flake { namespace nix::flake {
FlakeRef flakeRefFromJson(const nlohmann::json & json)
{
return FlakeRef::fromAttrs(jsonToAttrs(json));
}
FlakeRef getFlakeRef( FlakeRef getFlakeRef(
const nlohmann::json & json, const nlohmann::json & json,
const char * attr) const char * attr,
const char * info)
{ {
auto i = json.find(attr); auto i = json.find(attr);
if (i != json.end()) if (i != json.end()) {
return flakeRefFromJson(*i); auto attrs = jsonToAttrs(*i);
// FIXME: remove when we drop support for version 5.
if (info) {
auto j = json.find(info);
if (j != json.end()) {
for (auto k : jsonToAttrs(*j))
attrs.insert_or_assign(k.first, k.second);
}
}
return FlakeRef::fromAttrs(attrs);
}
throw Error("attribute '%s' missing in lock file", attr); throw Error("attribute '%s' missing in lock file", attr);
} }
LockedNode::LockedNode(const nlohmann::json & json) LockedNode::LockedNode(const nlohmann::json & json)
: lockedRef(getFlakeRef(json, "locked")) : lockedRef(getFlakeRef(json, "locked", "info"))
, originalRef(getFlakeRef(json, "original")) , originalRef(getFlakeRef(json, "original", nullptr))
, info(TreeInfo::fromJson(json))
, isFlake(json.find("flake") != json.end() ? (bool) json["flake"] : true) , isFlake(json.find("flake") != json.end() ? (bool) json["flake"] : true)
{ {
if (!lockedRef.input->isImmutable()) if (!lockedRef.input.isImmutable())
throw Error("lockfile contains mutable flakeref '%s'", lockedRef); throw Error("lockfile contains mutable lock '%s'", attrsToJson(lockedRef.input.toAttrs()));
} }
StorePath LockedNode::computeStorePath(Store & store) const StorePath LockedNode::computeStorePath(Store & store) const
{ {
return info.computeStorePath(store); return lockedRef.input.computeStorePath(store);
} }
std::shared_ptr<Node> Node::findInput(const InputPath & path) std::shared_ptr<Node> Node::findInput(const InputPath & path)
@ -53,7 +58,7 @@ std::shared_ptr<Node> Node::findInput(const InputPath & path)
LockFile::LockFile(const nlohmann::json & json, const Path & path) LockFile::LockFile(const nlohmann::json & json, const Path & path)
{ {
auto version = json.value("version", 0); auto version = json.value("version", 0);
if (version != 5) if (version < 5 || version > 6)
throw Error("lock file '%s' has unsupported version %d", path, version); throw Error("lock file '%s' has unsupported version %d", path, version);
std::unordered_map<std::string, std::shared_ptr<Node>> nodeMap; std::unordered_map<std::string, std::shared_ptr<Node>> nodeMap;
@ -119,7 +124,6 @@ nlohmann::json LockFile::toJson() const
if (auto lockedNode = std::dynamic_pointer_cast<const LockedNode>(node)) { if (auto lockedNode = std::dynamic_pointer_cast<const LockedNode>(node)) {
n["original"] = fetchers::attrsToJson(lockedNode->originalRef.toAttrs()); n["original"] = fetchers::attrsToJson(lockedNode->originalRef.toAttrs());
n["locked"] = fetchers::attrsToJson(lockedNode->lockedRef.toAttrs()); n["locked"] = fetchers::attrsToJson(lockedNode->lockedRef.toAttrs());
n["info"] = lockedNode->info.toJson();
if (!lockedNode->isFlake) n["flake"] = false; if (!lockedNode->isFlake) n["flake"] = false;
} }
@ -129,7 +133,7 @@ nlohmann::json LockFile::toJson() const
}; };
nlohmann::json json; nlohmann::json json;
json["version"] = 5; json["version"] = 6;
json["root"] = dumpNode("root", root); json["root"] = dumpNode("root", root);
json["nodes"] = std::move(nodes); json["nodes"] = std::move(nodes);
@ -176,7 +180,7 @@ bool LockFile::isImmutable() const
for (auto & i : nodes) { for (auto & i : nodes) {
if (i == root) continue; if (i == root) continue;
auto lockedNode = std::dynamic_pointer_cast<const LockedNode>(i); auto lockedNode = std::dynamic_pointer_cast<const LockedNode>(i);
if (lockedNode && !lockedNode->lockedRef.input->isImmutable()) return false; if (lockedNode && !lockedNode->lockedRef.input.isImmutable()) return false;
} }
return true; return true;

View file

@ -31,15 +31,13 @@ struct Node : std::enable_shared_from_this<Node>
struct LockedNode : Node struct LockedNode : Node
{ {
FlakeRef lockedRef, originalRef; FlakeRef lockedRef, originalRef;
TreeInfo info;
bool isFlake = true; bool isFlake = true;
LockedNode( LockedNode(
const FlakeRef & lockedRef, const FlakeRef & lockedRef,
const FlakeRef & originalRef, const FlakeRef & originalRef,
const TreeInfo & info,
bool isFlake = true) bool isFlake = true)
: lockedRef(lockedRef), originalRef(originalRef), info(info), isFlake(isFlake) : lockedRef(lockedRef), originalRef(originalRef), isFlake(isFlake)
{ } { }
LockedNode(const nlohmann::json & json); LockedNode(const nlohmann::json & json);

View file

@ -689,7 +689,7 @@ std::pair<bool, std::string> EvalState::resolveSearchPathElem(const SearchPathEl
if (isUri(elem.second)) { if (isUri(elem.second)) {
try { try {
res = { true, store->toRealPath(fetchers::downloadTarball( res = { true, store->toRealPath(fetchers::downloadTarball(
store, resolveUri(elem.second), "source", false).storePath) }; store, resolveUri(elem.second), "source", false).first.storePath) };
} catch (FileTransferError & e) { } catch (FileTransferError & 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, "" };

View file

@ -56,23 +56,23 @@ static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Va
attrs.insert_or_assign("url", url.find("://") != std::string::npos ? url : "file://" + url); attrs.insert_or_assign("url", url.find("://") != std::string::npos ? url : "file://" + url);
if (ref) attrs.insert_or_assign("ref", *ref); if (ref) attrs.insert_or_assign("ref", *ref);
if (rev) attrs.insert_or_assign("rev", rev->gitRev()); if (rev) attrs.insert_or_assign("rev", rev->gitRev());
if (fetchSubmodules) attrs.insert_or_assign("submodules", true); if (fetchSubmodules) attrs.insert_or_assign("submodules", fetchers::Explicit<bool>{true});
auto input = fetchers::inputFromAttrs(attrs); auto input = fetchers::Input::fromAttrs(std::move(attrs));
// FIXME: use name? // FIXME: use name?
auto [tree, input2] = input->fetchTree(state.store); auto [tree, input2] = input.fetch(state.store);
state.mkAttrs(v, 8); state.mkAttrs(v, 8);
auto storePath = state.store->printStorePath(tree.storePath); auto storePath = state.store->printStorePath(tree.storePath);
mkString(*state.allocAttr(v, state.sOutPath), storePath, PathSet({storePath})); mkString(*state.allocAttr(v, state.sOutPath), storePath, PathSet({storePath}));
// Backward compatibility: set 'rev' to // Backward compatibility: set 'rev' to
// 0000000000000000000000000000000000000000 for a dirty tree. // 0000000000000000000000000000000000000000 for a dirty tree.
auto rev2 = input2->getRev().value_or(Hash(htSHA1)); 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("rev")), rev2.gitRev());
mkString(*state.allocAttr(v, state.symbols.create("shortRev")), rev2.gitShortRev()); mkString(*state.allocAttr(v, state.symbols.create("shortRev")), rev2.gitShortRev());
// Backward compatibility: set 'revCount' to 0 for a dirty tree. // Backward compatibility: set 'revCount' to 0 for a dirty tree.
mkInt(*state.allocAttr(v, state.symbols.create("revCount")), mkInt(*state.allocAttr(v, state.symbols.create("revCount")),
tree.info.revCount.value_or(0)); input2.getRevCount().value_or(0));
mkBool(*state.allocAttr(v, state.symbols.create("submodules")), fetchSubmodules); mkBool(*state.allocAttr(v, state.symbols.create("submodules")), fetchSubmodules);
v.attrs->sort(); v.attrs->sort();

View file

@ -59,23 +59,23 @@ static void prim_fetchMercurial(EvalState & state, const Pos & pos, Value * * ar
attrs.insert_or_assign("url", url.find("://") != std::string::npos ? url : "file://" + url); attrs.insert_or_assign("url", url.find("://") != std::string::npos ? url : "file://" + url);
if (ref) attrs.insert_or_assign("ref", *ref); if (ref) attrs.insert_or_assign("ref", *ref);
if (rev) attrs.insert_or_assign("rev", rev->gitRev()); if (rev) attrs.insert_or_assign("rev", rev->gitRev());
auto input = fetchers::inputFromAttrs(attrs); auto input = fetchers::Input::fromAttrs(std::move(attrs));
// FIXME: use name // FIXME: use name
auto [tree, input2] = input->fetchTree(state.store); auto [tree, input2] = input.fetch(state.store);
state.mkAttrs(v, 8); state.mkAttrs(v, 8);
auto storePath = state.store->printStorePath(tree.storePath); auto storePath = state.store->printStorePath(tree.storePath);
mkString(*state.allocAttr(v, state.sOutPath), storePath, PathSet({storePath})); mkString(*state.allocAttr(v, state.sOutPath), storePath, PathSet({storePath}));
if (input2->getRef()) if (input2.getRef())
mkString(*state.allocAttr(v, state.symbols.create("branch")), *input2->getRef()); mkString(*state.allocAttr(v, state.symbols.create("branch")), *input2.getRef());
// Backward compatibility: set 'rev' to // Backward compatibility: set 'rev' to
// 0000000000000000000000000000000000000000 for a dirty tree. // 0000000000000000000000000000000000000000 for a dirty tree.
auto rev2 = input2->getRev().value_or(Hash(htSHA1)); 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("rev")), rev2.gitRev());
mkString(*state.allocAttr(v, state.symbols.create("shortRev")), std::string(rev2.gitRev(), 0, 12)); mkString(*state.allocAttr(v, state.symbols.create("shortRev")), std::string(rev2.gitRev(), 0, 12));
if (tree.info.revCount) if (auto revCount = input2.getRevCount())
mkInt(*state.allocAttr(v, state.symbols.create("revCount")), *tree.info.revCount); mkInt(*state.allocAttr(v, state.symbols.create("revCount")), *revCount);
v.attrs->sort(); v.attrs->sort();
if (state.allowedPaths) if (state.allowedPaths)

View file

@ -13,31 +13,36 @@ namespace nix {
void emitTreeAttrs( void emitTreeAttrs(
EvalState & state, EvalState & state,
const fetchers::Tree & tree, const fetchers::Tree & tree,
std::shared_ptr<const fetchers::Input> input, const fetchers::Input & input,
Value & v) Value & v)
{ {
assert(input.isImmutable());
state.mkAttrs(v, 8); state.mkAttrs(v, 8);
auto storePath = state.store->printStorePath(tree.storePath); auto storePath = state.store->printStorePath(tree.storePath);
mkString(*state.allocAttr(v, state.sOutPath), storePath, PathSet({storePath})); mkString(*state.allocAttr(v, state.sOutPath), storePath, PathSet({storePath}));
assert(tree.info.narHash); // FIXME: support arbitrary input attributes.
mkString(*state.allocAttr(v, state.symbols.create("narHash")),
tree.info.narHash.to_string(SRI));
if (input->getRev()) { auto narHash = input.getNarHash();
mkString(*state.allocAttr(v, state.symbols.create("rev")), input->getRev()->gitRev()); assert(narHash);
mkString(*state.allocAttr(v, state.symbols.create("shortRev")), input->getRev()->gitShortRev()); mkString(*state.allocAttr(v, state.symbols.create("narHash")),
narHash->to_string(SRI));
if (auto rev = input.getRev()) {
mkString(*state.allocAttr(v, state.symbols.create("rev")), rev->gitRev());
mkString(*state.allocAttr(v, state.symbols.create("shortRev")), rev->gitShortRev());
} }
if (tree.info.revCount) if (auto revCount = input.getRevCount())
mkInt(*state.allocAttr(v, state.symbols.create("revCount")), *tree.info.revCount); mkInt(*state.allocAttr(v, state.symbols.create("revCount")), *revCount);
if (tree.info.lastModified) { if (auto lastModified = input.getLastModified()) {
mkInt(*state.allocAttr(v, state.symbols.create("lastModified")), *tree.info.lastModified); mkInt(*state.allocAttr(v, state.symbols.create("lastModified")), *lastModified);
mkString(*state.allocAttr(v, state.symbols.create("lastModifiedDate")), mkString(*state.allocAttr(v, state.symbols.create("lastModifiedDate")),
fmt("%s", std::put_time(std::gmtime(&*tree.info.lastModified), "%Y%m%d%H%M%S"))); fmt("%s", std::put_time(std::gmtime(&*lastModified), "%Y%m%d%H%M%S")));
} }
v.attrs->sort(); v.attrs->sort();
@ -47,7 +52,7 @@ static void prim_fetchTree(EvalState & state, const Pos & pos, Value * * args, V
{ {
settings.requireExperimentalFeature("flakes"); settings.requireExperimentalFeature("flakes");
std::shared_ptr<const fetchers::Input> input; fetchers::Input input;
PathSet context; PathSet context;
state.forceValue(*args[0]); state.forceValue(*args[0]);
@ -62,7 +67,7 @@ static void prim_fetchTree(EvalState & state, const Pos & pos, Value * * args, V
if (attr.value->type == tString) if (attr.value->type == tString)
attrs.emplace(attr.name, attr.value->string.s); attrs.emplace(attr.name, attr.value->string.s);
else if (attr.value->type == tBool) else if (attr.value->type == tBool)
attrs.emplace(attr.name, attr.value->boolean); attrs.emplace(attr.name, fetchers::Explicit<bool>{attr.value->boolean});
else if (attr.value->type == tInt) else if (attr.value->type == tInt)
attrs.emplace(attr.name, attr.value->integer); attrs.emplace(attr.name, attr.value->integer);
else else
@ -73,18 +78,42 @@ static void prim_fetchTree(EvalState & state, const Pos & pos, Value * * args, V
if (!attrs.count("type")) if (!attrs.count("type"))
throw Error("attribute 'type' is missing in call to 'fetchTree', at %s", pos); throw Error("attribute 'type' is missing in call to 'fetchTree', at %s", pos);
input = fetchers::inputFromAttrs(attrs); input = fetchers::Input::fromAttrs(std::move(attrs));
} else } else
input = fetchers::inputFromURL(state.coerceToString(pos, *args[0], context, false, false)); input = fetchers::Input::fromURL(state.coerceToString(pos, *args[0], context, false, false));
if (!evalSettings.pureEval && !input->isDirect()) if (!evalSettings.pureEval && !input.isDirect())
input = lookupInRegistries(state.store, input).first; input = lookupInRegistries(state.store, input).first;
if (evalSettings.pureEval && !input->isImmutable()) if (evalSettings.pureEval && !input.isImmutable())
throw Error("in pure evaluation mode, 'fetchTree' requires an immutable input, at %s", pos); throw Error("in pure evaluation mode, 'fetchTree' requires an immutable input, at %s", pos);
// FIXME: use fetchOrSubstituteTree /* The tree may already be in the Nix store, or it could be
auto [tree, input2] = input->fetchTree(state.store); substituted (which is often faster than fetching from the
original source). So check that. */
if (input.hasAllInfo()) {
auto storePath = input.computeStorePath(*state.store);
try {
state.store->ensurePath(storePath);
debug("using substituted/cached input '%s' in '%s'",
input.to_string(), state.store->printStorePath(storePath));
auto actualPath = state.store->toRealPath(storePath);
if (state.allowedPaths)
state.allowedPaths->insert(actualPath);
emitTreeAttrs(state, fetchers::Tree { .actualPath = actualPath, .storePath = std::move(storePath) }, input, v);
return;
} catch (Error & e) {
debug("substitution of input '%s' failed: %s", input.to_string(), e.what());
}
}
auto [tree, input2] = input.fetch(state.store);
if (state.allowedPaths) if (state.allowedPaths)
state.allowedPaths->insert(tree.actualPath); state.allowedPaths->insert(tree.actualPath);
@ -137,7 +166,7 @@ static void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v,
auto storePath = auto storePath =
unpack unpack
? fetchers::downloadTarball(state.store, *url, name, (bool) expectedHash).storePath ? fetchers::downloadTarball(state.store, *url, name, (bool) expectedHash).first.storePath
: fetchers::downloadFile(state.store, *url, name, (bool) expectedHash).storePath; : fetchers::downloadFile(state.store, *url, name, (bool) expectedHash).storePath;
auto path = state.store->toRealPath(storePath); auto path = state.store->toRealPath(storePath);

View file

@ -27,7 +27,7 @@ nlohmann::json attrsToJson(const Attrs & attrs)
{ {
nlohmann::json json; nlohmann::json json;
for (auto & attr : attrs) { for (auto & attr : attrs) {
if (auto v = std::get_if<int64_t>(&attr.second)) { if (auto v = std::get_if<uint64_t>(&attr.second)) {
json[attr.first] = *v; json[attr.first] = *v;
} else if (auto v = std::get_if<std::string>(&attr.second)) { } else if (auto v = std::get_if<std::string>(&attr.second)) {
json[attr.first] = *v; json[attr.first] = *v;
@ -55,16 +55,16 @@ std::string getStrAttr(const Attrs & attrs, const std::string & name)
return *s; return *s;
} }
std::optional<int64_t> maybeGetIntAttr(const Attrs & attrs, const std::string & name) std::optional<uint64_t> maybeGetIntAttr(const Attrs & attrs, const std::string & name)
{ {
auto i = attrs.find(name); auto i = attrs.find(name);
if (i == attrs.end()) return {}; if (i == attrs.end()) return {};
if (auto v = std::get_if<int64_t>(&i->second)) if (auto v = std::get_if<uint64_t>(&i->second))
return *v; return *v;
throw Error("input attribute '%s' is not an integer", name); throw Error("input attribute '%s' is not an integer", name);
} }
int64_t getIntAttr(const Attrs & attrs, const std::string & name) uint64_t getIntAttr(const Attrs & attrs, const std::string & name)
{ {
auto s = maybeGetIntAttr(attrs, name); auto s = maybeGetIntAttr(attrs, name);
if (!s) if (!s)
@ -76,8 +76,8 @@ std::optional<bool> maybeGetBoolAttr(const Attrs & attrs, const std::string & na
{ {
auto i = attrs.find(name); auto i = attrs.find(name);
if (i == attrs.end()) return {}; if (i == attrs.end()) return {};
if (auto v = std::get_if<int64_t>(&i->second)) if (auto v = std::get_if<Explicit<bool>>(&i->second))
return *v; return v->t;
throw Error("input attribute '%s' is not a Boolean", name); throw Error("input attribute '%s' is not a Boolean", name);
} }
@ -93,7 +93,7 @@ std::map<std::string, std::string> attrsToQuery(const Attrs & attrs)
{ {
std::map<std::string, std::string> query; std::map<std::string, std::string> query;
for (auto & attr : attrs) { for (auto & attr : attrs) {
if (auto v = std::get_if<int64_t>(&attr.second)) { if (auto v = std::get_if<uint64_t>(&attr.second)) {
query.insert_or_assign(attr.first, fmt("%d", *v)); query.insert_or_assign(attr.first, fmt("%d", *v));
} else if (auto v = std::get_if<std::string>(&attr.second)) { } else if (auto v = std::get_if<std::string>(&attr.second)) {
query.insert_or_assign(attr.first, *v); query.insert_or_assign(attr.first, *v);

View file

@ -13,9 +13,14 @@ namespace nix::fetchers {
template<typename T> template<typename T>
struct Explicit { struct Explicit {
T t; T t;
bool operator ==(const Explicit<T> & other) const
{
return t == other.t;
}
}; };
typedef std::variant<std::string, int64_t, Explicit<bool>> Attr; typedef std::variant<std::string, uint64_t, Explicit<bool>> Attr;
typedef std::map<std::string, Attr> Attrs; typedef std::map<std::string, Attr> Attrs;
Attrs jsonToAttrs(const nlohmann::json & json); Attrs jsonToAttrs(const nlohmann::json & json);
@ -26,9 +31,9 @@ std::optional<std::string> maybeGetStrAttr(const Attrs & attrs, const std::strin
std::string getStrAttr(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); std::optional<uint64_t> maybeGetIntAttr(const Attrs & attrs, const std::string & name);
int64_t getIntAttr(const Attrs & attrs, const std::string & name); uint64_t getIntAttr(const Attrs & attrs, const std::string & name);
std::optional<bool> maybeGetBoolAttr(const Attrs & attrs, const std::string & name); std::optional<bool> maybeGetBoolAttr(const Attrs & attrs, const std::string & name);

View file

@ -5,82 +5,234 @@
namespace nix::fetchers { namespace nix::fetchers {
std::unique_ptr<std::vector<std::unique_ptr<InputScheme>>> inputSchemes = nullptr; std::unique_ptr<std::vector<std::shared_ptr<InputScheme>>> inputSchemes = nullptr;
void registerInputScheme(std::unique_ptr<InputScheme> && inputScheme) void registerInputScheme(std::shared_ptr<InputScheme> && inputScheme)
{ {
if (!inputSchemes) inputSchemes = std::make_unique<std::vector<std::unique_ptr<InputScheme>>>(); if (!inputSchemes) inputSchemes = std::make_unique<std::vector<std::shared_ptr<InputScheme>>>();
inputSchemes->push_back(std::move(inputScheme)); inputSchemes->push_back(std::move(inputScheme));
} }
std::unique_ptr<Input> inputFromURL(const ParsedURL & url) Input Input::fromURL(const std::string & url)
{
return fromURL(parseURL(url));
}
static void fixupInput(Input & input)
{
// Check common attributes.
input.getType();
input.getRef();
if (input.getRev())
input.immutable = true;
input.getRevCount();
input.getLastModified();
if (input.getNarHash())
input.immutable = true;
}
Input Input::fromURL(const ParsedURL & url)
{ {
for (auto & inputScheme : *inputSchemes) { for (auto & inputScheme : *inputSchemes) {
auto res = inputScheme->inputFromURL(url); auto res = inputScheme->inputFromURL(url);
if (res) return res; if (res) {
res->scheme = inputScheme;
fixupInput(*res);
return std::move(*res);
}
} }
throw Error("input '%s' is unsupported", url.url); throw Error("input '%s' is unsupported", url.url);
} }
std::unique_ptr<Input> inputFromURL(const std::string & url) Input Input::fromAttrs(Attrs && attrs)
{ {
return inputFromURL(parseURL(url));
}
std::unique_ptr<Input> inputFromAttrs(const Attrs & attrs)
{
auto attrs2(attrs);
attrs2.erase("narHash");
for (auto & inputScheme : *inputSchemes) { for (auto & inputScheme : *inputSchemes) {
auto res = inputScheme->inputFromAttrs(attrs2); auto res = inputScheme->inputFromAttrs(attrs);
if (res) { if (res) {
if (auto narHash = maybeGetStrAttr(attrs, "narHash")) res->scheme = inputScheme;
// FIXME: require SRI hash. fixupInput(*res);
res->narHash = Hash(*narHash); return std::move(*res);
return res;
} }
} }
throw Error("input '%s' is unsupported", attrsToJson(attrs));
Input input;
input.attrs = attrs;
fixupInput(input);
return input;
}
ParsedURL Input::toURL() const
{
if (!scheme)
throw Error("cannot show unsupported input '%s'", attrsToJson(attrs));
return scheme->toURL(*this);
}
std::string Input::to_string() const
{
return toURL().to_string();
} }
Attrs Input::toAttrs() const Attrs Input::toAttrs() const
{ {
auto attrs = toAttrsInternal();
if (narHash)
attrs.emplace("narHash", narHash->to_string(SRI));
attrs.emplace("type", type());
return attrs; return attrs;
} }
std::pair<Tree, std::shared_ptr<const Input>> Input::fetchTree(ref<Store> store) const bool Input::hasAllInfo() const
{ {
auto [tree, input] = fetchTreeInternal(store); return getNarHash() && scheme && scheme->hasAllInfo(*this);
}
bool Input::operator ==(const Input & other) const
{
return attrs == other.attrs;
}
bool Input::contains(const Input & other) const
{
auto other2(other);
other2.attrs.erase("ref");
other2.attrs.erase("rev");
if (*this == other2) return true;
return false;
}
std::pair<Tree, Input> Input::fetch(ref<Store> store) const
{
if (!scheme)
throw Error("cannot fetch unsupported input '%s'", attrsToJson(toAttrs()));
auto [tree, input] = scheme->fetch(store, *this);
if (tree.actualPath == "") if (tree.actualPath == "")
tree.actualPath = store->toRealPath(tree.storePath); tree.actualPath = store->toRealPath(tree.storePath);
if (!tree.info.narHash) auto narHash = store->queryPathInfo(tree.storePath)->narHash;
tree.info.narHash = store->queryPathInfo(tree.storePath)->narHash; input.attrs.insert_or_assign("narHash", narHash.to_string(SRI));
if (input->narHash) if (auto narHash2 = getNarHash()) {
assert(input->narHash == tree.info.narHash); if (narHash != *narHash2)
throw Error("NAR hash mismatch in input '%s' (%s), expected '%s', got '%s'",
to_string(), tree.actualPath, narHash2->to_string(SRI), narHash.to_string(SRI));
}
if (narHash && narHash != input->narHash) // FIXME: check lastModified, revCount
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)); input.immutable = true;
assert(input.hasAllInfo());
return {std::move(tree), input}; return {std::move(tree), input};
} }
std::shared_ptr<const Input> Input::applyOverrides( Input Input::applyOverrides(
std::optional<std::string> ref, std::optional<std::string> ref,
std::optional<Hash> rev) const std::optional<Hash> rev) const
{
if (!scheme) return *this;
return scheme->applyOverrides(*this, ref, rev);
}
void Input::clone(const Path & destDir) const
{
assert(scheme);
scheme->clone(*this, destDir);
}
std::optional<Path> Input::getSourcePath() const
{
assert(scheme);
return scheme->getSourcePath(*this);
}
void Input::markChangedFile(
std::string_view file,
std::optional<std::string> commitMsg) const
{
assert(scheme);
return scheme->markChangedFile(*this, file, commitMsg);
}
StorePath Input::computeStorePath(Store & store) const
{
auto narHash = getNarHash();
if (!narHash)
throw Error("cannot compute store path for mutable input '%s'", to_string());
return store.makeFixedOutputPath(true, *narHash, "source");
}
std::string Input::getType() const
{
return getStrAttr(attrs, "type");
}
std::optional<Hash> Input::getNarHash() const
{
if (auto s = maybeGetStrAttr(attrs, "narHash"))
// FIXME: require SRI hash.
return Hash(*s, htSHA256);
return {};
}
std::optional<std::string> Input::getRef() const
{
if (auto s = maybeGetStrAttr(attrs, "ref"))
return *s;
return {};
}
std::optional<Hash> Input::getRev() const
{
if (auto s = maybeGetStrAttr(attrs, "rev"))
return Hash(*s, htSHA1);
return {};
}
std::optional<uint64_t> Input::getRevCount() const
{
if (auto n = maybeGetIntAttr(attrs, "revCount"))
return *n;
return {};
}
std::optional<time_t> Input::getLastModified() const
{
if (auto n = maybeGetIntAttr(attrs, "lastModified"))
return *n;
return {};
}
ParsedURL InputScheme::toURL(const Input & input)
{
throw Error("don't know how to convert input '%s' to a URL", attrsToJson(input.attrs));
}
Input InputScheme::applyOverrides(
const Input & input,
std::optional<std::string> ref,
std::optional<Hash> rev)
{ {
if (ref) if (ref)
throw Error("don't know how to apply '%s' to '%s'", *ref, to_string()); throw Error("don't know how to set branch/tag name of input '%s' to '%s'", input.to_string(), *ref);
if (rev) if (rev)
throw Error("don't know how to apply '%s' to '%s'", rev->to_string(Base16, false), to_string()); throw Error("don't know how to set revision of input '%s' to '%s'", input.to_string(), rev->gitRev());
return shared_from_this(); return input;
}
std::optional<Path> InputScheme::getSourcePath(const Input & input)
{
return {};
}
void InputScheme::markChangedFile(const Input & input, std::string_view file, std::optional<std::string> commitMsg)
{
assert(false);
}
void InputScheme::clone(const Input & input, const Path & destDir)
{
throw Error("do not know how to clone input '%s'", input.to_string());
} }
} }

View file

@ -3,7 +3,6 @@
#include "types.hh" #include "types.hh"
#include "hash.hh" #include "hash.hh"
#include "path.hh" #include "path.hh"
#include "tree-info.hh"
#include "attrs.hh" #include "attrs.hh"
#include "url.hh" #include "url.hh"
@ -13,89 +12,100 @@ namespace nix { class Store; }
namespace nix::fetchers { namespace nix::fetchers {
struct Input;
struct Tree struct Tree
{ {
Path actualPath; Path actualPath;
StorePath storePath; StorePath storePath;
TreeInfo info;
}; };
struct Input : std::enable_shared_from_this<Input> struct InputScheme;
struct Input
{ {
std::optional<Hash> narHash; // FIXME: implement friend class InputScheme;
virtual std::string type() const = 0; std::shared_ptr<InputScheme> scheme; // note: can be null
Attrs attrs;
bool immutable = false;
bool direct = true;
virtual ~Input() { } public:
static Input fromURL(const std::string & url);
virtual bool operator ==(const Input & other) const { return false; } static Input fromURL(const ParsedURL & url);
/* Check whether this is a "direct" input, that is, not static Input fromAttrs(Attrs && attrs);
one that goes through a registry. */
virtual bool isDirect() const { return true; }
/* Check whether this is an "immutable" input, that is, ParsedURL toURL() const;
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; } std::string to_string() const;
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; Attrs toAttrs() const;
std::pair<Tree, std::shared_ptr<const Input>> fetchTree(ref<Store> store) const; /* Check whether this is a "direct" input, that is, not
one that goes through a registry. */
bool isDirect() const { return direct; }
virtual std::shared_ptr<const Input> applyOverrides( /* Check whether this is an "immutable" input, that is,
one that contains a commit hash or content hash. */
bool isImmutable() const { return immutable; }
bool hasAllInfo() const;
bool operator ==(const Input & other) const;
bool contains(const Input & other) const;
std::pair<Tree, Input> fetch(ref<Store> store) const;
Input applyOverrides(
std::optional<std::string> ref, std::optional<std::string> ref,
std::optional<Hash> rev) const; std::optional<Hash> rev) const;
virtual std::optional<Path> getSourcePath() const { return {}; } void clone(const Path & destDir) const;
virtual void markChangedFile( std::optional<Path> getSourcePath() const;
void markChangedFile(
std::string_view file, std::string_view file,
std::optional<std::string> commitMsg) const std::optional<std::string> commitMsg) const;
{ assert(false); }
virtual void clone(const Path & destDir) const StorePath computeStorePath(Store & store) const;
{
throw Error("do not know how to clone input '%s'", to_string());
}
private: // Convience functions for common attributes.
std::string getType() const;
virtual std::pair<Tree, std::shared_ptr<const Input>> fetchTreeInternal(ref<Store> store) const = 0; std::optional<Hash> getNarHash() const;
std::optional<std::string> getRef() const;
virtual Attrs toAttrsInternal() const = 0; std::optional<Hash> getRev() const;
std::optional<uint64_t> getRevCount() const;
std::optional<time_t> getLastModified() const;
}; };
struct InputScheme struct InputScheme
{ {
virtual ~InputScheme() { } virtual std::optional<Input> inputFromURL(const ParsedURL & url) = 0;
virtual std::unique_ptr<Input> inputFromURL(const ParsedURL & url) = 0; virtual std::optional<Input> inputFromAttrs(const Attrs & attrs) = 0;
virtual std::unique_ptr<Input> inputFromAttrs(const Attrs & attrs) = 0; virtual ParsedURL toURL(const Input & input);
virtual bool hasAllInfo(const Input & input) = 0;
virtual Input applyOverrides(
const Input & input,
std::optional<std::string> ref,
std::optional<Hash> rev);
virtual void clone(const Input & input, const Path & destDir);
virtual std::optional<Path> getSourcePath(const Input & input);
virtual void markChangedFile(const Input & input, std::string_view file, std::optional<std::string> commitMsg);
virtual std::pair<Tree, Input> fetch(ref<Store> store, const Input & input) = 0;
}; };
std::unique_ptr<Input> inputFromURL(const ParsedURL & url); void registerInputScheme(std::shared_ptr<InputScheme> && fetcher);
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 struct DownloadFileResult
{ {
@ -110,7 +120,7 @@ DownloadFileResult downloadFile(
const std::string & name, const std::string & name,
bool immutable); bool immutable);
Tree downloadTarball( std::pair<Tree, time_t> downloadTarball(
ref<Store> store, ref<Store> store,
const std::string & url, const std::string & url,
const std::string & name, const std::string & name,

View file

@ -22,110 +22,121 @@ static bool isNotDotGitDirectory(const Path & path)
return not std::regex_match(path, gitDirRegex); return not std::regex_match(path, gitDirRegex);
} }
struct GitInput : Input struct GitInputScheme : InputScheme
{ {
ParsedURL url; std::optional<Input> inputFromURL(const ParsedURL & url) override
std::optional<std::string> ref;
std::optional<Hash> rev;
bool shallow = false;
bool submodules = 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); if (url.scheme != "git" &&
return url.scheme != "git+http" &&
other2 url.scheme != "git+https" &&
&& url == other2->url url.scheme != "git+ssh" &&
&& rev == other2->rev url.scheme != "git+file") return {};
&& ref == other2->ref;
}
bool isImmutable() const override auto url2(url);
{ if (hasPrefix(url2.scheme, "git+")) url2.scheme = std::string(url2.scheme, 4);
return (bool) rev || narHash; url2.query.clear();
}
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 attrs;
attrs.emplace("url", url.to_string()); attrs.emplace("type", "git");
if (ref)
attrs.emplace("ref", *ref); for (auto &[name, value] : url.query) {
if (rev) if (name == "rev" || name == "ref")
attrs.emplace("rev", rev->gitRev()); attrs.emplace(name, value);
if (shallow) else
attrs.emplace("shallow", true); url2.query.emplace(name, value);
if (submodules) }
attrs.emplace("submodules", true);
return attrs; attrs.emplace("url", url2.to_string());
return inputFromAttrs(attrs);
} }
void clone(const Path & destDir) const override std::optional<Input> inputFromAttrs(const Attrs & attrs) override
{ {
auto [isLocal, actualUrl] = getActualUrl(); if (maybeGetStrAttr(attrs, "type") != "git") return {};
for (auto & [name, value] : attrs)
if (name != "type" && name != "url" && name != "ref" && name != "rev" && name != "shallow" && name != "submodules" && name != "lastModified" && name != "revCount" && name != "narHash")
throw Error("unsupported Git input attribute '%s'", name);
parseURL(getStrAttr(attrs, "url"));
maybeGetBoolAttr(attrs, "shallow");
maybeGetBoolAttr(attrs, "submodules");
if (auto ref = maybeGetStrAttr(attrs, "ref")) {
if (!std::regex_match(*ref, refRegex))
throw BadURL("invalid Git branch/tag name '%s'", *ref);
}
Input input;
input.attrs = attrs;
return input;
}
ParsedURL toURL(const Input & input) override
{
auto url = parseURL(getStrAttr(input.attrs, "url"));
if (url.scheme != "git") url.scheme = "git+" + url.scheme;
if (auto rev = input.getRev()) url.query.insert_or_assign("rev", rev->gitRev());
if (auto ref = input.getRef()) url.query.insert_or_assign("ref", *ref);
if (maybeGetBoolAttr(input.attrs, "shallow").value_or(false))
url.query.insert_or_assign("shallow", "1");
return url;
}
bool hasAllInfo(const Input & input) override
{
bool maybeDirty = !input.getRef();
bool shallow = maybeGetBoolAttr(input.attrs, "shallow").value_or(false);
return
maybeGetIntAttr(input.attrs, "lastModified")
&& (shallow || maybeDirty || maybeGetIntAttr(input.attrs, "revCount"));
}
Input applyOverrides(
const Input & input,
std::optional<std::string> ref,
std::optional<Hash> rev) override
{
auto res(input);
if (rev) res.attrs.insert_or_assign("rev", rev->gitRev());
if (ref) res.attrs.insert_or_assign("ref", *ref);
if (!res.getRef() && res.getRev())
throw Error("Git input '%s' has a commit hash but no branch/tag name", res.to_string());
return res;
}
void clone(const Input & input, const Path & destDir) override
{
auto [isLocal, actualUrl] = getActualUrl(input);
Strings args = {"clone"}; Strings args = {"clone"};
args.push_back(actualUrl); args.push_back(actualUrl);
if (ref) { if (auto ref = input.getRef()) {
args.push_back("--branch"); args.push_back("--branch");
args.push_back(*ref); args.push_back(*ref);
} }
if (rev) throw Error("cloning a specific revision is not implemented"); if (input.getRev()) throw Error("cloning a specific revision is not implemented");
args.push_back(destDir); args.push_back(destDir);
runProgram("git", true, args); runProgram("git", true, args);
} }
std::shared_ptr<const Input> applyOverrides( std::optional<Path> getSourcePath(const Input & input) override
std::optional<std::string> ref,
std::optional<Hash> rev) const override
{ {
if (!ref && !rev) return shared_from_this(); auto url = parseURL(getStrAttr(input.attrs, "url"));
if (url.scheme == "file" && !input.getRef() && !input.getRev())
auto res = std::make_shared<GitInput>(*this);
if (ref) res->ref = ref;
if (rev) res->rev = rev;
if (!res->ref && res->rev)
throw Error("Git input '%s' has a commit hash but no branch/tag name", res->to_string());
return res;
}
std::optional<Path> getSourcePath() const override
{
if (url.scheme == "file" && !ref && !rev)
return url.path; return url.path;
return {}; return {};
} }
void markChangedFile(std::string_view file, std::optional<std::string> commitMsg) const override void markChangedFile(const Input & input, std::string_view file, std::optional<std::string> commitMsg) override
{ {
auto sourcePath = getSourcePath(); auto sourcePath = getSourcePath(input);
assert(sourcePath); assert(sourcePath);
runProgram("git", true, runProgram("git", true,
@ -136,23 +147,25 @@ struct GitInput : Input
{ "-C", *sourcePath, "commit", std::string(file), "-m", *commitMsg }); { "-C", *sourcePath, "commit", std::string(file), "-m", *commitMsg });
} }
std::pair<bool, std::string> getActualUrl() const std::pair<bool, std::string> getActualUrl(const Input & input) const
{ {
// Don't clone file:// URIs (but otherwise treat them the // Don't clone file:// URIs (but otherwise treat them the
// same as remote URIs, i.e. don't use the working tree or // same as remote URIs, i.e. don't use the working tree or
// HEAD). // HEAD).
static bool forceHttp = getEnv("_NIX_FORCE_HTTP") == "1"; // for testing static bool forceHttp = getEnv("_NIX_FORCE_HTTP") == "1"; // for testing
auto url = parseURL(getStrAttr(input.attrs, "url"));
bool isLocal = url.scheme == "file" && !forceHttp; bool isLocal = url.scheme == "file" && !forceHttp;
return {isLocal, isLocal ? url.path : url.base}; return {isLocal, isLocal ? url.path : url.base};
} }
std::pair<Tree, std::shared_ptr<const Input>> fetchTreeInternal(nix::ref<Store> store) const override std::pair<Tree, Input> fetch(ref<Store> store, const Input & _input) override
{ {
auto name = "source"; auto name = "source";
auto input = std::make_shared<GitInput>(*this); Input input(_input);
assert(!rev || rev->type == htSHA1); bool shallow = maybeGetBoolAttr(input.attrs, "shallow").value_or(false);
bool submodules = maybeGetBoolAttr(input.attrs, "submodules").value_or(false);
std::string cacheType = "git"; std::string cacheType = "git";
if (shallow) cacheType += "-shallow"; if (shallow) cacheType += "-shallow";
@ -163,39 +176,38 @@ struct GitInput : Input
return Attrs({ return Attrs({
{"type", cacheType}, {"type", cacheType},
{"name", name}, {"name", name},
{"rev", input->rev->gitRev()}, {"rev", input.getRev()->gitRev()},
}); });
}; };
auto makeResult = [&](const Attrs & infoAttrs, StorePath && storePath) auto makeResult = [&](const Attrs & infoAttrs, StorePath && storePath)
-> std::pair<Tree, std::shared_ptr<const Input>> -> std::pair<Tree, Input>
{ {
assert(input->rev); assert(input.getRev());
assert(!rev || rev == input->rev); assert(!_input.getRev() || _input.getRev() == input.getRev());
if (!shallow)
input.attrs.insert_or_assign("revCount", getIntAttr(infoAttrs, "revCount"));
input.attrs.insert_or_assign("lastModified", getIntAttr(infoAttrs, "lastModified"));
return { return {
Tree { Tree {
.actualPath = store->toRealPath(storePath), .actualPath = store->toRealPath(storePath),
.storePath = std::move(storePath), .storePath = std::move(storePath),
.info = TreeInfo {
.revCount = shallow ? std::nullopt : std::optional(getIntAttr(infoAttrs, "revCount")),
.lastModified = getIntAttr(infoAttrs, "lastModified"),
},
}, },
input input
}; };
}; };
if (rev) { if (input.getRev()) {
if (auto res = getCache()->lookup(store, getImmutableAttrs())) if (auto res = getCache()->lookup(store, getImmutableAttrs()))
return makeResult(res->first, std::move(res->second)); return makeResult(res->first, std::move(res->second));
} }
auto [isLocal, actualUrl_] = getActualUrl(); auto [isLocal, actualUrl_] = getActualUrl(input);
auto actualUrl = actualUrl_; // work around clang bug auto actualUrl = actualUrl_; // work around clang bug
// If this is a local directory and no ref or revision is // If this is a local directory and no ref or revision is
// given, then allow the use of an unclean working tree. // given, then allow the use of an unclean working tree.
if (!input->ref && !input->rev && isLocal) { if (!input.getRef() && !input.getRev() && isLocal) {
bool clean = false; bool clean = false;
/* Check whether this repo has any commits. There are /* Check whether this repo has any commits. There are
@ -254,35 +266,37 @@ struct GitInput : Input
auto storePath = store->addToStore("source", actualUrl, true, htSHA256, filter); auto storePath = store->addToStore("source", actualUrl, true, htSHA256, filter);
auto tree = Tree { // FIXME: maybe we should use the timestamp of the last
.actualPath = store->printStorePath(storePath), // modified dirty file?
.storePath = std::move(storePath), input.attrs.insert_or_assign(
.info = TreeInfo { "lastModified",
// FIXME: maybe we should use the timestamp of the last haveCommits ? std::stoull(runProgram("git", true, { "-C", actualUrl, "log", "-1", "--format=%ct", "HEAD" })) : 0);
// modified dirty file?
.lastModified = haveCommits ? std::stoull(runProgram("git", true, { "-C", actualUrl, "log", "-1", "--format=%ct", "HEAD" })) : 0,
}
};
return {std::move(tree), input}; return {
Tree {
.actualPath = store->printStorePath(storePath),
.storePath = std::move(storePath),
}, input
};
} }
} }
if (!input->ref) input->ref = isLocal ? readHead(actualUrl) : "master"; if (!input.getRef()) input.attrs.insert_or_assign("ref", isLocal ? readHead(actualUrl) : "master");
Attrs mutableAttrs({ Attrs mutableAttrs({
{"type", cacheType}, {"type", cacheType},
{"name", name}, {"name", name},
{"url", actualUrl}, {"url", actualUrl},
{"ref", *input->ref}, {"ref", *input.getRef()},
}); });
Path repoDir; Path repoDir;
if (isLocal) { if (isLocal) {
if (!input->rev) if (!input.getRev())
input->rev = Hash(chomp(runProgram("git", true, { "-C", actualUrl, "rev-parse", *input->ref })), htSHA1); input.attrs.insert_or_assign("rev",
Hash(chomp(runProgram("git", true, { "-C", actualUrl, "rev-parse", *input.getRef() })), htSHA1).gitRev());
repoDir = actualUrl; repoDir = actualUrl;
@ -290,8 +304,8 @@ struct GitInput : Input
if (auto res = getCache()->lookup(store, mutableAttrs)) { if (auto res = getCache()->lookup(store, mutableAttrs)) {
auto rev2 = Hash(getStrAttr(res->first, "rev"), htSHA1); auto rev2 = Hash(getStrAttr(res->first, "rev"), htSHA1);
if (!rev || rev == rev2) { if (!input.getRev() || input.getRev() == rev2) {
input->rev = rev2; input.attrs.insert_or_assign("rev", rev2.gitRev());
return makeResult(res->first, std::move(res->second)); return makeResult(res->first, std::move(res->second));
} }
} }
@ -305,18 +319,18 @@ struct GitInput : Input
} }
Path localRefFile = Path localRefFile =
input->ref->compare(0, 5, "refs/") == 0 input.getRef()->compare(0, 5, "refs/") == 0
? cacheDir + "/" + *input->ref ? cacheDir + "/" + *input.getRef()
: cacheDir + "/refs/heads/" + *input->ref; : cacheDir + "/refs/heads/" + *input.getRef();
bool doFetch; bool doFetch;
time_t now = time(0); time_t now = time(0);
/* If a rev was specified, we need to fetch if it's not in the /* If a rev was specified, we need to fetch if it's not in the
repo. */ repo. */
if (input->rev) { if (input.getRev()) {
try { try {
runProgram("git", true, { "-C", repoDir, "cat-file", "-e", input->rev->gitRev() }); runProgram("git", true, { "-C", repoDir, "cat-file", "-e", input.getRev()->gitRev() });
doFetch = false; doFetch = false;
} catch (ExecError & e) { } catch (ExecError & e) {
if (WIFEXITED(e.status)) { if (WIFEXITED(e.status)) {
@ -339,7 +353,7 @@ struct GitInput : Input
// FIXME: git stderr messes up our progress indicator, so // FIXME: git stderr messes up our progress indicator, so
// we're using --quiet for now. Should process its stderr. // we're using --quiet for now. Should process its stderr.
try { try {
runProgram("git", true, { "-C", repoDir, "fetch", "--quiet", "--force", "--", actualUrl, fmt("%s:%s", *input->ref, *input->ref) }); runProgram("git", true, { "-C", repoDir, "fetch", "--quiet", "--force", "--", actualUrl, fmt("%s:%s", *input.getRef(), *input.getRef()) });
} catch (Error & e) { } catch (Error & e) {
if (!pathExists(localRefFile)) throw; if (!pathExists(localRefFile)) throw;
warn("could not update local clone of Git repository '%s'; continuing with the most recent version", actualUrl); warn("could not update local clone of Git repository '%s'; continuing with the most recent version", actualUrl);
@ -354,8 +368,8 @@ struct GitInput : Input
utimes(localRefFile.c_str(), times); utimes(localRefFile.c_str(), times);
} }
if (!input->rev) if (!input.getRev())
input->rev = Hash(chomp(readFile(localRefFile)), htSHA1); input.attrs.insert_or_assign("rev", Hash(chomp(readFile(localRefFile)), htSHA1).gitRev());
} }
bool isShallow = chomp(runProgram("git", true, { "-C", repoDir, "rev-parse", "--is-shallow-repository" })) == "true"; bool isShallow = chomp(runProgram("git", true, { "-C", repoDir, "rev-parse", "--is-shallow-repository" })) == "true";
@ -365,7 +379,7 @@ struct GitInput : Input
// FIXME: check whether rev is an ancestor of ref. // FIXME: check whether rev is an ancestor of ref.
printTalkative("using revision %s of repo '%s'", input->rev->gitRev(), actualUrl); printTalkative("using revision %s of repo '%s'", input.getRev()->gitRev(), actualUrl);
/* Now that we know the ref, check again whether we have it in /* Now that we know the ref, check again whether we have it in
the store. */ the store. */
@ -387,7 +401,7 @@ struct GitInput : Input
runProgram("git", true, { "-C", tmpDir, "fetch", "--quiet", "--force", runProgram("git", true, { "-C", tmpDir, "fetch", "--quiet", "--force",
"--update-head-ok", "--", repoDir, "refs/*:refs/*" }); "--update-head-ok", "--", repoDir, "refs/*:refs/*" });
runProgram("git", true, { "-C", tmpDir, "checkout", "--quiet", input->rev->gitRev() }); runProgram("git", true, { "-C", tmpDir, "checkout", "--quiet", input.getRev()->gitRev() });
runProgram("git", true, { "-C", tmpDir, "remote", "add", "origin", actualUrl }); runProgram("git", true, { "-C", tmpDir, "remote", "add", "origin", actualUrl });
runProgram("git", true, { "-C", tmpDir, "submodule", "--quiet", "update", "--init", "--recursive" }); runProgram("git", true, { "-C", tmpDir, "submodule", "--quiet", "update", "--init", "--recursive" });
@ -396,7 +410,7 @@ struct GitInput : Input
// FIXME: should pipe this, or find some better way to extract a // FIXME: should pipe this, or find some better way to extract a
// revision. // revision.
auto source = sinkToSource([&](Sink & sink) { auto source = sinkToSource([&](Sink & sink) {
RunOptions gitOptions("git", { "-C", repoDir, "archive", input->rev->gitRev() }); RunOptions gitOptions("git", { "-C", repoDir, "archive", input.getRev()->gitRev() });
gitOptions.standardOut = &sink; gitOptions.standardOut = &sink;
runProgram2(gitOptions); runProgram2(gitOptions);
}); });
@ -406,18 +420,18 @@ struct GitInput : Input
auto storePath = store->addToStore(name, tmpDir, true, htSHA256, filter); auto storePath = store->addToStore(name, tmpDir, true, htSHA256, filter);
auto lastModified = std::stoull(runProgram("git", true, { "-C", repoDir, "log", "-1", "--format=%ct", input->rev->gitRev() })); auto lastModified = std::stoull(runProgram("git", true, { "-C", repoDir, "log", "-1", "--format=%ct", input.getRev()->gitRev() }));
Attrs infoAttrs({ Attrs infoAttrs({
{"rev", input->rev->gitRev()}, {"rev", input.getRev()->gitRev()},
{"lastModified", lastModified}, {"lastModified", lastModified},
}); });
if (!shallow) if (!shallow)
infoAttrs.insert_or_assign("revCount", infoAttrs.insert_or_assign("revCount",
std::stoull(runProgram("git", true, { "-C", repoDir, "rev-list", "--count", input->rev->gitRev() }))); std::stoull(runProgram("git", true, { "-C", repoDir, "rev-list", "--count", input.getRev()->gitRev() })));
if (!this->rev) if (!_input.getRev())
getCache()->add( getCache()->add(
store, store,
mutableAttrs, mutableAttrs,
@ -436,60 +450,6 @@ struct GitInput : Input
} }
}; };
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" && name != "submodules")
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);
input->submodules = maybeGetBoolAttr(attrs, "submodules").value_or(false);
return input;
}
};
static auto r1 = OnStartup([] { registerInputScheme(std::make_unique<GitInputScheme>()); }); static auto r1 = OnStartup([] { registerInputScheme(std::make_unique<GitInputScheme>()); });
} }

View file

@ -8,37 +8,80 @@
namespace nix::fetchers { namespace nix::fetchers {
struct GitArchiveInput : Input struct GitArchiveInputScheme : InputScheme
{ {
std::string owner; virtual std::string type() = 0;
std::string repo;
std::optional<std::string> ref;
std::optional<Hash> rev;
virtual std::shared_ptr<GitArchiveInput> _clone() const = 0; std::optional<Input> inputFromURL(const ParsedURL & url) override
bool operator ==(const Input & other) const override
{ {
auto other2 = dynamic_cast<const GitArchiveInput *>(&other); if (url.scheme != type()) return {};
return
other2 auto path = tokenizeString<std::vector<std::string>>(url.path, "/");
&& owner == other2->owner
&& repo == other2->repo std::optional<Hash> rev;
&& rev == other2->rev std::optional<std::string> ref;
&& ref == other2->ref;
if (path.size() == 2) {
} else if (path.size() == 3) {
if (std::regex_match(path[2], revRegex))
rev = Hash(path[2], htSHA1);
else if (std::regex_match(path[2], refRegex))
ref = path[2];
else
throw BadURL("in URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[2]);
} else
throw BadURL("URL '%s' is invalid", url.url);
for (auto &[name, value] : url.query) {
if (name == "rev") {
if (rev)
throw BadURL("URL '%s' contains multiple commit hashes", url.url);
rev = Hash(value, htSHA1);
}
else if (name == "ref") {
if (!std::regex_match(value, refRegex))
throw BadURL("URL '%s' contains an invalid branch/tag name", url.url);
if (ref)
throw BadURL("URL '%s' contains multiple branch/tag names", url.url);
ref = value;
}
}
if (ref && rev)
throw BadURL("URL '%s' contains both a commit hash and a branch/tag name", url.url);
Input input;
input.attrs.insert_or_assign("type", type());
input.attrs.insert_or_assign("owner", path[0]);
input.attrs.insert_or_assign("repo", path[1]);
if (rev) input.attrs.insert_or_assign("rev", rev->gitRev());
if (ref) input.attrs.insert_or_assign("ref", *ref);
return input;
} }
bool isImmutable() const override std::optional<Input> inputFromAttrs(const Attrs & attrs) override
{ {
return (bool) rev || narHash; if (maybeGetStrAttr(attrs, "type") != type()) return {};
for (auto & [name, value] : attrs)
if (name != "type" && name != "owner" && name != "repo" && name != "ref" && name != "rev" && name != "narHash" && name != "lastModified")
throw Error("unsupported input attribute '%s'", name);
getStrAttr(attrs, "owner");
getStrAttr(attrs, "repo");
Input input;
input.attrs = attrs;
return input;
} }
std::optional<std::string> getRef() const override { return ref; } ParsedURL toURL(const Input & input) override
std::optional<Hash> getRev() const override { return rev; }
ParsedURL toURL() const override
{ {
auto owner = getStrAttr(input.attrs, "owner");
auto repo = getStrAttr(input.attrs, "repo");
auto ref = input.getRef();
auto rev = input.getRev();
auto path = owner + "/" + repo; auto path = owner + "/" + repo;
assert(!(ref && rev)); assert(!(ref && rev));
if (ref) path += "/" + *ref; if (ref) path += "/" + *ref;
@ -49,32 +92,44 @@ struct GitArchiveInput : Input
}; };
} }
Attrs toAttrsInternal() const override bool hasAllInfo(const Input & input) override
{ {
Attrs attrs; return input.getRev() && maybeGetIntAttr(input.attrs, "lastModified");
attrs.emplace("owner", owner);
attrs.emplace("repo", repo);
if (ref)
attrs.emplace("ref", *ref);
if (rev)
attrs.emplace("rev", rev->gitRev());
return attrs;
} }
virtual Hash getRevFromRef(nix::ref<Store> store, std::string_view ref) const = 0; Input applyOverrides(
const Input & _input,
virtual std::string getDownloadUrl() const = 0; std::optional<std::string> ref,
std::optional<Hash> rev) override
std::pair<Tree, std::shared_ptr<const Input>> fetchTreeInternal(nix::ref<Store> store) const override
{ {
auto rev = this->rev; auto input(_input);
auto ref = this->ref.value_or("master"); if (rev) {
input.attrs.insert_or_assign("rev", rev->gitRev());
input.attrs.erase("ref");
}
if (ref) {
if (input.getRev())
throw BadURL("input '%s' contains both a commit hash and a branch/tag name", input.to_string());
input.attrs.insert_or_assign("ref", *ref);
}
return input;
}
if (!rev) rev = getRevFromRef(store, ref); virtual Hash getRevFromRef(nix::ref<Store> store, const Input & input) const = 0;
auto input = _clone(); virtual std::string getDownloadUrl(const Input & input) const = 0;
input->ref = {};
input->rev = *rev; std::pair<Tree, Input> fetch(ref<Store> store, const Input & _input) override
{
Input input(_input);
if (!maybeGetStrAttr(input.attrs, "ref")) input.attrs.insert_or_assign("ref", "master");
auto rev = input.getRev();
if (!rev) rev = getRevFromRef(store, input);
input.attrs.erase("ref");
input.attrs.insert_or_assign("rev", rev->gitRev());
Attrs immutableAttrs({ Attrs immutableAttrs({
{"type", "git-tarball"}, {"type", "git-tarball"},
@ -82,131 +137,44 @@ struct GitArchiveInput : Input
}); });
if (auto res = getCache()->lookup(store, immutableAttrs)) { if (auto res = getCache()->lookup(store, immutableAttrs)) {
input.attrs.insert_or_assign("lastModified", getIntAttr(res->first, "lastModified"));
return { return {
Tree{ Tree{
.actualPath = store->toRealPath(res->second), .actualPath = store->toRealPath(res->second),
.storePath = std::move(res->second), .storePath = std::move(res->second),
.info = TreeInfo {
.lastModified = getIntAttr(res->first, "lastModified"),
},
}, },
input input
}; };
} }
auto url = input->getDownloadUrl(); auto url = getDownloadUrl(input);
auto [tree, lastModified] = downloadTarball(store, url, "source", true);
auto tree = downloadTarball(store, url, "source", true); input.attrs.insert_or_assign("lastModified", lastModified);
getCache()->add( getCache()->add(
store, store,
immutableAttrs, immutableAttrs,
{ {
{"rev", rev->gitRev()}, {"rev", rev->gitRev()},
{"lastModified", *tree.info.lastModified} {"lastModified", lastModified}
}, },
tree.storePath, tree.storePath,
true); true);
return {std::move(tree), input}; return {std::move(tree), input};
} }
std::shared_ptr<const Input> applyOverrides(
std::optional<std::string> ref,
std::optional<Hash> rev) const override
{
if (!ref && !rev) return shared_from_this();
auto res = _clone();
if (ref) res->ref = ref;
if (rev) res->rev = rev;
return res;
}
}; };
struct GitArchiveInputScheme : InputScheme struct GitHubInputScheme : GitArchiveInputScheme
{ {
std::string type; std::string type() override { return "github"; }
GitArchiveInputScheme(std::string && type) : type(type) Hash getRevFromRef(nix::ref<Store> store, const Input & input) const override
{ }
virtual std::unique_ptr<GitArchiveInput> create() = 0;
std::unique_ptr<Input> inputFromURL(const ParsedURL & url) override
{
if (url.scheme != type) return nullptr;
auto path = tokenizeString<std::vector<std::string>>(url.path, "/");
auto input = create();
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 URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[2]);
} else
throw BadURL("URL '%s' is invalid", url.url);
for (auto &[name, value] : url.query) {
if (name == "rev") {
if (input->rev)
throw BadURL("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("URL '%s' contains an invalid branch/tag name", url.url);
if (input->ref)
throw BadURL("URL '%s' contains multiple branch/tag names", url.url);
input->ref = value;
}
}
if (input->ref && input->rev)
throw BadURL("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") != type) return {};
for (auto & [name, value] : attrs)
if (name != "type" && name != "owner" && name != "repo" && name != "ref" && name != "rev")
throw Error("unsupported input attribute '%s'", name);
auto input = create();
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;
}
};
struct GitHubInput : GitArchiveInput
{
std::string type() const override { return "github"; }
std::shared_ptr<GitArchiveInput> _clone() const override
{ return std::make_shared<GitHubInput>(*this); }
Hash getRevFromRef(nix::ref<Store> store, std::string_view ref) const override
{ {
auto url = fmt("https://api.github.com/repos/%s/%s/commits/%s", auto url = fmt("https://api.github.com/repos/%s/%s/commits/%s",
owner, repo, ref); getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"), *input.getRef());
auto json = nlohmann::json::parse( auto json = nlohmann::json::parse(
readFile( readFile(
store->toRealPath( store->toRealPath(
@ -216,13 +184,14 @@ struct GitHubInput : GitArchiveInput
return rev; return rev;
} }
std::string getDownloadUrl() const override std::string getDownloadUrl(const Input & input) const override
{ {
// FIXME: use regular /archive URLs instead? api.github.com // FIXME: use regular /archive URLs instead? api.github.com
// might have stricter rate limits. // might have stricter rate limits.
auto url = fmt("https://api.github.com/repos/%s/%s/tarball/%s", auto url = fmt("https://api.github.com/repos/%s/%s/tarball/%s",
owner, repo, rev->to_string(Base16, false)); getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"),
input.getRev()->to_string(Base16, false));
std::string accessToken = settings.githubAccessToken.get(); std::string accessToken = settings.githubAccessToken.get();
if (accessToken != "") if (accessToken != "")
@ -231,35 +200,23 @@ struct GitHubInput : GitArchiveInput
return url; return url;
} }
void clone(const Path & destDir) const override void clone(const Input & input, const Path & destDir) override
{ {
std::shared_ptr<const Input> input = inputFromURL(fmt("git+ssh://git@github.com/%s/%s.git", owner, repo)); Input::fromURL(fmt("git+ssh://git@github.com/%s/%s.git",
input = input->applyOverrides(ref.value_or("master"), rev); getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo")))
input->clone(destDir); .applyOverrides(input.getRef().value_or("master"), input.getRev())
.clone(destDir);
} }
}; };
struct GitHubInputScheme : GitArchiveInputScheme struct GitLabInputScheme : GitArchiveInputScheme
{ {
GitHubInputScheme() : GitArchiveInputScheme("github") { } std::string type() override { return "gitlab"; }
std::unique_ptr<GitArchiveInput> create() override Hash getRevFromRef(nix::ref<Store> store, const Input & input) const override
{
return std::make_unique<GitHubInput>();
}
};
struct GitLabInput : GitArchiveInput
{
std::string type() const override { return "gitlab"; }
std::shared_ptr<GitArchiveInput> _clone() const override
{ return std::make_shared<GitLabInput>(*this); }
Hash getRevFromRef(nix::ref<Store> store, std::string_view ref) const override
{ {
auto url = fmt("https://gitlab.com/api/v4/projects/%s%%2F%s/repository/branches/%s", auto url = fmt("https://gitlab.com/api/v4/projects/%s%%2F%s/repository/branches/%s",
owner, repo, ref); getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"), *input.getRef());
auto json = nlohmann::json::parse( auto json = nlohmann::json::parse(
readFile( readFile(
store->toRealPath( store->toRealPath(
@ -269,12 +226,13 @@ struct GitLabInput : GitArchiveInput
return rev; return rev;
} }
std::string getDownloadUrl() const override std::string getDownloadUrl(const Input & input) const override
{ {
// FIXME: This endpoint has a rate limit threshold of 5 requests per minute. // FIXME: This endpoint has a rate limit threshold of 5 requests per minute.
auto url = fmt("https://gitlab.com/api/v4/projects/%s%%2F%s/repository/archive.tar.gz?sha=%s", auto url = fmt("https://gitlab.com/api/v4/projects/%s%%2F%s/repository/archive.tar.gz?sha=%s",
owner, repo, rev->to_string(Base16, false)); getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"),
input.getRev()->to_string(Base16, false));
/* # FIXME: add privat token auth (`curl --header "PRIVATE-TOKEN: <your_access_token>"`) /* # FIXME: add privat token auth (`curl --header "PRIVATE-TOKEN: <your_access_token>"`)
std::string accessToken = settings.githubAccessToken.get(); std::string accessToken = settings.githubAccessToken.get();
@ -284,21 +242,12 @@ struct GitLabInput : GitArchiveInput
return url; return url;
} }
void clone(const Path & destDir) const override void clone(const Input & input, const Path & destDir) override
{ {
std::shared_ptr<const Input> input = inputFromURL(fmt("git+ssh://git@gitlab.com/%s/%s.git", owner, repo)); Input::fromURL(fmt("git+ssh://git@gitlab.com/%s/%s.git",
input = input->applyOverrides(ref.value_or("master"), rev); getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo")))
input->clone(destDir); .applyOverrides(input.getRef().value_or("master"), input.getRev())
} .clone(destDir);
};
struct GitLabInputScheme : GitArchiveInputScheme
{
GitLabInputScheme() : GitArchiveInputScheme("gitlab") { }
std::unique_ptr<GitArchiveInput> create() override
{
return std::make_unique<GitLabInput>();
} }
}; };

View file

@ -4,135 +4,99 @@ namespace nix::fetchers {
std::regex flakeRegex("[a-zA-Z][a-zA-Z0-9_-]*", std::regex::ECMAScript); std::regex flakeRegex("[a-zA-Z][a-zA-Z0-9_-]*", std::regex::ECMAScript);
struct IndirectInput : Input
{
std::string id;
std::optional<Hash> rev;
std::optional<std::string> ref;
std::string type() const override { return "indirect"; }
bool operator ==(const Input & other) const override
{
auto other2 = dynamic_cast<const IndirectInput *>(&other);
return
other2
&& id == other2->id
&& rev == other2->rev
&& ref == other2->ref;
}
bool isDirect() const override
{
return false;
}
std::optional<std::string> getRef() const override { return ref; }
std::optional<Hash> getRev() const override { return rev; }
bool contains(const Input & other) const override
{
auto other2 = dynamic_cast<const IndirectInput *>(&other);
return
other2
&& id == other2->id
&& (!ref || ref == other2->ref)
&& (!rev || rev == other2->rev);
}
ParsedURL toURL() const override
{
ParsedURL url;
url.scheme = "flake";
url.path = id;
if (ref) { url.path += '/'; url.path += *ref; };
if (rev) { url.path += '/'; url.path += rev->gitRev(); };
return url;
}
Attrs toAttrsInternal() const override
{
Attrs attrs;
attrs.emplace("id", id);
if (ref)
attrs.emplace("ref", *ref);
if (rev)
attrs.emplace("rev", rev->gitRev());
return attrs;
}
std::shared_ptr<const Input> applyOverrides(
std::optional<std::string> ref,
std::optional<Hash> rev) const override
{
if (!ref && !rev) return shared_from_this();
auto res = std::make_shared<IndirectInput>(*this);
if (ref) res->ref = ref;
if (rev) res->rev = rev;
return res;
}
std::pair<Tree, std::shared_ptr<const Input>> fetchTreeInternal(nix::ref<Store> store) const override
{
throw Error("indirect input '%s' cannot be fetched directly", to_string());
}
};
struct IndirectInputScheme : InputScheme struct IndirectInputScheme : InputScheme
{ {
std::unique_ptr<Input> inputFromURL(const ParsedURL & url) override std::optional<Input> inputFromURL(const ParsedURL & url) override
{ {
if (url.scheme != "flake") return nullptr; if (url.scheme != "flake") return {};
auto path = tokenizeString<std::vector<std::string>>(url.path, "/"); auto path = tokenizeString<std::vector<std::string>>(url.path, "/");
auto input = std::make_unique<IndirectInput>();
std::optional<Hash> rev;
std::optional<std::string> ref;
if (path.size() == 1) { if (path.size() == 1) {
} else if (path.size() == 2) { } else if (path.size() == 2) {
if (std::regex_match(path[1], revRegex)) if (std::regex_match(path[1], revRegex))
input->rev = Hash(path[1], htSHA1); rev = Hash(path[1], htSHA1);
else if (std::regex_match(path[1], refRegex)) else if (std::regex_match(path[1], refRegex))
input->ref = path[1]; ref = path[1];
else else
throw BadURL("in flake URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[1]); throw BadURL("in flake URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[1]);
} else if (path.size() == 3) { } else if (path.size() == 3) {
if (!std::regex_match(path[1], refRegex)) if (!std::regex_match(path[1], refRegex))
throw BadURL("in flake URL '%s', '%s' is not a branch/tag name", url.url, path[1]); throw BadURL("in flake URL '%s', '%s' is not a branch/tag name", url.url, path[1]);
input->ref = path[1]; ref = path[1];
if (!std::regex_match(path[2], revRegex)) if (!std::regex_match(path[2], revRegex))
throw BadURL("in flake URL '%s', '%s' is not a commit hash", url.url, path[2]); throw BadURL("in flake URL '%s', '%s' is not a commit hash", url.url, path[2]);
input->rev = Hash(path[2], htSHA1); rev = Hash(path[2], htSHA1);
} else } else
throw BadURL("GitHub URL '%s' is invalid", url.url); throw BadURL("GitHub URL '%s' is invalid", url.url);
std::string id = path[0];
if (!std::regex_match(id, flakeRegex))
throw BadURL("'%s' is not a valid flake ID", id);
// FIXME: forbid query params? // FIXME: forbid query params?
input->id = path[0]; Input input;
if (!std::regex_match(input->id, flakeRegex)) input.direct = false;
throw BadURL("'%s' is not a valid flake ID", input->id); input.attrs.insert_or_assign("type", "indirect");
input.attrs.insert_or_assign("id", id);
if (rev) input.attrs.insert_or_assign("rev", rev->gitRev());
if (ref) input.attrs.insert_or_assign("ref", *ref);
return input; return input;
} }
std::unique_ptr<Input> inputFromAttrs(const Attrs & attrs) override std::optional<Input> inputFromAttrs(const Attrs & attrs) override
{ {
if (maybeGetStrAttr(attrs, "type") != "indirect") return {}; if (maybeGetStrAttr(attrs, "type") != "indirect") return {};
for (auto & [name, value] : attrs) for (auto & [name, value] : attrs)
if (name != "type" && name != "id" && name != "ref" && name != "rev") if (name != "type" && name != "id" && name != "ref" && name != "rev" && name != "narHash")
throw Error("unsupported indirect input attribute '%s'", name); throw Error("unsupported indirect input attribute '%s'", name);
auto input = std::make_unique<IndirectInput>(); auto id = getStrAttr(attrs, "id");
input->id = getStrAttr(attrs, "id"); if (!std::regex_match(id, flakeRegex))
input->ref = maybeGetStrAttr(attrs, "ref"); throw BadURL("'%s' is not a valid flake ID", id);
if (auto rev = maybeGetStrAttr(attrs, "rev"))
input->rev = Hash(*rev, htSHA1); Input input;
input.direct = false;
input.attrs = attrs;
return input; return input;
} }
ParsedURL toURL(const Input & input) override
{
ParsedURL url;
url.scheme = "flake";
url.path = getStrAttr(input.attrs, "id");
if (auto ref = input.getRef()) { url.path += '/'; url.path += *ref; };
if (auto rev = input.getRev()) { url.path += '/'; url.path += rev->gitRev(); };
return url;
}
bool hasAllInfo(const Input & input) override
{
return false;
}
Input applyOverrides(
const Input & _input,
std::optional<std::string> ref,
std::optional<Hash> rev) override
{
auto input(_input);
if (rev) input.attrs.insert_or_assign("rev", rev->gitRev());
if (ref) input.attrs.insert_or_assign("ref", *ref);
return input;
}
std::pair<Tree, Input> fetch(ref<Store> store, const Input & input) override
{
throw Error("indirect input '%s' cannot be fetched directly", input.to_string());
}
}; };
static auto r1 = OnStartup([] { registerInputScheme(std::make_unique<IndirectInputScheme>()); }); static auto r1 = OnStartup([] { registerInputScheme(std::make_unique<IndirectInputScheme>()); });

View file

@ -10,80 +10,92 @@ using namespace std::string_literals;
namespace nix::fetchers { namespace nix::fetchers {
struct MercurialInput : Input struct MercurialInputScheme : InputScheme
{ {
ParsedURL url; std::optional<Input> inputFromURL(const ParsedURL & url) override
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); if (url.scheme != "hg+http" &&
return url.scheme != "hg+https" &&
other2 url.scheme != "hg+ssh" &&
&& url == other2->url url.scheme != "hg+file") return {};
&& rev == other2->rev
&& ref == other2->ref; 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);
} }
bool isImmutable() const override std::optional<Input> inputFromAttrs(const Attrs & attrs) override
{ {
return (bool) rev || narHash; if (maybeGetStrAttr(attrs, "type") != "hg") return {};
for (auto & [name, value] : attrs)
if (name != "type" && name != "url" && name != "ref" && name != "rev" && name != "revCount" && name != "narHash")
throw Error("unsupported Mercurial input attribute '%s'", name);
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 input;
input.attrs = attrs;
return input;
} }
std::optional<std::string> getRef() const override { return ref; } ParsedURL toURL(const Input & input) override
std::optional<Hash> getRev() const override { return rev; }
ParsedURL toURL() const override
{ {
ParsedURL url2(url); auto url = parseURL(getStrAttr(input.attrs, "url"));
url2.scheme = "hg+" + url2.scheme; url.scheme = "hg+" + url.scheme;
if (rev) url2.query.insert_or_assign("rev", rev->gitRev()); if (auto rev = input.getRev()) url.query.insert_or_assign("rev", rev->gitRev());
if (ref) url2.query.insert_or_assign("ref", *ref); if (auto ref = input.getRef()) url.query.insert_or_assign("ref", *ref);
return url; return url;
} }
Attrs toAttrsInternal() const override bool hasAllInfo(const Input & input) override
{ {
Attrs attrs; // FIXME: ugly, need to distinguish between dirty and clean
attrs.emplace("url", url.to_string()); // default trees.
if (ref) return input.getRef() == "default" || maybeGetIntAttr(input.attrs, "revCount");
attrs.emplace("ref", *ref);
if (rev)
attrs.emplace("rev", rev->gitRev());
return attrs;
} }
std::shared_ptr<const Input> applyOverrides( Input applyOverrides(
const Input & input,
std::optional<std::string> ref, std::optional<std::string> ref,
std::optional<Hash> rev) const override std::optional<Hash> rev) override
{ {
if (!ref && !rev) return shared_from_this(); auto res(input);
if (rev) res.attrs.insert_or_assign("rev", rev->gitRev());
auto res = std::make_shared<MercurialInput>(*this); if (ref) res.attrs.insert_or_assign("ref", *ref);
if (ref) res->ref = ref;
if (rev) res->rev = rev;
return res; return res;
} }
std::optional<Path> getSourcePath() const std::optional<Path> getSourcePath(const Input & input) override
{ {
if (url.scheme == "file" && !ref && !rev) auto url = parseURL(getStrAttr(input.attrs, "url"));
if (url.scheme == "file" && !input.getRef() && !input.getRev())
return url.path; return url.path;
return {}; return {};
} }
void markChangedFile(std::string_view file, std::optional<std::string> commitMsg) const override void markChangedFile(const Input & input, std::string_view file, std::optional<std::string> commitMsg) override
{ {
auto sourcePath = getSourcePath(); auto sourcePath = getSourcePath(input);
assert(sourcePath); assert(sourcePath);
// FIXME: shut up if file is already tracked. // FIXME: shut up if file is already tracked.
@ -95,26 +107,27 @@ struct MercurialInput : Input
{ "commit", *sourcePath + "/" + std::string(file), "-m", *commitMsg }); { "commit", *sourcePath + "/" + std::string(file), "-m", *commitMsg });
} }
std::pair<bool, std::string> getActualUrl() const std::pair<bool, std::string> getActualUrl(const Input & input) const
{ {
auto url = parseURL(getStrAttr(input.attrs, "url"));
bool isLocal = url.scheme == "file"; bool isLocal = url.scheme == "file";
return {isLocal, isLocal ? url.path : url.base}; return {isLocal, isLocal ? url.path : url.base};
} }
std::pair<Tree, std::shared_ptr<const Input>> fetchTreeInternal(nix::ref<Store> store) const override std::pair<Tree, Input> fetch(ref<Store> store, const Input & _input) override
{ {
auto name = "source"; auto name = "source";
auto input = std::make_shared<MercurialInput>(*this); Input input(_input);
auto [isLocal, actualUrl_] = getActualUrl(); auto [isLocal, actualUrl_] = getActualUrl(input);
auto actualUrl = actualUrl_; // work around clang bug auto actualUrl = actualUrl_; // work around clang bug
// FIXME: return lastModified. // FIXME: return lastModified.
// FIXME: don't clone local repositories. // FIXME: don't clone local repositories.
if (!input->ref && !input->rev && isLocal && pathExists(actualUrl + "/.hg")) { if (!input.getRef() && !input.getRev() && isLocal && pathExists(actualUrl + "/.hg")) {
bool clean = runProgram("hg", true, { "status", "-R", actualUrl, "--modified", "--added", "--removed" }) == ""; bool clean = runProgram("hg", true, { "status", "-R", actualUrl, "--modified", "--added", "--removed" }) == "";
@ -129,7 +142,7 @@ struct MercurialInput : Input
if (settings.warnDirty) if (settings.warnDirty)
warn("Mercurial tree '%s' is unclean", actualUrl); warn("Mercurial tree '%s' is unclean", actualUrl);
input->ref = chomp(runProgram("hg", true, { "branch", "-R", actualUrl })); input.attrs.insert_or_assign("ref", chomp(runProgram("hg", true, { "branch", "-R", actualUrl })));
auto files = tokenizeString<std::set<std::string>>( auto files = tokenizeString<std::set<std::string>>(
runProgram("hg", true, { "status", "-R", actualUrl, "--clean", "--modified", "--added", "--no-status", "--print0" }), "\0"s); runProgram("hg", true, { "status", "-R", actualUrl, "--clean", "--modified", "--added", "--no-status", "--print0" }), "\0"s);
@ -158,53 +171,50 @@ struct MercurialInput : Input
} }
} }
if (!input->ref) input->ref = "default"; if (!input.getRef()) input.attrs.insert_or_assign("ref", "default");
auto getImmutableAttrs = [&]() auto getImmutableAttrs = [&]()
{ {
return Attrs({ return Attrs({
{"type", "hg"}, {"type", "hg"},
{"name", name}, {"name", name},
{"rev", input->rev->gitRev()}, {"rev", input.getRev()->gitRev()},
}); });
}; };
auto makeResult = [&](const Attrs & infoAttrs, StorePath && storePath) auto makeResult = [&](const Attrs & infoAttrs, StorePath && storePath)
-> std::pair<Tree, std::shared_ptr<const Input>> -> std::pair<Tree, Input>
{ {
assert(input->rev); assert(input.getRev());
assert(!rev || rev == input->rev); assert(!_input.getRev() || _input.getRev() == input.getRev());
input.attrs.insert_or_assign("revCount", getIntAttr(infoAttrs, "revCount"));
return { return {
Tree{ Tree{
.actualPath = store->toRealPath(storePath), .actualPath = store->toRealPath(storePath),
.storePath = std::move(storePath), .storePath = std::move(storePath),
.info = TreeInfo {
.revCount = getIntAttr(infoAttrs, "revCount"),
},
}, },
input input
}; };
}; };
if (input->rev) { if (input.getRev()) {
if (auto res = getCache()->lookup(store, getImmutableAttrs())) if (auto res = getCache()->lookup(store, getImmutableAttrs()))
return makeResult(res->first, std::move(res->second)); return makeResult(res->first, std::move(res->second));
} }
assert(input->rev || input->ref); auto revOrRef = input.getRev() ? input.getRev()->gitRev() : *input.getRef();
auto revOrRef = input->rev ? input->rev->gitRev() : *input->ref;
Attrs mutableAttrs({ Attrs mutableAttrs({
{"type", "hg"}, {"type", "hg"},
{"name", name}, {"name", name},
{"url", actualUrl}, {"url", actualUrl},
{"ref", *input->ref}, {"ref", *input.getRef()},
}); });
if (auto res = getCache()->lookup(store, mutableAttrs)) { if (auto res = getCache()->lookup(store, mutableAttrs)) {
auto rev2 = Hash(getStrAttr(res->first, "rev"), htSHA1); auto rev2 = Hash(getStrAttr(res->first, "rev"), htSHA1);
if (!rev || rev == rev2) { if (!input.getRev() || input.getRev() == rev2) {
input->rev = rev2; input.attrs.insert_or_assign("rev", rev2.gitRev());
return makeResult(res->first, std::move(res->second)); return makeResult(res->first, std::move(res->second));
} }
} }
@ -213,10 +223,10 @@ struct MercurialInput : Input
/* If this is a commit hash that we already have, we don't /* If this is a commit hash that we already have, we don't
have to pull again. */ have to pull again. */
if (!(input->rev if (!(input.getRev()
&& pathExists(cacheDir) && pathExists(cacheDir)
&& runProgram( && runProgram(
RunOptions("hg", { "log", "-R", cacheDir, "-r", input->rev->gitRev(), "--template", "1" }) RunOptions("hg", { "log", "-R", cacheDir, "-r", input.getRev()->gitRev(), "--template", "1" })
.killStderr(true)).second == "1")) .killStderr(true)).second == "1"))
{ {
Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching Mercurial repository '%s'", actualUrl)); Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching Mercurial repository '%s'", actualUrl));
@ -245,9 +255,9 @@ struct MercurialInput : Input
runProgram("hg", true, { "log", "-R", cacheDir, "-r", revOrRef, "--template", "{node} {rev} {branch}" })); runProgram("hg", true, { "log", "-R", cacheDir, "-r", revOrRef, "--template", "{node} {rev} {branch}" }));
assert(tokens.size() == 3); assert(tokens.size() == 3);
input->rev = Hash(tokens[0], htSHA1); input.attrs.insert_or_assign("rev", Hash(tokens[0], htSHA1).gitRev());
auto revCount = std::stoull(tokens[1]); auto revCount = std::stoull(tokens[1]);
input->ref = tokens[2]; input.attrs.insert_or_assign("ref", tokens[2]);
if (auto res = getCache()->lookup(store, getImmutableAttrs())) if (auto res = getCache()->lookup(store, getImmutableAttrs()))
return makeResult(res->first, std::move(res->second)); return makeResult(res->first, std::move(res->second));
@ -255,18 +265,18 @@ struct MercurialInput : Input
Path tmpDir = createTempDir(); Path tmpDir = createTempDir();
AutoDelete delTmpDir(tmpDir, true); AutoDelete delTmpDir(tmpDir, true);
runProgram("hg", true, { "archive", "-R", cacheDir, "-r", input->rev->gitRev(), tmpDir }); runProgram("hg", true, { "archive", "-R", cacheDir, "-r", input.getRev()->gitRev(), tmpDir });
deletePath(tmpDir + "/.hg_archival.txt"); deletePath(tmpDir + "/.hg_archival.txt");
auto storePath = store->addToStore(name, tmpDir); auto storePath = store->addToStore(name, tmpDir);
Attrs infoAttrs({ Attrs infoAttrs({
{"rev", input->rev->gitRev()}, {"rev", input.getRev()->gitRev()},
{"revCount", (int64_t) revCount}, {"revCount", (int64_t) revCount},
}); });
if (!this->rev) if (!_input.getRev())
getCache()->add( getCache()->add(
store, store,
mutableAttrs, mutableAttrs,
@ -285,54 +295,6 @@ struct MercurialInput : Input
} }
}; };
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>()); }); static auto r1 = OnStartup([] { registerInputScheme(std::make_unique<MercurialInputScheme>()); });
} }

View file

@ -3,76 +3,86 @@
namespace nix::fetchers { namespace nix::fetchers {
struct PathInput : Input struct PathInputScheme : InputScheme
{ {
Path path; std::optional<Input> inputFromURL(const ParsedURL & url) override
/* Allow the user to pass in "fake" tree info attributes. This is
useful for making a pinned tree work the same as the repository
from which is exported
(e.g. path:/nix/store/...-source?lastModified=1585388205&rev=b0c285...). */
std::optional<Hash> rev;
std::optional<uint64_t> revCount;
std::optional<time_t> lastModified;
std::string type() const override { return "path"; }
std::optional<Hash> getRev() const override { return rev; }
bool operator ==(const Input & other) const override
{ {
auto other2 = dynamic_cast<const PathInput *>(&other); if (url.scheme != "path") return {};
return
other2 if (url.authority && *url.authority != "")
&& path == other2->path throw Error("path URL '%s' should not have an authority ('%s')", url.url, *url.authority);
&& rev == other2->rev
&& revCount == other2->revCount Input input;
&& lastModified == other2->lastModified; input.attrs.insert_or_assign("type", "path");
input.attrs.insert_or_assign("path", url.path);
for (auto & [name, value] : url.query)
if (name == "rev" || name == "narHash")
input.attrs.insert_or_assign(name, value);
else if (name == "revCount" || name == "lastModified") {
uint64_t n;
if (!string2Int(value, n))
throw Error("path URL '%s' has invalid parameter '%s'", url.to_string(), name);
input.attrs.insert_or_assign(name, n);
}
else
throw Error("path URL '%s' has unsupported parameter '%s'", url.to_string(), name);
return input;
} }
bool isImmutable() const override std::optional<Input> inputFromAttrs(const Attrs & attrs) override
{ {
return narHash || rev; if (maybeGetStrAttr(attrs, "type") != "path") return {};
getStrAttr(attrs, "path");
for (auto & [name, value] : attrs)
/* Allow the user to pass in "fake" tree info
attributes. This is useful for making a pinned tree
work the same as the repository from which is exported
(e.g. path:/nix/store/...-source?lastModified=1585388205&rev=b0c285...). */
if (name == "type" || name == "rev" || name == "revCount" || name == "lastModified" || name == "narHash" || name == "path")
// checked in Input::fromAttrs
;
else
throw Error("unsupported path input attribute '%s'", name);
Input input;
input.attrs = attrs;
return input;
} }
ParsedURL toURL() const override ParsedURL toURL(const Input & input) override
{ {
auto query = attrsToQuery(toAttrsInternal()); auto query = attrsToQuery(input.attrs);
query.erase("path"); query.erase("path");
query.erase("type");
return ParsedURL { return ParsedURL {
.scheme = "path", .scheme = "path",
.path = path, .path = getStrAttr(input.attrs, "path"),
.query = query, .query = query,
}; };
} }
Attrs toAttrsInternal() const override bool hasAllInfo(const Input & input) override
{ {
Attrs attrs; return true;
attrs.emplace("path", path);
if (rev)
attrs.emplace("rev", rev->gitRev());
if (revCount)
attrs.emplace("revCount", *revCount);
if (lastModified)
attrs.emplace("lastModified", *lastModified);
if (!rev && narHash)
attrs.emplace("narHash", narHash->to_string(SRI));
return attrs;
} }
std::optional<Path> getSourcePath() const override std::optional<Path> getSourcePath(const Input & input) override
{ {
return path; return getStrAttr(input.attrs, "path");
} }
void markChangedFile(std::string_view file, std::optional<std::string> commitMsg) const override void markChangedFile(const Input & input, std::string_view file, std::optional<std::string> commitMsg) override
{ {
// nothing to do
} }
std::pair<Tree, std::shared_ptr<const Input>> fetchTreeInternal(nix::ref<Store> store) const override std::pair<Tree, Input> fetch(ref<Store> store, const Input & input) override
{ {
auto input = std::make_shared<PathInput>(*this); auto path = getStrAttr(input.attrs, "path");
// FIXME: check whether access to 'path' is allowed. // FIXME: check whether access to 'path' is allowed.
@ -85,83 +95,13 @@ struct PathInput : Input
// FIXME: try to substitute storePath. // FIXME: try to substitute storePath.
storePath = store->addToStore("source", path); storePath = store->addToStore("source", path);
input->narHash = store->queryPathInfo(*storePath)->narHash; return {
Tree {
return .actualPath = store->toRealPath(*storePath),
{ .storePath = std::move(*storePath),
Tree { },
.actualPath = store->toRealPath(*storePath), input
.storePath = std::move(*storePath), };
.info = TreeInfo {
.revCount = revCount,
.lastModified = lastModified
}
},
input
};
}
};
struct PathInputScheme : InputScheme
{
std::unique_ptr<Input> inputFromURL(const ParsedURL & url) override
{
if (url.scheme != "path") return nullptr;
auto input = std::make_unique<PathInput>();
input->path = url.path;
if (url.authority && *url.authority != "")
throw Error("path URL '%s' should not have an authority ('%s')", url.url, *url.authority);
for (auto & [name, value] : url.query)
if (name == "rev")
input->rev = Hash(value, htSHA1);
else if (name == "revCount") {
uint64_t revCount;
if (!string2Int(value, revCount))
throw Error("path URL '%s' has invalid parameter '%s'", url.to_string(), name);
input->revCount = revCount;
}
else if (name == "lastModified") {
time_t lastModified;
if (!string2Int(value, lastModified))
throw Error("path URL '%s' has invalid parameter '%s'", url.to_string(), name);
input->lastModified = lastModified;
}
else if (name == "narHash")
// FIXME: require SRI hash.
input->narHash = Hash(value);
else
throw Error("path URL '%s' has unsupported parameter '%s'", url.to_string(), name);
return input;
}
std::unique_ptr<Input> inputFromAttrs(const Attrs & attrs) override
{
if (maybeGetStrAttr(attrs, "type") != "path") return {};
auto input = std::make_unique<PathInput>();
input->path = getStrAttr(attrs, "path");
for (auto & [name, value] : attrs)
if (name == "rev")
input->rev = Hash(getStrAttr(attrs, "rev"), htSHA1);
else if (name == "revCount")
input->revCount = getIntAttr(attrs, "revCount");
else if (name == "lastModified")
input->lastModified = getIntAttr(attrs, "lastModified");
else if (name == "narHash")
// FIXME: require SRI hash.
input->narHash = Hash(getStrAttr(attrs, "narHash"));
else if (name == "type" || name == "path")
;
else
throw Error("unsupported path input attribute '%s'", name);
return input;
} }
}; };

View file

@ -22,20 +22,7 @@ std::shared_ptr<Registry> Registry::read(
auto version = json.value("version", 0); auto version = json.value("version", 0);
// FIXME: remove soon if (version == 2) {
if (version == 1) {
auto flakes = json["flakes"];
for (auto i = flakes.begin(); i != flakes.end(); ++i) {
auto url = i->value("url", i->value("uri", ""));
if (url.empty())
throw Error("flake registry '%s' lacks a 'url' attribute for entry '%s'",
path, i.key());
registry->entries.push_back(
{inputFromURL(i.key()), inputFromURL(url), {}});
}
}
else if (version == 2) {
for (auto & i : json["flakes"]) { for (auto & i : json["flakes"]) {
auto toAttrs = jsonToAttrs(i["to"]); auto toAttrs = jsonToAttrs(i["to"]);
Attrs extraAttrs; Attrs extraAttrs;
@ -47,8 +34,8 @@ std::shared_ptr<Registry> Registry::read(
auto exact = i.find("exact"); auto exact = i.find("exact");
registry->entries.push_back( registry->entries.push_back(
Entry { Entry {
.from = inputFromAttrs(jsonToAttrs(i["from"])), .from = Input::fromAttrs(jsonToAttrs(i["from"])),
.to = inputFromAttrs(toAttrs), .to = Input::fromAttrs(std::move(toAttrs)),
.extraAttrs = extraAttrs, .extraAttrs = extraAttrs,
.exact = exact != i.end() && exact.value() .exact = exact != i.end() && exact.value()
}); });
@ -72,8 +59,8 @@ void Registry::write(const Path & path)
nlohmann::json arr; nlohmann::json arr;
for (auto & entry : entries) { for (auto & entry : entries) {
nlohmann::json obj; nlohmann::json obj;
obj["from"] = attrsToJson(entry.from->toAttrs()); obj["from"] = attrsToJson(entry.from.toAttrs());
obj["to"] = attrsToJson(entry.to->toAttrs()); obj["to"] = attrsToJson(entry.to.toAttrs());
if (!entry.extraAttrs.empty()) if (!entry.extraAttrs.empty())
obj["to"].update(attrsToJson(entry.extraAttrs)); obj["to"].update(attrsToJson(entry.extraAttrs));
if (entry.exact) if (entry.exact)
@ -90,8 +77,8 @@ void Registry::write(const Path & path)
} }
void Registry::add( void Registry::add(
const std::shared_ptr<const Input> & from, const Input & from,
const std::shared_ptr<const Input> & to, const Input & to,
const Attrs & extraAttrs) const Attrs & extraAttrs)
{ {
entries.emplace_back( entries.emplace_back(
@ -102,11 +89,11 @@ void Registry::add(
}); });
} }
void Registry::remove(const std::shared_ptr<const Input> & input) void Registry::remove(const Input & input)
{ {
// FIXME: use C++20 std::erase. // FIXME: use C++20 std::erase.
for (auto i = entries.begin(); i != entries.end(); ) for (auto i = entries.begin(); i != entries.end(); )
if (*i->from == *input) if (i->from == input)
i = entries.erase(i); i = entries.erase(i);
else else
++i; ++i;
@ -145,8 +132,8 @@ std::shared_ptr<Registry> getFlagRegistry()
} }
void overrideRegistry( void overrideRegistry(
const std::shared_ptr<const Input> & from, const Input & from,
const std::shared_ptr<const Input> & to, const Input & to,
const Attrs & extraAttrs) const Attrs & extraAttrs)
{ {
flagRegistry->add(from, to, extraAttrs); flagRegistry->add(from, to, extraAttrs);
@ -180,32 +167,33 @@ Registries getRegistries(ref<Store> store)
return registries; return registries;
} }
std::pair<std::shared_ptr<const Input>, Attrs> lookupInRegistries( std::pair<Input, Attrs> lookupInRegistries(
ref<Store> store, ref<Store> store,
std::shared_ptr<const Input> input) const Input & _input)
{ {
Attrs extraAttrs; Attrs extraAttrs;
int n = 0; int n = 0;
Input input(_input);
restart: restart:
n++; n++;
if (n > 100) throw Error("cycle detected in flake registr for '%s'", input); if (n > 100) throw Error("cycle detected in flake registry for '%s'", input.to_string());
for (auto & registry : getRegistries(store)) { for (auto & registry : getRegistries(store)) {
// FIXME: O(n) // FIXME: O(n)
for (auto & entry : registry->entries) { for (auto & entry : registry->entries) {
if (entry.exact) { if (entry.exact) {
if (*entry.from == *input) { if (entry.from == input) {
input = entry.to; input = entry.to;
extraAttrs = entry.extraAttrs; extraAttrs = entry.extraAttrs;
goto restart; goto restart;
} }
} else { } else {
if (entry.from->contains(*input)) { if (entry.from.contains(input)) {
input = entry.to->applyOverrides( input = entry.to.applyOverrides(
!entry.from->getRef() && input->getRef() ? input->getRef() : std::optional<std::string>(), !entry.from.getRef() && input.getRef() ? input.getRef() : std::optional<std::string>(),
!entry.from->getRev() && input->getRev() ? input->getRev() : std::optional<Hash>()); !entry.from.getRev() && input.getRev() ? input.getRev() : std::optional<Hash>());
extraAttrs = entry.extraAttrs; extraAttrs = entry.extraAttrs;
goto restart; goto restart;
} }
@ -213,8 +201,8 @@ std::pair<std::shared_ptr<const Input>, Attrs> lookupInRegistries(
} }
} }
if (!input->isDirect()) if (!input.isDirect())
throw Error("cannot find flake '%s' in the flake registries", input->to_string()); throw Error("cannot find flake '%s' in the flake registries", input.to_string());
return {input, extraAttrs}; return {input, extraAttrs};
} }

View file

@ -20,8 +20,7 @@ struct Registry
struct Entry struct Entry
{ {
std::shared_ptr<const Input> from; Input from, to;
std::shared_ptr<const Input> to;
Attrs extraAttrs; Attrs extraAttrs;
bool exact = false; bool exact = false;
}; };
@ -38,11 +37,11 @@ struct Registry
void write(const Path & path); void write(const Path & path);
void add( void add(
const std::shared_ptr<const Input> & from, const Input & from,
const std::shared_ptr<const Input> & to, const Input & to,
const Attrs & extraAttrs); const Attrs & extraAttrs);
void remove(const std::shared_ptr<const Input> & input); void remove(const Input & input);
}; };
typedef std::vector<std::shared_ptr<Registry>> Registries; typedef std::vector<std::shared_ptr<Registry>> Registries;
@ -54,12 +53,12 @@ Path getUserRegistryPath();
Registries getRegistries(ref<Store> store); Registries getRegistries(ref<Store> store);
void overrideRegistry( void overrideRegistry(
const std::shared_ptr<const Input> & from, const Input & from,
const std::shared_ptr<const Input> & to, const Input & to,
const Attrs & extraAttrs); const Attrs & extraAttrs);
std::pair<std::shared_ptr<const Input>, Attrs> lookupInRegistries( std::pair<Input, Attrs> lookupInRegistries(
ref<Store> store, ref<Store> store,
std::shared_ptr<const Input> input); const Input & input);
} }

View file

@ -101,7 +101,7 @@ DownloadFileResult downloadFile(
}; };
} }
Tree downloadTarball( std::pair<Tree, time_t> downloadTarball(
ref<Store> store, ref<Store> store,
const std::string & url, const std::string & url,
const std::string & name, const std::string & name,
@ -116,12 +116,12 @@ Tree downloadTarball(
auto cached = getCache()->lookupExpired(store, inAttrs); auto cached = getCache()->lookupExpired(store, inAttrs);
if (cached && !cached->expired) if (cached && !cached->expired)
return Tree { return {
.actualPath = store->toRealPath(cached->storePath), Tree {
.storePath = std::move(cached->storePath), .actualPath = store->toRealPath(cached->storePath),
.info = TreeInfo { .storePath = std::move(cached->storePath),
.lastModified = getIntAttr(cached->infoAttrs, "lastModified"),
}, },
getIntAttr(cached->infoAttrs, "lastModified")
}; };
auto res = downloadFile(store, url, name, immutable); auto res = downloadFile(store, url, name, immutable);
@ -156,118 +156,75 @@ Tree downloadTarball(
*unpackedStorePath, *unpackedStorePath,
immutable); immutable);
return Tree { return {
.actualPath = store->toRealPath(*unpackedStorePath), Tree {
.storePath = std::move(*unpackedStorePath), .actualPath = store->toRealPath(*unpackedStorePath),
.info = TreeInfo { .storePath = std::move(*unpackedStorePath),
.lastModified = 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 (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 struct TarballInputScheme : InputScheme
{ {
std::unique_ptr<Input> inputFromURL(const ParsedURL & url) override std::optional<Input> inputFromURL(const ParsedURL & url) override
{ {
if (url.scheme != "file" && url.scheme != "http" && url.scheme != "https") return nullptr; if (url.scheme != "file" && url.scheme != "http" && url.scheme != "https") return {};
if (!hasSuffix(url.path, ".zip") if (!hasSuffix(url.path, ".zip")
&& !hasSuffix(url.path, ".tar") && !hasSuffix(url.path, ".tar")
&& !hasSuffix(url.path, ".tar.gz") && !hasSuffix(url.path, ".tar.gz")
&& !hasSuffix(url.path, ".tar.xz") && !hasSuffix(url.path, ".tar.xz")
&& !hasSuffix(url.path, ".tar.bz2")) && !hasSuffix(url.path, ".tar.bz2"))
return nullptr; return {};
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);
}
Input input;
input.attrs.insert_or_assign("type", "tarball");
input.attrs.insert_or_assign("url", url.to_string());
auto narHash = url.query.find("narHash");
if (narHash != url.query.end())
input.attrs.insert_or_assign("narHash", narHash->second);
return input; return input;
} }
std::unique_ptr<Input> inputFromAttrs(const Attrs & attrs) override std::optional<Input> inputFromAttrs(const Attrs & attrs) override
{ {
if (maybeGetStrAttr(attrs, "type") != "tarball") return {}; if (maybeGetStrAttr(attrs, "type") != "tarball") return {};
for (auto & [name, value] : attrs) for (auto & [name, value] : attrs)
if (name != "type" && name != "url" && name != "hash") if (name != "type" && name != "url" && /* name != "hash" && */ name != "narHash")
throw Error("unsupported tarball input attribute '%s'", name); throw Error("unsupported tarball input attribute '%s'", name);
auto input = std::make_unique<TarballInput>(parseURL(getStrAttr(attrs, "url"))); Input input;
if (auto hash = maybeGetStrAttr(attrs, "hash")) input.attrs = attrs;
// FIXME: require SRI hash. //input.immutable = (bool) maybeGetStrAttr(input.attrs, "hash");
input->hash = Hash(*hash);
return input; return input;
} }
ParsedURL toURL(const Input & input) override
{
auto url = parseURL(getStrAttr(input.attrs, "url"));
// NAR hashes are preferred over file hashes since tar/zip files
// don't have a canonical representation.
if (auto narHash = input.getNarHash())
url.query.insert_or_assign("narHash", narHash->to_string(SRI));
/*
else if (auto hash = maybeGetStrAttr(input.attrs, "hash"))
url.query.insert_or_assign("hash", Hash(*hash).to_string(SRI));
*/
return url;
}
bool hasAllInfo(const Input & input) override
{
return true;
}
std::pair<Tree, Input> fetch(ref<Store> store, const Input & input) override
{
auto tree = downloadTarball(store, getStrAttr(input.attrs, "url"), "source", false).first;
return {std::move(tree), input};
}
}; };
static auto r1 = OnStartup([] { registerInputScheme(std::make_unique<TarballInputScheme>()); }); static auto r1 = OnStartup([] { registerInputScheme(std::make_unique<TarballInputScheme>()); });

View file

@ -1,60 +0,0 @@
#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");
}
TreeInfo TreeInfo::fromJson(const nlohmann::json & json)
{
TreeInfo info;
auto i = json.find("info");
if (i != json.end()) {
const nlohmann::json & i2(*i);
auto j = i2.find("narHash");
if (j != i2.end())
info.narHash = Hash((std::string) *j);
else
throw Error("attribute 'narHash' missing in lock file");
j = i2.find("revCount");
if (j != i2.end())
info.revCount = *j;
j = i2.find("lastModified");
if (j != i2.end())
info.lastModified = *j;
return info;
}
i = json.find("narHash");
if (i != json.end()) {
info.narHash = Hash((std::string) *i);
return info;
}
throw Error("attribute 'info' missing in lock file");
}
nlohmann::json TreeInfo::toJson() const
{
nlohmann::json json;
assert(narHash);
json["narHash"] = narHash.to_string(SRI);
if (revCount)
json["revCount"] = *revCount;
if (lastModified)
json["lastModified"] = *lastModified;
return json;
}
}

View file

@ -1,33 +0,0 @@
#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;
static TreeInfo fromJson(const nlohmann::json & json);
nlohmann::json toJson() const;
};
}

View file

@ -62,13 +62,13 @@ static void printFlakeInfo(const Store & store, const Flake & flake)
if (flake.description) if (flake.description)
logger->stdout("Description: %s", *flake.description); logger->stdout("Description: %s", *flake.description);
logger->stdout("Path: %s", store.printStorePath(flake.sourceInfo->storePath)); logger->stdout("Path: %s", store.printStorePath(flake.sourceInfo->storePath));
if (auto rev = flake.lockedRef.input->getRev()) if (auto rev = flake.lockedRef.input.getRev())
logger->stdout("Revision: %s", rev->to_string(Base16, false)); logger->stdout("Revision: %s", rev->to_string(Base16, false));
if (flake.sourceInfo->info.revCount) if (auto revCount = flake.lockedRef.input.getRevCount())
logger->stdout("Revisions: %s", *flake.sourceInfo->info.revCount); logger->stdout("Revisions: %s", *revCount);
if (flake.sourceInfo->info.lastModified) if (auto lastModified = flake.lockedRef.input.getLastModified())
logger->stdout("Last modified: %s", logger->stdout("Last modified: %s",
std::put_time(std::localtime(&*flake.sourceInfo->info.lastModified), "%F %T")); std::put_time(std::localtime(&*lastModified), "%F %T"));
} }
static nlohmann::json flakeToJson(const Store & store, const Flake & flake) static nlohmann::json flakeToJson(const Store & store, const Flake & flake)
@ -82,13 +82,12 @@ static nlohmann::json flakeToJson(const Store & store, const Flake & flake)
j["resolved"] = attrsToJson(flake.resolvedRef.toAttrs()); j["resolved"] = attrsToJson(flake.resolvedRef.toAttrs());
j["url"] = flake.lockedRef.to_string(); // FIXME: rename to lockedUrl j["url"] = flake.lockedRef.to_string(); // FIXME: rename to lockedUrl
j["locked"] = attrsToJson(flake.lockedRef.toAttrs()); j["locked"] = attrsToJson(flake.lockedRef.toAttrs());
j["info"] = flake.sourceInfo->info.toJson(); if (auto rev = flake.lockedRef.input.getRev())
if (auto rev = flake.lockedRef.input->getRev())
j["revision"] = rev->to_string(Base16, false); j["revision"] = rev->to_string(Base16, false);
if (flake.sourceInfo->info.revCount) if (auto revCount = flake.lockedRef.input.getRevCount())
j["revCount"] = *flake.sourceInfo->info.revCount; j["revCount"] = *revCount;
if (flake.sourceInfo->info.lastModified) if (auto lastModified = flake.lockedRef.input.getLastModified())
j["lastModified"] = *flake.sourceInfo->info.lastModified; j["lastModified"] = *lastModified;
j["path"] = store.printStorePath(flake.sourceInfo->storePath); j["path"] = store.printStorePath(flake.sourceInfo->storePath);
return j; return j;
} }
@ -501,7 +500,7 @@ struct CmdFlakeClone : FlakeCommand
if (destDir.empty()) if (destDir.empty())
throw Error("missing flag '--dest'"); throw Error("missing flag '--dest'");
getFlakeRef().resolve(store).input->clone(destDir); getFlakeRef().resolve(store).input.clone(destDir);
} }
}; };
@ -563,9 +562,10 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun
auto lockedInput = std::dynamic_pointer_cast<const LockedNode>(input.second); auto lockedInput = std::dynamic_pointer_cast<const LockedNode>(input.second);
assert(lockedInput); assert(lockedInput);
auto jsonObj3 = jsonObj2 ? jsonObj2->object(input.first) : std::optional<JSONObject>(); auto jsonObj3 = jsonObj2 ? jsonObj2->object(input.first) : std::optional<JSONObject>();
if (!dryRun) auto storePath =
lockedInput->lockedRef.input->fetchTree(store); dryRun
auto storePath = lockedInput->computeStorePath(*store); ? lockedInput->lockedRef.input.computeStorePath(*store)
: lockedInput->lockedRef.input.fetch(store).first.storePath;
if (jsonObj3) if (jsonObj3)
jsonObj3->attr("path", store->printStorePath(storePath)); jsonObj3->attr("path", store->printStorePath(storePath));
sources.insert(std::move(storePath)); sources.insert(std::move(storePath));

View file

@ -194,7 +194,7 @@ void EvalCommand::completeFlakeRef(std::string_view prefix)
/* Look for registry entries that match the prefix. */ /* Look for registry entries that match the prefix. */
for (auto & registry : fetchers::getRegistries(getStore())) { for (auto & registry : fetchers::getRegistries(getStore())) {
for (auto & entry : registry->entries) { for (auto & entry : registry->entries) {
auto from = entry.from->to_string(); auto from = entry.from.to_string();
if (!hasPrefix(prefix, "flake:") && hasPrefix(from, "flake:")) { if (!hasPrefix(prefix, "flake:") && hasPrefix(from, "flake:")) {
std::string from2(from, 6); std::string from2(from, 6);
if (hasPrefix(from2, prefix)) if (hasPrefix(from2, prefix))

View file

@ -330,7 +330,7 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf
for (size_t i = 0; i < manifest.elements.size(); ++i) { for (size_t i = 0; i < manifest.elements.size(); ++i) {
auto & element(manifest.elements[i]); auto & element(manifest.elements[i]);
if (element.source if (element.source
&& !element.source->originalRef.input->isImmutable() && !element.source->originalRef.input.isImmutable()
&& matches(*store, element, i, matchers)) && matches(*store, element, i, matchers))
{ {
Activity act(*logger, lvlChatty, actUnknown, Activity act(*logger, lvlChatty, actUnknown,

View file

@ -31,8 +31,8 @@ struct CmdRegistryList : StoreCommand
registry->type == Registry::User ? "user " : registry->type == Registry::User ? "user " :
registry->type == Registry::System ? "system" : registry->type == Registry::System ? "system" :
"global", "global",
entry.from->to_string(), entry.from.to_string(),
entry.to->to_string()); entry.to.to_string());
} }
} }
} }
@ -107,7 +107,7 @@ struct CmdRegistryPin : virtual Args, EvalCommand
auto ref = parseFlakeRef(url); auto ref = parseFlakeRef(url);
auto userRegistry = fetchers::getUserRegistry(); auto userRegistry = fetchers::getUserRegistry();
userRegistry->remove(ref.input); userRegistry->remove(ref.input);
auto [tree, resolved] = ref.resolve(store).input->fetchTree(store); auto [tree, resolved] = ref.resolve(store).input.fetch(store);
fetchers::Attrs extraAttrs; fetchers::Attrs extraAttrs;
if (ref.subdir != "") extraAttrs["dir"] = ref.subdir; if (ref.subdir != "") extraAttrs["dir"] = ref.subdir;
userRegistry->add(ref.input, resolved, extraAttrs); userRegistry->add(ref.input, resolved, extraAttrs);