mirror of
https://github.com/NixOS/nix
synced 2025-07-07 01:51:47 +02:00
Merge remote-tracking branch 'origin/master' into parallel-nix-copy
This commit is contained in:
commit
4c96761c2b
73 changed files with 1353 additions and 1047 deletions
|
@ -616,6 +616,8 @@ InstallableFlake::InstallableFlake(
|
|||
|
||||
std::tuple<std::string, FlakeRef, InstallableValue::DerivationInfo> InstallableFlake::toDerivation()
|
||||
{
|
||||
Activity act(*logger, lvlTalkative, actUnknown, fmt("evaluating derivation '%s'", what()));
|
||||
|
||||
auto attr = getCursor(*state);
|
||||
|
||||
auto attrPath = attr->getAttrPathStr();
|
||||
|
|
|
@ -35,6 +35,7 @@ extern "C" {
|
|||
#include "finally.hh"
|
||||
#include "markdown.hh"
|
||||
#include "local-fs-store.hh"
|
||||
#include "progress-bar.hh"
|
||||
|
||||
#if HAVE_BOEHMGC
|
||||
#define GC_INCLUDE_NEW
|
||||
|
@ -252,6 +253,10 @@ void NixRepl::mainLoop()
|
|||
rl_set_list_possib_func(listPossibleCallback);
|
||||
#endif
|
||||
|
||||
/* Stop the progress bar because it interferes with the display of
|
||||
the repl. */
|
||||
stopProgressBar();
|
||||
|
||||
std::string input;
|
||||
|
||||
while (true) {
|
||||
|
@ -1037,9 +1042,10 @@ void runRepl(
|
|||
|
||||
struct CmdRepl : InstallablesCommand
|
||||
{
|
||||
CmdRepl(){
|
||||
CmdRepl() {
|
||||
evalSettings.pureEval = false;
|
||||
}
|
||||
|
||||
void prepare()
|
||||
{
|
||||
if (!settings.isExperimentalFeatureEnabled(Xp::ReplFlake) && !(file) && this->_installables.size() >= 1) {
|
||||
|
@ -1053,12 +1059,15 @@ struct CmdRepl : InstallablesCommand
|
|||
}
|
||||
installables = InstallablesCommand::load();
|
||||
}
|
||||
|
||||
std::vector<std::string> files;
|
||||
|
||||
Strings getDefaultFlakeAttrPaths() override
|
||||
{
|
||||
return {""};
|
||||
}
|
||||
virtual bool useDefaultInstallables() override
|
||||
|
||||
bool useDefaultInstallables() override
|
||||
{
|
||||
return file.has_value() or expr.has_value();
|
||||
}
|
||||
|
|
|
@ -2501,18 +2501,18 @@ void EvalState::printStats()
|
|||
}
|
||||
{
|
||||
auto list = topObj.list("functions");
|
||||
for (auto & i : functionCalls) {
|
||||
for (auto & [fun, count] : functionCalls) {
|
||||
auto obj = list.object();
|
||||
if (i.first->name)
|
||||
obj.attr("name", (const std::string &) i.first->name);
|
||||
if (fun->name)
|
||||
obj.attr("name", (std::string_view) symbols[fun->name]);
|
||||
else
|
||||
obj.attr("name", nullptr);
|
||||
if (auto pos = positions[i.first->pos]) {
|
||||
obj.attr("file", (const std::string &) pos.file);
|
||||
if (auto pos = positions[fun->pos]) {
|
||||
obj.attr("file", (std::string_view) pos.file);
|
||||
obj.attr("line", pos.line);
|
||||
obj.attr("column", pos.column);
|
||||
}
|
||||
obj.attr("count", i.second);
|
||||
obj.attr("count", count);
|
||||
}
|
||||
}
|
||||
{
|
||||
|
|
|
@ -341,7 +341,6 @@ LockedFlake lockFlake(
|
|||
|
||||
debug("old lock file: %s", oldLockFile);
|
||||
|
||||
// FIXME: check whether all overrides are used.
|
||||
std::map<InputPath, FlakeInput> overrides;
|
||||
std::set<InputPath> overridesUsed, updatesUsed;
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ typedef std::string FlakeId;
|
|||
* object that fetcher generates (usually via
|
||||
* FlakeRef::fromAttrs(attrs) or parseFlakeRef(url) calls).
|
||||
*
|
||||
* The actual fetch not have been performed yet (i.e. a FlakeRef may
|
||||
* The actual fetch may not have been performed yet (i.e. a FlakeRef may
|
||||
* be lazy), but the fetcher can be invoked at any time via the
|
||||
* FlakeRef to ensure the store is populated with this input.
|
||||
*/
|
||||
|
|
|
@ -370,7 +370,7 @@ struct GitInputScheme : InputScheme
|
|||
auto gitDir = ".git";
|
||||
|
||||
runProgram("git", true,
|
||||
{ "-C", *sourcePath, "--git-dir", gitDir, "add", "--force", "--intent-to-add", "--", std::string(file) });
|
||||
{ "-C", *sourcePath, "--git-dir", gitDir, "add", "--intent-to-add", "--", std::string(file) });
|
||||
|
||||
if (commitMsg)
|
||||
runProgram("git", true,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include <map>
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
|
||||
namespace nix {
|
||||
|
||||
|
@ -48,6 +49,7 @@ private:
|
|||
bool visible = true;
|
||||
ActivityId parent;
|
||||
std::optional<std::string> name;
|
||||
std::chrono::time_point<std::chrono::steady_clock> startTime;
|
||||
};
|
||||
|
||||
struct ActivitiesByType
|
||||
|
@ -91,10 +93,11 @@ public:
|
|||
state_.lock()->active = isTTY;
|
||||
updateThread = std::thread([&]() {
|
||||
auto state(state_.lock());
|
||||
auto nextWakeup = std::chrono::milliseconds::max();
|
||||
while (state->active) {
|
||||
if (!state->haveUpdate)
|
||||
state.wait(updateCV);
|
||||
draw(*state);
|
||||
state.wait_for(updateCV, nextWakeup);
|
||||
nextWakeup = draw(*state);
|
||||
state.wait_for(quitCV, std::chrono::milliseconds(50));
|
||||
}
|
||||
});
|
||||
|
@ -118,7 +121,8 @@ public:
|
|||
updateThread.join();
|
||||
}
|
||||
|
||||
bool isVerbose() override {
|
||||
bool isVerbose() override
|
||||
{
|
||||
return printBuildLogs;
|
||||
}
|
||||
|
||||
|
@ -159,11 +163,13 @@ public:
|
|||
if (lvl <= verbosity && !s.empty() && type != actBuildWaiting)
|
||||
log(*state, lvl, s + "...");
|
||||
|
||||
state->activities.emplace_back(ActInfo());
|
||||
state->activities.emplace_back(ActInfo {
|
||||
.s = s,
|
||||
.type = type,
|
||||
.parent = parent,
|
||||
.startTime = std::chrono::steady_clock::now()
|
||||
});
|
||||
auto i = std::prev(state->activities.end());
|
||||
i->s = s;
|
||||
i->type = type;
|
||||
i->parent = parent;
|
||||
state->its.emplace(act, i);
|
||||
state->activitiesByType[type].its.emplace(act, i);
|
||||
|
||||
|
@ -327,10 +333,12 @@ public:
|
|||
updateCV.notify_one();
|
||||
}
|
||||
|
||||
void draw(State & state)
|
||||
std::chrono::milliseconds draw(State & state)
|
||||
{
|
||||
auto nextWakeup = std::chrono::milliseconds::max();
|
||||
|
||||
state.haveUpdate = false;
|
||||
if (!state.active) return;
|
||||
if (!state.active) return nextWakeup;
|
||||
|
||||
std::string line;
|
||||
|
||||
|
@ -341,12 +349,25 @@ public:
|
|||
line += "]";
|
||||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
if (!state.activities.empty()) {
|
||||
if (!status.empty()) line += " ";
|
||||
auto i = state.activities.rbegin();
|
||||
|
||||
while (i != state.activities.rend() && (!i->visible || (i->s.empty() && i->lastLine.empty())))
|
||||
while (i != state.activities.rend()) {
|
||||
if (i->visible && (!i->s.empty() || !i->lastLine.empty())) {
|
||||
/* Don't show activities until some time has
|
||||
passed, to avoid displaying very short
|
||||
activities. */
|
||||
auto delay = std::chrono::milliseconds(10);
|
||||
if (i->startTime + delay < now)
|
||||
break;
|
||||
else
|
||||
nextWakeup = std::min(nextWakeup, std::chrono::duration_cast<std::chrono::milliseconds>(delay - (now - i->startTime)));
|
||||
}
|
||||
++i;
|
||||
}
|
||||
|
||||
if (i != state.activities.rend()) {
|
||||
line += i->s;
|
||||
|
@ -366,6 +387,8 @@ public:
|
|||
if (width <= 0) width = std::numeric_limits<decltype(width)>::max();
|
||||
|
||||
writeToStderr("\r" + filterANSIEscapes(line, false, width) + ANSI_NORMAL + "\e[K");
|
||||
|
||||
return nextWakeup;
|
||||
}
|
||||
|
||||
std::string getStatus(State & state)
|
||||
|
|
|
@ -705,8 +705,7 @@ static void movePath(const Path & src, const Path & dst)
|
|||
if (changePerm)
|
||||
chmod_(src, st.st_mode | S_IWUSR);
|
||||
|
||||
if (rename(src.c_str(), dst.c_str()))
|
||||
throw SysError("renaming '%1%' to '%2%'", src, dst);
|
||||
renameFile(src, dst);
|
||||
|
||||
if (changePerm)
|
||||
chmod_(dst, st.st_mode);
|
||||
|
@ -914,12 +913,6 @@ void DerivationGoal::buildDone()
|
|||
outputPaths
|
||||
);
|
||||
|
||||
if (buildMode == bmCheck) {
|
||||
cleanupPostOutputsRegisteredModeCheck();
|
||||
done(BuildResult::Built, std::move(builtOutputs));
|
||||
return;
|
||||
}
|
||||
|
||||
cleanupPostOutputsRegisteredModeNonCheck();
|
||||
|
||||
/* Repeat the build if necessary. */
|
||||
|
|
|
@ -223,8 +223,7 @@ static void movePath(const Path & src, const Path & dst)
|
|||
if (changePerm)
|
||||
chmod_(src, st.st_mode | S_IWUSR);
|
||||
|
||||
if (rename(src.c_str(), dst.c_str()))
|
||||
throw SysError("renaming '%1%' to '%2%'", src, dst);
|
||||
renameFile(src, dst);
|
||||
|
||||
if (changePerm)
|
||||
chmod_(dst, st.st_mode);
|
||||
|
@ -311,7 +310,7 @@ bool LocalDerivationGoal::cleanupDecideWhetherDiskFull()
|
|||
if (buildMode != bmCheck && status.known->isValid()) continue;
|
||||
auto p = worker.store.printStorePath(status.known->path);
|
||||
if (pathExists(chrootRootDir + p))
|
||||
rename((chrootRootDir + p).c_str(), p.c_str());
|
||||
renameFile((chrootRootDir + p), p);
|
||||
}
|
||||
|
||||
return diskFull;
|
||||
|
@ -845,18 +844,43 @@ void LocalDerivationGoal::startBuilder()
|
|||
/* Some distros patch Linux to not allow unprivileged
|
||||
* user namespaces. If we get EPERM or EINVAL, try
|
||||
* without CLONE_NEWUSER and see if that works.
|
||||
* Details: https://salsa.debian.org/kernel-team/linux/-/commit/d98e00eda6bea437e39b9e80444eee84a32438a6
|
||||
*/
|
||||
usingUserNamespace = false;
|
||||
flags &= ~CLONE_NEWUSER;
|
||||
child = clone(childEntry, stack + stackSize, flags, this);
|
||||
}
|
||||
/* Otherwise exit with EPERM so we can handle this in the
|
||||
parent. This is only done when sandbox-fallback is set
|
||||
to true (the default). */
|
||||
if (child == -1 && (errno == EPERM || errno == EINVAL) && settings.sandboxFallback)
|
||||
_exit(1);
|
||||
if (child == -1) throw SysError("cloning builder process");
|
||||
|
||||
if (child == -1) {
|
||||
switch(errno) {
|
||||
case EPERM:
|
||||
case EINVAL: {
|
||||
int errno_ = errno;
|
||||
if (!userNamespacesEnabled && errno==EPERM)
|
||||
notice("user namespaces appear to be disabled; they are required for sandboxing; check /proc/sys/user/max_user_namespaces");
|
||||
if (userNamespacesEnabled) {
|
||||
Path procSysKernelUnprivilegedUsernsClone = "/proc/sys/kernel/unprivileged_userns_clone";
|
||||
if (pathExists(procSysKernelUnprivilegedUsernsClone)
|
||||
&& trim(readFile(procSysKernelUnprivilegedUsernsClone)) == "0") {
|
||||
notice("user namespaces appear to be disabled; they are required for sandboxing; check /proc/sys/kernel/unprivileged_userns_clone");
|
||||
}
|
||||
}
|
||||
Path procSelfNsUser = "/proc/self/ns/user";
|
||||
if (!pathExists(procSelfNsUser))
|
||||
notice("/proc/self/ns/user does not exist; your kernel was likely built without CONFIG_USER_NS=y, which is required for sandboxing");
|
||||
/* Otherwise exit with EPERM so we can handle this in the
|
||||
parent. This is only done when sandbox-fallback is set
|
||||
to true (the default). */
|
||||
if (settings.sandboxFallback)
|
||||
_exit(1);
|
||||
/* Mention sandbox-fallback in the error message so the user
|
||||
knows that having it disabled contributed to the
|
||||
unrecoverability of this failure */
|
||||
throw SysError(errno_, "creating sandboxed builder process using clone(), without sandbox-fallback");
|
||||
}
|
||||
default:
|
||||
throw SysError("creating sandboxed builder process using clone()");
|
||||
}
|
||||
}
|
||||
writeFull(builderOut.writeSide.get(),
|
||||
fmt("%d %d\n", usingUserNamespace, child));
|
||||
_exit(0);
|
||||
|
@ -2350,10 +2374,8 @@ DrvOutputs LocalDerivationGoal::registerOutputs()
|
|||
if (*scratchPath != finalPath) {
|
||||
// Also rewrite the output path
|
||||
auto source = sinkToSource([&](Sink & nextSink) {
|
||||
StringSink sink;
|
||||
dumpPath(actualPath, sink);
|
||||
RewritingSink rsink2(oldHashPart, std::string(finalPath.hashPart()), nextSink);
|
||||
rsink2(sink.s);
|
||||
dumpPath(actualPath, rsink2);
|
||||
rsink2.flush();
|
||||
});
|
||||
Path tmpPath = actualPath + ".tmp";
|
||||
|
@ -2600,8 +2622,7 @@ DrvOutputs LocalDerivationGoal::registerOutputs()
|
|||
Path prev = path + checkSuffix;
|
||||
deletePath(prev);
|
||||
Path dst = path + checkSuffix;
|
||||
if (rename(path.c_str(), dst.c_str()))
|
||||
throw SysError("renaming '%s' to '%s'", path, dst);
|
||||
renameFile(path, dst);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,8 +22,7 @@ void builtinUnpackChannel(const BasicDerivation & drv)
|
|||
auto entries = readDirectory(out);
|
||||
if (entries.size() != 1)
|
||||
throw Error("channel tarball '%s' contains more than one file", src);
|
||||
if (rename((out + "/" + entries[0].name).c_str(), (out + "/" + channelName).c_str()) == -1)
|
||||
throw SysError("renaming channel directory");
|
||||
renameFile((out + "/" + entries[0].name), (out + "/" + channelName));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -308,6 +308,9 @@ struct curlFileTransfer : public FileTransfer
|
|||
|
||||
curl_easy_setopt(req, CURLOPT_HTTPHEADER, requestHeaders);
|
||||
|
||||
if (settings.downloadSpeed.get() > 0)
|
||||
curl_easy_setopt(req, CURLOPT_MAX_RECV_SPEED_LARGE, (curl_off_t) (settings.downloadSpeed.get() * 1024));
|
||||
|
||||
if (request.head)
|
||||
curl_easy_setopt(req, CURLOPT_NOBODY, 1);
|
||||
|
||||
|
|
|
@ -39,9 +39,7 @@ static void makeSymlink(const Path & link, const Path & target)
|
|||
createSymlink(target, tempLink);
|
||||
|
||||
/* Atomically replace the old one. */
|
||||
if (rename(tempLink.c_str(), link.c_str()) == -1)
|
||||
throw SysError("cannot rename '%1%' to '%2%'",
|
||||
tempLink , link);
|
||||
renameFile(tempLink, link);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -114,7 +114,13 @@ std::vector<Path> getUserConfigFiles()
|
|||
|
||||
unsigned int Settings::getDefaultCores()
|
||||
{
|
||||
return std::max(1U, std::thread::hardware_concurrency());
|
||||
const unsigned int concurrency = std::max(1U, std::thread::hardware_concurrency());
|
||||
const unsigned int maxCPU = getMaxCPU();
|
||||
|
||||
if (maxCPU > 0)
|
||||
return maxCPU;
|
||||
else
|
||||
return concurrency;
|
||||
}
|
||||
|
||||
StringSet Settings::getDefaultSystemFeatures()
|
||||
|
|
|
@ -746,6 +746,13 @@ public:
|
|||
/nix/store/xfghy8ixrhz3kyy6p724iv3cxji088dx-bash-4.4-p23`.
|
||||
)"};
|
||||
|
||||
Setting<unsigned int> downloadSpeed {
|
||||
this, 0, "download-speed",
|
||||
R"(
|
||||
Specify the maximum transfer rate in kilobytes per second you want
|
||||
Nix to use for downloads.
|
||||
)"};
|
||||
|
||||
Setting<std::string> netrcFile{
|
||||
this, fmt("%s/%s", nixConfDir, "netrc"), "netrc-file",
|
||||
R"(
|
||||
|
|
|
@ -57,8 +57,7 @@ protected:
|
|||
AutoDelete del(tmp, false);
|
||||
StreamToSourceAdapter source(istream);
|
||||
writeFile(tmp, source);
|
||||
if (rename(tmp.c_str(), path2.c_str()))
|
||||
throw SysError("renaming '%1%' to '%2%'", tmp, path2);
|
||||
renameFile(tmp, path2);
|
||||
del.cancel();
|
||||
}
|
||||
|
||||
|
|
|
@ -1430,8 +1430,7 @@ StorePath LocalStore::addToStoreFromDump(Source & source0, std::string_view name
|
|||
writeFile(realPath, dumpSource);
|
||||
} else {
|
||||
/* Move the temporary path we restored above. */
|
||||
if (rename(tempPath.c_str(), realPath.c_str()))
|
||||
throw Error("renaming '%s' to '%s'", tempPath, realPath);
|
||||
moveFile(tempPath, realPath);
|
||||
}
|
||||
|
||||
/* For computing the nar hash. In recursive SHA-256 mode, this
|
||||
|
@ -1942,8 +1941,7 @@ void LocalStore::addBuildLog(const StorePath & drvPath, std::string_view log)
|
|||
|
||||
writeFile(tmpFile, compress("bzip2", log));
|
||||
|
||||
if (rename(tmpFile.c_str(), logPath.c_str()) != 0)
|
||||
throw SysError("renaming '%1%' to '%2%'", tmpFile, logPath);
|
||||
renameFile(tmpFile, logPath);
|
||||
}
|
||||
|
||||
std::optional<std::string> LocalStore::getVersion()
|
||||
|
|
|
@ -229,7 +229,9 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats,
|
|||
}
|
||||
|
||||
/* Atomically replace the old file with the new hard link. */
|
||||
if (rename(tempLink.c_str(), path.c_str()) == -1) {
|
||||
try {
|
||||
renameFile(tempLink, path);
|
||||
} catch (SysError & e) {
|
||||
if (unlink(tempLink.c_str()) == -1)
|
||||
printError("unable to unlink '%1%'", tempLink);
|
||||
if (errno == EMLINK) {
|
||||
|
@ -240,7 +242,7 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats,
|
|||
debug("'%s' has reached maximum number of links", linkPath);
|
||||
return;
|
||||
}
|
||||
throw SysError("cannot rename '%1%' to '%2%'", tempLink, path);
|
||||
throw;
|
||||
}
|
||||
|
||||
stats.filesLinked++;
|
||||
|
|
|
@ -580,7 +580,6 @@ ref<const ValidPathInfo> RemoteStore::addCAToStore(
|
|||
|
||||
try {
|
||||
conn->to.written = 0;
|
||||
conn->to.warn = true;
|
||||
connections->incCapacity();
|
||||
{
|
||||
Finally cleanup([&]() { connections->decCapacity(); });
|
||||
|
@ -591,7 +590,6 @@ ref<const ValidPathInfo> RemoteStore::addCAToStore(
|
|||
dumpString(contents, conn->to);
|
||||
}
|
||||
}
|
||||
conn->to.warn = false;
|
||||
conn.processStderr();
|
||||
} catch (SysError & e) {
|
||||
/* Daemon closed while we were sending the path. Probably OOM
|
||||
|
|
|
@ -204,13 +204,19 @@ public:
|
|||
int errNo;
|
||||
|
||||
template<typename... Args>
|
||||
SysError(const Args & ... args)
|
||||
SysError(int errNo_, const Args & ... args)
|
||||
: Error("")
|
||||
{
|
||||
errNo = errno;
|
||||
errNo = errNo_;
|
||||
auto hf = hintfmt(args...);
|
||||
err.msg = hintfmt("%1%: %2%", normaltxt(hf.str()), strerror(errNo));
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
SysError(const Args & ... args)
|
||||
: SysError(errno, args ...)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
172
src/libutil/filesystem.cc
Normal file
172
src/libutil/filesystem.cc
Normal file
|
@ -0,0 +1,172 @@
|
|||
#include <sys/time.h>
|
||||
#include <filesystem>
|
||||
|
||||
#include "finally.hh"
|
||||
#include "util.hh"
|
||||
#include "types.hh"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace nix {
|
||||
|
||||
static Path tempName(Path tmpRoot, const Path & prefix, bool includePid,
|
||||
int & counter)
|
||||
{
|
||||
tmpRoot = canonPath(tmpRoot.empty() ? getEnv("TMPDIR").value_or("/tmp") : tmpRoot, true);
|
||||
if (includePid)
|
||||
return (format("%1%/%2%-%3%-%4%") % tmpRoot % prefix % getpid() % counter++).str();
|
||||
else
|
||||
return (format("%1%/%2%-%3%") % tmpRoot % prefix % counter++).str();
|
||||
}
|
||||
|
||||
Path createTempDir(const Path & tmpRoot, const Path & prefix,
|
||||
bool includePid, bool useGlobalCounter, mode_t mode)
|
||||
{
|
||||
static int globalCounter = 0;
|
||||
int localCounter = 0;
|
||||
int & counter(useGlobalCounter ? globalCounter : localCounter);
|
||||
|
||||
while (1) {
|
||||
checkInterrupt();
|
||||
Path tmpDir = tempName(tmpRoot, prefix, includePid, counter);
|
||||
if (mkdir(tmpDir.c_str(), mode) == 0) {
|
||||
#if __FreeBSD__
|
||||
/* Explicitly set the group of the directory. This is to
|
||||
work around around problems caused by BSD's group
|
||||
ownership semantics (directories inherit the group of
|
||||
the parent). For instance, the group of /tmp on
|
||||
FreeBSD is "wheel", so all directories created in /tmp
|
||||
will be owned by "wheel"; but if the user is not in
|
||||
"wheel", then "tar" will fail to unpack archives that
|
||||
have the setgid bit set on directories. */
|
||||
if (chown(tmpDir.c_str(), (uid_t) -1, getegid()) != 0)
|
||||
throw SysError("setting group of directory '%1%'", tmpDir);
|
||||
#endif
|
||||
return tmpDir;
|
||||
}
|
||||
if (errno != EEXIST)
|
||||
throw SysError("creating directory '%1%'", tmpDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::pair<AutoCloseFD, Path> createTempFile(const Path & prefix)
|
||||
{
|
||||
Path tmpl(getEnv("TMPDIR").value_or("/tmp") + "/" + prefix + ".XXXXXX");
|
||||
// Strictly speaking, this is UB, but who cares...
|
||||
// FIXME: use O_TMPFILE.
|
||||
AutoCloseFD fd(mkstemp((char *) tmpl.c_str()));
|
||||
if (!fd)
|
||||
throw SysError("creating temporary file '%s'", tmpl);
|
||||
closeOnExec(fd.get());
|
||||
return {std::move(fd), tmpl};
|
||||
}
|
||||
|
||||
void createSymlink(const Path & target, const Path & link,
|
||||
std::optional<time_t> mtime)
|
||||
{
|
||||
if (symlink(target.c_str(), link.c_str()))
|
||||
throw SysError("creating symlink from '%1%' to '%2%'", link, target);
|
||||
if (mtime) {
|
||||
struct timeval times[2];
|
||||
times[0].tv_sec = *mtime;
|
||||
times[0].tv_usec = 0;
|
||||
times[1].tv_sec = *mtime;
|
||||
times[1].tv_usec = 0;
|
||||
if (lutimes(link.c_str(), times))
|
||||
throw SysError("setting time of symlink '%s'", link);
|
||||
}
|
||||
}
|
||||
|
||||
void replaceSymlink(const Path & target, const Path & link,
|
||||
std::optional<time_t> mtime)
|
||||
{
|
||||
for (unsigned int n = 0; true; n++) {
|
||||
Path tmp = canonPath(fmt("%s/.%d_%s", dirOf(link), n, baseNameOf(link)));
|
||||
|
||||
try {
|
||||
createSymlink(target, tmp, mtime);
|
||||
} catch (SysError & e) {
|
||||
if (e.errNo == EEXIST) continue;
|
||||
throw;
|
||||
}
|
||||
|
||||
renameFile(tmp, link);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void setWriteTime(const fs::path & p, const struct stat & st)
|
||||
{
|
||||
struct timeval times[2];
|
||||
times[0] = {
|
||||
.tv_sec = st.st_atime,
|
||||
.tv_usec = 0,
|
||||
};
|
||||
times[1] = {
|
||||
.tv_sec = st.st_mtime,
|
||||
.tv_usec = 0,
|
||||
};
|
||||
if (lutimes(p.c_str(), times) != 0)
|
||||
throw SysError("changing modification time of '%s'", p);
|
||||
}
|
||||
|
||||
void copy(const fs::directory_entry & from, const fs::path & to, bool andDelete)
|
||||
{
|
||||
// TODO: Rewrite the `is_*` to use `symlink_status()`
|
||||
auto statOfFrom = lstat(from.path().c_str());
|
||||
auto fromStatus = from.symlink_status();
|
||||
|
||||
// Mark the directory as writable so that we can delete its children
|
||||
if (andDelete && fs::is_directory(fromStatus)) {
|
||||
fs::permissions(from.path(), fs::perms::owner_write, fs::perm_options::add | fs::perm_options::nofollow);
|
||||
}
|
||||
|
||||
|
||||
if (fs::is_symlink(fromStatus) || fs::is_regular_file(fromStatus)) {
|
||||
fs::copy(from.path(), to, fs::copy_options::copy_symlinks | fs::copy_options::overwrite_existing);
|
||||
} else if (fs::is_directory(fromStatus)) {
|
||||
fs::create_directory(to);
|
||||
for (auto & entry : fs::directory_iterator(from.path())) {
|
||||
copy(entry, to / entry.path().filename(), andDelete);
|
||||
}
|
||||
} else {
|
||||
throw Error("file '%s' has an unsupported type", from.path());
|
||||
}
|
||||
|
||||
setWriteTime(to, statOfFrom);
|
||||
if (andDelete) {
|
||||
if (!fs::is_symlink(fromStatus))
|
||||
fs::permissions(from.path(), fs::perms::owner_write, fs::perm_options::add | fs::perm_options::nofollow);
|
||||
fs::remove(from.path());
|
||||
}
|
||||
}
|
||||
|
||||
void renameFile(const Path & oldName, const Path & newName)
|
||||
{
|
||||
fs::rename(oldName, newName);
|
||||
}
|
||||
|
||||
void moveFile(const Path & oldName, const Path & newName)
|
||||
{
|
||||
try {
|
||||
renameFile(oldName, newName);
|
||||
} catch (fs::filesystem_error & e) {
|
||||
auto oldPath = fs::path(oldName);
|
||||
auto newPath = fs::path(newName);
|
||||
// For the move to be as atomic as possible, copy to a temporary
|
||||
// directory
|
||||
fs::path temp = createTempDir(newPath.parent_path(), "rename-tmp");
|
||||
Finally removeTemp = [&]() { fs::remove(temp); };
|
||||
auto tempCopyTarget = temp / "copy-target";
|
||||
if (e.code().value() == EXDEV) {
|
||||
fs::remove(newPath);
|
||||
warn("Can’t rename %s as %s, copying instead", oldName, newName);
|
||||
copy(fs::directory_entry(oldPath), tempCopyTarget, true);
|
||||
renameFile(tempCopyTarget, newPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -6,7 +6,8 @@
|
|||
|
||||
namespace nix {
|
||||
|
||||
void toJSON(std::ostream & str, const char * start, const char * end)
|
||||
template<>
|
||||
void toJSON<std::string_view>(std::ostream & str, const std::string_view & s)
|
||||
{
|
||||
constexpr size_t BUF_SIZE = 4096;
|
||||
char buf[BUF_SIZE + 7]; // BUF_SIZE + largest single sequence of puts
|
||||
|
@ -21,7 +22,7 @@ void toJSON(std::ostream & str, const char * start, const char * end)
|
|||
};
|
||||
|
||||
put('"');
|
||||
for (auto i = start; i != end; i++) {
|
||||
for (auto i = s.begin(); i != s.end(); i++) {
|
||||
if (bufPos >= BUF_SIZE) flush();
|
||||
if (*i == '\"' || *i == '\\') { put('\\'); put(*i); }
|
||||
else if (*i == '\n') { put('\\'); put('n'); }
|
||||
|
@ -44,7 +45,7 @@ void toJSON(std::ostream & str, const char * start, const char * end)
|
|||
|
||||
void toJSON(std::ostream & str, const char * s)
|
||||
{
|
||||
if (!s) str << "null"; else toJSON(str, s, s + strlen(s));
|
||||
if (!s) str << "null"; else toJSON(str, std::string_view(s));
|
||||
}
|
||||
|
||||
template<> void toJSON<int>(std::ostream & str, const int & n) { str << n; }
|
||||
|
@ -55,11 +56,7 @@ template<> void toJSON<long long>(std::ostream & str, const long long & n) { str
|
|||
template<> void toJSON<unsigned long long>(std::ostream & str, const unsigned long long & n) { str << n; }
|
||||
template<> void toJSON<float>(std::ostream & str, const float & n) { str << n; }
|
||||
template<> void toJSON<double>(std::ostream & str, const double & n) { str << n; }
|
||||
|
||||
template<> void toJSON<std::string>(std::ostream & str, const std::string & s)
|
||||
{
|
||||
toJSON(str, s.c_str(), s.c_str() + s.size());
|
||||
}
|
||||
template<> void toJSON<std::string>(std::ostream & str, const std::string & s) { toJSON(str, (std::string_view) s); }
|
||||
|
||||
template<> void toJSON<bool>(std::ostream & str, const bool & b)
|
||||
{
|
||||
|
@ -154,7 +151,7 @@ JSONObject::~JSONObject()
|
|||
}
|
||||
}
|
||||
|
||||
void JSONObject::attr(const std::string & s)
|
||||
void JSONObject::attr(std::string_view s)
|
||||
{
|
||||
comma();
|
||||
toJSON(state->str, s);
|
||||
|
@ -162,19 +159,19 @@ void JSONObject::attr(const std::string & s)
|
|||
if (state->indent) state->str << ' ';
|
||||
}
|
||||
|
||||
JSONList JSONObject::list(const std::string & name)
|
||||
JSONList JSONObject::list(std::string_view name)
|
||||
{
|
||||
attr(name);
|
||||
return JSONList(state);
|
||||
}
|
||||
|
||||
JSONObject JSONObject::object(const std::string & name)
|
||||
JSONObject JSONObject::object(std::string_view name)
|
||||
{
|
||||
attr(name);
|
||||
return JSONObject(state);
|
||||
}
|
||||
|
||||
JSONPlaceholder JSONObject::placeholder(const std::string & name)
|
||||
JSONPlaceholder JSONObject::placeholder(std::string_view name)
|
||||
{
|
||||
attr(name);
|
||||
return JSONPlaceholder(state);
|
||||
|
@ -196,7 +193,11 @@ JSONObject JSONPlaceholder::object()
|
|||
|
||||
JSONPlaceholder::~JSONPlaceholder()
|
||||
{
|
||||
assert(!first || std::uncaught_exceptions());
|
||||
if (first) {
|
||||
assert(std::uncaught_exceptions());
|
||||
if (state->stack != 0)
|
||||
write(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
namespace nix {
|
||||
|
||||
void toJSON(std::ostream & str, const char * start, const char * end);
|
||||
void toJSON(std::ostream & str, const char * s);
|
||||
|
||||
template<typename T>
|
||||
|
@ -107,7 +106,7 @@ private:
|
|||
open();
|
||||
}
|
||||
|
||||
void attr(const std::string & s);
|
||||
void attr(std::string_view s);
|
||||
|
||||
public:
|
||||
|
||||
|
@ -128,18 +127,18 @@ public:
|
|||
~JSONObject();
|
||||
|
||||
template<typename T>
|
||||
JSONObject & attr(const std::string & name, const T & v)
|
||||
JSONObject & attr(std::string_view name, const T & v)
|
||||
{
|
||||
attr(name);
|
||||
toJSON(state->str, v);
|
||||
return *this;
|
||||
}
|
||||
|
||||
JSONList list(const std::string & name);
|
||||
JSONList list(std::string_view name);
|
||||
|
||||
JSONObject object(const std::string & name);
|
||||
JSONObject object(std::string_view name);
|
||||
|
||||
JSONPlaceholder placeholder(const std::string & name);
|
||||
JSONPlaceholder placeholder(std::string_view name);
|
||||
};
|
||||
|
||||
class JSONPlaceholder : JSONWriter
|
||||
|
|
|
@ -48,24 +48,9 @@ FdSink::~FdSink()
|
|||
}
|
||||
|
||||
|
||||
size_t threshold = 256 * 1024 * 1024;
|
||||
|
||||
static void warnLargeDump()
|
||||
{
|
||||
warn("dumping very large path (> 256 MiB); this may run out of memory");
|
||||
}
|
||||
|
||||
|
||||
void FdSink::write(std::string_view data)
|
||||
{
|
||||
written += data.size();
|
||||
static bool warned = false;
|
||||
if (warn && !warned) {
|
||||
if (written > threshold) {
|
||||
warnLargeDump();
|
||||
warned = true;
|
||||
}
|
||||
}
|
||||
try {
|
||||
writeFull(fd, data);
|
||||
} catch (SysError & e) {
|
||||
|
@ -448,11 +433,6 @@ Error readError(Source & source)
|
|||
|
||||
void StringSink::operator () (std::string_view data)
|
||||
{
|
||||
static bool warned = false;
|
||||
if (!warned && s.size() > threshold) {
|
||||
warnLargeDump();
|
||||
warned = true;
|
||||
}
|
||||
s.append(data);
|
||||
}
|
||||
|
||||
|
|
|
@ -97,19 +97,17 @@ protected:
|
|||
struct FdSink : BufferedSink
|
||||
{
|
||||
int fd;
|
||||
bool warn = false;
|
||||
size_t written = 0;
|
||||
|
||||
FdSink() : fd(-1) { }
|
||||
FdSink(int fd) : fd(fd) { }
|
||||
FdSink(FdSink&&) = default;
|
||||
|
||||
FdSink& operator=(FdSink && s)
|
||||
FdSink & operator=(FdSink && s)
|
||||
{
|
||||
flush();
|
||||
fd = s.fd;
|
||||
s.fd = -1;
|
||||
warn = s.warn;
|
||||
written = s.written;
|
||||
return *this;
|
||||
}
|
||||
|
|
|
@ -102,8 +102,8 @@ namespace nix {
|
|||
|
||||
TEST(toJSON, substringEscape) {
|
||||
std::stringstream out;
|
||||
const char *s = "foo\t";
|
||||
toJSON(out, s+3, s + strlen(s));
|
||||
std::string_view s = "foo\t";
|
||||
toJSON(out, s.substr(3));
|
||||
|
||||
ASSERT_EQ(out.str(), "\"\\t\"");
|
||||
}
|
||||
|
|
|
@ -35,6 +35,9 @@
|
|||
#ifdef __linux__
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/resource.h>
|
||||
|
||||
#include <mntent.h>
|
||||
#include <cmath>
|
||||
#endif
|
||||
|
||||
|
||||
|
@ -505,61 +508,6 @@ void deletePath(const Path & path, uint64_t & bytesFreed)
|
|||
}
|
||||
|
||||
|
||||
static Path tempName(Path tmpRoot, const Path & prefix, bool includePid,
|
||||
int & counter)
|
||||
{
|
||||
tmpRoot = canonPath(tmpRoot.empty() ? getEnv("TMPDIR").value_or("/tmp") : tmpRoot, true);
|
||||
if (includePid)
|
||||
return (format("%1%/%2%-%3%-%4%") % tmpRoot % prefix % getpid() % counter++).str();
|
||||
else
|
||||
return (format("%1%/%2%-%3%") % tmpRoot % prefix % counter++).str();
|
||||
}
|
||||
|
||||
|
||||
Path createTempDir(const Path & tmpRoot, const Path & prefix,
|
||||
bool includePid, bool useGlobalCounter, mode_t mode)
|
||||
{
|
||||
static int globalCounter = 0;
|
||||
int localCounter = 0;
|
||||
int & counter(useGlobalCounter ? globalCounter : localCounter);
|
||||
|
||||
while (1) {
|
||||
checkInterrupt();
|
||||
Path tmpDir = tempName(tmpRoot, prefix, includePid, counter);
|
||||
if (mkdir(tmpDir.c_str(), mode) == 0) {
|
||||
#if __FreeBSD__
|
||||
/* Explicitly set the group of the directory. This is to
|
||||
work around around problems caused by BSD's group
|
||||
ownership semantics (directories inherit the group of
|
||||
the parent). For instance, the group of /tmp on
|
||||
FreeBSD is "wheel", so all directories created in /tmp
|
||||
will be owned by "wheel"; but if the user is not in
|
||||
"wheel", then "tar" will fail to unpack archives that
|
||||
have the setgid bit set on directories. */
|
||||
if (chown(tmpDir.c_str(), (uid_t) -1, getegid()) != 0)
|
||||
throw SysError("setting group of directory '%1%'", tmpDir);
|
||||
#endif
|
||||
return tmpDir;
|
||||
}
|
||||
if (errno != EEXIST)
|
||||
throw SysError("creating directory '%1%'", tmpDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::pair<AutoCloseFD, Path> createTempFile(const Path & prefix)
|
||||
{
|
||||
Path tmpl(getEnv("TMPDIR").value_or("/tmp") + "/" + prefix + ".XXXXXX");
|
||||
// Strictly speaking, this is UB, but who cares...
|
||||
// FIXME: use O_TMPFILE.
|
||||
AutoCloseFD fd(mkstemp((char *) tmpl.c_str()));
|
||||
if (!fd)
|
||||
throw SysError("creating temporary file '%s'", tmpl);
|
||||
closeOnExec(fd.get());
|
||||
return {std::move(fd), tmpl};
|
||||
}
|
||||
|
||||
|
||||
std::string getUserName()
|
||||
{
|
||||
auto pw = getpwuid(geteuid());
|
||||
|
@ -574,6 +522,7 @@ Path getHome()
|
|||
{
|
||||
static Path homeDir = []()
|
||||
{
|
||||
std::optional<std::string> unownedUserHomeDir = {};
|
||||
auto homeDir = getEnv("HOME");
|
||||
if (homeDir) {
|
||||
// Only use $HOME if doesn't exist or is owned by the current user.
|
||||
|
@ -585,8 +534,7 @@ Path getHome()
|
|||
homeDir.reset();
|
||||
}
|
||||
} else if (st.st_uid != geteuid()) {
|
||||
warn("$HOME ('%s') is not owned by you, falling back to the one defined in the 'passwd' file", *homeDir);
|
||||
homeDir.reset();
|
||||
unownedUserHomeDir.swap(homeDir);
|
||||
}
|
||||
}
|
||||
if (!homeDir) {
|
||||
|
@ -597,6 +545,9 @@ Path getHome()
|
|||
|| !pw || !pw->pw_dir || !pw->pw_dir[0])
|
||||
throw Error("cannot determine user's home directory");
|
||||
homeDir = pw->pw_dir;
|
||||
if (unownedUserHomeDir.has_value() && unownedUserHomeDir != homeDir) {
|
||||
warn("$HOME ('%s') is not owned by you, falling back to the one defined in the 'passwd' file ('%s')", *unownedUserHomeDir, *homeDir);
|
||||
}
|
||||
}
|
||||
return *homeDir;
|
||||
}();
|
||||
|
@ -678,44 +629,6 @@ Paths createDirs(const Path & path)
|
|||
}
|
||||
|
||||
|
||||
void createSymlink(const Path & target, const Path & link,
|
||||
std::optional<time_t> mtime)
|
||||
{
|
||||
if (symlink(target.c_str(), link.c_str()))
|
||||
throw SysError("creating symlink from '%1%' to '%2%'", link, target);
|
||||
if (mtime) {
|
||||
struct timeval times[2];
|
||||
times[0].tv_sec = *mtime;
|
||||
times[0].tv_usec = 0;
|
||||
times[1].tv_sec = *mtime;
|
||||
times[1].tv_usec = 0;
|
||||
if (lutimes(link.c_str(), times))
|
||||
throw SysError("setting time of symlink '%s'", link);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void replaceSymlink(const Path & target, const Path & link,
|
||||
std::optional<time_t> mtime)
|
||||
{
|
||||
for (unsigned int n = 0; true; n++) {
|
||||
Path tmp = canonPath(fmt("%s/.%d_%s", dirOf(link), n, baseNameOf(link)));
|
||||
|
||||
try {
|
||||
createSymlink(target, tmp, mtime);
|
||||
} catch (SysError & e) {
|
||||
if (e.errNo == EEXIST) continue;
|
||||
throw;
|
||||
}
|
||||
|
||||
if (rename(tmp.c_str(), link.c_str()) != 0)
|
||||
throw SysError("renaming '%1%' to '%2%'", tmp, link);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void readFull(int fd, char * buf, size_t count)
|
||||
{
|
||||
while (count) {
|
||||
|
@ -788,7 +701,55 @@ void drainFD(int fd, Sink & sink, bool block)
|
|||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
unsigned int getMaxCPU()
|
||||
{
|
||||
#if __linux__
|
||||
try {
|
||||
FILE *fp = fopen("/proc/mounts", "r");
|
||||
if (!fp)
|
||||
return 0;
|
||||
|
||||
Strings cgPathParts;
|
||||
|
||||
struct mntent *ent;
|
||||
while ((ent = getmntent(fp))) {
|
||||
std::string mountType, mountPath;
|
||||
|
||||
mountType = ent->mnt_type;
|
||||
mountPath = ent->mnt_dir;
|
||||
|
||||
if (mountType == "cgroup2") {
|
||||
cgPathParts.push_back(mountPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
|
||||
if (cgPathParts.size() > 0 && pathExists("/proc/self/cgroup")) {
|
||||
std::string currentCgroup = readFile("/proc/self/cgroup");
|
||||
Strings cgValues = tokenizeString<Strings>(currentCgroup, ":");
|
||||
cgPathParts.push_back(trim(cgValues.back(), "\n"));
|
||||
cgPathParts.push_back("cpu.max");
|
||||
std::string fullCgPath = canonPath(concatStringsSep("/", cgPathParts));
|
||||
|
||||
if (pathExists(fullCgPath)) {
|
||||
std::string cpuMax = readFile(fullCgPath);
|
||||
std::vector<std::string> cpuMaxParts = tokenizeString<std::vector<std::string>>(cpuMax, " ");
|
||||
std::string quota = cpuMaxParts[0];
|
||||
std::string period = trim(cpuMaxParts[1], "\n");
|
||||
|
||||
if (quota != "max")
|
||||
return std::ceil(std::stoi(quota) / std::stof(period));
|
||||
}
|
||||
}
|
||||
} catch (Error &) { ignoreException(); }
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
|
|
@ -168,6 +168,17 @@ void createSymlink(const Path & target, const Path & link,
|
|||
void replaceSymlink(const Path & target, const Path & link,
|
||||
std::optional<time_t> mtime = {});
|
||||
|
||||
void renameFile(const Path & src, const Path & dst);
|
||||
|
||||
/**
|
||||
* Similar to 'renameFile', but fallback to a copy+remove if `src` and `dst`
|
||||
* are on a different filesystem.
|
||||
*
|
||||
* Beware that this might not be atomic because of the copy that happens behind
|
||||
* the scenes
|
||||
*/
|
||||
void moveFile(const Path & src, const Path & dst);
|
||||
|
||||
|
||||
/* Wrappers arount read()/write() that read/write exactly the
|
||||
requested number of bytes. */
|
||||
|
@ -182,6 +193,9 @@ std::string drainFD(int fd, bool block = true, const size_t reserveSize=0);
|
|||
|
||||
void drainFD(int fd, Sink & sink, bool block = true);
|
||||
|
||||
/* If cgroups are active, attempt to calculate the number of CPUs available.
|
||||
If cgroups are unavailable or if cpu.max is set to "max", return 0. */
|
||||
unsigned int getMaxCPU();
|
||||
|
||||
/* Automatic cleanup of resources. */
|
||||
|
||||
|
|
|
@ -940,12 +940,12 @@ static void queryJSON(Globals & globals, std::vector<DrvInfo> & elems, bool prin
|
|||
JSONObject metaObj = pkgObj.object("meta");
|
||||
StringSet metaNames = i.queryMetaNames();
|
||||
for (auto & j : metaNames) {
|
||||
auto placeholder = metaObj.placeholder(j);
|
||||
Value * v = i.queryMeta(j);
|
||||
if (!v) {
|
||||
printError("derivation '%s' has invalid meta attribute '%s'", i.queryName(), j);
|
||||
placeholder.write(nullptr);
|
||||
metaObj.attr(j, nullptr);
|
||||
} else {
|
||||
auto placeholder = metaObj.placeholder(j);
|
||||
PathSet context;
|
||||
printValueAsJSON(*globals.state, true, *v, noPos, placeholder, context);
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ flake output attributes:
|
|||
|
||||
* `bundlers.<system>.default`
|
||||
|
||||
If an attribute *name* is given, `nix run` tries the following flake
|
||||
If an attribute *name* is given, `nix bundle` tries the following flake
|
||||
output attributes:
|
||||
|
||||
* `bundlers.<system>.<name>`
|
||||
|
|
|
@ -6,7 +6,7 @@ R""(
|
|||
lock file:
|
||||
|
||||
```console
|
||||
# nix flake update
|
||||
# nix flake update --commit-lock-file
|
||||
* Updated 'nix': 'github:NixOS/nix/9fab14adbc3810d5cc1f88672fde1eee4358405c' -> 'github:NixOS/nix/8927cba62f5afb33b01016d5c4f7f8b7d0adde3c'
|
||||
* Updated 'nixpkgs': 'github:NixOS/nixpkgs/3d2d8f281a27d466fa54b469b5993f7dde198375' -> 'github:NixOS/nixpkgs/a3a3dda3bacf61e8a39258a0ed9c924eeca8e293'
|
||||
…
|
||||
|
|
|
@ -212,7 +212,8 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON
|
|||
ANSI_BOLD "Last modified:" ANSI_NORMAL " %s",
|
||||
std::put_time(std::localtime(&*lastModified), "%F %T"));
|
||||
|
||||
logger->cout(ANSI_BOLD "Inputs:" ANSI_NORMAL);
|
||||
if (!lockedFlake.lockFile.root->inputs.empty())
|
||||
logger->cout(ANSI_BOLD "Inputs:" ANSI_NORMAL);
|
||||
|
||||
std::unordered_set<std::shared_ptr<Node>> visited;
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ them to be rolled back easily.
|
|||
|
||||
The default profile used by `nix profile` is `$HOME/.nix-profile`,
|
||||
which, if it does not exist, is created as a symlink to
|
||||
`/nix/var/nix/profiles/per-user/default` if Nix is invoked by the
|
||||
`/nix/var/nix/profiles/default` if Nix is invoked by the
|
||||
`root` user, or `/nix/var/nix/profiles/per-user/`*username* otherwise.
|
||||
|
||||
You can specify another profile location using `--profile` *path*.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue