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:
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
'';
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user