aboutsummaryrefslogtreecommitdiff
path: root/src/libutil
diff options
context:
space:
mode:
Diffstat (limited to 'src/libutil')
-rw-r--r--src/libutil/archive.cc3
-rw-r--r--src/libutil/archive.hh1
-rw-r--r--src/libutil/args.cc3
-rw-r--r--src/libutil/args.hh8
-rw-r--r--src/libutil/canon-path.cc2
-rw-r--r--src/libutil/cgroup.cc5
-rw-r--r--src/libutil/comparator.hh2
-rw-r--r--src/libutil/compression.cc3
-rw-r--r--src/libutil/config-impl.hh2
-rw-r--r--src/libutil/config.cc3
-rw-r--r--src/libutil/current-process.cc111
-rw-r--r--src/libutil/current-process.hh37
-rw-r--r--src/libutil/environment-variables.cc51
-rw-r--r--src/libutil/environment-variables.hh42
-rw-r--r--src/libutil/error.cc19
-rw-r--r--src/libutil/error.hh6
-rw-r--r--src/libutil/experimental-features.cc2
-rw-r--r--src/libutil/file-descriptor.cc252
-rw-r--r--src/libutil/file-descriptor.hh80
-rw-r--r--src/libutil/file-system.cc670
-rw-r--r--src/libutil/file-system.hh269
-rw-r--r--src/libutil/filesystem.cc175
-rw-r--r--src/libutil/hash.cc3
-rw-r--r--src/libutil/hash.hh1
-rw-r--r--src/libutil/input-accessor.hh6
-rw-r--r--src/libutil/logging.cc6
-rw-r--r--src/libutil/meson.build21
-rw-r--r--src/libutil/namespaces.cc72
-rw-r--r--src/libutil/namespaces.hh20
-rw-r--r--src/libutil/processes.cc405
-rw-r--r--src/libutil/processes.hh115
-rw-r--r--src/libutil/references.cc4
-rw-r--r--src/libutil/regex-combinators.hh2
-rw-r--r--src/libutil/serialise.cc1
-rw-r--r--src/libutil/serialise.hh3
-rw-r--r--src/libutil/shlex.cc2
-rw-r--r--src/libutil/signals.cc2
-rw-r--r--src/libutil/signals.hh2
-rw-r--r--src/libutil/source-path.hh2
-rw-r--r--src/libutil/split.hh2
-rw-r--r--src/libutil/strings.cc232
-rw-r--r--src/libutil/strings.hh256
-rw-r--r--src/libutil/suggestions.cc5
-rw-r--r--src/libutil/suggestions.hh5
-rw-r--r--src/libutil/tarfile.cc2
-rw-r--r--src/libutil/terminal.cc104
-rw-r--r--src/libutil/terminal.hh40
-rw-r--r--src/libutil/thread-pool.cc1
-rw-r--r--src/libutil/thread-pool.hh3
-rw-r--r--src/libutil/types.hh111
-rw-r--r--src/libutil/unix-domain-socket.cc105
-rw-r--r--src/libutil/unix-domain-socket.hh31
-rw-r--r--src/libutil/url-name.cc1
-rw-r--r--src/libutil/url-name.hh3
-rw-r--r--src/libutil/url.cc2
-rw-r--r--src/libutil/users.cc105
-rw-r--r--src/libutil/users.hh61
-rw-r--r--src/libutil/util.cc1916
-rw-r--r--src/libutil/util.hh971
59 files changed, 3259 insertions, 3110 deletions
diff --git a/src/libutil/archive.cc b/src/libutil/archive.cc
index a18c54ebf..1b42ee4b5 100644
--- a/src/libutil/archive.cc
+++ b/src/libutil/archive.cc
@@ -12,8 +12,9 @@
#include <fcntl.h>
#include "archive.hh"
-#include "util.hh"
+#include "file-system.hh"
#include "config.hh"
+#include "logging.hh"
#include "signals.hh"
namespace nix {
diff --git a/src/libutil/archive.hh b/src/libutil/archive.hh
index 017b6633c..f163a1947 100644
--- a/src/libutil/archive.hh
+++ b/src/libutil/archive.hh
@@ -3,6 +3,7 @@
#include "types.hh"
#include "serialise.hh"
+#include "file-system.hh"
namespace nix {
diff --git a/src/libutil/args.cc b/src/libutil/args.cc
index 520f50c30..4983e49af 100644
--- a/src/libutil/args.cc
+++ b/src/libutil/args.cc
@@ -2,7 +2,10 @@
#include "args/root.hh"
#include "hash.hh"
#include "json-utils.hh"
+#include "environment-variables.hh"
+
#include "experimental-features-json.hh"
+#include "logging.hh"
#include <glob.h>
diff --git a/src/libutil/args.hh b/src/libutil/args.hh
index 77f7ff2a8..35a5238c0 100644
--- a/src/libutil/args.hh
+++ b/src/libutil/args.hh
@@ -1,13 +1,17 @@
#pragma once
///@file
-#include <iostream>
+#include "experimental-features.hh"
+#include "types.hh"
+#include <functional>
#include <map>
#include <memory>
+#include <limits>
#include <nlohmann/json_fwd.hpp>
+#include <optional>
+#include <set>
-#include "util.hh"
namespace nix {
diff --git a/src/libutil/canon-path.cc b/src/libutil/canon-path.cc
index 040464532..f678fae94 100644
--- a/src/libutil/canon-path.cc
+++ b/src/libutil/canon-path.cc
@@ -1,5 +1,5 @@
#include "canon-path.hh"
-#include "util.hh"
+#include "file-system.hh"
namespace nix {
diff --git a/src/libutil/cgroup.cc b/src/libutil/cgroup.cc
index 9320d2371..e28e21c3e 100644
--- a/src/libutil/cgroup.cc
+++ b/src/libutil/cgroup.cc
@@ -1,14 +1,17 @@
+#include "logging.hh"
#if __linux__
#include "cgroup.hh"
-#include "util.hh"
+#include "file-system.hh"
#include "finally.hh"
+#include "strings.hh"
#include <chrono>
#include <cmath>
#include <regex>
#include <unordered_set>
#include <thread>
+#include <signal.h>
#include <dirent.h>
#include <mntent.h>
diff --git a/src/libutil/comparator.hh b/src/libutil/comparator.hh
index cbc2bb4fd..136a3f742 100644
--- a/src/libutil/comparator.hh
+++ b/src/libutil/comparator.hh
@@ -1,6 +1,8 @@
#pragma once
///@file
+#include <tuple>
+
#define DECLARE_ONE_CMP(PRE, QUAL, COMPARATOR, MY_TYPE) \
PRE bool QUAL operator COMPARATOR(const MY_TYPE & other) const;
#define DECLARE_EQUAL(prefix, qualification, my_type) \
diff --git a/src/libutil/compression.cc b/src/libutil/compression.cc
index 575a03712..678557a58 100644
--- a/src/libutil/compression.cc
+++ b/src/libutil/compression.cc
@@ -1,7 +1,5 @@
#include "compression.hh"
#include "tarfile.hh"
-#include "util.hh"
-#include "finally.hh"
#include "signals.hh"
#include "logging.hh"
@@ -13,7 +11,6 @@
#include <brotli/decode.h>
#include <brotli/encode.h>
-#include <iostream>
namespace nix {
diff --git a/src/libutil/config-impl.hh b/src/libutil/config-impl.hh
index 9f69e8444..756175f95 100644
--- a/src/libutil/config-impl.hh
+++ b/src/libutil/config-impl.hh
@@ -12,7 +12,9 @@
* instantiation.
*/
+#include "args.hh"
#include "config.hh"
+#include "logging.hh"
namespace nix {
diff --git a/src/libutil/config.cc b/src/libutil/config.cc
index 81efcd507..8180886ce 100644
--- a/src/libutil/config.cc
+++ b/src/libutil/config.cc
@@ -2,6 +2,9 @@
#include "args.hh"
#include "abstract-setting-to-json.hh"
#include "experimental-features.hh"
+#include "file-system.hh"
+#include "logging.hh"
+#include "strings.hh"
#include "config-impl.hh"
diff --git a/src/libutil/current-process.cc b/src/libutil/current-process.cc
new file mode 100644
index 000000000..c64dd1e0d
--- /dev/null
+++ b/src/libutil/current-process.cc
@@ -0,0 +1,111 @@
+#include "current-process.hh"
+#include "error.hh"
+#include "file-system.hh"
+#include "logging.hh"
+#include "namespaces.hh"
+#include "signals.hh"
+#include "strings.hh"
+
+#ifdef __APPLE__
+# include <mach-o/dyld.h>
+#endif
+
+#if __linux__
+# include <sys/resource.h>
+#endif
+
+#include <sys/mount.h>
+#include <cgroup.hh>
+
+namespace nix {
+
+unsigned int getMaxCPU()
+{
+ #if __linux__
+ try {
+ auto cgroupFS = getCgroupFS();
+ if (!cgroupFS) return 0;
+
+ auto cgroups = getCgroups("/proc/self/cgroup");
+ auto cgroup = cgroups[""];
+ if (cgroup == "") return 0;
+
+ auto cpuFile = *cgroupFS + "/" + cgroup + "/cpu.max";
+
+ auto cpuMax = readFile(cpuFile);
+ auto cpuMaxParts = tokenizeString<std::vector<std::string>>(cpuMax, " \n");
+
+ if (cpuMaxParts.size() != 2) {
+ return 0;
+ }
+
+ auto quota = cpuMaxParts[0];
+ auto period = cpuMaxParts[1];
+ if (quota != "max")
+ return std::ceil(std::stoi(quota) / std::stof(period));
+ } catch (Error &) { ignoreException(lvlDebug); }
+ #endif
+
+ return 0;
+}
+
+rlim_t savedStackSize = 0;
+
+void setStackSize(rlim_t stackSize)
+{
+ struct rlimit limit;
+ if (getrlimit(RLIMIT_STACK, &limit) == 0 && limit.rlim_cur < stackSize) {
+ savedStackSize = limit.rlim_cur;
+ limit.rlim_cur = std::min(stackSize, limit.rlim_max);
+ if (setrlimit(RLIMIT_STACK, &limit) != 0) {
+ logger->log(
+ lvlError,
+ HintFmt(
+ "Failed to increase stack size from %1% to %2% (maximum allowed stack size: %3%): %4%",
+ savedStackSize,
+ stackSize,
+ limit.rlim_max,
+ std::strerror(errno)
+ ).str()
+ );
+ }
+ }
+}
+
+void restoreProcessContext(bool restoreMounts)
+{
+ restoreSignals();
+ if (restoreMounts) {
+ restoreMountNamespace();
+ }
+
+ if (savedStackSize) {
+ struct rlimit limit;
+ if (getrlimit(RLIMIT_STACK, &limit) == 0) {
+ limit.rlim_cur = savedStackSize;
+ setrlimit(RLIMIT_STACK, &limit);
+ }
+ }
+}
+
+std::optional<Path> getSelfExe()
+{
+ static auto cached = []() -> std::optional<Path>
+ {
+ #if __linux__
+ return readLink("/proc/self/exe");
+ #elif __APPLE__
+ char buf[1024];
+ uint32_t size = sizeof(buf);
+ if (_NSGetExecutablePath(buf, &size) == 0)
+ return buf;
+ else
+ return std::nullopt;
+ #else
+ return std::nullopt;
+ #endif
+ }();
+ return cached;
+}
+
+}
diff --git a/src/libutil/current-process.hh b/src/libutil/current-process.hh
new file mode 100644
index 000000000..8d5a2791d
--- /dev/null
+++ b/src/libutil/current-process.hh
@@ -0,0 +1,37 @@
+#pragma once
+///@file
+
+#include <optional>
+#include <sys/resource.h>
+
+#include "types.hh"
+
+namespace nix {
+
+/**
+ * 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();
+
+
+/**
+ * Change the stack size.
+ */
+void setStackSize(rlim_t stackSize);
+
+
+/**
+ * Restore the original inherited Unix process context (such as signal
+ * masks, stack size).
+
+ * See startSignalHandlerThread(), saveSignalMask().
+ */
+void restoreProcessContext(bool restoreMounts = true);
+
+/**
+ * @return the path of the current executable.
+ */
+std::optional<Path> getSelfExe();
+
+}
diff --git a/src/libutil/environment-variables.cc b/src/libutil/environment-variables.cc
new file mode 100644
index 000000000..71c404a0e
--- /dev/null
+++ b/src/libutil/environment-variables.cc
@@ -0,0 +1,51 @@
+#include <cstring>
+#include <map>
+#include <optional>
+#include <string>
+
+extern char * * environ __attribute__((weak));
+
+namespace nix {
+
+std::optional<std::string> getEnv(const std::string & key)
+{
+ char * value = getenv(key.c_str());
+ if (!value) return {};
+ return std::string(value);
+}
+
+std::optional<std::string> getEnvNonEmpty(const std::string & key) {
+ auto value = getEnv(key);
+ if (value == "") return {};
+ return value;
+}
+
+std::map<std::string, std::string> getEnv()
+{
+ std::map<std::string, std::string> env;
+ for (size_t i = 0; environ[i]; ++i) {
+ auto s = environ[i];
+ auto eq = strchr(s, '=');
+ if (!eq)
+ // invalid env, just keep going
+ continue;
+ env.emplace(std::string(s, eq), std::string(eq + 1));
+ }
+ return env;
+}
+
+
+void clearEnv()
+{
+ for (auto & name : getEnv())
+ unsetenv(name.first.c_str());
+}
+
+void replaceEnv(const std::map<std::string, std::string> & newEnv)
+{
+ clearEnv();
+ for (auto & newEnvVar : newEnv)
+ setenv(newEnvVar.first.c_str(), newEnvVar.second.c_str(), 1);
+}
+
+}
diff --git a/src/libutil/environment-variables.hh b/src/libutil/environment-variables.hh
new file mode 100644
index 000000000..784b95921
--- /dev/null
+++ b/src/libutil/environment-variables.hh
@@ -0,0 +1,42 @@
+#pragma once
+/**
+ * @file
+ *
+ * Utilities for working with the current process's environment
+ * variables.
+ */
+
+#include <map>
+#include <optional>
+#include <string>
+
+
+namespace nix {
+
+/**
+ * @return an environment variable.
+ */
+std::optional<std::string> getEnv(const std::string & key);
+
+/**
+ * @return a non empty environment variable. Returns nullopt if the env
+ * variable is set to ""
+ */
+std::optional<std::string> getEnvNonEmpty(const std::string & key);
+
+/**
+ * Get the entire environment.
+ */
+std::map<std::string, std::string> getEnv();
+
+/**
+ * Clear the environment.
+ */
+void clearEnv();
+
+/**
+ * Replace the entire environment with the given one.
+ */
+void replaceEnv(const std::map<std::string, std::string> & newEnv);
+
+}
diff --git a/src/libutil/error.cc b/src/libutil/error.cc
index b534ff87e..e5d6a9fa8 100644
--- a/src/libutil/error.cc
+++ b/src/libutil/error.cc
@@ -1,15 +1,15 @@
+#include "environment-variables.hh"
#include "error.hh"
+#include "logging.hh"
#include "position.hh"
+#include "terminal.hh"
#include <iostream>
#include <optional>
-#include "serialise.hh"
#include <sstream>
namespace nix {
-const std::string nativeSystem = SYSTEM;
-
void BaseError::addTrace(std::shared_ptr<Pos> && e, HintFmt hint)
{
err.traces.push_front(Trace { .pos = std::move(e), .hint = hint });
@@ -415,4 +415,17 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s
return out;
}
+void ignoreException(Verbosity lvl)
+{
+ /* Make sure no exceptions leave this function.
+ printError() also throws when remote is closed. */
+ try {
+ try {
+ throw;
+ } catch (std::exception & e) {
+ printMsg(lvl, "error (ignored): %1%", e.what());
+ }
+ } catch (...) { }
+}
+
}
diff --git a/src/libutil/error.hh b/src/libutil/error.hh
index 323365d65..0884f9f32 100644
--- a/src/libutil/error.hh
+++ b/src/libutil/error.hh
@@ -202,4 +202,10 @@ public:
}
};
+/**
+ * Exception handling in destructors: print an error message, then
+ * ignore the exception.
+ */
+void ignoreException(Verbosity lvl = lvlError);
+
}
diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc
index 25beba467..f1cbfdb16 100644
--- a/src/libutil/experimental-features.cc
+++ b/src/libutil/experimental-features.cc
@@ -1,7 +1,7 @@
#include "experimental-features.hh"
// Required for instances of to_json and from_json for ExperimentalFeature
#include "experimental-features-json.hh"
-#include "util.hh"
+#include "strings.hh"
#include "nlohmann/json.hpp"
diff --git a/src/libutil/file-descriptor.cc b/src/libutil/file-descriptor.cc
new file mode 100644
index 000000000..a19ceaf2a
--- /dev/null
+++ b/src/libutil/file-descriptor.cc
@@ -0,0 +1,252 @@
+#include "file-system.hh"
+#include "finally.hh"
+#include "logging.hh"
+#include "serialise.hh"
+#include "signals.hh"
+
+#include <fcntl.h>
+#include <unistd.h>
+
+namespace nix {
+
+std::string readFile(int fd)
+{
+ struct stat st;
+ if (fstat(fd, &st) == -1)
+ throw SysError("statting file");
+
+ return drainFD(fd, true, st.st_size);
+}
+
+
+std::string readLine(int fd)
+{
+ std::string s;
+ while (1) {
+ checkInterrupt();
+ char ch;
+ // FIXME: inefficient
+ ssize_t rd = read(fd, &ch, 1);
+ if (rd == -1) {
+ if (errno != EINTR)
+ throw SysError("reading a line");
+ } else if (rd == 0)
+ throw EndOfFile("unexpected EOF reading a line");
+ else {
+ if (ch == '\n') return s;
+ s += ch;
+ }
+ }
+}
+
+
+void writeLine(int fd, std::string s)
+{
+ s += '\n';
+ writeFull(fd, s);
+}
+
+
+void readFull(int fd, char * buf, size_t count)
+{
+ while (count) {
+ checkInterrupt();
+ ssize_t res = read(fd, buf, count);
+ if (res == -1) {
+ if (errno == EINTR) continue;
+ throw SysError("reading from file");
+ }
+ if (res == 0) throw EndOfFile("unexpected end-of-file");
+ count -= res;
+ buf += res;
+ }
+}
+
+
+void writeFull(int fd, std::string_view s, bool allowInterrupts)
+{
+ while (!s.empty()) {
+ if (allowInterrupts) checkInterrupt();
+ ssize_t res = write(fd, s.data(), s.size());
+ if (res == -1 && errno != EINTR)
+ throw SysError("writing to file");
+ if (res > 0)
+ s.remove_prefix(res);
+ }
+}
+
+
+std::string drainFD(int fd, bool block, const size_t reserveSize)
+{
+ // the parser needs two extra bytes to append terminating characters, other users will
+ // not care very much about the extra memory.
+ StringSink sink(reserveSize + 2);
+ drainFD(fd, sink, block);
+ return std::move(sink.s);
+}
+
+
+void drainFD(int fd, Sink & sink, bool block)
+{
+ // silence GCC maybe-uninitialized warning in finally
+ int saved = 0;
+
+ if (!block) {
+ saved = fcntl(fd, F_GETFL);
+ if (fcntl(fd, F_SETFL, saved | O_NONBLOCK) == -1)
+ throw SysError("making file descriptor non-blocking");
+ }
+
+ Finally finally([&]() {
+ if (!block) {
+ if (fcntl(fd, F_SETFL, saved) == -1)
+ throw SysError("making file descriptor blocking");
+ }
+ });
+
+ std::array<unsigned char, 64 * 1024> buf;
+ while (1) {
+ checkInterrupt();
+ ssize_t rd = read(fd, buf.data(), buf.size());
+ if (rd == -1) {
+ if (!block && (errno == EAGAIN || errno == EWOULDBLOCK))
+ break;
+ if (errno != EINTR)
+ throw SysError("reading from file");
+ }
+ else if (rd == 0) break;
+ else sink({(char *) buf.data(), (size_t) rd});
+ }
+}
+
+AutoCloseFD::AutoCloseFD() : fd{-1} {}
+
+
+AutoCloseFD::AutoCloseFD(int fd) : fd{fd} {}
+
+
+AutoCloseFD::AutoCloseFD(AutoCloseFD && that) : fd{that.fd}
+{
+ that.fd = -1;
+}
+
+
+AutoCloseFD & AutoCloseFD::operator =(AutoCloseFD && that)
+{
+ close();
+ fd = that.fd;
+ that.fd = -1;
+ return *this;
+}
+
+
+AutoCloseFD::~AutoCloseFD()
+{
+ try {
+ close();
+ } catch (...) {
+ ignoreException();
+ }
+}
+
+
+int AutoCloseFD::get() const
+{
+ return fd;
+}
+
+
+void AutoCloseFD::close()
+{
+ if (fd != -1) {
+ if (::close(fd) == -1)
+ /* This should never happen. */
+ throw SysError("closing file descriptor %1%", fd);
+ fd = -1;
+ }
+}
+
+void AutoCloseFD::fsync()
+{
+ if (fd != -1) {
+ int result;
+#if __APPLE__
+ result = ::fcntl(fd, F_FULLFSYNC);
+#else
+ result = ::fsync(fd);
+#endif
+ if (result == -1)
+ throw SysError("fsync file descriptor %1%", fd);
+ }
+}
+
+
+AutoCloseFD::operator bool() const
+{
+ return fd != -1;
+}
+
+
+int AutoCloseFD::release()
+{
+ int oldFD = fd;
+ fd = -1;
+ return oldFD;
+}
+
+
+void Pipe::create()
+{
+ int fds[2];
+#if HAVE_PIPE2
+ if (pipe2(fds, O_CLOEXEC) != 0) throw SysError("creating pipe");
+#else
+ if (pipe(fds) != 0) throw SysError("creating pipe");
+ closeOnExec(fds[0]);
+ closeOnExec(fds[1]);
+#endif
+ readSide = AutoCloseFD{fds[0]};
+ writeSide = AutoCloseFD{fds[1]};
+}
+
+
+void Pipe::close()
+{
+ readSide.close();
+ writeSide.close();
+}
+
+
+void closeMostFDs(const std::set<int> & exceptions)
+{
+#if __linux__
+ try {
+ for (auto & s : readDirectory("/proc/self/fd")) {
+ auto fd = std::stoi(s.name);
+ if (!exceptions.count(fd)) {
+ debug("closing leaked FD %d", fd);
+ close(fd);
+ }
+ }
+ return;
+ } catch (SysError &) {
+ }
+#endif
+
+ int maxFD = 0;
+ maxFD = sysconf(_SC_OPEN_MAX);
+ for (int fd = 0; fd < maxFD; ++fd)
+ if (!exceptions.count(fd))
+ close(fd); /* ignore result */
+}
+
+
+void closeOnExec(int fd)
+{
+ int prev;
+ if ((prev = fcntl(fd, F_GETFD, 0)) == -1 ||
+ fcntl(fd, F_SETFD, prev | FD_CLOEXEC) == -1)
+ throw SysError("setting close-on-exec flag");
+}
+
+}
diff --git a/src/libutil/file-descriptor.hh b/src/libutil/file-descriptor.hh
new file mode 100644
index 000000000..f59baa7a0
--- /dev/null
+++ b/src/libutil/file-descriptor.hh
@@ -0,0 +1,80 @@
+#pragma once
+///@file
+
+#include "error.hh"
+
+namespace nix {
+
+struct Sink;
+struct Source;
+
+/**
+ * Read a line from a file descriptor.
+ */
+std::string readLine(int fd);
+
+/**
+ * Write a line to a file descriptor.
+ */
+void writeLine(int fd, std::string s);
+
+/**
+ * Read the contents of a file into a string.
+ */
+std::string readFile(int fd);
+
+/**
+ * Wrappers arount read()/write() that read/write exactly the
+ * requested number of bytes.
+ */
+void readFull(int fd, char * buf, size_t count);
+void writeFull(int fd, std::string_view s, bool allowInterrupts = true);
+
+/**
+ * Read a file descriptor until EOF occurs.
+ */
+std::string drainFD(int fd, bool block = true, const size_t reserveSize=0);
+
+void drainFD(int fd, Sink & sink, bool block = true);
+
+class AutoCloseFD
+{
+ int fd;
+public:
+ AutoCloseFD();
+ explicit AutoCloseFD(int fd);
+ AutoCloseFD(const AutoCloseFD & fd) = delete;
+ AutoCloseFD(AutoCloseFD&& fd);
+ ~AutoCloseFD();
+ AutoCloseFD& operator =(const AutoCloseFD & fd) = delete;
+ AutoCloseFD& operator =(AutoCloseFD&& fd) noexcept(false);
+ int get() const;
+ explicit operator bool() const;
+ int release();
+ void close();
+ void fsync();
+ void reset() { *this = {}; }
+};
+
+class Pipe
+{
+public:
+ AutoCloseFD readSide, writeSide;
+ void create();
+ void close();
+};
+
+/**
+ * Close all file descriptors except those listed in the given set.
+ * Good practice in child processes.
+ */
+void closeMostFDs(const std::set<int> & exceptions);
+
+/**
+ * Set the close-on-exec flag for the given file descriptor.
+ */
+void closeOnExec(int fd);
+
+MakeError(EndOfFile, Error);
+
+}
diff --git a/src/libutil/file-system.cc b/src/libutil/file-system.cc
new file mode 100644
index 000000000..d573b22b4
--- /dev/null
+++ b/src/libutil/file-system.cc
@@ -0,0 +1,670 @@
+#include <sys/time.h>
+#include <filesystem>
+#include <atomic>
+
+#include "environment-variables.hh"
+#include "file-descriptor.hh"
+#include "file-system.hh"
+#include "finally.hh"
+#include "logging.hh"
+#include "serialise.hh"
+#include "signals.hh"
+#include "types.hh"
+#include "users.hh"
+
+namespace fs = std::filesystem;
+
+namespace nix {
+
+Path absPath(Path path, std::optional<PathView> dir, bool resolveSymlinks)
+{
+ if (path.empty() || path[0] != '/') {
+ if (!dir) {
+#ifdef __GNU__
+ /* GNU (aka. GNU/Hurd) doesn't have any limitation on path
+ lengths and doesn't define `PATH_MAX'. */
+ char *buf = getcwd(NULL, 0);
+ if (buf == NULL)
+#else
+ char buf[PATH_MAX];
+ if (!getcwd(buf, sizeof(buf)))
+#endif
+ throw SysError("cannot get cwd");
+ path = concatStrings(buf, "/", path);
+#ifdef __GNU__
+ free(buf);
+#endif
+ } else
+ path = concatStrings(*dir, "/", path);
+ }
+ return canonPath(path, resolveSymlinks);
+}
+
+
+Path canonPath(PathView path, bool resolveSymlinks)
+{
+ assert(path != "");
+
+ std::string s;
+ s.reserve(256);
+
+ if (path[0] != '/')
+ throw Error("not an absolute path: '%1%'", path);
+
+ std::string temp;
+
+ /* Count the number of times we follow a symlink and stop at some
+ arbitrary (but high) limit to prevent infinite loops. */
+ unsigned int followCount = 0, maxFollow = 1024;
+
+ while (1) {
+
+ /* Skip slashes. */
+ while (!path.empty() && path[0] == '/') path.remove_prefix(1);
+ if (path.empty()) break;
+
+ /* Ignore `.'. */
+ if (path == "." || path.substr(0, 2) == "./")
+ path.remove_prefix(1);
+
+ /* If `..', delete the last component. */
+ else if (path == ".." || path.substr(0, 3) == "../")
+ {
+ if (!s.empty()) s.erase(s.rfind('/'));
+ path.remove_prefix(2);
+ }
+
+ /* Normal component; copy it. */
+ else {
+ s += '/';
+ if (const auto slash = path.find('/'); slash == std::string::npos) {
+ s += path;
+ path = {};
+ } else {
+ s += path.substr(0, slash);
+ path = path.substr(slash);
+ }
+
+ /* If s points to a symlink, resolve it and continue from there */
+ if (resolveSymlinks && isLink(s)) {
+ if (++followCount >= maxFollow)
+ throw Error("infinite symlink recursion in path '%1%'", path);
+ temp = concatStrings(readLink(s), path);
+ path = temp;
+ if (!temp.empty() && temp[0] == '/') {
+ s.clear(); /* restart for symlinks pointing to absolute path */
+ } else {
+ s = dirOf(s);
+ if (s == "/") { // we don’t want trailing slashes here, which dirOf only produces if s = /
+ s.clear();
+ }
+ }
+ }
+ }
+ }
+
+ return s.empty() ? "/" : std::move(s);
+}
+
+void chmodPath(const Path & path, mode_t mode)
+{
+ if (chmod(path.c_str(), mode) == -1)
+ throw SysError("setting permissions on '%s'", path);
+}
+
+Path dirOf(const PathView path)
+{
+ Path::size_type pos = path.rfind('/');
+ if (pos == std::string::npos)
+ return ".";
+ return pos == 0 ? "/" : Path(path, 0, pos);
+}
+
+
+std::string_view baseNameOf(std::string_view path)
+{
+ if (path.empty())
+ return "";
+
+ auto last = path.size() - 1;
+ if (path[last] == '/' && last > 0)
+ last -= 1;
+
+ auto pos = path.rfind('/', last);
+ if (pos == std::string::npos)
+ pos = 0;
+ else
+ pos += 1;
+
+ return path.substr(pos, last - pos + 1);
+}
+
+
+std::string expandTilde(std::string_view path)
+{
+ // TODO: expand ~user ?
+ auto tilde = path.substr(0, 2);
+ if (tilde == "~/" || tilde == "~")
+ return getHome() + std::string(path.substr(1));
+ else
+ return std::string(path);
+}
+
+
+bool isInDir(std::string_view path, std::string_view dir)
+{
+ return path.substr(0, 1) == "/"
+ && path.substr(0, dir.size()) == dir
+ && path.size() >= dir.size() + 2
+ && path[dir.size()] == '/';
+}
+
+
+bool isDirOrInDir(std::string_view path, std::string_view dir)
+{
+ return path == dir || isInDir(path, dir);
+}
+
+
+struct stat stat(const Path & path)
+{
+ struct stat st;
+ if (stat(path.c_str(), &st))
+ throw SysError("getting status of '%1%'", path);
+ return st;
+}
+
+
+struct stat lstat(const Path & path)
+{
+ struct stat st;
+ if (lstat(path.c_str(), &st))
+ throw SysError("getting status of '%1%'", path);
+ return st;
+}
+
+std::optional<struct stat> maybeLstat(const Path & path)
+{
+ std::optional<struct stat> st{std::in_place};
+ if (lstat(path.c_str(), &*st))
+ {
+ if (errno == ENOENT || errno == ENOTDIR)
+ st.reset();
+ else
+ throw SysError("getting status of '%s'", path);
+ }
+ return st;
+}
+
+bool pathExists(const Path & path)
+{
+ return maybeLstat(path).has_value();
+}
+
+bool pathAccessible(const Path & path)
+{
+ try {
+ return pathExists(path);
+ } catch (SysError & e) {
+ // swallow EPERM
+ if (e.errNo == EPERM) return false;
+ throw;
+ }
+}
+
+
+Path readLink(const Path & path)
+{
+ checkInterrupt();
+ std::vector<char> buf;
+ for (ssize_t bufSize = PATH_MAX/4; true; bufSize += bufSize/2) {
+ buf.resize(bufSize);
+ ssize_t rlSize = readlink(path.c_str(), buf.data(), bufSize);
+ if (rlSize == -1)
+ if (errno == EINVAL)
+ throw Error("'%1%' is not a symlink", path);
+ else
+ throw SysError("reading symbolic link '%1%'", path);
+ else if (rlSize < bufSize)
+ return std::string(buf.data(), rlSize);
+ }
+}
+
+
+bool isLink(const Path & path)
+{
+ struct stat st = lstat(path);
+ return S_ISLNK(st.st_mode);
+}
+
+
+DirEntries readDirectory(DIR *dir, const Path & path)
+{
+ DirEntries entries;
+ entries.reserve(64);
+
+ struct dirent * dirent;
+ while (errno = 0, dirent = readdir(dir)) { /* sic */
+ checkInterrupt();
+ std::string name = dirent->d_name;
+ if (name == "." || name == "..") continue;
+ entries.emplace_back(name, dirent->d_ino,
+#ifdef HAVE_STRUCT_DIRENT_D_TYPE
+ dirent->d_type
+#else
+ DT_UNKNOWN
+#endif
+ );
+ }
+ if (errno) throw SysError("reading directory '%1%'", path);
+
+ return entries;
+}
+
+DirEntries readDirectory(const Path & path)
+{
+ AutoCloseDir dir(opendir(path.c_str()));
+ if (!dir) throw SysError("opening directory '%1%'", path);
+
+ return readDirectory(dir.get(), path);
+}
+
+
+unsigned char getFileType(const Path & path)
+{
+ struct stat st = lstat(path);
+ if (S_ISDIR(st.st_mode)) return DT_DIR;
+ if (S_ISLNK(st.st_mode)) return DT_LNK;
+ if (S_ISREG(st.st_mode)) return DT_REG;
+ return DT_UNKNOWN;
+}
+
+
+std::string readFile(const Path & path)
+{
+ AutoCloseFD fd{open(path.c_str(), O_RDONLY | O_CLOEXEC)};
+ if (!fd)
+ throw SysError("opening file '%1%'", path);
+ return readFile(fd.get());
+}
+
+
+void readFile(const Path & path, Sink & sink)
+{
+ AutoCloseFD fd{open(path.c_str(), O_RDONLY | O_CLOEXEC)};
+ if (!fd)
+ throw SysError("opening file '%s'", path);
+ drainFD(fd.get(), sink);
+}
+
+
+void writeFile(const Path & path, std::string_view s, mode_t mode, bool sync)
+{
+ AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)};
+ if (!fd)
+ throw SysError("opening file '%1%'", path);
+ try {
+ writeFull(fd.get(), s);
+ } catch (Error & e) {
+ e.addTrace({}, "writing file '%1%'", path);
+ throw;
+ }
+ if (sync)
+ fd.fsync();
+ // Explicitly close to make sure exceptions are propagated.
+ fd.close();
+ if (sync)
+ syncParent(path);
+}
+
+
+void writeFile(const Path & path, Source & source, mode_t mode, bool sync)
+{
+ AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)};
+ if (!fd)
+ throw SysError("opening file '%1%'", path);
+
+ std::vector<char> buf(64 * 1024);
+
+ try {
+ while (true) {
+ try {
+ auto n = source.read(buf.data(), buf.size());
+ writeFull(fd.get(), {buf.data(), n});
+ } catch (EndOfFile &) { break; }
+ }
+ } catch (Error & e) {
+ e.addTrace({}, "writing file '%1%'", path);
+ throw;
+ }
+ if (sync)
+ fd.fsync();
+ // Explicitly close to make sure exceptions are propagated.
+ fd.close();
+ if (sync)
+ syncParent(path);
+}
+
+void syncParent(const Path & path)
+{
+ AutoCloseFD fd{open(dirOf(path).c_str(), O_RDONLY, 0)};
+ if (!fd)
+ throw SysError("opening file '%1%'", path);
+ fd.fsync();
+}
+
+static void _deletePath(int parentfd, const Path & path, uint64_t & bytesFreed)
+{
+ checkInterrupt();
+
+ std::string name(baseNameOf(path));
+
+ struct stat st;
+ if (fstatat(parentfd, name.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) {
+ if (errno == ENOENT) return;
+ throw SysError("getting status of '%1%'", path);
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ /* We are about to delete a file. Will it likely free space? */
+
+ switch (st.st_nlink) {
+ /* Yes: last link. */
+ case 1:
+ bytesFreed += st.st_size;
+ break;
+ /* Maybe: yes, if 'auto-optimise-store' or manual optimisation
+ was performed. Instead of checking for real let's assume
+ it's an optimised file and space will be freed.
+
+ In worst case we will double count on freed space for files
+ with exactly two hardlinks for unoptimised packages.
+ */
+ case 2:
+ bytesFreed += st.st_size;
+ break;
+ /* No: 3+ links. */
+ default:
+ break;
+ }
+ }
+
+ if (S_ISDIR(st.st_mode)) {
+ /* Make the directory accessible. */
+ const auto PERM_MASK = S_IRUSR | S_IWUSR | S_IXUSR;
+ if ((st.st_mode & PERM_MASK) != PERM_MASK) {
+ if (fchmodat(parentfd, name.c_str(), st.st_mode | PERM_MASK, 0) == -1)
+ throw SysError("chmod '%1%'", path);
+ }
+
+ int fd = openat(parentfd, path.c_str(), O_RDONLY);
+ if (fd == -1)
+ throw SysError("opening directory '%1%'", path);
+ AutoCloseDir dir(fdopendir(fd));
+ if (!dir)
+ throw SysError("opening directory '%1%'", path);
+ for (auto & i : readDirectory(dir.get(), path))
+ _deletePath(dirfd(dir.get()), path + "/" + i.name, bytesFreed);
+ }
+
+ int flags = S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0;
+ if (unlinkat(parentfd, name.c_str(), flags) == -1) {
+ if (errno == ENOENT) return;
+ throw SysError("cannot unlink '%1%'", path);
+ }
+}
+
+static void _deletePath(const Path & path, uint64_t & bytesFreed)
+{
+ Path dir = dirOf(path);
+ if (dir == "")
+ dir = "/";
+
+ AutoCloseFD dirfd{open(dir.c_str(), O_RDONLY)};
+ if (!dirfd) {
+ if (errno == ENOENT) return;
+ throw SysError("opening directory '%1%'", path);
+ }
+
+ _deletePath(dirfd.get(), path, bytesFreed);
+}
+
+
+void deletePath(const Path & path)
+{
+ uint64_t dummy;
+ deletePath(path, dummy);
+}
+
+
+void deletePath(const Path & path, uint64_t & bytesFreed)
+{
+ //Activity act(*logger, lvlDebug, "recursively deleting path '%1%'", path);
+ bytesFreed = 0;
+ _deletePath(path, bytesFreed);
+}
+
+Paths createDirs(const Path & path)
+{
+ Paths created;
+ if (path == "/") return created;
+
+ struct stat st;
+ if (lstat(path.c_str(), &st) == -1) {
+ created = createDirs(dirOf(path));
+ if (mkdir(path.c_str(), 0777) == -1 && errno != EEXIST)
+ throw SysError("creating directory '%1%'", path);
+ st = lstat(path);
+ created.push_back(path);
+ }
+
+ if (S_ISLNK(st.st_mode) && stat(path.c_str(), &st) == -1)
+ throw SysError("statting symlink '%1%'", path);
+
+ if (!S_ISDIR(st.st_mode)) throw Error("'%1%' is not a directory", path);
+
+ return created;
+}
+
+
+//////////////////////////////////////////////////////////////////////
+
+AutoDelete::AutoDelete() : del{false} {}
+
+AutoDelete::AutoDelete(const std::string & p, bool recursive) : path(p)
+{
+ del = true;
+ this->recursive = recursive;
+}
+
+AutoDelete::~AutoDelete()
+{
+ try {
+ if (del) {
+ if (recursive)
+ deletePath(path);
+ else {
+ if (remove(path.c_str()) == -1)
+ throw SysError("cannot unlink '%1%'", path);
+ }
+ }
+ } catch (...) {
+ ignoreException();
+ }
+}
+
+void AutoDelete::cancel()
+{
+ del = false;
+}
+
+void AutoDelete::reset(const Path & p, bool recursive) {
+ path = p;
+ this->recursive = recursive;
+ del = true;
+}
+
+//////////////////////////////////////////////////////////////////////
+
+static Path tempName(Path tmpRoot, const Path & prefix, bool includePid,
+ std::atomic<unsigned int> & counter)
+{
+ tmpRoot = canonPath(tmpRoot.empty() ? getEnv("TMPDIR").value_or("/tmp") : tmpRoot, true);
+ if (includePid)
+ return fmt("%1%/%2%-%3%-%4%", tmpRoot, prefix, getpid(), counter++);
+ else
+ return fmt("%1%/%2%-%3%", tmpRoot, prefix, counter++);
+}
+
+Path createTempDir(const Path & tmpRoot, const Path & prefix,
+ bool includePid, bool useGlobalCounter, mode_t mode)
+{
+ static std::atomic<unsigned int> globalCounter = 0;
+ std::atomic<unsigned int> localCounter = 0;
+ auto & 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)
+{
+ if (symlink(target.c_str(), link.c_str()))
+ throw SysError("creating symlink from '%1%' to '%2%'", link, target);
+}
+
+void replaceSymlink(const Path & target, const Path & link)
+{
+ for (unsigned int n = 0; true; n++) {
+ Path tmp = canonPath(fmt("%s/.%d_%s", dirOf(link), n, baseNameOf(link)));
+
+ try {
+ createSymlink(target, tmp);
+ } 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, CopyFileFlags flags)
+{
+ // 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 (flags.deleteAfter && 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)) {
+ auto opts = fs::copy_options::overwrite_existing;
+
+ if (!flags.followSymlinks) {
+ opts |= fs::copy_options::copy_symlinks;
+ }
+
+ fs::copy(from.path(), to, opts);
+ } else if (fs::is_directory(fromStatus)) {
+ fs::create_directory(to);
+ for (auto & entry : fs::directory_iterator(from.path())) {
+ copy(entry, to / entry.path().filename(), flags);
+ }
+ } else {
+ throw Error("file '%s' has an unsupported type", from.path());
+ }
+
+ setWriteTime(to, statOfFrom);
+ if (flags.deleteAfter) {
+ 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 copyFile(const Path & oldPath, const Path & newPath, CopyFileFlags flags)
+{
+ return copy(fs::directory_entry(fs::path(oldPath)), fs::path(newPath), flags);
+}
+
+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, { .deleteAfter = true });
+ renameFile(tempCopyTarget, newPath);
+ }
+ }
+}
+
+}
diff --git a/src/libutil/file-system.hh b/src/libutil/file-system.hh
new file mode 100644
index 000000000..b9b753980
--- /dev/null
+++ b/src/libutil/file-system.hh
@@ -0,0 +1,269 @@
+#pragma once
+/**
+ * @file
+ *
+ * Utiltities for working with the file sytem and file paths.
+ */
+
+#include "types.hh"
+#include "file-descriptor.hh"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <dirent.h>
+#include <unistd.h>
+
+#include <boost/lexical_cast.hpp>
+
+#include <functional>
+#include <optional>
+
+#ifndef HAVE_STRUCT_DIRENT_D_TYPE
+#define DT_UNKNOWN 0
+#define DT_REG 1
+#define DT_LNK 2
+#define DT_DIR 3
+#endif
+
+namespace nix {
+
+struct Sink;
+struct Source;
+
+/**
+ * @return An absolutized path, resolving paths relative to the
+ * specified directory, or the current directory otherwise. The path
+ * is also canonicalised.
+ */
+Path absPath(Path path,
+ std::optional<PathView> dir = {},
+ bool resolveSymlinks = false);
+
+/**
+ * Canonicalise a path by removing all `.` or `..` components and
+ * double or trailing slashes. Optionally resolves all symlink
+ * components such that each component of the resulting path is *not*
+ * a symbolic link.
+ */
+Path canonPath(PathView path, bool resolveSymlinks = false);
+
+/**
+ * Change the permissions of a path
+ * Not called `chmod` as it shadows and could be confused with
+ * `int chmod(char *, mode_t)`, which does not handle errors
+ */
+void chmodPath(const Path & path, mode_t mode);
+
+/**
+ * @return The directory part of the given canonical path, i.e.,
+ * everything before the final `/`. If the path is the root or an
+ * immediate child thereof (e.g., `/foo`), this means `/`
+ * is returned.
+ */
+Path dirOf(const PathView path);
+
+/**
+ * @return the base name of the given canonical path, i.e., everything
+ * following the final `/` (trailing slashes are removed).
+ */
+std::string_view baseNameOf(std::string_view path);
+
+/**
+ * Perform tilde expansion on a path.
+ */
+std::string expandTilde(std::string_view path);
+
+/**
+ * Check whether 'path' is a descendant of 'dir'. Both paths must be
+ * canonicalized.
+ */
+bool isInDir(std::string_view path, std::string_view dir);
+
+/**
+ * Check whether 'path' is equal to 'dir' or a descendant of
+ * 'dir'. Both paths must be canonicalized.
+ */
+bool isDirOrInDir(std::string_view path, std::string_view dir);
+
+/**
+ * Get status of `path`.
+ */
+struct stat stat(const Path & path);
+struct stat lstat(const Path & path);
+
+/**
+ * `lstat` the given path if it exists.
+ * @return std::nullopt if the path doesn't exist, or an optional containing the result of `lstat` otherwise
+ */
+std::optional<struct stat> maybeLstat(const Path & path);
+
+/**
+ * @return true iff the given path exists.
+ */
+bool pathExists(const Path & path);
+
+/**
+ * A version of pathExists that returns false on a permission error.
+ * Useful for inferring default paths across directories that might not
+ * be readable.
+ * @return true iff the given path can be accessed and exists
+ */
+bool pathAccessible(const Path & path);
+
+/**
+ * Read the contents (target) of a symbolic link. The result is not
+ * in any way canonicalised.
+ */
+Path readLink(const Path & path);
+
+bool isLink(const Path & path);
+
+/**
+ * Read the contents of a directory. The entries `.` and `..` are
+ * removed.
+ */
+struct DirEntry
+{
+ std::string name;
+ ino_t ino;
+ /**
+ * one of DT_*
+ */
+ unsigned char type;
+ DirEntry(std::string name, ino_t ino, unsigned char type)
+ : name(std::move(name)), ino(ino), type(type) { }
+};
+
+typedef std::vector<DirEntry> DirEntries;
+
+DirEntries readDirectory(const Path & path);
+
+unsigned char getFileType(const Path & path);
+
+/**
+ * Read the contents of a file into a string.
+ */
+std::string readFile(const Path & path);
+void readFile(const Path & path, Sink & sink);
+
+/**
+ * Write a string to a file.
+ */
+void writeFile(const Path & path, std::string_view s, mode_t mode = 0666, bool sync = false);
+
+void writeFile(const Path & path, Source & source, mode_t mode = 0666, bool sync = false);
+
+/**
+ * Flush a file's parent directory to disk
+ */
+void syncParent(const Path & path);
+
+/**
+ * Delete a path; i.e., in the case of a directory, it is deleted
+ * recursively. It's not an error if the path does not exist. The
+ * second variant returns the number of bytes and blocks freed.
+ */
+void deletePath(const Path & path);
+
+void deletePath(const Path & path, uint64_t & bytesFreed);
+
+/**
+ * Create a directory and all its parents, if necessary. Returns the
+ * list of created directories, in order of creation.
+ */
+Paths createDirs(const Path & path);
+inline Paths createDirs(PathView path)
+{
+ return createDirs(Path(path));
+}
+
+/**
+ * Create a symlink.
+ */
+void createSymlink(const Path & target, const Path & link);
+
+/**
+ * Atomically create or replace a symlink.
+ */
+void replaceSymlink(const Path & target, const Path & link);
+
+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);
+
+struct CopyFileFlags
+{
+ /**
+ * Delete the file after copying.
+ */
+ bool deleteAfter = false;
+
+ /**
+ * Follow symlinks and copy the eventual target.
+ */
+ bool followSymlinks = false;
+};
+
+/**
+ * Recursively copy the content of `oldPath` to `newPath`. If `andDelete` is
+ * `true`, then also remove `oldPath` (making this equivalent to `moveFile`, but
+ * with the guaranty that the destination will be “fresh”, with no stale inode
+ * or file descriptor pointing to it).
+ */
+void copyFile(const Path & oldPath, const Path & newPath, CopyFileFlags flags);
+
+/**
+ * Automatic cleanup of resources.
+ */
+
+
+class AutoDelete
+{
+ Path path;
+ bool del;
+ bool recursive;
+public:
+ AutoDelete();
+ AutoDelete(const Path & p, bool recursive = true);
+ ~AutoDelete();
+ void cancel();
+ void reset(const Path & p, bool recursive = true);
+ operator Path() const { return path; }
+ operator PathView() const { return path; }
+};
+
+struct DIRDeleter
+{
+ void operator()(DIR * dir) const {
+ closedir(dir);
+ }
+};
+
+typedef std::unique_ptr<DIR, DIRDeleter> AutoCloseDir;
+
+/**
+ * Create a temporary directory.
+ */
+Path createTempDir(const Path & tmpRoot = "", const Path & prefix = "nix",
+ bool includePid = true, bool useGlobalCounter = true, mode_t mode = 0755);
+
+/**
+ * Create a temporary file, returning a file handle and its path.
+ */
+std::pair<AutoCloseFD, Path> createTempFile(const Path & prefix = "nix");
+
+/**
+ * Used in various places.
+ */
+typedef std::function<bool(const Path & path)> PathFilter;
+
+extern PathFilter defaultPathFilter;
+
+}
diff --git a/src/libutil/filesystem.cc b/src/libutil/filesystem.cc
deleted file mode 100644
index b637281d0..000000000
--- a/src/libutil/filesystem.cc
+++ /dev/null
@@ -1,175 +0,0 @@
-#include <sys/time.h>
-#include <filesystem>
-#include <atomic>
-
-#include "finally.hh"
-#include "util.hh"
-#include "signals.hh"
-#include "types.hh"
-
-namespace fs = std::filesystem;
-
-namespace nix {
-
-static Path tempName(Path tmpRoot, const Path & prefix, bool includePid,
- std::atomic<unsigned int> & counter)
-{
- tmpRoot = canonPath(tmpRoot.empty() ? getEnv("TMPDIR").value_or("/tmp") : tmpRoot, true);
- if (includePid)
- return fmt("%1%/%2%-%3%-%4%", tmpRoot, prefix, getpid(), counter++);
- else
- return fmt("%1%/%2%-%3%", tmpRoot, prefix, counter++);
-}
-
-Path createTempDir(const Path & tmpRoot, const Path & prefix,
- bool includePid, bool useGlobalCounter, mode_t mode)
-{
- static std::atomic<unsigned int> globalCounter = 0;
- std::atomic<unsigned int> localCounter = 0;
- auto & 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)
-{
- if (symlink(target.c_str(), link.c_str()))
- throw SysError("creating symlink from '%1%' to '%2%'", link, target);
-}
-
-void replaceSymlink(const Path & target, const Path & link)
-{
- for (unsigned int n = 0; true; n++) {
- Path tmp = canonPath(fmt("%s/.%d_%s", dirOf(link), n, baseNameOf(link)));
-
- try {
- createSymlink(target, tmp);
- } 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, CopyFileFlags flags)
-{
- // 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 (flags.deleteAfter && 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)) {
- auto opts = fs::copy_options::overwrite_existing;
-
- if (!flags.followSymlinks) {
- opts |= fs::copy_options::copy_symlinks;
- }
-
- fs::copy(from.path(), to, opts);
- } else if (fs::is_directory(fromStatus)) {
- fs::create_directory(to);
- for (auto & entry : fs::directory_iterator(from.path())) {
- copy(entry, to / entry.path().filename(), flags);
- }
- } else {
- throw Error("file '%s' has an unsupported type", from.path());
- }
-
- setWriteTime(to, statOfFrom);
- if (flags.deleteAfter) {
- 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 copyFile(const Path & oldPath, const Path & newPath, CopyFileFlags flags)
-{
- return copy(fs::directory_entry(fs::path(oldPath)), fs::path(newPath), flags);
-}
-
-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, { .deleteAfter = true });
- renameFile(tempCopyTarget, newPath);
- }
- }
-}
-
-}
diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc
index 2c36d9d94..006b5000c 100644
--- a/src/libutil/hash.cc
+++ b/src/libutil/hash.cc
@@ -1,4 +1,3 @@
-#include <iostream>
#include <cstring>
#include <openssl/crypto.h>
@@ -8,8 +7,8 @@
#include "args.hh"
#include "hash.hh"
#include "archive.hh"
+#include "logging.hh"
#include "split.hh"
-#include "util.hh"
#include <sys/types.h>
#include <sys/stat.h>
diff --git a/src/libutil/hash.hh b/src/libutil/hash.hh
index ae3ee40f4..bb2a5e2a7 100644
--- a/src/libutil/hash.hh
+++ b/src/libutil/hash.hh
@@ -3,6 +3,7 @@
#include "types.hh"
#include "serialise.hh"
+#include "file-system.hh"
namespace nix {
diff --git a/src/libutil/input-accessor.hh b/src/libutil/input-accessor.hh
index 740175af0..d8e17c9b1 100644
--- a/src/libutil/input-accessor.hh
+++ b/src/libutil/input-accessor.hh
@@ -1,8 +1,10 @@
#pragma once
///@file
-#include "types.hh"
-#include "archive.hh"
+
+#include <map>
+#include <optional>
+#include <string>
namespace nix {
diff --git a/src/libutil/logging.cc b/src/libutil/logging.cc
index 0e63f9bd6..febbfdb55 100644
--- a/src/libutil/logging.cc
+++ b/src/libutil/logging.cc
@@ -1,12 +1,12 @@
+#include "environment-variables.hh"
+#include "file-descriptor.hh"
#include "logging.hh"
-#include "util.hh"
#include "config.hh"
-#include "source-path.hh"
#include "position.hh"
+#include "terminal.hh"
#include <atomic>
#include <nlohmann/json.hpp>
-#include <iostream>
namespace nix {
diff --git a/src/libutil/meson.build b/src/libutil/meson.build
index 58e0bd062..96450fbe2 100644
--- a/src/libutil/meson.build
+++ b/src/libutil/meson.build
@@ -6,13 +6,16 @@ libutil_sources = files(
'compression.cc',
'compute-levels.cc',
'config.cc',
+ 'current-process.cc',
'english.cc',
+ 'environment-variables.cc',
'error.cc',
'escape-char.cc',
'escape-string.cc',
'exit.cc',
'experimental-features.cc',
- 'filesystem.cc',
+ 'file-descriptor.cc',
+ 'file-system.cc',
'git.cc',
'hash.cc',
'hilite.cc',
@@ -21,18 +24,22 @@ libutil_sources = files(
'namespaces.cc',
'position.cc',
'print-elided.cc',
+ 'processes.cc',
'references.cc',
'regex.cc',
'serialise.cc',
'shlex.cc',
'signals.cc',
'source-path.cc',
+ 'strings.cc',
'suggestions.cc',
'tarfile.cc',
+ 'terminal.cc',
'thread-pool.cc',
+ 'unix-domain-socket.cc',
'url.cc',
'url-name.cc',
- 'util.cc',
+ 'users.cc',
'xml-writer.cc',
)
@@ -52,13 +59,17 @@ libutil_headers = files(
'compute-levels.hh',
'config-impl.hh',
'config.hh',
+ 'current-process.hh',
'english.hh',
+ 'environment-variables.hh',
'error.hh',
'escape-char.hh',
'escape-string.hh',
'exit.hh',
'experimental-features.hh',
'experimental-features-json.hh',
+ 'file-descriptor.hh',
+ 'file-system.hh',
'finally.hh',
'fmt.hh',
'git.hh',
@@ -75,6 +86,7 @@ libutil_headers = files(
'pool.hh',
'position.hh',
'print-elided.hh',
+ 'processes.hh',
'ref.hh',
'references.hh',
'regex-combinators.hh',
@@ -85,16 +97,19 @@ libutil_headers = files(
'signals.hh',
'source-path.hh',
'split.hh',
+ 'strings.hh',
'suggestions.hh',
'sync.hh',
'tarfile.hh',
+ 'terminal.hh',
'thread-pool.hh',
'topo-sort.hh',
'types.hh',
+ 'unix-domain-socket.hh',
'url-parts.hh',
'url-name.hh',
'url.hh',
- 'util.hh',
+ 'users.hh',
'variant-wrapper.hh',
'xml-writer.hh',
)
diff --git a/src/libutil/namespaces.cc b/src/libutil/namespaces.cc
index dec3a7189..98d3cd306 100644
--- a/src/libutil/namespaces.cc
+++ b/src/libutil/namespaces.cc
@@ -1,13 +1,73 @@
-#if __linux__
-
+#include "file-descriptor.hh"
+#include "file-system.hh"
+#include "logging.hh"
#include "namespaces.hh"
-#include "util.hh"
-#include "finally.hh"
+#include "processes.hh"
+#include "strings.hh"
#include <sys/mount.h>
+#if __linux__
+# include <mutex>
+# include <sys/resource.h>
+#endif
+
namespace nix {
+#if __linux__
+static AutoCloseFD fdSavedMountNamespace;
+static AutoCloseFD fdSavedRoot;
+#endif
+
+void saveMountNamespace()
+{
+#if __linux__
+ static std::once_flag done;
+ std::call_once(done, []() {
+ fdSavedMountNamespace = AutoCloseFD{open("/proc/self/ns/mnt", O_RDONLY)};
+ if (!fdSavedMountNamespace)
+ throw SysError("saving parent mount namespace");
+
+ fdSavedRoot = AutoCloseFD{open("/proc/self/root", O_RDONLY)};
+ });
+#endif
+}
+
+void restoreMountNamespace()
+{
+#if __linux__
+ try {
+ auto savedCwd = absPath(".");
+
+ if (fdSavedMountNamespace && setns(fdSavedMountNamespace.get(), CLONE_NEWNS) == -1)
+ throw SysError("restoring parent mount namespace");
+
+ if (fdSavedRoot) {
+ if (fchdir(fdSavedRoot.get()))
+ throw SysError("chdir into saved root");
+ if (chroot("."))
+ throw SysError("chroot into saved root");
+ }
+
+ if (chdir(savedCwd.c_str()) == -1)
+ throw SysError("restoring cwd");
+ } catch (Error & e) {
+ debug(e.msg());
+ }
+#endif
+}
+
+void unshareFilesystem()
+{
+#ifdef __linux__
+ if (unshare(CLONE_FS) != 0 && errno != EPERM)
+ throw SysError("unsharing filesystem state in download thread");
+#endif
+}
+
+
+#if __linux__
+
static void diagnoseUserNamespaces()
{
if (!pathExists("/proc/self/ns/user")) {
@@ -93,6 +153,6 @@ bool mountAndPidNamespacesSupported()
return res;
}
-}
-
#endif
+
+}
diff --git a/src/libutil/namespaces.hh b/src/libutil/namespaces.hh
index 0b7eeb66c..3a920e665 100644
--- a/src/libutil/namespaces.hh
+++ b/src/libutil/namespaces.hh
@@ -3,6 +3,26 @@
namespace nix {
+/**
+ * Save the current mount namespace. Ignored if called more than
+ * once.
+ */
+void saveMountNamespace();
+
+/**
+ * Restore the mount namespace saved by saveMountNamespace(). Ignored
+ * if saveMountNamespace() was never called.
+ */
+void restoreMountNamespace();
+
+/**
+ * Cause this thread to not share any FS attributes with the main
+ * thread, because this causes setns() in restoreMountNamespace() to
+ * fail.
+ */
+void unshareFilesystem();
+
+
#if __linux__
bool userNamespacesSupported();
diff --git a/src/libutil/processes.cc b/src/libutil/processes.cc
new file mode 100644
index 000000000..8639a77f8
--- /dev/null
+++ b/src/libutil/processes.cc
@@ -0,0 +1,405 @@
+#include "current-process.hh"
+#include "environment-variables.hh"
+#include "finally.hh"
+#include "logging.hh"
+#include "processes.hh"
+#include "serialise.hh"
+#include "signals.hh"
+
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <future>
+#include <iostream>
+#include <thread>
+
+#include <grp.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#ifdef __APPLE__
+# include <sys/syscall.h>
+#endif
+
+#ifdef __linux__
+# include <sys/prctl.h>
+# include <sys/mman.h>
+#endif
+
+
+namespace nix {
+
+Pid::Pid()
+{
+}
+
+
+Pid::Pid(pid_t pid)
+ : pid(pid)
+{
+}
+
+
+Pid::~Pid() noexcept(false)
+{
+ if (pid != -1) kill();
+}
+
+
+void Pid::operator =(pid_t pid)
+{
+ if (this->pid != -1 && this->pid != pid) kill();
+ this->pid = pid;
+ killSignal = SIGKILL; // reset signal to default
+}
+
+
+Pid::operator pid_t()
+{
+ return pid;
+}
+
+
+int Pid::kill()
+{
+ assert(pid != -1);
+
+ debug("killing process %1%", pid);
+
+ /* Send the requested signal to the child. If it has its own
+ process group, send the signal to every process in the child
+ process group (which hopefully includes *all* its children). */
+ if (::kill(separatePG ? -pid : pid, killSignal) != 0) {
+ /* On BSDs, killing a process group will return EPERM if all
+ processes in the group are zombies (or something like
+ that). So try to detect and ignore that situation. */
+#if __FreeBSD__ || __APPLE__
+ if (errno != EPERM || ::kill(pid, 0) != 0)
+#endif
+ logError(SysError("killing process %d", pid).info());
+ }
+
+ return wait();
+}
+
+
+int Pid::wait()
+{
+ assert(pid != -1);
+ while (1) {
+ int status;
+ int res = waitpid(pid, &status, 0);
+ if (res == pid) {
+ pid = -1;
+ return status;
+ }
+ if (errno != EINTR)
+ throw SysError("cannot get exit status of PID %d", pid);
+ checkInterrupt();
+ }
+}
+
+
+void Pid::setSeparatePG(bool separatePG)
+{
+ this->separatePG = separatePG;
+}
+
+
+void Pid::setKillSignal(int signal)
+{
+ this->killSignal = signal;
+}
+
+
+pid_t Pid::release()
+{
+ pid_t p = pid;
+ pid = -1;
+ return p;
+}
+
+
+void killUser(uid_t uid)
+{
+ debug("killing all processes running under uid '%1%'", uid);
+
+ assert(uid != 0); /* just to be safe... */
+
+ /* The system call kill(-1, sig) sends the signal `sig' to all
+ users to which the current process can send signals. So we
+ fork a process, switch to uid, and send a mass kill. */
+
+ Pid pid = startProcess([&]() {
+
+ if (setuid(uid) == -1)
+ throw SysError("setting uid");
+
+ while (true) {
+#ifdef __APPLE__
+ /* OSX's kill syscall takes a third parameter that, among
+ other things, determines if kill(-1, signo) affects the
+ calling process. In the OSX libc, it's set to true,
+ which means "follow POSIX", which we don't want here
+ */
+ if (syscall(SYS_kill, -1, SIGKILL, false) == 0) break;
+#else
+ if (kill(-1, SIGKILL) == 0) break;
+#endif
+ if (errno == ESRCH || errno == EPERM) break; /* no more processes */
+ if (errno != EINTR)
+ throw SysError("cannot kill processes for uid '%1%'", uid);
+ }
+
+ _exit(0);
+ });
+
+ int status = pid.wait();
+ if (status != 0)
+ throw Error("cannot kill processes for uid '%1%': %2%", uid, statusToString(status));
+
+ /* !!! We should really do some check to make sure that there are
+ no processes left running under `uid', but there is no portable
+ way to do so (I think). The most reliable way may be `ps -eo
+ uid | grep -q $uid'. */
+}
+
+
+//////////////////////////////////////////////////////////////////////
+
+
+static pid_t doFork(std::function<void()> fun)
+{
+ pid_t pid = fork();
+ if (pid != 0) return pid;
+ fun();
+ abort();
+}
+
+#if __linux__
+static int childEntry(void * arg)
+{
+ auto main = (std::function<void()> *) arg;
+ (*main)();
+ return 1;
+}
+#endif
+
+
+pid_t startProcess(std::function<void()> fun, const ProcessOptions & options)
+{
+ std::function<void()> wrapper = [&]() {
+ logger = makeSimpleLogger();
+ try {
+#if __linux__
+ if (options.dieWithParent && prctl(PR_SET_PDEATHSIG, SIGKILL) == -1)
+ throw SysError("setting death signal");
+#endif
+ fun();
+ } catch (std::exception & e) {
+ try {
+ std::cerr << options.errorPrefix << e.what() << "\n";
+ } catch (...) { }
+ } catch (...) { }
+ if (options.runExitHandlers)
+ exit(1);
+ else
+ _exit(1);
+ };
+
+ pid_t pid = -1;
+
+ if (options.cloneFlags) {
+ #ifdef __linux__
+ // Not supported, since then we don't know when to free the stack.
+ assert(!(options.cloneFlags & CLONE_VM));
+
+ size_t stackSize = 1 * 1024 * 1024;
+ auto stack = (char *) mmap(0, stackSize,
+ PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
+ if (stack == MAP_FAILED) throw SysError("allocating stack");
+
+ Finally freeStack([&]() { munmap(stack, stackSize); });
+
+ pid = clone(childEntry, stack + stackSize, options.cloneFlags | SIGCHLD, &wrapper);
+ #else
+ throw Error("clone flags are only supported on Linux");
+ #endif
+ } else
+ pid = doFork(wrapper);
+
+ if (pid == -1) throw SysError("unable to fork");
+
+ return pid;
+}
+
+std::string runProgram(Path program, bool searchPath, const Strings & args,
+ const std::optional<std::string> & input, bool isInteractive)
+{
+ auto res = runProgram(RunOptions {.program = program, .searchPath = searchPath, .args = args, .input = input, .isInteractive = isInteractive});
+
+ if (!statusOk(res.first))
+ throw ExecError(res.first, "program '%1%' %2%", program, statusToString(res.first));
+
+ return res.second;
+}
+
+// Output = error code + "standard out" output stream
+std::pair<int, std::string> runProgram(RunOptions && options)
+{
+ StringSink sink;
+ options.standardOut = &sink;
+
+ int status = 0;
+
+ try {
+ runProgram2(options);
+ } catch (ExecError & e) {
+ status = e.status;
+ }
+
+ return {status, std::move(sink.s)};
+}
+
+void runProgram2(const RunOptions & options)
+{
+ checkInterrupt();
+
+ assert(!(options.standardIn && options.input));
+
+ std::unique_ptr<Source> source_;
+ Source * source = options.standardIn;
+
+ if (options.input) {
+ source_ = std::make_unique<StringSource>(*options.input);
+ source = source_.get();
+ }
+
+ /* Create a pipe. */
+ Pipe out, in;
+ if (options.standardOut) out.create();
+ if (source) in.create();
+
+ ProcessOptions processOptions;
+
+ std::optional<Finally<std::function<void()>>> resumeLoggerDefer;
+ if (options.isInteractive) {
+ logger->pause();
+ resumeLoggerDefer.emplace(
+ []() {
+ logger->resume();
+ }
+ );
+ }
+
+ /* Fork. */
+ Pid pid = startProcess([&]() {
+ if (options.environment)
+ replaceEnv(*options.environment);
+ if (options.standardOut && dup2(out.writeSide.get(), STDOUT_FILENO) == -1)
+ throw SysError("dupping stdout");
+ if (options.mergeStderrToStdout)
+ if (dup2(STDOUT_FILENO, STDERR_FILENO) == -1)
+ throw SysError("cannot dup stdout into stderr");
+ if (source && dup2(in.readSide.get(), STDIN_FILENO) == -1)
+ throw SysError("dupping stdin");
+
+ if (options.chdir && chdir((*options.chdir).c_str()) == -1)
+ throw SysError("chdir failed");
+ if (options.gid && setgid(*options.gid) == -1)
+ throw SysError("setgid failed");
+ /* Drop all other groups if we're setgid. */
+ if (options.gid && setgroups(0, 0) == -1)
+ throw SysError("setgroups failed");
+ if (options.uid && setuid(*options.uid) == -1)
+ throw SysError("setuid failed");
+
+ Strings args_(options.args);
+ args_.push_front(options.program);
+
+ restoreProcessContext();
+
+ if (options.searchPath)
+ execvp(options.program.c_str(), stringsToCharPtrs(args_).data());
+ // This allows you to refer to a program with a pathname relative
+ // to the PATH variable.
+ else
+ execv(options.program.c_str(), stringsToCharPtrs(args_).data());
+
+ throw SysError("executing '%1%'", options.program);
+ }, processOptions);
+
+ out.writeSide.close();
+
+ std::thread writerThread;
+
+ std::promise<void> promise;
+
+ Finally doJoin([&]() {
+ if (writerThread.joinable())
+ writerThread.join();
+ });
+
+
+ if (source) {
+ in.readSide.close();
+ writerThread = std::thread([&]() {
+ try {
+ std::vector<char> buf(8 * 1024);
+ while (true) {
+ size_t n;
+ try {
+ n = source->read(buf.data(), buf.size());
+ } catch (EndOfFile &) {
+ break;
+ }
+ writeFull(in.writeSide.get(), {buf.data(), n});
+ }
+ promise.set_value();
+ } catch (...) {
+ promise.set_exception(std::current_exception());
+ }
+ in.writeSide.close();
+ });
+ }
+
+ if (options.standardOut)
+ drainFD(out.readSide.get(), *options.standardOut);
+
+ /* Wait for the child to finish. */
+ int status = pid.wait();
+
+ /* Wait for the writer thread to finish. */
+ if (source) promise.get_future().get();
+
+ if (status)
+ throw ExecError(status, "program '%1%' %2%", options.program, statusToString(status));
+}
+
+std::string statusToString(int status)
+{
+ if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+ if (WIFEXITED(status))
+ return fmt("failed with exit code %1%", WEXITSTATUS(status));
+ else if (WIFSIGNALED(status)) {
+ int sig = WTERMSIG(status);
+#if HAVE_STRSIGNAL
+ const char * description = strsignal(sig);
+ return fmt("failed due to signal %1% (%2%)", sig, description);
+#else
+ return fmt("failed due to signal %1%", sig);
+#endif
+ }
+ else
+ return "died abnormally";
+ } else return "succeeded";
+}
+
+
+bool statusOk(int status)
+{
+ return WIFEXITED(status) && WEXITSTATUS(status) == 0;
+}
+
+}
diff --git a/src/libutil/processes.hh b/src/libutil/processes.hh
new file mode 100644
index 000000000..91a4edfd2
--- /dev/null
+++ b/src/libutil/processes.hh
@@ -0,0 +1,115 @@
+#pragma once
+///@file
+
+#include "types.hh"
+#include "error.hh"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <signal.h>
+
+#include <boost/lexical_cast.hpp>
+
+#include <functional>
+#include <map>
+#include <optional>
+
+namespace nix {
+
+struct Sink;
+struct Source;
+
+class Pid
+{
+ pid_t pid = -1;
+ bool separatePG = false;
+ int killSignal = SIGKILL;
+public:
+ Pid();
+ Pid(pid_t pid);
+ ~Pid() noexcept(false);
+ void operator =(pid_t pid);
+ operator pid_t();
+ int kill();
+ int wait();
+
+ void setSeparatePG(bool separatePG);
+ void setKillSignal(int signal);
+ pid_t release();
+};
+
+/**
+ * Kill all processes running under the specified uid by sending them
+ * a SIGKILL.
+ */
+void killUser(uid_t uid);
+
+
+/**
+ * Fork a process that runs the given function, and return the child
+ * pid to the caller.
+ */
+struct ProcessOptions
+{
+ std::string errorPrefix = "";
+ bool dieWithParent = true;
+ bool runExitHandlers = false;
+ /**
+ * use clone() with the specified flags (Linux only)
+ */
+ int cloneFlags = 0;
+};
+
+pid_t startProcess(std::function<void()> fun, const ProcessOptions & options = ProcessOptions());
+
+
+/**
+ * Run a program and return its stdout in a string (i.e., like the
+ * shell backtick operator).
+ */
+std::string runProgram(Path program, bool searchPath = false,
+ const Strings & args = Strings(),
+ const std::optional<std::string> & input = {}, bool isInteractive = false);
+
+struct RunOptions
+{
+ Path program;
+ bool searchPath = true;
+ Strings args;
+ std::optional<uid_t> uid;
+ std::optional<uid_t> gid;
+ std::optional<Path> chdir;
+ std::optional<std::map<std::string, std::string>> environment;
+ std::optional<std::string> input;
+ Source * standardIn = nullptr;
+ Sink * standardOut = nullptr;
+ bool mergeStderrToStdout = false;
+ bool isInteractive = false;
+};
+
+std::pair<int, std::string> runProgram(RunOptions && options);
+
+void runProgram2(const RunOptions & options);
+
+class ExecError : public Error
+{
+public:
+ int status;
+
+ template<typename... Args>
+ ExecError(int status, const Args & ... args)
+ : Error(args...), status(status)
+ { }
+};
+
+/**
+ * Convert the exit status of a child as returned by wait() into an
+ * error string.
+ */
+std::string statusToString(int status);
+
+bool statusOk(int status);
+
+}
diff --git a/src/libutil/references.cc b/src/libutil/references.cc
index 7f59b4c09..6189f69b9 100644
--- a/src/libutil/references.cc
+++ b/src/libutil/references.cc
@@ -1,9 +1,7 @@
#include "references.hh"
#include "hash.hh"
-#include "util.hh"
-#include "archive.hh"
+#include "logging.hh"
-#include <map>
#include <cstdlib>
#include <mutex>
#include <algorithm>
diff --git a/src/libutil/regex-combinators.hh b/src/libutil/regex-combinators.hh
index 87d6aa678..37962944e 100644
--- a/src/libutil/regex-combinators.hh
+++ b/src/libutil/regex-combinators.hh
@@ -1,6 +1,8 @@
#pragma once
///@file
+#include "strings.hh"
+
#include <string_view>
namespace nix::regex {
diff --git a/src/libutil/serialise.cc b/src/libutil/serialise.cc
index a6cc919d2..3a8a01f16 100644
--- a/src/libutil/serialise.cc
+++ b/src/libutil/serialise.cc
@@ -1,5 +1,4 @@
#include "serialise.hh"
-#include "util.hh"
#include "signals.hh"
#include <cstring>
diff --git a/src/libutil/serialise.hh b/src/libutil/serialise.hh
index d1c791823..c9294ba2d 100644
--- a/src/libutil/serialise.hh
+++ b/src/libutil/serialise.hh
@@ -3,8 +3,9 @@
#include <memory>
+#include "strings.hh"
#include "types.hh"
-#include "util.hh"
+#include "file-descriptor.hh"
namespace boost::context { struct stack_context; }
diff --git a/src/libutil/shlex.cc b/src/libutil/shlex.cc
index b5f340251..21fa0502a 100644
--- a/src/libutil/shlex.cc
+++ b/src/libutil/shlex.cc
@@ -1,5 +1,5 @@
#include "shlex.hh"
-#include "util.hh"
+#include "strings.hh"
namespace nix {
diff --git a/src/libutil/signals.cc b/src/libutil/signals.cc
index 41fdc9dc8..a94c2802a 100644
--- a/src/libutil/signals.cc
+++ b/src/libutil/signals.cc
@@ -1,7 +1,7 @@
#include "signals.hh"
-#include "util.hh"
#include "error.hh"
#include "sync.hh"
+#include "terminal.hh"
#include <thread>
diff --git a/src/libutil/signals.hh b/src/libutil/signals.hh
index 71593df95..02f8d2ca3 100644
--- a/src/libutil/signals.hh
+++ b/src/libutil/signals.hh
@@ -1,7 +1,6 @@
#pragma once
/// @file
-#include "types.hh"
#include "error.hh"
#include <sys/types.h>
@@ -12,7 +11,6 @@
#include <atomic>
#include <functional>
-#include <sstream>
namespace nix {
diff --git a/src/libutil/source-path.hh b/src/libutil/source-path.hh
index 4d4c51062..311e9f7a6 100644
--- a/src/libutil/source-path.hh
+++ b/src/libutil/source-path.hh
@@ -6,7 +6,9 @@
*/
#include "ref.hh"
+#include "archive.hh"
#include "canon-path.hh"
+#include "file-system.hh"
#include "repair-flag.hh"
#include "input-accessor.hh"
diff --git a/src/libutil/split.hh b/src/libutil/split.hh
index 4ff940eef..5455b6bff 100644
--- a/src/libutil/split.hh
+++ b/src/libutil/split.hh
@@ -4,8 +4,6 @@
#include <optional>
#include <string_view>
-#include "util.hh"
-
namespace nix {
/**
diff --git a/src/libutil/strings.cc b/src/libutil/strings.cc
new file mode 100644
index 000000000..9cb319cce
--- /dev/null
+++ b/src/libutil/strings.cc
@@ -0,0 +1,232 @@
+#include "strings.hh"
+
+namespace nix {
+
+std::vector<char *> stringsToCharPtrs(const Strings & ss)
+{
+ std::vector<char *> res;
+ for (auto & s : ss) res.push_back((char *) s.c_str());
+ res.push_back(0);
+ return res;
+}
+
+
+template<class C> C tokenizeString(std::string_view s, std::string_view separators)
+{
+ C result;
+ auto pos = s.find_first_not_of(separators, 0);
+ while (pos != std::string_view::npos) {
+ auto end = s.find_first_of(separators, pos + 1);
+ if (end == std::string_view::npos) end = s.size();
+ result.insert(result.end(), std::string(s, pos, end - pos));
+ pos = s.find_first_not_of(separators, end);
+ }
+ return result;
+}
+
+template Strings tokenizeString(std::string_view s, std::string_view separators);
+template StringSet tokenizeString(std::string_view s, std::string_view separators);
+template std::vector<std::string> tokenizeString(std::string_view s, std::string_view separators);
+
+
+std::string chomp(std::string_view s)
+{
+ size_t i = s.find_last_not_of(" \n\r\t");
+ return i == std::string_view::npos ? "" : std::string(s, 0, i + 1);
+}
+
+
+std::string trim(std::string_view s, std::string_view whitespace)
+{
+ auto i = s.find_first_not_of(whitespace);
+ if (i == s.npos) return "";
+ auto j = s.find_last_not_of(whitespace);
+ return std::string(s, i, j == s.npos ? j : j - i + 1);
+}
+
+
+std::string replaceStrings(
+ std::string res,
+ std::string_view from,
+ std::string_view to)
+{
+ if (from.empty()) return res;
+ size_t pos = 0;
+ while ((pos = res.find(from, pos)) != std::string::npos) {
+ res.replace(pos, from.size(), to);
+ pos += to.size();
+ }
+ return res;
+}
+
+
+Rewriter::Rewriter(std::map<std::string, std::string> rewrites)
+ : rewrites(std::move(rewrites))
+{
+ for (const auto & [k, v] : this->rewrites) {
+ assert(!k.empty());
+ initials.push_back(k[0]);
+ }
+ std::ranges::sort(initials);
+ auto [firstDupe, end] = std::ranges::unique(initials);
+ initials.erase(firstDupe, end);
+}
+
+std::string Rewriter::operator()(std::string s)
+{
+ size_t j = 0;
+ while ((j = s.find_first_of(initials, j)) != std::string::npos) {
+ size_t skip = 1;
+ for (auto & [from, to] : rewrites) {
+ if (s.compare(j, from.size(), from) == 0) {
+ s.replace(j, from.size(), to);
+ skip = to.size();
+ break;
+ }
+ }
+ j += skip;
+ }
+ return s;
+}
+
+
+std::string toLower(const std::string & s)
+{
+ std::string r(s);
+ for (auto & c : r)
+ c = std::tolower(c);
+ return r;
+}
+
+
+std::string shellEscape(const std::string_view s)
+{
+ std::string r;
+ r.reserve(s.size() + 2);
+ r += "'";
+ for (auto & i : s)
+ if (i == '\'') r += "'\\''"; else r += i;
+ r += '\'';
+ return r;
+}
+
+constexpr char base64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+std::string base64Encode(std::string_view s)
+{
+ std::string res;
+ res.reserve((s.size() + 2) / 3 * 4);
+ int data = 0, nbits = 0;
+
+ for (char c : s) {
+ data = data << 8 | (unsigned char) c;
+ nbits += 8;
+ while (nbits >= 6) {
+ nbits -= 6;
+ res.push_back(base64Chars[data >> nbits & 0x3f]);
+ }
+ }
+
+ if (nbits) res.push_back(base64Chars[data << (6 - nbits) & 0x3f]);
+ while (res.size() % 4) res.push_back('=');
+
+ return res;
+}
+
+
+std::string base64Decode(std::string_view s)
+{
+ constexpr char npos = -1;
+ constexpr std::array<char, 256> base64DecodeChars = [&]() {
+ std::array<char, 256> result{};
+ for (auto& c : result)
+ c = npos;
+ for (int i = 0; i < 64; i++)
+ result[base64Chars[i]] = i;
+ return result;
+ }();
+
+ std::string res;
+ // Some sequences are missing the padding consisting of up to two '='.
+ // vvv
+ res.reserve((s.size() + 2) / 4 * 3);
+ unsigned int d = 0, bits = 0;
+
+ for (char c : s) {
+ if (c == '=') break;
+ if (c == '\n') continue;
+
+ char digit = base64DecodeChars[(unsigned char) c];
+ if (digit == npos)
+ throw Error("invalid character in Base64 string: '%c'", c);
+
+ bits += 6;
+ d = d << 6 | digit;
+ if (bits >= 8) {
+ res.push_back(d >> (bits - 8) & 0xff);
+ bits -= 8;
+ }
+ }
+
+ return res;
+}
+
+
+std::string stripIndentation(std::string_view s)
+{
+ size_t minIndent = 10000;
+ size_t curIndent = 0;
+ bool atStartOfLine = true;
+
+ for (auto & c : s) {
+ if (atStartOfLine && c == ' ')
+ curIndent++;
+ else if (c == '\n') {
+ if (atStartOfLine)
+ minIndent = std::max(minIndent, curIndent);
+ curIndent = 0;
+ atStartOfLine = true;
+ } else {
+ if (atStartOfLine) {
+ minIndent = std::min(minIndent, curIndent);
+ atStartOfLine = false;
+ }
+ }
+ }
+
+ std::string res;
+
+ size_t pos = 0;
+ while (pos < s.size()) {
+ auto eol = s.find('\n', pos);
+ if (eol == s.npos) eol = s.size();
+ if (eol - pos > minIndent)
+ res.append(s.substr(pos + minIndent, eol - pos - minIndent));
+ res.push_back('\n');
+ pos = eol + 1;
+ }
+
+ return res;
+}
+
+
+std::pair<std::string_view, std::string_view> getLine(std::string_view s)
+{
+ auto newline = s.find('\n');
+
+ if (newline == s.npos) {
+ return {s, ""};
+ } else {
+ auto line = s.substr(0, newline);
+ if (!line.empty() && line[line.size() - 1] == '\r')
+ line = line.substr(0, line.size() - 1);
+ return {line, s.substr(newline + 1)};
+ }
+}
+
+std::string showBytes(uint64_t bytes)
+{
+ return fmt("%.2f MiB", bytes / (1024.0 * 1024.0));
+}
+
+}
diff --git a/src/libutil/strings.hh b/src/libutil/strings.hh
new file mode 100644
index 000000000..daeb5be50
--- /dev/null
+++ b/src/libutil/strings.hh
@@ -0,0 +1,256 @@
+#pragma once
+///@file
+
+#include "error.hh"
+#include "types.hh"
+
+#include <boost/lexical_cast.hpp>
+#include <vector>
+
+namespace nix {
+
+/**
+ * Tree formatting.
+ */
+constexpr char treeConn[] = "├───";
+constexpr char treeLast[] = "└───";
+constexpr char treeLine[] = "│ ";
+constexpr char treeNull[] = " ";
+
+/**
+ * Convert a list of strings to a null-terminated vector of `char
+ * *`s. The result must not be accessed beyond the lifetime of the
+ * list of strings.
+ */
+std::vector<char *> stringsToCharPtrs(const Strings & ss);
+
+
+MakeError(FormatError, Error);
+
+
+/**
+ * String tokenizer.
+ */
+template<class C> C tokenizeString(std::string_view s, std::string_view separators = " \t\n\r");
+
+
+/**
+ * Concatenate the given strings with a separator between the
+ * elements.
+ */
+template<class C>
+std::string concatStringsSep(const std::string_view sep, const C & ss)
+{
+ size_t size = 0;
+ // need a cast to string_view since this is also called with Symbols
+ for (const auto & s : ss) size += sep.size() + std::string_view(s).size();
+ std::string s;
+ s.reserve(size);
+ for (auto & i : ss) {
+ if (s.size() != 0) s += sep;
+ s += i;
+ }
+ return s;
+}
+
+template<class ... Parts>
+auto concatStrings(Parts && ... parts)
+ -> std::enable_if_t<(... && std::is_convertible_v<Parts, std::string_view>), std::string>
+{
+ std::string_view views[sizeof...(parts)] = { parts... };
+ return concatStringsSep({}, views);
+}
+
+
+/**
+ * Add quotes around a collection of strings.
+ */
+template<class C> Strings quoteStrings(const C & c)
+{
+ Strings res;
+ for (auto & s : c)
+ res.push_back("'" + s + "'");
+ return res;
+}
+
+/**
+ * Remove trailing whitespace from a string.
+ *
+ * \todo return std::string_view.
+ */
+std::string chomp(std::string_view s);
+
+
+/**
+ * Remove whitespace from the start and end of a string.
+ */
+std::string trim(std::string_view s, std::string_view whitespace = " \n\r\t");
+
+
+/**
+ * Replace all occurrences of a string inside another string.
+ */
+std::string replaceStrings(
+ std::string s,
+ std::string_view from,
+ std::string_view to);
+
+
+/**
+ * Rewrites a string given a map of replacements, applying the replacements in
+ * sorted order, only once, considering only the strings appearing in the input
+ * string in performing replacement.
+ *
+ * - Replacements are not performed on intermediate strings. That is, for an input
+ * `"abb"` with replacements `{"ab" -> "ba"}`, the result is `"bab"`.
+ * - Transitive replacements are not performed. For example, for the input `"abcde"`
+ * with replacements `{"a" -> "b", "b" -> "c", "e" -> "b"}`, the result is
+ * `"bccdb"`.
+ */
+class Rewriter
+{
+private:
+ std::string initials;
+ std::map<std::string, std::string> rewrites;
+
+public:
+ explicit Rewriter(std::map<std::string, std::string> rewrites);
+
+ std::string operator()(std::string s);
+};
+
+inline std::string rewriteStrings(std::string s, const StringMap & rewrites)
+{
+ return Rewriter(rewrites)(s);
+}
+
+
+
+/**
+ * Parse a string into an integer.
+ */
+template<class N>
+std::optional<N> string2Int(const std::string_view s)
+{
+ if (s.substr(0, 1) == "-" && !std::numeric_limits<N>::is_signed)
+ return std::nullopt;
+ try {
+ return boost::lexical_cast<N>(s.data(), s.size());
+ } catch (const boost::bad_lexical_cast &) {
+ return std::nullopt;
+ }
+}
+
+/**
+ * Like string2Int(), but support an optional suffix 'K', 'M', 'G' or
+ * 'T' denoting a binary unit prefix.
+ */
+template<class N>
+N string2IntWithUnitPrefix(std::string_view s)
+{
+ N multiplier = 1;
+ if (!s.empty()) {
+ char u = std::toupper(*s.rbegin());
+ if (std::isalpha(u)) {
+ if (u == 'K') multiplier = 1ULL << 10;
+ else if (u == 'M') multiplier = 1ULL << 20;
+ else if (u == 'G') multiplier = 1ULL << 30;
+ else if (u == 'T') multiplier = 1ULL << 40;
+ else throw UsageError("invalid unit specifier '%1%'", u);
+ s.remove_suffix(1);
+ }
+ }
+ if (auto n = string2Int<N>(s))
+ return *n * multiplier;
+ throw UsageError("'%s' is not an integer", s);
+}
+
+/**
+ * Parse a string into a float.
+ */
+template<class N>
+std::optional<N> string2Float(const std::string_view s)
+{
+ try {
+ return boost::lexical_cast<N>(s.data(), s.size());
+ } catch (const boost::bad_lexical_cast &) {
+ return std::nullopt;
+ }
+}
+
+
+/**
+ * Convert a little-endian integer to host order.
+ */
+template<typename T>
+T readLittleEndian(unsigned char * p)
+{
+ T x = 0;
+ for (size_t i = 0; i < sizeof(x); ++i, ++p) {
+ x |= ((T) *p) << (i * 8);
+ }
+ return x;
+}
+
+/**
+ * Convert a string to lower case.
+ */
+std::string toLower(const std::string & s);
+
+
+/**
+ * Escape a string as a shell word.
+ */
+std::string shellEscape(const std::string_view s);
+
+/**
+ * Base64 encoding/decoding.
+ */
+std::string base64Encode(std::string_view s);
+std::string base64Decode(std::string_view s);
+
+
+/**
+ * Remove common leading whitespace from the lines in the string
+ * 's'. For example, if every line is indented by at least 3 spaces,
+ * then we remove 3 spaces from the start of every line.
+ */
+std::string stripIndentation(std::string_view s);
+
+/**
+ * Get the prefix of 's' up to and excluding the next line break (LF
+ * optionally preceded by CR), and the remainder following the line
+ * break.
+ */
+std::pair<std::string_view, std::string_view> getLine(std::string_view s);
+
+std::string showBytes(uint64_t bytes);
+
+
+/**
+ * Provide an addition operator between strings and string_views
+ * inexplicably omitted from the standard library.
+ */
+inline std::string operator + (const std::string & s1, std::string_view s2)
+{
+ auto s = s1;
+ s.append(s2);
+ return s;
+}
+
+inline std::string operator + (std::string && s, std::string_view s2)
+{
+ s.append(s2);
+ return std::move(s);
+}
+
+inline std::string operator + (std::string_view s1, const char * s2)
+{
+ std::string s;
+ s.reserve(s1.size() + strlen(s2));
+ s.append(s1);
+ s.append(s2);
+ return s;
+}
+
+}
diff --git a/src/libutil/suggestions.cc b/src/libutil/suggestions.cc
index 9510a5f0c..cae52cf29 100644
--- a/src/libutil/suggestions.cc
+++ b/src/libutil/suggestions.cc
@@ -1,7 +1,10 @@
#include "suggestions.hh"
#include "ansicolor.hh"
-#include "util.hh"
+#include "terminal.hh"
+
#include <algorithm>
+#include <ostream>
+#include <vector>
namespace nix {
diff --git a/src/libutil/suggestions.hh b/src/libutil/suggestions.hh
index 9abf5ee5f..3cac1371e 100644
--- a/src/libutil/suggestions.hh
+++ b/src/libutil/suggestions.hh
@@ -2,8 +2,11 @@
///@file
#include "comparator.hh"
-#include "types.hh"
+
#include <set>
+#include <string_view>
+#include <string>
+#include <variant>
namespace nix {
diff --git a/src/libutil/tarfile.cc b/src/libutil/tarfile.cc
index f3f04a936..760a5a65a 100644
--- a/src/libutil/tarfile.cc
+++ b/src/libutil/tarfile.cc
@@ -1,6 +1,8 @@
#include <archive.h>
#include <archive_entry.h>
+#include "file-system.hh"
+#include "logging.hh"
#include "serialise.hh"
#include "tarfile.hh"
diff --git a/src/libutil/terminal.cc b/src/libutil/terminal.cc
new file mode 100644
index 000000000..b58331d04
--- /dev/null
+++ b/src/libutil/terminal.cc
@@ -0,0 +1,104 @@
+#include "terminal.hh"
+#include "environment-variables.hh"
+#include "sync.hh"
+
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+namespace nix {
+
+bool shouldANSI()
+{
+ return isatty(STDERR_FILENO)
+ && getEnv("TERM").value_or("dumb") != "dumb"
+ && !(getEnv("NO_COLOR").has_value() || getEnv("NOCOLOR").has_value());
+}
+
+std::string filterANSIEscapes(std::string_view s, bool filterAll, unsigned int width)
+{
+ std::string t, e;
+ size_t w = 0;
+ auto i = s.begin();
+
+ while (w < (size_t) width && i != s.end()) {
+
+ if (*i == '\e') {
+ std::string e;
+ e += *i++;
+ char last = 0;
+
+ if (i != s.end() && *i == '[') {
+ e += *i++;
+ // eat parameter bytes
+ while (i != s.end() && *i >= 0x30 && *i <= 0x3f) e += *i++;
+ // eat intermediate bytes
+ while (i != s.end() && *i >= 0x20 && *i <= 0x2f) e += *i++;
+ // eat final byte
+ if (i != s.end() && *i >= 0x40 && *i <= 0x7e) e += last = *i++;
+ } else {
+ if (i != s.end() && *i >= 0x40 && *i <= 0x5f) e += *i++;
+ }
+
+ if (!filterAll && last == 'm')
+ t += e;
+ }
+
+ else if (*i == '\t') {
+ i++; t += ' '; w++;
+ while (w < (size_t) width && w % 8) {
+ t += ' '; w++;
+ }
+ }
+
+ else if (*i == '\r' || *i == '\a')
+ // do nothing for now
+ i++;
+
+ else {
+ w++;
+ // Copy one UTF-8 character.
+ if ((*i & 0xe0) == 0xc0) {
+ t += *i++;
+ if (i != s.end() && ((*i & 0xc0) == 0x80)) t += *i++;
+ } else if ((*i & 0xf0) == 0xe0) {
+ t += *i++;
+ if (i != s.end() && ((*i & 0xc0) == 0x80)) {
+ t += *i++;
+ if (i != s.end() && ((*i & 0xc0) == 0x80)) t += *i++;
+ }
+ } else if ((*i & 0xf8) == 0xf0) {
+ t += *i++;
+ if (i != s.end() && ((*i & 0xc0) == 0x80)) {
+ t += *i++;
+ if (i != s.end() && ((*i & 0xc0) == 0x80)) {
+ t += *i++;
+ if (i != s.end() && ((*i & 0xc0) == 0x80)) t += *i++;
+ }
+ }
+ } else
+ t += *i++;
+ }
+ }
+
+ return t;
+}
+
+static Sync<std::pair<unsigned short, unsigned short>> windowSize{{0, 0}};
+
+void updateWindowSize()
+{
+ struct winsize ws;
+ if (ioctl(2, TIOCGWINSZ, &ws) == 0) {
+ auto windowSize_(windowSize.lock());
+ windowSize_->first = ws.ws_row;
+ windowSize_->second = ws.ws_col;
+ }
+}
+
+
+std::pair<unsigned short, unsigned short> getWindowSize()
+{
+ return *windowSize.lock();
+}
+
+}
diff --git a/src/libutil/terminal.hh b/src/libutil/terminal.hh
new file mode 100644
index 000000000..43df5bd70
--- /dev/null
+++ b/src/libutil/terminal.hh
@@ -0,0 +1,40 @@
+#pragma once
+///@file
+
+#include <limits>
+#include <string>
+
+namespace nix {
+
+/**
+ * Determine whether ANSI escape sequences are appropriate for the
+ * present output.
+ */
+bool shouldANSI();
+
+/**
+ * Truncate a string to 'width' printable characters. If 'filterAll'
+ * is true, all ANSI escape sequences are filtered out. Otherwise,
+ * some escape sequences (such as colour setting) are copied but not
+ * included in the character count. Also, tabs are expanded to
+ * spaces.
+ */
+std::string filterANSIEscapes(std::string_view s,
+ bool filterAll = false,
+ unsigned int width = std::numeric_limits<unsigned int>::max());
+
+/**
+ * Recalculate the window size, updating a global variable. Used in the
+ * `SIGWINCH` signal handler.
+ */
+void updateWindowSize();
+
+/**
+ * @return the number of rows and columns of the terminal.
+ *
+ * The value is cached so this is quick. The cached result is computed
+ * by `updateWindowSize()`.
+ */
+std::pair<unsigned short, unsigned short> getWindowSize();
+
+}
diff --git a/src/libutil/thread-pool.cc b/src/libutil/thread-pool.cc
index a25769d9b..0ff83e997 100644
--- a/src/libutil/thread-pool.cc
+++ b/src/libutil/thread-pool.cc
@@ -1,4 +1,5 @@
#include "thread-pool.hh"
+#include "logging.hh"
#include "signals.hh"
namespace nix {
diff --git a/src/libutil/thread-pool.hh b/src/libutil/thread-pool.hh
index 0e09fae97..3db7ce88f 100644
--- a/src/libutil/thread-pool.hh
+++ b/src/libutil/thread-pool.hh
@@ -1,13 +1,12 @@
#pragma once
///@file
+#include "error.hh"
#include "sync.hh"
-#include "util.hh"
#include <queue>
#include <functional>
#include <thread>
-#include <map>
#include <atomic>
namespace nix {
diff --git a/src/libutil/types.hh b/src/libutil/types.hh
index c86f52175..4974634d8 100644
--- a/src/libutil/types.hh
+++ b/src/libutil/types.hh
@@ -4,6 +4,7 @@
#include "ref.hh"
#include <list>
+#include <optional>
#include <set>
#include <string>
#include <limits>
@@ -51,6 +52,116 @@ struct Explicit {
}
};
+/**
+ * Get a value for the specified key from an associate container.
+ */
+template <class T>
+const typename T::mapped_type * get(const T & map, const typename T::key_type & key)
+{
+ auto i = map.find(key);
+ if (i == map.end()) return nullptr;
+ return &i->second;
+}
+
+template <class T>
+typename T::mapped_type * get(T & map, const typename T::key_type & key)
+{
+ auto i = map.find(key);
+ if (i == map.end()) return nullptr;
+ return &i->second;
+}
+
+/**
+ * Get a value for the specified key from an associate container, or a default value if the key isn't present.
+ */
+template <class T>
+const typename T::mapped_type & getOr(T & map,
+ const typename T::key_type & key,
+ const typename T::mapped_type & defaultValue)
+{
+ auto i = map.find(key);
+ if (i == map.end()) return defaultValue;
+ return i->second;
+}
+
+/**
+ * Remove and return the first item from a container.
+ */
+template <class T>
+std::optional<typename T::value_type> remove_begin(T & c)
+{
+ auto i = c.begin();
+ if (i == c.end()) return {};
+ auto v = std::move(*i);
+ c.erase(i);
+ return v;
+}
+
+
+/**
+ * Remove and return the first item from a container.
+ */
+template <class T>
+std::optional<typename T::value_type> pop(T & c)
+{
+ if (c.empty()) return {};
+ auto v = std::move(c.front());
+ c.pop();
+ return v;
+}
+
+
+/**
+ * A RAII helper that increments a counter on construction and
+ * decrements it on destruction.
+ */
+template<typename T>
+struct MaintainCount
+{
+ T & counter;
+ long delta;
+ MaintainCount(T & counter, long delta = 1) : counter(counter), delta(delta) { counter += delta; }
+ ~MaintainCount() { counter -= delta; }
+};
+
+
+/**
+ * A Rust/Python-like enumerate() iterator adapter.
+ *
+ * Borrowed from http://reedbeta.com/blog/python-like-enumerate-in-cpp17.
+ */
+template <typename T,
+ typename TIter = decltype(std::begin(std::declval<T>())),
+ typename = decltype(std::end(std::declval<T>()))>
+constexpr auto enumerate(T && iterable)
+{
+ struct iterator
+ {
+ size_t i;
+ TIter iter;
+ constexpr bool operator != (const iterator & other) const { return iter != other.iter; }
+ constexpr void operator ++ () { ++i; ++iter; }
+ constexpr auto operator * () const { return std::tie(i, *iter); }
+ };
+
+ struct iterable_wrapper
+ {
+ T iterable;
+ constexpr auto begin() { return iterator{ 0, std::begin(iterable) }; }
+ constexpr auto end() { return iterator{ 0, std::end(iterable) }; }
+ };
+
+ return iterable_wrapper{ std::forward<T>(iterable) };
+}
+
+
+/**
+ * C++17 std::visit boilerplate
+ */
+template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
+template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
+
+
/**
* This wants to be a little bit like rust's Cow type.
diff --git a/src/libutil/unix-domain-socket.cc b/src/libutil/unix-domain-socket.cc
new file mode 100644
index 000000000..a9a2a415a
--- /dev/null
+++ b/src/libutil/unix-domain-socket.cc
@@ -0,0 +1,105 @@
+#include "file-system.hh"
+#include "processes.hh"
+#include "unix-domain-socket.hh"
+#include "strings.hh"
+
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+namespace nix {
+
+AutoCloseFD createUnixDomainSocket()
+{
+ AutoCloseFD fdSocket{socket(PF_UNIX, SOCK_STREAM
+ #ifdef SOCK_CLOEXEC
+ | SOCK_CLOEXEC
+ #endif
+ , 0)};
+ if (!fdSocket)
+ throw SysError("cannot create Unix domain socket");
+ closeOnExec(fdSocket.get());
+ return fdSocket;
+}
+
+
+AutoCloseFD createUnixDomainSocket(const Path & path, mode_t mode)
+{
+ auto fdSocket = nix::createUnixDomainSocket();
+
+ bind(fdSocket.get(), path);
+
+ chmodPath(path.c_str(), mode);
+
+ if (listen(fdSocket.get(), 100) == -1)
+ throw SysError("cannot listen on socket '%1%'", path);
+
+ return fdSocket;
+}
+
+static void bindConnectProcHelper(
+ std::string_view operationName, auto && operation,
+ int fd, const std::string & path)
+{
+ struct sockaddr_un addr;
+ addr.sun_family = AF_UNIX;
+
+ // Casting between types like these legacy C library interfaces
+ // require is forbidden in C++. To maintain backwards
+ // compatibility, the implementation of the bind/connect functions
+ // contains some hints to the compiler that allow for this
+ // special case.
+ auto * psaddr = reinterpret_cast<struct sockaddr *>(&addr);
+
+ if (path.size() + 1 >= sizeof(addr.sun_path)) {
+ Pipe pipe;
+ pipe.create();
+ Pid pid = startProcess([&] {
+ try {
+ pipe.readSide.close();
+ Path dir = dirOf(path);
+ if (chdir(dir.c_str()) == -1)
+ throw SysError("chdir to '%s' failed", dir);
+ std::string base(baseNameOf(path));
+ if (base.size() + 1 >= sizeof(addr.sun_path))
+ throw Error("socket path '%s' is too long", base);
+ memcpy(addr.sun_path, base.c_str(), base.size() + 1);
+ if (operation(fd, psaddr, sizeof(addr)) == -1)
+ throw SysError("cannot %s to socket at '%s'", operationName, path);
+ writeFull(pipe.writeSide.get(), "0\n");
+ } catch (SysError & e) {
+ writeFull(pipe.writeSide.get(), fmt("%d\n", e.errNo));
+ } catch (...) {
+ writeFull(pipe.writeSide.get(), "-1\n");
+ }
+ });
+ pipe.writeSide.close();
+ auto errNo = string2Int<int>(chomp(drainFD(pipe.readSide.get())));
+ if (!errNo || *errNo == -1)
+ throw Error("cannot %s to socket at '%s'", operationName, path);
+ else if (*errNo > 0) {
+ errno = *errNo;
+ throw SysError("cannot %s to socket at '%s'", operationName, path);
+ }
+ } else {
+ memcpy(addr.sun_path, path.c_str(), path.size() + 1);
+ if (operation(fd, psaddr, sizeof(addr)) == -1)
+ throw SysError("cannot %s to socket at '%s'", operationName, path);
+ }
+}
+
+
+void bind(int fd, const std::string & path)
+{
+ unlink(path.c_str());
+
+ bindConnectProcHelper("bind", ::bind, fd, path);
+}
+
+
+void connect(int fd, const std::string & path)
+{
+ bindConnectProcHelper("connect", ::connect, fd, path);
+}
+
+}
diff --git a/src/libutil/unix-domain-socket.hh b/src/libutil/unix-domain-socket.hh
new file mode 100644
index 000000000..692ad2627
--- /dev/null
+++ b/src/libutil/unix-domain-socket.hh
@@ -0,0 +1,31 @@
+#pragma once
+///@file
+
+#include "file-descriptor.hh"
+#include "types.hh"
+
+#include <unistd.h>
+
+namespace nix {
+
+/**
+ * Create a Unix domain socket.
+ */
+AutoCloseFD createUnixDomainSocket();
+
+/**
+ * Create a Unix domain socket in listen mode.
+ */
+AutoCloseFD createUnixDomainSocket(const Path & path, mode_t mode);
+
+/**
+ * Bind a Unix domain socket to a path.
+ */
+void bind(int fd, const std::string & path);
+
+/**
+ * Connect to a Unix domain socket.
+ */
+void connect(int fd, const std::string & path);
+
+}
diff --git a/src/libutil/url-name.cc b/src/libutil/url-name.cc
index 7c526752c..12c55db9a 100644
--- a/src/libutil/url-name.cc
+++ b/src/libutil/url-name.cc
@@ -1,4 +1,3 @@
-#include <iostream>
#include <regex>
#include "url-name.hh"
diff --git a/src/libutil/url-name.hh b/src/libutil/url-name.hh
index 3a3f88e76..dabd23ca1 100644
--- a/src/libutil/url-name.hh
+++ b/src/libutil/url-name.hh
@@ -5,9 +5,6 @@
#include <optional>
#include "url.hh"
-#include "url-parts.hh"
-#include "util.hh"
-#include "split.hh"
namespace nix {
diff --git a/src/libutil/url.cc b/src/libutil/url.cc
index afccc4245..46688cef5 100644
--- a/src/libutil/url.cc
+++ b/src/libutil/url.cc
@@ -1,7 +1,7 @@
#include "url.hh"
#include "url-parts.hh"
-#include "util.hh"
#include "split.hh"
+#include "strings.hh"
namespace nix {
diff --git a/src/libutil/users.cc b/src/libutil/users.cc
new file mode 100644
index 000000000..a9a8a7353
--- /dev/null
+++ b/src/libutil/users.cc
@@ -0,0 +1,105 @@
+#include "environment-variables.hh"
+#include "file-system.hh"
+#include "logging.hh"
+#include "strings.hh"
+
+#include <pwd.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+namespace nix {
+
+std::string getUserName()
+{
+ auto pw = getpwuid(geteuid());
+ std::string name = pw ? pw->pw_name : getEnv("USER").value_or("");
+ if (name.empty())
+ throw Error("cannot figure out user name");
+ return name;
+}
+
+Path getHomeOf(uid_t userId)
+{
+ std::vector<char> buf(16384);
+ struct passwd pwbuf;
+ struct passwd * pw;
+ if (getpwuid_r(userId, &pwbuf, buf.data(), buf.size(), &pw) != 0
+ || !pw || !pw->pw_dir || !pw->pw_dir[0])
+ throw Error("cannot determine user's home directory");
+ return pw->pw_dir;
+}
+
+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.
+ struct stat st;
+ int result = stat(homeDir->c_str(), &st);
+ if (result != 0) {
+ if (errno != ENOENT) {
+ warn("couldn't stat $HOME ('%s') for reason other than not existing ('%d'), falling back to the one defined in the 'passwd' file", *homeDir, errno);
+ homeDir.reset();
+ }
+ } else if (st.st_uid != geteuid()) {
+ unownedUserHomeDir.swap(homeDir);
+ }
+ }
+ if (!homeDir) {
+ homeDir = getHomeOf(geteuid());
+ 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;
+ }();
+ return homeDir;
+}
+
+
+Path getCacheDir()
+{
+ auto cacheDir = getEnv("XDG_CACHE_HOME");
+ return cacheDir ? *cacheDir : getHome() + "/.cache";
+}
+
+
+Path getConfigDir()
+{
+ auto configDir = getEnv("XDG_CONFIG_HOME");
+ return configDir ? *configDir : getHome() + "/.config";
+}
+
+std::vector<Path> getConfigDirs()
+{
+ Path configHome = getConfigDir();
+ auto configDirs = getEnv("XDG_CONFIG_DIRS").value_or("/etc/xdg");
+ std::vector<Path> result = tokenizeString<std::vector<std::string>>(configDirs, ":");
+ result.insert(result.begin(), configHome);
+ return result;
+}
+
+
+Path getDataDir()
+{
+ auto dataDir = getEnv("XDG_DATA_HOME");
+ return dataDir ? *dataDir : getHome() + "/.local/share";
+}
+
+Path getStateDir()
+{
+ auto stateDir = getEnv("XDG_STATE_HOME");
+ return stateDir ? *stateDir : getHome() + "/.local/state";
+}
+
+Path createNixStateDir()
+{
+ Path dir = getStateDir() + "/nix";
+ createDirs(dir);
+ return dir;
+}
+
+}
diff --git a/src/libutil/users.hh b/src/libutil/users.hh
new file mode 100644
index 000000000..3add4c732
--- /dev/null
+++ b/src/libutil/users.hh
@@ -0,0 +1,61 @@
+#pragma once
+///@file
+
+#include "types.hh"
+
+#include <sys/types.h>
+
+namespace nix {
+
+std::string getUserName();
+
+/**
+ * @return the given user's home directory from /etc/passwd.
+ */
+Path getHomeOf(uid_t userId);
+
+/**
+ * @return $HOME or the user's home directory from /etc/passwd.
+ */
+Path getHome();
+
+/**
+ * @return $XDG_CACHE_HOME or $HOME/.cache.
+ */
+Path getCacheDir();
+
+/**
+ * @return $XDG_CONFIG_HOME or $HOME/.config.
+ */
+Path getConfigDir();
+
+/**
+ * @return the directories to search for user configuration files
+ */
+std::vector<Path> getConfigDirs();
+
+/**
+ * @return $XDG_DATA_HOME or $HOME/.local/share.
+ */
+Path getDataDir();
+
+/**
+ * @return $XDG_STATE_HOME or $HOME/.local/state.
+ *
+ * @note Not to be confused with settings.nixStateDir.
+ */
+Path getStateDir();
+
+/**
+ * Create $XDG_STATE_HOME/nix or $HOME/.local/state/nix, and return
+ * the path to it.
+ * @note Not to be confused with settings.nixStateDir.
+ */
+Path createNixStateDir();
+
+/**
+ * Perform tilde expansion on a path.
+ */
+std::string expandTilde(std::string_view path);
+
+}
diff --git a/src/libutil/util.cc b/src/libutil/util.cc
deleted file mode 100644
index 2c0fcc897..000000000
--- a/src/libutil/util.cc
+++ /dev/null
@@ -1,1916 +0,0 @@
-#include "util.hh"
-#include "sync.hh"
-#include "finally.hh"
-#include "serialise.hh"
-#include "cgroup.hh"
-#include "signals.hh"
-
-#include <array>
-#include <cctype>
-#include <cerrno>
-#include <climits>
-#include <cstdio>
-#include <cstdlib>
-#include <cstring>
-#include <future>
-#include <iostream>
-#include <mutex>
-#include <sstream>
-#include <thread>
-
-#include <fcntl.h>
-#include <grp.h>
-#include <pwd.h>
-#include <sys/ioctl.h>
-#include <sys/types.h>
-#include <sys/socket.h>
-#include <sys/wait.h>
-#include <sys/time.h>
-#include <sys/un.h>
-#include <unistd.h>
-
-#ifdef __APPLE__
-#include <sys/syscall.h>
-#include <mach-o/dyld.h>
-#endif
-
-#ifdef __linux__
-#include <sys/prctl.h>
-#include <sys/resource.h>
-#include <sys/mman.h>
-
-#include <cmath>
-#endif
-
-
-extern char * * environ __attribute__((weak));
-
-
-#ifdef NDEBUG
-#error "Lix may not be built with assertions disabled (i.e. with -DNDEBUG)."
-#endif
-
-namespace nix {
-
-std::optional<std::string> getEnv(const std::string & key)
-{
- char * value = getenv(key.c_str());
- if (!value) return {};
- return std::string(value);
-}
-
-std::optional<std::string> getEnvNonEmpty(const std::string & key) {
- auto value = getEnv(key);
- if (value == "") return {};
- return value;
-}
-
-std::map<std::string, std::string> getEnv()
-{
- std::map<std::string, std::string> env;
- for (size_t i = 0; environ[i]; ++i) {
- auto s = environ[i];
- auto eq = strchr(s, '=');
- if (!eq)
- // invalid env, just keep going
- continue;
- env.emplace(std::string(s, eq), std::string(eq + 1));
- }
- return env;
-}
-
-
-void clearEnv()
-{
- for (auto & name : getEnv())
- unsetenv(name.first.c_str());
-}
-
-void replaceEnv(const std::map<std::string, std::string> & newEnv)
-{
- clearEnv();
- for (auto & newEnvVar : newEnv)
- setenv(newEnvVar.first.c_str(), newEnvVar.second.c_str(), 1);
-}
-
-
-Path absPath(Path path, std::optional<PathView> dir, bool resolveSymlinks)
-{
- if (path.empty() || path[0] != '/') {
- if (!dir) {
-#ifdef __GNU__
- /* GNU (aka. GNU/Hurd) doesn't have any limitation on path
- lengths and doesn't define `PATH_MAX'. */
- char *buf = getcwd(NULL, 0);
- if (buf == NULL)
-#else
- char buf[PATH_MAX];
- if (!getcwd(buf, sizeof(buf)))
-#endif
- throw SysError("cannot get cwd");
- path = concatStrings(buf, "/", path);
-#ifdef __GNU__
- free(buf);
-#endif
- } else
- path = concatStrings(*dir, "/", path);
- }
- return canonPath(path, resolveSymlinks);
-}
-
-
-Path canonPath(PathView path, bool resolveSymlinks)
-{
- assert(path != "");
-
- std::string s;
- s.reserve(256);
-
- if (path[0] != '/')
- throw Error("not an absolute path: '%1%'", path);
-
- std::string temp;
-
- /* Count the number of times we follow a symlink and stop at some
- arbitrary (but high) limit to prevent infinite loops. */
- unsigned int followCount = 0, maxFollow = 1024;
-
- while (1) {
-
- /* Skip slashes. */
- while (!path.empty() && path[0] == '/') path.remove_prefix(1);
- if (path.empty()) break;
-
- /* Ignore `.'. */
- if (path == "." || path.substr(0, 2) == "./")
- path.remove_prefix(1);
-
- /* If `..', delete the last component. */
- else if (path == ".." || path.substr(0, 3) == "../")
- {
- if (!s.empty()) s.erase(s.rfind('/'));
- path.remove_prefix(2);
- }
-
- /* Normal component; copy it. */
- else {
- s += '/';
- if (const auto slash = path.find('/'); slash == std::string::npos) {
- s += path;
- path = {};
- } else {
- s += path.substr(0, slash);
- path = path.substr(slash);
- }
-
- /* If s points to a symlink, resolve it and continue from there */
- if (resolveSymlinks && isLink(s)) {
- if (++followCount >= maxFollow)
- throw Error("infinite symlink recursion in path '%1%'", path);
- temp = concatStrings(readLink(s), path);
- path = temp;
- if (!temp.empty() && temp[0] == '/') {
- s.clear(); /* restart for symlinks pointing to absolute path */
- } else {
- s = dirOf(s);
- if (s == "/") { // we don’t want trailing slashes here, which dirOf only produces if s = /
- s.clear();
- }
- }
- }
- }
- }
-
- return s.empty() ? "/" : std::move(s);
-}
-
-void chmodPath(const Path & path, mode_t mode)
-{
- if (chmod(path.c_str(), mode) == -1)
- throw SysError("setting permissions on '%s'", path);
-}
-
-Path dirOf(const PathView path)
-{
- Path::size_type pos = path.rfind('/');
- if (pos == std::string::npos)
- return ".";
- return pos == 0 ? "/" : Path(path, 0, pos);
-}
-
-
-std::string_view baseNameOf(std::string_view path)
-{
- if (path.empty())
- return "";
-
- auto last = path.size() - 1;
- if (path[last] == '/' && last > 0)
- last -= 1;
-
- auto pos = path.rfind('/', last);
- if (pos == std::string::npos)
- pos = 0;
- else
- pos += 1;
-
- return path.substr(pos, last - pos + 1);
-}
-
-
-std::string expandTilde(std::string_view path)
-{
- // TODO: expand ~user ?
- auto tilde = path.substr(0, 2);
- if (tilde == "~/" || tilde == "~")
- return getHome() + std::string(path.substr(1));
- else
- return std::string(path);
-}
-
-
-bool isInDir(std::string_view path, std::string_view dir)
-{
- return path.substr(0, 1) == "/"
- && path.substr(0, dir.size()) == dir
- && path.size() >= dir.size() + 2
- && path[dir.size()] == '/';
-}
-
-
-bool isDirOrInDir(std::string_view path, std::string_view dir)
-{
- return path == dir || isInDir(path, dir);
-}
-
-
-struct stat stat(const Path & path)
-{
- struct stat st;
- if (stat(path.c_str(), &st))
- throw SysError("getting status of '%1%'", path);
- return st;
-}
-
-
-struct stat lstat(const Path & path)
-{
- struct stat st;
- if (lstat(path.c_str(), &st))
- throw SysError("getting status of '%1%'", path);
- return st;
-}
-
-std::optional<struct stat> maybeLstat(const Path & path)
-{
- std::optional<struct stat> st{std::in_place};
- if (lstat(path.c_str(), &*st))
- {
- if (errno == ENOENT || errno == ENOTDIR)
- st.reset();
- else
- throw SysError("getting status of '%s'", path);
- }
- return st;
-}
-
-bool pathExists(const Path & path)
-{
- return maybeLstat(path).has_value();
-}
-
-bool pathAccessible(const Path & path)
-{
- try {
- return pathExists(path);
- } catch (SysError & e) {
- // swallow EPERM
- if (e.errNo == EPERM) return false;
- throw;
- }
-}
-
-
-Path readLink(const Path & path)
-{
- checkInterrupt();
- std::vector<char> buf;
- for (ssize_t bufSize = PATH_MAX/4; true; bufSize += bufSize/2) {
- buf.resize(bufSize);
- ssize_t rlSize = readlink(path.c_str(), buf.data(), bufSize);
- if (rlSize == -1)
- if (errno == EINVAL)
- throw Error("'%1%' is not a symlink", path);
- else
- throw SysError("reading symbolic link '%1%'", path);
- else if (rlSize < bufSize)
- return std::string(buf.data(), rlSize);
- }
-}
-
-
-bool isLink(const Path & path)
-{
- struct stat st = lstat(path);
- return S_ISLNK(st.st_mode);
-}
-
-
-DirEntries readDirectory(DIR *dir, const Path & path)
-{
- DirEntries entries;
- entries.reserve(64);
-
- struct dirent * dirent;
- while (errno = 0, dirent = readdir(dir)) { /* sic */
- checkInterrupt();
- std::string name = dirent->d_name;
- if (name == "." || name == "..") continue;
- entries.emplace_back(name, dirent->d_ino,
-#ifdef HAVE_STRUCT_DIRENT_D_TYPE
- dirent->d_type
-#else
- DT_UNKNOWN
-#endif
- );
- }
- if (errno) throw SysError("reading directory '%1%'", path);
-
- return entries;
-}
-
-DirEntries readDirectory(const Path & path)
-{
- AutoCloseDir dir(opendir(path.c_str()));
- if (!dir) throw SysError("opening directory '%1%'", path);
-
- return readDirectory(dir.get(), path);
-}
-
-
-unsigned char getFileType(const Path & path)
-{
- struct stat st = lstat(path);
- if (S_ISDIR(st.st_mode)) return DT_DIR;
- if (S_ISLNK(st.st_mode)) return DT_LNK;
- if (S_ISREG(st.st_mode)) return DT_REG;
- return DT_UNKNOWN;
-}
-
-
-std::string readFile(int fd)
-{
- struct stat st;
- if (fstat(fd, &st) == -1)
- throw SysError("statting file");
-
- return drainFD(fd, true, st.st_size);
-}
-
-
-std::string readFile(const Path & path)
-{
- AutoCloseFD fd{open(path.c_str(), O_RDONLY | O_CLOEXEC)};
- if (!fd)
- throw SysError("opening file '%1%'", path);
- return readFile(fd.get());
-}
-
-
-void readFile(const Path & path, Sink & sink)
-{
- AutoCloseFD fd{open(path.c_str(), O_RDONLY | O_CLOEXEC)};
- if (!fd)
- throw SysError("opening file '%s'", path);
- drainFD(fd.get(), sink);
-}
-
-
-void writeFile(const Path & path, std::string_view s, mode_t mode, bool sync)
-{
- AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)};
- if (!fd)
- throw SysError("opening file '%1%'", path);
- try {
- writeFull(fd.get(), s);
- } catch (Error & e) {
- e.addTrace({}, "writing file '%1%'", path);
- throw;
- }
- if (sync)
- fd.fsync();
- // Explicitly close to make sure exceptions are propagated.
- fd.close();
- if (sync)
- syncParent(path);
-}
-
-
-void writeFile(const Path & path, Source & source, mode_t mode, bool sync)
-{
- AutoCloseFD fd{open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)};
- if (!fd)
- throw SysError("opening file '%1%'", path);
-
- std::vector<char> buf(64 * 1024);
-
- try {
- while (true) {
- try {
- auto n = source.read(buf.data(), buf.size());
- writeFull(fd.get(), {buf.data(), n});
- } catch (EndOfFile &) { break; }
- }
- } catch (Error & e) {
- e.addTrace({}, "writing file '%1%'", path);
- throw;
- }
- if (sync)
- fd.fsync();
- // Explicitly close to make sure exceptions are propagated.
- fd.close();
- if (sync)
- syncParent(path);
-}
-
-void syncParent(const Path & path)
-{
- AutoCloseFD fd{open(dirOf(path).c_str(), O_RDONLY, 0)};
- if (!fd)
- throw SysError("opening file '%1%'", path);
- fd.fsync();
-}
-
-std::string readLine(int fd)
-{
- std::string s;
- while (1) {
- checkInterrupt();
- char ch;
- // FIXME: inefficient
- ssize_t rd = read(fd, &ch, 1);
- if (rd == -1) {
- if (errno != EINTR)
- throw SysError("reading a line");
- } else if (rd == 0)
- throw EndOfFile("unexpected EOF reading a line");
- else {
- if (ch == '\n') return s;
- s += ch;
- }
- }
-}
-
-
-void writeLine(int fd, std::string s)
-{
- s += '\n';
- writeFull(fd, s);
-}
-
-
-static void _deletePath(int parentfd, const Path & path, uint64_t & bytesFreed)
-{
- checkInterrupt();
-
- std::string name(baseNameOf(path));
-
- struct stat st;
- if (fstatat(parentfd, name.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) {
- if (errno == ENOENT) return;
- throw SysError("getting status of '%1%'", path);
- }
-
- if (!S_ISDIR(st.st_mode)) {
- /* We are about to delete a file. Will it likely free space? */
-
- switch (st.st_nlink) {
- /* Yes: last link. */
- case 1:
- bytesFreed += st.st_size;
- break;
- /* Maybe: yes, if 'auto-optimise-store' or manual optimisation
- was performed. Instead of checking for real let's assume
- it's an optimised file and space will be freed.
-
- In worst case we will double count on freed space for files
- with exactly two hardlinks for unoptimised packages.
- */
- case 2:
- bytesFreed += st.st_size;
- break;
- /* No: 3+ links. */
- default:
- break;
- }
- }
-
- if (S_ISDIR(st.st_mode)) {
- /* Make the directory accessible. */
- const auto PERM_MASK = S_IRUSR | S_IWUSR | S_IXUSR;
- if ((st.st_mode & PERM_MASK) != PERM_MASK) {
- if (fchmodat(parentfd, name.c_str(), st.st_mode | PERM_MASK, 0) == -1)
- throw SysError("chmod '%1%'", path);
- }
-
- int fd = openat(parentfd, path.c_str(), O_RDONLY);
- if (fd == -1)
- throw SysError("opening directory '%1%'", path);
- AutoCloseDir dir(fdopendir(fd));
- if (!dir)
- throw SysError("opening directory '%1%'", path);
- for (auto & i : readDirectory(dir.get(), path))
- _deletePath(dirfd(dir.get()), path + "/" + i.name, bytesFreed);
- }
-
- int flags = S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0;
- if (unlinkat(parentfd, name.c_str(), flags) == -1) {
- if (errno == ENOENT) return;
- throw SysError("cannot unlink '%1%'", path);
- }
-}
-
-static void _deletePath(const Path & path, uint64_t & bytesFreed)
-{
- Path dir = dirOf(path);
- if (dir == "")
- dir = "/";
-
- AutoCloseFD dirfd{open(dir.c_str(), O_RDONLY)};
- if (!dirfd) {
- if (errno == ENOENT) return;
- throw SysError("opening directory '%1%'", path);
- }
-
- _deletePath(dirfd.get(), path, bytesFreed);
-}
-
-
-void deletePath(const Path & path)
-{
- uint64_t dummy;
- deletePath(path, dummy);
-}
-
-
-void deletePath(const Path & path, uint64_t & bytesFreed)
-{
- //Activity act(*logger, lvlDebug, "recursively deleting path '%1%'", path);
- bytesFreed = 0;
- _deletePath(path, bytesFreed);
-}
-
-
-std::string getUserName()
-{
- auto pw = getpwuid(geteuid());
- std::string name = pw ? pw->pw_name : getEnv("USER").value_or("");
- if (name.empty())
- throw Error("cannot figure out user name");
- return name;
-}
-
-Path getHomeOf(uid_t userId)
-{
- std::vector<char> buf(16384);
- struct passwd pwbuf;
- struct passwd * pw;
- if (getpwuid_r(userId, &pwbuf, buf.data(), buf.size(), &pw) != 0
- || !pw || !pw->pw_dir || !pw->pw_dir[0])
- throw Error("cannot determine user's home directory");
- return pw->pw_dir;
-}
-
-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.
- struct stat st;
- int result = stat(homeDir->c_str(), &st);
- if (result != 0) {
- if (errno != ENOENT) {
- warn("couldn't stat $HOME ('%s') for reason other than not existing ('%d'), falling back to the one defined in the 'passwd' file", *homeDir, errno);
- homeDir.reset();
- }
- } else if (st.st_uid != geteuid()) {
- unownedUserHomeDir.swap(homeDir);
- }
- }
- if (!homeDir) {
- homeDir = getHomeOf(geteuid());
- 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;
- }();
- return homeDir;
-}
-
-
-Path getCacheDir()
-{
- auto cacheDir = getEnv("XDG_CACHE_HOME");
- return cacheDir ? *cacheDir : getHome() + "/.cache";
-}
-
-
-Path getConfigDir()
-{
- auto configDir = getEnv("XDG_CONFIG_HOME");
- return configDir ? *configDir : getHome() + "/.config";
-}
-
-std::vector<Path> getConfigDirs()
-{
- Path configHome = getConfigDir();
- auto configDirs = getEnv("XDG_CONFIG_DIRS").value_or("/etc/xdg");
- std::vector<Path> result = tokenizeString<std::vector<std::string>>(configDirs, ":");
- result.insert(result.begin(), configHome);
- return result;
-}
-
-
-Path getDataDir()
-{
- auto dataDir = getEnv("XDG_DATA_HOME");
- return dataDir ? *dataDir : getHome() + "/.local/share";
-}
-
-Path getStateDir()
-{
- auto stateDir = getEnv("XDG_STATE_HOME");
- return stateDir ? *stateDir : getHome() + "/.local/state";
-}
-
-Path createNixStateDir()
-{
- Path dir = getStateDir() + "/nix";
- createDirs(dir);
- return dir;
-}
-
-
-std::optional<Path> getSelfExe()
-{
- static auto cached = []() -> std::optional<Path>
- {
- #if __linux__
- return readLink("/proc/self/exe");
- #elif __APPLE__
- char buf[1024];
- uint32_t size = sizeof(buf);
- if (_NSGetExecutablePath(buf, &size) == 0)
- return buf;
- else
- return std::nullopt;
- #else
- return std::nullopt;
- #endif
- }();
- return cached;
-}
-
-
-Paths createDirs(const Path & path)
-{
- Paths created;
- if (path == "/") return created;
-
- struct stat st;
- if (lstat(path.c_str(), &st) == -1) {
- created = createDirs(dirOf(path));
- if (mkdir(path.c_str(), 0777) == -1 && errno != EEXIST)
- throw SysError("creating directory '%1%'", path);
- st = lstat(path);
- created.push_back(path);
- }
-
- if (S_ISLNK(st.st_mode) && stat(path.c_str(), &st) == -1)
- throw SysError("statting symlink '%1%'", path);
-
- if (!S_ISDIR(st.st_mode)) throw Error("'%1%' is not a directory", path);
-
- return created;
-}
-
-
-void readFull(int fd, char * buf, size_t count)
-{
- while (count) {
- checkInterrupt();
- ssize_t res = read(fd, buf, count);
- if (res == -1) {
- if (errno == EINTR) continue;
- throw SysError("reading from file");
- }
- if (res == 0) throw EndOfFile("unexpected end-of-file");
- count -= res;
- buf += res;
- }
-}
-
-
-void writeFull(int fd, std::string_view s, bool allowInterrupts)
-{
- while (!s.empty()) {
- if (allowInterrupts) checkInterrupt();
- ssize_t res = write(fd, s.data(), s.size());
- if (res == -1 && errno != EINTR)
- throw SysError("writing to file");
- if (res > 0)
- s.remove_prefix(res);
- }
-}
-
-
-std::string drainFD(int fd, bool block, const size_t reserveSize)
-{
- // the parser needs two extra bytes to append terminating characters, other users will
- // not care very much about the extra memory.
- StringSink sink(reserveSize + 2);
- drainFD(fd, sink, block);
- return std::move(sink.s);
-}
-
-
-void drainFD(int fd, Sink & sink, bool block)
-{
- // silence GCC maybe-uninitialized warning in finally
- int saved = 0;
-
- if (!block) {
- saved = fcntl(fd, F_GETFL);
- if (fcntl(fd, F_SETFL, saved | O_NONBLOCK) == -1)
- throw SysError("making file descriptor non-blocking");
- }
-
- Finally finally([&]() {
- if (!block) {
- if (fcntl(fd, F_SETFL, saved) == -1)
- throw SysError("making file descriptor blocking");
- }
- });
-
- std::array<unsigned char, 64 * 1024> buf;
- while (1) {
- checkInterrupt();
- ssize_t rd = read(fd, buf.data(), buf.size());
- if (rd == -1) {
- if (!block && (errno == EAGAIN || errno == EWOULDBLOCK))
- break;
- if (errno != EINTR)
- throw SysError("reading from file");
- }
- else if (rd == 0) break;
- else sink({(char *) buf.data(), (size_t) rd});
- }
-}
-
-//////////////////////////////////////////////////////////////////////
-
-unsigned int getMaxCPU()
-{
- #if __linux__
- try {
- auto cgroupFS = getCgroupFS();
- if (!cgroupFS) return 0;
-
- auto cgroups = getCgroups("/proc/self/cgroup");
- auto cgroup = cgroups[""];
- if (cgroup == "") return 0;
-
- auto cpuFile = *cgroupFS + "/" + cgroup + "/cpu.max";
-
- auto cpuMax = readFile(cpuFile);
- auto cpuMaxParts = tokenizeString<std::vector<std::string>>(cpuMax, " \n");
-
- if (cpuMaxParts.size() != 2) {
- return 0;
- }
-
- auto quota = cpuMaxParts[0];
- auto period = cpuMaxParts[1];
- if (quota != "max")
- return std::ceil(std::stoi(quota) / std::stof(period));
- } catch (Error &) { ignoreException(lvlDebug); }
- #endif
-
- return 0;
-}
-
-//////////////////////////////////////////////////////////////////////
-
-
-AutoDelete::AutoDelete() : del{false} {}
-
-AutoDelete::AutoDelete(const std::string & p, bool recursive) : path(p)
-{
- del = true;
- this->recursive = recursive;
-}
-
-AutoDelete::~AutoDelete()
-{
- try {
- if (del) {
- if (recursive)
- deletePath(path);
- else {
- if (remove(path.c_str()) == -1)
- throw SysError("cannot unlink '%1%'", path);
- }
- }
- } catch (...) {
- ignoreException();
- }
-}
-
-void AutoDelete::cancel()
-{
- del = false;
-}
-
-void AutoDelete::reset(const Path & p, bool recursive) {
- path = p;
- this->recursive = recursive;
- del = true;
-}
-
-
-
-//////////////////////////////////////////////////////////////////////
-
-
-AutoCloseFD::AutoCloseFD() : fd{-1} {}
-
-
-AutoCloseFD::AutoCloseFD(int fd) : fd{fd} {}
-
-
-AutoCloseFD::AutoCloseFD(AutoCloseFD && that) : fd{that.fd}
-{
- that.fd = -1;
-}
-
-
-AutoCloseFD & AutoCloseFD::operator =(AutoCloseFD && that)
-{
- close();
- fd = that.fd;
- that.fd = -1;
- return *this;
-}
-
-
-AutoCloseFD::~AutoCloseFD()
-{
- try {
- close();
- } catch (...) {
- ignoreException();
- }
-}
-
-
-int AutoCloseFD::get() const
-{
- return fd;
-}
-
-
-void AutoCloseFD::close()
-{
- if (fd != -1) {
- if (::close(fd) == -1)
- /* This should never happen. */
- throw SysError("closing file descriptor %1%", fd);
- fd = -1;
- }
-}
-
-void AutoCloseFD::fsync()
-{
- if (fd != -1) {
- int result;
-#if __APPLE__
- result = ::fcntl(fd, F_FULLFSYNC);
-#else
- result = ::fsync(fd);
-#endif
- if (result == -1)
- throw SysError("fsync file descriptor %1%", fd);
- }
-}
-
-
-AutoCloseFD::operator bool() const
-{
- return fd != -1;
-}
-
-
-int AutoCloseFD::release()
-{
- int oldFD = fd;
- fd = -1;
- return oldFD;
-}
-
-
-void Pipe::create()
-{
- int fds[2];
-#if HAVE_PIPE2
- if (pipe2(fds, O_CLOEXEC) != 0) throw SysError("creating pipe");
-#else
- if (pipe(fds) != 0) throw SysError("creating pipe");
- closeOnExec(fds[0]);
- closeOnExec(fds[1]);
-#endif
- readSide = AutoCloseFD{fds[0]};
- writeSide = AutoCloseFD{fds[1]};
-}
-
-
-void Pipe::close()
-{
- readSide.close();
- writeSide.close();
-}
-
-
-//////////////////////////////////////////////////////////////////////
-
-
-Pid::Pid()
-{
-}
-
-
-Pid::Pid(pid_t pid)
- : pid(pid)
-{
-}
-
-
-Pid::~Pid() noexcept(false)
-{
- if (pid != -1) kill();
-}
-
-
-void Pid::operator =(pid_t pid)
-{
- if (this->pid != -1 && this->pid != pid) kill();
- this->pid = pid;
- killSignal = SIGKILL; // reset signal to default
-}
-
-
-Pid::operator pid_t()
-{
- return pid;
-}
-
-
-int Pid::kill()
-{
- assert(pid != -1);
-
- debug("killing process %1%", pid);
-
- /* Send the requested signal to the child. If it has its own
- process group, send the signal to every process in the child
- process group (which hopefully includes *all* its children). */
- if (::kill(separatePG ? -pid : pid, killSignal) != 0) {
- /* On BSDs, killing a process group will return EPERM if all
- processes in the group are zombies (or something like
- that). So try to detect and ignore that situation. */
-#if __FreeBSD__ || __APPLE__
- if (errno != EPERM || ::kill(pid, 0) != 0)
-#endif
- logError(SysError("killing process %d", pid).info());
- }
-
- return wait();
-}
-
-
-int Pid::wait()
-{
- assert(pid != -1);
- while (1) {
- int status;
- int res = waitpid(pid, &status, 0);
- if (res == pid) {
- pid = -1;
- return status;
- }
- if (errno != EINTR)
- throw SysError("cannot get exit status of PID %d", pid);
- checkInterrupt();
- }
-}
-
-
-void Pid::setSeparatePG(bool separatePG)
-{
- this->separatePG = separatePG;
-}
-
-
-void Pid::setKillSignal(int signal)
-{
- this->killSignal = signal;
-}
-
-
-pid_t Pid::release()
-{
- pid_t p = pid;
- pid = -1;
- return p;
-}
-
-
-void killUser(uid_t uid)
-{
- debug("killing all processes running under uid '%1%'", uid);
-
- assert(uid != 0); /* just to be safe... */
-
- /* The system call kill(-1, sig) sends the signal `sig' to all
- users to which the current process can send signals. So we
- fork a process, switch to uid, and send a mass kill. */
-
- Pid pid = startProcess([&]() {
-
- if (setuid(uid) == -1)
- throw SysError("setting uid");
-
- while (true) {
-#ifdef __APPLE__
- /* OSX's kill syscall takes a third parameter that, among
- other things, determines if kill(-1, signo) affects the
- calling process. In the OSX libc, it's set to true,
- which means "follow POSIX", which we don't want here
- */
- if (syscall(SYS_kill, -1, SIGKILL, false) == 0) break;
-#else
- if (kill(-1, SIGKILL) == 0) break;
-#endif
- if (errno == ESRCH || errno == EPERM) break; /* no more processes */
- if (errno != EINTR)
- throw SysError("cannot kill processes for uid '%1%'", uid);
- }
-
- _exit(0);
- });
-
- int status = pid.wait();
- if (status != 0)
- throw Error("cannot kill processes for uid '%1%': %2%", uid, statusToString(status));
-
- /* !!! We should really do some check to make sure that there are
- no processes left running under `uid', but there is no portable
- way to do so (I think). The most reliable way may be `ps -eo
- uid | grep -q $uid'. */
-}
-
-
-//////////////////////////////////////////////////////////////////////
-
-
-static pid_t doFork(std::function<void()> fun)
-{
- pid_t pid = fork();
- if (pid != 0) return pid;
- fun();
- abort();
-}
-
-
-#if __linux__
-static int childEntry(void * arg)
-{
- auto main = (std::function<void()> *) arg;
- (*main)();
- return 1;
-}
-#endif
-
-
-pid_t startProcess(std::function<void()> fun, const ProcessOptions & options)
-{
- std::function<void()> wrapper = [&]() {
- logger = makeSimpleLogger();
- try {
-#if __linux__
- if (options.dieWithParent && prctl(PR_SET_PDEATHSIG, SIGKILL) == -1)
- throw SysError("setting death signal");
-#endif
- fun();
- } catch (std::exception & e) {
- try {
- std::cerr << options.errorPrefix << e.what() << "\n";
- } catch (...) { }
- } catch (...) { }
- if (options.runExitHandlers)
- exit(1);
- else
- _exit(1);
- };
-
- pid_t pid = -1;
-
- if (options.cloneFlags) {
- #ifdef __linux__
- // Not supported, since then we don't know when to free the stack.
- assert(!(options.cloneFlags & CLONE_VM));
-
- size_t stackSize = 1 * 1024 * 1024;
- auto stack = (char *) mmap(0, stackSize,
- PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
- if (stack == MAP_FAILED) throw SysError("allocating stack");
-
- Finally freeStack([&]() { munmap(stack, stackSize); });
-
- pid = clone(childEntry, stack + stackSize, options.cloneFlags | SIGCHLD, &wrapper);
- #else
- throw Error("clone flags are only supported on Linux");
- #endif
- } else
- pid = doFork(wrapper);
-
- if (pid == -1) throw SysError("unable to fork");
-
- return pid;
-}
-
-
-std::vector<char *> stringsToCharPtrs(const Strings & ss)
-{
- std::vector<char *> res;
- for (auto & s : ss) res.push_back((char *) s.c_str());
- res.push_back(0);
- return res;
-}
-
-std::string runProgram(Path program, bool searchPath, const Strings & args,
- const std::optional<std::string> & input, bool isInteractive)
-{
- auto res = runProgram(RunOptions {.program = program, .searchPath = searchPath, .args = args, .input = input, .isInteractive = isInteractive});
-
- if (!statusOk(res.first))
- throw ExecError(res.first, "program '%1%' %2%", program, statusToString(res.first));
-
- return res.second;
-}
-
-// Output = error code + "standard out" output stream
-std::pair<int, std::string> runProgram(RunOptions && options)
-{
- StringSink sink;
- options.standardOut = &sink;
-
- int status = 0;
-
- try {
- runProgram2(options);
- } catch (ExecError & e) {
- status = e.status;
- }
-
- return {status, std::move(sink.s)};
-}
-
-void runProgram2(const RunOptions & options)
-{
- checkInterrupt();
-
- assert(!(options.standardIn && options.input));
-
- std::unique_ptr<Source> source_;
- Source * source = options.standardIn;
-
- if (options.input) {
- source_ = std::make_unique<StringSource>(*options.input);
- source = source_.get();
- }
-
- /* Create a pipe. */
- Pipe out, in;
- if (options.standardOut) out.create();
- if (source) in.create();
-
- ProcessOptions processOptions;
-
- std::optional<Finally<std::function<void()>>> resumeLoggerDefer;
- if (options.isInteractive) {
- logger->pause();
- resumeLoggerDefer.emplace(
- []() {
- logger->resume();
- }
- );
- }
-
- /* Fork. */
- Pid pid = startProcess([&]() {
- if (options.environment)
- replaceEnv(*options.environment);
- if (options.standardOut && dup2(out.writeSide.get(), STDOUT_FILENO) == -1)
- throw SysError("dupping stdout");
- if (options.mergeStderrToStdout)
- if (dup2(STDOUT_FILENO, STDERR_FILENO) == -1)
- throw SysError("cannot dup stdout into stderr");
- if (source && dup2(in.readSide.get(), STDIN_FILENO) == -1)
- throw SysError("dupping stdin");
-
- if (options.chdir && chdir((*options.chdir).c_str()) == -1)
- throw SysError("chdir failed");
- if (options.gid && setgid(*options.gid) == -1)
- throw SysError("setgid failed");
- /* Drop all other groups if we're setgid. */
- if (options.gid && setgroups(0, 0) == -1)
- throw SysError("setgroups failed");
- if (options.uid && setuid(*options.uid) == -1)
- throw SysError("setuid failed");
-
- Strings args_(options.args);
- args_.push_front(options.program);
-
- restoreProcessContext();
-
- if (options.searchPath)
- execvp(options.program.c_str(), stringsToCharPtrs(args_).data());
- // This allows you to refer to a program with a pathname relative
- // to the PATH variable.
- else
- execv(options.program.c_str(), stringsToCharPtrs(args_).data());
-
- throw SysError("executing '%1%'", options.program);
- }, processOptions);
-
- out.writeSide.close();
-
- std::thread writerThread;
-
- std::promise<void> promise;
-
- Finally doJoin([&]() {
- if (writerThread.joinable())
- writerThread.join();
- });
-
-
- if (source) {
- in.readSide.close();
- writerThread = std::thread([&]() {
- try {
- std::vector<char> buf(8 * 1024);
- while (true) {
- size_t n;
- try {
- n = source->read(buf.data(), buf.size());
- } catch (EndOfFile &) {
- break;
- }
- writeFull(in.writeSide.get(), {buf.data(), n});
- }
- promise.set_value();
- } catch (...) {
- promise.set_exception(std::current_exception());
- }
- in.writeSide.close();
- });
- }
-
- if (options.standardOut)
- drainFD(out.readSide.get(), *options.standardOut);
-
- /* Wait for the child to finish. */
- int status = pid.wait();
-
- /* Wait for the writer thread to finish. */
- if (source) promise.get_future().get();
-
- if (status)
- throw ExecError(status, "program '%1%' %2%", options.program, statusToString(status));
-}
-
-
-void closeMostFDs(const std::set<int> & exceptions)
-{
-#if __linux__
- try {
- for (auto & s : readDirectory("/proc/self/fd")) {
- auto fd = std::stoi(s.name);
- if (!exceptions.count(fd)) {
- debug("closing leaked FD %d", fd);
- close(fd);
- }
- }
- return;
- } catch (SysError &) {
- }
-#endif
-
- int maxFD = 0;
- maxFD = sysconf(_SC_OPEN_MAX);
- for (int fd = 0; fd < maxFD; ++fd)
- if (!exceptions.count(fd))
- close(fd); /* ignore result */
-}
-
-
-void closeOnExec(int fd)
-{
- int prev;
- if ((prev = fcntl(fd, F_GETFD, 0)) == -1 ||
- fcntl(fd, F_SETFD, prev | FD_CLOEXEC) == -1)
- throw SysError("setting close-on-exec flag");
-}
-
-
-//////////////////////////////////////////////////////////////////////
-
-
-template<class C> C tokenizeString(std::string_view s, std::string_view separators)
-{
- C result;
- auto pos = s.find_first_not_of(separators, 0);
- while (pos != std::string_view::npos) {
- auto end = s.find_first_of(separators, pos + 1);
- if (end == std::string_view::npos) end = s.size();
- result.insert(result.end(), std::string(s, pos, end - pos));
- pos = s.find_first_not_of(separators, end);
- }
- return result;
-}
-
-template Strings tokenizeString(std::string_view s, std::string_view separators);
-template StringSet tokenizeString(std::string_view s, std::string_view separators);
-template std::vector<std::string> tokenizeString(std::string_view s, std::string_view separators);
-
-
-std::string chomp(std::string_view s)
-{
- size_t i = s.find_last_not_of(" \n\r\t");
- return i == std::string_view::npos ? "" : std::string(s, 0, i + 1);
-}
-
-
-std::string trim(std::string_view s, std::string_view whitespace)
-{
- auto i = s.find_first_not_of(whitespace);
- if (i == s.npos) return "";
- auto j = s.find_last_not_of(whitespace);
- return std::string(s, i, j == s.npos ? j : j - i + 1);
-}
-
-
-std::string replaceStrings(
- std::string res,
- std::string_view from,
- std::string_view to)
-{
- if (from.empty()) return res;
- size_t pos = 0;
- while ((pos = res.find(from, pos)) != std::string::npos) {
- res.replace(pos, from.size(), to);
- pos += to.size();
- }
- return res;
-}
-
-
-Rewriter::Rewriter(std::map<std::string, std::string> rewrites)
- : rewrites(std::move(rewrites))
-{
- for (const auto & [k, v] : this->rewrites) {
- assert(!k.empty());
- initials.push_back(k[0]);
- }
- std::ranges::sort(initials);
- auto [firstDupe, end] = std::ranges::unique(initials);
- initials.erase(firstDupe, end);
-}
-
-std::string Rewriter::operator()(std::string s)
-{
- size_t j = 0;
- while ((j = s.find_first_of(initials, j)) != std::string::npos) {
- size_t skip = 1;
- for (auto & [from, to] : rewrites) {
- if (s.compare(j, from.size(), from) == 0) {
- s.replace(j, from.size(), to);
- skip = to.size();
- break;
- }
- }
- j += skip;
- }
- return s;
-}
-
-
-std::string statusToString(int status)
-{
- if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
- if (WIFEXITED(status))
- return fmt("failed with exit code %1%", WEXITSTATUS(status));
- else if (WIFSIGNALED(status)) {
- int sig = WTERMSIG(status);
-#if HAVE_STRSIGNAL
- const char * description = strsignal(sig);
- return fmt("failed due to signal %1% (%2%)", sig, description);
-#else
- return fmt("failed due to signal %1%", sig);
-#endif
- }
- else
- return "died abnormally";
- } else return "succeeded";
-}
-
-
-bool statusOk(int status)
-{
- return WIFEXITED(status) && WEXITSTATUS(status) == 0;
-}
-
-
-std::string toLower(const std::string & s)
-{
- std::string r(s);
- for (auto & c : r)
- c = std::tolower(c);
- return r;
-}
-
-
-std::string shellEscape(const std::string_view s)
-{
- std::string r;
- r.reserve(s.size() + 2);
- r += "'";
- for (auto & i : s)
- if (i == '\'') r += "'\\''"; else r += i;
- r += '\'';
- return r;
-}
-
-
-void ignoreException(Verbosity lvl)
-{
- /* Make sure no exceptions leave this function.
- printError() also throws when remote is closed. */
- try {
- try {
- throw;
- } catch (std::exception & e) {
- printMsg(lvl, "error (ignored): %1%", e.what());
- }
- } catch (...) { }
-}
-
-bool shouldANSI()
-{
- return isatty(STDERR_FILENO)
- && getEnv("TERM").value_or("dumb") != "dumb"
- && !(getEnv("NO_COLOR").has_value() || getEnv("NOCOLOR").has_value());
-}
-
-std::string filterANSIEscapes(std::string_view s, bool filterAll, unsigned int width)
-{
- std::string t, e;
- size_t w = 0;
- auto i = s.begin();
-
- while (w < (size_t) width && i != s.end()) {
-
- if (*i == '\e') {
- std::string e;
- e += *i++;
- char last = 0;
-
- if (i != s.end() && *i == '[') {
- e += *i++;
- // eat parameter bytes
- while (i != s.end() && *i >= 0x30 && *i <= 0x3f) e += *i++;
- // eat intermediate bytes
- while (i != s.end() && *i >= 0x20 && *i <= 0x2f) e += *i++;
- // eat final byte
- if (i != s.end() && *i >= 0x40 && *i <= 0x7e) e += last = *i++;
- } else {
- if (i != s.end() && *i >= 0x40 && *i <= 0x5f) e += *i++;
- }
-
- if (!filterAll && last == 'm')
- t += e;
- }
-
- else if (*i == '\t') {
- i++; t += ' '; w++;
- while (w < (size_t) width && w % 8) {
- t += ' '; w++;
- }
- }
-
- else if (*i == '\r' || *i == '\a')
- // do nothing for now
- i++;
-
- else {
- w++;
- // Copy one UTF-8 character.
- if ((*i & 0xe0) == 0xc0) {
- t += *i++;
- if (i != s.end() && ((*i & 0xc0) == 0x80)) t += *i++;
- } else if ((*i & 0xf0) == 0xe0) {
- t += *i++;
- if (i != s.end() && ((*i & 0xc0) == 0x80)) {
- t += *i++;
- if (i != s.end() && ((*i & 0xc0) == 0x80)) t += *i++;
- }
- } else if ((*i & 0xf8) == 0xf0) {
- t += *i++;
- if (i != s.end() && ((*i & 0xc0) == 0x80)) {
- t += *i++;
- if (i != s.end() && ((*i & 0xc0) == 0x80)) {
- t += *i++;
- if (i != s.end() && ((*i & 0xc0) == 0x80)) t += *i++;
- }
- }
- } else
- t += *i++;
- }
- }
-
- return t;
-}
-
-
-constexpr char base64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
-
-std::string base64Encode(std::string_view s)
-{
- std::string res;
- res.reserve((s.size() + 2) / 3 * 4);
- int data = 0, nbits = 0;
-
- for (char c : s) {
- data = data << 8 | (unsigned char) c;
- nbits += 8;
- while (nbits >= 6) {
- nbits -= 6;
- res.push_back(base64Chars[data >> nbits & 0x3f]);
- }
- }
-
- if (nbits) res.push_back(base64Chars[data << (6 - nbits) & 0x3f]);
- while (res.size() % 4) res.push_back('=');
-
- return res;
-}
-
-
-std::string base64Decode(std::string_view s)
-{
- constexpr char npos = -1;
- constexpr std::array<char, 256> base64DecodeChars = [&]() {
- std::array<char, 256> result{};
- for (auto& c : result)
- c = npos;
- for (int i = 0; i < 64; i++)
- result[base64Chars[i]] = i;
- return result;
- }();
-
- std::string res;
- // Some sequences are missing the padding consisting of up to two '='.
- // vvv
- res.reserve((s.size() + 2) / 4 * 3);
- unsigned int d = 0, bits = 0;
-
- for (char c : s) {
- if (c == '=') break;
- if (c == '\n') continue;
-
- char digit = base64DecodeChars[(unsigned char) c];
- if (digit == npos)
- throw Error("invalid character in Base64 string: '%c'", c);
-
- bits += 6;
- d = d << 6 | digit;
- if (bits >= 8) {
- res.push_back(d >> (bits - 8) & 0xff);
- bits -= 8;
- }
- }
-
- return res;
-}
-
-
-std::string stripIndentation(std::string_view s)
-{
- size_t minIndent = 10000;
- size_t curIndent = 0;
- bool atStartOfLine = true;
-
- for (auto & c : s) {
- if (atStartOfLine && c == ' ')
- curIndent++;
- else if (c == '\n') {
- if (atStartOfLine)
- minIndent = std::max(minIndent, curIndent);
- curIndent = 0;
- atStartOfLine = true;
- } else {
- if (atStartOfLine) {
- minIndent = std::min(minIndent, curIndent);
- atStartOfLine = false;
- }
- }
- }
-
- std::string res;
-
- size_t pos = 0;
- while (pos < s.size()) {
- auto eol = s.find('\n', pos);
- if (eol == s.npos) eol = s.size();
- if (eol - pos > minIndent)
- res.append(s.substr(pos + minIndent, eol - pos - minIndent));
- res.push_back('\n');
- pos = eol + 1;
- }
-
- return res;
-}
-
-
-std::pair<std::string_view, std::string_view> getLine(std::string_view s)
-{
- auto newline = s.find('\n');
-
- if (newline == s.npos) {
- return {s, ""};
- } else {
- auto line = s.substr(0, newline);
- if (!line.empty() && line[line.size() - 1] == '\r')
- line = line.substr(0, line.size() - 1);
- return {line, s.substr(newline + 1)};
- }
-}
-
-
-//////////////////////////////////////////////////////////////////////
-
-static Sync<std::pair<unsigned short, unsigned short>> windowSize{{0, 0}};
-
-
-void updateWindowSize()
-{
- struct winsize ws;
- if (ioctl(2, TIOCGWINSZ, &ws) == 0) {
- auto windowSize_(windowSize.lock());
- windowSize_->first = ws.ws_row;
- windowSize_->second = ws.ws_col;
- }
-}
-
-
-std::pair<unsigned short, unsigned short> getWindowSize()
-{
- return *windowSize.lock();
-}
-
-
-rlim_t savedStackSize = 0;
-
-void setStackSize(rlim_t stackSize)
-{
- struct rlimit limit;
- if (getrlimit(RLIMIT_STACK, &limit) == 0 && limit.rlim_cur < stackSize) {
- savedStackSize = limit.rlim_cur;
- limit.rlim_cur = std::min(stackSize, limit.rlim_max);
- if (setrlimit(RLIMIT_STACK, &limit) != 0) {
- logger->log(
- lvlError,
- HintFmt(
- "Failed to increase stack size from %1% to %2% (maximum allowed stack size: %3%): %4%",
- savedStackSize,
- stackSize,
- limit.rlim_max,
- std::strerror(errno)
- ).str()
- );
- }
- }
-}
-
-#if __linux__
-static AutoCloseFD fdSavedMountNamespace;
-static AutoCloseFD fdSavedRoot;
-#endif
-
-void saveMountNamespace()
-{
-#if __linux__
- static std::once_flag done;
- std::call_once(done, []() {
- fdSavedMountNamespace = AutoCloseFD{open("/proc/self/ns/mnt", O_RDONLY)};
- if (!fdSavedMountNamespace)
- throw SysError("saving parent mount namespace");
-
- fdSavedRoot = AutoCloseFD{open("/proc/self/root", O_RDONLY)};
- });
-#endif
-}
-
-void restoreMountNamespace()
-{
-#if __linux__
- try {
- auto savedCwd = absPath(".");
-
- if (fdSavedMountNamespace && setns(fdSavedMountNamespace.get(), CLONE_NEWNS) == -1)
- throw SysError("restoring parent mount namespace");
-
- if (fdSavedRoot) {
- if (fchdir(fdSavedRoot.get()))
- throw SysError("chdir into saved root");
- if (chroot("."))
- throw SysError("chroot into saved root");
- }
-
- if (chdir(savedCwd.c_str()) == -1)
- throw SysError("restoring cwd");
- } catch (Error & e) {
- debug(e.msg());
- }
-#endif
-}
-
-void unshareFilesystem()
-{
-#ifdef __linux__
- if (unshare(CLONE_FS) != 0 && errno != EPERM)
- throw SysError("unsharing filesystem state in download thread");
-#endif
-}
-
-void restoreProcessContext(bool restoreMounts)
-{
- restoreSignals();
- if (restoreMounts) {
- restoreMountNamespace();
- }
-
- if (savedStackSize) {
- struct rlimit limit;
- if (getrlimit(RLIMIT_STACK, &limit) == 0) {
- limit.rlim_cur = savedStackSize;
- setrlimit(RLIMIT_STACK, &limit);
- }
- }
-}
-
-AutoCloseFD createUnixDomainSocket()
-{
- AutoCloseFD fdSocket{socket(PF_UNIX, SOCK_STREAM
- #ifdef SOCK_CLOEXEC
- | SOCK_CLOEXEC
- #endif
- , 0)};
- if (!fdSocket)
- throw SysError("cannot create Unix domain socket");
- closeOnExec(fdSocket.get());
- return fdSocket;
-}
-
-
-AutoCloseFD createUnixDomainSocket(const Path & path, mode_t mode)
-{
- auto fdSocket = nix::createUnixDomainSocket();
-
- bind(fdSocket.get(), path);
-
- chmodPath(path.c_str(), mode);
-
- if (listen(fdSocket.get(), 100) == -1)
- throw SysError("cannot listen on socket '%1%'", path);
-
- return fdSocket;
-}
-
-
-static void bindConnectProcHelper(
- std::string_view operationName, auto && operation,
- int fd, const std::string & path)
-{
- struct sockaddr_un addr;
- addr.sun_family = AF_UNIX;
-
- // Casting between types like these legacy C library interfaces
- // require is forbidden in C++. To maintain backwards
- // compatibility, the implementation of the bind/connect functions
- // contains some hints to the compiler that allow for this
- // special case.
- auto * psaddr = reinterpret_cast<struct sockaddr *>(&addr);
-
- if (path.size() + 1 >= sizeof(addr.sun_path)) {
- Pipe pipe;
- pipe.create();
- Pid pid = startProcess([&] {
- try {
- pipe.readSide.close();
- Path dir = dirOf(path);
- if (chdir(dir.c_str()) == -1)
- throw SysError("chdir to '%s' failed", dir);
- std::string base(baseNameOf(path));
- if (base.size() + 1 >= sizeof(addr.sun_path))
- throw Error("socket path '%s' is too long", base);
- memcpy(addr.sun_path, base.c_str(), base.size() + 1);
- if (operation(fd, psaddr, sizeof(addr)) == -1)
- throw SysError("cannot %s to socket at '%s'", operationName, path);
- writeFull(pipe.writeSide.get(), "0\n");
- } catch (SysError & e) {
- writeFull(pipe.writeSide.get(), fmt("%d\n", e.errNo));
- } catch (...) {
- writeFull(pipe.writeSide.get(), "-1\n");
- }
- });
- pipe.writeSide.close();
- auto errNo = string2Int<int>(chomp(drainFD(pipe.readSide.get())));
- if (!errNo || *errNo == -1)
- throw Error("cannot %s to socket at '%s'", operationName, path);
- else if (*errNo > 0) {
- errno = *errNo;
- throw SysError("cannot %s to socket at '%s'", operationName, path);
- }
- } else {
- memcpy(addr.sun_path, path.c_str(), path.size() + 1);
- if (operation(fd, psaddr, sizeof(addr)) == -1)
- throw SysError("cannot %s to socket at '%s'", operationName, path);
- }
-}
-
-
-void bind(int fd, const std::string & path)
-{
- unlink(path.c_str());
-
- bindConnectProcHelper("bind", ::bind, fd, path);
-}
-
-
-void connect(int fd, const std::string & path)
-{
- bindConnectProcHelper("connect", ::connect, fd, path);
-}
-
-
-std::string showBytes(uint64_t bytes)
-{
- return fmt("%.2f MiB", bytes / (1024.0 * 1024.0));
-}
-
-
-// FIXME: move to libstore/build
-void commonChildInit()
-{
- logger = makeSimpleLogger();
-
- const static std::string pathNullDevice = "/dev/null";
- restoreProcessContext(false);
-
- /* Put the child in a separate session (and thus a separate
- process group) so that it has no controlling terminal (meaning
- that e.g. ssh cannot open /dev/tty) and it doesn't receive
- terminal signals. */
- if (setsid() == -1)
- throw SysError("creating a new session");
-
- /* Dup stderr to stdout. */
- if (dup2(STDERR_FILENO, STDOUT_FILENO) == -1)
- throw SysError("cannot dup stderr into stdout");
-
- /* Reroute stdin to /dev/null. */
- int fdDevNull = open(pathNullDevice.c_str(), O_RDWR);
- if (fdDevNull == -1)
- throw SysError("cannot open '%1%'", pathNullDevice);
- if (dup2(fdDevNull, STDIN_FILENO) == -1)
- throw SysError("cannot dup null device into stdin");
- close(fdDevNull);
-}
-
-}
diff --git a/src/libutil/util.hh b/src/libutil/util.hh
deleted file mode 100644
index 14868776c..000000000
--- a/src/libutil/util.hh
+++ /dev/null
@@ -1,971 +0,0 @@
-#pragma once
-///@file
-
-#include "types.hh"
-#include "error.hh"
-#include "logging.hh"
-#include "ansicolor.hh"
-
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <sys/resource.h>
-#include <dirent.h>
-#include <unistd.h>
-#include <signal.h>
-
-#include <boost/lexical_cast.hpp>
-
-#include <atomic>
-#include <functional>
-#include <map>
-#include <sstream>
-#include <optional>
-
-#ifndef HAVE_STRUCT_DIRENT_D_TYPE
-#define DT_UNKNOWN 0
-#define DT_REG 1
-#define DT_LNK 2
-#define DT_DIR 3
-#endif
-
-namespace nix {
-
-struct Sink;
-struct Source;
-
-/**
- * The system for which Nix is compiled.
- */
-extern const std::string nativeSystem;
-
-
-/**
- * @return an environment variable.
- */
-std::optional<std::string> getEnv(const std::string & key);
-
-/**
- * @return a non empty environment variable. Returns nullopt if the env
- * variable is set to ""
- */
-std::optional<std::string> getEnvNonEmpty(const std::string & key);
-
-/**
- * Get the entire environment.
- */
-std::map<std::string, std::string> getEnv();
-
-/**
- * Clear the environment.
- */
-void clearEnv();
-
-/**
- * @return An absolutized path, resolving paths relative to the
- * specified directory, or the current directory otherwise. The path
- * is also canonicalised.
- */
-Path absPath(Path path,
- std::optional<PathView> dir = {},
- bool resolveSymlinks = false);
-
-/**
- * Canonicalise a path by removing all `.` or `..` components and
- * double or trailing slashes. Optionally resolves all symlink
- * components such that each component of the resulting path is *not*
- * a symbolic link.
- */
-Path canonPath(PathView path, bool resolveSymlinks = false);
-
-/**
- * Change the permissions of a path
- * Not called `chmod` as it shadows and could be confused with
- * `int chmod(char *, mode_t)`, which does not handle errors
- */
-void chmodPath(const Path & path, mode_t mode);
-
-/**
- * @return The directory part of the given canonical path, i.e.,
- * everything before the final `/`. If the path is the root or an
- * immediate child thereof (e.g., `/foo`), this means `/`
- * is returned.
- */
-Path dirOf(const PathView path);
-
-/**
- * @return the base name of the given canonical path, i.e., everything
- * following the final `/` (trailing slashes are removed).
- */
-std::string_view baseNameOf(std::string_view path);
-
-/**
- * Perform tilde expansion on a path.
- */
-std::string expandTilde(std::string_view path);
-
-/**
- * Check whether 'path' is a descendant of 'dir'. Both paths must be
- * canonicalized.
- */
-bool isInDir(std::string_view path, std::string_view dir);
-
-/**
- * Check whether 'path' is equal to 'dir' or a descendant of
- * 'dir'. Both paths must be canonicalized.
- */
-bool isDirOrInDir(std::string_view path, std::string_view dir);
-
-/**
- * Get status of `path`.
- */
-struct stat stat(const Path & path);
-struct stat lstat(const Path & path);
-
-/**
- * `lstat` the given path if it exists.
- * @return std::nullopt if the path doesn't exist, or an optional containing the result of `lstat` otherwise
- */
-std::optional<struct stat> maybeLstat(const Path & path);
-
-/**
- * @return true iff the given path exists.
- */
-bool pathExists(const Path & path);
-
-/**
- * A version of pathExists that returns false on a permission error.
- * Useful for inferring default paths across directories that might not
- * be readable.
- * @return true iff the given path can be accessed and exists
- */
-bool pathAccessible(const Path & path);
-
-/**
- * Read the contents (target) of a symbolic link. The result is not
- * in any way canonicalised.
- */
-Path readLink(const Path & path);
-
-bool isLink(const Path & path);
-
-/**
- * Read the contents of a directory. The entries `.` and `..` are
- * removed.
- */
-struct DirEntry
-{
- std::string name;
- ino_t ino;
- /**
- * one of DT_*
- */
- unsigned char type;
- DirEntry(std::string name, ino_t ino, unsigned char type)
- : name(std::move(name)), ino(ino), type(type) { }
-};
-
-typedef std::vector<DirEntry> DirEntries;
-
-DirEntries readDirectory(const Path & path);
-
-unsigned char getFileType(const Path & path);
-
-/**
- * Read the contents of a file into a string.
- */
-std::string readFile(int fd);
-std::string readFile(const Path & path);
-void readFile(const Path & path, Sink & sink);
-
-/**
- * Write a string to a file.
- */
-void writeFile(const Path & path, std::string_view s, mode_t mode = 0666, bool sync = false);
-
-void writeFile(const Path & path, Source & source, mode_t mode = 0666, bool sync = false);
-
-/**
- * Flush a file's parent directory to disk
- */
-void syncParent(const Path & path);
-
-/**
- * Read a line from a file descriptor.
- */
-std::string readLine(int fd);
-
-/**
- * Write a line to a file descriptor.
- */
-void writeLine(int fd, std::string s);
-
-/**
- * Delete a path; i.e., in the case of a directory, it is deleted
- * recursively. It's not an error if the path does not exist. The
- * second variant returns the number of bytes and blocks freed.
- */
-void deletePath(const Path & path);
-
-void deletePath(const Path & path, uint64_t & bytesFreed);
-
-std::string getUserName();
-
-/**
- * @return the given user's home directory from /etc/passwd.
- */
-Path getHomeOf(uid_t userId);
-
-/**
- * @return $HOME or the user's home directory from /etc/passwd.
- */
-Path getHome();
-
-/**
- * @return $XDG_CACHE_HOME or $HOME/.cache.
- */
-Path getCacheDir();
-
-/**
- * @return $XDG_CONFIG_HOME or $HOME/.config.
- */
-Path getConfigDir();
-
-/**
- * @return the directories to search for user configuration files
- */
-std::vector<Path> getConfigDirs();
-
-/**
- * @return $XDG_DATA_HOME or $HOME/.local/share.
- */
-Path getDataDir();
-
-/**
- * @return the path of the current executable.
- */
-std::optional<Path> getSelfExe();
-
-/**
- * @return $XDG_STATE_HOME or $HOME/.local/state.
- *
- * @note Not to be confused with settings.nixStateDir.
- */
-Path getStateDir();
-
-/**
- * Create $XDG_STATE_HOME/nix or $HOME/.local/state/nix, and return
- * the path to it.
- * @note Not to be confused with settings.nixStateDir.
- */
-Path createNixStateDir();
-
-/**
- * Create a directory and all its parents, if necessary. Returns the
- * list of created directories, in order of creation.
- */
-Paths createDirs(const Path & path);
-inline Paths createDirs(PathView path)
-{
- return createDirs(Path(path));
-}
-
-/**
- * Create a symlink.
- */
-void createSymlink(const Path & target, const Path & link);
-
-/**
- * Atomically create or replace a symlink.
- */
-void replaceSymlink(const Path & target, const Path & link);
-
-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);
-
-struct CopyFileFlags
-{
- /**
- * Delete the file after copying.
- */
- bool deleteAfter = false;
-
- /**
- * Follow symlinks and copy the eventual target.
- */
- bool followSymlinks = false;
-};
-
-/**
- * Recursively copy the content of `oldPath` to `newPath`. If `andDelete` is
- * `true`, then also remove `oldPath` (making this equivalent to `moveFile`, but
- * with the guaranty that the destination will be “fresh”, with no stale inode
- * or file descriptor pointing to it).
- */
-void copyFile(const Path & oldPath, const Path & newPath, CopyFileFlags flags);
-
-/**
- * Wrappers arount read()/write() that read/write exactly the
- * requested number of bytes.
- */
-void readFull(int fd, char * buf, size_t count);
-void writeFull(int fd, std::string_view s, bool allowInterrupts = true);
-
-MakeError(EndOfFile, Error);
-
-
-/**
- * Read a file descriptor until EOF occurs.
- */
-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.
- */
-
-
-class AutoDelete
-{
- Path path;
- bool del;
- bool recursive;
-public:
- AutoDelete();
- AutoDelete(const Path & p, bool recursive = true);
- ~AutoDelete();
- void cancel();
- void reset(const Path & p, bool recursive = true);
- operator Path() const { return path; }
- operator PathView() const { return path; }
-};
-
-
-class AutoCloseFD
-{
- int fd;
-public:
- AutoCloseFD();
- explicit AutoCloseFD(int fd);
- AutoCloseFD(const AutoCloseFD & fd) = delete;
- AutoCloseFD(AutoCloseFD&& fd);
- ~AutoCloseFD();
- AutoCloseFD& operator =(const AutoCloseFD & fd) = delete;
- AutoCloseFD& operator =(AutoCloseFD&& fd) noexcept(false);
- int get() const;
- explicit operator bool() const;
- int release();
- void close();
- void fsync();
- void reset() { *this = {}; }
-};
-
-
-/**
- * Create a temporary directory.
- */
-Path createTempDir(const Path & tmpRoot = "", const Path & prefix = "nix",
- bool includePid = true, bool useGlobalCounter = true, mode_t mode = 0755);
-
-/**
- * Create a temporary file, returning a file handle and its path.
- */
-std::pair<AutoCloseFD, Path> createTempFile(const Path & prefix = "nix");
-
-
-class Pipe
-{
-public:
- AutoCloseFD readSide, writeSide;
- void create();
- void close();
-};
-
-
-struct DIRDeleter
-{
- void operator()(DIR * dir) const {
- closedir(dir);
- }
-};
-
-typedef std::unique_ptr<DIR, DIRDeleter> AutoCloseDir;
-
-
-class Pid
-{
- pid_t pid = -1;
- bool separatePG = false;
- int killSignal = SIGKILL;
-public:
- Pid();
- Pid(pid_t pid);
- ~Pid() noexcept(false);
- void operator =(pid_t pid);
- operator pid_t();
- int kill();
- int wait();
-
- void setSeparatePG(bool separatePG);
- void setKillSignal(int signal);
- pid_t release();
-};
-
-
-/**
- * Kill all processes running under the specified uid by sending them
- * a SIGKILL.
- */
-void killUser(uid_t uid);
-
-
-/**
- * Fork a process that runs the given function, and return the child
- * pid to the caller.
- */
-struct ProcessOptions
-{
- std::string errorPrefix = "";
- bool dieWithParent = true;
- bool runExitHandlers = false;
- /**
- * use clone() with the specified flags (Linux only)
- */
- int cloneFlags = 0;
-};
-
-pid_t startProcess(std::function<void()> fun, const ProcessOptions & options = ProcessOptions());
-
-
-/**
- * Run a program and return its stdout in a string (i.e., like the
- * shell backtick operator).
- */
-std::string runProgram(Path program, bool searchPath = false,
- const Strings & args = Strings(),
- const std::optional<std::string> & input = {}, bool isInteractive = false);
-
-struct RunOptions
-{
- Path program;
- bool searchPath = true;
- Strings args;
- std::optional<uid_t> uid;
- std::optional<uid_t> gid;
- std::optional<Path> chdir;
- std::optional<std::map<std::string, std::string>> environment;
- std::optional<std::string> input;
- Source * standardIn = nullptr;
- Sink * standardOut = nullptr;
- bool mergeStderrToStdout = false;
- bool isInteractive = false;
-};
-
-std::pair<int, std::string> runProgram(RunOptions && options);
-
-void runProgram2(const RunOptions & options);
-
-
-/**
- * Change the stack size.
- */
-void setStackSize(rlim_t stackSize);
-
-
-/**
- * Restore the original inherited Unix process context (such as signal
- * masks, stack size).
-
- * See startSignalHandlerThread(), saveSignalMask().
- */
-void restoreProcessContext(bool restoreMounts = true);
-
-/**
- * Save the current mount namespace. Ignored if called more than
- * once.
- */
-void saveMountNamespace();
-
-/**
- * Restore the mount namespace saved by saveMountNamespace(). Ignored
- * if saveMountNamespace() was never called.
- */
-void restoreMountNamespace();
-
-/**
- * Cause this thread to not share any FS attributes with the main
- * thread, because this causes setns() in restoreMountNamespace() to
- * fail.
- */
-void unshareFilesystem();
-
-
-class ExecError : public Error
-{
-public:
- int status;
-
- template<typename... Args>
- ExecError(int status, const Args & ... args)
- : Error(args...), status(status)
- { }
-};
-
-/**
- * Convert a list of strings to a null-terminated vector of `char
- * *`s. The result must not be accessed beyond the lifetime of the
- * list of strings.
- */
-std::vector<char *> stringsToCharPtrs(const Strings & ss);
-
-/**
- * Close all file descriptors except those listed in the given set.
- * Good practice in child processes.
- */
-void closeMostFDs(const std::set<int> & exceptions);
-
-/**
- * Set the close-on-exec flag for the given file descriptor.
- */
-void closeOnExec(int fd);
-
-
-MakeError(FormatError, Error);
-
-
-/**
- * String tokenizer.
- */
-template<class C> C tokenizeString(std::string_view s, std::string_view separators = " \t\n\r");
-
-
-/**
- * Concatenate the given strings with a separator between the
- * elements.
- */
-template<class C>
-std::string concatStringsSep(const std::string_view sep, const C & ss)
-{
- size_t size = 0;
- // need a cast to string_view since this is also called with Symbols
- for (const auto & s : ss) size += sep.size() + std::string_view(s).size();
- std::string s;
- s.reserve(size);
- for (auto & i : ss) {
- if (s.size() != 0) s += sep;
- s += i;
- }
- return s;
-}
-
-template<class ... Parts>
-auto concatStrings(Parts && ... parts)
- -> std::enable_if_t<(... && std::is_convertible_v<Parts, std::string_view>), std::string>
-{
- std::string_view views[sizeof...(parts)] = { parts... };
- return concatStringsSep({}, views);
-}
-
-
-/**
- * Add quotes around a collection of strings.
- */
-template<class C> Strings quoteStrings(const C & c)
-{
- Strings res;
- for (auto & s : c)
- res.push_back("'" + s + "'");
- return res;
-}
-
-/**
- * Remove trailing whitespace from a string.
- *
- * \todo return std::string_view.
- */
-std::string chomp(std::string_view s);
-
-
-/**
- * Remove whitespace from the start and end of a string.
- */
-std::string trim(std::string_view s, std::string_view whitespace = " \n\r\t");
-
-
-/**
- * Replace all occurrences of a string inside another string.
- */
-std::string replaceStrings(
- std::string s,
- std::string_view from,
- std::string_view to);
-
-
-/**
- * Rewrites a string given a map of replacements, applying the replacements in
- * sorted order, only once, considering only the strings appearing in the input
- * string in performing replacement.
- *
- * - Replacements are not performed on intermediate strings. That is, for an input
- * `"abb"` with replacements `{"ab" -> "ba"}`, the result is `"bab"`.
- * - Transitive replacements are not performed. For example, for the input `"abcde"`
- * with replacements `{"a" -> "b", "b" -> "c", "e" -> "b"}`, the result is
- * `"bccdb"`.
- */
-class Rewriter
-{
-private:
- std::string initials;
- std::map<std::string, std::string> rewrites;
-
-public:
- explicit Rewriter(std::map<std::string, std::string> rewrites);
-
- std::string operator()(std::string s);
-};
-
-inline std::string rewriteStrings(std::string s, const StringMap & rewrites)
-{
- return Rewriter(rewrites)(s);
-}
-
-
-/**
- * Convert the exit status of a child as returned by wait() into an
- * error string.
- */
-std::string statusToString(int status);
-
-bool statusOk(int status);
-
-
-/**
- * Parse a string into an integer.
- */
-template<class N>
-std::optional<N> string2Int(const std::string_view s)
-{
- if (s.substr(0, 1) == "-" && !std::numeric_limits<N>::is_signed)
- return std::nullopt;
- try {
- return boost::lexical_cast<N>(s.data(), s.size());
- } catch (const boost::bad_lexical_cast &) {
- return std::nullopt;
- }
-}
-
-/**
- * Like string2Int(), but support an optional suffix 'K', 'M', 'G' or
- * 'T' denoting a binary unit prefix.
- */
-template<class N>
-N string2IntWithUnitPrefix(std::string_view s)
-{
- N multiplier = 1;
- if (!s.empty()) {
- char u = std::toupper(*s.rbegin());
- if (std::isalpha(u)) {
- if (u == 'K') multiplier = 1ULL << 10;
- else if (u == 'M') multiplier = 1ULL << 20;
- else if (u == 'G') multiplier = 1ULL << 30;
- else if (u == 'T') multiplier = 1ULL << 40;
- else throw UsageError("invalid unit specifier '%1%'", u);
- s.remove_suffix(1);
- }
- }
- if (auto n = string2Int<N>(s))
- return *n * multiplier;
- throw UsageError("'%s' is not an integer", s);
-}
-
-/**
- * Parse a string into a float.
- */
-template<class N>
-std::optional<N> string2Float(const std::string_view s)
-{
- try {
- return boost::lexical_cast<N>(s.data(), s.size());
- } catch (const boost::bad_lexical_cast &) {
- return std::nullopt;
- }
-}
-
-
-/**
- * Convert a little-endian integer to host order.
- */
-template<typename T>
-T readLittleEndian(unsigned char * p)
-{
- T x = 0;
- for (size_t i = 0; i < sizeof(x); ++i, ++p) {
- x |= ((T) *p) << (i * 8);
- }
- return x;
-}
-
-/**
- * Convert a string to lower case.
- */
-std::string toLower(const std::string & s);
-
-
-/**
- * Escape a string as a shell word.
- */
-std::string shellEscape(const std::string_view s);
-
-
-/**
- * Exception handling in destructors: print an error message, then
- * ignore the exception.
- */
-void ignoreException(Verbosity lvl = lvlError);
-
-
-
-/**
- * Tree formatting.
- */
-constexpr char treeConn[] = "├───";
-constexpr char treeLast[] = "└───";
-constexpr char treeLine[] = "│ ";
-constexpr char treeNull[] = " ";
-
-/**
- * Determine whether ANSI escape sequences are appropriate for the
- * present output.
- */
-bool shouldANSI();
-
-/**
- * Truncate a string to 'width' printable characters. If 'filterAll'
- * is true, all ANSI escape sequences are filtered out. Otherwise,
- * some escape sequences (such as colour setting) are copied but not
- * included in the character count. Also, tabs are expanded to
- * spaces.
- */
-std::string filterANSIEscapes(std::string_view s,
- bool filterAll = false,
- unsigned int width = std::numeric_limits<unsigned int>::max());
-
-
-/**
- * Base64 encoding/decoding.
- */
-std::string base64Encode(std::string_view s);
-std::string base64Decode(std::string_view s);
-
-
-/**
- * Remove common leading whitespace from the lines in the string
- * 's'. For example, if every line is indented by at least 3 spaces,
- * then we remove 3 spaces from the start of every line.
- */
-std::string stripIndentation(std::string_view s);
-
-
-/**
- * Get the prefix of 's' up to and excluding the next line break (LF
- * optionally preceded by CR), and the remainder following the line
- * break.
- */
-std::pair<std::string_view, std::string_view> getLine(std::string_view s);
-
-
-/**
- * Get a value for the specified key from an associate container.
- */
-template <class T>
-const typename T::mapped_type * get(const T & map, const typename T::key_type & key)
-{
- auto i = map.find(key);
- if (i == map.end()) return nullptr;
- return &i->second;
-}
-
-template <class T>
-typename T::mapped_type * get(T & map, const typename T::key_type & key)
-{
- auto i = map.find(key);
- if (i == map.end()) return nullptr;
- return &i->second;
-}
-
-/**
- * Get a value for the specified key from an associate container, or a default value if the key isn't present.
- */
-template <class T>
-const typename T::mapped_type & getOr(T & map,
- const typename T::key_type & key,
- const typename T::mapped_type & defaultValue)
-{
- auto i = map.find(key);
- if (i == map.end()) return defaultValue;
- return i->second;
-}
-
-/**
- * Remove and return the first item from a container.
- */
-template <class T>
-std::optional<typename T::value_type> remove_begin(T & c)
-{
- auto i = c.begin();
- if (i == c.end()) return {};
- auto v = std::move(*i);
- c.erase(i);
- return v;
-}
-
-
-/**
- * Remove and return the first item from a container.
- */
-template <class T>
-std::optional<typename T::value_type> pop(T & c)
-{
- if (c.empty()) return {};
- auto v = std::move(c.front());
- c.pop();
- return v;
-}
-
-
-/**
- * A RAII helper that increments a counter on construction and
- * decrements it on destruction.
- */
-template<typename T>
-struct MaintainCount
-{
- T & counter;
- long delta;
- MaintainCount(T & counter, long delta = 1) : counter(counter), delta(delta) { counter += delta; }
- ~MaintainCount() { counter -= delta; }
-};
-
-
-/**
- * @return the number of rows and columns of the terminal.
- */
-std::pair<unsigned short, unsigned short> getWindowSize();
-
-void updateWindowSize();
-
-
-/**
- * Used in various places.
- */
-typedef std::function<bool(const Path & path)> PathFilter;
-
-extern PathFilter defaultPathFilter;
-
-/**
- * Common initialisation performed in child processes.
- */
-void commonChildInit();
-
-/**
- * Create a Unix domain socket.
- */
-AutoCloseFD createUnixDomainSocket();
-
-/**
- * Create a Unix domain socket in listen mode.
- */
-AutoCloseFD createUnixDomainSocket(const Path & path, mode_t mode);
-
-/**
- * Bind a Unix domain socket to a path.
- */
-void bind(int fd, const std::string & path);
-
-/**
- * Connect to a Unix domain socket.
- */
-void connect(int fd, const std::string & path);
-
-
-/**
- * A Rust/Python-like enumerate() iterator adapter.
- *
- * Borrowed from http://reedbeta.com/blog/python-like-enumerate-in-cpp17.
- */
-template <typename T,
- typename TIter = decltype(std::begin(std::declval<T>())),
- typename = decltype(std::end(std::declval<T>()))>
-constexpr auto enumerate(T && iterable)
-{
- struct iterator
- {
- size_t i;
- TIter iter;
- constexpr bool operator != (const iterator & other) const { return iter != other.iter; }
- constexpr void operator ++ () { ++i; ++iter; }
- constexpr auto operator * () const { return std::tie(i, *iter); }
- };
-
- struct iterable_wrapper
- {
- T iterable;
- constexpr auto begin() { return iterator{ 0, std::begin(iterable) }; }
- constexpr auto end() { return iterator{ 0, std::end(iterable) }; }
- };
-
- return iterable_wrapper{ std::forward<T>(iterable) };
-}
-
-
-/**
- * C++17 std::visit boilerplate
- */
-template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
-template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
-
-
-std::string showBytes(uint64_t bytes);
-
-
-/**
- * Provide an addition operator between strings and string_views
- * inexplicably omitted from the standard library.
- */
-inline std::string operator + (const std::string & s1, std::string_view s2)
-{
- auto s = s1;
- s.append(s2);
- return s;
-}
-
-inline std::string operator + (std::string && s, std::string_view s2)
-{
- s.append(s2);
- return std::move(s);
-}
-
-inline std::string operator + (std::string_view s1, const char * s2)
-{
- std::string s;
- s.reserve(s1.size() + strlen(s2));
- s.append(s1);
- s.append(s2);
- return s;
-}
-
-}