diff --git a/mk/run_test.sh b/mk/run_test.sh index 7e95df2ac162..959c5729972c 100755 --- a/mk/run_test.sh +++ b/mk/run_test.sh @@ -18,6 +18,8 @@ fi run_test () { (cd tests && env ${TESTS_ENVIRONMENT} init.sh 2>/dev/null > /dev/null) log="$(cd $(dirname $1) && env ${TESTS_ENVIRONMENT} $(basename $1) 2>&1)" + # debug: show output of test + echo "$log" status=$? } diff --git a/src/libexpr/lexer.l b/src/libexpr/lexer.l index 462b3b60211b..f584ff2cb620 100644 --- a/src/libexpr/lexer.l +++ b/src/libexpr/lexer.l @@ -2,7 +2,6 @@ %option noyywrap %option never-interactive %option stack -%option nodefault %option nounput noyy_top_state diff --git a/src/libstore/build/find-cycles.cc b/src/libstore/build/find-cycles.cc new file mode 100644 index 000000000000..07e6e73e3b69 --- /dev/null +++ b/src/libstore/build/find-cycles.cc @@ -0,0 +1,389 @@ +#include "find-cycles.hh" + +#include // once_flag +#include + +// this is the second pass of cycle finding +// first pass: scanForReferences in nix/src/libstore/references.cc +// this is a separate file to recompile faster +// only nix/src/libstore/build/local-derivation-goal.cc +// depends on this + +namespace nix { + +// same as in nix/src/libstore/references.cc +static size_t refLength = 32; /* characters */ +// TODO rename to hashLength? + +void scanForCycleEdges( + const Path & path, + const StorePathSet & refs, + StoreCycleEdgeVec & edges) +{ + StringSet hashes; + std::map hashPathMap; // aka backMap + + // path ex: /run/user/1000/nix-test/tests/multiple-outputs/store/fyj0pvp3s5przbqcylczin2d35y4giw8-cyclic-outputs-a + // prefix: /run/user/1000/nix-test/tests/multiple-outputs/store/ + // -> prefix is dirname + auto storePrefixPath = std::filesystem::path(path); + storePrefixPath.remove_filename(); + std::string storePrefix = (std::string) storePrefixPath; // with trailing slash. ex: /nix/store/ + + debug(format("scanForCycleEdges: storePrefixPath = %1%") % storePrefixPath); + debug(format("scanForCycleEdges: storePrefix = %1%") % storePrefix); + + for (auto & i : refs) { + std::string hashPart(i.hashPart()); + auto inserted = hashPathMap.emplace(hashPart, i).second; + assert(inserted); + hashes.insert(hashPart); + } + + scanForCycleEdges2(path, hashes, edges, storePrefix); +} + +/* +based on nix/src/libutil/archive.cc -> dumpPath +*/ +void scanForCycleEdges2( + std::string path, + const StringSet & hashes, + StoreCycleEdgeVec & edges, + std::string storeDir) +{ + // static void search + static std::once_flag initialised; + static bool isBase32[256]; + std::call_once(initialised, [](){ + for (unsigned int i = 0; i < 256; ++i) isBase32[i] = false; + for (unsigned int i = 0; i < base32Chars.size(); ++i) + isBase32[(unsigned char) base32Chars[i]] = true; + }); + + // TODO search hashes in name? + + auto st = lstat(path); + + debug(format("scanForCycleEdges2: path = %1%") % path); + + if (S_ISREG(st.st_mode)) { // is regular file + // static void dumpContents + AutoCloseFD fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (!fd) throw SysError("opening file '%1%'", path); + + std::vector buf(65536); + //size_t rest = size; + size_t rest = st.st_size; + size_t start = 0; + + /* It's possible that a reference spans the previous and current + fragment, so search in the concatenation of the tail of the + previous fragment and the start of the current fragment. */ + // carry more than 32 byte (refLength == hash length) + // filepaths are longer than 32 byte + // -> assume ~1000 bytes for filepaths + // buffer size is 64 * 1024 = 65536 + // 1000 / 65536 = 1.5% + #define MAX_FILEPATH_LENGTH 1000 + bool bufCarryUsed = false; + //std::vector bufCarry(refLength); + //std::vector bufMatch(refLength); + std::vector bufCarry(MAX_FILEPATH_LENGTH); + std::vector bufMatch(MAX_FILEPATH_LENGTH); + + while (rest > 0) { + auto n = std::min(rest, buf.size()); + readFull(fd.get(), buf.data(), n); + + //printf("read file %s: n = %lu\n", path.c_str(), n); + debug(format("scanForCycleEdges2: read file %s: n = %lu") % path % n); + + if (bufCarryUsed) { + printf("scanForCycleEdges2: FIXME implement bufCarryUsed for filepaths\n"); + /* + for (size_t i = 1; i < std::min(refLength, buf.size()) - 1; ) { + bool match = true; + for (size_t j = 0; i < refLength; j++) { + if (i + j < refLength) { + // use carry buffer + if (!isBase32[(unsigned char) bufCarry[i + j]]) { + i += j + 1; // skip checked bytes + match = false; + break; + } + bufMatch[j] = bufCarry[i + j]; + } + else { + // use current buffer + if (!isBase32[(unsigned char) buf[i + j]]) { + i += j + 1; // skip checked bytes + match = false; + break; + } + bufMatch[j] = buf[i + j]; + } + } + if (!match) continue; + std::string ref(bufMatch.begin(), bufMatch.end()); + if (hashes.find(ref) != hashes.end()) { + StorePathRef rp(ref, path); // TODO rename? swap ref and path + edges.push_back(rp); + debug(format("scanForCycleEdges2: rp(%1%, %2%)") % ref % path); + } + break; // only one match possible between carry and first block + } + */ + bufCarryUsed = false; + } + + // static void search + // total offset in file = start + i + for (size_t i = 0; i + refLength <= buf.size(); ) { + int j; + bool match = true; + for (j = refLength - 1; j >= 0; --j) + if (!isBase32[(unsigned char) buf[i + j]]) { + i += j + 1; // seek to next block + match = false; + break; + } + if (!match) continue; + + // found possible match + std::string hash(buf.begin()+i, buf.begin()+i+refLength); + + // TODO check buffer bounds! + + if (hashes.find(hash) != hashes.end()) { + // found hash + debug(format("scanForCycleEdges2: found reference to '%1%' at offset '%2%'. hash = '%3%' + path = '%4%'") + % hash % (start + i) % hash % path); + debug(format("scanForCycleEdges2: rp(%1%, %2%)") % hash % path); + + // get file path + // = longest common substring + + // add storeDir prefix. ex: /nix/store/ + int storeDirLength = storeDir.size(); + std::string targetPath = storeDir + hash; + std::string targetStorePath; + if (std::string(buf.begin()+i-storeDirLength, buf.begin()+i+refLength) == targetPath) { + // found storeDir + hash + debug(format("scanForCycleEdges2: found reference to path '%1%' at offset '%2%'") + % targetPath % (start + i - storeDirLength)); + + // not using + // auto storePath = hashPathMap.find(hash); + // because the file can contain a substring of the full path + + // search end of path + // we assume that (max) 220 "file exists?" calls + // are faster than one "readdir" call on a large nix-store (with 1000s of entries) + // so here, we do not use "glob" to find the full path. + // shortest name of first dir is (hash + "-x"), hence "refLength + 2". + int testNameLength = refLength + 2; + int targetPathLastEnd = 0; + bool foundStorePath = false; + bool foundPath = false; + bool foundDir = false; + debug(format("scanForCycleEdges2: testNameLength %3i") % testNameLength); + for (; testNameLength < 255; testNameLength++) { + auto targetPathEnd = buf.begin()+i+targetPathLastEnd+testNameLength; + std::string testPath(buf.begin()+i-storeDirLength, targetPathEnd); + struct stat testStat; + if (stat(testPath.c_str(), &testStat) == 0) { + debug(format("scanForCycleEdges2: testNameLength %3i -> testPath %s -> exists") % testNameLength % testPath); + if (foundStorePath == false) { + // first component of filepath is the "StorePath" + // slash is optional = path can end after name + targetStorePath = testPath.substr(storeDirLength); + foundStorePath = true; + } + foundPath = true; + targetPath = testPath; + foundDir = (buf[i + targetPathLastEnd + testNameLength] == '/'); + if (foundDir) { + debug(format("scanForCycleEdges2: testNameLength %3i -> testPath %s/ -> dir") % testNameLength % testPath); + targetPathLastEnd += testNameLength; + testNameLength = 1; + continue; + } + //break; // dont break to find longest path + } + else { + debug(format("scanForCycleEdges2: testNameLength %3i -> testPath %s") % testNameLength % testPath); + } + if (i + targetPathLastEnd + testNameLength == n) { + // TODO test with carry + debug(format("scanForCycleEdges2: testNameLength: end of buffer")); + break; + } + } + debug(format("scanForCycleEdges2: foundPath = %1%") % foundPath); + testNameLength = 1; + targetPathLastEnd += testNameLength; + } + + debug(format("scanForCycleEdges2: targetPath '%1%'") % targetPath); + debug(format("scanForCycleEdges2: cycle edge:\n %1%\n %2%") % path % targetPath); + // print actual file paths in temp folder + // so user can inspect the files + + //StorePathRef rp(hash, path); + debug(format("scanForCycleEdges2: cycle edge: create StorePath")); + debug(format("scanForCycleEdges2: cycle edge: targetPath.substr(storeDirLength) = %s") % targetPath.substr(storeDirLength)); + // TODO StorePath is always the top-level dir: /nix/store/hash-name-version + // but here, we want to return a "StoreFile": /nix/store/hash-name-version/path/to/file + + debug(format("scanForCycleEdges2: cycle edge: targetStorePath = %s") % targetStorePath); + StoreCycleEdge edge({ + path, // source + //targetStorePath // target + targetPath // target + }); + // TODO targetPath or targetStorePath? + // i prefer targetPath as the path actually exists + // and allows further inspection of the file + + debug(format("scanForCycleEdges2: cycle edge: insert StorePathRef")); + edges.push_back(edge); + } + ++i; + } + + start += n; + rest -= n; + + if (n == buf.size()) { + // buffer is full + // carry last N bytes to next iteration + for (size_t i = 0; i < MAX_FILEPATH_LENGTH; i++) { + //bufCarry[i] = buf[buf.size() - refLength + i]; + bufCarry[i] = buf[buf.size() - MAX_FILEPATH_LENGTH + i]; + bufCarryUsed = true; + } + } + } + } + + else if (S_ISDIR(st.st_mode)) { // is directory + + /* If we're on a case-insensitive system like macOS, undo + the case hack applied by restorePath(). */ + + std::map unhacked; + + // TODO milahu: unswitch these for loops? archiveSettings.useCaseHack is constant + for (auto & i : readDirectory(path)) + #if __APPLE__ + //if (archiveSettings.useCaseHack) { + string name(i.name); + size_t pos = i.name.find(caseHackSuffix); + if (pos != string::npos) { + debug(format("removing case hack suffix from '%1%'") % (path + "/" + i.name)); + name.erase(pos); + } + if (unhacked.find(name) != unhacked.end()) + throw Error("file name collision in between '%1%' and '%2%'", + (path + "/" + unhacked[name]), + (path + "/" + i.name)); + unhacked[name] = i.name; + //} else + #else + unhacked[i.name] = i.name; + #endif + + for (auto & i : unhacked) { + // recurse + // no need to filter path like in dumpPath + debug(format("scanForCycleEdges2: recurse")); + scanForCycleEdges2( + path + "/" + i.second, + hashes, + edges, + storeDir + ); + } + } + + else if (S_ISLNK(st.st_mode)) { + std::string buf(readLink(path)); + // static void search + // TODO refactor this copy-pasta + for (size_t i = 0; i + refLength <= buf.size(); ) { + int j; + bool match = true; + for (j = refLength - 1; j >= 0; --j) + if (!isBase32[(unsigned char) buf[i + j]]) { + i += j + 1; // seek to next block + match = false; + break; + } + if (!match) continue; + //std::string ref(buf.substr(i, refLength)); + std::string ref(buf.begin()+i, buf.begin()+i+refLength); + //if (hashes.erase(ref)) { + if (hashes.find(ref) != hashes.end()) { + debug(format("scanForCycleEdges2: found reference to '%1%' at offset '%2%'. ref = '%3%' + path = '%4%'") + % ref % i % ref % path); + debug(format("scanForCycleEdges2: rp(%1%, %2%)") % ref % path); + //StorePathRef rp(ref, path); + /* + // TODO impl + StoreCycleEdge edge({ + path, // source + ref, // target + }) + edges.push_back(edge); + */ + } + ++i; + } + } + else throw Error("file '%1%' has an unsupported type", path); +} + +void transformEdgesToMultiedges( + StoreCycleEdgeVec & edges, + StoreCycleEdgeVec & multiedges) +{ + for (auto & edge2 : edges) { + bool edge2Joined = false; + for (auto & edge1 : multiedges) { + debug(format("edge1:")); + for (auto file : edge1) { + debug(format("- %s") % file); + } + debug(format("edge2:")); + for (auto file : edge2) { + debug(format("- %s") % file); + } + if (edge1.back() == edge2.front()) { + // a-b + b-c -> a-b-c + for (size_t i = 1; i < edge2.size(); i++) { + debug(format("edge1.push_back: edge2[%i] = %s") % i % edge2[i]); + edge1.push_back(edge2[i]); + } + edge2Joined = true; + break; + } + if (edge2.back() == edge1.front()) { + // b-c + a-b -> a-b-c + // size_t -> segfault https://stackoverflow.com/questions/64036592 + //for (size_t i = edge2.size() - 2; i >= 0; i--) { + for (int i = edge2.size() - 2; i >= 0; i--) { + debug(format("edge1.push_front: edge2[%i] = %s") % i % edge2[i]); + edge1.push_front(edge2[i]); + } + edge2Joined = true; + break; + } + } + if (!edge2Joined) { + multiedges.push_back(edge2); + } + } +} + +} diff --git a/src/libstore/build/find-cycles.hh b/src/libstore/build/find-cycles.hh new file mode 100644 index 000000000000..ea6fb5f4a7c7 --- /dev/null +++ b/src/libstore/build/find-cycles.hh @@ -0,0 +1,40 @@ +#pragma once + +//#include "hash.hh" +#include "path.hh" + +#include +#include + +namespace nix { + +// see nix/src/libstore/references.hh +// first pass: fast on success +//std::pair scanForReferences(const Path & path, const StorePathSet & refs); +//StorePathSet scanForReferences(Sink & toTee, const Path & path, const StorePathSet & refs); + +//typedef std::pair StoreCycleEdge; +// need deque to join edges +typedef std::deque StoreCycleEdge; +typedef std::vector StoreCycleEdgeVec; + +// second pass: get exact file paths of cycles +void scanForCycleEdges( + const Path & path, + const StorePathSet & refs, + StoreCycleEdgeVec & edges +); + +void scanForCycleEdges2( + std::string path, + const StringSet & hashes, + StoreCycleEdgeVec & seen, + std::string storePrefix +); + +void transformEdgesToMultiedges( + StoreCycleEdgeVec & edges, + StoreCycleEdgeVec & multiedges +); + +} diff --git a/src/libstore/build/find-cycles.py b/src/libstore/build/find-cycles.py new file mode 100644 index 000000000000..bdbb0d22d8c4 --- /dev/null +++ b/src/libstore/build/find-cycles.py @@ -0,0 +1,77 @@ +#! /usr/bin/env python + +# prototype for find-cycles.cc + +edges = [ + # sorted: + #[1, 2], + #[2, 3], + #[3, 1], + # unsorted: + [1, 2], + [3, 1], + [2, 3], +] + +edges_yaml = """\ +- + - a-to-c.2.txt + - c-to-b.2.txt +- + - b-to-a.2.txt + - a-to-c.2.txt +- + - c-to-b.2.txt + - b-to-a.2.txt +""" + +edges = [] +i = -1 +for line in edges_yaml.splitlines(): + if line == "-": + i += 1 + continue + try: + edges[i] + except IndexError: + edges.append([]) + val = line[3:] + # a: gdmbqa2y7xv2sc0hf6q5c6da3cai5ygw-cyclic-outputs-b/opt/from-b-to-a.2.txt + # b: b-to-a + #val = val[112:118] + edges[i].append(val) + +#print(repr(edges)); import sys; sys.exit() + +multiedges = [] + +for edge2 in edges: + edge2Joined = False + for edge1 in multiedges: + print(f"edge2 = {edge2}") + print(f"edge1 = {edge1}") + if edge1[-1] == edge2[0]: # edge1.back() == edge2.front() + # a-b + b-c -> a-b-c + print(f"append: edge1 = {edge1} + {edge2[1:]} = {edge1 + edge2[1:]}") + #edge1 = edge1 + edge2[1:] # wrong: this creates a new list + edge1.append(*edge2[1:]) + print(f"-> edge1 = {edge1}") + edge2Joined = True + break + if edge2[-1] == edge1[0]: # edge2.back() == edge1.front() + # b-c + a-b -> a-b-c + print(f"prepend: edge1 = {edge2[:-1]} + {edge1} = {edge2[:-1] + edge1}") + #edge1.prepend(*edge2[:-1]) + edge1.insert(0, *edge2[:-1]) + print(f"-> edge1 = {edge1}") + edge2Joined = True + break + if not edge2Joined: + print(f"init: edge1 = {edge2}") + multiedges.append(edge2) + +for edge1 in multiedges: + print("edge1:") + for point in edge1: + print(f" {point}") + diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 3ac9c20f9b7f..40130583ca82 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -15,6 +15,7 @@ #include "topo-sort.hh" #include "callback.hh" #include "json-utils.hh" +#include "find-cycles.hh" // scanForCycleEdges transformEdgesToMultiedges #include #include @@ -2192,7 +2193,15 @@ DrvOutputs LocalDerivationGoal::registerOutputs() outputStats.insert_or_assign(outputName, std::move(st)); } - auto sortedOutputNames = topoSort(outputsToSort, + debug("calling topoSort"); + + std::vector sortedOutputNames; + + try { + // TODO indent + sortedOutputNames = topoSort( + //auto sortedOutputNames = topoSortCycles( + outputsToSort, {[&](const std::string & name) { auto orifu = get(outputReferencesIfUnregistered, name); if (!orifu) @@ -2221,6 +2230,55 @@ DrvOutputs LocalDerivationGoal::registerOutputs() "cycle detected in build of '%s' in the references of output '%s' from output '%s'", worker.store.printStorePath(drvPath), path, parent); }}); + // TODO indent end + } + catch (Error & e) { + debug(format("catching cycle error: %1%") % e.what()); + + for (auto & sp : referenceablePaths) { + debug(format("analyze cycle: referenceablePaths[] = %1%") % sp.to_string()); + } + + // analyze cycle + StoreCycleEdgeVec edges; + for (auto & [outputName, _] : drv->outputs) { + debug(format("analyze cycle: outputName = %1%") % outputName); + auto actualPath = toRealPathChroot(worker.store.printStorePath(scratchOutputs.at(outputName))); + debug(format("analyze cycle: actualPath = %1%") % actualPath); + debug(format("analyze cycle: scanForCycleEdges")); + scanForCycleEdges(actualPath, referenceablePaths, edges); + } + + debug(format("error: cycles detected. found %i cycle edges:") % edges.size()); + for (auto edge : edges) { + debug(format("-")); + for (auto file : edge) { + debug(format(" - %s") % file); + } + } + // find paths in directed graph + // = connect adjacent edges to multiedges + // = transform edges to multiedges + // simple implementation with string compare + StoreCycleEdgeVec multiedges; + debug(format("transforming edges to multiedges")); + transformEdgesToMultiedges(edges, multiedges); + std::cout << "error: cycle detected. found " << multiedges.size() << " cycle edges:\n"; + //for (auto multiedge : multiedges) { + for (size_t i = 0; i < multiedges.size(); i++) { + auto multiedge = multiedges[i]; + //std::cout << "-\n"; + std::cout << (i + 1) << ":\n"; + for (auto file : multiedge) { + std::cout << " - " << file << "\n"; + } + } + std::cout << std::flush; // "\n" does not flush like std::endl + + throw e; // BuildError: cycle + } + + debug(format("no cycles -> continue")); std::reverse(sortedOutputNames.begin(), sortedOutputNames.end()); diff --git a/src/libstore/references.cc b/src/libstore/references.cc index 34dce092cb2f..24bb4ae3cd6a 100644 --- a/src/libstore/references.cc +++ b/src/libstore/references.cc @@ -12,6 +12,7 @@ namespace nix { static size_t refLength = 32; /* characters */ +// TODO rename to hashLength? static void search( @@ -83,6 +84,8 @@ StorePathSet scanForReferences( const StorePathSet & refs) { StringSet hashes; + + // backMap: map from hash to storepath std::map backMap; for (auto & i : refs) { @@ -108,7 +111,6 @@ StorePathSet scanForReferences( return found; } - RewritingSink::RewritingSink(const std::string & from, const std::string & to, Sink & nextSink) : from(from), to(to), nextSink(nextSink) { diff --git a/src/libstore/references.hh b/src/libstore/references.hh index a6119c861904..a03db42988ba 100644 --- a/src/libstore/references.hh +++ b/src/libstore/references.hh @@ -3,12 +3,38 @@ #include "hash.hh" #include "path.hh" +#include + namespace nix { +// first pass: fast on success std::pair scanForReferences(const Path & path, const StorePathSet & refs); - StorePathSet scanForReferences(Sink & toTee, const Path & path, const StorePathSet & refs); +// moved to nix/src/libstore/build/find-cycles.hh +/* + +//typedef std::pair StoreCycleEdge; +// need deque to join edges +typedef std::deque StoreCycleEdge; +typedef std::vector StoreCycleEdgeVec; + +// second pass: get exact file paths of cycles +void scanForCycleEdges( + const Path & path, + const StorePathSet & refs, + StoreCycleEdgeVec & edges +); + +void scanForCycleEdges2( + std::string path, + const StringSet & hashes, + StoreCycleEdgeVec & seen, + std::string storePrefix +); + +*/ + class RefScanSink : public Sink { StringSet hashes; diff --git a/src/libutil/topo-sort.hh b/src/libutil/topo-sort.hh index 7418be5e0886..d85a369abc79 100644 --- a/src/libutil/topo-sort.hh +++ b/src/libutil/topo-sort.hh @@ -39,4 +39,89 @@ std::vector topoSort(std::set items, return sorted; } + + +template +std::vector topoSortCycles(std::set items, + std::function(const T &)> getChildren, + std::function *)> handleCycle) +{ + std::vector sorted; + std::set visited, parents; + + std::function dfsVisit; + + dfsVisit = [&](const T & path, const T * parent) { + debug(format("topoSortCycles: path = %1%") % path); + if (parent != nullptr) { + debug(format("topoSortCycles: args: *parent = %1%") % *parent); + } + else { + debug(format("topoSortCycles: args: parent = nullptr") % *parent); + } + debug(format("topoSortCycles: state: sorted = %1%") % *parent); + + if (parents.count(path)) { + debug(format("topoSortCycles: found cycle")); + for (auto & i : parents) { + debug(format("topoSortCycles: parents[] = %1%") % i); + } + for (auto & i : sorted) { + debug(format("topoSortCycles: sorted[] = %1%") % i); + } + debug(format("topoSortCycles: calling handleCycle")); + // NOTE sorted can be larger than necessary. + // ex: + // sorted: a b c + // path: b + // -> cycle b c b + // -> a is not in cycle + // -> end of sorted is in cycle + std::vector * sortedPtr = &sorted; + handleCycle(path, const_cast *>(sortedPtr)); + } + + if (!visited.insert(path).second) { + debug(format("topoSortCycles: !visited.insert(path).second == true -> return")); + return; + } + debug(format("topoSortCycles: !visited.insert(path).second == false -> continue")); + + parents.insert(path); + sorted.push_back(path); + + debug(format("topoSortCycles: calling getChildren")); + std::set references = getChildren(path); + + for (auto & i : references) { + debug(format("topoSortCycles: references[] = %1%") % i); + } + + // recursion + for (auto & i : references) + /* Don't traverse into items that don't exist in our starting set. */ + if (i != path && items.count(i)) { + debug(format("topoSortCycles: dfsVisit? yes")); + dfsVisit(i, &path); + } + else { + debug(format("topoSortCycles: dfsVisit? no")); + } + + debug(format("topoSortCycles: done path: %1%") % path); + //sorted.push_back(path); // moved before recursion, so we have sorted in handleCycle + parents.erase(path); + for (auto & i : sorted) { + debug(format("topoSortCycles: done: sorted[] = %1%") % i); + } + }; + + for (auto & i : items) + dfsVisit(i, nullptr); + + std::reverse(sorted.begin(), sorted.end()); + + return sorted; +} + } diff --git a/test-cycle-error.sh b/test-cycle-error.sh new file mode 100755 index 000000000000..3419fa9bc2ab --- /dev/null +++ b/test-cycle-error.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +export TERM=dumb + + +if ! [ -f outputs/out/libexec/nix/build-remote ]; then + mkdir -p outputs/out/libexec/nix/ + ( + cd outputs/out/libexec/nix/ + ln -s ../../bin/nix build-remote + ) +fi + +make tests/multiple-outputs.sh.test + +exit 0 + +# src/nix/nix == outputs/out/bin/nix ? + +# options for nix +o=() +o+=(--impure --builders '' --expr 'with import { }; (pkgs.stdenv.mkDerivation { name = "cycle"; outputs = [ "a" "b" "c" ]; builder = (pkgs.writeShellApplication{ name = "builder.sh"; runtimeInputs = [ pkgs.busybox ]; checkPhase = ":"; text = "mkdir $a $b $c; echo $a > $b/a; echo $b > $c/b; echo $c > $a/c"; }) + "/bin/builder.sh"; }).a') + +#if false; then +if true; then + #if true; then + if false; then + [ -e ./outputs/out/bin/nix-build ] || ln -s nix ./outputs/out/bin/nix-build + ./outputs/out/bin/nix-build "${o[@]}" + else + ./outputs/out/bin/nix --extra-experimental-features nix-command build "${o[@]}" + fi +else + ./src/nix/nix --extra-experimental-features nix-command build "${o[@]}" +fi diff --git a/tests/multiple-outputs.nix b/tests/multiple-outputs.nix index 624a5dadea76..637018fa35a0 100644 --- a/tests/multiple-outputs.nix +++ b/tests/multiple-outputs.nix @@ -68,15 +68,35 @@ rec { ''; }; + # cycle: a -> c -> b -> a cyclic = (mkDerivation { name = "cyclic-outputs"; outputs = [ "a" "b" "c" ]; builder = builtins.toFile "builder.sh" '' - mkdir $a $b $c - echo $a > $b/foo - echo $b > $c/bar - echo $c > $a/baz + mkdir -p $a/opt $b/opt $c/opt + + # a b c a + ab=$a/a-to-b + bc=$b/b-to-c + ca=$c/c-to-a + echo $bc > $ab + echo $ca > $bc + echo $ab > $ca + + # a c b a + ac=$a/a-to-c.2 + cb=$c/c-to-b.2 + ba=$b/b-to-a.2 + echo $cb > $ac + echo $ba > $cb + echo $ac > $ba + + # a b c + ab=$a/a-to-b.3 + bc=$b/b-to-c.3 + echo $bc > $ab + echo ___ > $bc ''; }).a; diff --git a/tests/multiple-outputs.sh b/tests/multiple-outputs.sh index 0d45ad35bb5e..76e44b74ae44 100644 --- a/tests/multiple-outputs.sh +++ b/tests/multiple-outputs.sh @@ -4,6 +4,19 @@ clearStore rm -f $TEST_ROOT/result* +echo "test cyclic (verbose) ..." +# fixme: why need --builders '' +nix-build multiple-outputs.nix -A cyclic --no-out-link -vvvv || true +echo test cyclic done + +echo "test cyclic (not verbose) ..." +# fixme: why need --builders '' +nix-build multiple-outputs.nix -A cyclic --no-out-link || true +echo test cyclic done +# debug: run only this test +# TODO run all tests +exit 0 + # Test whether the output names match our expectations outPath=$(nix-instantiate multiple-outputs.nix --eval -A nameCheck.out.outPath) [ "$(echo "$outPath" | sed -E 's_^".*/[^-/]*-([^/]*)"$_\1_')" = "multiple-outputs-a" ]