* Started implementing the new evaluation model.
* Lots of refactorings. * Unit tests.
This commit is contained in:
parent
b9f09b3268
commit
822794001c
|
@ -9,16 +9,15 @@ nix_LDADD = -ldb_cxx-4 -lATerm
|
||||||
fix_SOURCES = fix.cc util.cc hash.cc md5.c
|
fix_SOURCES = fix.cc util.cc hash.cc md5.c
|
||||||
fix_LDADD = -lATerm
|
fix_LDADD = -lATerm
|
||||||
|
|
||||||
test_SOURCES = test.cc util.cc hash.cc md5.c
|
test_SOURCES = test.cc util.cc hash.cc md5.c eval.cc values.cc globals.cc db.cc
|
||||||
|
test_LDADD = -ldb_cxx-4 -lATerm
|
||||||
|
|
||||||
install-data-local:
|
install-data-local:
|
||||||
$(INSTALL) -d $(localstatedir)/nix
|
$(INSTALL) -d $(localstatedir)/nix
|
||||||
$(INSTALL) -d $(localstatedir)/nix/descriptors
|
|
||||||
$(INSTALL) -d $(localstatedir)/nix/sources
|
|
||||||
$(INSTALL) -d $(localstatedir)/nix/links
|
$(INSTALL) -d $(localstatedir)/nix/links
|
||||||
$(INSTALL) -d $(localstatedir)/nix/prebuilts
|
# $(INSTALL) -d $(localstatedir)/nix/prebuilts
|
||||||
$(INSTALL) -d $(localstatedir)/nix/prebuilts/imports
|
# $(INSTALL) -d $(localstatedir)/nix/prebuilts/imports
|
||||||
$(INSTALL) -d $(localstatedir)/nix/prebuilts/exports
|
# $(INSTALL) -d $(localstatedir)/nix/prebuilts/exports
|
||||||
$(INSTALL) -d $(localstatedir)/log/nix
|
$(INSTALL) -d $(localstatedir)/log/nix
|
||||||
$(INSTALL) -d $(prefix)/pkg
|
$(INSTALL) -d $(prefix)/values
|
||||||
$(bindir)/nix init
|
$(bindir)/nix init
|
||||||
|
|
297
src/eval.cc
Normal file
297
src/eval.cc
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
#include <map>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "eval.hh"
|
||||||
|
#include "globals.hh"
|
||||||
|
#include "values.hh"
|
||||||
|
#include "db.hh"
|
||||||
|
|
||||||
|
|
||||||
|
/* A Unix environment is a mapping from strings to strings. */
|
||||||
|
typedef map<string, string> Environment;
|
||||||
|
|
||||||
|
|
||||||
|
/* Return true iff the given path exists. */
|
||||||
|
bool pathExists(string path)
|
||||||
|
{
|
||||||
|
int res;
|
||||||
|
struct stat st;
|
||||||
|
res = stat(path.c_str(), &st);
|
||||||
|
if (!res) return true;
|
||||||
|
if (errno != ENOENT)
|
||||||
|
throw SysError("getting status of " + path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Compute a derived value by running a program. */
|
||||||
|
static Hash computeDerived(Hash sourceHash, string targetName,
|
||||||
|
string platform, Hash prog, Environment env)
|
||||||
|
{
|
||||||
|
string targetPath = nixValues + "/" +
|
||||||
|
(string) sourceHash + "-nf";
|
||||||
|
|
||||||
|
/* Check whether the target already exists. */
|
||||||
|
if (pathExists(targetPath))
|
||||||
|
throw Error("derived value in " + targetPath + " already exists");
|
||||||
|
|
||||||
|
/* Find the program corresponding to the hash `prog'. */
|
||||||
|
string progPath = queryValuePath(prog);
|
||||||
|
|
||||||
|
/* Finalize the environment. */
|
||||||
|
env["out"] = targetPath;
|
||||||
|
|
||||||
|
/* Create a log file. */
|
||||||
|
string logFileName =
|
||||||
|
nixLogDir + "/" + baseNameOf(targetPath) + ".log";
|
||||||
|
/* !!! auto-pclose on exit */
|
||||||
|
FILE * logFile = popen(("tee " + logFileName + " >&2").c_str(), "w"); /* !!! escaping */
|
||||||
|
if (!logFile)
|
||||||
|
throw SysError("unable to create log file " + logFileName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
/* Fork a child to build the package. */
|
||||||
|
pid_t pid;
|
||||||
|
switch (pid = fork()) {
|
||||||
|
|
||||||
|
case -1:
|
||||||
|
throw SysError("unable to fork");
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
|
||||||
|
try { /* child */
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
/* Try to use a prebuilt. */
|
||||||
|
string prebuiltHashS, prebuiltFile;
|
||||||
|
if (queryDB(nixDB, dbPrebuilts, hash, prebuiltHashS)) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
prebuiltFile = getFile(parseHash(prebuiltHashS));
|
||||||
|
} catch (Error e) {
|
||||||
|
cerr << "cannot obtain prebuilt (ignoring): " << e.what() << endl;
|
||||||
|
goto build;
|
||||||
|
}
|
||||||
|
|
||||||
|
cerr << "substituting prebuilt " << prebuiltFile << endl;
|
||||||
|
|
||||||
|
int res = system(("tar xfj " + prebuiltFile + " 1>&2").c_str()); // !!! escaping
|
||||||
|
if (WEXITSTATUS(res) != 0)
|
||||||
|
/* This is a fatal error, because path may now
|
||||||
|
have clobbered. */
|
||||||
|
throw Error("cannot unpack " + prebuiltFile);
|
||||||
|
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
build:
|
||||||
|
|
||||||
|
/* Fill in the environment. We don't bother freeing
|
||||||
|
the strings, since we'll exec or die soon
|
||||||
|
anyway. */
|
||||||
|
const char * env2[env.size() + 1];
|
||||||
|
int i = 0;
|
||||||
|
for (Environment::iterator it = env.begin();
|
||||||
|
it != env.end(); it++, i++)
|
||||||
|
env2[i] = (new string(it->first + "=" + it->second))->c_str();
|
||||||
|
env2[i] = 0;
|
||||||
|
|
||||||
|
/* Dup the log handle into stderr. */
|
||||||
|
if (dup2(fileno(logFile), STDERR_FILENO) == -1)
|
||||||
|
throw Error("cannot pipe standard error into log file: " + string(strerror(errno)));
|
||||||
|
|
||||||
|
/* Dup stderr to stdin. */
|
||||||
|
if (dup2(STDERR_FILENO, STDOUT_FILENO) == -1)
|
||||||
|
throw Error("cannot dup stderr into stdout");
|
||||||
|
|
||||||
|
/* Make the program executable. !!! hack. */
|
||||||
|
if (chmod(progPath.c_str(), 0755))
|
||||||
|
throw Error("cannot make program executable");
|
||||||
|
|
||||||
|
/* Execute the program. This should not return. */
|
||||||
|
execle(progPath.c_str(), baseNameOf(progPath).c_str(), 0, env2);
|
||||||
|
|
||||||
|
throw Error("unable to execute builder: " +
|
||||||
|
string(strerror(errno)));
|
||||||
|
|
||||||
|
} catch (exception & e) {
|
||||||
|
cerr << "build error: " << e.what() << endl;
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* parent */
|
||||||
|
|
||||||
|
/* Close the logging pipe. Note that this should not cause
|
||||||
|
the logger to exit until builder exits (because the latter
|
||||||
|
has an open file handle to the former). */
|
||||||
|
pclose(logFile);
|
||||||
|
|
||||||
|
/* Wait for the child to finish. */
|
||||||
|
int status;
|
||||||
|
if (waitpid(pid, &status, 0) != pid)
|
||||||
|
throw Error("unable to wait for child");
|
||||||
|
|
||||||
|
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
|
||||||
|
throw Error("unable to build package");
|
||||||
|
|
||||||
|
/* Check whether the result was created. */
|
||||||
|
if (!pathExists(targetPath))
|
||||||
|
throw Error("program " + progPath +
|
||||||
|
" failed to create a result in " + targetPath);
|
||||||
|
|
||||||
|
/* Remove write permission from the value. */
|
||||||
|
int res = system(("chmod -R -w " + targetPath).c_str()); // !!! escaping
|
||||||
|
if (WEXITSTATUS(res) != 0)
|
||||||
|
throw Error("cannot remove write permission from " + targetPath);
|
||||||
|
|
||||||
|
} catch (exception &) {
|
||||||
|
// system(("rm -rf " + targetPath).c_str());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hash the result. */
|
||||||
|
Hash targetHash = hashFile(targetPath);
|
||||||
|
|
||||||
|
/* Register targetHash -> targetPath. !!! this should be in
|
||||||
|
values.cc. */
|
||||||
|
setDB(nixDB, dbNFs, sourceHash, targetName);
|
||||||
|
|
||||||
|
/* Register that targetHash was produced by evaluating
|
||||||
|
sourceHash; i.e., that targetHash is a normal form of
|
||||||
|
sourceHash. !!! this shouldn't be here */
|
||||||
|
setDB(nixDB, dbNFs, sourceHash, targetHash);
|
||||||
|
|
||||||
|
return targetHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Throw an exception if the given platform string is not supported by
|
||||||
|
the platform we are executing on. */
|
||||||
|
static void checkPlatform(string platform)
|
||||||
|
{
|
||||||
|
if (platform != thisSystem)
|
||||||
|
throw Error("a `" + platform +
|
||||||
|
"' is required, but I am a `" + thisSystem + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Throw an exception with an error message containing the given
|
||||||
|
aterm. */
|
||||||
|
static Error badTerm(const string & msg, Expr e)
|
||||||
|
{
|
||||||
|
char * s = ATwriteToString(e);
|
||||||
|
return Error(msg + ", in `" + s + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Hash an expression. Hopefully the representation used by
|
||||||
|
ATwriteToString() won't change, otherwise all hashes will
|
||||||
|
change. */
|
||||||
|
static Hash hashExpr(Expr e)
|
||||||
|
{
|
||||||
|
char * s = ATwriteToString(e);
|
||||||
|
debug(s);
|
||||||
|
return hashString(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Evaluate an expression; the result must be a string. */
|
||||||
|
static string evalString(Expr e)
|
||||||
|
{
|
||||||
|
e = evalValue(e).e;
|
||||||
|
char * s;
|
||||||
|
if (ATmatch(e, "Str(<str>)", &s)) return s;
|
||||||
|
else throw badTerm("string value expected", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Evaluate an expression; the result must be a external
|
||||||
|
non-expression reference. */
|
||||||
|
static Hash evalExternal(Expr e)
|
||||||
|
{
|
||||||
|
EvalResult r = evalValue(e);
|
||||||
|
char * s;
|
||||||
|
if (ATmatch(r.e, "External(<str>)", &s)) return r.h;
|
||||||
|
else throw badTerm("external non-expression value expected", r.e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Evaluate an expression. */
|
||||||
|
EvalResult evalValue(Expr e)
|
||||||
|
{
|
||||||
|
EvalResult r;
|
||||||
|
char * s;
|
||||||
|
Expr eBuildPlatform, eProg;
|
||||||
|
ATermList args;
|
||||||
|
|
||||||
|
/* Normal forms. */
|
||||||
|
if (ATmatch(e, "Str(<str>)", &s) ||
|
||||||
|
ATmatch(e, "Bool(True)") ||
|
||||||
|
ATmatch(e, "Bool(False)"))
|
||||||
|
{
|
||||||
|
r.e = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* External expressions. */
|
||||||
|
|
||||||
|
/* External non-expressions. */
|
||||||
|
else if (ATmatch(e, "External(<str>)", &s)) {
|
||||||
|
r.e = e;
|
||||||
|
r.h = parseHash(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Execution primitive. */
|
||||||
|
|
||||||
|
else if (ATmatch(e, "Exec(<term>, <term>, [<list>])",
|
||||||
|
&eBuildPlatform, &eProg, &args))
|
||||||
|
{
|
||||||
|
string buildPlatform = evalString(eBuildPlatform);
|
||||||
|
|
||||||
|
checkPlatform(buildPlatform);
|
||||||
|
|
||||||
|
Hash prog = evalExternal(eProg);
|
||||||
|
|
||||||
|
Environment env;
|
||||||
|
while (!ATisEmpty(args)) {
|
||||||
|
debug("arg");
|
||||||
|
Expr arg = ATgetFirst(args);
|
||||||
|
throw badTerm("foo", arg);
|
||||||
|
args = ATgetNext(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
Hash sourceHash = hashExpr(
|
||||||
|
ATmake("Exec(Str(<str>), External(<str>), [])",
|
||||||
|
buildPlatform.c_str(), ((string) prog).c_str()));
|
||||||
|
|
||||||
|
/* Do we know a normal form for sourceHash? */
|
||||||
|
Hash targetHash;
|
||||||
|
string targetHashS;
|
||||||
|
if (queryDB(nixDB, dbNFs, sourceHash, targetHashS)) {
|
||||||
|
/* Yes. */
|
||||||
|
targetHash = parseHash(targetHashS);
|
||||||
|
debug("already built: " + (string) sourceHash
|
||||||
|
+ " -> " + (string) targetHash);
|
||||||
|
} else {
|
||||||
|
/* No, so we compute one. */
|
||||||
|
targetHash = computeDerived(sourceHash,
|
||||||
|
(string) sourceHash + "-nf", buildPlatform, prog, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
r.e = ATmake("External(<str>)", ((string) targetHash).c_str());
|
||||||
|
r.h = targetHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barf. */
|
||||||
|
else throw badTerm("invalid expression", e);
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
86
src/eval.hh
Normal file
86
src/eval.hh
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
#ifndef __EVAL_H
|
||||||
|
#define __EVAL_H
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <aterm2.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "hash.hh"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
|
||||||
|
/* Abstract syntax of Nix values:
|
||||||
|
|
||||||
|
e := Hash(h) -- reference to expression value
|
||||||
|
| External(h) -- reference to non-expression value
|
||||||
|
| Str(s) -- string constant
|
||||||
|
| Bool(b) -- boolean constant
|
||||||
|
| App(e, e) -- application
|
||||||
|
| Lam(x, e) -- lambda abstraction
|
||||||
|
| Exec(platform, e, [(s, e)])
|
||||||
|
-- primitive; execute e with args e* on platform
|
||||||
|
;
|
||||||
|
|
||||||
|
Semantics
|
||||||
|
|
||||||
|
Each rules given as eval(e) => (e', h'), i.e., expression e has a
|
||||||
|
normal form e' with hash code h'. evalE = fst . eval. evalH = snd
|
||||||
|
. eval.
|
||||||
|
|
||||||
|
eval(Hash(h)) => eval(loadExpr(h))
|
||||||
|
|
||||||
|
eval(External(h)) => (External(h), h)
|
||||||
|
|
||||||
|
eval(Str(s)@e) => (e, 0) # idem for Bool
|
||||||
|
|
||||||
|
eval(App(e1, e2)) => eval(App(e1', e2))
|
||||||
|
where e1' = evalE(e1)
|
||||||
|
|
||||||
|
eval(App(Lam(var, body), arg)@in) =>
|
||||||
|
eval(subst(var, arg, body))@out
|
||||||
|
[AND write out to storage, and dbNFs[hash(in)] = hash(out) ???]
|
||||||
|
|
||||||
|
eval(Exec(platform, prog, args)@e) =>
|
||||||
|
(External(h), h)
|
||||||
|
where
|
||||||
|
hIn = hashExpr(e)
|
||||||
|
|
||||||
|
fn = ... form name involving hIn ...
|
||||||
|
|
||||||
|
h =
|
||||||
|
if exec(evalE(platform) => Str(...)
|
||||||
|
, getFile(evalH(prog))
|
||||||
|
, map(makeArg . eval, args)
|
||||||
|
) then
|
||||||
|
hashExternal(fn)
|
||||||
|
else
|
||||||
|
undef
|
||||||
|
|
||||||
|
makeArg((argn, (External(h), h))) => (argn, getFile(h))
|
||||||
|
makeArg((argn, (Str(s), _))) => (argn, s)
|
||||||
|
makeArg((argn, (Bool(True), _))) => (argn, "1")
|
||||||
|
makeArg((argn, (Bool(False), _))) => (argn, undef)
|
||||||
|
|
||||||
|
getFile :: Hash -> FileName
|
||||||
|
loadExpr :: Hash -> FileName
|
||||||
|
hashExpr :: Expr -> Hash
|
||||||
|
hashExternal :: FileName -> Hash
|
||||||
|
exec :: Platform -> FileName -> [(String, String)] -> Status
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef ATerm Expr;
|
||||||
|
|
||||||
|
|
||||||
|
struct EvalResult
|
||||||
|
{
|
||||||
|
Expr e;
|
||||||
|
Hash h;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* Evaluate an expression. */
|
||||||
|
EvalResult evalValue(Expr e);
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* !__EVAL_H */
|
19
src/globals.cc
Normal file
19
src/globals.cc
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#include "globals.hh"
|
||||||
|
#include "db.hh"
|
||||||
|
|
||||||
|
|
||||||
|
string dbRefs = "refs";
|
||||||
|
string dbNFs = "nfs";
|
||||||
|
string dbNetSources = "netsources";
|
||||||
|
|
||||||
|
string nixValues = "/UNINIT";
|
||||||
|
string nixLogDir = "/UNINIT";
|
||||||
|
string nixDB = "/UNINIT";
|
||||||
|
|
||||||
|
|
||||||
|
void initDB()
|
||||||
|
{
|
||||||
|
createDB(nixDB, dbRefs);
|
||||||
|
createDB(nixDB, dbNFs);
|
||||||
|
createDB(nixDB, dbNetSources);
|
||||||
|
}
|
60
src/globals.hh
Normal file
60
src/globals.hh
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
#ifndef __GLOBALS_H
|
||||||
|
#define __GLOBALS_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
|
||||||
|
/* Database names. */
|
||||||
|
|
||||||
|
/* dbRefs :: Hash -> FileName
|
||||||
|
|
||||||
|
Maintains a mapping from hashes to filenames within the NixValues
|
||||||
|
directory. This mapping is for performance only; it can be
|
||||||
|
reconstructed unambiguously. The reason is that names in this
|
||||||
|
directory are not printed hashes but also might carry some
|
||||||
|
descriptive element (e.g., "aterm-2.0-ae749a..."). Without this
|
||||||
|
mapping, looking up a value would take O(n) time because we would
|
||||||
|
need to read the entire directory. */
|
||||||
|
extern string dbRefs;
|
||||||
|
|
||||||
|
/* dbNFs :: Hash -> Hash
|
||||||
|
|
||||||
|
Each pair (h1, h2) in this mapping records the fact that the value
|
||||||
|
referenced by h2 is a normal form obtained by evaluating the value
|
||||||
|
referenced by value h1.
|
||||||
|
*/
|
||||||
|
extern string dbNFs;
|
||||||
|
|
||||||
|
/* dbNetSources :: Hash -> URL
|
||||||
|
|
||||||
|
Each pair (hash, url) in this mapping states that the value
|
||||||
|
identified by hash can be obtained by fetching the value pointed
|
||||||
|
to by url.
|
||||||
|
|
||||||
|
TODO: this should be Hash -> [URL]
|
||||||
|
|
||||||
|
TODO: factor this out into a separate tool? */
|
||||||
|
extern string dbNetSources;
|
||||||
|
|
||||||
|
|
||||||
|
/* Path names. */
|
||||||
|
|
||||||
|
/* nixValues is the directory where all Nix values (both files and
|
||||||
|
directories, and both normal and non-normal forms) live. */
|
||||||
|
extern string nixValues;
|
||||||
|
|
||||||
|
/* nixLogDir is the directory where we log evaluations. */
|
||||||
|
extern string nixLogDir;
|
||||||
|
|
||||||
|
/* nixDB is the file name of the Berkeley DB database where we
|
||||||
|
maintain the dbXXX mappings. */
|
||||||
|
extern string nixDB;
|
||||||
|
|
||||||
|
|
||||||
|
/* Initialize the databases. */
|
||||||
|
void initDB();
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* !__GLOBALS_H */
|
15
src/hash.cc
15
src/hash.cc
|
@ -46,6 +46,8 @@ Hash::operator string() const
|
||||||
Hash parseHash(const string & s)
|
Hash parseHash(const string & s)
|
||||||
{
|
{
|
||||||
Hash hash;
|
Hash hash;
|
||||||
|
if (s.length() != Hash::hashSize * 2)
|
||||||
|
throw BadRefError("invalid hash: " + s);
|
||||||
for (unsigned int i = 0; i < Hash::hashSize; i++) {
|
for (unsigned int i = 0; i < Hash::hashSize; i++) {
|
||||||
string s2(s, i * 2, 2);
|
string s2(s, i * 2, 2);
|
||||||
if (!isxdigit(s2[0]) || !isxdigit(s2[1]))
|
if (!isxdigit(s2[0]) || !isxdigit(s2[1]))
|
||||||
|
@ -73,15 +75,24 @@ bool isHash(const string & s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Compute the MD5 hash of a file. */
|
||||||
|
Hash hashString(const string & s)
|
||||||
|
{
|
||||||
|
Hash hash;
|
||||||
|
md5_buffer(s.c_str(), s.length(), hash.hash);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Compute the MD5 hash of a file. */
|
/* Compute the MD5 hash of a file. */
|
||||||
Hash hashFile(const string & fileName)
|
Hash hashFile(const string & fileName)
|
||||||
{
|
{
|
||||||
Hash hash;
|
Hash hash;
|
||||||
FILE * file = fopen(fileName.c_str(), "rb");
|
FILE * file = fopen(fileName.c_str(), "rb");
|
||||||
if (!file)
|
if (!file)
|
||||||
throw Error("file `" + fileName + "' does not exist");
|
throw SysError("file `" + fileName + "' does not exist");
|
||||||
int err = md5_stream(file, hash.hash);
|
int err = md5_stream(file, hash.hash);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
if (err) throw Error("cannot hash file");
|
if (err) throw SysError("cannot hash file " + fileName);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ public:
|
||||||
|
|
||||||
Hash parseHash(const string & s);
|
Hash parseHash(const string & s);
|
||||||
bool isHash(const string & s);
|
bool isHash(const string & s);
|
||||||
|
Hash hashString(const string & s);
|
||||||
Hash hashFile(const string & fileName);
|
Hash hashFile(const string & fileName);
|
||||||
|
|
||||||
#endif /* !__HASH_H */
|
#endif /* !__HASH_H */
|
||||||
|
|
156
src/nix.cc
156
src/nix.cc
|
@ -11,155 +11,15 @@
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
#include <aterm1.h>
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "util.hh"
|
#include "util.hh"
|
||||||
#include "hash.hh"
|
#include "hash.hh"
|
||||||
#include "db.hh"
|
#include "db.hh"
|
||||||
|
#include "nix.hh"
|
||||||
|
#include "eval.hh"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
|
|
||||||
/* Database names. */
|
|
||||||
|
|
||||||
/* dbRefs :: Hash -> FileName
|
|
||||||
|
|
||||||
Maintains a mapping from hashes to filenames within the NixValues
|
|
||||||
directory. This mapping is for performance only; it can be
|
|
||||||
reconstructed unambiguously from the nixValues directory. The
|
|
||||||
reason is that names in this directory are not printed hashes but
|
|
||||||
also might carry some descriptive element (e.g.,
|
|
||||||
"aterm-2.0-ae749a..."). Without this mapping, looking up a value
|
|
||||||
would take O(n) time because we would need to read the entire
|
|
||||||
directory. */
|
|
||||||
static string dbRefs = "refs";
|
|
||||||
|
|
||||||
/* dbNFs :: Hash -> Hash
|
|
||||||
|
|
||||||
Each pair (h1, h2) in this mapping records the fact that h2 is a
|
|
||||||
normal form obtained by evaluating the value h1.
|
|
||||||
|
|
||||||
We would really like to have h2 be the hash of the object
|
|
||||||
referenced by h2. However, that gives a cyclic dependency: to
|
|
||||||
compute the hash (and thus the file name) of the object, we need to
|
|
||||||
compute the object, but to do that, we need the file name of the
|
|
||||||
object.
|
|
||||||
|
|
||||||
So for now we abandon the requirement that
|
|
||||||
|
|
||||||
hashFile(dbRefs[h]) == h.
|
|
||||||
|
|
||||||
I.e., this property does not hold for computed normal forms.
|
|
||||||
Rather, we use h2 = hash(h1). This allows dbNFs to be
|
|
||||||
reconstructed. Perhaps using a pseudo random number would be
|
|
||||||
better to prevent the system from being subverted in some way.
|
|
||||||
*/
|
|
||||||
static string dbNFs = "nfs";
|
|
||||||
|
|
||||||
/* dbNetSources :: Hash -> URL
|
|
||||||
|
|
||||||
Each pair (hash, url) in this mapping states that the object
|
|
||||||
identified by hash can be obtained by fetching the object pointed
|
|
||||||
to by url.
|
|
||||||
|
|
||||||
TODO: this should be Hash -> [URL]
|
|
||||||
|
|
||||||
TODO: factor this out into a separate tool? */
|
|
||||||
static string dbNetSources = "netsources";
|
|
||||||
|
|
||||||
|
|
||||||
/* Path names. */
|
|
||||||
|
|
||||||
/* nixValues is the directory where all Nix values (both files and
|
|
||||||
directories, and both normal and non-normal forms) live. */
|
|
||||||
static string nixValues;
|
|
||||||
|
|
||||||
/* nixLogDir is the directory where we log evaluations. */
|
|
||||||
static string nixLogDir;
|
|
||||||
|
|
||||||
/* nixDB is the file name of the Berkeley DB database where we
|
|
||||||
maintain the dbXXX mappings. */
|
|
||||||
static string nixDB;
|
|
||||||
|
|
||||||
|
|
||||||
/* Abstract syntax of Nix values:
|
|
||||||
|
|
||||||
e := Hash(h) -- external reference
|
|
||||||
| Str(s) -- string constant
|
|
||||||
| Bool(b) -- boolean constant
|
|
||||||
| Name(e) -- "&" operator; pointer (file name) formation
|
|
||||||
| App(e, e) -- application
|
|
||||||
| Lam(x, e) -- lambda abstraction
|
|
||||||
| Exec(platform, e, e*)
|
|
||||||
-- primitive; execute e with args e* on platform
|
|
||||||
;
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/* Download object referenced by the given URL into the sources
|
|
||||||
directory. Return the file name it was downloaded to. */
|
|
||||||
string fetchURL(string url)
|
|
||||||
{
|
|
||||||
string filename = baseNameOf(url);
|
|
||||||
string fullname = nixSourcesDir + "/" + filename;
|
|
||||||
struct stat st;
|
|
||||||
if (stat(fullname.c_str(), &st)) {
|
|
||||||
cerr << "fetching " << url << endl;
|
|
||||||
/* !!! quoting */
|
|
||||||
string shellCmd =
|
|
||||||
"cd " + nixSourcesDir + " && wget --quiet -N \"" + url + "\"";
|
|
||||||
int res = system(shellCmd.c_str());
|
|
||||||
if (WEXITSTATUS(res) != 0)
|
|
||||||
throw Error("cannot fetch " + url);
|
|
||||||
}
|
|
||||||
return fullname;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Obtain an object with the given hash. If a file with that hash is
|
|
||||||
known to exist in the local file system (as indicated by the dbRefs
|
|
||||||
database), we use that. Otherwise, we attempt to fetch it from the
|
|
||||||
network (using dbNetSources). We verify that the file has the
|
|
||||||
right hash. */
|
|
||||||
string getFile(Hash hash)
|
|
||||||
{
|
|
||||||
bool checkedNet = false;
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
|
|
||||||
string fn, url;
|
|
||||||
|
|
||||||
if (queryDB(nixDB, dbRefs, hash, fn)) {
|
|
||||||
|
|
||||||
/* Verify that the file hasn't changed. !!! race */
|
|
||||||
if (hashFile(fn) != hash)
|
|
||||||
throw Error("file " + fn + " is stale");
|
|
||||||
|
|
||||||
return fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkedNet)
|
|
||||||
throw Error("consistency problem: file fetched from " + url +
|
|
||||||
" should have hash " + (string) hash + ", but it doesn't");
|
|
||||||
|
|
||||||
if (!queryDB(nixDB, dbNetSources, hash, url))
|
|
||||||
throw Error("a file with hash " + (string) hash + " is requested, "
|
|
||||||
"but it is not known to exist locally or on the network");
|
|
||||||
|
|
||||||
checkedNet = true;
|
|
||||||
|
|
||||||
fn = fetchURL(url);
|
|
||||||
|
|
||||||
setDB(nixDB, dbRefs, hash, fn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
typedef map<string, string> Params;
|
|
||||||
|
|
||||||
|
|
||||||
void readPkgDescr(Hash hash,
|
void readPkgDescr(Hash hash,
|
||||||
Params & pkgImports, Params & fileImports, Params & arguments)
|
Params & pkgImports, Params & fileImports, Params & arguments)
|
||||||
{
|
{
|
||||||
|
@ -204,9 +64,6 @@ void readPkgDescr(Hash hash,
|
||||||
string getPkg(Hash hash);
|
string getPkg(Hash hash);
|
||||||
|
|
||||||
|
|
||||||
typedef map<string, string> Environment;
|
|
||||||
|
|
||||||
|
|
||||||
void fetchDeps(Hash hash, Environment & env)
|
void fetchDeps(Hash hash, Environment & env)
|
||||||
{
|
{
|
||||||
/* Read the package description file. */
|
/* Read the package description file. */
|
||||||
|
@ -538,15 +395,6 @@ void registerInstalledPkg(Hash hash, string path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void initDB()
|
|
||||||
{
|
|
||||||
createDB(nixDB, dbRefs);
|
|
||||||
createDB(nixDB, dbInstPkgs);
|
|
||||||
createDB(nixDB, dbPrebuilts);
|
|
||||||
createDB(nixDB, dbNetSources);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void verifyDB()
|
void verifyDB()
|
||||||
{
|
{
|
||||||
/* Check that all file references are still valid. */
|
/* Check that all file references are still valid. */
|
||||||
|
|
3
src/test-builder-1.sh
Normal file
3
src/test-builder-1.sh
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
echo "Hello World" > $out
|
5
src/test-builder-2.sh
Normal file
5
src/test-builder-2.sh
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
mkdir $out || exit 1
|
||||||
|
cd $out || exit 1
|
||||||
|
echo "Hello World" > bla
|
82
src/test.cc
82
src/test.cc
|
@ -1,16 +1,82 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
#include "hash.hh"
|
#include "hash.hh"
|
||||||
|
#include "util.hh"
|
||||||
|
#include "eval.hh"
|
||||||
|
#include "values.hh"
|
||||||
|
#include "globals.hh"
|
||||||
|
|
||||||
|
|
||||||
|
void evalTest(Expr e)
|
||||||
|
{
|
||||||
|
EvalResult r = evalValue(e);
|
||||||
|
|
||||||
|
char * s = ATwriteToString(r.e);
|
||||||
|
cout << (string) r.h << ": " << s << endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void runTests()
|
||||||
|
{
|
||||||
|
/* Hashing. */
|
||||||
|
string s = "0b0ffd0538622bfe20b92c4aa57254d9";
|
||||||
|
Hash h = parseHash(s);
|
||||||
|
if ((string) h != s) abort();
|
||||||
|
|
||||||
|
try {
|
||||||
|
h = parseHash("blah blah");
|
||||||
|
abort();
|
||||||
|
} catch (BadRefError err) { };
|
||||||
|
|
||||||
|
try {
|
||||||
|
h = parseHash("0b0ffd0538622bfe20b92c4aa57254d99");
|
||||||
|
abort();
|
||||||
|
} catch (BadRefError err) { };
|
||||||
|
|
||||||
|
|
||||||
|
/* Set up the test environment. */
|
||||||
|
|
||||||
|
mkdir("scratch", 0777);
|
||||||
|
|
||||||
|
string testDir = absPath("scratch");
|
||||||
|
cout << testDir << endl;
|
||||||
|
|
||||||
|
nixValues = testDir;
|
||||||
|
nixLogDir = testDir;
|
||||||
|
nixDB = testDir + "/db";
|
||||||
|
|
||||||
|
initDB();
|
||||||
|
|
||||||
|
/* Expression evaluation. */
|
||||||
|
|
||||||
|
evalTest(ATmake("Str(\"Hello World\")"));
|
||||||
|
evalTest(ATmake("Bool(True)"));
|
||||||
|
evalTest(ATmake("Bool(False)"));
|
||||||
|
|
||||||
|
Hash builder1 = addValue("./test-builder-1.sh");
|
||||||
|
|
||||||
|
evalTest(ATmake("Exec(Str(<str>), External(<str>), [])",
|
||||||
|
thisSystem.c_str(), ((string) builder1).c_str()));
|
||||||
|
|
||||||
|
Hash builder2 = addValue("./test-builder-2.sh");
|
||||||
|
|
||||||
|
evalTest(ATmake("Exec(Str(<str>), External(<str>), [])",
|
||||||
|
thisSystem.c_str(), ((string) builder2).c_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int main(int argc, char * * argv)
|
int main(int argc, char * * argv)
|
||||||
{
|
{
|
||||||
Hash h = hashFile("/etc/passwd");
|
ATerm bottomOfStack;
|
||||||
|
ATinit(argc, argv, &bottomOfStack);
|
||||||
cout << (string) h << endl;
|
|
||||||
|
|
||||||
h = parseHash("0b0ffd0538622bfe20b92c4aa57254d9");
|
try {
|
||||||
|
runTests();
|
||||||
cout << (string) h << endl;
|
} catch (exception & e) {
|
||||||
|
cerr << "error: " << e.what() << endl;
|
||||||
return 0;
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
54
src/util.cc
54
src/util.cc
|
@ -1,47 +1,55 @@
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
#include "util.hh"
|
#include "util.hh"
|
||||||
|
|
||||||
|
|
||||||
string thisSystem = SYSTEM;
|
string thisSystem = SYSTEM;
|
||||||
string nixHomeDir = "/nix";
|
|
||||||
string nixHomeDirEnvVar = "NIX";
|
|
||||||
|
|
||||||
|
|
||||||
|
SysError::SysError(string msg)
|
||||||
string absPath(string filename, string dir)
|
|
||||||
{
|
{
|
||||||
if (filename[0] != '/') {
|
char * sysMsg = strerror(errno);
|
||||||
|
err = msg + ": " + sysMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
string absPath(string path, string dir)
|
||||||
|
{
|
||||||
|
if (path[0] != '/') {
|
||||||
if (dir == "") {
|
if (dir == "") {
|
||||||
char buf[PATH_MAX];
|
char buf[PATH_MAX];
|
||||||
if (!getcwd(buf, sizeof(buf)))
|
if (!getcwd(buf, sizeof(buf)))
|
||||||
throw Error("cannot get cwd");
|
throw SysError("cannot get cwd");
|
||||||
dir = buf;
|
dir = buf;
|
||||||
}
|
}
|
||||||
filename = dir + "/" + filename;
|
path = dir + "/" + path;
|
||||||
/* !!! canonicalise */
|
/* !!! canonicalise */
|
||||||
char resolved[PATH_MAX];
|
char resolved[PATH_MAX];
|
||||||
if (!realpath(filename.c_str(), resolved))
|
if (!realpath(path.c_str(), resolved))
|
||||||
throw Error("cannot canonicalise path " + filename);
|
throw SysError("cannot canonicalise path " + path);
|
||||||
filename = resolved;
|
path = resolved;
|
||||||
}
|
}
|
||||||
return filename;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Return the directory part of the given path, i.e., everything
|
string dirOf(string path)
|
||||||
before the final `/'. */
|
|
||||||
string dirOf(string s)
|
|
||||||
{
|
{
|
||||||
unsigned int pos = s.rfind('/');
|
unsigned int pos = path.rfind('/');
|
||||||
if (pos == string::npos) throw Error("invalid file name");
|
if (pos == string::npos) throw Error("invalid file name: " + path);
|
||||||
return string(s, 0, pos);
|
return string(path, 0, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Return the base name of the given path, i.e., everything following
|
string baseNameOf(string path)
|
||||||
the final `/'. */
|
|
||||||
string baseNameOf(string s)
|
|
||||||
{
|
{
|
||||||
unsigned int pos = s.rfind('/');
|
unsigned int pos = path.rfind('/');
|
||||||
if (pos == string::npos) throw Error("invalid file name");
|
if (pos == string::npos) throw Error("invalid file name: " + path);
|
||||||
return string(s, pos + 1);
|
return string(path, pos + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void debug(string s)
|
||||||
|
{
|
||||||
|
cerr << "debug: " << s << endl;
|
||||||
}
|
}
|
||||||
|
|
29
src/util.hh
29
src/util.hh
|
@ -12,13 +12,21 @@ using namespace std;
|
||||||
|
|
||||||
class Error : public exception
|
class Error : public exception
|
||||||
{
|
{
|
||||||
|
protected:
|
||||||
string err;
|
string err;
|
||||||
public:
|
public:
|
||||||
|
Error() { }
|
||||||
Error(string _err) { err = _err; }
|
Error(string _err) { err = _err; }
|
||||||
~Error() throw () { };
|
~Error() throw () { }
|
||||||
const char * what() const throw () { return err.c_str(); }
|
const char * what() const throw () { return err.c_str(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class SysError : public Error
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SysError(string msg);
|
||||||
|
};
|
||||||
|
|
||||||
class UsageError : public Error
|
class UsageError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -33,15 +41,20 @@ typedef vector<string> Strings;
|
||||||
extern string thisSystem;
|
extern string thisSystem;
|
||||||
|
|
||||||
|
|
||||||
/* The prefix of the Nix installation, and the environment variable
|
/* Return an absolutized path, resolving paths relative to the
|
||||||
that can be used to override the default. */
|
specified directory, or the current directory otherwise. */
|
||||||
extern string nixHomeDir;
|
string absPath(string path, string dir = "");
|
||||||
extern string nixHomeDirEnvVar;
|
|
||||||
|
/* Return the directory part of the given path, i.e., everything
|
||||||
|
before the final `/'. */
|
||||||
|
string dirOf(string path);
|
||||||
|
|
||||||
|
/* Return the base name of the given path, i.e., everything following
|
||||||
|
the final `/'. */
|
||||||
|
string baseNameOf(string path);
|
||||||
|
|
||||||
|
|
||||||
string absPath(string filename, string dir = "");
|
void debug(string s);
|
||||||
string dirOf(string s);
|
|
||||||
string baseNameOf(string s);
|
|
||||||
|
|
||||||
|
|
||||||
#endif /* !__UTIL_H */
|
#endif /* !__UTIL_H */
|
||||||
|
|
100
src/values.cc
Normal file
100
src/values.cc
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
#include "values.hh"
|
||||||
|
#include "globals.hh"
|
||||||
|
#include "db.hh"
|
||||||
|
|
||||||
|
|
||||||
|
static void copyFile(string src, string dst)
|
||||||
|
{
|
||||||
|
int res = system(("cat " + src + " > " + dst).c_str()); /* !!! escape */
|
||||||
|
if (WEXITSTATUS(res) != 0)
|
||||||
|
throw Error("cannot copy " + src + " to " + dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static string absValuePath(string s)
|
||||||
|
{
|
||||||
|
return nixValues + "/" + s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Hash addValue(string path)
|
||||||
|
{
|
||||||
|
Hash hash = hashFile(path);
|
||||||
|
|
||||||
|
string name;
|
||||||
|
if (queryDB(nixDB, dbRefs, hash, name)) {
|
||||||
|
debug((string) hash + " already known");
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
string baseName = baseNameOf(path);
|
||||||
|
|
||||||
|
string targetName = (string) hash + "-" + baseName;
|
||||||
|
|
||||||
|
copyFile(path, absValuePath(targetName));
|
||||||
|
|
||||||
|
setDB(nixDB, dbRefs, hash, targetName);
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
/* Download object referenced by the given URL into the sources
|
||||||
|
directory. Return the file name it was downloaded to. */
|
||||||
|
string fetchURL(string url)
|
||||||
|
{
|
||||||
|
string filename = baseNameOf(url);
|
||||||
|
string fullname = nixSourcesDir + "/" + filename;
|
||||||
|
struct stat st;
|
||||||
|
if (stat(fullname.c_str(), &st)) {
|
||||||
|
cerr << "fetching " << url << endl;
|
||||||
|
/* !!! quoting */
|
||||||
|
string shellCmd =
|
||||||
|
"cd " + nixSourcesDir + " && wget --quiet -N \"" + url + "\"";
|
||||||
|
int res = system(shellCmd.c_str());
|
||||||
|
if (WEXITSTATUS(res) != 0)
|
||||||
|
throw Error("cannot fetch " + url);
|
||||||
|
}
|
||||||
|
return fullname;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
string queryValuePath(Hash hash)
|
||||||
|
{
|
||||||
|
bool checkedNet = false;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
|
||||||
|
string name, url;
|
||||||
|
|
||||||
|
if (queryDB(nixDB, dbRefs, hash, name)) {
|
||||||
|
string fn = absValuePath(name);
|
||||||
|
|
||||||
|
/* Verify that the file hasn't changed. !!! race */
|
||||||
|
if (hashFile(fn) != hash)
|
||||||
|
throw Error("file " + fn + " is stale");
|
||||||
|
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Error("a file with hash " + (string) hash + " is requested, "
|
||||||
|
"but it is not known to exist locally or on the network");
|
||||||
|
#if 0
|
||||||
|
if (checkedNet)
|
||||||
|
throw Error("consistency problem: file fetched from " + url +
|
||||||
|
" should have hash " + (string) hash + ", but it doesn't");
|
||||||
|
|
||||||
|
if (!queryDB(nixDB, dbNetSources, hash, url))
|
||||||
|
throw Error("a file with hash " + (string) hash + " is requested, "
|
||||||
|
"but it is not known to exist locally or on the network");
|
||||||
|
|
||||||
|
checkedNet = true;
|
||||||
|
|
||||||
|
fn = fetchURL(url);
|
||||||
|
|
||||||
|
setDB(nixDB, dbRefs, hash, fn);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
24
src/values.hh
Normal file
24
src/values.hh
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
#ifndef __VALUES_H
|
||||||
|
#define __VALUES_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "hash.hh"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
|
||||||
|
/* Copy a value to the nixValues directory and register it in dbRefs.
|
||||||
|
Return the hash code of the value. */
|
||||||
|
Hash addValue(string pathName);
|
||||||
|
|
||||||
|
|
||||||
|
/* Obtain the path of a value with the given hash. If a file with
|
||||||
|
that hash is known to exist in the local file system (as indicated
|
||||||
|
by the dbRefs database), we use that. Otherwise, we attempt to
|
||||||
|
fetch it from the network (using dbNetSources). We verify that the
|
||||||
|
file has the right hash. */
|
||||||
|
string queryValuePath(Hash hash);
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* !__VALUES_H */
|
Loading…
Reference in a new issue