diff options
Diffstat (limited to 'src/libutil')
30 files changed, 512 insertions, 210 deletions
diff --git a/src/libutil/apply-config-options.hh b/src/libutil/apply-config-options.hh new file mode 100644 index 000000000..533904a69 --- /dev/null +++ b/src/libutil/apply-config-options.hh @@ -0,0 +1,53 @@ +#pragma once +/** + * @file + * @brief Options for applying `Config` settings. + */ + +#include <types.hh> + +namespace nix { + +/** + * Options for applying `Config` settings. + */ +struct ApplyConfigOptions +{ + /** + * The configuration file being loaded. + * + * If set, relative paths are allowed and interpreted as relative to the + * directory of this path. + */ + std::optional<Path> path = std::nullopt; + + /** + * If set, tilde paths (like `~/.config/repl.nix`) are allowed and the + * tilde is substituted for this directory. + */ + std::optional<Path> home = std::nullopt; + + /** + * Is the configuration being loaded from the `$NIX_CONFIG` environment + * variable? + * + * Used for formatting error messages. + */ + bool fromEnvVar = false; + + /** + * Display the `relative` path field, with a reasonable default if none is + * available. + */ + std::string relativeDisplay() const { + if (path) { + return *path; + } else if (fromEnvVar) { + return "$NIX_CONFIG"; + } else { + return "<unknown>"; + } + } +}; + +} diff --git a/src/libutil/args.cc b/src/libutil/args.cc index 1342e7c6a..edcab23ac 100644 --- a/src/libutil/args.cc +++ b/src/libutil/args.cc @@ -1,10 +1,11 @@ #include "args.hh" #include "args/root.hh" #include "hash.hh" -#include "json-utils.hh" +#include "strings.hh" +#include "json-utils.hh" // IWYU pragma: keep (instances) #include "environment-variables.hh" -#include "experimental-features-json.hh" +#include "experimental-features-json.hh" // IWYU pragma: keep (instances) #include "logging.hh" #include <glob.h> diff --git a/src/libutil/args.hh b/src/libutil/args.hh index 5fdbaba7e..e2bac6415 100644 --- a/src/libutil/args.hh +++ b/src/libutil/args.hh @@ -3,6 +3,8 @@ #include "experimental-features.hh" #include "types.hh" +#include "ref.hh" + #include <functional> #include <map> #include <memory> diff --git a/src/libutil/backed-string-view.hh b/src/libutil/backed-string-view.hh new file mode 100644 index 000000000..96136331c --- /dev/null +++ b/src/libutil/backed-string-view.hh @@ -0,0 +1,69 @@ +#pragma once +/// @file String view that can be either owned or borrowed. +#include <variant> +#include <string> +#include <string_view> + +/** + * This wants to be a little bit like rust's Cow type. + * Some parts of the evaluator benefit greatly from being able to reuse + * existing allocations for strings, but have to be able to also use + * newly allocated storage for values. + * + * We do not define implicit conversions, even with ref qualifiers, + * since those can easily become ambiguous to the reader and can degrade + * into copying behaviour we want to avoid. + */ +class BackedStringView { +private: + std::variant<std::string, std::string_view> data; + + /** + * Needed to introduce a temporary since operator-> must return + * a pointer. Without this we'd need to store the view object + * even when we already own a string. + */ + class Ptr { + private: + std::string_view view; + public: + Ptr(std::string_view view): view(view) {} + const std::string_view * operator->() const { return &view; } + }; + +public: + BackedStringView(std::string && s): data(std::move(s)) {} + BackedStringView(std::string_view sv): data(sv) {} + template<size_t N> + BackedStringView(const char (& lit)[N]): data(std::string_view(lit)) {} + + BackedStringView(const BackedStringView &) = delete; + BackedStringView & operator=(const BackedStringView &) = delete; + + /** + * We only want move operations defined since the sole purpose of + * this type is to avoid copies. + */ + BackedStringView(BackedStringView && other) = default; + BackedStringView & operator=(BackedStringView && other) = default; + + bool isOwned() const + { + return std::holds_alternative<std::string>(data); + } + + std::string toOwned() && + { + return isOwned() + ? std::move(std::get<std::string>(data)) + : std::string(std::get<std::string_view>(data)); + } + + std::string_view operator*() const + { + return isOwned() + ? std::get<std::string>(data) + : std::get<std::string_view>(data); + } + Ptr operator->() const { return Ptr(**this); } +}; diff --git a/src/libutil/config-impl.hh b/src/libutil/config-impl.hh index 024018e00..748107b6e 100644 --- a/src/libutil/config-impl.hh +++ b/src/libutil/config-impl.hh @@ -51,14 +51,14 @@ bool BaseSetting<T>::isAppendable() return trait::appendable; } -template<> void BaseSetting<Strings>::appendOrSet(Strings newValue, bool append); -template<> void BaseSetting<StringSet>::appendOrSet(StringSet newValue, bool append); -template<> void BaseSetting<StringMap>::appendOrSet(StringMap newValue, bool append); -template<> void BaseSetting<ExperimentalFeatures>::appendOrSet(ExperimentalFeatures newValue, bool append); -template<> void BaseSetting<DeprecatedFeatures>::appendOrSet(DeprecatedFeatures newValue, bool append); +template<> void BaseSetting<Strings>::appendOrSet(Strings newValue, bool append, const ApplyConfigOptions & options); +template<> void BaseSetting<StringSet>::appendOrSet(StringSet newValue, bool append, const ApplyConfigOptions & options); +template<> void BaseSetting<StringMap>::appendOrSet(StringMap newValue, bool append, const ApplyConfigOptions & options); +template<> void BaseSetting<ExperimentalFeatures>::appendOrSet(ExperimentalFeatures newValue, bool append, const ApplyConfigOptions & options); +template<> void BaseSetting<DeprecatedFeatures>::appendOrSet(DeprecatedFeatures newValue, bool append, const ApplyConfigOptions & options); template<typename T> -void BaseSetting<T>::appendOrSet(T newValue, bool append) +void BaseSetting<T>::appendOrSet(T newValue, bool append, const ApplyConfigOptions & options) { static_assert( !trait::appendable, @@ -69,14 +69,14 @@ void BaseSetting<T>::appendOrSet(T newValue, bool append) } template<typename T> -void BaseSetting<T>::set(const std::string & str, bool append) +void BaseSetting<T>::set(const std::string & str, bool append, const ApplyConfigOptions & options) { if (experimentalFeatureSettings.isEnabled(experimentalFeature)) { - auto parsed = parse(str); + auto parsed = parse(str, options); if (deprecated && (append || parsed != value)) { warn("deprecated setting '%s' found (set to '%s')", name, str); } - appendOrSet(std::move(parsed), append); + appendOrSet(std::move(parsed), append, options); } else { assert(experimentalFeature); warn("Ignoring setting '%s' because experimental feature '%s' is not enabled", @@ -111,7 +111,7 @@ void BaseSetting<T>::convertToArg(Args & args, const std::string & category) } #define DECLARE_CONFIG_SERIALISER(TY) \ - template<> TY BaseSetting< TY >::parse(const std::string & str) const; \ + template<> TY BaseSetting< TY >::parse(const std::string & str, const ApplyConfigOptions & options) const; \ template<> std::string BaseSetting< TY >::to_string() const; DECLARE_CONFIG_SERIALISER(std::string) @@ -124,7 +124,7 @@ DECLARE_CONFIG_SERIALISER(ExperimentalFeatures) DECLARE_CONFIG_SERIALISER(DeprecatedFeatures) template<typename T> -T BaseSetting<T>::parse(const std::string & str) const +T BaseSetting<T>::parse(const std::string & str, const ApplyConfigOptions & options) const { static_assert(std::is_integral<T>::value, "Integer required."); diff --git a/src/libutil/config.cc b/src/libutil/config.cc index 8e20f1321..778da1413 100644 --- a/src/libutil/config.cc +++ b/src/libutil/config.cc @@ -1,4 +1,5 @@ #include "config.hh" +#include "apply-config-options.hh" #include "args.hh" #include "abstract-setting-to-json.hh" #include "experimental-features.hh" @@ -17,7 +18,7 @@ Config::Config(StringMap initials) : AbstractConfig(std::move(initials)) { } -bool Config::set(const std::string & name, const std::string & value) +bool Config::set(const std::string & name, const std::string & value, const ApplyConfigOptions & options) { bool append = false; auto i = _settings.find(name); @@ -30,7 +31,7 @@ bool Config::set(const std::string & name, const std::string & value) } else return false; } - i->second.setting->set(value, append); + i->second.setting->set(value, append, options); i->second.setting->overridden = true; return true; } @@ -91,7 +92,7 @@ void Config::getSettings(std::map<std::string, SettingInfo> & res, bool overridd } -static void applyConfigInner(const std::string & contents, const std::string & path, std::vector<std::pair<std::string, std::string>> & parsedContents) { +static void applyConfigInner(const std::string & contents, const ApplyConfigOptions & options, std::vector<std::pair<std::string, std::string>> & parsedContents) { unsigned int pos = 0; while (pos < contents.size()) { @@ -107,7 +108,7 @@ static void applyConfigInner(const std::string & contents, const std::string & p if (tokens.empty()) continue; if (tokens.size() < 2) - throw UsageError("illegal configuration line '%1%' in '%2%'", line, path); + throw UsageError("illegal configuration line '%1%' in '%2%'", line, options.relativeDisplay()); auto include = false; auto ignoreMissing = false; @@ -119,24 +120,32 @@ static void applyConfigInner(const std::string & contents, const std::string & p } if (include) { - if (tokens.size() != 2) - throw UsageError("illegal configuration line '%1%' in '%2%'", line, path); - auto p = absPath(tokens[1], dirOf(path)); - if (pathExists(p)) { + if (tokens.size() != 2) { + throw UsageError("illegal configuration line '%1%' in '%2%'", line, options.relativeDisplay()); + } + if (!options.path) { + throw UsageError("can only include configuration '%1%' from files", tokens[1]); + } + auto pathToInclude = absPath(tildePath(tokens[1], options.home), dirOf(*options.path)); + if (pathExists(pathToInclude)) { + auto includeOptions = ApplyConfigOptions { + .path = pathToInclude, + .home = options.home, + }; try { - std::string includedContents = readFile(path); - applyConfigInner(includedContents, p, parsedContents); + std::string includedContents = readFile(pathToInclude); + applyConfigInner(includedContents, includeOptions, parsedContents); } catch (SysError &) { // TODO: Do we actually want to ignore this? Or is it better to fail? } } else if (!ignoreMissing) { - throw Error("file '%1%' included from '%2%' not found", p, path); + throw Error("file '%1%' included from '%2%' not found", pathToInclude, *options.path); } continue; } if (tokens[1] != "=") - throw UsageError("illegal configuration line '%1%' in '%2%'", line, path); + throw UsageError("illegal configuration line '%1%' in '%2%'", line, options.relativeDisplay()); std::string name = std::move(tokens[0]); @@ -150,20 +159,20 @@ static void applyConfigInner(const std::string & contents, const std::string & p }; } -void AbstractConfig::applyConfig(const std::string & contents, const std::string & path) { +void AbstractConfig::applyConfig(const std::string & contents, const ApplyConfigOptions & options) { std::vector<std::pair<std::string, std::string>> parsedContents; - applyConfigInner(contents, path, parsedContents); + applyConfigInner(contents, options, parsedContents); // First apply experimental-feature related settings for (const auto & [name, value] : parsedContents) if (name == "experimental-features" || name == "extra-experimental-features") - set(name, value); + set(name, value, options); // Then apply other settings for (const auto & [name, value] : parsedContents) if (name != "experimental-features" && name != "extra-experimental-features") - set(name, value); + set(name, value, options); } void Config::resetOverridden() @@ -241,7 +250,7 @@ void AbstractSetting::convertToArg(Args & args, const std::string & category) bool AbstractSetting::isOverridden() const { return overridden; } -template<> std::string BaseSetting<std::string>::parse(const std::string & str) const +template<> std::string BaseSetting<std::string>::parse(const std::string & str, const ApplyConfigOptions & options) const { return str; } @@ -251,7 +260,7 @@ template<> std::string BaseSetting<std::string>::to_string() const return value; } -template<> std::optional<std::string> BaseSetting<std::optional<std::string>>::parse(const std::string & str) const +template<> std::optional<std::string> BaseSetting<std::optional<std::string>>::parse(const std::string & str, const ApplyConfigOptions & options) const { if (str == "") return std::nullopt; @@ -264,7 +273,7 @@ template<> std::string BaseSetting<std::optional<std::string>>::to_string() cons return value ? *value : ""; } -template<> bool BaseSetting<bool>::parse(const std::string & str) const +template<> bool BaseSetting<bool>::parse(const std::string & str, const ApplyConfigOptions & options) const { if (str == "true" || str == "yes" || str == "1") return true; @@ -297,12 +306,12 @@ template<> void BaseSetting<bool>::convertToArg(Args & args, const std::string & }); } -template<> Strings BaseSetting<Strings>::parse(const std::string & str) const +template<> Strings BaseSetting<Strings>::parse(const std::string & str, const ApplyConfigOptions & options) const { return tokenizeString<Strings>(str); } -template<> void BaseSetting<Strings>::appendOrSet(Strings newValue, bool append) +template<> void BaseSetting<Strings>::appendOrSet(Strings newValue, bool append, const ApplyConfigOptions & options) { if (!append) value.clear(); value.insert(value.end(), std::make_move_iterator(newValue.begin()), @@ -314,12 +323,12 @@ template<> std::string BaseSetting<Strings>::to_string() const return concatStringsSep(" ", value); } -template<> StringSet BaseSetting<StringSet>::parse(const std::string & str) const +template<> StringSet BaseSetting<StringSet>::parse(const std::string & str, const ApplyConfigOptions & options) const { return tokenizeString<StringSet>(str); } -template<> void BaseSetting<StringSet>::appendOrSet(StringSet newValue, bool append) +template<> void BaseSetting<StringSet>::appendOrSet(StringSet newValue, bool append, const ApplyConfigOptions & options) { if (!append) value.clear(); value.insert(std::make_move_iterator(newValue.begin()), std::make_move_iterator(newValue.end())); @@ -330,7 +339,7 @@ template<> std::string BaseSetting<StringSet>::to_string() const return concatStringsSep(" ", value); } -template<> ExperimentalFeatures BaseSetting<ExperimentalFeatures>::parse(const std::string & str) const +template<> ExperimentalFeatures BaseSetting<ExperimentalFeatures>::parse(const std::string & str, const ApplyConfigOptions & options) const { ExperimentalFeatures res{}; for (auto & s : tokenizeString<StringSet>(str)) { @@ -342,7 +351,7 @@ template<> ExperimentalFeatures BaseSetting<ExperimentalFeatures>::parse(const s return res; } -template<> void BaseSetting<ExperimentalFeatures>::appendOrSet(ExperimentalFeatures newValue, bool append) +template<> void BaseSetting<ExperimentalFeatures>::appendOrSet(ExperimentalFeatures newValue, bool append, const ApplyConfigOptions & options) { if (append) value = value | newValue; @@ -359,7 +368,7 @@ template<> std::string BaseSetting<ExperimentalFeatures>::to_string() const return concatStringsSep(" ", stringifiedXpFeatures); } -template<> DeprecatedFeatures BaseSetting<DeprecatedFeatures>::parse(const std::string & str) const +template<> DeprecatedFeatures BaseSetting<DeprecatedFeatures>::parse(const std::string & str, const ApplyConfigOptions & options) const { DeprecatedFeatures res{}; for (auto & s : tokenizeString<StringSet>(str)) { @@ -371,7 +380,7 @@ template<> DeprecatedFeatures BaseSetting<DeprecatedFeatures>::parse(const std:: return res; } -template<> void BaseSetting<DeprecatedFeatures>::appendOrSet(DeprecatedFeatures newValue, bool append) +template<> void BaseSetting<DeprecatedFeatures>::appendOrSet(DeprecatedFeatures newValue, bool append, const ApplyConfigOptions & options) { if (append) value = value | newValue; @@ -388,7 +397,7 @@ template<> std::string BaseSetting<DeprecatedFeatures>::to_string() const return concatStringsSep(" ", stringifiedDpFeatures); } -template<> StringMap BaseSetting<StringMap>::parse(const std::string & str) const +template<> StringMap BaseSetting<StringMap>::parse(const std::string & str, const ApplyConfigOptions & options) const { StringMap res; for (const auto & s : tokenizeString<Strings>(str)) { @@ -399,7 +408,7 @@ template<> StringMap BaseSetting<StringMap>::parse(const std::string & str) cons return res; } -template<> void BaseSetting<StringMap>::appendOrSet(StringMap newValue, bool append) +template<> void BaseSetting<StringMap>::appendOrSet(StringMap newValue, bool append, const ApplyConfigOptions & options) { if (!append) value.clear(); value.insert(std::make_move_iterator(newValue.begin()), std::make_move_iterator(newValue.end())); @@ -426,34 +435,40 @@ template class BaseSetting<StringMap>; template class BaseSetting<ExperimentalFeatures>; template class BaseSetting<DeprecatedFeatures>; -static Path parsePath(const AbstractSetting & s, const std::string & str) +static Path parsePath(const AbstractSetting & s, const std::string & str, const ApplyConfigOptions & options) { - if (str == "") + if (str == "") { throw UsageError("setting '%s' is a path and paths cannot be empty", s.name); - else - return canonPath(str); + } else { + auto tildeResolvedPath = tildePath(str, options.home); + if (options.path) { + return absPath(tildeResolvedPath, dirOf(*options.path)); + } else { + return canonPath(tildeResolvedPath); + } + } } -template<> Path PathsSetting<Path>::parse(const std::string & str) const +template<> Path PathsSetting<Path>::parse(const std::string & str, const ApplyConfigOptions & options) const { - return parsePath(*this, str); + return parsePath(*this, str, options); } -template<> std::optional<Path> PathsSetting<std::optional<Path>>::parse(const std::string & str) const +template<> std::optional<Path> PathsSetting<std::optional<Path>>::parse(const std::string & str, const ApplyConfigOptions & options) const { if (str == "") return std::nullopt; else - return parsePath(*this, str); + return parsePath(*this, str, options); } -template<> Paths PathsSetting<Paths>::parse(const std::string & str) const +template<> Paths PathsSetting<Paths>::parse(const std::string & str, const ApplyConfigOptions & options) const { auto strings = tokenizeString<Strings>(str); Paths parsed; for (auto str : strings) { - parsed.push_back(canonPath(str)); + parsed.push_back(parsePath(*this, str, options)); } return parsed; @@ -464,10 +479,10 @@ template class PathsSetting<std::optional<Path>>; template class PathsSetting<Paths>; -bool GlobalConfig::set(const std::string & name, const std::string & value) +bool GlobalConfig::set(const std::string & name, const std::string & value, const ApplyConfigOptions & options) { for (auto & config : *configRegistrations) - if (config->set(name, value)) return true; + if (config->set(name, value, options)) return true; unknownSettings.emplace(name, value); diff --git a/src/libutil/config.hh b/src/libutil/config.hh index dbca4b406..59cc281c5 100644 --- a/src/libutil/config.hh +++ b/src/libutil/config.hh @@ -10,6 +10,7 @@ #include "types.hh" #include "experimental-features.hh" #include "deprecated-features.hh" +#include "apply-config-options.hh" namespace nix { @@ -61,7 +62,7 @@ public: * Sets the value referenced by `name` to `value`. Returns true if the * setting is known, false otherwise. */ - virtual bool set(const std::string & name, const std::string & value) = 0; + virtual bool set(const std::string & name, const std::string & value, const ApplyConfigOptions & options = {}) = 0; struct SettingInfo { @@ -81,7 +82,7 @@ public: * - contents: configuration contents to be parsed and applied * - path: location of the configuration file */ - void applyConfig(const std::string & contents, const std::string & path = "<unknown>"); + void applyConfig(const std::string & contents, const ApplyConfigOptions & options = {}); /** * Resets the `overridden` flag of all Settings @@ -155,7 +156,7 @@ public: Config(StringMap initials = {}); - bool set(const std::string & name, const std::string & value) override; + bool set(const std::string & name, const std::string & value, const ApplyConfigOptions & options = {}) override; void addSetting(AbstractSetting * setting); @@ -200,7 +201,7 @@ protected: virtual ~AbstractSetting(); - virtual void set(const std::string & value, bool append = false) = 0; + virtual void set(const std::string & value, bool append = false, const ApplyConfigOptions & options = {}) = 0; /** * Whether the type is appendable; i.e. whether the `append` @@ -237,7 +238,7 @@ protected: * * Used by `set()`. */ - virtual T parse(const std::string & str) const; + virtual T parse(const std::string & str, const ApplyConfigOptions & options) const; /** * Append or overwrite `value` with `newValue`. @@ -247,7 +248,7 @@ protected: * * @param append Whether to append or overwrite. */ - virtual void appendOrSet(T newValue, bool append); + virtual void appendOrSet(T newValue, bool append, const ApplyConfigOptions & options); public: @@ -284,7 +285,7 @@ public: * Uses `parse()` to get the value from `str`, and `appendOrSet()` * to set it. */ - void set(const std::string & str, bool append = false) override final; + void set(const std::string & str, bool append = false, const ApplyConfigOptions & options = {}) override final; /** * C++ trick; This is template-specialized to compile-time indicate whether @@ -373,7 +374,7 @@ public: options->addSetting(this); } - T parse(const std::string & str) const override; + T parse(const std::string & str, const ApplyConfigOptions & options) const override; void operator =(const T & v) { this->assign(v); } }; @@ -384,7 +385,7 @@ struct GlobalConfig : public AbstractConfig typedef std::vector<Config*> ConfigRegistrations; static ConfigRegistrations * configRegistrations; - bool set(const std::string & name, const std::string & value) override; + bool set(const std::string & name, const std::string & value, const ApplyConfigOptions & options = {}) override; void getSettings(std::map<std::string, SettingInfo> & res, bool overriddenOnly = false) override; diff --git a/src/libutil/error.cc b/src/libutil/error.cc index e5d6a9fa8..a7cbfbfd0 100644 --- a/src/libutil/error.cc +++ b/src/libutil/error.cc @@ -3,6 +3,7 @@ #include "logging.hh" #include "position.hh" #include "terminal.hh" +#include "strings.hh" #include <iostream> #include <optional> diff --git a/src/libutil/error.hh b/src/libutil/error.hh index 06dfba0df..73c1ccadd 100644 --- a/src/libutil/error.hh +++ b/src/libutil/error.hh @@ -16,16 +16,12 @@ */ #include "suggestions.hh" -#include "ref.hh" -#include "types.hh" #include "fmt.hh" #include <cstring> #include <list> #include <memory> -#include <map> #include <optional> -#include <compare> #include <sys/types.h> #include <sys/stat.h> @@ -173,6 +169,7 @@ public: }; #define MakeError(newClass, superClass) \ + /* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \ class newClass : public superClass \ { \ public: \ diff --git a/src/libutil/file-system.cc b/src/libutil/file-system.cc index 631cf076b..1d3eba58f 100644 --- a/src/libutil/file-system.cc +++ b/src/libutil/file-system.cc @@ -10,6 +10,7 @@ #include "logging.hh" #include "serialise.hh" #include "signals.hh" +#include "strings.hh" #include "types.hh" #include "users.hh" @@ -17,15 +18,19 @@ namespace fs = std::filesystem; namespace nix { +Path getCwd() { + char buf[PATH_MAX]; + if (!getcwd(buf, sizeof(buf))) { + throw SysError("cannot get cwd"); + } + return Path(buf); +} + Path absPath(Path path, std::optional<PathView> dir, bool resolveSymlinks) { if (path.empty() || path[0] != '/') { if (!dir) { - char buf[PATH_MAX]; - if (!getcwd(buf, sizeof(buf))) { - throw SysError("cannot get cwd"); - } - path = concatStrings(buf, "/", path); + path = concatStrings(getCwd(), "/", path); } else { path = concatStrings(*dir, "/", path); } @@ -117,6 +122,21 @@ Path realPath(Path const & path) return ret; } +Path tildePath(Path const & path, const std::optional<Path> & home) +{ + if (path.starts_with("~/")) { + if (home) { + return *home + "/" + path.substr(2); + } else { + throw UsageError("`~` path not allowed: %1%", path); + } + } else if (path.starts_with('~')) { + throw UsageError("`~` paths must start with `~/`: %1%", path); + } else { + return path; + } +} + void chmodPath(const Path & path, mode_t mode) { if (chmod(path.c_str(), mode) == -1) diff --git a/src/libutil/file-system.hh b/src/libutil/file-system.hh index 9fe931556..2547c63b5 100644 --- a/src/libutil/file-system.hh +++ b/src/libutil/file-system.hh @@ -30,6 +30,13 @@ struct Sink; struct Source; /** + * Get the current working directory. + * + * Throw an error if the current directory cannot get got. + */ +Path getCwd(); + +/** * @return An absolutized path, resolving paths relative to the * specified directory, or the current directory otherwise. The path * is also canonicalised. @@ -63,6 +70,16 @@ Path canonPath(PathView path, bool resolveSymlinks = false); Path realPath(Path const & path); /** + * Resolve a tilde path like `~/puppy.nix` into an absolute path. + * + * If `home` is given, it's substituted for `~/` at the start of the input + * `path`. Otherwise, an error is thrown. + * + * If the path starts with `~` but not `~/`, an error is thrown. + */ +Path tildePath(Path const & path, const std::optional<Path> & home = std::nullopt); + +/** * 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 diff --git a/src/libutil/finally.hh b/src/libutil/finally.hh index dc51d7b1e..5e62dfa3b 100644 --- a/src/libutil/finally.hh +++ b/src/libutil/finally.hh @@ -22,6 +22,7 @@ public: Finally(Finally &&other) : fun(std::move(other.fun)) { other.movedFrom = true; } + // NOLINTNEXTLINE(bugprone-exception-escape): the noexcept is declared properly here, the analysis seems broken ~Finally() noexcept(noexcept(fun())) { try { diff --git a/src/libutil/fmt.cc b/src/libutil/fmt.cc new file mode 100644 index 000000000..bff5af020 --- /dev/null +++ b/src/libutil/fmt.cc @@ -0,0 +1,24 @@ +#include "fmt.hh" // IWYU pragma: keep +// Darwin and FreeBSD stdenv do not define _GNU_SOURCE but do have _Unwind_Backtrace. +#if __APPLE__ || __FreeBSD__ +#define BOOST_STACKTRACE_GNU_SOURCE_NOT_REQUIRED +#endif +#include <boost/stacktrace/stacktrace.hpp> + +template class boost::basic_format<char>; + +namespace nix { + +// Explicit instantiation saves about 30 cpu-seconds of compile time +template HintFmt::HintFmt(const std::string &, const Uncolored<std::string> &s); +template HintFmt::HintFmt(const std::string &, const std::string &s); +template HintFmt::HintFmt(const std::string &, const uint64_t &, const char * const &); + +HintFmt::HintFmt(const std::string & literal) : HintFmt("%s", Uncolored(literal)) {} + +void printStackTrace() +{ + std::cerr << boost::stacktrace::stacktrace() << std::endl; +} + +} diff --git a/src/libutil/fmt.hh b/src/libutil/fmt.hh index d015f7e5f..ee3e1e2e7 100644 --- a/src/libutil/fmt.hh +++ b/src/libutil/fmt.hh @@ -3,17 +3,17 @@ #include <iostream> #include <string> -#include <optional> #include <boost/format.hpp> -// Darwin and FreeBSD stdenv do not define _GNU_SOURCE but do have _Unwind_Backtrace. -#if __APPLE__ || __FreeBSD__ -#define BOOST_STACKTRACE_GNU_SOURCE_NOT_REQUIRED -#endif -#include <boost/stacktrace.hpp> #include "ansicolor.hh" +// Explicit instantiation in fmt.cc +extern template class boost::basic_format<char>; + namespace nix { +/** Prints a C++ stack trace to stderr using boost stacktrace */ +void printStackTrace(); + /** * Values wrapped in this struct are printed in magenta. * @@ -157,7 +157,9 @@ public: * Format the given string literally, without interpolating format * placeholders. */ - HintFmt(const std::string & literal) : HintFmt("%s", Uncolored(literal)) {} + // Moved out of line because it was instantiating the template below in + // every file in the project. + HintFmt(const std::string & literal); /** * Interpolate the given arguments into the format string. @@ -172,14 +174,14 @@ public: std::cerr << "HintFmt received incorrect number of format args. Original format string: '"; std::cerr << format << "'; number of arguments: " << sizeof...(args) << "\n"; // And regardless of the coredump give me a damn stacktrace. - std::cerr << boost::stacktrace::stacktrace() << std::endl; + printStackTrace(); abort(); } } catch (boost::io::format_error & ex) { // Same thing, but for anything that happens in the member initializers. std::cerr << "HintFmt received incorrect format string. Original format string: '"; std::cerr << format << "'; number of arguments: " << sizeof...(args) << "\n"; - std::cerr << boost::stacktrace::stacktrace() << std::endl; + printStackTrace(); abort(); } @@ -193,6 +195,11 @@ public: } }; +// Explicit instantiations in fmt.cc +extern template HintFmt::HintFmt(const std::string &, const Uncolored<std::string> &s); +extern template HintFmt::HintFmt(const std::string &, const std::string &s); +extern template HintFmt::HintFmt(const std::string &, const uint64_t &, const char * const &); + std::ostream & operator<<(std::ostream & os, const HintFmt & hf); } diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc index 925f71f80..d383e9802 100644 --- a/src/libutil/hash.cc +++ b/src/libutil/hash.cc @@ -7,8 +7,10 @@ #include "args.hh" #include "hash.hh" #include "archive.hh" +#include "charptr-cast.hh" #include "logging.hh" #include "split.hh" +#include "strings.hh" #include <sys/types.h> #include <sys/stat.h> @@ -129,7 +131,7 @@ std::string Hash::to_string(Base base, bool includeType) const break; case Base::Base64: case Base::SRI: - s += base64Encode(std::string_view(reinterpret_cast<const char *>(hash), hashSize)); + s += base64Encode(std::string_view(charptr_cast<const char *>(hash), hashSize)); break; } return s; diff --git a/src/libutil/meson.build b/src/libutil/meson.build index 3e10e4b63..a3f21de59 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -17,6 +17,7 @@ libutil_sources = files( 'experimental-features.cc', 'file-descriptor.cc', 'file-system.cc', + 'fmt.cc', 'git.cc', 'hash.cc', 'hilite.cc', @@ -46,11 +47,13 @@ libutil_sources = files( ) libutil_headers = files( + 'apply-config-options.hh', 'abstract-setting-to-json.hh', 'ansicolor.hh', 'archive.hh', 'args/root.hh', 'args.hh', + 'backed-string-view.hh', 'box_ptr.hh', 'canon-path.hh', 'cgroup.hh', @@ -92,6 +95,7 @@ libutil_headers = files( 'monitor-fd.hh', 'mount.hh', 'namespaces.hh', + 'notifying-counter.hh', 'pool.hh', 'position.hh', 'print-elided.hh', @@ -101,6 +105,7 @@ libutil_headers = files( 'regex-combinators.hh', 'regex.hh', 'repair-flag.hh', + 'result.hh', 'serialise.hh', 'shlex.hh', 'signals.hh', diff --git a/src/libutil/notifying-counter.hh b/src/libutil/notifying-counter.hh new file mode 100644 index 000000000..dc58aac91 --- /dev/null +++ b/src/libutil/notifying-counter.hh @@ -0,0 +1,99 @@ +#pragma once +/// @file + +#include <cassert> +#include <functional> +#include <memory> + +namespace nix { + +template<std::integral T> +class NotifyingCounter +{ +private: + T counter; + std::function<void()> notify; + +public: + class Bump + { + friend class NotifyingCounter; + + struct SubOnFree + { + T delta; + + void operator()(NotifyingCounter * c) const + { + c->add(-delta); + } + }; + + // lightly misuse unique_ptr to get RAII types with destructor callbacks + std::unique_ptr<NotifyingCounter<T>, SubOnFree> at; + + Bump(NotifyingCounter<T> & at, T delta) : at(&at, {delta}) {} + + public: + Bump() = default; + Bump(decltype(nullptr)) {} + + T delta() const + { + return at ? at.get_deleter().delta : 0; + } + + void reset() + { + at.reset(); + } + }; + + explicit NotifyingCounter(std::function<void()> notify, T initial = 0) + : counter(initial) + , notify(std::move(notify)) + { + assert(this->notify); + } + + // bumps hold pointers to this, so we should neither copy nor move. + NotifyingCounter(const NotifyingCounter &) = delete; + NotifyingCounter & operator=(const NotifyingCounter &) = delete; + NotifyingCounter(NotifyingCounter &&) = delete; + NotifyingCounter & operator=(NotifyingCounter &&) = delete; + + T get() const + { + return counter; + } + + operator T() const + { + return counter; + } + + void add(T delta) + { + counter += delta; + notify(); + } + + NotifyingCounter & operator+=(T delta) + { + add(delta); + return *this; + } + + NotifyingCounter & operator++(int) + { + return *this += 1; + } + + Bump addTemporarily(T delta) + { + add(delta); + return Bump{*this, delta}; + } +}; + +} diff --git a/src/libutil/processes.cc b/src/libutil/processes.cc index 61e1ad556..eec592221 100644 --- a/src/libutil/processes.cc +++ b/src/libutil/processes.cc @@ -3,6 +3,7 @@ #include "finally.hh" #include "logging.hh" #include "processes.hh" +#include "strings.hh" #include "serialise.hh" #include "signals.hh" diff --git a/src/libutil/regex.cc b/src/libutil/regex.cc index a9e6c6bee..a12d13550 100644 --- a/src/libutil/regex.cc +++ b/src/libutil/regex.cc @@ -1,6 +1,9 @@ #include <string> #include <regex> +// Declared as extern in precompiled-headers.hh +template class std::basic_regex<char>; + namespace nix::regex { std::string quoteRegexChars(const std::string & raw) { diff --git a/src/libutil/result.hh b/src/libutil/result.hh new file mode 100644 index 000000000..b01766fe4 --- /dev/null +++ b/src/libutil/result.hh @@ -0,0 +1,24 @@ +#pragma once +/// @file + +#include <boost/outcome/std_outcome.hpp> +#include <boost/outcome/std_result.hpp> +#include <boost/outcome/success_failure.hpp> +#include <exception> + +namespace nix { + +template<typename T, typename E = std::exception_ptr> +using Result = boost::outcome_v2::std_result<T, E>; + +template<typename T, typename D, typename E = std::exception_ptr> +using Outcome = boost::outcome_v2::std_outcome<T, D, E>; + +namespace result { + +using boost::outcome_v2::success; +using boost::outcome_v2::failure; + +} + +} diff --git a/src/libutil/serialise.cc b/src/libutil/serialise.cc index f1db05b0b..f509fedff 100644 --- a/src/libutil/serialise.cc +++ b/src/libutil/serialise.cc @@ -1,4 +1,5 @@ #include "serialise.hh" +#include "charptr-cast.hh" #include "signals.hh" #include <cstring> @@ -8,6 +9,46 @@ namespace nix { +namespace { +/** + * 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; +} +} + +template<typename T> +T readNum(Source & source) +{ + unsigned char buf[8]; + source(charptr_cast<char *>(buf), sizeof(buf)); + + auto n = readLittleEndian<uint64_t>(buf); + + if (n > (uint64_t) std::numeric_limits<T>::max()) + throw SerialisationError("serialised integer %d is too large for type '%s'", n, typeid(T).name()); + + return (T) n; +} + +template bool readNum<bool>(Source & source); + +template unsigned char readNum<unsigned char>(Source & source); + +template unsigned int readNum<unsigned int>(Source & source); + +template unsigned long readNum<unsigned long>(Source & source); +template long readNum<long>(Source & source); + +template unsigned long long readNum<unsigned long long>(Source & source); +template long long readNum<long long>(Source & source); void BufferedSink::operator () (std::string_view data) { @@ -126,7 +167,7 @@ size_t FdSource::readUnbuffered(char * data, size_t len) n = ::read(fd, data, len); } while (n == -1 && errno == EINTR); if (n == -1) { _good = false; throw SysError("reading from file"); } - if (n == 0) { _good = false; throw EndOfFile(std::string(*endOfFileError)); } + if (n == 0) { _good = false; throw EndOfFile(endOfFileError()); } read += n; return n; } @@ -137,6 +178,11 @@ bool FdSource::good() return _good; } +std::string FdSource::endOfFileError() const +{ + return specialEndOfFileError.has_value() ? *specialEndOfFileError : "unexpected end-of-file"; +} + size_t StringSource::read(char * data, size_t len) { diff --git a/src/libutil/serialise.hh b/src/libutil/serialise.hh index 6c637bd35..612658b2d 100644 --- a/src/libutil/serialise.hh +++ b/src/libutil/serialise.hh @@ -1,16 +1,13 @@ #pragma once ///@file -#include <concepts> #include <memory> +#include "charptr-cast.hh" #include "generator.hh" -#include "strings.hh" #include "types.hh" #include "file-descriptor.hh" -namespace boost::context { struct stack_context; } - namespace nix { @@ -154,7 +151,10 @@ struct FdSource : BufferedSource { int fd; size_t read = 0; - BackedStringView endOfFileError{"unexpected end-of-file"}; + /** Defaults to "unexpected end-of-file" */ + std::optional<std::string> specialEndOfFileError; + + std::string endOfFileError() const; FdSource() : fd(-1) { } FdSource(int fd) : fd(fd) { } @@ -387,7 +387,7 @@ struct SerializingTransform buf[5] = (n >> 40) & 0xff; buf[6] = (n >> 48) & 0xff; buf[7] = (unsigned char) (n >> 56) & 0xff; - return {reinterpret_cast<const char *>(buf.begin()), 8}; + return {charptr_cast<const char *>(buf.begin()), 8}; } static Bytes padding(size_t unpadded) @@ -419,6 +419,9 @@ struct SerializingTransform void writePadding(size_t len, Sink & sink); +// NOLINTBEGIN(cppcoreguidelines-avoid-capturing-lambda-coroutines): +// These coroutines do their entire job before the semicolon and are not +// retained, so they live long enough. inline Sink & operator<<(Sink & sink, uint64_t u) { return sink << [&]() -> WireFormatGenerator { co_yield u; }(); @@ -443,23 +446,12 @@ inline Sink & operator<<(Sink & sink, const Error & ex) { return sink << [&]() -> WireFormatGenerator { co_yield ex; }(); } +// NOLINTEND(cppcoreguidelines-avoid-capturing-lambda-coroutines) MakeError(SerialisationError, Error); template<typename T> -T readNum(Source & source) -{ - unsigned char buf[8]; - source((char *) buf, sizeof(buf)); - - auto n = readLittleEndian<uint64_t>(buf); - - if (n > (uint64_t) std::numeric_limits<T>::max()) - throw SerialisationError("serialised integer %d is too large for type '%s'", n, typeid(T).name()); - - return (T) n; -} - +T readNum(Source & source); inline unsigned int readInt(Source & source) { @@ -542,13 +534,17 @@ struct FramedSource : Source ~FramedSource() { - if (!eof) { - while (true) { - auto n = readInt(from); - if (!n) break; - std::vector<char> data(n); - from(data.data(), n); + try { + if (!eof) { + while (true) { + auto n = readInt(from); + if (!n) break; + std::vector<char> data(n); + from(data.data(), n); + } } + } catch (...) { + ignoreException(); } } @@ -612,23 +608,6 @@ struct FramedSink : nix::BufferedSink }; }; -/** - * Stack allocation strategy for sinkToSource. - * Mutable to avoid a boehm gc dependency in libutil. - * - * boost::context doesn't provide a virtual class, so we define our own. - */ -struct StackAllocator { - virtual boost::context::stack_context allocate() = 0; - virtual void deallocate(boost::context::stack_context sctx) = 0; - - /** - * The stack allocator to use in sinkToSource and potentially elsewhere. - * It is reassigned by the initGC() method in libexpr. - */ - static StackAllocator *defaultAllocator; -}; - /* Disabling GC when entering a coroutine (without the boehm patch). mutable to avoid boehm gc dependency in libutil. */ diff --git a/src/libutil/signals.cc b/src/libutil/signals.cc index a94c2802a..04a697d01 100644 --- a/src/libutil/signals.cc +++ b/src/libutil/signals.cc @@ -3,6 +3,7 @@ #include "sync.hh" #include "terminal.hh" +#include <map> #include <thread> namespace nix { diff --git a/src/libutil/source-path.cc b/src/libutil/source-path.cc index cfaac20c0..782005ef1 100644 --- a/src/libutil/source-path.cc +++ b/src/libutil/source-path.cc @@ -1,4 +1,5 @@ #include "source-path.hh" +#include "strings.hh" namespace nix { diff --git a/src/libutil/strings.hh b/src/libutil/strings.hh index 7330e2063..782807b61 100644 --- a/src/libutil/strings.hh +++ b/src/libutil/strings.hh @@ -165,19 +165,6 @@ std::optional<N> string2Float(const std::string_view s); /** - * 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); @@ -213,8 +200,18 @@ std::string showBytes(uint64_t bytes); /** - * Provide an addition operator between strings and string_views + * Provide an addition operator between `std::string` and `std::string_view` * inexplicably omitted from the standard library. + * + * > The reason for this is given in n3512 string_ref: a non-owning reference + * to a string, revision 2 by Jeffrey Yasskin: + * > + * > > I also omitted operator+(basic_string, basic_string_ref) because LLVM + * > > returns a lightweight object from this overload and only performs the + * > > concatenation lazily. If we define this overload, we'll have a hard time + * > > introducing that lightweight concatenation later. + * + * See: https://stackoverflow.com/a/47735624 */ inline std::string operator + (const std::string & s1, std::string_view s2) { diff --git a/src/libutil/thread-pool.hh b/src/libutil/thread-pool.hh index 3db7ce88f..380e1a2d2 100644 --- a/src/libutil/thread-pool.hh +++ b/src/libutil/thread-pool.hh @@ -4,6 +4,7 @@ #include "error.hh" #include "sync.hh" +#include <map> #include <queue> #include <functional> #include <thread> diff --git a/src/libutil/types.hh b/src/libutil/types.hh index 13cb062fb..66c41fe59 100644 --- a/src/libutil/types.hh +++ b/src/libutil/types.hh @@ -1,17 +1,15 @@ #pragma once ///@file -#include "ref.hh" - #include <list> #include <optional> #include <set> #include <string> -#include <limits> +#include <string_view> #include <map> -#include <variant> #include <vector> #include <span> +#include <stdint.h> // IWYU pragma: keep (this is used literally everywhere) namespace nix { @@ -166,70 +164,4 @@ constexpr auto enumerate(T && iterable) 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. - * Some parts of the evaluator benefit greatly from being able to reuse - * existing allocations for strings, but have to be able to also use - * newly allocated storage for values. - * - * We do not define implicit conversions, even with ref qualifiers, - * since those can easily become ambiguous to the reader and can degrade - * into copying behaviour we want to avoid. - */ -class BackedStringView { -private: - std::variant<std::string, std::string_view> data; - - /** - * Needed to introduce a temporary since operator-> must return - * a pointer. Without this we'd need to store the view object - * even when we already own a string. - */ - class Ptr { - private: - std::string_view view; - public: - Ptr(std::string_view view): view(view) {} - const std::string_view * operator->() const { return &view; } - }; - -public: - BackedStringView(std::string && s): data(std::move(s)) {} - BackedStringView(std::string_view sv): data(sv) {} - template<size_t N> - BackedStringView(const char (& lit)[N]): data(std::string_view(lit)) {} - - BackedStringView(const BackedStringView &) = delete; - BackedStringView & operator=(const BackedStringView &) = delete; - - /** - * We only want move operations defined since the sole purpose of - * this type is to avoid copies. - */ - BackedStringView(BackedStringView && other) = default; - BackedStringView & operator=(BackedStringView && other) = default; - - bool isOwned() const - { - return std::holds_alternative<std::string>(data); - } - - std::string toOwned() && - { - return isOwned() - ? std::move(std::get<std::string>(data)) - : std::string(std::get<std::string_view>(data)); - } - - std::string_view operator*() const - { - return isOwned() - ? std::get<std::string>(data) - : std::get<std::string_view>(data); - } - Ptr operator->() const { return Ptr(**this); } -}; - } diff --git a/src/libutil/url.hh b/src/libutil/url.hh index d2413ec0e..a821301ba 100644 --- a/src/libutil/url.hh +++ b/src/libutil/url.hh @@ -2,6 +2,7 @@ ///@file #include "error.hh" +#include <map> namespace nix { diff --git a/src/libutil/users.cc b/src/libutil/users.cc index a9a8a7353..ce36bad9b 100644 --- a/src/libutil/users.cc +++ b/src/libutil/users.cc @@ -36,7 +36,7 @@ Path getHome() 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. + // Only use `$HOME` if it exists and is owned by the current user. struct stat st; int result = stat(homeDir->c_str(), &st); if (result != 0) { diff --git a/src/libutil/variant-wrapper.hh b/src/libutil/variant-wrapper.hh index cedcb999c..a809cd2a4 100644 --- a/src/libutil/variant-wrapper.hh +++ b/src/libutil/variant-wrapper.hh @@ -8,6 +8,7 @@ * Force the default versions of all constructors (copy, move, copy * assignment). */ +// NOLINTBEGIN(bugprone-macro-parentheses) #define FORCE_DEFAULT_CONSTRUCTORS(CLASS_NAME) \ CLASS_NAME(const CLASS_NAME &) = default; \ CLASS_NAME(CLASS_NAME &) = default; \ @@ -15,6 +16,7 @@ \ CLASS_NAME & operator =(const CLASS_NAME &) = default; \ CLASS_NAME & operator =(CLASS_NAME &) = default; +// NOLINTEND(bugprone-macro-parentheses) /** * Make a wrapper constructor. All args are forwarded to the |