* Re-enable support for substitutes in the normaliser.
* A better substitute mechanism. Instead of generating a store expression for each store path for which we have a substitute, we can have a single store expression that builds a generic program that is invoked to build the desired store path, which is passed as an argument. This means that operations like `nix-pull' only produce O(1) files instead of O(N) files in the store when registering N substitutes. (It consumes O(N) database storage, of course, but that's not a performance problem). * Added a test for the substitute mechanism. * `nix-store --substitute' reads the substitutes from standard input, instead of from the command line. This prevents us from running into the kernel's limit on command line length.
This commit is contained in:
		
							parent
							
								
									bafb2357d1
								
							
						
					
					
						commit
						112ee89501
					
				
					 10 changed files with 444 additions and 108 deletions
				
			
		|  | @ -157,6 +157,26 @@ public: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | //////////////////////////////////////////////////////////////////////
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | void killChild(pid_t pid) | ||||||
|  | { | ||||||
|  |     /* Send a KILL signal to every process in the child process group
 | ||||||
|  |        (which hopefully includes *all* its children). */ | ||||||
|  |     if (kill(-pid, SIGKILL) != 0) | ||||||
|  |         printMsg(lvlError, format("killing process %1%") % pid); | ||||||
|  |     else { | ||||||
|  |         /* Wait until the child dies, disregarding the exit status. */ | ||||||
|  |         int status; | ||||||
|  |         while (waitpid(pid, &status, 0) == -1) | ||||||
|  |             if (errno != EINTR) printMsg(lvlError, | ||||||
|  |                 format("waiting for process %1%") % pid); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| //////////////////////////////////////////////////////////////////////
 | //////////////////////////////////////////////////////////////////////
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -197,9 +217,6 @@ private: | ||||||
|      |      | ||||||
|     /* The remainder is state held during the build. */ |     /* The remainder is state held during the build. */ | ||||||
| 
 | 
 | ||||||
|     /* Whether it's being built by a hook or by ourselves. */ |  | ||||||
|     bool inHook; |  | ||||||
| 
 |  | ||||||
|     /* Locks on the output paths. */ |     /* Locks on the output paths. */ | ||||||
|     PathLocks outputLocks; |     PathLocks outputLocks; | ||||||
| 
 | 
 | ||||||
|  | @ -305,20 +322,7 @@ NormalisationGoal::~NormalisationGoal() | ||||||
|     if (pid != -1) { |     if (pid != -1) { | ||||||
|         printMsg(lvlError, format("killing child process %1% (%2%)") |         printMsg(lvlError, format("killing child process %1% (%2%)") | ||||||
|             % pid % nePath); |             % pid % nePath); | ||||||
| 
 |         killChild(pid); | ||||||
|         /* Send a KILL signal to every process in the child
 |  | ||||||
|            process group (which hopefully includes *all* its |  | ||||||
|            children). */ |  | ||||||
|         if (kill(-pid, SIGKILL) != 0) |  | ||||||
|             printMsg(lvlError, format("killing process %1%") % pid); |  | ||||||
|         else { |  | ||||||
|             /* Wait until the child dies, disregarding the exit
 |  | ||||||
|                status. */ |  | ||||||
|             int status; |  | ||||||
|             while (waitpid(pid, &status, 0) == -1) |  | ||||||
|                 if (errno != EINTR) printMsg(lvlError, |  | ||||||
|                     format("waiting for process %1%") % pid); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|  | @ -488,7 +492,7 @@ void NormalisationGoal::buildDone() | ||||||
|     /* Close the log file. */ |     /* Close the log file. */ | ||||||
|     fdLogFile.close(); |     fdLogFile.close(); | ||||||
| 
 | 
 | ||||||
|     debug(format("builder process %1% finished") % pid); |     debug(format("builder process for `%1%' finished") % nePath); | ||||||
| 
 | 
 | ||||||
|     /* Check the exit status. */ |     /* Check the exit status. */ | ||||||
|     if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { |     if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { | ||||||
|  | @ -501,8 +505,9 @@ void NormalisationGoal::buildDone() | ||||||
|                 % nePath % WTERMSIG(status)); |                 % nePath % WTERMSIG(status)); | ||||||
|         else |         else | ||||||
|             throw Error(format("builder for `%1%' failed died abnormally") % nePath); |             throw Error(format("builder for `%1%' failed died abnormally") % nePath); | ||||||
|     } else |     } | ||||||
|         deleteTmpDir(true); |      | ||||||
|  |     deleteTmpDir(true); | ||||||
| 
 | 
 | ||||||
|     /* Compute a closure store expression, and register it as our
 |     /* Compute a closure store expression, and register it as our
 | ||||||
|        successor. */ |        successor. */ | ||||||
|  | @ -629,6 +634,7 @@ NormalisationGoal::HookReply NormalisationGoal::tryBuildHook() | ||||||
|         /* Acquire locks and such.  If we then see that there now is a
 |         /* Acquire locks and such.  If we then see that there now is a
 | ||||||
|            successor, we're done. */ |            successor, we're done. */ | ||||||
|         if (!prepareBuild()) { |         if (!prepareBuild()) { | ||||||
|  |             /* Tell the hook to exit. */ | ||||||
|             writeLine(toHook.writeSide, "cancel"); |             writeLine(toHook.writeSide, "cancel"); | ||||||
|             terminateBuildHook(); |             terminateBuildHook(); | ||||||
|             return rpDone; |             return rpDone; | ||||||
|  | @ -664,10 +670,9 @@ NormalisationGoal::HookReply NormalisationGoal::tryBuildHook() | ||||||
|             s += i->first + " " + i->second + "\n"; |             s += i->first + " " + i->second + "\n"; | ||||||
|         writeStringToFile(successorsListFN, s); |         writeStringToFile(successorsListFN, s); | ||||||
| 
 | 
 | ||||||
|  |         /* Tell the hook to proceed. */  | ||||||
|         writeLine(toHook.writeSide, "okay"); |         writeLine(toHook.writeSide, "okay"); | ||||||
| 
 | 
 | ||||||
|         inHook = true; |  | ||||||
|      |  | ||||||
|         return rpAccept; |         return rpAccept; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -1187,6 +1192,21 @@ private: | ||||||
|     /* The store path that should be realised through a substitute. */ |     /* The store path that should be realised through a substitute. */ | ||||||
|     Path storePath; |     Path storePath; | ||||||
| 
 | 
 | ||||||
|  |     /* The remaining substitutes for this path. */ | ||||||
|  |     Substitutes subs; | ||||||
|  | 
 | ||||||
|  |     /* The current substitute. */ | ||||||
|  |     Substitute sub; | ||||||
|  | 
 | ||||||
|  |     /* The normal form of the substitute store expression. */ | ||||||
|  |     Path nfSub; | ||||||
|  | 
 | ||||||
|  |     /* Pipe for the substitute's standard output/error. */ | ||||||
|  |     Pipe logPipe; | ||||||
|  | 
 | ||||||
|  |     /* The process ID of the builder. */ | ||||||
|  |     pid_t pid; | ||||||
|  | 
 | ||||||
|     typedef void (SubstitutionGoal::*GoalState)(); |     typedef void (SubstitutionGoal::*GoalState)(); | ||||||
|     GoalState state; |     GoalState state; | ||||||
| 
 | 
 | ||||||
|  | @ -1198,6 +1218,10 @@ public: | ||||||
| 
 | 
 | ||||||
|     /* The states. */ |     /* The states. */ | ||||||
|     void init(); |     void init(); | ||||||
|  |     void tryNext(); | ||||||
|  |     void exprNormalised(); | ||||||
|  |     void exprRealised(); | ||||||
|  |     void finished(); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -1205,12 +1229,19 @@ SubstitutionGoal::SubstitutionGoal(const Path & _storePath, Worker & _worker) | ||||||
|     : Goal(_worker) |     : Goal(_worker) | ||||||
| { | { | ||||||
|     storePath = _storePath; |     storePath = _storePath; | ||||||
|  |     pid = -1; | ||||||
|     state = &SubstitutionGoal::init; |     state = &SubstitutionGoal::init; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| SubstitutionGoal::~SubstitutionGoal() | SubstitutionGoal::~SubstitutionGoal() | ||||||
| { | { | ||||||
|  |     /* !!! turn this into a destructor for pids */ | ||||||
|  |     if (pid != -1) { | ||||||
|  |         printMsg(lvlError, format("killing child process %1% (%2%)") | ||||||
|  |             % pid % storePath); | ||||||
|  |         killChild(pid); | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -1230,7 +1261,171 @@ void SubstitutionGoal::init() | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     abort(); |     /* Otherwise, get the substitutes. */ | ||||||
|  |     subs = querySubstitutes(storePath); | ||||||
|  | 
 | ||||||
|  |     /* Try the first one. */ | ||||||
|  |     tryNext(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | void SubstitutionGoal::tryNext() | ||||||
|  | { | ||||||
|  |     debug(format("trying next substitute of `%1%'") % storePath); | ||||||
|  | 
 | ||||||
|  |     if (subs.size() == 0) throw Error( | ||||||
|  |         format("path `%1%' is required, but it has no (remaining) substitutes") | ||||||
|  |             % storePath); | ||||||
|  |     sub = subs.front(); | ||||||
|  |     subs.pop_front(); | ||||||
|  | 
 | ||||||
|  |     /* Normalise the substitute store expression. */ | ||||||
|  |     worker.addNormalisationGoal(sub.storeExpr, shared_from_this()); | ||||||
|  |     nrWaitees = 1; | ||||||
|  | 
 | ||||||
|  |     state = &SubstitutionGoal::exprNormalised; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | void SubstitutionGoal::exprNormalised() | ||||||
|  | { | ||||||
|  |     debug(format("store expr normalised of `%1%'") % storePath); | ||||||
|  | 
 | ||||||
|  |     /* Realise the substitute store expression. */ | ||||||
|  |     if (!querySuccessor(sub.storeExpr, nfSub)) | ||||||
|  |         nfSub = sub.storeExpr; | ||||||
|  |     worker.addRealisationGoal(nfSub, shared_from_this()); | ||||||
|  |     nrWaitees = 1; | ||||||
|  | 
 | ||||||
|  |     state = &SubstitutionGoal::exprRealised; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | void SubstitutionGoal::exprRealised() | ||||||
|  | { | ||||||
|  |     debug(format("store expr realised of `%1%'") % storePath); | ||||||
|  | 
 | ||||||
|  |     /* What's the substitute program? */ | ||||||
|  |     StoreExpr expr = storeExprFromPath(nfSub); | ||||||
|  |     assert(expr.type == StoreExpr::neClosure); | ||||||
|  |     assert(!expr.closure.roots.empty()); | ||||||
|  |     Path program = | ||||||
|  |         canonPath(*expr.closure.roots.begin() + "/" + sub.program); | ||||||
|  | 
 | ||||||
|  |     printMsg(lvlChatty, format("executing substitute `%1%'") % program); | ||||||
|  | 
 | ||||||
|  |     logPipe.create(); | ||||||
|  | 
 | ||||||
|  |     /* Fork the substitute program. */ | ||||||
|  |     switch (pid = fork()) { | ||||||
|  |          | ||||||
|  |     case -1: | ||||||
|  |         throw SysError("unable to fork"); | ||||||
|  | 
 | ||||||
|  |     case 0: | ||||||
|  |         try { /* child */ | ||||||
|  | 
 | ||||||
|  |             logPipe.readSide.close(); | ||||||
|  | 
 | ||||||
|  |             /* !!! close other handles */ | ||||||
|  | 
 | ||||||
|  |             /* !!! this is cut & paste - fix */ | ||||||
|  | 
 | ||||||
|  |             /* Put the child in a separate process group so that it
 | ||||||
|  |                doesn't receive terminal signals. */ | ||||||
|  |             if (setpgid(0, 0) == -1) | ||||||
|  |                 throw SysError(format("setting process group")); | ||||||
|  |              | ||||||
|  |             /* Dup the write side of the logger pipe into stderr. */ | ||||||
|  |             if (dup2(logPipe.writeSide, STDERR_FILENO) == -1) | ||||||
|  |                 throw SysError("cannot pipe standard error into log file"); | ||||||
|  |             logPipe.readSide.close(); | ||||||
|  |              | ||||||
|  |             /* Dup stderr to stdin. */ | ||||||
|  |             if (dup2(STDERR_FILENO, STDOUT_FILENO) == -1) | ||||||
|  |                 throw SysError("cannot dup stderr into stdout"); | ||||||
|  | 
 | ||||||
|  |             /* Reroute stdin to /dev/null. */ | ||||||
|  |             int fdDevNull = open(pathNullDevice.c_str(), O_RDWR); | ||||||
|  |             if (fdDevNull == -1) | ||||||
|  |                 throw SysError(format("cannot open `%1%'") % pathNullDevice); | ||||||
|  |             if (dup2(fdDevNull, STDIN_FILENO) == -1) | ||||||
|  |                 throw SysError("cannot dup null device into stdin"); | ||||||
|  | 
 | ||||||
|  |             /* Fill in the arguments.  !!! cut & paste */ | ||||||
|  |             const char * argArr[sub.args.size() + 3]; | ||||||
|  |             const char * * p = argArr; | ||||||
|  |             string progName = baseNameOf(program); | ||||||
|  |             *p++ = progName.c_str(); | ||||||
|  |             *p++ = storePath.c_str(); | ||||||
|  |             for (Strings::const_iterator i = sub.args.begin(); | ||||||
|  |                  i != sub.args.end(); i++) | ||||||
|  |                 *p++ = i->c_str(); | ||||||
|  |             *p = 0; | ||||||
|  | 
 | ||||||
|  |             execv(program.c_str(), (char * *) argArr); | ||||||
|  |              | ||||||
|  |             throw SysError(format("executing `%1%'") % program); | ||||||
|  |              | ||||||
|  |         } catch (exception & e) { | ||||||
|  |             cerr << format("substitute error: %1%\n") % e.what(); | ||||||
|  |         } | ||||||
|  |         _exit(1); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /* parent */ | ||||||
|  |     logPipe.writeSide.close(); | ||||||
|  |     worker.childStarted(shared_from_this(), | ||||||
|  |         pid, logPipe.readSide, false); | ||||||
|  | 
 | ||||||
|  |     state = &SubstitutionGoal::finished; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | void SubstitutionGoal::finished() | ||||||
|  | { | ||||||
|  |     debug(format("substitute finished of `%1%'") % storePath); | ||||||
|  | 
 | ||||||
|  |     int status; | ||||||
|  | 
 | ||||||
|  |     /* Since we got an EOF on the logger pipe, the substitute is
 | ||||||
|  |        presumed to have terminated.  */ | ||||||
|  |     /* !!! this could block! */ | ||||||
|  |     if (waitpid(pid, &status, 0) != pid) | ||||||
|  |         throw SysError(format("substitute for `%1%' should have terminated") | ||||||
|  |             % storePath); | ||||||
|  | 
 | ||||||
|  |     /* So the child is gone now. */ | ||||||
|  |     worker.childTerminated(pid); | ||||||
|  |     pid = -1; | ||||||
|  | 
 | ||||||
|  |     /* Close the read side of the logger pipe. */ | ||||||
|  |     logPipe.readSide.close(); | ||||||
|  | 
 | ||||||
|  |     debug(format("substitute for `%1%' finished") % storePath); | ||||||
|  | 
 | ||||||
|  |     /* Check the exit status. */ | ||||||
|  |     /* !!! cut & paste */ | ||||||
|  |     if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { | ||||||
|  |         if (WIFEXITED(status)) | ||||||
|  |             throw Error(format("builder for `%1%' failed with exit code %2%") | ||||||
|  |                 % storePath % WEXITSTATUS(status)); | ||||||
|  |         else if (WIFSIGNALED(status)) | ||||||
|  |             throw Error(format("builder for `%1%' failed due to signal %2%") | ||||||
|  |                 % storePath % WTERMSIG(status)); | ||||||
|  |         else | ||||||
|  |             throw Error(format("builder for `%1%' failed died abnormally") % storePath); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!pathExists(storePath)) | ||||||
|  |         throw Error(format("substitute did not produce path `%1%'") % storePath); | ||||||
|  | 
 | ||||||
|  |     Transaction txn; | ||||||
|  |     createStoreTransaction(txn); | ||||||
|  |     registerValidPath(txn, storePath); | ||||||
|  |     txn.commit(); | ||||||
|  | 
 | ||||||
|  |     amDone(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -41,10 +41,12 @@ static TableId dbSuccessors; | ||||||
| */ | */ | ||||||
| static TableId dbSuccessorsRev; | static TableId dbSuccessorsRev; | ||||||
| 
 | 
 | ||||||
| /* dbSubstitutes :: Path -> [Path]
 | /* dbSubstitutes :: Path -> [(Path, Path, [string])]
 | ||||||
| 
 | 
 | ||||||
|    Each pair $(p, [ps])$ tells Nix that it can realise any of the |    Each pair $(p, subs)$ tells Nix that it can use any of the | ||||||
|    Nix expressions stored at paths $ps$ to produce a path $p$. |    substitutes in $subs$ to build path $p$.  Each substitute is a | ||||||
|  |    tuple $(storeExpr, program, args)$ (see the type `Substitute' in | ||||||
|  |    `store.hh'). | ||||||
| 
 | 
 | ||||||
|    The main purpose of this is for distributed caching of derivates. |    The main purpose of this is for distributed caching of derivates. | ||||||
|    One system can compute a derivate and put it on a website (as a Nix |    One system can compute a derivate and put it on a website (as a Nix | ||||||
|  | @ -56,11 +58,20 @@ static TableId dbSubstitutes; | ||||||
| 
 | 
 | ||||||
| /* dbSubstitutesRev :: Path -> [Path]
 | /* dbSubstitutesRev :: Path -> [Path]
 | ||||||
| 
 | 
 | ||||||
|    The reverse mapping of dbSubstitutes. |    The reverse mapping of dbSubstitutes; it maps store expressions | ||||||
|  |    back to the paths for which they are substitutes. | ||||||
| */ | */ | ||||||
| static TableId dbSubstitutesRev; | static TableId dbSubstitutesRev; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | bool Substitute::operator == (const Substitute & sub) | ||||||
|  | { | ||||||
|  |     return storeExpr == sub.storeExpr | ||||||
|  |         && program == sub.program | ||||||
|  |         && args == sub.args; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| void openDB() | void openDB() | ||||||
| { | { | ||||||
|     nixDB.open(nixDBPath); |     nixDB.open(nixDBPath); | ||||||
|  | @ -241,44 +252,89 @@ Paths queryPredecessors(const Path & sucPath) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| void registerSubstitute(const Path & srcPath, const Path & subPath) | static Substitutes readSubstitutes(const Transaction & txn, | ||||||
|  |     const Path & srcPath) | ||||||
|  | { | ||||||
|  |     Strings ss; | ||||||
|  |     nixDB.queryStrings(txn, dbSubstitutes, srcPath, ss); | ||||||
|  | 
 | ||||||
|  |     Substitutes subs; | ||||||
|  |      | ||||||
|  |     for (Strings::iterator i = ss.begin(); i != ss.end(); ++i) { | ||||||
|  |         if (i->size() < 4 || (*i)[3] != 0) { | ||||||
|  |             /* Old-style substitute.  !!! remove this code
 | ||||||
|  |                eventually? */ | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |         Strings ss2 = unpackStrings(*i); | ||||||
|  |         if (ss2.size() != 3) throw Error("malformed substitute"); | ||||||
|  |         Strings::iterator j = ss2.begin(); | ||||||
|  |         Substitute sub; | ||||||
|  |         sub.storeExpr = *j++; | ||||||
|  |         sub.program = *j++; | ||||||
|  |         sub.args = unpackStrings(*j++); | ||||||
|  |         subs.push_back(sub); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return subs; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | static void writeSubstitutes(const Transaction & txn, | ||||||
|  |     const Path & srcPath, const Substitutes & subs) | ||||||
|  | { | ||||||
|  |     Strings ss; | ||||||
|  | 
 | ||||||
|  |     for (Substitutes::const_iterator i = subs.begin(); | ||||||
|  |          i != subs.end(); ++i) | ||||||
|  |     { | ||||||
|  |         Strings ss2; | ||||||
|  |         ss2.push_back(i->storeExpr); | ||||||
|  |         ss2.push_back(i->program); | ||||||
|  |         ss2.push_back(packStrings(i->args)); | ||||||
|  |         ss.push_back(packStrings(ss2)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (ss.size() == 0) | ||||||
|  |         nixDB.delPair(txn, dbSubstitutes, srcPath); | ||||||
|  |     else | ||||||
|  |         nixDB.setStrings(txn, dbSubstitutes, srcPath, ss); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | void registerSubstitute(const Path & srcPath, | ||||||
|  |     const Substitute & sub) | ||||||
| { | { | ||||||
|     assertStorePath(srcPath); |     assertStorePath(srcPath); | ||||||
|     assertStorePath(subPath); |     assertStorePath(sub.storeExpr); | ||||||
|      |  | ||||||
|     if (!isValidPathTxn(subPath, noTxn)) throw Error( |  | ||||||
| 	format("path `%1%' cannot be a substitute, since it is not valid") |  | ||||||
| 	% subPath); |  | ||||||
|      |      | ||||||
|     Transaction txn(nixDB); |     Transaction txn(nixDB); | ||||||
| 
 | 
 | ||||||
|     Paths subs; |     Substitutes subs = readSubstitutes(txn, srcPath); | ||||||
|     nixDB.queryStrings(txn, dbSubstitutes, srcPath, subs); |  | ||||||
| 
 | 
 | ||||||
|     if (find(subs.begin(), subs.end(), subPath) != subs.end()) { |     if (find(subs.begin(), subs.end(), sub) != subs.end()) { | ||||||
|         /* Nothing to do if the substitute is already known. */ |         /* Nothing to do if the substitute is already known. */ | ||||||
|         txn.abort(); |         txn.abort(); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|     subs.push_front(subPath); /* new substitutes take precedence */ |     subs.push_front(sub); /* new substitutes take precedence */ | ||||||
|  | 
 | ||||||
|  |     writeSubstitutes(txn, srcPath, subs); | ||||||
|      |      | ||||||
|     Paths revs; |     Paths revs; | ||||||
|     nixDB.queryStrings(txn, dbSubstitutesRev, subPath, revs); |     nixDB.queryStrings(txn, dbSubstitutesRev, sub.storeExpr, revs); | ||||||
|     if (find(revs.begin(), revs.end(), srcPath) == revs.end()) |     if (find(revs.begin(), revs.end(), srcPath) == revs.end()) | ||||||
|         revs.push_back(srcPath); |         revs.push_back(srcPath); | ||||||
|      |      | ||||||
|     nixDB.setStrings(txn, dbSubstitutes, srcPath, subs); |     nixDB.setStrings(txn, dbSubstitutesRev, sub.storeExpr, revs); | ||||||
|     nixDB.setStrings(txn, dbSubstitutesRev, subPath, revs); |  | ||||||
| 
 | 
 | ||||||
|     txn.commit(); |     txn.commit(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| Paths querySubstitutes(const Path & srcPath) | Substitutes querySubstitutes(const Path & srcPath) | ||||||
| { | { | ||||||
|     Paths subPaths; |     return readSubstitutes(noTxn, srcPath); | ||||||
|     nixDB.queryStrings(noTxn, dbSubstitutes, srcPath, subPaths); |  | ||||||
|     return subPaths; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -291,16 +347,6 @@ void registerValidPath(const Transaction & txn, const Path & _path) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| static void setOrClearStrings(Transaction & txn, |  | ||||||
|     TableId table, const string & key, const Strings & value) |  | ||||||
| { |  | ||||||
|     if (value.size() > 0) |  | ||||||
|         nixDB.setStrings(txn, table, key, value); |  | ||||||
|     else |  | ||||||
|         nixDB.delPair(txn, table, key); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| static void invalidatePath(const Path & path, Transaction & txn) | static void invalidatePath(const Path & path, Transaction & txn) | ||||||
| { | { | ||||||
|     debug(format("unregistering path `%1%'") % path); |     debug(format("unregistering path `%1%'") % path); | ||||||
|  | @ -319,12 +365,15 @@ static void invalidatePath(const Path & path, Transaction & txn) | ||||||
|     revs.clear(); |     revs.clear(); | ||||||
|     nixDB.queryStrings(txn, dbSubstitutesRev, path, revs); |     nixDB.queryStrings(txn, dbSubstitutesRev, path, revs); | ||||||
|     for (Paths::iterator i = revs.begin(); i != revs.end(); ++i) { |     for (Paths::iterator i = revs.begin(); i != revs.end(); ++i) { | ||||||
|         Paths subs; |         Substitutes subs = readSubstitutes(txn, *i), subs2; | ||||||
|         nixDB.queryStrings(txn, dbSubstitutes, *i, subs); |         bool found = false; | ||||||
|         if (find(subs.begin(), subs.end(), path) == subs.end()) |         for (Substitutes::iterator j = subs.begin(); j != subs.end(); ++j) | ||||||
|             throw Error("integrity error in substitutes mapping"); |             if (j->storeExpr != path) | ||||||
|         subs.remove(path); |                 subs2.push_back(*j); | ||||||
|         setOrClearStrings(txn, dbSubstitutes, *i, subs); |             else | ||||||
|  |                 found = true; | ||||||
|  |         if (!found) throw Error("integrity error in substitutes mapping"); | ||||||
|  |         writeSubstitutes(txn, *i, subs); | ||||||
| 
 | 
 | ||||||
| 	/* If path *i now has no substitutes left, and is not valid,
 | 	/* If path *i now has no substitutes left, and is not valid,
 | ||||||
| 	   then it too should be invalidated.  This is because it may | 	   then it too should be invalidated.  This is because it may | ||||||
|  | @ -438,28 +487,28 @@ void verifyStore() | ||||||
| 
 | 
 | ||||||
|     /* Check that the values of the substitute mappings are valid
 |     /* Check that the values of the substitute mappings are valid
 | ||||||
|        paths. */  |        paths. */  | ||||||
|     Paths subs; |     Paths subKeys; | ||||||
|     nixDB.enumTable(txn, dbSubstitutes, subs); |     nixDB.enumTable(txn, dbSubstitutes, subKeys); | ||||||
|     for (Paths::iterator i = subs.begin(); i != subs.end(); ++i) { |     for (Paths::iterator i = subKeys.begin(); i != subKeys.end(); ++i) { | ||||||
|         Paths subPaths, subPaths2; |         Substitutes subs = readSubstitutes(txn, *i), subs2; | ||||||
|         nixDB.queryStrings(txn, dbSubstitutes, *i, subPaths); |         for (Substitutes::iterator j = subs.begin(); j != subs.end(); ++j) | ||||||
|         for (Paths::iterator j = subPaths.begin(); j != subPaths.end(); ++j) |             if (validPaths.find(j->storeExpr) == validPaths.end()) | ||||||
|             if (validPaths.find(*j) == validPaths.end()) |  | ||||||
|                 printMsg(lvlError, |                 printMsg(lvlError, | ||||||
|                     format("found substitute mapping to non-existent path `%1%'") % *j); |                     format("found substitute mapping to non-existent path `%1%'") | ||||||
|  |                     % j->storeExpr); | ||||||
|             else |             else | ||||||
|                 subPaths2.push_back(*j); |                 subs2.push_back(*j); | ||||||
|         if (subPaths.size() != subPaths2.size()) |         if (subs.size() != subs2.size()) | ||||||
|             setOrClearStrings(txn, dbSubstitutes, *i, subPaths2); |             writeSubstitutes(txn, *i, subs2); | ||||||
| 	if (subPaths2.size() > 0) | 	if (subs2.size() > 0) | ||||||
| 	    usablePaths.insert(*i); | 	    usablePaths.insert(*i); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /* Check that the keys of the reverse substitute mappings are
 |     /* Check that the keys of the reverse substitute mappings are
 | ||||||
|        valid paths. */  |        valid paths. */  | ||||||
|     Paths rsubs; |     Paths rsubKeys; | ||||||
|     nixDB.enumTable(txn, dbSubstitutesRev, rsubs); |     nixDB.enumTable(txn, dbSubstitutesRev, rsubKeys); | ||||||
|     for (Paths::iterator i = rsubs.begin(); i != rsubs.end(); ++i) { |     for (Paths::iterator i = rsubKeys.begin(); i != rsubKeys.end(); ++i) { | ||||||
|         if (validPaths.find(*i) == validPaths.end()) { |         if (validPaths.find(*i) == validPaths.end()) { | ||||||
|             printMsg(lvlError, |             printMsg(lvlError, | ||||||
|                 format("found reverse substitute mapping for non-existent path `%1%'") % *i); |                 format("found reverse substitute mapping for non-existent path `%1%'") % *i); | ||||||
|  | @ -469,9 +518,9 @@ void verifyStore() | ||||||
| 
 | 
 | ||||||
|     /* Check that the values of the successor mappings are usable
 |     /* Check that the values of the successor mappings are usable
 | ||||||
|        paths. */  |        paths. */  | ||||||
|     Paths sucs; |     Paths sucKeys; | ||||||
|     nixDB.enumTable(txn, dbSuccessors, sucs); |     nixDB.enumTable(txn, dbSuccessors, sucKeys); | ||||||
|     for (Paths::iterator i = sucs.begin(); i != sucs.end(); ++i) { |     for (Paths::iterator i = sucKeys.begin(); i != sucKeys.end(); ++i) { | ||||||
|         /* Note that *i itself does not have to be valid, just its
 |         /* Note that *i itself does not have to be valid, just its
 | ||||||
|            successor. */ |            successor. */ | ||||||
|         Path sucPath; |         Path sucPath; | ||||||
|  | @ -486,9 +535,9 @@ void verifyStore() | ||||||
| 
 | 
 | ||||||
|     /* Check that the keys of the reverse successor mappings are valid
 |     /* Check that the keys of the reverse successor mappings are valid
 | ||||||
|        paths. */  |        paths. */  | ||||||
|     Paths rsucs; |     Paths rsucKeys; | ||||||
|     nixDB.enumTable(txn, dbSuccessorsRev, rsucs); |     nixDB.enumTable(txn, dbSuccessorsRev, rsucKeys); | ||||||
|     for (Paths::iterator i = rsucs.begin(); i != rsucs.end(); ++i) { |     for (Paths::iterator i = rsucKeys.begin(); i != rsucKeys.end(); ++i) { | ||||||
|         if (usablePaths.find(*i) == usablePaths.end()) { |         if (usablePaths.find(*i) == usablePaths.end()) { | ||||||
|             printMsg(lvlError, |             printMsg(lvlError, | ||||||
|                 format("found reverse successor mapping for non-existent path `%1%'") % *i); |                 format("found reverse successor mapping for non-existent path `%1%'") % *i); | ||||||
|  |  | ||||||
|  | @ -9,6 +9,29 @@ | ||||||
| using namespace std; | using namespace std; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | /* A substitute is a program invocation that constructs some store
 | ||||||
|  |    path (typically by fetching it from somewhere, e.g., from the | ||||||
|  |    network). */ | ||||||
|  | struct Substitute | ||||||
|  | { | ||||||
|  |     /* Store expression to be normalised and realised in order to
 | ||||||
|  |        obtain `program'. */ | ||||||
|  |     Path storeExpr; | ||||||
|  | 
 | ||||||
|  |     /* Program to be executed to create the store path.  Must be in
 | ||||||
|  |        the output path of `storeExpr'. */ | ||||||
|  |     Path program; | ||||||
|  | 
 | ||||||
|  |     /* Extra arguments to be passed to the program (the first argument
 | ||||||
|  |        is the store path to be substituted). */ | ||||||
|  |     Strings args; | ||||||
|  | 
 | ||||||
|  |     bool operator == (const Substitute & sub); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | typedef list<Substitute> Substitutes; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| /* Open the database environment. */ | /* Open the database environment. */ | ||||||
| void openDB(); | void openDB(); | ||||||
| 
 | 
 | ||||||
|  | @ -40,10 +63,11 @@ bool querySuccessor(const Path & srcPath, Path & sucPath); | ||||||
| Paths queryPredecessors(const Path & sucPath); | Paths queryPredecessors(const Path & sucPath); | ||||||
| 
 | 
 | ||||||
| /* Register a substitute. */ | /* Register a substitute. */ | ||||||
| void registerSubstitute(const Path & srcPath, const Path & subPath); | void registerSubstitute(const Path & srcPath, | ||||||
|  |     const Substitute & sub); | ||||||
| 
 | 
 | ||||||
| /* Return the substitutes expression for the given path. */ | /* Return the substitutes expression for the given path. */ | ||||||
| Paths querySubstitutes(const Path & srcPath); | Substitutes querySubstitutes(const Path & srcPath); | ||||||
| 
 | 
 | ||||||
| /* Register the validity of a path. */ | /* Register the validity of a path. */ | ||||||
| void registerValidPath(const Transaction & txn, const Path & path); | void registerValidPath(const Transaction & txn, const Path & path); | ||||||
|  |  | ||||||
|  | @ -499,7 +499,7 @@ static void opQuery(Globals & globals, | ||||||
|                 installedPaths.insert(i->second.outPath); |                 installedPaths.insert(i->second.outPath); | ||||||
|              |              | ||||||
|             for (DrvInfoList::iterator i = drvs2.begin(); i != drvs2.end(); ++i) { |             for (DrvInfoList::iterator i = drvs2.begin(); i != drvs2.end(); ++i) { | ||||||
|                 Paths subs = querySubstitutes(i->drvPath); |                 Substitutes subs = querySubstitutes(i->drvPath); | ||||||
|                 cout << format("%1%%2%%3% %4%\n") |                 cout << format("%1%%2%%3% %4%\n") | ||||||
|                     % (installedPaths.find(i->outPath) |                     % (installedPaths.find(i->outPath) | ||||||
|                         != installedPaths.end() ? 'I' : '-') |                         != installedPaths.end() ? 'I' : '-') | ||||||
|  |  | ||||||
|  | @ -18,12 +18,6 @@ void printHelp() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| static Path checkPath(const Path & arg) |  | ||||||
| { |  | ||||||
|     return arg; /* !!! check that arg is in the store */ |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| /* Realise paths from the given store expressions. */ | /* Realise paths from the given store expressions. */ | ||||||
| static void opRealise(Strings opFlags, Strings opArgs) | static void opRealise(Strings opFlags, Strings opArgs) | ||||||
| { | { | ||||||
|  | @ -32,7 +26,7 @@ static void opRealise(Strings opFlags, Strings opArgs) | ||||||
|     for (Strings::iterator i = opArgs.begin(); |     for (Strings::iterator i = opArgs.begin(); | ||||||
|          i != opArgs.end(); i++) |          i != opArgs.end(); i++) | ||||||
|     { |     { | ||||||
|         Path nfPath = normaliseStoreExpr(checkPath(*i)); |         Path nfPath = normaliseStoreExpr(*i); | ||||||
|         realiseClosure(nfPath); |         realiseClosure(nfPath); | ||||||
|         cout << format("%1%\n") % (string) nfPath; |         cout << format("%1%\n") % (string) nfPath; | ||||||
|     } |     } | ||||||
|  | @ -46,7 +40,7 @@ static void opDelete(Strings opFlags, Strings opArgs) | ||||||
| 
 | 
 | ||||||
|     for (Strings::iterator it = opArgs.begin(); |     for (Strings::iterator it = opArgs.begin(); | ||||||
|          it != opArgs.end(); it++) |          it != opArgs.end(); it++) | ||||||
|         deleteFromStore(checkPath(*it)); |         deleteFromStore(*it); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -101,7 +95,7 @@ static void opQuery(Strings opFlags, Strings opArgs) | ||||||
|                  i != opArgs.end(); i++) |                  i != opArgs.end(); i++) | ||||||
|             { |             { | ||||||
|                 StringSet paths = storeExprRoots( |                 StringSet paths = storeExprRoots( | ||||||
|                     maybeNormalise(checkPath(*i), normalise, realise)); |                     maybeNormalise(*i, normalise, realise)); | ||||||
|                 for (StringSet::iterator j = paths.begin();  |                 for (StringSet::iterator j = paths.begin();  | ||||||
|                      j != paths.end(); j++) |                      j != paths.end(); j++) | ||||||
|                     cout << format("%s\n") % *j; |                     cout << format("%s\n") % *j; | ||||||
|  | @ -115,7 +109,7 @@ static void opQuery(Strings opFlags, Strings opArgs) | ||||||
|                  i != opArgs.end(); i++) |                  i != opArgs.end(); i++) | ||||||
|             { |             { | ||||||
|                 StringSet paths2 = storeExprRequisites( |                 StringSet paths2 = storeExprRequisites( | ||||||
|                     maybeNormalise(checkPath(*i), normalise, realise), |                     maybeNormalise(*i, normalise, realise), | ||||||
|                     includeExprs, includeSuccessors); |                     includeExprs, includeSuccessors); | ||||||
|                 paths.insert(paths2.begin(), paths2.end()); |                 paths.insert(paths2.begin(), paths2.end()); | ||||||
|             } |             } | ||||||
|  | @ -129,7 +123,7 @@ static void opQuery(Strings opFlags, Strings opArgs) | ||||||
|             for (Strings::iterator i = opArgs.begin(); |             for (Strings::iterator i = opArgs.begin(); | ||||||
|                  i != opArgs.end(); i++) |                  i != opArgs.end(); i++) | ||||||
|             { |             { | ||||||
|                 Paths preds = queryPredecessors(checkPath(*i)); |                 Paths preds = queryPredecessors(*i); | ||||||
|                 for (Paths::iterator j = preds.begin(); |                 for (Paths::iterator j = preds.begin(); | ||||||
|                      j != preds.end(); j++) |                      j != preds.end(); j++) | ||||||
|                     cout << format("%s\n") % *j; |                     cout << format("%s\n") % *j; | ||||||
|  | @ -141,7 +135,7 @@ static void opQuery(Strings opFlags, Strings opArgs) | ||||||
|             PathSet roots; |             PathSet roots; | ||||||
|             for (Strings::iterator i = opArgs.begin(); |             for (Strings::iterator i = opArgs.begin(); | ||||||
|                  i != opArgs.end(); i++) |                  i != opArgs.end(); i++) | ||||||
|                 roots.insert(maybeNormalise(checkPath(*i), normalise, realise)); |                 roots.insert(maybeNormalise(*i, normalise, realise)); | ||||||
| 	    printDotGraph(roots); | 	    printDotGraph(roots); | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
|  | @ -162,8 +156,8 @@ static void opSuccessor(Strings opFlags, Strings opArgs) | ||||||
|     for (Strings::iterator i = opArgs.begin(); |     for (Strings::iterator i = opArgs.begin(); | ||||||
|          i != opArgs.end(); ) |          i != opArgs.end(); ) | ||||||
|     { |     { | ||||||
|         Path path1 = checkPath(*i++); |         Path path1 = *i++; | ||||||
|         Path path2 = checkPath(*i++); |         Path path2 = *i++; | ||||||
|         registerSuccessor(txn, path1, path2); |         registerSuccessor(txn, path1, path2); | ||||||
|     } |     } | ||||||
|     txn.commit(); |     txn.commit(); | ||||||
|  | @ -173,14 +167,28 @@ static void opSuccessor(Strings opFlags, Strings opArgs) | ||||||
| static void opSubstitute(Strings opFlags, Strings opArgs) | static void opSubstitute(Strings opFlags, Strings opArgs) | ||||||
| { | { | ||||||
|     if (!opFlags.empty()) throw UsageError("unknown flag"); |     if (!opFlags.empty()) throw UsageError("unknown flag"); | ||||||
|     if (opArgs.size() % 2) throw UsageError("expecting even number of arguments"); |     if (!opArgs.empty()) | ||||||
|  |         throw UsageError("no arguments expected"); | ||||||
| 
 | 
 | ||||||
|     for (Strings::iterator i = opArgs.begin(); |     while (1) { | ||||||
|          i != opArgs.end(); ) |         Path srcPath; | ||||||
|     { |         Substitute sub; | ||||||
|         Path src = checkPath(*i++); |         getline(cin, srcPath); | ||||||
|         Path sub = checkPath(*i++); |         if (cin.eof()) break; | ||||||
|         registerSubstitute(src, sub); |         getline(cin, sub.storeExpr); | ||||||
|  |         getline(cin, sub.program); | ||||||
|  |         string s; | ||||||
|  |         getline(cin, s); | ||||||
|  |         istringstream st(s); | ||||||
|  |         int n; | ||||||
|  |         st >> n; | ||||||
|  |         if (!st) throw Error("number expected"); | ||||||
|  |         while (n--) { | ||||||
|  |             getline(cin, s); | ||||||
|  |             sub.args.push_back(s); | ||||||
|  |         } | ||||||
|  |         if (!cin || cin.eof()) throw Error("missing input"); | ||||||
|  |         registerSubstitute(srcPath, sub); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -260,7 +268,7 @@ static void opInit(Strings opFlags, Strings opArgs) | ||||||
| { | { | ||||||
|     if (!opFlags.empty()) throw UsageError("unknown flag"); |     if (!opFlags.empty()) throw UsageError("unknown flag"); | ||||||
|     if (!opArgs.empty()) |     if (!opArgs.empty()) | ||||||
|         throw UsageError("--init does not have arguments"); |         throw UsageError("no arguments expected"); | ||||||
|     initDB(); |     initDB(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -17,9 +17,10 @@ dependencies.sh: dependencies.nix | ||||||
| locking.sh: locking.nix | locking.sh: locking.nix | ||||||
| parallel.sh: parallel.nix | parallel.sh: parallel.nix | ||||||
| build-hook.sh: build-hook.nix | build-hook.sh: build-hook.nix | ||||||
|  | substitutes.sh: substitutes.nix substituter.nix | ||||||
| 
 | 
 | ||||||
| TESTS = init.sh simple.sh dependencies.sh locking.sh parallel.sh \ | TESTS = init.sh simple.sh dependencies.sh locking.sh parallel.sh \ | ||||||
|   build-hook.sh |   build-hook.sh substitutes.sh | ||||||
| 
 | 
 | ||||||
| XFAIL_TESTS = | XFAIL_TESTS = | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										22
									
								
								tests/substituter.builder.sh
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								tests/substituter.builder.sh
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | ||||||
|  | # Set a PATH (!!! impure). | ||||||
|  | export PATH=/bin:/usr/bin:$PATH | ||||||
|  | 
 | ||||||
|  | mkdir $out | ||||||
|  | 
 | ||||||
|  | cat > $out/substituter <<EOF | ||||||
|  | #! /bin/sh -ex | ||||||
|  | echo \$* | ||||||
|  | 
 | ||||||
|  | case \$* in | ||||||
|  |     *aaaa*) | ||||||
|  |         echo "Closure([\"\$2\"],[(\"\$2\",[])])" > \$1 | ||||||
|  |         ;; | ||||||
|  |     *) | ||||||
|  |         mkdir \$1 | ||||||
|  |         echo \$3 \$4 > \$1/hello | ||||||
|  |         ;; | ||||||
|  | esac         | ||||||
|  | EOF | ||||||
|  | 
 | ||||||
|  | chmod +x $out/substituter | ||||||
|  | 
 | ||||||
							
								
								
									
										6
									
								
								tests/substituter.nix.in
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								tests/substituter.nix.in
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | derivation { | ||||||
|  |   name = "substituter"; | ||||||
|  |   system = "@system@"; | ||||||
|  |   builder = "@shell@"; | ||||||
|  |   args = ["-e" "-x" ./substituter.builder.sh]; | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								tests/substitutes.nix.in
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								tests/substitutes.nix.in
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | derivation { | ||||||
|  |   name = "substitutes"; | ||||||
|  |   system = "@system@"; | ||||||
|  |   builder = "@shell@"; | ||||||
|  |   args = ["-e" "-x" ./simple.builder.sh]; | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								tests/substitutes.sh
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								tests/substitutes.sh
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | ||||||
|  | # Instantiate. | ||||||
|  | storeExpr=$($TOP/src/nix-instantiate/nix-instantiate substitutes.nix) | ||||||
|  | echo "store expr is $storeExpr" | ||||||
|  | 
 | ||||||
|  | # Find the output path. | ||||||
|  | outPath=$($TOP/src/nix-store/nix-store -qvvvvv "$storeExpr") | ||||||
|  | echo "output path is $outPath" | ||||||
|  | 
 | ||||||
|  | # Instantiate the substitute program. | ||||||
|  | subExpr=$($TOP/src/nix-instantiate/nix-instantiate substituter.nix) | ||||||
|  | echo "store expr is $subExpr" | ||||||
|  | 
 | ||||||
|  | # Register a fake successor, and a substitute for it. | ||||||
|  | suc=$NIX_STORE_DIR/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-s.store | ||||||
|  | (echo $suc && echo $subExpr && echo "/substituter" && echo 3 && echo $outPath && echo Hallo && echo Wereld) | $TOP/src/nix-store/nix-store --substitute | ||||||
|  | $TOP/src/nix-store/nix-store --successor $storeExpr $suc | ||||||
|  | 
 | ||||||
|  | # Register a substitute for the output path. | ||||||
|  | (echo $outPath && echo $subExpr && echo "/substituter" && echo 3 && echo $outPath && echo Hallo && echo Wereld) | $TOP/src/nix-store/nix-store --substitute | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | $TOP/src/nix-store/nix-store -rvvvvv "$storeExpr" | ||||||
|  | 
 | ||||||
|  | text=$(cat "$outPath"/hello) | ||||||
|  | if test "$text" != "Hallo Wereld"; then exit 1; fi | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue