chore: drop experimental feature recursive-nix

This was discussed as part of https://git.lix.systems/lix-project/lix/issues/767
with a one month long comment period.

This commit removes the recursive-nix feature from Lix. It has seen
limited usage and is difficult to stabilize, especially on non-Linux
systems where its behavior is underspecified.

Maintaining this feature complicates core work on the store, as we must
account for the potential presence of the daemon in the sandbox, adding
unnecessary complexity. Additionally, its inclusion in the
platform-independent local store creates risks for non-Linux platforms.

For more details on this removal, refer to the release note entry or the
issue entry.

Change-Id: I9137202f563c0a317f9c5da79cd9fd07d801427a
Signed-off-by: Raito Bezarius <raito@lix.systems>
This commit is contained in:
Raito Bezarius
2025-04-25 17:09:01 +02:00
parent 834133c1d5
commit 5ca168f6f2
16 changed files with 36 additions and 770 deletions
+23
View File
@@ -0,0 +1,23 @@
---
synopsis: "Removal of the `recursive-nix` experimental feature"
issues: [fj#767]
cls: [2872]
category: "Breaking Changes"
credits: ["raito"]
---
The `recursive-nix` experimental feature and all associated code have been removed.
`recursive-nix` enabled running Nix operations (like evaluations and builds) *inside* a derivation builder. This worked by spawning a temporary Nix daemon socket within the build environment, allowing the derivation to emit outputs that appeared in the outer store. This was primarily used to prototype **dynamic derivations** (dyndrvs), where build plans are generated on-the-fly during a build.
However, this approach introduced critical issues:
- It entrenched the legacy Nix daemon protocol as part of the derivation ABI, which is a blocker for future stabilization.
- It imposed tight coupling between sandbox setup code and knowledge of Nix internals, complicating refactoring and long-term maintenance.
- It was never intended to be the final design for dynamic derivations. The original Nix implementation team, who are leading dyndrv development, have agreed it will be replaced (likely via `varlink` or similar) before any stabilization.
- There is currently no known usage of `recursive-nix` on `lix` or elsewhere **in production**.
If you're using `recursive-nix` for something niche or experimental, we'd love to hear from you on the RFD issue.
You can still run `nix` inside a builder manually if needed — including with isolated user namespaces and fake stores — but the special daemon-handshake machinery is gone.
This removal unblocks several important internal cleanups.
+4 -492
View File
@@ -110,7 +110,6 @@ LocalDerivationGoal::~LocalDerivationGoal() noexcept(false)
/* Careful: we should never ever throw an exception from a
destructor. */
try { killChild(); } catch (...) { ignoreExceptionInDestructor(); }
try { stopDaemon(); } catch (...) { ignoreExceptionInDestructor(); }
try { deleteTmpDir(false, true); } catch (...) { ignoreExceptionInDestructor(); }
}
@@ -330,8 +329,6 @@ void LocalDerivationGoal::cleanupHookFinally()
void LocalDerivationGoal::cleanupPreChildKill()
{
sandboxMountNamespace.reset();
sandboxUserNamespace.reset();
}
@@ -343,9 +340,6 @@ void LocalDerivationGoal::cleanupPostChildKill()
open and modifies them after they have been chown'ed to
root. */
killSandbox(true);
/* Terminate the recursive Nix daemon. */
stopDaemon();
}
@@ -734,11 +728,6 @@ try {
}
}
/* Fire up a Nix daemon to process recursive Nix calls from the
builder. */
if (parsedDrv->getRequiredSystemFeatures().count("recursive-nix"))
startDaemon();
/* Run the builder. */
printMsg(lvlChatty, "executing builder '%1%'", drv->builder);
printMsg(lvlChatty, "using builder args '%1%'", concatStringsSep(" ", drv->args));
@@ -979,486 +968,6 @@ try {
co_return result::current_exception();
}
static StorePath pathPartOfReq(const SingleDerivedPath & req)
{
return std::visit(overloaded {
[&](const SingleDerivedPath::Opaque & bo) {
return bo.path;
},
[&](const SingleDerivedPath::Built & bfd) {
return pathPartOfReq(*bfd.drvPath);
},
}, req.raw());
}
static StorePath pathPartOfReq(const DerivedPath & req)
{
return std::visit(overloaded {
[&](const DerivedPath::Opaque & bo) {
return bo.path;
},
[&](const DerivedPath::Built & bfd) {
return pathPartOfReq(*bfd.drvPath);
},
}, req.raw());
}
bool LocalDerivationGoal::isAllowed(const DerivedPath & req)
{
return this->isAllowed(pathPartOfReq(req));
}
struct RestrictedStoreConfig final : LocalFSStoreConfig
{
using LocalFSStoreConfig::LocalFSStoreConfig;
const std::string name() override { return "Restricted Store"; }
};
/* A wrapper around LocalStore that only allows building/querying of
paths that are in the input closures of the build or were added via
recursive Nix calls. */
struct RestrictedStore : public virtual IndirectRootStore, public virtual GcStore
{
RestrictedStoreConfig config_;
RestrictedStoreConfig & config() override { return config_; }
const RestrictedStoreConfig & config() const override { return config_; }
ref<LocalStore> next;
LocalDerivationGoal & goal;
RestrictedStore(RestrictedStoreConfig config, ref<LocalStore> next, LocalDerivationGoal & goal)
: Store(config)
, config_(std::move(config))
, next(next), goal(goal)
{ }
Path getRealStoreDir() override
{ return next->config().realStoreDir; }
std::string getUri() override
{ return next->getUri(); }
kj::Promise<Result<StorePathSet>> queryAllValidPaths() override
try {
StorePathSet paths;
for (auto & p : goal.inputPaths) paths.insert(p);
for (auto & p : goal.addedPaths) paths.insert(p);
co_return paths;
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<std::shared_ptr<const ValidPathInfo>>>
queryPathInfoUncached(const StorePath & path) override
try {
if (goal.isAllowed(path)) {
try {
/* Censor impure information. */
auto info = std::make_shared<ValidPathInfo>(*TRY_AWAIT(next->queryPathInfo(path)));
info->deriver.reset();
info->registrationTime = 0;
info->ultimate = false;
info->sigs.clear();
co_return info;
} catch (InvalidPath &) {
co_return result::success(nullptr);
}
} else
co_return result::success(nullptr);
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<void>>
queryReferrers(const StorePath & path, StorePathSet & referrers) override
{ return {result::success()}; }
kj::Promise<Result<std::map<std::string, std::optional<StorePath>>>>
queryPartialDerivationOutputMap(const StorePath & path, Store * evalStore = nullptr) override
try {
if (!goal.isAllowed(path))
throw InvalidPath("cannot query output map for unknown path '%s' in recursive Nix", printStorePath(path));
co_return TRY_AWAIT(next->queryPartialDerivationOutputMap(path, evalStore));
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<std::optional<StorePath>>>
queryPathFromHashPart(const std::string & hashPart) override
try {
throw Error("queryPathFromHashPart");
} catch (...) {
return {result::current_exception()};
}
kj::Promise<Result<StorePath>> addToStoreRecursive(
std::string_view name,
const PreparedDump & source,
HashType hashAlgo,
RepairFlag repair) override
try { throw Error("addToStoreRecursive"); } catch (...) { return {result::current_exception()}; }
kj::Promise<Result<StorePath>> addToStoreFlat(
std::string_view name,
const Path & srcPath,
HashType hashAlgo,
RepairFlag repair) override
try { throw Error("addToStoreFlat"); } catch (...) { return {result::current_exception()}; }
kj::Promise<Result<void>> addToStore(const ValidPathInfo & info, AsyncInputStream & narSource,
RepairFlag repair = NoRepair, CheckSigsFlag checkSigs = CheckSigs) override
try {
TRY_AWAIT(next->addToStore(info, narSource, repair, checkSigs));
goal.addDependency(info.path);
co_return result::success();
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<StorePath>> addTextToStore(
std::string_view name,
std::string_view s,
const StorePathSet & references,
RepairFlag repair = NoRepair) override
try {
auto path = TRY_AWAIT(next->addTextToStore(name, s, references, repair));
goal.addDependency(path);
co_return path;
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<StorePath>> addToStoreFromDump(
AsyncInputStream & dump,
std::string_view name,
FileIngestionMethod method,
HashType hashAlgo,
RepairFlag repair,
const StorePathSet & references) override
try {
auto path = TRY_AWAIT(next->addToStoreFromDump(dump, name, method, hashAlgo, repair, references));
goal.addDependency(path);
co_return path;
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<box_ptr<Source>>> narFromPath(const StorePath & path) override
try {
if (!goal.isAllowed(path))
throw InvalidPath("cannot dump unknown path '%s' in recursive Nix", printStorePath(path));
co_return TRY_AWAIT(LocalFSStore::narFromPath(path));
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<void>> ensurePath(const StorePath & path) override
try {
if (!goal.isAllowed(path))
throw InvalidPath("cannot substitute unknown path '%s' in recursive Nix", printStorePath(path));
/* Nothing to be done; 'path' must already be valid. */
return {result::success()};
} catch (...) {
return {result::current_exception()};
}
kj::Promise<Result<void>> registerDrvOutput(const Realisation & info) override
// XXX: This should probably be allowed as a no-op if the realisation
// corresponds to an allowed derivation
try { throw Error("registerDrvOutput"); } catch (...) { return {result::current_exception()}; }
kj::Promise<Result<std::shared_ptr<const Realisation>>>
queryRealisationUncached(const DrvOutput & id) override
// XXX: This should probably be allowed if the realisation corresponds to
// an allowed derivation
try {
if (!goal.isAllowed(id))
co_return result::success(nullptr);
co_return TRY_AWAIT(next->queryRealisation(id));
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<void>> buildPaths(
const std::vector<DerivedPath> & paths,
BuildMode buildMode,
std::shared_ptr<Store> evalStore
) override
try {
for (auto & result : TRY_AWAIT(buildPathsWithResults(paths, buildMode, evalStore)))
if (!result.success())
result.rethrow();
co_return result::success();
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<std::vector<KeyedBuildResult>>> buildPathsWithResults(
const std::vector<DerivedPath> & paths,
BuildMode buildMode = bmNormal,
std::shared_ptr<Store> evalStore = nullptr) override
try {
assert(!evalStore);
if (buildMode != bmNormal) throw Error("unsupported build mode");
StorePathSet newPaths;
std::set<Realisation> newRealisations;
for (auto & req : paths) {
if (!goal.isAllowed(req))
throw InvalidPath("cannot build '%s' in recursive Nix because path is unknown", req.to_string(*next));
}
auto results = TRY_AWAIT(next->buildPathsWithResults(paths, buildMode));
for (auto & result : results) {
for (auto & [outputName, output] : result.builtOutputs) {
newPaths.insert(output.outPath);
newRealisations.insert(output);
}
}
StorePathSet closure;
TRY_AWAIT(next->computeFSClosure(newPaths, closure));
for (auto & path : closure)
goal.addDependency(path);
for (auto & real : TRY_AWAIT(Realisation::closure(*next, newRealisations)))
goal.addedDrvOutputs.insert(real.id);
co_return results;
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<BuildResult>> buildDerivation(
const StorePath & drvPath, const BasicDerivation & drv, BuildMode buildMode = bmNormal
) override
try { unsupported("buildDerivation"); } catch (...) { return {result::current_exception()}; }
kj::Promise<Result<void>> addTempRoot(const StorePath & path) override
{ return {result::success()}; }
kj::Promise<Result<void>> addIndirectRoot(const Path & path) override
{ return {result::success()}; }
kj::Promise<Result<Roots>> findRoots(bool censor) override
{ return {Roots()}; }
kj::Promise<Result<void>>
collectGarbage(const GCOptions & options, GCResults & results) override
{
return {result::success()};
}
kj::Promise<Result<void>>
addSignatures(const StorePath & storePath, const StringSet & sigs) override
try { unsupported("addSignatures"); } catch (...) { return {result::current_exception()}; }
kj::Promise<Result<void>> queryMissing(const std::vector<DerivedPath> & targets,
StorePathSet & willBuild, StorePathSet & willSubstitute, StorePathSet & unknown,
uint64_t & downloadSize, uint64_t & narSize) override
try {
/* This is slightly impure since it leaks information to the
client about what paths will be built/substituted or are
already present. Probably not a big deal. */
std::vector<DerivedPath> allowed;
for (auto & req : targets) {
if (goal.isAllowed(req))
allowed.emplace_back(req);
else
unknown.insert(pathPartOfReq(req));
}
TRY_AWAIT(next->queryMissing(allowed, willBuild, willSubstitute,
unknown, downloadSize, narSize));
co_return result::success();
} catch (...) {
co_return result::current_exception();
}
virtual kj::Promise<Result<std::optional<std::string>>> getBuildLogExact(const StorePath & path) override
{
return {std::nullopt};
}
virtual kj::Promise<Result<void>>
addBuildLog(const StorePath & path, std::string_view log) override
try {
unsupported("addBuildLog");
} catch (...) {
return {result::current_exception()};
}
kj::Promise<Result<std::optional<TrustedFlag>>> isTrustedClient() override
{ return {result::success(NotTrusted)}; }
};
void LocalDerivationGoal::startDaemon()
{
experimentalFeatureSettings.require(Xp::RecursiveNix);
StoreConfig::Params params;
params["path-info-cache-size"] = "0";
params["store"] = worker.store.config().storeDir;
if (auto & optRoot = getLocalStore().config().rootDir.get())
params["root"] = *optRoot;
params["state"] = "/no-such-path";
params["log"] = "/no-such-path";
auto store = make_ref<RestrictedStore>(
params,
ref<LocalStore>::unsafeFromPtr(
std::dynamic_pointer_cast<LocalStore>(worker.store.shared_from_this())
),
*this
);
addedPaths.clear();
auto socketName = ".nix-socket";
Path socketPath = tmpDir + "/" + socketName;
env["NIX_REMOTE"] = "unix://" + tmpDirInSandbox + "/" + socketName;
daemonSocket = createUnixDomainSocket(socketPath, 0600);
chownToBuilder(socketPath);
daemonThread = std::thread([this, store]() {
setCurrentThreadName("recursive nix daemon");
while (true) {
/* Accept a connection. */
struct sockaddr_un remoteAddr;
socklen_t remoteAddrLen = sizeof(remoteAddr);
AutoCloseFD remote{accept(daemonSocket.get(),
reinterpret_cast<struct sockaddr *>(&remoteAddr), &remoteAddrLen)};
if (!remote) {
if (errno == EINTR || errno == EAGAIN) continue;
if (errno == EINVAL || errno == ECONNABORTED) break;
throw SysError("accepting connection");
}
closeOnExec(remote.get());
debug("received daemon connection");
auto workerThread = std::thread([store, remote{std::move(remote)}]() {
setCurrentThreadName("recursive nix worker");
FdSource from(remote.get());
FdSink to(remote.get());
try {
AsyncIoRoot aio;
daemon::processConnection(aio, store, from, to,
NotTrusted, daemon::Recursive);
debug("terminated daemon connection");
} catch (SysError &) {
ignoreExceptionExceptInterrupt();
}
});
daemonWorkerThreads.push_back(std::move(workerThread));
}
debug("daemon shutting down");
});
}
void LocalDerivationGoal::stopDaemon()
{
if (daemonSocket && shutdown(daemonSocket.get(), SHUT_RDWR) == -1) {
// According to the POSIX standard, the 'shutdown' function should
// return an ENOTCONN error when attempting to shut down a socket that
// hasn't been connected yet. This situation occurs when the 'accept'
// function is called on a socket without any accepted connections,
// leaving the socket unconnected. While Linux doesn't seem to produce
// an error for sockets that have only been accepted, more
// POSIX-compliant operating systems like OpenBSD, macOS, and others do
// return the ENOTCONN error. Therefore, we handle this error here to
// avoid raising an exception for compliant behaviour.
if (errno == ENOTCONN) {
daemonSocket.close();
} else {
throw SysError("shutting down daemon socket");
}
}
if (daemonThread.joinable())
daemonThread.join();
// FIXME: should prune worker threads more quickly.
// FIXME: shutdown the client socket to speed up worker termination.
for (auto & thread : daemonWorkerThreads)
thread.join();
daemonWorkerThreads.clear();
// release the socket.
daemonSocket.close();
}
void LocalDerivationGoal::addDependency(const StorePath & path)
{
if (isAllowed(path)) return;
addedPaths.insert(path);
/* If we're doing a sandbox build, then we have to make the path
appear in the sandbox. */
if (useChroot) {
debug("materialising '%s' in the sandbox", worker.store.printStorePath(path));
#if __linux__
Path source = worker.store.Store::toRealPath(path);
Path target = chrootRootDir + worker.store.printStorePath(path);
if (pathExists(target)) {
// There is a similar debug message in bindPath, so only run it in this block to not have double messages.
debug("bind-mounting %s -> %s", target, source);
throw Error("store path '%s' already exists in the sandbox", worker.store.printStorePath(path));
}
/* Bind-mount the path into the sandbox. This requires
entering its mount namespace, which is not possible
in multithreaded programs. So we do this in a
child process.*/
Pid child = startProcess([&]() {
if (usingUserNamespace && (setns(sandboxUserNamespace.get(), 0) == -1))
throw SysError("entering sandbox user namespace");
if (setns(sandboxMountNamespace.get(), 0) == -1)
throw SysError("entering sandbox mount namespace");
bindPath(source, target);
_exit(0);
});
int status = child.wait();
if (status != 0)
throw Error("could not add path '%s' to sandbox", worker.store.printStorePath(path));
#else
throw Error("don't know how to make path '%s' (produced by a recursive Nix call) appear in the sandbox",
worker.store.printStorePath(path));
#endif
}
}
void LocalDerivationGoal::chownToBuilder(const Path & path)
{
if (!buildUser) return;
@@ -1692,6 +1201,10 @@ void LocalDerivationGoal::runChild()
if (!parsedDrv->useUidRange())
chmodPath(chrootRootDir + "/etc", 0555);
/* The comment below is now outdated. Recursive Nix has been removed.
* So there's no need to make path appear in the sandbox.
* TODO(Raito): cleanup before a merge.
*/
/* Unshare this mount namespace. This is necessary because
pivot_root() below changes the root of the mount
namespace. This means that the call to setns() in
@@ -2010,7 +1523,6 @@ try {
StorePathSet referenceablePaths;
for (auto & p : inputPaths) referenceablePaths.insert(p);
for (auto & i : scratchOutputs) referenceablePaths.insert(i.second);
for (auto & p : addedPaths) referenceablePaths.insert(p);
/* FIXME `needsHashRewrite` should probably be removed and we get to the
real reason why we aren't using the chroot dir */
@@ -47,13 +47,6 @@ struct LocalDerivationGoal : public DerivationGoal
*/
Pipe userNamespaceSync;
/**
* The mount namespace and user namespace of the builder, used to add additional
* paths to the sandbox as a result of recursive Nix calls.
*/
AutoCloseFD sandboxMountNamespace;
AutoCloseFD sandboxUserNamespace;
/**
* On Linux, whether we're doing the build in its own user
* namespace.
@@ -134,50 +127,11 @@ struct LocalDerivationGoal : public DerivationGoal
const static Path homeDir;
/**
* The recursive Nix daemon socket.
*/
AutoCloseFD daemonSocket;
/**
* The daemon main thread.
*/
std::thread daemonThread;
/**
* The daemon worker threads.
*/
std::vector<std::thread> daemonWorkerThreads;
/**
* Paths that were added via recursive Nix calls.
*/
StorePathSet addedPaths;
/**
* Realisations that were added via recursive Nix calls.
*/
std::set<DrvOutput> addedDrvOutputs;
/**
* Recursive Nix calls are only allowed to build or realize paths
* in the original input closure or added via a recursive Nix call
* (so e.g. you can't do 'nix-store -r /nix/store/<bla>' where
* /nix/store/<bla> is some arbitrary path in a binary cache).
*/
bool isAllowed(const StorePath & path)
{
return inputPaths.count(path) || addedPaths.count(path);
}
bool isAllowed(const DrvOutput & id)
{
return addedDrvOutputs.count(id);
}
bool isAllowed(const DerivedPath & req);
friend struct RestrictedStore;
/**
* Create a LocalDerivationGoal without an on-disk .drv file,
* possibly a platform-specific subclass
@@ -236,16 +190,6 @@ struct LocalDerivationGoal : public DerivationGoal
*/
kj::Promise<Result<void>> writeStructuredAttrs();
void startDaemon();
void stopDaemon();
/**
* Add 'path' to the set of paths that may be referenced by the
* outputs, and make it appear in the sandbox.
*/
void addDependency(const StorePath & path);
/**
* Make a file owned by the builder.
*/
+6 -14
View File
@@ -264,7 +264,7 @@ struct ClientSettings
};
static void performOp(AsyncIoRoot & aio, TunnelLogger * logger, ref<Store> store,
TrustedFlag trusted, RecursiveFlag recursive, WorkerProto::Version clientVersion,
TrustedFlag trusted, WorkerProto::Version clientVersion,
Source & from, BufferedSink & to, WorkerProto::Op op)
{
WorkerProto::ReadConn rconn{from, clientVersion};
@@ -773,12 +773,7 @@ static void performOp(AsyncIoRoot & aio, TunnelLogger * logger, ref<Store> store
}
logger->startWork();
// FIXME: use some setting in recursive mode. Will need to use
// non-global variables.
if (!recursive)
clientSettings.apply(trusted);
clientSettings.apply(trusted);
logger->stopWork();
break;
}
@@ -1014,10 +1009,9 @@ void processConnection(
ref<Store> store,
FdSource & from,
FdSink & to,
TrustedFlag trusted,
RecursiveFlag recursive)
TrustedFlag trusted)
{
auto monitor = !recursive ? std::make_unique<MonitorFdHup>(from.fd) : nullptr;
auto monitor = std::make_unique<MonitorFdHup>(from.fd);
/* Exchange the greeting. */
unsigned int magic = readInt(from);
@@ -1031,9 +1025,7 @@ void processConnection(
auto tunnelLogger = new TunnelLogger(to, clientVersion);
auto prevLogger = nix::logger;
// FIXME
if (!recursive)
logger = tunnelLogger;
logger = tunnelLogger;
unsigned int opCount = 0;
@@ -1089,7 +1081,7 @@ void processConnection(
debug("performing daemon worker op: %d", op);
try {
performOp(aio, tunnelLogger, store, trusted, recursive, clientVersion, from, to, op);
performOp(aio, tunnelLogger, store, trusted, clientVersion, from, to, op);
} catch (Error & e) {
/* If we're not in a state where we can send replies, then
something went wrong processing the input of the
+1 -4
View File
@@ -7,14 +7,11 @@
namespace nix::daemon {
enum RecursiveFlag : bool { NotRecursive = false, Recursive = true };
void processConnection(
AsyncIoRoot & aio,
ref<Store> store,
FdSource & from,
FdSink & to,
TrustedFlag trusted,
RecursiveFlag recursive);
TrustedFlag trusted);
}
-12
View File
@@ -973,18 +973,6 @@ Pid LinuxLocalDerivationGoal::startChild(std::function<void()> openSlave)
"nixbld:!:%1%:\n"
"nogroup:x:65534:\n", sandboxGid()));
/* Save the mount- and user namespace of the child. We have to do this
*before* the child does a chroot. */
sandboxMountNamespace = AutoCloseFD{open(fmt("/proc/%d/ns/mnt", pid.get()).c_str(), O_RDONLY)};
if (sandboxMountNamespace.get() == -1)
throw SysError("getting sandbox mount namespace");
if (usingUserNamespace) {
sandboxUserNamespace = AutoCloseFD{open(fmt("/proc/%d/ns/user", pid.get()).c_str(), O_RDONLY)};
if (sandboxUserNamespace.get() == -1)
throw SysError("getting sandbox user namespace");
}
/* Move the child into its own cgroup. */
if (cgroup)
writeFile(*cgroup + "/cgroup.procs", fmt("%d", pid.get()));
-3
View File
@@ -490,9 +490,6 @@ StringSet StoreConfig::getDefaultSystemFeatures()
if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations))
res.insert("ca-derivations");
if (experimentalFeatureSettings.isEnabled(Xp::RecursiveNix))
res.insert("recursive-nix");
return res;
}
@@ -1,39 +0,0 @@
---
name: recursive-nix
internalName: RecursiveNix
---
Allow derivation builders to call Nix, and thus build derivations
recursively.
Example:
```
with import <nixpkgs> {};
runCommand "foo"
{
buildInputs = [ nix jq ];
NIX_PATH = "nixpkgs=${<nixpkgs>}";
}
''
hello=$(nix-build -E '(import <nixpkgs> {}).hello.overrideDerivation (args: { name = "recursive-hello"; })')
mkdir -p $out/bin
ln -s $hello/bin/hello $out/bin/hello
''
```
An important restriction on recursive builders is disallowing
arbitrary substitutions. For example, running
```
nix-store -r /nix/store/kmwd1hq55akdb9sc7l3finr175dajlby-hello-2.10
```
in the above `runCommand` script would be disallowed, as this could
lead to derivations with hidden dependencies or breaking
reproducibility by relying on the current state of the Nix store. An
exception would be if
`/nix/store/kmwd1hq55akdb9sc7l3finr175dajlby-hello-2.10` were
already in the build inputs or built by a previous recursive Nix
call.
-1
View File
@@ -154,7 +154,6 @@ experimental_feature_definitions = files(
'experimental-features/parse-toml-timestamps.md',
'experimental-features/pipe-operator.md',
'experimental-features/read-only-local-store.md',
'experimental-features/recursive-nix.md',
'experimental-features/repl-automation.md',
# keep-sorted end
)
+2 -2
View File
@@ -368,7 +368,7 @@ static void daemonLoopImpl(std::optional<TrustedFlag> forceTrustClientOpt)
FdSource from(remote.get());
FdSink to(remote.get());
processConnection(
aio, aio.blockOn(openUncachedStore()), from, to, trusted, NotRecursive
aio, aio.blockOn(openUncachedStore()), from, to, trusted
);
exit(0);
@@ -451,7 +451,7 @@ processStdioConnection(AsyncIoRoot & aio, ref<Store> store, TrustedFlag trustCli
{
FdSource from(STDIN_FILENO);
FdSink to(STDOUT_FILENO);
processConnection(aio, store, from, to, trustClient, NotRecursive);
processConnection(aio, store, from, to, trustClient);
}
/**
-9
View File
@@ -1,9 +0,0 @@
#!/usr/bin/env bash
source common.sh
requireDaemonNewerThan "2.4pre20210623"
export NIX_TESTS_CA_BY_DEFAULT=1
cd ..
source ./recursive.sh
@@ -1,33 +0,0 @@
with import ./config.nix;
let innerName = "foo"; in
mkDerivation rec {
name = "${innerName}.drv";
SHELL = shell;
requiredSystemFeatures = [ "recursive-nix" ];
drv = builtins.unsafeDiscardOutputDependency (import ./text-hashed-output.nix).hello.drvPath;
buildCommand = ''
export NIX_CONFIG='experimental-features = nix-command ca-derivations'
PATH=${builtins.getEnv "EXTRA_PATH"}:$PATH
# JSON of pre-existing drv
nix derivation show $drv | jq .[] > drv0.json
# Fix name
jq < drv0.json '.name = "${innerName}"' > drv1.json
# Extend `buildCommand`
jq < drv1.json '.env.buildCommand += "echo \"I am alive!\" >> $out/hello\n"' > drv0.json
# Used as our output
cp $(nix derivation add < drv0.json) $out
'';
__contentAddressed = true;
outputHashMode = "text";
outputHashAlgo = "sha256";
}
@@ -1,27 +0,0 @@
source common.sh
# FIXME
if [[ $(uname) != Linux ]]; then skipTest "Not running Linux"; fi
export NIX_TESTS_CA_BY_DEFAULT=1
enableFeatures 'recursive-nix'
restartDaemon
clearStore
rm -f $TEST_ROOT/result
EXTRA_PATH=$(dirname $(type -p nix)):$(dirname $(type -p jq))
export EXTRA_PATH
# Will produce a drv
metaDrv=$(nix-instantiate ./recursive-mod-json.nix)
# computed "dynamic" derivation
drv=$(nix-store -r $metaDrv)
# build that dyn drv
res=$(nix-store -r $drv)
grep 'I am alive!' $res/hello
-3
View File
@@ -49,14 +49,12 @@ functional_tests_scripts = [
'ca/nix-run.sh',
'ca/nix-shell.sh',
'ca/post-hook.sh',
'ca/recursive.sh',
'ca/repl.sh',
'ca/selfref-gc.sh',
'ca/signatures.sh',
'ca/substitute.sh',
'ca/why-depends.sh',
'dyn-drv/text-hashed-output.sh',
'dyn-drv/recursive-mod-json.sh',
'dyn-drv/build-built-drv.sh',
'dyn-drv/eval-outputOf.sh',
'dyn-drv/dep-built-drv.sh',
@@ -127,7 +125,6 @@ functional_tests_scripts = [
'flakes/search-root.sh',
'readfile-context.sh',
'nix-channel.sh',
'recursive.sh',
'dependencies.sh',
'check-reqs.sh',
'build-remote-content-addressed-fixed.sh',
-56
View File
@@ -1,56 +0,0 @@
with import ./config.nix;
mkDerivation rec {
name = "recursive";
dummy = builtins.toFile "dummy" "bla bla";
SHELL = shell;
# Note: this is a string without context.
unreachable = builtins.getEnv "unreachable";
NIX_TESTS_CA_BY_DEFAULT = builtins.getEnv "NIX_TESTS_CA_BY_DEFAULT";
requiredSystemFeatures = [ "recursive-nix" ];
buildCommand = ''
mkdir $out
opts="--experimental-features nix-command ${if (NIX_TESTS_CA_BY_DEFAULT == "1") then "--extra-experimental-features ca-derivations" else ""}"
PATH=${builtins.getEnv "NIX_BIN_DIR"}:$PATH
# Check that we can query/build paths in our input closure.
nix $opts path-info $dummy
nix $opts build $dummy
# Make sure we cannot query/build paths not in out input closure.
[[ -e $unreachable ]]
(! nix $opts path-info $unreachable)
(! nix $opts build $unreachable)
# Add something to the store.
echo foobar > foobar
foobar=$(nix $opts store add-path ./foobar)
nix $opts path-info $foobar
nix $opts build $foobar
# Add it to our closure.
ln -s $foobar $out/foobar
[[ $(nix $opts path-info --all | wc -l) -eq 4 ]]
# Build a derivation.
nix $opts build -L --impure --expr '
with import ${./config.nix};
mkDerivation {
name = "inner1";
buildCommand = "echo $fnord blaat > $out";
fnord = builtins.toFile "fnord" "fnord";
}
'
[[ $(nix $opts path-info --json ./result) =~ fnord ]]
ln -s $(nix $opts path-info ./result) $out/inner1
'';
}
-19
View File
@@ -1,19 +0,0 @@
source common.sh
enableFeatures 'recursive-nix'
restartDaemon
clearStore
rm -f $TEST_ROOT/result
export unreachable=$(nix store add-path ./recursive.sh)
NIX_BIN_DIR=$(dirname $(type -p nix)) nix --extra-experimental-features 'nix-command recursive-nix' build -o $TEST_ROOT/result -L --impure --file ./recursive.nix
[[ $(cat $TEST_ROOT/result/inner1) =~ blaat ]]
# Make sure the recursively created paths are in the closure.
nix path-info -r $TEST_ROOT/result | grep foobar
nix path-info -r $TEST_ROOT/result | grep fnord
nix path-info -r $TEST_ROOT/result | grep inner1