Support sandbox builds by non-root users
This allows an unprivileged user to perform builds on a diverted store (i.e. where the physical store location differs from the logical location). Example: $ NIX_LOG_DIR=/tmp/log NIX_REMOTE="local?real=/tmp/store&state=/tmp/var" nix-build -E \ 'with import <nixpkgs> {}; runCommand "foo" { buildInputs = [procps nettools]; } "id; ps; ifconfig; echo $out > $out"' will do a build in the Nix store physically in /tmp/store but logically in /nix/store (and thus using substituters for the latter).
This commit is contained in:
parent
2f8b0e557b
commit
5e51ffb1c2
|
@ -436,12 +436,11 @@ private:
|
||||||
AutoCloseFD fdUserLock;
|
AutoCloseFD fdUserLock;
|
||||||
|
|
||||||
string user;
|
string user;
|
||||||
uid_t uid;
|
uid_t uid = 0;
|
||||||
gid_t gid;
|
gid_t gid = 0;
|
||||||
std::vector<gid_t> supplementaryGIDs;
|
std::vector<gid_t> supplementaryGIDs;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
UserLock();
|
|
||||||
~UserLock();
|
~UserLock();
|
||||||
|
|
||||||
void acquire();
|
void acquire();
|
||||||
|
@ -450,8 +449,8 @@ public:
|
||||||
void kill();
|
void kill();
|
||||||
|
|
||||||
string getUser() { return user; }
|
string getUser() { return user; }
|
||||||
uid_t getUID() { return uid; }
|
uid_t getUID() { assert(uid); return uid; }
|
||||||
uid_t getGID() { return gid; }
|
uid_t getGID() { assert(gid); return gid; }
|
||||||
std::vector<gid_t> getSupplementaryGIDs() { return supplementaryGIDs; }
|
std::vector<gid_t> getSupplementaryGIDs() { return supplementaryGIDs; }
|
||||||
|
|
||||||
bool enabled() { return uid != 0; }
|
bool enabled() { return uid != 0; }
|
||||||
|
@ -462,12 +461,6 @@ public:
|
||||||
PathSet UserLock::lockedPaths;
|
PathSet UserLock::lockedPaths;
|
||||||
|
|
||||||
|
|
||||||
UserLock::UserLock()
|
|
||||||
{
|
|
||||||
uid = gid = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
UserLock::~UserLock()
|
UserLock::~UserLock()
|
||||||
{
|
{
|
||||||
release();
|
release();
|
||||||
|
@ -768,6 +761,9 @@ private:
|
||||||
/* Whether this is a fixed-output derivation. */
|
/* Whether this is a fixed-output derivation. */
|
||||||
bool fixedOutput;
|
bool fixedOutput;
|
||||||
|
|
||||||
|
/* Whether to run the build in a private network namespace. */
|
||||||
|
bool privateNetwork = false;
|
||||||
|
|
||||||
typedef void (DerivationGoal::*GoalState)();
|
typedef void (DerivationGoal::*GoalState)();
|
||||||
GoalState state;
|
GoalState state;
|
||||||
|
|
||||||
|
@ -1269,16 +1265,13 @@ void DerivationGoal::tryToBuild()
|
||||||
{
|
{
|
||||||
trace("trying to build");
|
trace("trying to build");
|
||||||
|
|
||||||
if (worker.store.storeDir != worker.store.realStoreDir)
|
|
||||||
throw Error("building with a diverted Nix store is not supported");
|
|
||||||
|
|
||||||
/* Check for the possibility that some other goal in this process
|
/* Check for the possibility that some other goal in this process
|
||||||
has locked the output since we checked in haveDerivation().
|
has locked the output since we checked in haveDerivation().
|
||||||
(It can't happen between here and the lockPaths() call below
|
(It can't happen between here and the lockPaths() call below
|
||||||
because we're not allowing multi-threading.) If so, put this
|
because we're not allowing multi-threading.) If so, put this
|
||||||
goal to sleep until another goal finishes, then try again. */
|
goal to sleep until another goal finishes, then try again. */
|
||||||
for (auto & i : drv->outputs)
|
for (auto & i : drv->outputs)
|
||||||
if (pathIsLockedByMe(i.second.path)) {
|
if (pathIsLockedByMe(worker.store.toRealPath(i.second.path))) {
|
||||||
debug(format("putting derivation ‘%1%’ to sleep because ‘%2%’ is locked by another goal")
|
debug(format("putting derivation ‘%1%’ to sleep because ‘%2%’ is locked by another goal")
|
||||||
% drvPath % i.second.path);
|
% drvPath % i.second.path);
|
||||||
worker.waitForAnyGoal(shared_from_this());
|
worker.waitForAnyGoal(shared_from_this());
|
||||||
|
@ -1290,7 +1283,11 @@ void DerivationGoal::tryToBuild()
|
||||||
can't acquire the lock, then continue; hopefully some other
|
can't acquire the lock, then continue; hopefully some other
|
||||||
goal can start a build, and if not, the main loop will sleep a
|
goal can start a build, and if not, the main loop will sleep a
|
||||||
few seconds and then retry this goal. */
|
few seconds and then retry this goal. */
|
||||||
if (!outputLocks.lockPaths(drv->outputPaths(), "", false)) {
|
PathSet lockFiles;
|
||||||
|
for (auto & outPath : drv->outputPaths())
|
||||||
|
lockFiles.insert(worker.store.toRealPath(outPath));
|
||||||
|
|
||||||
|
if (!outputLocks.lockPaths(lockFiles, "", false)) {
|
||||||
worker.waitForAWhile(shared_from_this());
|
worker.waitForAWhile(shared_from_this());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1320,7 +1317,7 @@ void DerivationGoal::tryToBuild()
|
||||||
Path path = i.second.path;
|
Path path = i.second.path;
|
||||||
if (worker.store.isValidPath(path)) continue;
|
if (worker.store.isValidPath(path)) continue;
|
||||||
debug(format("removing invalid path ‘%1%’") % path);
|
debug(format("removing invalid path ‘%1%’") % path);
|
||||||
deletePath(path);
|
deletePath(worker.store.toRealPath(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Don't do a remote build if the derivation has the attribute
|
/* Don't do a remote build if the derivation has the attribute
|
||||||
|
@ -1445,7 +1442,7 @@ void DerivationGoal::buildDone()
|
||||||
#if HAVE_STATVFS
|
#if HAVE_STATVFS
|
||||||
unsigned long long required = 8ULL * 1024 * 1024; // FIXME: make configurable
|
unsigned long long required = 8ULL * 1024 * 1024; // FIXME: make configurable
|
||||||
struct statvfs st;
|
struct statvfs st;
|
||||||
if (statvfs(worker.store.storeDir.c_str(), &st) == 0 &&
|
if (statvfs(worker.store.realStoreDir.c_str(), &st) == 0 &&
|
||||||
(unsigned long long) st.f_bavail * st.f_bsize < required)
|
(unsigned long long) st.f_bavail * st.f_bsize < required)
|
||||||
diskFull = true;
|
diskFull = true;
|
||||||
if (statvfs(tmpDir.c_str(), &st) == 0 &&
|
if (statvfs(tmpDir.c_str(), &st) == 0 &&
|
||||||
|
@ -1683,6 +1680,9 @@ void DerivationGoal::startBuilder()
|
||||||
useChroot = !fixedOutput && get(drv->env, "__noChroot") != "1";
|
useChroot = !fixedOutput && get(drv->env, "__noChroot") != "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (worker.store.storeDir != worker.store.realStoreDir)
|
||||||
|
useChroot = true;
|
||||||
|
|
||||||
/* Construct the environment passed to the builder. */
|
/* Construct the environment passed to the builder. */
|
||||||
env.clear();
|
env.clear();
|
||||||
|
|
||||||
|
@ -1819,10 +1819,8 @@ void DerivationGoal::startBuilder()
|
||||||
|
|
||||||
/* If `build-users-group' is not empty, then we have to build as
|
/* If `build-users-group' is not empty, then we have to build as
|
||||||
one of the members of that group. */
|
one of the members of that group. */
|
||||||
if (settings.buildUsersGroup != "") {
|
if (settings.buildUsersGroup != "" && getuid() == 0) {
|
||||||
buildUser.acquire();
|
buildUser.acquire();
|
||||||
assert(buildUser.getUID() != 0);
|
|
||||||
assert(buildUser.getGID() != 0);
|
|
||||||
|
|
||||||
/* Make sure that no other processes are executing under this
|
/* Make sure that no other processes are executing under this
|
||||||
uid. */
|
uid. */
|
||||||
|
@ -1906,7 +1904,7 @@ void DerivationGoal::startBuilder()
|
||||||
environment using bind-mounts. We put it in the Nix store
|
environment using bind-mounts. We put it in the Nix store
|
||||||
to ensure that we can create hard-links to non-directory
|
to ensure that we can create hard-links to non-directory
|
||||||
inputs in the fake Nix store in the chroot (see below). */
|
inputs in the fake Nix store in the chroot (see below). */
|
||||||
chrootRootDir = drvPath + ".chroot";
|
chrootRootDir = worker.store.toRealPath(drvPath) + ".chroot";
|
||||||
deletePath(chrootRootDir);
|
deletePath(chrootRootDir);
|
||||||
|
|
||||||
/* Clean up the chroot directory automatically. */
|
/* Clean up the chroot directory automatically. */
|
||||||
|
@ -1917,7 +1915,7 @@ void DerivationGoal::startBuilder()
|
||||||
if (mkdir(chrootRootDir.c_str(), 0750) == -1)
|
if (mkdir(chrootRootDir.c_str(), 0750) == -1)
|
||||||
throw SysError(format("cannot create ‘%1%’") % chrootRootDir);
|
throw SysError(format("cannot create ‘%1%’") % chrootRootDir);
|
||||||
|
|
||||||
if (chown(chrootRootDir.c_str(), 0, buildUser.getGID()) == -1)
|
if (buildUser.enabled() && chown(chrootRootDir.c_str(), 0, buildUser.getGID()) == -1)
|
||||||
throw SysError(format("cannot change ownership of ‘%1%’") % chrootRootDir);
|
throw SysError(format("cannot change ownership of ‘%1%’") % chrootRootDir);
|
||||||
|
|
||||||
/* Create a writable /tmp in the chroot. Many builders need
|
/* Create a writable /tmp in the chroot. Many builders need
|
||||||
|
@ -1960,18 +1958,19 @@ void DerivationGoal::startBuilder()
|
||||||
createDirs(chrootStoreDir);
|
createDirs(chrootStoreDir);
|
||||||
chmod_(chrootStoreDir, 01775);
|
chmod_(chrootStoreDir, 01775);
|
||||||
|
|
||||||
if (chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1)
|
if (buildUser.enabled() && chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1)
|
||||||
throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
|
throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
|
||||||
|
|
||||||
for (auto & i : inputPaths) {
|
for (auto & i : inputPaths) {
|
||||||
|
Path r = worker.store.toRealPath(i);
|
||||||
struct stat st;
|
struct stat st;
|
||||||
if (lstat(i.c_str(), &st))
|
if (lstat(r.c_str(), &st))
|
||||||
throw SysError(format("getting attributes of path ‘%1%’") % i);
|
throw SysError(format("getting attributes of path ‘%1%’") % i);
|
||||||
if (S_ISDIR(st.st_mode))
|
if (S_ISDIR(st.st_mode))
|
||||||
dirsInChroot[i] = i;
|
dirsInChroot[i] = r;
|
||||||
else {
|
else {
|
||||||
Path p = chrootRootDir + i;
|
Path p = chrootRootDir + i;
|
||||||
if (link(i.c_str(), p.c_str()) == -1) {
|
if (link(r.c_str(), p.c_str()) == -1) {
|
||||||
/* Hard-linking fails if we exceed the maximum
|
/* Hard-linking fails if we exceed the maximum
|
||||||
link count on a file (e.g. 32000 of ext3),
|
link count on a file (e.g. 32000 of ext3),
|
||||||
which is quite possible after a `nix-store
|
which is quite possible after a `nix-store
|
||||||
|
@ -1979,7 +1978,7 @@ void DerivationGoal::startBuilder()
|
||||||
if (errno != EMLINK)
|
if (errno != EMLINK)
|
||||||
throw SysError(format("linking ‘%1%’ to ‘%2%’") % p % i);
|
throw SysError(format("linking ‘%1%’ to ‘%2%’") % p % i);
|
||||||
StringSink sink;
|
StringSink sink;
|
||||||
dumpPath(i, sink);
|
dumpPath(r, sink);
|
||||||
StringSource source(*sink.s);
|
StringSource source(*sink.s);
|
||||||
restorePath(p, source);
|
restorePath(p, source);
|
||||||
}
|
}
|
||||||
|
@ -2112,6 +2111,10 @@ void DerivationGoal::startBuilder()
|
||||||
CLONE_PARENT to ensure that the real builder is parented to
|
CLONE_PARENT to ensure that the real builder is parented to
|
||||||
us.
|
us.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
if (!fixedOutput)
|
||||||
|
privateNetwork = true;
|
||||||
|
|
||||||
ProcessOptions options;
|
ProcessOptions options;
|
||||||
options.allowVfork = false;
|
options.allowVfork = false;
|
||||||
Pid helper = startProcess([&]() {
|
Pid helper = startProcess([&]() {
|
||||||
|
@ -2120,7 +2123,10 @@ void DerivationGoal::startBuilder()
|
||||||
PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
|
PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
|
||||||
if (stack == MAP_FAILED) throw SysError("allocating stack");
|
if (stack == MAP_FAILED) throw SysError("allocating stack");
|
||||||
int flags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD;
|
int flags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD;
|
||||||
if (!fixedOutput) flags |= CLONE_NEWNET;
|
if (getuid() != 0)
|
||||||
|
flags |= CLONE_NEWUSER;
|
||||||
|
if (privateNetwork)
|
||||||
|
flags |= CLONE_NEWNET;
|
||||||
pid_t child = clone(childEntry, stack + stackSize, flags, this);
|
pid_t child = clone(childEntry, stack + stackSize, flags, this);
|
||||||
if (child == -1 && errno == EINVAL)
|
if (child == -1 && errno == EINVAL)
|
||||||
/* Fallback for Linux < 2.13 where CLONE_NEWPID and
|
/* Fallback for Linux < 2.13 where CLONE_NEWPID and
|
||||||
|
@ -2174,6 +2180,8 @@ void DerivationGoal::runChild()
|
||||||
#if __linux__
|
#if __linux__
|
||||||
if (useChroot) {
|
if (useChroot) {
|
||||||
|
|
||||||
|
if (privateNetwork) {
|
||||||
|
|
||||||
/* Initialise the loopback interface. */
|
/* Initialise the loopback interface. */
|
||||||
AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
|
AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
|
||||||
if (fd == -1) throw SysError("cannot open IP socket");
|
if (fd == -1) throw SysError("cannot open IP socket");
|
||||||
|
@ -2185,6 +2193,7 @@ void DerivationGoal::runChild()
|
||||||
throw SysError("cannot set loopback interface flags");
|
throw SysError("cannot set loopback interface flags");
|
||||||
|
|
||||||
fd.close();
|
fd.close();
|
||||||
|
}
|
||||||
|
|
||||||
/* Set the hostname etc. to fixed values. */
|
/* Set the hostname etc. to fixed values. */
|
||||||
char hostname[] = "localhost";
|
char hostname[] = "localhost";
|
||||||
|
@ -2266,7 +2275,7 @@ void DerivationGoal::runChild()
|
||||||
createDirs(dirOf(target));
|
createDirs(dirOf(target));
|
||||||
writeFile(target, "");
|
writeFile(target, "");
|
||||||
}
|
}
|
||||||
if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
|
if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REC, 0) == -1)
|
||||||
throw SysError(format("bind mount from ‘%1%’ to ‘%2%’ failed") % source % target);
|
throw SysError(format("bind mount from ‘%1%’ to ‘%2%’ failed") % source % target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2284,7 +2293,8 @@ void DerivationGoal::runChild()
|
||||||
requires the kernel to be compiled with
|
requires the kernel to be compiled with
|
||||||
CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case
|
CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case
|
||||||
if /dev/ptx/ptmx exists). */
|
if /dev/ptx/ptmx exists). */
|
||||||
if (pathExists("/dev/pts/ptmx") &&
|
if (getuid() == 0 &&
|
||||||
|
pathExists("/dev/pts/ptmx") &&
|
||||||
!pathExists(chrootRootDir + "/dev/ptmx")
|
!pathExists(chrootRootDir + "/dev/ptmx")
|
||||||
&& dirsInChroot.find("/dev/pts") == dirsInChroot.end())
|
&& dirsInChroot.find("/dev/pts") == dirsInChroot.end())
|
||||||
{
|
{
|
||||||
|
@ -2587,10 +2597,10 @@ void DerivationGoal::registerOutputs()
|
||||||
if (buildMode == bmRepair)
|
if (buildMode == bmRepair)
|
||||||
replaceValidPath(path, actualPath);
|
replaceValidPath(path, actualPath);
|
||||||
else
|
else
|
||||||
if (buildMode != bmCheck && rename(actualPath.c_str(), path.c_str()) == -1)
|
if (buildMode != bmCheck && rename(actualPath.c_str(), worker.store.toRealPath(path).c_str()) == -1)
|
||||||
throw SysError(format("moving build output ‘%1%’ from the sandbox to the Nix store") % path);
|
throw SysError(format("moving build output ‘%1%’ from the sandbox to the Nix store") % path);
|
||||||
}
|
}
|
||||||
if (buildMode != bmCheck) actualPath = path;
|
if (buildMode != bmCheck) actualPath = worker.store.toRealPath(path);
|
||||||
} else {
|
} else {
|
||||||
Path redirected = redirectedOutputs[path];
|
Path redirected = redirectedOutputs[path];
|
||||||
if (buildMode == bmRepair
|
if (buildMode == bmRepair
|
||||||
|
@ -2641,8 +2651,6 @@ void DerivationGoal::registerOutputs()
|
||||||
rewritten = true;
|
rewritten = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Activity act(*logger, lvlTalkative, format("scanning for references inside ‘%1%’") % path);
|
|
||||||
|
|
||||||
/* Check that fixed-output derivations produced the right
|
/* Check that fixed-output derivations produced the right
|
||||||
outputs (i.e., the content hash should match the specified
|
outputs (i.e., the content hash should match the specified
|
||||||
hash). */
|
hash). */
|
||||||
|
@ -2668,13 +2676,15 @@ void DerivationGoal::registerOutputs()
|
||||||
% dest % printHashType(ht) % printHash16or32(h2));
|
% dest % printHashType(ht) % printHash16or32(h2));
|
||||||
if (worker.store.isValidPath(dest))
|
if (worker.store.isValidPath(dest))
|
||||||
return;
|
return;
|
||||||
if (actualPath != dest) {
|
Path actualDest = worker.store.toRealPath(dest);
|
||||||
PathLocks outputLocks({dest});
|
if (actualPath != actualDest) {
|
||||||
deletePath(dest);
|
PathLocks outputLocks({actualDest});
|
||||||
if (rename(actualPath.c_str(), dest.c_str()) == -1)
|
deletePath(actualDest);
|
||||||
|
if (rename(actualPath.c_str(), actualDest.c_str()) == -1)
|
||||||
throw SysError(format("moving ‘%1%’ to ‘%2%’") % actualPath % dest);
|
throw SysError(format("moving ‘%1%’ to ‘%2%’") % actualPath % dest);
|
||||||
}
|
}
|
||||||
path = actualPath = dest;
|
path = dest;
|
||||||
|
actualPath = actualDest;
|
||||||
} else {
|
} else {
|
||||||
if (h != h2)
|
if (h != h2)
|
||||||
throw BuildError(
|
throw BuildError(
|
||||||
|
@ -2692,6 +2702,7 @@ void DerivationGoal::registerOutputs()
|
||||||
contained in it. Compute the SHA-256 NAR hash at the same
|
contained in it. Compute the SHA-256 NAR hash at the same
|
||||||
time. The hash is stored in the database so that we can
|
time. The hash is stored in the database so that we can
|
||||||
verify later on whether nobody has messed with the store. */
|
verify later on whether nobody has messed with the store. */
|
||||||
|
Activity act(*logger, lvlTalkative, format("scanning for references inside ‘%1%’") % path);
|
||||||
HashResult hash;
|
HashResult hash;
|
||||||
PathSet references = scanForReferences(actualPath, allPaths, hash);
|
PathSet references = scanForReferences(actualPath, allPaths, hash);
|
||||||
|
|
||||||
|
@ -2700,7 +2711,7 @@ void DerivationGoal::registerOutputs()
|
||||||
auto info = *worker.store.queryPathInfo(path);
|
auto info = *worker.store.queryPathInfo(path);
|
||||||
if (hash.first != info.narHash) {
|
if (hash.first != info.narHash) {
|
||||||
if (settings.keepFailed) {
|
if (settings.keepFailed) {
|
||||||
Path dst = path + checkSuffix;
|
Path dst = worker.store.toRealPath(path + checkSuffix);
|
||||||
deletePath(dst);
|
deletePath(dst);
|
||||||
if (rename(actualPath.c_str(), dst.c_str()))
|
if (rename(actualPath.c_str(), dst.c_str()))
|
||||||
throw SysError(format("renaming ‘%1%’ to ‘%2%’") % actualPath % dst);
|
throw SysError(format("renaming ‘%1%’ to ‘%2%’") % actualPath % dst);
|
||||||
|
@ -2743,7 +2754,7 @@ void DerivationGoal::registerOutputs()
|
||||||
/* Our requisites are the union of the closures of our references. */
|
/* Our requisites are the union of the closures of our references. */
|
||||||
for (auto & i : references)
|
for (auto & i : references)
|
||||||
/* Don't call computeFSClosure on ourselves. */
|
/* Don't call computeFSClosure on ourselves. */
|
||||||
if (actualPath != i)
|
if (path != i)
|
||||||
worker.store.computeFSClosure(i, used);
|
worker.store.computeFSClosure(i, used);
|
||||||
} else
|
} else
|
||||||
used = references;
|
used = references;
|
||||||
|
@ -2775,8 +2786,7 @@ void DerivationGoal::registerOutputs()
|
||||||
checkRefs("disallowedRequisites", false, true);
|
checkRefs("disallowedRequisites", false, true);
|
||||||
|
|
||||||
if (curRound == nrRounds) {
|
if (curRound == nrRounds) {
|
||||||
worker.store.optimisePath(path); // FIXME: combine with scanForReferences()
|
worker.store.optimisePath(actualPath); // FIXME: combine with scanForReferences()
|
||||||
|
|
||||||
worker.markContentsGood(path);
|
worker.markContentsGood(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ public:
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// FIXME: not thread-safe!
|
||||||
bool pathIsLockedByMe(const Path & path);
|
bool pathIsLockedByMe(const Path & path);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -503,6 +503,11 @@ public:
|
||||||
const Path & gcRoot, bool indirect, bool allowOutsideRootsDir = false);
|
const Path & gcRoot, bool indirect, bool allowOutsideRootsDir = false);
|
||||||
|
|
||||||
virtual Path getRealStoreDir() { return storeDir; }
|
virtual Path getRealStoreDir() { return storeDir; }
|
||||||
|
|
||||||
|
Path toRealPath(const Path & storePath)
|
||||||
|
{
|
||||||
|
return getRealStoreDir() + "/" + baseNameOf(storePath);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue