* Merge the multiple-outputs-sandbox branch (svn merge --reintegrate
^/nix/branches/multiple-outputs-sandbox). Multiple output support still isn't complete, but it wasn't complete in the trunk either, so it doesn't hurt.
This commit is contained in:
		
						commit
						adaf64a99b
					
				
					 21 changed files with 343 additions and 125 deletions
				
			
		
							
								
								
									
										27
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -61,23 +61,11 @@ | ||||||
| # /externals/ | # /externals/ | ||||||
| /externals/Makefile | /externals/Makefile | ||||||
| /externals/Makefile.in | /externals/Makefile.in | ||||||
| /externals/aterm-* |  | ||||||
| /externals/have-aterm |  | ||||||
| /externals/build-aterm |  | ||||||
| /externals/inst-aterm |  | ||||||
| /externals/bzip2-* | /externals/bzip2-* | ||||||
| /externals/have-bzip2 |  | ||||||
| /externals/build-bzip2 | /externals/build-bzip2 | ||||||
| /externals/inst-bzip2 | /externals/inst-bzip2 | ||||||
| 
 | /externals/sqlite-* | ||||||
| # /make/examples/aterm/ | /externals/build-sqlite | ||||||
| /make/examples/aterm/result* |  | ||||||
| 
 |  | ||||||
| # /make/examples/aterm/aterm/ |  | ||||||
| /make/examples/aterm/aterm/* |  | ||||||
| 
 |  | ||||||
| # /make/examples/aterm/test/ |  | ||||||
| /make/examples/aterm/test/* |  | ||||||
| 
 | 
 | ||||||
| # /misc/ | # /misc/ | ||||||
| /misc/Makefile.in | /misc/Makefile.in | ||||||
|  | @ -100,13 +88,16 @@ | ||||||
| /scripts/nix-channel | /scripts/nix-channel | ||||||
| /scripts/nix-build | /scripts/nix-build | ||||||
| /scripts/nix-copy-closure | /scripts/nix-copy-closure | ||||||
| /scripts/readmanifest.pm | /scripts/nix-generate-patches | ||||||
| /scripts/readconfig.pm | /scripts/NixConfig.pm | ||||||
|  | /scripts/NixManifest.pm | ||||||
|  | /scripts/GeneratePatches.pm | ||||||
| /scripts/download-using-manifests.pl | /scripts/download-using-manifests.pl | ||||||
| /scripts/copy-from-other-stores.pl | /scripts/copy-from-other-stores.pl | ||||||
| /scripts/generate-patches.pl |  | ||||||
| /scripts/find-runtime-roots.pl | /scripts/find-runtime-roots.pl | ||||||
| /scripts/build-remote.pl | /scripts/build-remote.pl | ||||||
|  | /scripts/nix-reduce-build | ||||||
|  | /scripts/nix-http-export.cgi | ||||||
| 
 | 
 | ||||||
| # /src/ | # /src/ | ||||||
| /src/Makefile | /src/Makefile | ||||||
|  | @ -168,6 +159,7 @@ | ||||||
| /src/libstore/derivations-ast.cc | /src/libstore/derivations-ast.cc | ||||||
| /src/libstore/derivations-ast.hh | /src/libstore/derivations-ast.hh | ||||||
| /src/libstore/.libs | /src/libstore/.libs | ||||||
|  | /src/libstore/schema.sql.hh | ||||||
| 
 | 
 | ||||||
| # /src/libutil/ | # /src/libutil/ | ||||||
| /src/libutil/Makefile | /src/libutil/Makefile | ||||||
|  | @ -242,6 +234,7 @@ | ||||||
| /tests/config.nix | /tests/config.nix | ||||||
| /tests/common.sh | /tests/common.sh | ||||||
| /tests/dummy | /tests/dummy | ||||||
|  | /tests/result* | ||||||
| 
 | 
 | ||||||
| # /tests/lang/ | # /tests/lang/ | ||||||
| /tests/lang/*.out | /tests/lang/*.out | ||||||
|  |  | ||||||
|  | @ -274,8 +274,8 @@ AC_SUBST(sqlite_bin) | ||||||
| # Whether to use the Boehm garbage collector. | # Whether to use the Boehm garbage collector. | ||||||
| AC_ARG_ENABLE(gc, AC_HELP_STRING([--enable-gc], | AC_ARG_ENABLE(gc, AC_HELP_STRING([--enable-gc], | ||||||
|   [enable garbage collection in the Nix expression evaluator (requires Boehm GC)]), |   [enable garbage collection in the Nix expression evaluator (requires Boehm GC)]), | ||||||
|   gc=$enableval, gc=) |   gc=$enableval, gc=no) | ||||||
| if test -n "$gc"; then | if test "$gc" = yes; then | ||||||
|   PKG_CHECK_MODULES([BDW_GC], [bdw-gc]) |   PKG_CHECK_MODULES([BDW_GC], [bdw-gc]) | ||||||
|   CXXFLAGS="$BDW_GC_CFLAGS $CXXFLAGS" |   CXXFLAGS="$BDW_GC_CFLAGS $CXXFLAGS" | ||||||
|   AC_DEFINE(HAVE_BOEHMGC, 1, [Whether to use the Boehm garbage collector.]) |   AC_DEFINE(HAVE_BOEHMGC, 1, [Whether to use the Boehm garbage collector.]) | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| all-local: config.nix | all-local: config.nix | ||||||
| 
 | 
 | ||||||
| files = nar.nix buildenv.nix buildenv.pl unpack-channel.nix unpack-channel.sh | files = nar.nix buildenv.nix buildenv.pl unpack-channel.nix unpack-channel.sh derivation.nix | ||||||
| 
 | 
 | ||||||
| install-exec-local: | install-exec-local: | ||||||
| 	$(INSTALL) -d $(DESTDIR)$(datadir)/nix/corepkgs | 	$(INSTALL) -d $(DESTDIR)$(datadir)/nix/corepkgs | ||||||
|  |  | ||||||
							
								
								
									
										27
									
								
								corepkgs/derivation.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								corepkgs/derivation.nix
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | ||||||
|  | /* This is the implementation of the ‘derivation’ builtin function. | ||||||
|  |    It's actually a wrapper around the ‘derivationStrict’ primop. */ | ||||||
|  | 
 | ||||||
