1#include "crypto.hh"
2#include "globals.hh"
3#include "store-api.hh"
4#include "util.hh"
5#include "nar-info-disk-cache.hh"
6#include "thread-pool.hh"
7#include "json.hh"
8#include "derivations.hh"
9
10#include <future>
11
12
13namespace nix {
14
15
16bool Store::isInStore(const Path & path) const
17{
18    return isInDir(path, storeDir);
19}
20
21
22bool Store::isStorePath(const Path & path) const
23{
24    return isInStore(path)
25        && path.size() >= storeDir.size() + 1 + storePathHashLen
26        && path.find('/', storeDir.size() + 1) == Path::npos;
27}
28
29
30void Store::assertStorePath(const Path & path) const
31{
32    if (!isStorePath(path))
33        throw Error(format("path ‘%1%’ is not in the Nix store") % path);
34}
35
36
37Path Store::toStorePath(const Path & path) const
38{
39    if (!isInStore(path))
40        throw Error(format("path ‘%1%’ is not in the Nix store") % path);
41    Path::size_type slash = path.find('/', storeDir.size() + 1);
42    if (slash == Path::npos)
43        return path;
44    else
45        return Path(path, 0, slash);
46}
47
48
49Path Store::followLinksToStore(const Path & _path) const
50{
51    Path path = absPath(_path);
52    while (!isInStore(path)) {
53        if (!isLink(path)) break;
54        string target = readLink(path);
55        path = absPath(target, dirOf(path));
56    }
57    if (!isInStore(path))
58        throw Error(format("path ‘%1%’ is not in the Nix store") % path);
59    return path;
60}
61
62
63Path Store::followLinksToStorePath(const Path & path) const
64{
65    return toStorePath(followLinksToStore(path));
66}
67
68
69string storePathToName(const Path & path)
70{
71    auto base = baseNameOf(path);
72    assert(base.size() == storePathHashLen || (base.size() > storePathHashLen && base[storePathHashLen] == '-'));
73    return base.size() == storePathHashLen ? "" : string(base, storePathHashLen + 1);
74}
75
76
77string storePathToHash(const Path & path)
78{
79    auto base = baseNameOf(path);
80    assert(base.size() >= storePathHashLen);
81    return string(base, 0, storePathHashLen);
82}
83
84
85void checkStoreName(const string & name)
86{
87    string validChars = "+-._?=";
88    /* Disallow names starting with a dot for possible security
89       reasons (e.g., "." and ".."). */
90    if (string(name, 0, 1) == ".")
91        throw Error(format("illegal name: ‘%1%’") % name);
92    for (auto & i : name)
93        if (!((i >= 'A' && i <= 'Z') ||
94              (i >= 'a' && i <= 'z') ||
95              (i >= '0' && i <= '9') ||
96              validChars.find(i) != string::npos))
97        {
98            throw Error(format("invalid character ‘%1%’ in name ‘%2%’")
99                % i % name);
100        }
101}
102
103
104/* Store paths have the following form:
105
106   <store>/<h>-<name>
107
108   where
109
110   <store> = the location of the Nix store, usually /nix/store
111
112   <name> = a human readable name for the path, typically obtained
113     from the name attribute of the derivation, or the name of the
114     source file from which the store path is created.  For derivation
115     outputs other than the default "out" output, the string "-<id>"
116     is suffixed to <name>.
117
118   <h> = base-32 representation of the first 160 bits of a SHA-256
119     hash of <s>; the hash part of the store name
120
121   <s> = the string "<type>:sha256:<h2>:<store>:<name>";
122     note that it includes the location of the store as well as the
123     name to make sure that changes to either of those are reflected
124     in the hash (e.g. you won't get /nix/store/<h>-name1 and
125     /nix/store/<h>-name2 with equal hash parts).
126
127   <type> = one of:
128     "text:<r1>:<r2>:...<rN>"
129       for plain text files written to the store using
130       addTextToStore(); <r1> ... <rN> are the references of the
131       path.
132     "source"
133       for paths copied to the store using addToStore() when recursive
134       = true and hashAlgo = "sha256"
135     "output:<id>"
136       for either the outputs created by derivations, OR paths copied
137       to the store using addToStore() with recursive != true or
138       hashAlgo != "sha256" (in that case "source" is used; it's
139       silly, but it's done that way for compatibility).  <id> is the
140       name of the output (usually, "out").
141
142   <h2> = base-16 representation of a SHA-256 hash of:
143     if <type> = "text:...":
144       the string written to the resulting store path
145     if <type> = "source":
146       the serialisation of the path from which this store path is
147       copied, as returned by hashPath()
148     if <type> = "output:<id>":
149       for non-fixed derivation outputs:
150         the derivation (see hashDerivationModulo() in
151         primops.cc)
152       for paths copied by addToStore() or produced by fixed-output
153       derivations:
154         the string "fixed:out:<rec><algo>:<hash>:", where
155           <rec> = "r:" for recursive (path) hashes, or "" for flat
156             (file) hashes
157           <algo> = "md5", "sha1" or "sha256"
158           <hash> = base-16 representation of the path or flat hash of
159             the contents of the path (or expected contents of the
160             path for fixed-output derivations)
161
162   It would have been nicer to handle fixed-output derivations under
163   "source", e.g. have something like "source:<rec><algo>", but we're
164   stuck with this for now...
165
166   The main reason for this way of computing names is to prevent name
167   collisions (for security).  For instance, it shouldn't be feasible
168   to come up with a derivation whose output path collides with the
169   path for a copied source.  The former would have a <s> starting with
170   "output:out:", while the latter would have a <s> starting with
171   "source:".
172*/
173
174
175Path Store::makeStorePath(const string & type,
176    const Hash & hash, const string & name) const
177{
178    /* e.g., "source:sha256:1abc...:/nix/store:foo.tar.gz" */
179    string s = type + ":" + hash.to_string(Base16) + ":" + storeDir + ":" + name;
180
181    checkStoreName(name);
182
183    return storeDir + "/"
184        + compressHash(hashString(htSHA256, s), 20).to_string(Base32, false)
185        + "-" + name;
186}
187
188
189Path Store::makeOutputPath(const string & id,
190    const Hash & hash, const string & name) const
191{
192    return makeStorePath("output:" + id, hash,
193        name + (id == "out" ? "" : "-" + id));
194}
195
196
197Path Store::makeFixedOutputPath(bool recursive,
198    const Hash & hash, const string & name) const
199{
200    return hash.type == htSHA256 && recursive
201        ? makeStorePath("source", hash, name)
202        : makeStorePath("output:out", hashString(htSHA256,
203                "fixed:out:" + (recursive ? (string) "r:" : "") +
204                hash.to_string(Base16) + ":"),
205            name);
206}
207
208
209Path Store::makeTextPath(const string & name, const Hash & hash,
210    const PathSet & references) const
211{
212    assert(hash.type == htSHA256);
213    /* Stuff the references (if any) into the type.  This is a bit
214       hacky, but we can't put them in `s' since that would be
215       ambiguous. */
216    string type = "text";
217    for (auto & i : references) {
218        type += ":";
219        type += i;
220    }
221    return makeStorePath(type, hash, name);
222}
223
224
225std::pair<Path, Hash> Store::computeStorePathForPath(const Path & srcPath,
226    bool recursive, HashType hashAlgo, PathFilter & filter) const
227{
228    Hash h = recursive ? hashPath(hashAlgo, srcPath, filter).first : hashFile(hashAlgo, srcPath);
229    string name = baseNameOf(srcPath);
230    Path dstPath = makeFixedOutputPath(recursive, h, name);
231    return std::pair<Path, Hash>(dstPath, h);
232}
233
234
235Path Store::computeStorePathForText(const string & name, const string & s,
236    const PathSet & references) const
237{
238    return makeTextPath(name, hashString(htSHA256, s), references);
239}
240
241
242Store::Store(const Params & params)
243    : Config(params)
244    , state({(size_t) pathInfoCacheSize})
245{
246}
247
248
249std::string Store::getUri()
250{
251    return "";
252}
253
254
255bool Store::isValidPath(const Path & storePath)
256{
257    auto hashPart = storePathToHash(storePath);
258
259    {
260        auto state_(state.lock());
261        auto res = state_->pathInfoCache.get(hashPart);
262        if (res) {
263            stats.narInfoReadAverted++;
264            return *res != 0;
265        }
266    }
267
268    if (diskCache) {
269        auto res = diskCache->lookupNarInfo(getUri(), hashPart);
270        if (res.first != NarInfoDiskCache::oUnknown) {
271            stats.narInfoReadAverted++;
272            auto state_(state.lock());
273            state_->pathInfoCache.upsert(hashPart,
274                res.first == NarInfoDiskCache::oInvalid ? 0 : res.second);
275            return res.first == NarInfoDiskCache::oValid;
276        }
277    }
278
279    bool valid = isValidPathUncached(storePath);
280
281    if (diskCache && !valid)
282        // FIXME: handle valid = true case.
283        diskCache->upsertNarInfo(getUri(), hashPart, 0);
284
285    return valid;
286}
287
288
289/* Default implementation for stores that only implement
290   queryPathInfoUncached(). */
291bool Store::isValidPathUncached(const Path & path)
292{
293    try {
294        queryPathInfo(path);
295        return true;
296    } catch (InvalidPath &) {
297        return false;
298    }
299}
300
301
302ref<const ValidPathInfo> Store::queryPathInfo(const Path & storePath)
303{
304    std::promise<ref<ValidPathInfo>> promise;
305
306    queryPathInfo(storePath,
307        [&](ref<ValidPathInfo> info) {
308            promise.set_value(info);
309        },
310        [&](std::exception_ptr exc) {
311            promise.set_exception(exc);
312        });
313
314    return promise.get_future().get();
315}
316
317
318void Store::queryPathInfo(const Path & storePath,
319    std::function<void(ref<ValidPathInfo>)> success,
320    std::function<void(std::exception_ptr exc)> failure)
321{
322    auto hashPart = storePathToHash(storePath);
323
324    try {
325
326        {
327            auto res = state.lock()->pathInfoCache.get(hashPart);
328            if (res) {
329                stats.narInfoReadAverted++;
330                if (!*res)
331                    throw InvalidPath(format("path ‘%s’ is not valid") % storePath);
332                return success(ref<ValidPathInfo>(*res));
333            }
334        }
335
336        if (diskCache) {
337            auto res = diskCache->lookupNarInfo(getUri(), hashPart);
338            if (res.first != NarInfoDiskCache::oUnknown) {
339                stats.narInfoReadAverted++;
340                {
341                    auto state_(state.lock());
342                    state_->pathInfoCache.upsert(hashPart,
343                        res.first == NarInfoDiskCache::oInvalid ? 0 : res.second);
344                    if (res.first == NarInfoDiskCache::oInvalid ||
345                        (res.second->path != storePath && storePathToName(storePath) != ""))
346                        throw InvalidPath(format("path ‘%s’ is not valid") % storePath);
347                }
348                return success(ref<ValidPathInfo>(res.second));
349            }
350        }
351
352    } catch (std::exception & e) {
353        return callFailure(failure);
354    }
355
356    queryPathInfoUncached(storePath,
357        [this, storePath, hashPart, success, failure](std::shared_ptr<ValidPathInfo> info) {
358
359            if (diskCache)
360                diskCache->upsertNarInfo(getUri(), hashPart, info);
361
362            {
363                auto state_(state.lock());
364                state_->pathInfoCache.upsert(hashPart, info);
365            }
366
367            if (!info
368                || (info->path != storePath && storePathToName(storePath) != ""))
369            {
370                stats.narInfoMissing++;
371                return failure(std::make_exception_ptr(InvalidPath(format("path ‘%s’ is not valid") % storePath)));
372            }
373
374            callSuccess(success, failure, ref<ValidPathInfo>(info));
375
376        }, failure);
377}
378
379
380PathSet Store::queryValidPaths(const PathSet & paths, SubstituteFlag maybeSubstitute)
381{
382    struct State
383    {
384        size_t left;
385        PathSet valid;
386        std::exception_ptr exc;
387    };
388
389    Sync<State> state_(State{paths.size(), PathSet()});
390
391    std::condition_variable wakeup;
392
393    for (auto & path : paths)
394        queryPathInfo(path,
395            [path, &state_, &wakeup](ref<ValidPathInfo> info) {
396                auto state(state_.lock());
397                state->valid.insert(path);
398                assert(state->left);
399                if (!--state->left)
400                    wakeup.notify_one();
401            },
402            [path, &state_, &wakeup](std::exception_ptr exc) {
403                auto state(state_.lock());
404                try {
405                    std::rethrow_exception(exc);
406                } catch (InvalidPath &) {
407                } catch (...) {
408                    state->exc = exc;
409                }
410                assert(state->left);
411                if (!--state->left)
412                    wakeup.notify_one();
413            });
414
415    while (true) {
416        auto state(state_.lock());
417        if (!state->left) {
418            if (state->exc) std::rethrow_exception(state->exc);
419            return state->valid;
420        }
421        state.wait(wakeup);
422    }
423}
424
425
426/* Return a string accepted by decodeValidPathInfo() that
427   registers the specified paths as valid.  Note: it's the
428   responsibility of the caller to provide a closure. */
429string Store::makeValidityRegistration(const PathSet & paths,
430    bool showDerivers, bool showHash)
431{
432    string s = "";
433
434    for (auto & i : paths) {
435        s += i + "\n";
436
437        auto info = queryPathInfo(i);
438
439        if (showHash) {
440            s += info->narHash.to_string(Base16, false) + "\n";
441            s += (format("%1%\n") % info->narSize).str();
442        }
443
444        Path deriver = showDerivers ? info->deriver : "";
445        s += deriver + "\n";
446
447        s += (format("%1%\n") % info->references.size()).str();
448
449        for (auto & j : info->references)
450            s += j + "\n";
451    }
452
453    return s;
454}
455
456
457void Store::pathInfoToJSON(JSONPlaceholder & jsonOut, const PathSet & storePaths,
458    bool includeImpureInfo, bool showClosureSize, AllowInvalidFlag allowInvalid)
459{
460    auto jsonList = jsonOut.list();
461
462    for (auto storePath : storePaths) {
463        auto jsonPath = jsonList.object();
464        jsonPath.attr("path", storePath);
465
466        try {
467            auto info = queryPathInfo(storePath);
468            storePath = info->path;
469
470            jsonPath
471                .attr("narHash", info->narHash.to_string())
472                .attr("narSize", info->narSize);
473
474            {
475                auto jsonRefs = jsonPath.list("references");
476                for (auto & ref : info->references)
477                    jsonRefs.elem(ref);
478            }
479
480            if (info->ca != "")
481                jsonPath.attr("ca", info->ca);
482
483            std::pair<uint64_t, uint64_t> closureSizes;
484
485            if (showClosureSize) {
486                closureSizes = getClosureSize(storePath);
487                jsonPath.attr("closureSize", closureSizes.first);
488            }
489
490            if (includeImpureInfo) {
491
492                if (info->deriver != "")
493                    jsonPath.attr("deriver", info->deriver);
494
495                if (info->registrationTime)
496                    jsonPath.attr("registrationTime", info->registrationTime);
497
498                if (info->ultimate)
499                    jsonPath.attr("ultimate", info->ultimate);
500
501                if (!info->sigs.empty()) {
502                    auto jsonSigs = jsonPath.list("signatures");
503                    for (auto & sig : info->sigs)
504                        jsonSigs.elem(sig);
505                }
506
507                auto narInfo = std::dynamic_pointer_cast<const NarInfo>(
508                    std::shared_ptr<const ValidPathInfo>(info));
509
510                if (narInfo) {
511                    if (narInfo->fileHash)
512                        jsonPath.attr("downloadHash", narInfo->fileHash.to_string());
513                    if (narInfo->fileSize)
514                        jsonPath.attr("downloadSize", narInfo->fileSize);
515                    if (showClosureSize)
516                        jsonPath.attr("closureDownloadSize", closureSizes.second);
517                }
518            }
519
520        } catch (InvalidPath &) {
521            jsonPath.attr("valid", false);
522        }
523    }
524}
525
526
527std::pair<uint64_t, uint64_t> Store::getClosureSize(const Path & storePath)
528{
529    uint64_t totalNarSize = 0, totalDownloadSize = 0;
530    PathSet closure;
531    computeFSClosure(storePath, closure, false, false);
532    for (auto & p : closure) {
533        auto info = queryPathInfo(p);
534        totalNarSize += info->narSize;
535        auto narInfo = std::dynamic_pointer_cast<const NarInfo>(
536            std::shared_ptr<const ValidPathInfo>(info));
537        if (narInfo)
538            totalDownloadSize += narInfo->fileSize;
539    }
540    return {totalNarSize, totalDownloadSize};
541}
542
543
544const Store::Stats & Store::getStats()
545{
546    {
547        auto state_(state.lock());
548        stats.pathInfoCacheSize = state_->pathInfoCache.size();
549    }
550    return stats;
551}
552
553
554void Store::buildPaths(const PathSet & paths, BuildMode buildMode)
555{
556    for (auto & path : paths)
557        if (isDerivation(path))
558            unsupported();
559
560    if (queryValidPaths(paths).size() != paths.size())
561        unsupported();
562}
563
564
565void copyStorePath(ref<Store> srcStore, ref<Store> dstStore,
566    const Path & storePath, RepairFlag repair, CheckSigsFlag checkSigs)
567{
568    auto info = srcStore->queryPathInfo(storePath);
569
570    StringSink sink;
571    srcStore->narFromPath({storePath}, sink);
572
573    if (!info->narHash && !checkSigs) {
574        auto info2 = make_ref<ValidPathInfo>(*info);
575        info2->narHash = hashString(htSHA256, *sink.s);
576        if (!info->narSize) info2->narSize = sink.s->size();
577        info = info2;
578    }
579
580    assert(info->narHash);
581
582    if (info->ultimate) {
583        auto info2 = make_ref<ValidPathInfo>(*info);
584        info2->ultimate = false;
585        info = info2;
586    }
587
588    assert(info->narHash);
589
590    dstStore->addToStore(*info, sink.s, repair, checkSigs);
591}
592
593
594void copyPaths(ref<Store> srcStore, ref<Store> dstStore, const PathSet & storePaths,
595    RepairFlag repair, CheckSigsFlag checkSigs, SubstituteFlag substitute)
596{
597    PathSet valid = dstStore->queryValidPaths(storePaths, substitute);
598
599    PathSet missing;
600    for (auto & path : storePaths)
601        if (!valid.count(path)) missing.insert(path);
602
603    ThreadPool pool;
604
605    processGraph<Path>(pool,
606        PathSet(missing.begin(), missing.end()),
607
608        [&](const Path & storePath) {
609            if (dstStore->isValidPath(storePath)) return PathSet();
610            return srcStore->queryPathInfo(storePath)->references;
611        },
612
613        [&](const Path & storePath) {
614            checkInterrupt();
615
616            if (!dstStore->isValidPath(storePath)) {
617                printInfo("copying ‘%s’...", storePath);
618                copyStorePath(srcStore, dstStore, storePath, repair, checkSigs);
619            }
620        });
621}
622
623
624void copyClosure(ref<Store> srcStore, ref<Store> dstStore,
625    const PathSet & storePaths, RepairFlag repair, CheckSigsFlag checkSigs,
626    SubstituteFlag substitute)
627{
628    PathSet closure;
629    srcStore->computeFSClosure({storePaths}, closure);
630    copyPaths(srcStore, dstStore, closure, repair, checkSigs, substitute);
631}
632
633
634ValidPathInfo decodeValidPathInfo(std::istream & str, bool hashGiven)
635{
636    ValidPathInfo info;
637    getline(str, info.path);
638    if (str.eof()) { info.path = ""; return info; }
639    if (hashGiven) {
640        string s;
641        getline(str, s);
642        info.narHash = Hash(s, htSHA256);
643        getline(str, s);
644        if (!string2Int(s, info.narSize)) throw Error("number expected");
645    }
646    getline(str, info.deriver);
647    string s; int n;
648    getline(str, s);
649    if (!string2Int(s, n)) throw Error("number expected");
650    while (n--) {
651        getline(str, s);
652        info.references.insert(s);
653    }
654    if (!str || str.eof()) throw Error("missing input");
655    return info;
656}
657
658
659string showPaths(const PathSet & paths)
660{
661    string s;
662    for (auto & i : paths) {
663        if (s.size() != 0) s += ", ";
664        s += "‘" + i + "’";
665    }
666    return s;
667}
668
669
670std::string ValidPathInfo::fingerprint() const
671{
672    if (narSize == 0 || !narHash)
673        throw Error(format("cannot calculate fingerprint of path ‘%s’ because its size/hash is not known")
674            % path);
675    return
676        "1;" + path + ";"
677        + narHash.to_string(Base32) + ";"
678        + std::to_string(narSize) + ";"
679        + concatStringsSep(",", references);
680}
681
682
683void ValidPathInfo::sign(const SecretKey & secretKey)
684{
685    sigs.insert(secretKey.signDetached(fingerprint()));
686}
687
688
689bool ValidPathInfo::isContentAddressed(const Store & store) const
690{
691    auto warn = [&]() {
692        printError(format("warning: path ‘%s’ claims to be content-addressed but isn't") % path);
693    };
694
695    if (hasPrefix(ca, "text:")) {
696        Hash hash(std::string(ca, 5));
697        if (store.makeTextPath(storePathToName(path), hash, references) == path)
698            return true;
699        else
700            warn();
701    }
702
703    else if (hasPrefix(ca, "fixed:")) {
704        bool recursive = ca.compare(6, 2, "r:") == 0;
705        Hash hash(std::string(ca, recursive ? 8 : 6));
706        if (store.makeFixedOutputPath(recursive, hash, storePathToName(path)) == path)
707            return true;
708        else
709            warn();
710    }
711
712    return false;
713}
714
715
716size_t ValidPathInfo::checkSignatures(const Store & store, const PublicKeys & publicKeys) const
717{
718    if (isContentAddressed(store)) return maxSigs;
719
720    size_t good = 0;
721    for (auto & sig : sigs)
722        if (checkSignature(publicKeys, sig))
723            good++;
724    return good;
725}
726
727
728bool ValidPathInfo::checkSignature(const PublicKeys & publicKeys, const std::string & sig) const
729{
730    return verifyDetached(fingerprint(), sig, publicKeys);
731}
732
733
734Strings ValidPathInfo::shortRefs() const
735{
736    Strings refs;
737    for (auto & r : references)
738        refs.push_back(baseNameOf(r));
739    return refs;
740}
741
742
743std::string makeFixedOutputCA(bool recursive, const Hash & hash)
744{
745    return "fixed:" + (recursive ? (std::string) "r:" : "") + hash.to_string();
746}
747
748
749}
750
751
752#include "local-store.hh"
753#include "remote-store.hh"
754
755
756namespace nix {
757
758
759RegisterStoreImplementation::Implementations * RegisterStoreImplementation::implementations = 0;
760
761
762ref<Store> openStore(const std::string & uri_,
763    const Store::Params & extraParams)
764{
765    auto uri(uri_);
766    Store::Params params(extraParams);
767    auto q = uri.find('?');
768    if (q != std::string::npos) {
769        for (auto s : tokenizeString<Strings>(uri.substr(q + 1), "&")) {
770            auto e = s.find('=');
771            if (e != std::string::npos)
772                params[s.substr(0, e)] = s.substr(e + 1);
773        }
774        uri = uri_.substr(0, q);
775    }
776
777    for (auto fun : *RegisterStoreImplementation::implementations) {
778        auto store = fun(uri, params);
779        if (store) {
780            store->warnUnknownSettings();
781            return ref<Store>(store);
782        }
783    }
784
785    throw Error("don't know how to open Nix store ‘%s’", uri);
786}
787
788
789StoreType getStoreType(const std::string & uri, const std::string & stateDir)
790{
791    if (uri == "daemon") {
792        return tDaemon;
793    } else if (uri == "local") {
794        return tLocal;
795    } else if (uri == "" || uri == "auto") {
796        if (access(stateDir.c_str(), R_OK | W_OK) == 0)
797            return tLocal;
798        else if (pathExists(settings.nixDaemonSocketFile))
799            return tDaemon;
800        else
801            return tLocal;
802    } else {
803        return tOther;
804    }
805}
806
807
808static RegisterStoreImplementation regStore([](
809    const std::string & uri, const Store::Params & params)
810    -> std::shared_ptr<Store>
811{
812    switch (getStoreType(uri, get(params, "state", settings.nixStateDir))) {
813        case tDaemon:
814            return std::shared_ptr<Store>(std::make_shared<UDSRemoteStore>(params));
815        case tLocal:
816            return std::shared_ptr<Store>(std::make_shared<LocalStore>(params));
817        default:
818            return nullptr;
819    }
820});
821
822
823std::list<ref<Store>> getDefaultSubstituters()
824{
825    static auto stores([]() {
826        std::list<ref<Store>> stores;
827
828        StringSet done;
829
830        auto addStore = [&](const std::string & uri) {
831            if (done.count(uri)) return;
832            done.insert(uri);
833            stores.push_back(openStore(uri));
834        };
835
836        for (auto uri : settings.substituters.get())
837            addStore(uri);
838
839        for (auto uri : settings.extraSubstituters.get())
840            addStore(uri);
841
842        stores.sort([](ref<Store> & a, ref<Store> & b) {
843            return a->getPriority() < b->getPriority();
844        });
845
846        return stores;
847    } ());
848
849    return stores;
850}
851
852
853}
854