Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 96 additions & 19 deletions src/libstore/dummy-store.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,43 @@ struct DummyStore : virtual Store

ref<const Config> config;

ref<MemorySourceAccessor> contents;
struct PathInfoAndContents
{
UnkeyedValidPathInfo info;
ref<MemorySourceAccessor> contents;
};

/**
* This is map conceptually owns the file system objects for each
* store object.
*/
std::map<StorePath, PathInfoAndContents> contents;

/**
* This view conceptually just borrows the file systems objects of
* each store object from `contents`, and combines them together
* into one store-wide source accessor.
*
* This is needed just in order to implement `Store::getFSAccessor`.
*/
ref<MemorySourceAccessor> wholeStoreView = make_ref<MemorySourceAccessor>();
Comment on lines +34 to +41
Copy link
Member

Choose a reason for hiding this comment

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

This would be a lot simpler if we didn't have a store-wide getFSAccessor requirement.

Either we could keep it:
Also it seems that this is a general problem, and we could have a store mix-in that implements getFSAccessor by means of an abstract method that gets an accessor for an individual store path.
Then all synchronization would be limited to that particular method.

Or we could drop it in favor of per-store path accessors.
Then non-listable stores also become more "valid" Store implementations.

Copy link
Contributor

Choose a reason for hiding this comment

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

What we also could do is have a "subdirectory" view of mounted accessors. That would help with thread safety as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

I like the point about non-listable stores.

The store-wide accessor could go in the same interface (in the store hierarchy) as the stuff for listing stores, like #8213

Non-listable stores can also contain different objects for different store dirs too. E.g. binary cache can be heterogeneous in store dir.


DummyStore(ref<const Config> config)
: Store{*config}
, config(config)
, contents(make_ref<MemorySourceAccessor>())
{
contents->setPathDisplay(config->storeDir);
wholeStoreView->setPathDisplay(config->storeDir);
MemorySink sink{*wholeStoreView};
sink.createDirectory(CanonPath::root);
}

void queryPathInfoUncached(
const StorePath & path, Callback<std::shared_ptr<const ValidPathInfo>> callback) noexcept override
{
callback(nullptr);
if (auto it = contents.find(path); it != contents.end())
callback(std::make_shared<ValidPathInfo>(StorePath{path}, it->second.info));
else
callback(nullptr);
}

/**
Expand All @@ -50,7 +73,33 @@ struct DummyStore : virtual Store

void addToStore(const ValidPathInfo & info, Source & source, RepairFlag repair, CheckSigsFlag checkSigs) override
{
unsupported("addToStore");
if (config->readOnly)
unsupported("addToStore");

if (repair)
throw Error("repairing is not supported for '%s' store", config->getHumanReadableURI());

if (checkSigs)
throw Error("checking signatures is not supported for '%s' store", config->getHumanReadableURI());

auto temp = make_ref<MemorySourceAccessor>();
MemorySink tempSink{*temp};
parseDump(tempSink, source);
auto path = info.path;

auto [it, _] = contents.insert({
path,
{
std::move(info),
make_ref<MemorySourceAccessor>(std::move(*temp)),
},
});

auto & pathAndContents = it->second;

bool inserted = wholeStoreView->open(CanonPath(path.to_string()), pathAndContents.contents->root);
if (!inserted)
unreachable();
}

StorePath addToStoreFromDump(
Expand All @@ -65,6 +114,9 @@ struct DummyStore : virtual Store
if (config->readOnly)
unsupported("addToStoreFromDump");

if (repair)
throw Error("repairing is not supported for '%s' store", config->getHumanReadableURI());

auto temp = make_ref<MemorySourceAccessor>();

{
Expand All @@ -85,27 +137,52 @@ struct DummyStore : virtual Store
}

auto hash = hashPath({temp, CanonPath::root}, hashMethod.getFileIngestionMethod(), hashAlgo).first;

auto desc = ContentAddressWithReferences::fromParts(
hashMethod,
hash,
auto narHash = hashPath({temp, CanonPath::root}, FileIngestionMethod::NixArchive, HashAlgorithm::SHA256);

auto info = ValidPathInfo::makeFromCA(
*this,
name,
ContentAddressWithReferences::fromParts(
hashMethod,
std::move(hash),
{
.others = references,
// caller is not capable of creating a self-reference, because
// this is content-addressed without modulus
.self = false,
}),
std::move(narHash.first));

info.narSize = narHash.second.value();

auto path = info.path;

auto [it, _] = contents.insert({
path,
{
.others = references,
// caller is not capable of creating a self-reference, because
// this is content-addressed without modulus
.self = false,
});
std::move(info),
make_ref<MemorySourceAccessor>(std::move(*temp)),
},
});

auto dstPath = makeFixedOutputPathFromCA(name, desc);
auto & pathAndContents = it->second;

contents->open(CanonPath(printStorePath(dstPath)), std::move(temp->root));
bool inserted = wholeStoreView->open(CanonPath(path.to_string()), pathAndContents.contents->root);
if (!inserted)
unreachable();

return dstPath;
return path;
}

void narFromPath(const StorePath & path, Sink & sink) override
{
unsupported("narFromPath");
auto object = contents.find(path);
if (object == contents.end())
throw Error("path '%s' is not valid", printStorePath(path));

const auto & [info, accessor] = object->second;
SourcePath sourcePath(accessor);
dumpPath(sourcePath, sink, FileSerialisationMethod::NixArchive);
}

void
Expand All @@ -116,7 +193,7 @@ struct DummyStore : virtual Store

virtual ref<SourceAccessor> getFSAccessor(bool requireValidPath) override
{
return this->contents;
return wholeStoreView;
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/libstore/dummy-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ R"(

This store type represents a store in memory.
Store objects can be read and written, but only so long as the store is open.
Once the store is closed, all data will be forgoton.
Once the store is closed, all data will be discarded.

It's useful when you want to use the Nix evaluator when no actual Nix store exists, e.g.

Expand Down
9 changes: 7 additions & 2 deletions src/libstore/include/nix/store/dummy-store.hh
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ namespace nix {

struct DummyStoreConfig : public std::enable_shared_from_this<DummyStoreConfig>, virtual StoreConfig
{
using StoreConfig::StoreConfig;
DummyStoreConfig(const Params & params)
: StoreConfig(params)
{
// Disable caching since this a temporary in-memory store.
pathInfoCacheSize = 0;
}

DummyStoreConfig(std::string_view scheme, std::string_view authority, const Params & params)
: StoreConfig(params)
: DummyStoreConfig(params)
{
if (!authority.empty())
throw UsageError("`%s` store URIs must not contain an authority part %s", scheme, authority);
Expand Down
16 changes: 8 additions & 8 deletions src/libutil-tests/git.cc
Original file line number Diff line number Diff line change
Expand Up @@ -233,30 +233,30 @@ TEST_F(GitTest, both_roundrip)
.contents{
{
"foo",
File::Regular{
make_ref<File>(File::Regular{
.contents = "hello\n\0\n\tworld!",
},
}),
},
{
"bar",
File::Directory{
make_ref<File>(File::Directory{
.contents =
{
{
"baz",
File::Regular{
make_ref<File>(File::Regular{
.executable = true,
.contents = "good day,\n\0\n\tworld!",
},
}),
},
{
"quux",
File::Symlink{
make_ref<File>(File::Symlink{
.target = "/over/there",
},
}),
},
},
},
}),
},
},
};
Expand Down
18 changes: 13 additions & 5 deletions src/libutil/include/nix/util/memory-source-accessor.hh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ struct MemorySourceAccessor : virtual SourceAccessor
{
using Name = std::string;

std::map<Name, File, std::less<>> contents;
std::map<Name, ref<File>, std::less<>> contents;

bool operator==(const Directory &) const noexcept;
// TODO libc++ 16 (used by darwin) missing `std::map::operator <=>`, can't do yet.
Expand All @@ -58,7 +58,7 @@ struct MemorySourceAccessor : virtual SourceAccessor
Stat lstat() const;
};

File root{File::Directory{}};
std::optional<File> root;

bool operator==(const MemorySourceAccessor &) const noexcept = default;

Expand Down Expand Up @@ -89,13 +89,21 @@ struct MemorySourceAccessor : virtual SourceAccessor
SourcePath addFile(CanonPath path, std::string && contents);
};

inline bool MemorySourceAccessor::File::Directory::operator==(
const MemorySourceAccessor::File::Directory &) const noexcept = default;
inline bool
MemorySourceAccessor::File::Directory::operator==(const MemorySourceAccessor::File::Directory & other) const noexcept
{
return std::ranges::equal(contents, other.contents, [](const auto & lhs, const auto & rhs) -> bool {
return lhs.first == rhs.first && *lhs.second == *rhs.second;
});
};

inline bool
MemorySourceAccessor::File::Directory::operator<(const MemorySourceAccessor::File::Directory & other) const noexcept
{
return contents < other.contents;
return std::ranges::lexicographical_compare(
contents, other.contents, [](const auto & lhs, const auto & rhs) -> bool {
return lhs.first < rhs.first && *lhs.second < *rhs.second;
});
}

inline bool MemorySourceAccessor::File::operator==(const MemorySourceAccessor::File &) const noexcept = default;
Expand Down
27 changes: 23 additions & 4 deletions src/libutil/memory-source-accessor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,22 @@ namespace nix {

MemorySourceAccessor::File * MemorySourceAccessor::open(const CanonPath & path, std::optional<File> create)
{
File * cur = &root;
bool hasRoot = root.has_value();

// Special handling of root directory.
if (path.isRoot() && !hasRoot) {
if (create) {
root = std::move(*create);
return &root.value();
}
return nullptr;
}

// Root does not exist.
if (!hasRoot)
return nullptr;

File * cur = &root.value();

bool newF = false;

Expand All @@ -24,11 +39,11 @@ MemorySourceAccessor::File * MemorySourceAccessor::open(const CanonPath & path,
i,
{
std::string{name},
File::Directory{},
make_ref<File>(File::Directory{}),
});
}
}
cur = &i->second;
cur = &*i->second;
}

if (newF && create)
Expand Down Expand Up @@ -92,7 +107,7 @@ MemorySourceAccessor::DirEntries MemorySourceAccessor::readDirectory(const Canon
if (auto * d = std::get_if<File::Directory>(&f->raw)) {
DirEntries res;
for (auto & [name, file] : d->contents)
res.insert_or_assign(name, file.lstat().type);
res.insert_or_assign(name, file->lstat().type);
return res;
} else
throw Error("file '%s' is not a directory", path);
Expand All @@ -112,6 +127,10 @@ std::string MemorySourceAccessor::readLink(const CanonPath & path)

SourcePath MemorySourceAccessor::addFile(CanonPath path, std::string && contents)
{
// Create root directory automatically if necessary as a convenience.
if (!root && !path.isRoot())
open(CanonPath::root, File::Directory{});

auto * f = open(path, File{File::Regular{}});
if (!f)
throw Error("file '%s' cannot be made because some parent file is not a directory", path);
Expand Down
4 changes: 4 additions & 0 deletions tests/functional/eval-store.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ rm -rf "$eval_store"
[[ $(nix eval --eval-store "$eval_store?require-sigs=false" --impure --raw --file ./ifd.nix) = hi ]]
ls $NIX_STORE_DIR/*dependencies-top/foobar
(! ls $eval_store/nix/store/*dependencies-top/foobar)

# Can't write .drv by default
(! nix-instantiate dependencies.nix --eval-store "dummy://")
nix-instantiate dependencies.nix --eval-store "dummy://?read-only=false"
1 change: 1 addition & 0 deletions tests/functional/flakes/flakes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ nix flake update flake1 flake2/flake1 --flake "$flake3Dir"

# Test 'nix flake metadata --json'.
nix flake metadata "$flake3Dir" --json | jq .
nix flake metadata "$flake3Dir" --json --eval-store "dummy://?read-only=false" | jq .

# Test flake in store does not evaluate.
rm -rf $badFlakeDir
Expand Down
1 change: 1 addition & 0 deletions tests/nixos/github-flakes.nix
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ in

client.succeed("nix registry pin nixpkgs")
client.succeed("nix flake metadata nixpkgs --tarball-ttl 0 >&2")
client.succeed("nix eval nixpkgs#hello --eval-store dummy://?read-only=false >&2")

# Test fetchTree on a github URL.
hash = client.succeed(f"nix eval --no-trust-tarballs-from-git-forges --raw --expr '(fetchTree {info['url']}).narHash'")
Expand Down
Loading