|  | drvAttrs @ { outputs ? [ "out" ], ... }: | ||||||
|  | 
 | ||||||
|  | let | ||||||
|  | 
 | ||||||
|  |   strict = derivationStrict drvAttrs; | ||||||
|  |    | ||||||
|  |   commonAttrs = drvAttrs // (builtins.listToAttrs outputsList) // | ||||||
|  |     { all = map (x: x.value) outputsList; | ||||||
|  |       inherit drvAttrs; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |   outputToAttrListElement = outputName: | ||||||
|  |     { name = outputName; | ||||||
|  |       value = commonAttrs // { | ||||||
|  |         outPath = builtins.getAttr outputName strict; | ||||||
|  |         drvPath = strict.drvPath; | ||||||
|  |         type = "derivation"; | ||||||
|  |         inherit outputName; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |   outputsList = map outputToAttrListElement outputs; | ||||||
|  |      | ||||||
|  | in (builtins.head outputsList).value | ||||||
|  | @ -307,6 +307,7 @@ EOF | ||||||
|      |      | ||||||
|     for my $manifestLink (glob "$manifestDir/*.nixmanifest") { |     for my $manifestLink (glob "$manifestDir/*.nixmanifest") { | ||||||
|         my $manifest = Cwd::abs_path($manifestLink); |         my $manifest = Cwd::abs_path($manifestLink); | ||||||
|  |         next unless -f $manifest; | ||||||
|         my $timestamp = lstat($manifest)->mtime; |         my $timestamp = lstat($manifest)->mtime; | ||||||
|         $seen{$manifest} = 1; |         $seen{$manifest} = 1; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -148,8 +148,6 @@ EvalState::EvalState() | ||||||
|     nrAttrsets = nrOpUpdates = nrOpUpdateValuesCopied = 0; |     nrAttrsets = nrOpUpdates = nrOpUpdateValuesCopied = 0; | ||||||
|     deepestStack = (char *) -1; |     deepestStack = (char *) -1; | ||||||
| 
 | 
 | ||||||
|     createBaseEnv(); |  | ||||||
|      |  | ||||||
|     allowUnsafeEquality = getEnv("NIX_NO_UNSAFE_EQ", "") == ""; |     allowUnsafeEquality = getEnv("NIX_NO_UNSAFE_EQ", "") == ""; | ||||||
| 
 | 
 | ||||||
| #if HAVE_BOEHMGC | #if HAVE_BOEHMGC | ||||||
|  | @ -188,6 +186,8 @@ EvalState::EvalState() | ||||||
|     foreach (Strings::iterator, i, paths) addToSearchPath(*i); |     foreach (Strings::iterator, i, paths) addToSearchPath(*i); | ||||||
|     addToSearchPath("nix=" + nixDataDir + "/nix/corepkgs"); |     addToSearchPath("nix=" + nixDataDir + "/nix/corepkgs"); | ||||||
|     searchPathInsertionPoint = searchPath.begin(); |     searchPathInsertionPoint = searchPath.begin(); | ||||||
|  | 
 | ||||||
|  |     createBaseEnv(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -356,27 +356,31 @@ static void prim_derivationStrict(EvalState & state, Value * * args, Value & v) | ||||||
|            inputs to ensure that they are available when the builder |            inputs to ensure that they are available when the builder | ||||||
|            runs. */ |            runs. */ | ||||||
|         if (path.at(0) == '=') { |         if (path.at(0) == '=') { | ||||||
|             path = string(path, 1); |             /* !!! This doesn't work if readOnlyMode is set. */ | ||||||
|             PathSet refs; computeFSClosure(*store, path, refs); |             PathSet refs; computeFSClosure(*store, string(path, 1), refs); | ||||||
|             foreach (PathSet::iterator, j, refs) { |             foreach (PathSet::iterator, j, refs) { | ||||||
|                 drv.inputSrcs.insert(*j); |                 drv.inputSrcs.insert(*j); | ||||||
|                 if (isDerivation(*j)) |                 if (isDerivation(*j)) | ||||||
|                     drv.inputDrvs[*j] = singleton<StringSet>("out"); |                     drv.inputDrvs[*j] = store->queryDerivationOutputNames(*j); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /* See prim_unsafeDiscardOutputDependency. */ |         /* See prim_unsafeDiscardOutputDependency. */ | ||||||
|         bool useDrvAsSrc = false; |         else if (path.at(0) == '~') | ||||||
|         if (path.at(0) == '~') { |             drv.inputSrcs.insert(string(path, 1)); | ||||||
|             path = string(path, 1); | 
 | ||||||
|             useDrvAsSrc = true; |         /* Handle derivation outputs of the form ‘!<name>!<path>’. */ | ||||||
|  |         else if (path.at(0) == '!') { | ||||||
|  |             size_t index = path.find("!", 1); | ||||||
|  |             drv.inputDrvs[string(path, index + 1)].insert(string(path, 1, index - 1)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         assert(isStorePath(path)); |         /* Handle derivation contexts returned by
 | ||||||
|  |            ‘builtins.storePath’. */ | ||||||
|  |         else if (isDerivation(path)) | ||||||
|  |             drv.inputDrvs[path] = store->queryDerivationOutputNames(path); | ||||||
| 
 | 
 | ||||||
|         debug(format("derivation uses `%1%'") % path); |         /* Otherwise it's a source file. */ | ||||||
|         if (!useDrvAsSrc && isDerivation(path)) |  | ||||||
|             drv.inputDrvs[path] = singleton<StringSet>("out"); |  | ||||||
|         else |         else | ||||||
|             drv.inputSrcs.insert(path); |             drv.inputSrcs.insert(path); | ||||||
|     } |     } | ||||||
|  | @ -447,10 +451,8 @@ static void prim_derivationStrict(EvalState & state, Value * * args, Value & v) | ||||||
|     state.mkAttrs(v, 1 + drv.outputs.size()); |     state.mkAttrs(v, 1 + drv.outputs.size()); | ||||||
|     mkString(*state.allocAttr(v, state.sDrvPath), drvPath, singleton<PathSet>("=" + drvPath)); |     mkString(*state.allocAttr(v, state.sDrvPath), drvPath, singleton<PathSet>("=" + drvPath)); | ||||||
|     foreach (DerivationOutputs::iterator, i, drv.outputs) { |     foreach (DerivationOutputs::iterator, i, drv.outputs) { | ||||||
|         /* The output path of an output X is ‘<X>Path’,
 |         mkString(*state.allocAttr(v, state.symbols.create(i->first)), | ||||||
|            e.g. ‘outPath’. */ |             i->second.path, singleton<PathSet>("!" + i->first + "!" + drvPath)); | ||||||
|         mkString(*state.allocAttr(v, state.symbols.create(i->first + "Path")), |  | ||||||
|             i->second.path, singleton<PathSet>(drvPath)); |  | ||||||
|     } |     } | ||||||
|     v.attrs->sort(); |     v.attrs->sort(); | ||||||
| } | } | ||||||
|  | @ -1042,15 +1044,6 @@ void EvalState::createBaseEnv() | ||||||
|     addPrimOp("__getEnv", 1, prim_getEnv); |     addPrimOp("__getEnv", 1, prim_getEnv); | ||||||
|     addPrimOp("__trace", 2, prim_trace); |     addPrimOp("__trace", 2, prim_trace); | ||||||
| 
 | 
 | ||||||
|     // Derivations
 |  | ||||||
|     addPrimOp("derivationStrict", 1, prim_derivationStrict); |  | ||||||
| 
 |  | ||||||
|     /* Add a wrapper around the derivation primop that computes the
 |  | ||||||
|        `drvPath' and `outPath' attributes lazily. */ |  | ||||||
|     string s = "attrs: let res = derivationStrict attrs; in attrs // { drvPath = res.drvPath; outPath = res.outPath; type = \"derivation\"; }"; |  | ||||||
|     mkThunk_(v, parseExprFromString(s, "/")); |  | ||||||
|     addConstant("derivation", v); |  | ||||||
| 
 |  | ||||||
|     // Paths
 |     // Paths
 | ||||||
|     addPrimOp("__toPath", 1, prim_toPath); |     addPrimOp("__toPath", 1, prim_toPath); | ||||||
|     addPrimOp("__storePath", 1, prim_storePath); |     addPrimOp("__storePath", 1, prim_storePath); | ||||||
|  | @ -1099,6 +1092,14 @@ void EvalState::createBaseEnv() | ||||||
|     addPrimOp("__parseDrvName", 1, prim_parseDrvName); |     addPrimOp("__parseDrvName", 1, prim_parseDrvName); | ||||||
|     addPrimOp("__compareVersions", 2, prim_compareVersions); |     addPrimOp("__compareVersions", 2, prim_compareVersions); | ||||||
| 
 | 
 | ||||||
|  |     // Derivations
 | ||||||
|  |     addPrimOp("derivationStrict", 1, prim_derivationStrict); | ||||||
|  | 
 | ||||||
|  |     /* Add a wrapper around the derivation primop that computes the
 | ||||||
|  |        `drvPath' and `outPath' attributes lazily. */ | ||||||
|  |     mkThunk_(v, parseExprFromFile(findFile("nix/derivation.nix"))); | ||||||
|  |     addConstant("derivation", v); | ||||||
|  | 
 | ||||||
|     /* Now that we've added all primops, sort the `builtins' attribute
 |     /* Now that we've added all primops, sort the `builtins' attribute
 | ||||||
|        set, because attribute lookups expect it to be sorted. */ |        set, because attribute lookups expect it to be sorted. */ | ||||||
|     baseEnv.values[0]->attrs->sort(); |     baseEnv.values[0]->attrs->sort(); | ||||||
|  |  | ||||||
|  | @ -278,10 +278,6 @@ public: | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| MakeError(SubstError, Error) |  | ||||||
| MakeError(BuildError, Error) /* denotes a permanent build failure */ |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| //////////////////////////////////////////////////////////////////////
 | //////////////////////////////////////////////////////////////////////
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -1982,7 +1978,8 @@ void DerivationGoal::computeClosure() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /* Register each output path as valid, and register the sets of
 |     /* Register each output path as valid, and register the sets of
 | ||||||
|        paths referenced by each of them. */ |        paths referenced by each of them.  If there are cycles in the | ||||||
|  |        outputs, this will fail. */ | ||||||
|     ValidPathInfos infos; |     ValidPathInfos infos; | ||||||
|     foreach (DerivationOutputs::iterator, i, drv.outputs) { |     foreach (DerivationOutputs::iterator, i, drv.outputs) { | ||||||
|         ValidPathInfo info; |         ValidPathInfo info; | ||||||
|  |  | ||||||
|  | @ -371,36 +371,6 @@ static void addAdditionalRoots(StoreAPI & store, PathSet & roots) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| static void dfsVisit(StoreAPI & store, const PathSet & paths, |  | ||||||
|     const Path & path, PathSet & visited, Paths & sorted) |  | ||||||
| { |  | ||||||
|     if (visited.find(path) != visited.end()) return; |  | ||||||
|     visited.insert(path); |  | ||||||
|      |  | ||||||
|     PathSet references; |  | ||||||
|     if (store.isValidPath(path)) |  | ||||||
|         store.queryReferences(path, references); |  | ||||||
|      |  | ||||||
|     foreach (PathSet::iterator, i, references) |  | ||||||
|         /* Don't traverse into paths that don't exist.  That can
 |  | ||||||
|            happen due to substitutes for non-existent paths. */ |  | ||||||
|         if (*i != path && paths.find(*i) != paths.end()) |  | ||||||
|             dfsVisit(store, paths, *i, visited, sorted); |  | ||||||
| 
 |  | ||||||
|     sorted.push_front(path); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| Paths topoSortPaths(StoreAPI & store, const PathSet & paths) |  | ||||||
| { |  | ||||||
|     Paths sorted; |  | ||||||
|     PathSet visited; |  | ||||||
|     foreach (PathSet::const_iterator, i, paths) |  | ||||||
|         dfsVisit(store, paths, *i, visited, sorted); |  | ||||||
|     return sorted; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| struct GCLimitReached { }; | struct GCLimitReached { }; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -820,6 +820,28 @@ PathSet LocalStore::queryDerivationOutputs(const Path & path) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | StringSet LocalStore::queryDerivationOutputNames(const Path & path) | ||||||
|  | { | ||||||
|  |     SQLiteTxn txn(db); | ||||||
|  |      | ||||||
|  |     SQLiteStmtUse use(stmtQueryDerivationOutputs); | ||||||
|  |     stmtQueryDerivationOutputs.bind(queryValidPathId(path)); | ||||||
|  |      | ||||||
|  |     StringSet outputNames; | ||||||
|  |     int r; | ||||||
|  |     while ((r = sqlite3_step(stmtQueryDerivationOutputs)) == SQLITE_ROW) { | ||||||
|  |         const char * s = (const char *) sqlite3_column_text(stmtQueryDerivationOutputs, 0); | ||||||
|  |         assert(s); | ||||||
|  |         outputNames.insert(s); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     if (r != SQLITE_DONE) | ||||||
|  |         throwSQLiteError(db, format("error getting output names of `%1%'") % path); | ||||||
|  | 
 | ||||||
|  |     return outputNames; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| void LocalStore::startSubstituter(const Path & substituter, RunningSubstituter & run) | void LocalStore::startSubstituter(const Path & substituter, RunningSubstituter & run) | ||||||
| { | { | ||||||
|     if (run.pid != -1) return; |     if (run.pid != -1) return; | ||||||
|  | @ -944,12 +966,14 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos) | ||||||
|     while (1) { |     while (1) { | ||||||
|         try { |         try { | ||||||
|             SQLiteTxn txn(db); |             SQLiteTxn txn(db); | ||||||
|  |             PathSet paths; | ||||||
|      |      | ||||||
|             foreach (ValidPathInfos::const_iterator, i, infos) { |             foreach (ValidPathInfos::const_iterator, i, infos) { | ||||||
|                 assert(i->hash.type == htSHA256); |                 assert(i->hash.type == htSHA256); | ||||||
|                 /* !!! Maybe the registration info should be updated if the
 |                 /* !!! Maybe the registration info should be updated if the
 | ||||||
|                    path is already valid. */ |                    path is already valid. */ | ||||||
|                 if (!isValidPath(i->path)) addValidPath(*i); |                 if (!isValidPath(i->path)) addValidPath(*i); | ||||||
|  |                 paths.insert(i->path); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             foreach (ValidPathInfos::const_iterator, i, infos) { |             foreach (ValidPathInfos::const_iterator, i, infos) { | ||||||
|  | @ -958,6 +982,12 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos) | ||||||
|                     addReference(referrer, queryValidPathId(*j)); |                     addReference(referrer, queryValidPathId(*j)); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             /* Do a topological sort of the paths.  This will throw an
 | ||||||
|  |                error if a cycle is detected and roll back the | ||||||
|  |                transaction.  Cycles can only occur when a derivation | ||||||
|  |                has multiple outputs. */ | ||||||
|  |             topoSortPaths(*this, paths); | ||||||
|  | 
 | ||||||
|             txn.commit(); |             txn.commit(); | ||||||
|             break; |             break; | ||||||
|         } catch (SQLiteBusy & e) { |         } catch (SQLiteBusy & e) { | ||||||
|  |  | ||||||
|  | @ -118,6 +118,8 @@ public: | ||||||
|     PathSet queryValidDerivers(const Path & path); |     PathSet queryValidDerivers(const Path & path); | ||||||
| 
 | 
 | ||||||
|     PathSet queryDerivationOutputs(const Path & path); |     PathSet queryDerivationOutputs(const Path & path); | ||||||
|  | 
 | ||||||
|  |     StringSet queryDerivationOutputNames(const Path & path); | ||||||
|      |      | ||||||
|     PathSet querySubstitutablePaths(); |     PathSet querySubstitutablePaths(); | ||||||
|      |      | ||||||
|  |  | ||||||
|  | @ -97,4 +97,40 @@ void queryMissing(StoreAPI & store, const PathSet & targets, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|   |   | ||||||
|  | static void dfsVisit(StoreAPI & store, const PathSet & paths, | ||||||
|  |     const Path & path, PathSet & visited, Paths & sorted, | ||||||
|  |     PathSet & parents) | ||||||
|  | { | ||||||
|  |     if (parents.find(path) != parents.end()) | ||||||
|  |         throw BuildError(format("cycle detected in the references of `%1%'") % path); | ||||||
|  |      | ||||||
|  |     if (visited.find(path) != visited.end()) return; | ||||||
|  |     visited.insert(path); | ||||||
|  |     parents.insert(path); | ||||||
|  |      | ||||||
|  |     PathSet references; | ||||||
|  |     if (store.isValidPath(path)) | ||||||
|  |         store.queryReferences(path, references); | ||||||
|  |      | ||||||
|  |     foreach (PathSet::iterator, i, references) | ||||||
|  |         /* Don't traverse into paths that don't exist.  That can
 | ||||||
|  |            happen due to substitutes for non-existent paths. */ | ||||||
|  |         if (*i != path && paths.find(*i) != paths.end()) | ||||||
|  |             dfsVisit(store, paths, *i, visited, sorted, parents); | ||||||
|  | 
 | ||||||
|  |     sorted.push_front(path); | ||||||
|  |     parents.erase(path); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Paths topoSortPaths(StoreAPI & store, const PathSet & paths) | ||||||
|  | { | ||||||
|  |     Paths sorted; | ||||||
|  |     PathSet visited, parents; | ||||||
|  |     foreach (PathSet::const_iterator, i, paths) | ||||||
|  |         dfsVisit(store, paths, *i, visited, sorted, parents); | ||||||
|  |     return sorted; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -326,6 +326,16 @@ PathSet RemoteStore::queryDerivationOutputs(const Path & path) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | PathSet RemoteStore::queryDerivationOutputNames(const Path & path) | ||||||
|  | { | ||||||
|  |     openConnection(); | ||||||
|  |     writeInt(wopQueryDerivationOutputNames, to); | ||||||
|  |     writeString(path, to); | ||||||
|  |     processStderr(); | ||||||
|  |     return readStrings<PathSet>(from); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| Path RemoteStore::addToStore(const Path & _srcPath, | Path RemoteStore::addToStore(const Path & _srcPath, | ||||||
|     bool recursive, HashType hashAlgo, PathFilter & filter) |     bool recursive, HashType hashAlgo, PathFilter & filter) | ||||||
| { | { | ||||||
|  |  | ||||||
|  | @ -41,6 +41,8 @@ public: | ||||||
|      |      | ||||||
|     PathSet queryDerivationOutputs(const Path & path); |     PathSet queryDerivationOutputs(const Path & path); | ||||||
|      |      | ||||||
|  |     StringSet queryDerivationOutputNames(const Path & path); | ||||||
|  | 
 | ||||||
|     bool hasSubstitutes(const Path & path); |     bool hasSubstitutes(const Path & path); | ||||||
|      |      | ||||||
|     bool querySubstitutablePathInfo(const Path & path, |     bool querySubstitutablePathInfo(const Path & path, | ||||||
|  |  | ||||||
|  | @ -140,6 +140,9 @@ public: | ||||||
| 
 | 
 | ||||||
|     /* Query the outputs of the derivation denoted by `path'. */ |     /* Query the outputs of the derivation denoted by `path'. */ | ||||||
|     virtual PathSet queryDerivationOutputs(const Path & path) = 0; |     virtual PathSet queryDerivationOutputs(const Path & path) = 0; | ||||||
|  | 
 | ||||||
|  |     /* Query the output names of the derivation denoted by `path'. */ | ||||||
|  |     virtual StringSet queryDerivationOutputNames(const Path & path) = 0; | ||||||
|      |      | ||||||
|     /* Query whether a path has substitutes. */ |     /* Query whether a path has substitutes. */ | ||||||
|     virtual bool hasSubstitutes(const Path & path) = 0; |     virtual bool hasSubstitutes(const Path & path) = 0; | ||||||
|  | @ -346,6 +349,10 @@ void exportPaths(StoreAPI & store, const Paths & paths, | ||||||
|     bool sign, Sink & sink); |     bool sign, Sink & sink); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | MakeError(SubstError, Error) | ||||||
|  | MakeError(BuildError, Error) /* denotes a permanent build failure */ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -39,6 +39,7 @@ typedef enum { | ||||||
|     wopClearFailedPaths = 25, |     wopClearFailedPaths = 25, | ||||||
|     wopQueryPathInfo = 26, |     wopQueryPathInfo = 26, | ||||||
|     wopImportPaths = 27, |     wopImportPaths = 27, | ||||||
|  |     wopQueryDerivationOutputNames = 28, | ||||||
| } WorkerOp; | } WorkerOp; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -50,26 +50,30 @@ static Path useDeriver(Path path) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /* Realisation the given path.  For a derivation that means build it;
 | /* Realise the given path.  For a derivation that means build it; for
 | ||||||
|    for other paths it means ensure their validity. */ |    other paths it means ensure their validity. */ | ||||||
| static Path realisePath(const Path & path) | static PathSet realisePath(const Path & path) | ||||||
| { | { | ||||||
|     if (isDerivation(path)) { |     if (isDerivation(path)) { | ||||||
|         PathSet paths; |         store->buildDerivations(singleton<PathSet>(path)); | ||||||
|         paths.insert(path); |         Derivation drv = derivationFromPath(*store, path); | ||||||
|         store->buildDerivations(paths); | 
 | ||||||
|         Path outPath = findOutput(derivationFromPath(*store, path), "out"); |         PathSet outputs; | ||||||
|          |         foreach (DerivationOutputs::iterator, i, drv.outputs) { | ||||||
|         if (gcRoot == "") |             Path outPath = i->second.path; | ||||||
|             printGCWarning(); |             if (gcRoot == "") | ||||||
|         else |                 printGCWarning(); | ||||||
|             outPath = addPermRoot(*store, outPath, |             else | ||||||
|                 makeRootName(gcRoot, rootNr), indirectRoot); |                 outPath = addPermRoot(*store, outPath, | ||||||
|          |                     makeRootName(gcRoot, rootNr), indirectRoot); | ||||||
|         return outPath; |             outputs.insert(outPath); | ||||||
|     } else { |         } | ||||||
|  |         return outputs; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     else { | ||||||
|         store->ensurePath(path); |         store->ensurePath(path); | ||||||
|         return path; |         return singleton<PathSet>(path); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -96,8 +100,11 @@ static void opRealise(Strings opFlags, Strings opArgs) | ||||||
|         if (isDerivation(*i)) drvPaths.insert(*i); |         if (isDerivation(*i)) drvPaths.insert(*i); | ||||||
|     store->buildDerivations(drvPaths); |     store->buildDerivations(drvPaths); | ||||||
| 
 | 
 | ||||||
|     foreach (Strings::iterator, i, opArgs) |     foreach (Strings::iterator, i, opArgs) { | ||||||
|         cout << format("%1%\n") % realisePath(*i); |         PathSet paths = realisePath(*i); | ||||||
|  |         foreach (PathSet::iterator, j, paths) | ||||||
|  |             cout << format("%1%\n") % *j; | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -157,14 +164,17 @@ static void opPrintFixedPath(Strings opFlags, Strings opArgs) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| static Path maybeUseOutput(const Path & storePath, bool useOutput, bool forceRealise) | static PathSet maybeUseOutputs(const Path & storePath, bool useOutput, bool forceRealise) | ||||||
| { | { | ||||||
|     if (forceRealise) realisePath(storePath); |     if (forceRealise) realisePath(storePath); | ||||||
|     if (useOutput && isDerivation(storePath)) { |     if (useOutput && isDerivation(storePath)) { | ||||||
|         Derivation drv = derivationFromPath(*store, storePath); |         Derivation drv = derivationFromPath(*store, storePath); | ||||||
|         return findOutput(drv, "out"); |         PathSet outputs; | ||||||
|  |         foreach (DerivationOutputs::iterator, i, drv.outputs) | ||||||
|  |             outputs.insert(i->second.path); | ||||||
|  |         return outputs; | ||||||
|     } |     } | ||||||
|     else return storePath; |     else return singleton<PathSet>(storePath); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -257,7 +267,8 @@ static void opQuery(Strings opFlags, Strings opArgs) | ||||||
|                 *i = followLinksToStorePath(*i); |                 *i = followLinksToStorePath(*i); | ||||||
|                 if (forceRealise) realisePath(*i); |                 if (forceRealise) realisePath(*i); | ||||||
|                 Derivation drv = derivationFromPath(*store, *i); |                 Derivation drv = derivationFromPath(*store, *i); | ||||||
|                 cout << format("%1%\n") % findOutput(drv, "out"); |                 foreach (DerivationOutputs::iterator, j, drv.outputs) | ||||||
|  |                     cout << format("%1%\n") % j->second.path; | ||||||
|             } |             } | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
|  | @ -268,11 +279,13 @@ static void opQuery(Strings opFlags, Strings opArgs) | ||||||
|         case qReferrersClosure: { |         case qReferrersClosure: { | ||||||
|             PathSet paths; |             PathSet paths; | ||||||
|             foreach (Strings::iterator, i, opArgs) { |             foreach (Strings::iterator, i, opArgs) { | ||||||
|                 Path path = maybeUseOutput(followLinksToStorePath(*i), useOutput, forceRealise); |                 PathSet ps = maybeUseOutputs(followLinksToStorePath(*i), useOutput, forceRealise); | ||||||
|                 if (query == qRequisites) computeFSClosure(*store, path, paths, false, includeOutputs); |                 foreach (PathSet::iterator, j, ps) { | ||||||
|                 else if (query == qReferences) store->queryReferences(path, paths); |                     if (query == qRequisites) computeFSClosure(*store, *j, paths, false, includeOutputs); | ||||||
|                 else if (query == qReferrers) store->queryReferrers(path, paths); |                     else if (query == qReferences) store->queryReferences(*j, paths); | ||||||
|                 else if (query == qReferrersClosure) computeFSClosure(*store, path, paths, true); |                     else if (query == qReferrers) store->queryReferrers(*j, paths); | ||||||
|  |                     else if (query == qReferrersClosure) computeFSClosure(*store, *j, paths, true); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|             Paths sorted = topoSortPaths(*store, paths); |             Paths sorted = topoSortPaths(*store, paths); | ||||||
|             for (Paths::reverse_iterator i = sorted.rbegin();  |             for (Paths::reverse_iterator i = sorted.rbegin();  | ||||||
|  | @ -304,13 +317,15 @@ static void opQuery(Strings opFlags, Strings opArgs) | ||||||
|         case qHash: |         case qHash: | ||||||
|         case qSize: |         case qSize: | ||||||
|             foreach (Strings::iterator, i, opArgs) { |             foreach (Strings::iterator, i, opArgs) { | ||||||
|                 Path path = maybeUseOutput(followLinksToStorePath(*i), useOutput, forceRealise); |                 PathSet paths = maybeUseOutputs(followLinksToStorePath(*i), useOutput, forceRealise); | ||||||
|                 ValidPathInfo info = store->queryPathInfo(path); |                 foreach (PathSet::iterator, j, paths) { | ||||||
|                 if (query == qHash) { |                     ValidPathInfo info = store->queryPathInfo(*j); | ||||||
|                     assert(info.hash.type == htSHA256); |                     if (query == qHash) { | ||||||
|                     cout << format("sha256:%1%\n") % printHash32(info.hash); |                         assert(info.hash.type == htSHA256); | ||||||
|                 } else if (query == qSize)  |                         cout << format("sha256:%1%\n") % printHash32(info.hash); | ||||||
|                     cout << format("%1%\n") % info.narSize; |                     } else if (query == qSize)  | ||||||
|  |                         cout << format("%1%\n") % info.narSize; | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|             break; |             break; | ||||||
| 
 | 
 | ||||||
|  | @ -323,16 +338,20 @@ static void opQuery(Strings opFlags, Strings opArgs) | ||||||
|              |              | ||||||
|         case qGraph: { |         case qGraph: { | ||||||
|             PathSet roots; |             PathSet roots; | ||||||
|             foreach (Strings::iterator, i, opArgs) |             foreach (Strings::iterator, i, opArgs) { | ||||||
|                 roots.insert(maybeUseOutput(followLinksToStorePath(*i), useOutput, forceRealise)); |                 PathSet paths = maybeUseOutputs(followLinksToStorePath(*i), useOutput, forceRealise); | ||||||
|  |                 roots.insert(paths.begin(), paths.end()); | ||||||
|  |             } | ||||||
|             printDotGraph(roots); |             printDotGraph(roots); | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         case qXml: { |         case qXml: { | ||||||
|             PathSet roots; |             PathSet roots; | ||||||
|             foreach (Strings::iterator, i, opArgs) |             foreach (Strings::iterator, i, opArgs) { | ||||||
|                 roots.insert(maybeUseOutput(followLinksToStorePath(*i), useOutput, forceRealise)); |                 PathSet paths = maybeUseOutputs(followLinksToStorePath(*i), useOutput, forceRealise); | ||||||
|  |                 roots.insert(paths.begin(), paths.end()); | ||||||
|  |             } | ||||||
|             printXmlGraph(roots); |             printXmlGraph(roots); | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
|  | @ -345,10 +364,11 @@ static void opQuery(Strings opFlags, Strings opArgs) | ||||||
|              |              | ||||||
|         case qRoots: { |         case qRoots: { | ||||||
|             PathSet referrers; |             PathSet referrers; | ||||||
|             foreach (Strings::iterator, i, opArgs) |             foreach (Strings::iterator, i, opArgs) { | ||||||
|                 computeFSClosure(*store, |                 PathSet paths = maybeUseOutputs(followLinksToStorePath(*i), useOutput, forceRealise); | ||||||
|                     maybeUseOutput(followLinksToStorePath(*i), useOutput, forceRealise), |                 foreach (PathSet::iterator, j, paths) | ||||||
|                     referrers, true); |                     computeFSClosure(*store, *j, referrers, true); | ||||||
|  |             } | ||||||
|             Roots roots = store->findRoots(); |             Roots roots = store->findRoots(); | ||||||
|             foreach (Roots::iterator, i, roots) |             foreach (Roots::iterator, i, roots) | ||||||
|                 if (referrers.find(i->second) != referrers.end()) |                 if (referrers.find(i->second) != referrers.end()) | ||||||
|  |  | ||||||
|  | @ -331,6 +331,16 @@ static void performOp(unsigned int clientVersion, | ||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     case wopQueryDerivationOutputNames: { | ||||||
|  |         Path path = readStorePath(from); | ||||||
|  |         startWork(); | ||||||
|  |         StringSet names; | ||||||
|  |         names = store->queryDerivationOutputNames(path); | ||||||
|  |         stopWork(); | ||||||
|  |         writeStrings(names, to); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     case wopQueryDeriver: { |     case wopQueryDeriver: { | ||||||
|         Path path = readStorePath(from); |         Path path = readStorePath(from); | ||||||
|         startWork(); |         startWork(); | ||||||
|  |  | ||||||
|  | @ -8,7 +8,8 @@ TESTS = init.sh hash.sh lang.sh add.sh simple.sh dependencies.sh \ | ||||||
|   referrers.sh user-envs.sh logging.sh nix-build.sh misc.sh fixed.sh \ |   referrers.sh user-envs.sh logging.sh nix-build.sh misc.sh fixed.sh \ | ||||||
|   gc-runtime.sh install-package.sh check-refs.sh filter-source.sh \ |   gc-runtime.sh install-package.sh check-refs.sh filter-source.sh \ | ||||||
|   remote-store.sh export.sh export-graph.sh negative-caching.sh \ |   remote-store.sh export.sh export-graph.sh negative-caching.sh \ | ||||||
|   binary-patching.sh timeout.sh secure-drv-outputs.sh nix-channel.sh |   binary-patching.sh timeout.sh secure-drv-outputs.sh nix-channel.sh \ | ||||||
|  |   multiple-outputs.sh | ||||||
| 
 | 
 | ||||||
| XFAIL_TESTS = | XFAIL_TESTS = | ||||||
| 
 | 
 | ||||||
|  | @ -35,5 +36,6 @@ EXTRA_DIST = $(TESTS) \ | ||||||
|   binary-patching.nix \ |   binary-patching.nix \ | ||||||
|   timeout.nix timeout.builder.sh \ |   timeout.nix timeout.builder.sh \ | ||||||
|   secure-drv-outputs.nix \ |   secure-drv-outputs.nix \ | ||||||
|  |   multiple-outputs.nix \ | ||||||
|   $(wildcard lang/*.nix) $(wildcard lang/*.exp) $(wildcard lang/*.exp.xml) $(wildcard lang/*.flags) $(wildcard lang/dir*/*.nix) \ |   $(wildcard lang/*.nix) $(wildcard lang/*.exp) $(wildcard lang/*.exp.xml) $(wildcard lang/*.flags) $(wildcard lang/dir*/*.nix) \ | ||||||
|   common.sh.in |   common.sh.in | ||||||
|  |  | ||||||
							
								
								
									
										67
									
								
								tests/multiple-outputs.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								tests/multiple-outputs.nix
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,67 @@ | ||||||
|  | with import ./config.nix; | ||||||
|  | 
 | ||||||
|  | rec { | ||||||
|  | 
 | ||||||
|  |   a = mkDerivation { | ||||||
|  |     name = "multiple-outputs-a"; | ||||||
|  |     outputs = [ "first" "second" ]; | ||||||
|  |     builder = builtins.toFile "builder.sh" | ||||||
|  |       '' | ||||||
|  |         mkdir $first $second | ||||||
|  |         test -z $all | ||||||
|  |         echo "second" > $first/file | ||||||
|  |         echo "first" > $second/file | ||||||
|  |       ''; | ||||||
|  |     helloString = "Hello, world!"; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   b = mkDerivation { | ||||||
|  |     defaultOutput = assert a.second.helloString == "Hello, world!"; a; | ||||||
|  |     firstOutput = assert a.outputName == "first"; a.first.first; | ||||||
|  |     secondOutput = assert a.second.outputName == "second"; a.second.first.first.second.second.first.second; | ||||||
|  |     allOutputs = a.all; | ||||||
|  |     name = "multiple-outputs-b"; | ||||||
|  |     builder = builtins.toFile "builder.sh" | ||||||
|  |       '' | ||||||
|  |         mkdir $out | ||||||
|  |         test "$firstOutput $secondOutput" = "$allOutputs" | ||||||
|  |         test "$defaultOutput" = "$firstOutput" | ||||||
|  |         test "$(cat $firstOutput/file)" = "second" | ||||||
|  |         test "$(cat $secondOutput/file)" = "first" | ||||||
|  |         echo "success" > $out/file | ||||||
|  |       ''; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   c = mkDerivation { | ||||||
|  |     name = "multiple-outputs-c"; | ||||||
|  |     drv = b.drvPath; | ||||||
|  |     builder = builtins.toFile "builder.sh" | ||||||
|  |       '' | ||||||
|  |         mkdir $out | ||||||
|  |         ln -s $drv $out/drv | ||||||
|  |       ''; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   d = mkDerivation { | ||||||
|  |     name = "multiple-outputs-d"; | ||||||
|  |     drv = builtins.unsafeDiscardOutputDependency b.drvPath; | ||||||
|  |     builder = builtins.toFile "builder.sh" | ||||||
|  |       '' | ||||||
|  |         mkdir $out | ||||||
|  |         echo $drv > $out/drv | ||||||
|  |       ''; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   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 | ||||||
|  |       ''; | ||||||
|  |   }).a; | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								tests/multiple-outputs.sh
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								tests/multiple-outputs.sh
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | ||||||
|  | source common.sh | ||||||
|  | 
 | ||||||
|  | clearStore | ||||||
|  | 
 | ||||||
|  | # Test whether read-only evaluation works when referring to the | ||||||
|  | # ‘drvPath’ attribute. | ||||||
|  | echo "evaluating c..." | ||||||
|  | #drvPath=$(nix-instantiate multiple-outputs.nix -A c --readonly-mode) | ||||||
|  | 
 | ||||||
|  | # And check whether the resulting derivation explicitly depends on all | ||||||
|  | # outputs. | ||||||
|  | drvPath=$(nix-instantiate multiple-outputs.nix -A c) | ||||||
|  | #[ "$drvPath" = "$drvPath2" ] | ||||||
|  | grep -q 'multiple-outputs-a.drv",\["first","second"\]' $drvPath | ||||||
|  | grep -q 'multiple-outputs-b.drv",\["out"\]' $drvPath | ||||||
|  | 
 | ||||||
|  | # While we're at it, test the ‘unsafeDiscardOutputDependency’ primop. | ||||||
|  | outPath=$(nix-build multiple-outputs.nix -A d) | ||||||
|  | drvPath=$(cat $outPath/drv) | ||||||
|  | outPath=$(nix-store -q $drvPath) | ||||||
|  | ! [ -e "$outPath" ] | ||||||
|  | 
 | ||||||
|  | # Do a build of something that depends on a derivation with multiple | ||||||
|  | # outputs. | ||||||
|  | echo "building b..." | ||||||
|  | outPath=$(nix-build multiple-outputs.nix -A b) | ||||||
|  | echo "output path is $outPath" | ||||||
|  | [ "$(cat "$outPath"/file)" = "success" ] | ||||||
|  | 
 | ||||||
|  | # Make sure that nix-build works on derivations with multiple outputs. | ||||||
|  | echo "building a.first..." | ||||||
|  | nix-build multiple-outputs.nix -A a.first | ||||||
|  | 
 | ||||||
|  | # Cyclic outputs should be rejected. | ||||||
|  | echo "building cyclic..." | ||||||
|  | if nix-build multiple-outputs.nix -A cyclic; then | ||||||
|  |     echo "Cyclic outputs incorrectly accepted!" | ||||||
|  |     exit 1 | ||||||
|  | fi | ||||||
|  | 
 | ||||||
|  | echo "collecting garbage..." | ||||||
|  | nix-store --gc | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue