diff options
-rw-r--r-- | doc/manual/rl-next/relative-and-tilde-paths-in-config.md | 30 | ||||
-rw-r--r-- | src/libfetchers/fetch-settings.cc | 2 | ||||
-rw-r--r-- | src/libstore/globals.cc | 27 | ||||
-rw-r--r-- | src/libstore/globals.hh | 5 | ||||
-rw-r--r-- | src/libutil/config-impl.hh | 22 | ||||
-rw-r--r-- | src/libutil/config.cc | 97 | ||||
-rw-r--r-- | src/libutil/config.hh | 19 | ||||
-rw-r--r-- | src/libutil/file-system.cc | 15 | ||||
-rw-r--r-- | src/libutil/file-system.hh | 10 | ||||
-rw-r--r-- | tests/unit/libutil/config.cc | 2 | ||||
-rw-r--r-- | tests/unit/libutil/paths-setting.cc | 93 |
11 files changed, 216 insertions, 106 deletions
diff --git a/doc/manual/rl-next/relative-and-tilde-paths-in-config.md b/doc/manual/rl-next/relative-and-tilde-paths-in-config.md new file mode 100644 index 000000000..6645496a2 --- /dev/null +++ b/doc/manual/rl-next/relative-and-tilde-paths-in-config.md @@ -0,0 +1,30 @@ +--- +synopsis: Relative and tilde paths in configuration +issues: [fj#482] +cls: [1851, 1863, 1864] +category: Features +credits: [9999years] +--- + +[Configuration settings](@docroot@/command-ref/conf-file.md) can now refer to +files with paths relative to the file they're written in or relative to your +home directory (with `~/`). + +This makes settings like +[`repl-overlays`](@docroot@/command-ref/conf-file.md#conf-repl-overlays) and +[`secret-key-files`](@docroot@/command-ref/conf-file.md#conf-repl-overlays) +much easier to set, especially if you'd like to refer to files in an existing +dotfiles repo cloned into your home directory. + +If you put `repl-overlays = repl.nix` in your `~/.config/nix/nix.conf`, it'll +load `~/.config/nix/repl.nix`. Similarly, you can set `repl-overlays = +~/.dotfiles/repl.nix` to load a file relative to your home directory. + +Configuration files can also +[`include`](@docroot@/command-ref/conf-file.md#file-format) paths relative to +your home directory. + +Only user configuration files (like `$XDG_CONFIG_HOME/nix/nix.conf` or the +files listed in `$NIX_USER_CONF_FILES`) can use tilde paths relative to your +home directory. Configuration listed in the `$NIX_CONFIG` environment variable +may not use relative paths. diff --git a/src/libfetchers/fetch-settings.cc b/src/libfetchers/fetch-settings.cc index aeb3c542b..007f2725f 100644 --- a/src/libfetchers/fetch-settings.cc +++ b/src/libfetchers/fetch-settings.cc @@ -7,7 +7,7 @@ namespace nix { -template<> AcceptFlakeConfig BaseSetting<AcceptFlakeConfig>::parse(const std::string & str) const +template<> AcceptFlakeConfig BaseSetting<AcceptFlakeConfig>::parse(const std::string & str, const ApplyConfigOptions & options) const { if (str == "true") return AcceptFlakeConfig::True; else if (str == "ask") return AcceptFlakeConfig::Ask; diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc index b534882de..c114e22dc 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -126,29 +126,30 @@ Settings::Settings() void loadConfFile() { - auto applyConfigFile = [&](const Path & path) { + auto applyConfigFile = [&](const ApplyConfigOptions & options) { try { - std::string contents = readFile(path); - globalConfig.applyConfig(contents, path); - } catch (SysError &) { } + std::string contents = readFile(*options.path); + globalConfig.applyConfig(contents, options); + } catch (SysError &) { + } }; - applyConfigFile(settings.nixConfDir + "/nix.conf"); + applyConfigFile(ApplyConfigOptions{.path = settings.nixConfDir + "/nix.conf"}); /* We only want to send overrides to the daemon, i.e. stuff from ~/.nix/nix.conf or the command line. */ globalConfig.resetOverridden(); auto files = settings.nixUserConfFiles; + auto home = getHome(); for (auto file = files.rbegin(); file != files.rend(); file++) { - applyConfigFile(*file); + applyConfigFile(ApplyConfigOptions{.path = *file, .home = home}); } auto nixConfEnv = getEnv("NIX_CONFIG"); if (nixConfEnv.has_value()) { - globalConfig.applyConfig(nixConfEnv.value(), "NIX_CONFIG"); + globalConfig.applyConfig(nixConfEnv.value(), ApplyConfigOptions{.fromEnvVar = true}); } - } std::vector<Path> getUserConfigFiles() @@ -274,7 +275,7 @@ NLOHMANN_JSON_SERIALIZE_ENUM(SandboxMode, { {SandboxMode::smDisabled, false}, }); -template<> SandboxMode BaseSetting<SandboxMode>::parse(const std::string & str) const +template<> SandboxMode BaseSetting<SandboxMode>::parse(const std::string & str, const ApplyConfigOptions & options) const { if (str == "true") return smEnabled; else if (str == "relaxed") return smRelaxed; @@ -317,7 +318,7 @@ template<> void BaseSetting<SandboxMode>::convertToArg(Args & args, const std::s }); } -unsigned int MaxBuildJobsSetting::parse(const std::string & str) const +unsigned int MaxBuildJobsSetting::parse(const std::string & str, const ApplyConfigOptions & options) const { if (str == "auto") return std::max(1U, std::thread::hardware_concurrency()); else { @@ -325,15 +326,15 @@ unsigned int MaxBuildJobsSetting::parse(const std::string & str) const return *n; else throw UsageError("configuration setting '%s' should be 'auto' or an integer", name); + } } -} -Paths PluginFilesSetting::parse(const std::string & str) const +Paths PluginFilesSetting::parse(const std::string & str, const ApplyConfigOptions & options) const { if (pluginsLoaded) throw UsageError("plugin-files set after plugins were loaded, you may need to move the flag before the subcommand"); - return BaseSetting<Paths>::parse(str); + return BaseSetting<Paths>::parse(str, options); } diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index aba99d969..51550b2c3 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -26,7 +26,7 @@ struct MaxBuildJobsSetting : public BaseSetting<unsigned int> options->addSetting(this); } - unsigned int parse(const std::string & str) const override; + unsigned int parse(const std::string & str, const ApplyConfigOptions & options) const override; }; struct PluginFilesSetting : public BaseSetting<Paths> @@ -43,7 +43,7 @@ struct PluginFilesSetting : public BaseSetting<Paths> options->addSetting(this); } - Paths parse(const std::string & str) const override; + Paths parse(const std::string & str, const ApplyConfigOptions & options) const override; }; const uint32_t maxIdsPerBuild = @@ -1088,6 +1088,7 @@ void loadConfFile(); // Used by the Settings constructor std::vector<Path> getUserConfigFiles(); +std::vector<Path> getHomeConfigFile(); extern const std::string nixVersion; 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/file-system.cc b/src/libutil/file-system.cc index 1d266067e..8c69c9864 100644 --- a/src/libutil/file-system.cc +++ b/src/libutil/file-system.cc @@ -118,6 +118,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 e49323e84..0a54d1a3b 100644 --- a/src/libutil/file-system.hh +++ b/src/libutil/file-system.hh @@ -63,6 +63,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/tests/unit/libutil/config.cc b/tests/unit/libutil/config.cc index 886e70da5..1629969ba 100644 --- a/tests/unit/libutil/config.cc +++ b/tests/unit/libutil/config.cc @@ -80,7 +80,7 @@ namespace nix { class TestSetting : public AbstractSetting { public: TestSetting() : AbstractSetting("test", "test", {}) {} - void set(const std::string & value, bool append) override {} + void set(const std::string & value, bool append, const ApplyConfigOptions & options) override {} std::string to_string() const override { return {}; } bool isAppendable() override { return false; } }; diff --git a/tests/unit/libutil/paths-setting.cc b/tests/unit/libutil/paths-setting.cc index 17cb125c8..2d37ad525 100644 --- a/tests/unit/libutil/paths-setting.cc +++ b/tests/unit/libutil/paths-setting.cc @@ -11,14 +11,13 @@ namespace nix { class PathsSettingTestConfig : public Config { public: - PathsSettingTestConfig() - : Config() - { } + PathsSettingTestConfig() : Config() {} PathsSetting<Paths> paths{this, Paths(), "paths", "documentation"}; }; -struct PathsSettingTest : public ::testing::Test { +struct PathsSettingTest : public ::testing::Test +{ public: PathsSettingTestConfig mkConfig() { @@ -26,61 +25,99 @@ public: } }; -TEST_F(PathsSettingTest, parse) { +TEST_F(PathsSettingTest, parse) +{ auto config = mkConfig(); // Not an absolute path: - ASSERT_THROW(config.paths.parse("puppy.nix"), Error); + ASSERT_THROW(config.paths.parse("puppy.nix", {}), Error); - ASSERT_THAT( - config.paths.parse("/puppy.nix"), - Eq<Paths>({"/puppy.nix"}) - ); + ASSERT_THAT(config.paths.parse("/puppy.nix", {}), Eq<Paths>({"/puppy.nix"})); // Splits on whitespace: ASSERT_THAT( - config.paths.parse("/puppy.nix /doggy.nix"), - Eq<Paths>({"/puppy.nix", "/doggy.nix"}) + config.paths.parse("/puppy.nix /doggy.nix", {}), Eq<Paths>({"/puppy.nix", "/doggy.nix"}) ); // Splits on _any_ whitespace: ASSERT_THAT( - config.paths.parse("/puppy.nix \t /doggy.nix\n\n\n/borzoi.nix\r/goldie.nix"), + config.paths.parse("/puppy.nix \t /doggy.nix\n\n\n/borzoi.nix\r/goldie.nix", {}), Eq<Paths>({"/puppy.nix", "/doggy.nix", "/borzoi.nix", "/goldie.nix"}) ); // Canonicizes paths: + ASSERT_THAT(config.paths.parse("/puppy/../doggy.nix", {}), Eq<Paths>({"/doggy.nix"})); +} + +TEST_F(PathsSettingTest, parseRelative) +{ + auto options = ApplyConfigOptions{.path = "/doggy/kinds/config.nix"}; + auto config = mkConfig(); + ASSERT_THAT( + config.paths.parse("puppy.nix", options), + Eq<Paths>({"/doggy/kinds/puppy.nix"}) + ); + + // Splits on whitespace: ASSERT_THAT( - config.paths.parse("/puppy/../doggy.nix"), - Eq<Paths>({"/doggy.nix"}) + config.paths.parse("puppy.nix /doggy.nix", options), Eq<Paths>({"/doggy/kinds/puppy.nix", "/doggy.nix"}) ); + + // Canonicizes paths: + ASSERT_THAT(config.paths.parse("../soft.nix", options), Eq<Paths>({"/doggy/soft.nix"})); + + // Canonicizes paths: + ASSERT_THAT(config.paths.parse("./soft.nix", options), Eq<Paths>({"/doggy/kinds/soft.nix"})); } -TEST_F(PathsSettingTest, append) { +TEST_F(PathsSettingTest, parseHome) +{ + auto options = ApplyConfigOptions{ + .path = "/doggy/kinds/config.nix", + .home = "/home/puppy" + }; auto config = mkConfig(); - ASSERT_TRUE(config.paths.isAppendable()); + ASSERT_THAT( + config.paths.parse("puppy.nix", options), + Eq<Paths>({"/doggy/kinds/puppy.nix"}) + ); - // Starts with no paths: ASSERT_THAT( - config.paths.get(), - Eq<Paths>({}) + config.paths.parse("~/.config/nix/puppy.nix", options), + Eq<Paths>({"/home/puppy/.config/nix/puppy.nix"}) + ); + + // Splits on whitespace: + ASSERT_THAT( + config.paths.parse("~/puppy.nix ~/doggy.nix", options), + Eq<Paths>({"/home/puppy/puppy.nix", "/home/puppy/doggy.nix"}) ); + // Canonicizes paths: + ASSERT_THAT(config.paths.parse("~/../why.nix", options), Eq<Paths>({"/home/why.nix"})); + + // Home paths for other users not allowed. Needs to start with `~/`. + ASSERT_THROW(config.paths.parse("~root/config.nix", options), Error); +} + +TEST_F(PathsSettingTest, append) +{ + auto config = mkConfig(); + + ASSERT_TRUE(config.paths.isAppendable()); + + // Starts with no paths: + ASSERT_THAT(config.paths.get(), Eq<Paths>({})); + // Can append a path: config.paths.set("/puppy.nix", true); - ASSERT_THAT( - config.paths.get(), - Eq<Paths>({"/puppy.nix"}) - ); + ASSERT_THAT(config.paths.get(), Eq<Paths>({"/puppy.nix"})); // Can append multiple paths: config.paths.set("/silly.nix /doggy.nix", true); - ASSERT_THAT( - config.paths.get(), - Eq<Paths>({"/puppy.nix", "/silly.nix", "/doggy.nix"}) - ); + ASSERT_THAT(config.paths.get(), Eq<Paths>({"/puppy.nix", "/silly.nix", "/doggy.nix"})); } } // namespace nix |