daemon: Read unsigned nar size and download size from substituter.
[jackhill/guix/guix.git] / nix / libstore / local-store.cc
index 882bce1..d52b102 100644 (file)
@@ -21,6 +21,7 @@
 #include <stdio.h>
 #include <time.h>
 #include <grp.h>
+#include <ctype.h>
 
 #if HAVE_UNSHARE && HAVE_STATVFS && HAVE_SYS_MOUNT_H
 #include <sched.h>
 #include <sys/mount.h>
 #endif
 
-#if HAVE_LINUX_FS_H
-#include <linux/fs.h>
 #include <sys/ioctl.h>
 #include <errno.h>
-#endif
 
 #include <sqlite3.h>
 
@@ -51,7 +49,7 @@ void checkStoreNotSymlink()
         if (S_ISLNK(st.st_mode))
             throw Error(format(
                 "the path `%1%' is a symlink; "
-                "this is not allowed for the Nix store and its parent directories")
+                "this is not allowed for the store and its parent directories")
                 % path);
         path = dirOf(path);
     }
@@ -59,7 +57,6 @@ void checkStoreNotSymlink()
 
 
 LocalStore::LocalStore(bool reserveSpace)
-    : didSetSubstituterEnv(false)
 {
     schemaPath = settings.nixDBPath + "/schema";
 
@@ -88,8 +85,9 @@ LocalStore::LocalStore(bool reserveSpace)
 
         Path perUserDir = profilesDir + "/per-user";
         createDirs(perUserDir);
-        if (chmod(perUserDir.c_str(), 01777) == -1)
-            throw SysError(format("could not set permissions on '%1%' to 1777") % perUserDir);
+        if (chmod(perUserDir.c_str(), 0755) == -1)
+            throw SysError(format("could not set permissions on '%1%' to 755")
+                           % perUserDir);
 
         mode_t perm = 01775;
 
@@ -153,7 +151,7 @@ LocalStore::LocalStore(bool reserveSpace)
     }
 
     if (!lockFile(globalLock, ltRead, false)) {
-        printMsg(lvlError, "waiting for the big Nix store lock...");
+        printMsg(lvlError, "waiting for the big store lock...");
         lockFile(globalLock, ltRead, true);
     }
 
@@ -161,7 +159,7 @@ LocalStore::LocalStore(bool reserveSpace)
        upgrade.  */
     int curSchema = getSchema();
     if (curSchema > nixSchemaVersion)
-        throw Error(format("current Nix store schema is version %1%, but I only support %2%")
+        throw Error(format("current store schema is version %1%, but I only support %2%")
             % curSchema % nixSchemaVersion);
 
     else if (curSchema == 0) { /* new store */
@@ -171,27 +169,10 @@ LocalStore::LocalStore(bool reserveSpace)
     }
 
     else if (curSchema < nixSchemaVersion) {
-        if (curSchema < 5)
-            throw Error(
-                "Your Nix store has a database in Berkeley DB format,\n"
-                "which is no longer supported. To convert to the new format,\n"
-                "please upgrade Nix to version 0.12 first.");
-
-        if (!lockFile(globalLock, ltWrite, false)) {
-            printMsg(lvlError, "waiting for exclusive access to the Nix store...");
-            lockFile(globalLock, ltWrite, true);
-        }
-
-        /* Get the schema version again, because another process may
-           have performed the upgrade already. */
-        curSchema = getSchema();
-
-        if (curSchema < 6) upgradeStore6();
-        else if (curSchema < 7) { upgradeStore7(); openDB(true); }
-
-        writeFile(schemaPath, (format("%1%") % nixSchemaVersion).str());
-
-        lockFile(globalLock, ltRead, true);
+       /* Guix always used version 7 of the schema.  */
+       throw Error(
+           format("Your store database uses an implausibly old schema, version %1%.")
+           % curSchema);
     }
 
     else openDB(false);
@@ -200,19 +181,6 @@ LocalStore::LocalStore(bool reserveSpace)
 
 LocalStore::~LocalStore()
 {
-    try {
-        foreach (RunningSubstituters::iterator, i, runningSubstituters) {
-            if (i->second.disabled) continue;
-            i->second.to.close();
-            i->second.from.close();
-            i->second.error.close();
-            if (i->second.pid != -1)
-                i->second.pid.wait(true);
-        }
-    } catch (...) {
-        ignoreException();
-    }
-
     try {
         if (fdTempRoots != -1) {
             fdTempRoots.close();
@@ -239,13 +207,13 @@ int LocalStore::getSchema()
 void LocalStore::openDB(bool create)
 {
     if (access(settings.nixDBPath.c_str(), R_OK | W_OK))
-        throw SysError(format("Nix database directory `%1%' is not writable") % settings.nixDBPath);
+        throw SysError(format("store database directory `%1%' is not writable") % settings.nixDBPath);
 
-    /* Open the Nix database. */
+    /* Open the store database. */
     string dbPath = settings.nixDBPath + "/db.sqlite";
     if (sqlite3_open_v2(dbPath.c_str(), &db.db,
             SQLITE_OPEN_READWRITE | (create ? SQLITE_OPEN_CREATE : 0), 0) != SQLITE_OK)
-        throw Error(format("cannot open Nix database `%1%'") % dbPath);
+        throw Error(format("cannot open store database `%1%'") % dbPath);
 
     if (sqlite3_busy_timeout(db, 60 * 60 * 1000) != SQLITE_OK)
         throwSQLiteError(db, "setting timeout");
@@ -333,8 +301,8 @@ void LocalStore::openDB(bool create)
 }
 
 
-/* To improve purity, users may want to make the Nix store a read-only
-   bind mount.  So make the Nix store writable for this process. */
+/* To improve purity, users may want to make the store a read-only
+   bind mount.  So make the store writable for this process. */
 void LocalStore::makeStoreWritable()
 {
 #if HAVE_UNSHARE && HAVE_STATVFS && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_REMOUNT)
@@ -342,7 +310,7 @@ void LocalStore::makeStoreWritable()
     /* Check if /nix/store is on a read-only mount. */
     struct statvfs stat;
     if (statvfs(settings.nixStore.c_str(), &stat) != 0)
-        throw SysError("getting info about the Nix store mount point");
+        throw SysError("getting info about the store mount point");
 
     if (stat.f_flag & ST_RDONLY) {
         if (unshare(CLONE_NEWNS) == -1)
@@ -437,8 +405,8 @@ static void canonicalisePathMetaData_(const Path & path, uid_t fromUid, InodesSe
        lchown if available, otherwise don't bother.  Wrong ownership
        of a symlink doesn't matter, since the owning user can't change
        the symlink and can't delete it because the directory is not
-       writable.  The only exception is top-level paths in the Nix
-       store (since that directory is group-writable for the Nix build
+       writable.  The only exception is top-level paths in the
+       store (since that directory is group-writable for the build
        users group); we check for this case below. */
     if (st.st_uid != geteuid()) {
 #if HAVE_LCHOWN
@@ -812,129 +780,62 @@ Path LocalStore::queryPathFromHashPart(const string & hashPart)
     });
 }
 
-
-void LocalStore::setSubstituterEnv()
-{
-    if (didSetSubstituterEnv) return;
-
-    /* Pass configuration options (including those overridden with
-       --option) to substituters. */
-    setenv("_NIX_OPTIONS", settings.pack().c_str(), 1);
-
-    didSetSubstituterEnv = true;
-}
-
-
-void LocalStore::startSubstituter(const Path & substituter, RunningSubstituter & run)
-{
-    if (run.disabled || run.pid != -1) return;
-
-    debug(format("starting substituter program `%1%'") % substituter);
-
-    Pipe toPipe, fromPipe, errorPipe;
-
-    toPipe.create();
-    fromPipe.create();
-    errorPipe.create();
-
-    setSubstituterEnv();
-
-    run.pid = startProcess([&]() {
-        if (dup2(toPipe.readSide, STDIN_FILENO) == -1)
-            throw SysError("dupping stdin");
-        if (dup2(fromPipe.writeSide, STDOUT_FILENO) == -1)
-            throw SysError("dupping stdout");
-        if (dup2(errorPipe.writeSide, STDERR_FILENO) == -1)
-            throw SysError("dupping stderr");
-        execl(substituter.c_str(), substituter.c_str(), "--query", NULL);
-        throw SysError(format("executing `%1%'") % substituter);
-    });
-
-    run.program = baseNameOf(substituter);
-    run.to = toPipe.writeSide.borrow();
-    run.from = run.fromBuf.fd = fromPipe.readSide.borrow();
-    run.error = errorPipe.readSide.borrow();
-
-    toPipe.readSide.close();
-    fromPipe.writeSide.close();
-    errorPipe.writeSide.close();
-
-    /* The substituter may exit right away if it's disabled in any way
-       (e.g. copy-from-other-stores.pl will exit if no other stores
-       are configured). */
-    try {
-        getLineFromSubstituter(run);
-    } catch (EndOfFile & e) {
-        run.to.close();
-        run.from.close();
-        run.error.close();
-        run.disabled = true;
-        if (run.pid.wait(true) != 0) throw;
-    }
-}
-
-
-/* Read a line from the substituter's stdout, while also processing
-   its stderr. */
-string LocalStore::getLineFromSubstituter(RunningSubstituter & run)
+/* Read a line from the substituter's reply file descriptor, while also
+   processing its stderr. */
+string LocalStore::getLineFromSubstituter(Agent & run)
 {
     string res, err;
 
-    /* We might have stdout data left over from the last time. */
-    if (run.fromBuf.hasData()) goto haveData;
-
     while (1) {
         checkInterrupt();
 
         fd_set fds;
         FD_ZERO(&fds);
-        FD_SET(run.from, &fds);
-        FD_SET(run.error, &fds);
+        FD_SET(run.fromAgent.readSide, &fds);
+        FD_SET(run.builderOut.readSide, &fds);
 
         /* Wait for data to appear on the substituter's stdout or
            stderr. */
-        if (select(run.from > run.error ? run.from + 1 : run.error + 1, &fds, 0, 0, 0) == -1) {
+        if (select(std::max(run.fromAgent.readSide, run.builderOut.readSide) + 1, &fds, 0, 0, 0) == -1) {
             if (errno == EINTR) continue;
             throw SysError("waiting for input from the substituter");
         }
 
         /* Completely drain stderr before dealing with stdout. */
-        if (FD_ISSET(run.error, &fds)) {
+        if (FD_ISSET(run.fromAgent.readSide, &fds)) {
             char buf[4096];
-            ssize_t n = read(run.error, (unsigned char *) buf, sizeof(buf));
+            ssize_t n = read(run.fromAgent.readSide, (unsigned char *) buf, sizeof(buf));
             if (n == -1) {
                 if (errno == EINTR) continue;
                 throw SysError("reading from substituter's stderr");
             }
-            if (n == 0) throw EndOfFile(format("substituter `%1%' died unexpectedly") % run.program);
+            if (n == 0) throw EndOfFile(format("`%1% substitute' died unexpectedly")
+                                       % settings.guixProgram);
             err.append(buf, n);
             string::size_type p;
             while (((p = err.find('\n')) != string::npos)
                   || ((p = err.find('\r')) != string::npos)) {
                string thing(err, 0, p + 1);
-               writeToStderr(run.program + ": " + thing);
+               writeToStderr("substitute: " + thing);
                 err = string(err, p + 1);
             }
         }
 
         /* Read from stdout until we get a newline or the buffer is empty. */
-        else if (run.fromBuf.hasData() || FD_ISSET(run.from, &fds)) {
-        haveData:
-            do {
-                unsigned char c;
-                run.fromBuf(&c, 1);
-                if (c == '\n') {
-                    if (!err.empty()) printMsg(lvlError, run.program + ": " + err);
-                    return res;
-                }
-                res += c;
-            } while (run.fromBuf.hasData());
+        else if (FD_ISSET(run.builderOut.readSide, &fds)) {
+           unsigned char c;
+           readFull(run.builderOut.readSide, (unsigned char *) &c, 1);
+           if (c == '\n') {
+               if (!err.empty()) printMsg(lvlError, "substitute: " + err);
+               return res;
+           }
+           res += c;
         }
     }
 }
 
 
-template<class T> T LocalStore::getIntLineFromSubstituter(RunningSubstituter & run)
+template<class T> T LocalStore::getIntLineFromSubstituter(Agent & run)
 {
     string s = getLineFromSubstituter(run);
     T res;
@@ -947,44 +848,49 @@ PathSet LocalStore::querySubstitutablePaths(const PathSet & paths)
 {
     PathSet res;
 
-    if (!settings.useSubstitutes) return res;
-
-    foreach (Paths::iterator, i, settings.substituters) {
-        if (res.size() == paths.size()) break;
-        RunningSubstituter & run(runningSubstituters[*i]);
-        startSubstituter(*i, run);
-        if (run.disabled) continue;
-        string s = "have ";
-        foreach (PathSet::const_iterator, j, paths)
-            if (res.find(*j) == res.end()) { s += *j; s += " "; }
-        writeLine(run.to, s);
-        while (true) {
-            /* FIXME: we only read stderr when an error occurs, so
-               substituters should only write (short) messages to
-               stderr when they fail.  I.e. they shouldn't write debug
-               output. */
-            Path path = getLineFromSubstituter(run);
-            if (path == "") break;
-            res.insert(path);
-        }
+    if (!settings.useSubstitutes || paths.empty()) return res;
+
+    Agent & run = *substituter();
+
+    string s = "have ";
+    foreach (PathSet::const_iterator, j, paths)
+       if (res.find(*j) == res.end()) { s += *j; s += " "; }
+    writeLine(run.toAgent.writeSide, s);
+    while (true) {
+       /* FIXME: we only read stderr when an error occurs, so
+          substituters should only write (short) messages to
+          stderr when they fail.  I.e. they shouldn't write debug
+          output. */
+       Path path = getLineFromSubstituter(run);
+       if (path == "") break;
+       res.insert(path);
     }
+
     return res;
 }
 
 
-void LocalStore::querySubstitutablePathInfos(const Path & substituter,
-    PathSet & paths, SubstitutablePathInfos & infos)
+std::shared_ptr<Agent> LocalStore::substituter()
+{
+    if (!runningSubstituter) {
+       const Strings args = { "substitute", "--query" };
+       const std::map<string, string> env = { { "_NIX_OPTIONS", settings.pack() } };
+       runningSubstituter = std::make_shared<Agent>(settings.guixProgram, args, env);
+    }
+
+    return runningSubstituter;
+}
+
+void LocalStore::querySubstitutablePathInfos(PathSet & paths, SubstitutablePathInfos & infos)
 {
     if (!settings.useSubstitutes) return;
 
-    RunningSubstituter & run(runningSubstituters[substituter]);
-    startSubstituter(substituter, run);
-    if (run.disabled) return;
+    Agent & run = *substituter();
 
     string s = "info ";
     foreach (PathSet::const_iterator, i, paths)
         if (infos.find(*i) == infos.end()) { s += *i; s += " "; }
-    writeLine(run.to, s);
+    writeLine(run.toAgent.writeSide, s);
 
     while (true) {
         Path path = getLineFromSubstituter(run);
@@ -1001,8 +907,8 @@ void LocalStore::querySubstitutablePathInfos(const Path & substituter,
             assertStorePath(p);
             info.references.insert(p);
         }
-        info.downloadSize = getIntLineFromSubstituter<long long>(run);
-        info.narSize = getIntLineFromSubstituter<long long>(run);
+        info.downloadSize = getIntLineFromSubstituter<unsigned long long>(run);
+        info.narSize = getIntLineFromSubstituter<unsigned long long>(run);
     }
 }
 
@@ -1010,10 +916,9 @@ void LocalStore::querySubstitutablePathInfos(const Path & substituter,
 void LocalStore::querySubstitutablePathInfos(const PathSet & paths,
     SubstitutablePathInfos & infos)
 {
-    PathSet todo = paths;
-    foreach (Paths::iterator, i, settings.substituters) {
-        if (todo.empty()) break;
-        querySubstitutablePathInfos(*i, todo, infos);
+    if (!paths.empty()) {
+       PathSet todo = paths;
+       querySubstitutablePathInfos(todo, infos);
     }
 }
 
@@ -1239,6 +1144,93 @@ static void checkSecrecy(const Path & path)
 }
 
 
+/* Return the authentication agent, a "guix authenticate" process started
+   lazily.  */
+static std::shared_ptr<Agent> authenticationAgent()
+{
+    static std::shared_ptr<Agent> agent;
+
+    if (!agent) {
+       Strings args = { "authenticate" };
+       agent = std::make_shared<Agent>(settings.guixProgram, args);
+    }
+
+    return agent;
+}
+
+/* Read an integer and the byte that immediately follows it from FD.  Return
+   the integer.  */
+static int readInteger(int fd)
+{
+    string str;
+
+    while (1) {
+        char ch;
+        ssize_t rd = read(fd, &ch, 1);
+        if (rd == -1) {
+            if (errno != EINTR)
+                throw SysError("reading an integer");
+        } else if (rd == 0)
+            throw EndOfFile("unexpected EOF reading an integer");
+        else {
+           if (isdigit(ch)) {
+               str += ch;
+           } else {
+               break;
+           }
+        }
+    }
+
+    return stoi(str);
+}
+
+/* Read from FD a reply coming from 'guix authenticate'.  The reply has the
+   form "CODE LEN:STR".  CODE is an integer, where zero indicates success.
+   LEN specifies the length in bytes of the string that immediately
+   follows.  */
+static std::string readAuthenticateReply(int fd)
+{
+    int code = readInteger(fd);
+    int len = readInteger(fd);
+
+    string str;
+    str.resize(len);
+    readFull(fd, (unsigned char *) &str[0], len);
+
+    if (code == 0)
+       return str;
+    else
+       throw Error(str);
+}
+
+/* Sign HASH with the key stored in file SECRETKEY.  Return the signature as a
+   string, or raise an exception upon error.  */
+static std::string signHash(const string &secretKey, const Hash &hash)
+{
+    auto agent = authenticationAgent();
+    auto hexHash = printHash(hash);
+
+    writeLine(agent->toAgent.writeSide,
+             (format("sign %1%:%2% %3%:%4%")
+              % secretKey.size() % secretKey
+              % hexHash.size() % hexHash).str());
+
+    return readAuthenticateReply(agent->fromAgent.readSide);
+}
+
+/* Verify SIGNATURE and return the base16-encoded hash over which it was
+   computed.  */
+static std::string verifySignature(const string &signature)
+{
+    auto agent = authenticationAgent();
+
+    writeLine(agent->toAgent.writeSide,
+             (format("verify %1%:%2%")
+              % signature.size() % signature).str());
+
+    return readAuthenticateReply(agent->fromAgent.readSide);
+}
+
 void LocalStore::exportPath(const Path & path, bool sign,
     Sink & sink)
 {
@@ -1278,22 +1270,10 @@ void LocalStore::exportPath(const Path & path, bool sign,
 
         writeInt(1, hashAndWriteSink);
 
-        Path tmpDir = createTempDir();
-        AutoDelete delTmp(tmpDir);
-        Path hashFile = tmpDir + "/hash";
-        writeFile(hashFile, printHash(hash));
-
         Path secretKey = settings.nixConfDir + "/signing-key.sec";
         checkSecrecy(secretKey);
 
-        Strings args;
-        args.push_back("rsautl");
-        args.push_back("-sign");
-        args.push_back("-inkey");
-        args.push_back(secretKey);
-        args.push_back("-in");
-        args.push_back(hashFile);
-        string signature = runProgram(OPENSSL_PATH, true, args);
+       string signature = signHash(secretKey, hash);
 
         writeString(signature, hashAndWriteSink);
 
@@ -1351,7 +1331,7 @@ Path LocalStore::importPath(bool requireSignature, Source & source)
 
     unsigned int magic = readInt(hashAndReadSource);
     if (magic != EXPORT_MAGIC)
-        throw Error("Nix archive cannot be imported; wrong format");
+        throw Error("normalized archive cannot be imported; wrong format");
 
     Path dstPath = readStorePath(hashAndReadSource);
 
@@ -1372,18 +1352,7 @@ Path LocalStore::importPath(bool requireSignature, Source & source)
         string signature = readString(hashAndReadSource);
 
         if (requireSignature) {
-            Path sigFile = tmpDir + "/sig";
-            writeFile(sigFile, signature);
-
-            Strings args;
-            args.push_back("rsautl");
-            args.push_back("-verify");
-            args.push_back("-inkey");
-            args.push_back(settings.nixConfDir + "/signing-key.pub");
-            args.push_back("-pubin");
-            args.push_back("-in");
-            args.push_back(sigFile);
-            string hash2 = runProgram(OPENSSL_PATH, true, args);
+           string hash2 = verifySignature(signature);
 
             /* Note: runProgram() throws an exception if the signature
                is invalid. */
@@ -1480,7 +1449,7 @@ void LocalStore::invalidatePathChecked(const Path & path)
 
 bool LocalStore::verifyStore(bool checkContents, bool repair)
 {
-    printMsg(lvlError, format("reading the Nix store..."));
+    printMsg(lvlError, format("reading the store..."));
 
     bool errors = false;
 
@@ -1568,7 +1537,7 @@ void LocalStore::verifyPath(const Path & path, const PathSet & store,
     done.insert(path);
 
     if (!isStorePath(path)) {
-        printMsg(lvlError, format("path `%1%' is not in the Nix store") % path);
+        printMsg(lvlError, format("path `%1%' is not in the store") % path);
         invalidatePath(path);
         return;
     }
@@ -1633,144 +1602,22 @@ void LocalStore::markContentsGood(const Path & path)
 }
 
 
-/* Functions for upgrading from the pre-SQLite database. */
-
-PathSet LocalStore::queryValidPathsOld()
-{
-    PathSet paths;
-    for (auto & i : readDirectory(settings.nixDBPath + "/info"))
-        if (i.name.at(0) != '.') paths.insert(settings.nixStore + "/" + i.name);
-    return paths;
-}
-
-
-ValidPathInfo LocalStore::queryPathInfoOld(const Path & path)
-{
-    ValidPathInfo res;
-    res.path = path;
-
-    /* Read the info file. */
-    string baseName = baseNameOf(path);
-    Path infoFile = (format("%1%/info/%2%") % settings.nixDBPath % baseName).str();
-    if (!pathExists(infoFile))
-        throw Error(format("path `%1%' is not valid") % path);
-    string info = readFile(infoFile);
-
-    /* Parse it. */
-    Strings lines = tokenizeString<Strings>(info, "\n");
-
-    foreach (Strings::iterator, i, lines) {
-        string::size_type p = i->find(':');
-        if (p == string::npos)
-            throw Error(format("corrupt line in `%1%': %2%") % infoFile % *i);
-        string name(*i, 0, p);
-        string value(*i, p + 2);
-        if (name == "References") {
-            Strings refs = tokenizeString<Strings>(value, " ");
-            res.references = PathSet(refs.begin(), refs.end());
-        } else if (name == "Deriver") {
-            res.deriver = value;
-        } else if (name == "Hash") {
-            res.hash = parseHashField(path, value);
-        } else if (name == "Registered-At") {
-            int n = 0;
-            string2Int(value, n);
-            res.registrationTime = n;
-        }
-    }
-
-    return res;
-}
-
-
-/* Upgrade from schema 5 (Nix 0.12) to schema 6 (Nix >= 0.15). */
-void LocalStore::upgradeStore6()
-{
-    printMsg(lvlError, "upgrading Nix store to new schema (this may take a while)...");
-
-    openDB(true);
-
-    PathSet validPaths = queryValidPathsOld();
-
-    SQLiteTxn txn(db);
-
-    foreach (PathSet::iterator, i, validPaths) {
-        addValidPath(queryPathInfoOld(*i), false);
-        std::cerr << ".";
-    }
-
-    std::cerr << "|";
-
-    foreach (PathSet::iterator, i, validPaths) {
-        ValidPathInfo info = queryPathInfoOld(*i);
-        unsigned long long referrer = queryValidPathId(*i);
-        foreach (PathSet::iterator, j, info.references)
-            addReference(referrer, queryValidPathId(*j));
-        std::cerr << ".";
-    }
-
-    std::cerr << "\n";
-
-    txn.commit();
-}
-
-
-#if defined(FS_IOC_SETFLAGS) && defined(FS_IOC_GETFLAGS) && defined(FS_IMMUTABLE_FL)
-
-static void makeMutable(const Path & path)
-{
-    checkInterrupt();
-
-    struct stat st = lstat(path);
-
-    if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode)) return;
-
-    if (S_ISDIR(st.st_mode)) {
-        for (auto & i : readDirectory(path))
-            makeMutable(path + "/" + i.name);
-    }
-
-    /* The O_NOFOLLOW is important to prevent us from changing the
-       mutable bit on the target of a symlink (which would be a
-       security hole). */
-    AutoCloseFD fd = open(path.c_str(), O_RDONLY | O_NOFOLLOW);
-    if (fd == -1) {
-        if (errno == ELOOP) return; // it's a symlink
-        throw SysError(format("opening file `%1%'") % path);
-    }
-
-    unsigned int flags = 0, old;
-
-    /* Silently ignore errors getting/setting the immutable flag so
-       that we work correctly on filesystems that don't support it. */
-    if (ioctl(fd, FS_IOC_GETFLAGS, &flags)) return;
-    old = flags;
-    flags &= ~FS_IMMUTABLE_FL;
-    if (old == flags) return;
-    if (ioctl(fd, FS_IOC_SETFLAGS, &flags)) return;
-}
-
-/* Upgrade from schema 6 (Nix 0.15) to schema 7 (Nix >= 1.3). */
-void LocalStore::upgradeStore7()
+void LocalStore::vacuumDB()
 {
-    if (getuid() != 0) return;
-    printMsg(lvlError, "removing immutable bits from the Nix store (this may take a while)...");
-    makeMutable(settings.nixStore);
+    if (sqlite3_exec(db, "vacuum;", 0, 0, 0) != SQLITE_OK)
+        throwSQLiteError(db, "vacuuming SQLite database");
 }
 
-#else
 
-void LocalStore::upgradeStore7()
+void LocalStore::createUser(const std::string & userName, uid_t userId)
 {
-}
-
-#endif
-
+    auto dir = settings.nixStateDir + "/profiles/per-user/" + userName;
 
-void LocalStore::vacuumDB()
-{
-    if (sqlite3_exec(db, "vacuum;", 0, 0, 0) != SQLITE_OK)
-        throwSQLiteError(db, "vacuuming SQLite database");
+    createDirs(dir);
+    if (chmod(dir.c_str(), 0755) == -1)
+       throw SysError(format("changing permissions of directory '%s'") % dir);
+    if (chown(dir.c_str(), userId, -1) == -1)
+       throw SysError(format("changing owner of directory '%s'") % dir);
 }