mirror of
https://github.com/privatevoid-net/nix-super.git
synced 2024-11-27 00:06:16 +02:00
* Refactoring: move all database manipulation into store.cc.
* Removed `--query --generators'.
This commit is contained in:
parent
5fc7127643
commit
ebff82222c
10 changed files with 143 additions and 170 deletions
|
@ -70,6 +70,14 @@ void Transaction::abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Transaction::moveTo(Transaction & t)
|
||||||
|
{
|
||||||
|
if (t.txn) throw Error("target txn already exists");
|
||||||
|
t.txn = txn;
|
||||||
|
txn = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void Database::requireEnv()
|
void Database::requireEnv()
|
||||||
{
|
{
|
||||||
if (!env) throw Error("database environment not open");
|
if (!env) throw Error("database environment not open");
|
||||||
|
|
|
@ -29,6 +29,8 @@ public:
|
||||||
|
|
||||||
void abort();
|
void abort();
|
||||||
void commit();
|
void commit();
|
||||||
|
|
||||||
|
void moveTo(Transaction & t);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
11
src/expr.cc
11
src/expr.cc
|
@ -39,14 +39,11 @@ Path writeTerm(ATerm t, const string & suffix)
|
||||||
(string) h + suffix + ".nix");
|
(string) h + suffix + ".nix");
|
||||||
|
|
||||||
if (!isValidPath(path)) {
|
if (!isValidPath(path)) {
|
||||||
if (!ATwriteToNamedTextFile(t, path.c_str()))
|
char * s = ATwriteToString(t);
|
||||||
throw Error(format("cannot write aterm %1%") % path);
|
if (!s) throw Error(format("cannot write aterm to `%1%'") % path);
|
||||||
|
addTextToStore(path, string(s));
|
||||||
Transaction txn(nixDB);
|
|
||||||
registerValidPath(txn, path);
|
|
||||||
txn.commit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,37 +1,8 @@
|
||||||
#include "globals.hh"
|
#include "globals.hh"
|
||||||
#include "db.hh"
|
|
||||||
|
|
||||||
|
|
||||||
Database nixDB;
|
|
||||||
|
|
||||||
|
|
||||||
TableId dbValidPaths;
|
|
||||||
TableId dbSuccessors;
|
|
||||||
TableId dbSuccessorsRev;
|
|
||||||
TableId dbSubstitutes;
|
|
||||||
TableId dbSubstitutesRev;
|
|
||||||
|
|
||||||
|
|
||||||
string nixStore = "/UNINIT";
|
string nixStore = "/UNINIT";
|
||||||
string nixDataDir = "/UNINIT";
|
string nixDataDir = "/UNINIT";
|
||||||
string nixLogDir = "/UNINIT";
|
string nixLogDir = "/UNINIT";
|
||||||
string nixDBPath = "/UNINIT";
|
string nixDBPath = "/UNINIT";
|
||||||
|
|
||||||
|
|
||||||
bool keepFailed = false;
|
bool keepFailed = false;
|
||||||
|
|
||||||
|
|
||||||
void openDB()
|
|
||||||
{
|
|
||||||
nixDB.open(nixDBPath);
|
|
||||||
dbValidPaths = nixDB.openTable("validpaths");
|
|
||||||
dbSuccessors = nixDB.openTable("successors");
|
|
||||||
dbSuccessorsRev = nixDB.openTable("successors-rev");
|
|
||||||
dbSubstitutes = nixDB.openTable("substitutes");
|
|
||||||
dbSubstitutesRev = nixDB.openTable("substitutes-rev");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void initDB()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,65 +3,8 @@
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "db.hh"
|
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
|
|
||||||
extern Database nixDB;
|
|
||||||
|
|
||||||
|
|
||||||
/* Database tables. */
|
|
||||||
|
|
||||||
|
|
||||||
/* dbValidPaths :: Path -> ()
|
|
||||||
|
|
||||||
The existence of a key $p$ indicates that path $p$ is valid (that
|
|
||||||
is, produced by a succesful build). */
|
|
||||||
extern TableId dbValidPaths;
|
|
||||||
|
|
||||||
|
|
||||||
/* dbSuccessors :: Path -> Path
|
|
||||||
|
|
||||||
Each pair $(p_1, p_2)$ in this mapping records the fact that the
|
|
||||||
Nix expression stored at path $p_1$ has a successor expression
|
|
||||||
stored at path $p_2$.
|
|
||||||
|
|
||||||
Note that a term $y$ is a successor of $x$ iff there exists a
|
|
||||||
sequence of rewrite steps that rewrites $x$ into $y$.
|
|
||||||
*/
|
|
||||||
extern TableId dbSuccessors;
|
|
||||||
|
|
||||||
|
|
||||||
/* dbSuccessorsRev :: Path -> [Path]
|
|
||||||
|
|
||||||
The reverse mapping of dbSuccessors (i.e., it stores the
|
|
||||||
predecessors of a Nix expression).
|
|
||||||
*/
|
|
||||||
extern TableId dbSuccessorsRev;
|
|
||||||
|
|
||||||
|
|
||||||
/* dbSubstitutes :: Path -> [Path]
|
|
||||||
|
|
||||||
Each pair $(p, [ps])$ tells Nix that it can realise any of the
|
|
||||||
Nix expressions stored at paths $ps$ to produce a path $p$.
|
|
||||||
|
|
||||||
The main purpose of this is for distributed caching of derivates.
|
|
||||||
One system can compute a derivate and put it on a website (as a Nix
|
|
||||||
archive), for instance, and then another system can register a
|
|
||||||
substitute for that derivate. The substitute in this case might be
|
|
||||||
a Nix expression that fetches the Nix archive.
|
|
||||||
*/
|
|
||||||
extern TableId dbSubstitutes;
|
|
||||||
|
|
||||||
|
|
||||||
/* dbSubstitutesRev :: Path -> [Path]
|
|
||||||
|
|
||||||
The reverse mapping of dbSubstitutes.
|
|
||||||
*/
|
|
||||||
extern TableId dbSubstitutesRev;
|
|
||||||
|
|
||||||
|
|
||||||
/* Path names. */
|
/* Path names. */
|
||||||
|
|
||||||
/* nixStore is the directory where we generally store atomic and
|
/* nixStore is the directory where we generally store atomic and
|
||||||
|
@ -83,11 +26,4 @@ extern string nixDBPath;
|
||||||
extern bool keepFailed;
|
extern bool keepFailed;
|
||||||
|
|
||||||
|
|
||||||
/* Open the database environment. */
|
|
||||||
void openDB();
|
|
||||||
|
|
||||||
/* Create the required database tables. */
|
|
||||||
void initDB();
|
|
||||||
|
|
||||||
|
|
||||||
#endif /* !__GLOBALS_H */
|
#endif /* !__GLOBALS_H */
|
||||||
|
|
|
@ -23,7 +23,6 @@ Query flags:
|
||||||
|
|
||||||
--list / -l: query the output paths (roots) of a Nix expression (default)
|
--list / -l: query the output paths (roots) of a Nix expression (default)
|
||||||
--requisites / -r: print all paths necessary to realise expression
|
--requisites / -r: print all paths necessary to realise expression
|
||||||
--generators / -g: find expressions producing a subset of given ids
|
|
||||||
--predecessors: print predecessors of a Nix expression
|
--predecessors: print predecessors of a Nix expression
|
||||||
--graph: print a dot graph rooted at given ids
|
--graph: print a dot graph rooted at given ids
|
||||||
|
|
||||||
|
|
22
src/nix.cc
22
src/nix.cc
|
@ -73,7 +73,7 @@ Path maybeNormalise(const Path & ne, bool normalise)
|
||||||
/* Perform various sorts of queries. */
|
/* Perform various sorts of queries. */
|
||||||
static void opQuery(Strings opFlags, Strings opArgs)
|
static void opQuery(Strings opFlags, Strings opArgs)
|
||||||
{
|
{
|
||||||
enum { qList, qRequisites, qGenerators, qPredecessors, qGraph
|
enum { qList, qRequisites, qPredecessors, qGraph
|
||||||
} query = qList;
|
} query = qList;
|
||||||
bool normalise = false;
|
bool normalise = false;
|
||||||
bool includeExprs = true;
|
bool includeExprs = true;
|
||||||
|
@ -83,7 +83,6 @@ static void opQuery(Strings opFlags, Strings opArgs)
|
||||||
i != opFlags.end(); i++)
|
i != opFlags.end(); i++)
|
||||||
if (*i == "--list" || *i == "-l") query = qList;
|
if (*i == "--list" || *i == "-l") query = qList;
|
||||||
else if (*i == "--requisites" || *i == "-r") query = qRequisites;
|
else if (*i == "--requisites" || *i == "-r") query = qRequisites;
|
||||||
else if (*i == "--generators" || *i == "-g") query = qGenerators;
|
|
||||||
else if (*i == "--predecessors") query = qPredecessors;
|
else if (*i == "--predecessors") query = qPredecessors;
|
||||||
else if (*i == "--graph") query = qGraph;
|
else if (*i == "--graph") query = qGraph;
|
||||||
else if (*i == "--normalise" || *i == "-n") normalise = true;
|
else if (*i == "--normalise" || *i == "-n") normalise = true;
|
||||||
|
@ -124,22 +123,6 @@ static void opQuery(Strings opFlags, Strings opArgs)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if 0
|
|
||||||
case qGenerators: {
|
|
||||||
FSIds outIds;
|
|
||||||
for (Strings::iterator i = opArgs.begin();
|
|
||||||
i != opArgs.end(); i++)
|
|
||||||
outIds.push_back(checkPath(*i));
|
|
||||||
|
|
||||||
FSIds genIds = findGenerators(outIds);
|
|
||||||
|
|
||||||
for (FSIds::iterator i = genIds.begin();
|
|
||||||
i != genIds.end(); i++)
|
|
||||||
cout << format("%s\n") % expandId(*i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
case qPredecessors: {
|
case qPredecessors: {
|
||||||
for (Strings::iterator i = opArgs.begin();
|
for (Strings::iterator i = opArgs.begin();
|
||||||
i != opArgs.end(); i++)
|
i != opArgs.end(); i++)
|
||||||
|
@ -172,7 +155,8 @@ static void opSuccessor(Strings opFlags, Strings opArgs)
|
||||||
if (!opFlags.empty()) throw UsageError("unknown flag");
|
if (!opFlags.empty()) throw UsageError("unknown flag");
|
||||||
if (opArgs.size() % 2) throw UsageError("expecting even number of arguments");
|
if (opArgs.size() % 2) throw UsageError("expecting even number of arguments");
|
||||||
|
|
||||||
Transaction txn(nixDB); /* !!! this could be a big transaction */
|
Transaction txn;
|
||||||
|
createStoreTransaction(txn);
|
||||||
for (Strings::iterator i = opArgs.begin();
|
for (Strings::iterator i = opArgs.begin();
|
||||||
i != opArgs.end(); )
|
i != opArgs.end(); )
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
#include "normalise.hh"
|
#include "normalise.hh"
|
||||||
#include "references.hh"
|
#include "references.hh"
|
||||||
#include "db.hh"
|
|
||||||
#include "exec.hh"
|
#include "exec.hh"
|
||||||
#include "pathlocks.hh"
|
#include "pathlocks.hh"
|
||||||
#include "globals.hh"
|
#include "globals.hh"
|
||||||
|
@ -11,7 +10,7 @@
|
||||||
static Path useSuccessor(const Path & path)
|
static Path useSuccessor(const Path & path)
|
||||||
{
|
{
|
||||||
string pathSucc;
|
string pathSucc;
|
||||||
if (nixDB.queryString(noTxn, dbSuccessors, path, pathSucc)) {
|
if (querySuccessor(path, pathSucc)) {
|
||||||
debug(format("successor %1% -> %2%") % (string) path % pathSucc);
|
debug(format("successor %1% -> %2%") % (string) path % pathSucc);
|
||||||
return pathSucc;
|
return pathSucc;
|
||||||
} else
|
} else
|
||||||
|
@ -349,7 +348,8 @@ Path normaliseNixExpr(const Path & _nePath, PathSet pending)
|
||||||
for recoverability: unregistered paths in the store can be
|
for recoverability: unregistered paths in the store can be
|
||||||
deleted arbitrarily, while registered paths can only be deleted
|
deleted arbitrarily, while registered paths can only be deleted
|
||||||
by running the garbage collector. */
|
by running the garbage collector. */
|
||||||
Transaction txn(nixDB);
|
Transaction txn;
|
||||||
|
createStoreTransaction(txn);
|
||||||
for (PathSet::iterator i = ne.derivation.outputs.begin();
|
for (PathSet::iterator i = ne.derivation.outputs.begin();
|
||||||
i != ne.derivation.outputs.end(); i++)
|
i != ne.derivation.outputs.end(); i++)
|
||||||
registerValidPath(txn, *i);
|
registerValidPath(txn, *i);
|
||||||
|
@ -434,50 +434,3 @@ PathSet nixExprRequisites(const Path & nePath,
|
||||||
paths, doneSet);
|
paths, doneSet);
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#if 0
|
|
||||||
PathSet findGenerators(const PathSet & outputs)
|
|
||||||
{
|
|
||||||
FSIdSet ids(_ids.begin(), _ids.end());
|
|
||||||
FSIds generators;
|
|
||||||
|
|
||||||
/* !!! hack; for performance, we just look at the rhs of successor
|
|
||||||
mappings, since we know that those are Nix expressions. */
|
|
||||||
|
|
||||||
Strings sucs;
|
|
||||||
nixDB.enumTable(noTxn, dbSuccessors, sucs);
|
|
||||||
|
|
||||||
for (Strings::iterator i = sucs.begin();
|
|
||||||
i != sucs.end(); i++)
|
|
||||||
{
|
|
||||||
string s;
|
|
||||||
if (!nixDB.queryString(noTxn, dbSuccessors, *i, s)) continue;
|
|
||||||
FSId id = parseHash(s);
|
|
||||||
|
|
||||||
NixExpr ne;
|
|
||||||
try {
|
|
||||||
/* !!! should substitutes be used? */
|
|
||||||
ne = parseNixExpr(termFromId(id));
|
|
||||||
} catch (...) { /* !!! only catch parse errors */
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ne.type != NixExpr::neClosure) continue;
|
|
||||||
|
|
||||||
bool okay = true;
|
|
||||||
for (ClosureElems::const_iterator i = ne.closure.elems.begin();
|
|
||||||
i != ne.closure.elems.end(); i++)
|
|
||||||
if (ids.find(i->second.id) == ids.end()) {
|
|
||||||
okay = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!okay) continue;
|
|
||||||
|
|
||||||
generators.push_back(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return generators;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
105
src/store.cc
105
src/store.cc
|
@ -1,7 +1,10 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "store.hh"
|
#include "store.hh"
|
||||||
#include "globals.hh"
|
#include "globals.hh"
|
||||||
|
@ -10,6 +13,81 @@
|
||||||
#include "pathlocks.hh"
|
#include "pathlocks.hh"
|
||||||
|
|
||||||
|
|
||||||
|
/* Nix database. */
|
||||||
|
static Database nixDB;
|
||||||
|
|
||||||
|
|
||||||
|
/* Database tables. */
|
||||||
|
|
||||||
|
/* dbValidPaths :: Path -> ()
|
||||||
|
|
||||||
|
The existence of a key $p$ indicates that path $p$ is valid (that
|
||||||
|
is, produced by a succesful build). */
|
||||||
|
static TableId dbValidPaths;
|
||||||
|
|
||||||
|
/* dbSuccessors :: Path -> Path
|
||||||
|
|
||||||
|
Each pair $(p_1, p_2)$ in this mapping records the fact that the
|
||||||
|
Nix expression stored at path $p_1$ has a successor expression
|
||||||
|
stored at path $p_2$.
|
||||||
|
|
||||||
|
Note that a term $y$ is a successor of $x$ iff there exists a
|
||||||
|
sequence of rewrite steps that rewrites $x$ into $y$.
|
||||||
|
*/
|
||||||
|
static TableId dbSuccessors;
|
||||||
|
|
||||||
|
/* dbSuccessorsRev :: Path -> [Path]
|
||||||
|
|
||||||
|
The reverse mapping of dbSuccessors (i.e., it stores the
|
||||||
|
predecessors of a Nix expression).
|
||||||
|
*/
|
||||||
|
static TableId dbSuccessorsRev;
|
||||||
|
|
||||||
|
/* dbSubstitutes :: Path -> [Path]
|
||||||
|
|
||||||
|
Each pair $(p, [ps])$ tells Nix that it can realise any of the
|
||||||
|
Nix expressions stored at paths $ps$ to produce a path $p$.
|
||||||
|
|
||||||
|
The main purpose of this is for distributed caching of derivates.
|
||||||
|
One system can compute a derivate and put it on a website (as a Nix
|
||||||
|
archive), for instance, and then another system can register a
|
||||||
|
substitute for that derivate. The substitute in this case might be
|
||||||
|
a Nix expression that fetches the Nix archive.
|
||||||
|
*/
|
||||||
|
static TableId dbSubstitutes;
|
||||||
|
|
||||||
|
/* dbSubstitutesRev :: Path -> [Path]
|
||||||
|
|
||||||
|
The reverse mapping of dbSubstitutes.
|
||||||
|
*/
|
||||||
|
static TableId dbSubstitutesRev;
|
||||||
|
|
||||||
|
|
||||||
|
void openDB()
|
||||||
|
{
|
||||||
|
nixDB.open(nixDBPath);
|
||||||
|
dbValidPaths = nixDB.openTable("validpaths");
|
||||||
|
dbSuccessors = nixDB.openTable("successors");
|
||||||
|
dbSuccessorsRev = nixDB.openTable("successors-rev");
|
||||||
|
dbSubstitutes = nixDB.openTable("substitutes");
|
||||||
|
dbSubstitutesRev = nixDB.openTable("substitutes-rev");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void initDB()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void createStoreTransaction(Transaction & txn)
|
||||||
|
{
|
||||||
|
Transaction txn2(nixDB);
|
||||||
|
txn2.moveTo(txn);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Path copying. */
|
||||||
|
|
||||||
struct CopySink : DumpSink
|
struct CopySink : DumpSink
|
||||||
{
|
{
|
||||||
int fd;
|
int fd;
|
||||||
|
@ -104,6 +182,12 @@ void registerSuccessor(const Transaction & txn,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool querySuccessor(const Path & srcPath, Path & sucPath)
|
||||||
|
{
|
||||||
|
return nixDB.queryString(noTxn, dbSuccessors, srcPath, sucPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Paths queryPredecessors(const Path & sucPath)
|
Paths queryPredecessors(const Path & sucPath)
|
||||||
{
|
{
|
||||||
Paths revs;
|
Paths revs;
|
||||||
|
@ -204,6 +288,27 @@ Path addToStore(const Path & _srcPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void addTextToStore(const Path & dstPath, const string & s)
|
||||||
|
{
|
||||||
|
if (!isValidPath(dstPath)) {
|
||||||
|
|
||||||
|
/* !!! locking? -> parallel writes are probably idempotent */
|
||||||
|
|
||||||
|
int fd = open(dstPath.c_str(), O_CREAT | O_EXCL | O_WRONLY, 0666);
|
||||||
|
if (fd == -1) throw SysError(format("creating store file `%1%'") % dstPath);
|
||||||
|
|
||||||
|
if (write(fd, s.c_str(), s.size()) != (ssize_t) s.size())
|
||||||
|
throw SysError(format("writing store file `%1%'") % dstPath);
|
||||||
|
|
||||||
|
close(fd); /* !!! close on exception */
|
||||||
|
|
||||||
|
Transaction txn(nixDB);
|
||||||
|
registerValidPath(txn, dstPath);
|
||||||
|
txn.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void deleteFromStore(const Path & _path)
|
void deleteFromStore(const Path & _path)
|
||||||
{
|
{
|
||||||
Path path(canonPath(_path));
|
Path path(canonPath(_path));
|
||||||
|
|
18
src/store.hh
18
src/store.hh
|
@ -9,6 +9,15 @@
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
|
|
||||||
|
/* Open the database environment. */
|
||||||
|
void openDB();
|
||||||
|
|
||||||
|
/* Create the required database tables. */
|
||||||
|
void initDB();
|
||||||
|
|
||||||
|
/* Get a transaction object. */
|
||||||
|
void createStoreTransaction(Transaction & txn);
|
||||||
|
|
||||||
/* Copy a path recursively. */
|
/* Copy a path recursively. */
|
||||||
void copyPath(const Path & src, const Path & dst);
|
void copyPath(const Path & src, const Path & dst);
|
||||||
|
|
||||||
|
@ -22,6 +31,10 @@ void copyPath(const Path & src, const Path & dst);
|
||||||
void registerSuccessor(const Transaction & txn,
|
void registerSuccessor(const Transaction & txn,
|
||||||
const Path & srcPath, const Path & sucPath);
|
const Path & srcPath, const Path & sucPath);
|
||||||
|
|
||||||
|
/* Return the predecessors of the Nix expression stored at the given
|
||||||
|
path. */
|
||||||
|
bool querySuccessor(const Path & srcPath, Path & sucPath);
|
||||||
|
|
||||||
/* Return the predecessors of the Nix expression stored at the given
|
/* Return the predecessors of the Nix expression stored at the given
|
||||||
path. */
|
path. */
|
||||||
Paths queryPredecessors(const Path & sucPath);
|
Paths queryPredecessors(const Path & sucPath);
|
||||||
|
@ -42,6 +55,11 @@ bool isValidPath(const Path & path);
|
||||||
the resulting path. The resulting path is returned. */
|
the resulting path. The resulting path is returned. */
|
||||||
Path addToStore(const Path & srcPath);
|
Path addToStore(const Path & srcPath);
|
||||||
|
|
||||||
|
/* Like addToStore, but the path of the output is given, and the
|
||||||
|
contents written to the output path is a regular file containing
|
||||||
|
the given string. */
|
||||||
|
void addTextToStore(const Path & dstPath, const string & s);
|
||||||
|
|
||||||
/* Delete a value from the nixStore directory. */
|
/* Delete a value from the nixStore directory. */
|
||||||
void deleteFromStore(const Path & path);
|
void deleteFromStore(const Path & path);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue