From 49d61b2e4bf338042364c85d3c2ead0b33963e65 Mon Sep 17 00:00:00 2001 From: piegames Date: Sat, 13 Jul 2024 05:24:41 +0200 Subject: [PATCH] libexpr: Introduce Deprecated features They are like experimental features, but opt-in instead of opt-out. They will allow us to gracefully remove language features. See #437 Change-Id: I9ca04cc48e6926750c4d622c2b229b25cc142c42 --- ...st.nix => generate-features-shortlist.nix} | 7 +- doc/manual/generate-features.nix | 18 +++ doc/manual/generate-xp-features.nix | 13 --- doc/manual/meson.build | 6 +- doc/manual/src/SUMMARY.md | 1 + doc/manual/src/command-ref/meson.build | 25 +++- .../src/contributing/deprecated-features.md | 37 ++++++ doc/manual/src/contributing/meson.build | 17 ++- src/libexpr/eval.cc | 17 ++- src/libexpr/eval.hh | 15 ++- src/libexpr/parser/parser.cc | 8 +- src/libexpr/parser/state.hh | 2 +- src/libutil/config-impl.hh | 6 + src/libutil/config.cc | 62 +++++++++- src/libutil/config.hh | 43 ++++++- src/libutil/deprecated-features-json.hh | 29 +++++ src/libutil/deprecated-features.cc | 108 ++++++++++++++++++ src/libutil/deprecated-features.hh | 69 +++++++++++ src/libutil/meson.build | 3 + src/nix/main.cc | 6 + tests/unit/libexpr-support/tests/libexpr.hh | 4 +- tests/unit/libexpr/trivial.cc | 26 ++--- 22 files changed, 468 insertions(+), 54 deletions(-) rename doc/manual/{generate-xp-features-shortlist.nix => generate-features-shortlist.nix} (52%) create mode 100644 doc/manual/generate-features.nix delete mode 100644 doc/manual/generate-xp-features.nix create mode 100644 doc/manual/src/contributing/deprecated-features.md create mode 100644 src/libutil/deprecated-features-json.hh create mode 100644 src/libutil/deprecated-features.cc create mode 100644 src/libutil/deprecated-features.hh diff --git a/doc/manual/generate-xp-features-shortlist.nix b/doc/manual/generate-features-shortlist.nix similarity index 52% rename from doc/manual/generate-xp-features-shortlist.nix rename to doc/manual/generate-features-shortlist.nix index ea8bf8d49..055698d64 100644 --- a/doc/manual/generate-xp-features-shortlist.nix +++ b/doc/manual/generate-features-shortlist.nix @@ -1,9 +1,14 @@ +# Usually "experimental" or "deprecated" +kind: +# "xp" or "dp" +kindShort: + with builtins; with import ./utils.nix; let showExperimentalFeature = name: doc: '' - - [`${name}`](@docroot@/contributing/experimental-features.md#xp-feature-${name}) + - [`${name}`](@docroot@/contributing/${kind}-features.md#${kindShort}-feature-${name}) ''; in xps: indent " " (concatStrings (attrValues (mapAttrs showExperimentalFeature xps))) diff --git a/doc/manual/generate-features.nix b/doc/manual/generate-features.nix new file mode 100644 index 000000000..4a12ccdce --- /dev/null +++ b/doc/manual/generate-features.nix @@ -0,0 +1,18 @@ +# Usually "experimental" or "deprecated" +_kind: +# "xp" or "dp" +kindShort: + +with builtins; +with import ./utils.nix; + +let + showFeature = + name: doc: + squash '' + ## [`${name}`]{#${kindShort}-feature-${name}} + + ${doc} + ''; +in +xps: (concatStringsSep "\n" (attrValues (mapAttrs showFeature xps))) diff --git a/doc/manual/generate-xp-features.nix b/doc/manual/generate-xp-features.nix deleted file mode 100644 index c56ddeea1..000000000 --- a/doc/manual/generate-xp-features.nix +++ /dev/null @@ -1,13 +0,0 @@ -with builtins; -with import ./utils.nix; - -let - showExperimentalFeature = - name: doc: - squash '' - ## [`${name}`]{#xp-feature-${name}} - - ${doc} - ''; -in -xps: (concatStringsSep "\n" (attrValues (mapAttrs showExperimentalFeature xps))) diff --git a/doc/manual/meson.build b/doc/manual/meson.build index 673c86149..1bdbba77e 100644 --- a/doc/manual/meson.build +++ b/doc/manual/meson.build @@ -72,9 +72,9 @@ generate_manual_deps = files( # Generates builtins.md and builtin-constants.md. subdir('src/language') -# Generates new-cli pages, experimental-features-shortlist.md, and conf-file.md. +# Generates new-cli pages, {experimental,deprecated}-features-shortlist.md, and conf-file.md. subdir('src/command-ref') -# Generates experimental-feature-descriptions.md. +# Generates {experimental,deprecated}-feature-descriptions.md. subdir('src/contributing') # Generates rl-next-generated.md. subdir('src/release-notes') @@ -106,6 +106,8 @@ manual = custom_target( nix3_cli_files, experimental_features_shortlist_md, experimental_feature_descriptions_md, + deprecated_features_shortlist_md, + deprecated_feature_descriptions_md, conf_file_md, builtins_md, builtin_constants_md, diff --git a/doc/manual/src/SUMMARY.md b/doc/manual/src/SUMMARY.md index 4fac7cbca..03dc7e198 100644 --- a/doc/manual/src/SUMMARY.md +++ b/doc/manual/src/SUMMARY.md @@ -192,6 +192,7 @@ - [Hacking](contributing/hacking.md) - [Testing](contributing/testing.md) - [Experimental Features](contributing/experimental-features.md) + - [Deprecated Features](contributing/deprecated-features.md) - [CLI guideline](contributing/cli-guideline.md) - [C++ style guide](contributing/cxx.md) - [Release Notes](release-notes/release-notes.md) diff --git a/doc/manual/src/command-ref/meson.build b/doc/manual/src/command-ref/meson.build index 03d5f0a9c..ef6f9d2e0 100644 --- a/doc/manual/src/command-ref/meson.build +++ b/doc/manual/src/command-ref/meson.build @@ -7,10 +7,10 @@ xp_features_json = custom_target( experimental_features_shortlist_md = custom_target( command : nix_eval_for_docs + [ '--expr', - 'import @INPUT0@ (builtins.fromJSON (builtins.readFile @INPUT1@))', + 'import @INPUT0@ "experimental" "xp" (builtins.fromJSON (builtins.readFile @INPUT1@))', ], input : [ - '../../generate-xp-features-shortlist.nix', + '../../generate-features-shortlist.nix', xp_features_json, ], capture : true, @@ -18,6 +18,26 @@ experimental_features_shortlist_md = custom_target( env : nix_env_for_docs, ) +dp_features_json = custom_target( + command : [nix, '__dump-dp-features'], + capture : true, + output : 'dp-features.json', +) + +deprecated_features_shortlist_md = custom_target( + command : nix_eval_for_docs + [ + '--expr', + 'import @INPUT0@ "deprecated" "dp" (builtins.fromJSON (builtins.readFile @INPUT1@))', + ], + input : [ + '../../generate-features-shortlist.nix', + dp_features_json, + ], + capture : true, + output : 'deprecated-features-shortlist.md', + env : nix_env_for_docs, +) + # Intermediate step for manpage generation. # This splorks the output of generate-manpage.nix as JSON, # which gets written as a directory tree below. @@ -60,6 +80,7 @@ conf_file_md = custom_target( '../../utils.nix', conf_file_json, experimental_features_shortlist_md, + deprecated_features_shortlist_md, ], output : 'conf-file.md', env : nix_env_for_docs, diff --git a/doc/manual/src/contributing/deprecated-features.md b/doc/manual/src/contributing/deprecated-features.md new file mode 100644 index 000000000..7536944f7 --- /dev/null +++ b/doc/manual/src/contributing/deprecated-features.md @@ -0,0 +1,37 @@ +This section describes the notion of *deprecated features*, and how it fits into the big picture of the development of Lix. + +# What are deprecated features? + +Deprecated features are disabled by default, with the intent to eventually remove them. +Users must explicitly enable them to keep using them, by toggling the associated [deprecated feature flags](@docroot@/command-ref/conf-file.md#conf-deprecated-features). +This allows backwards compatibility and a graceful transition away from undesired features. + +# Which features can be deprecated? + +Undesired features should be soft-deprecated by yielding a warning when used for a significant amount of time before the can be deprecated. +Legacy obsolete feature with little to no usage may go through this process faster. +Deprecated features should have a migration path to a preferred alternative. + +# Lifecycle of a deprecated feature + +This description is not normative, but a feature removal may roughly happen like this: + +1. Add a warning when the feature is being used. +2. Disable the feature by default, putting it behind a deprecated feature flag. + - If disabling the feature started out as an opt-in experimental feature, turn that experimental flag into a no-op or remove it entirely. + For example, `--extra-experimental-features=no-url-literals` becomes `--extra-deprecated-features=url-literals`. +3. Decide on a time frame for how long that feature will still be supported for backwards compatibility, and clearly communicate that in the error messages. + - Sometimes, automatic migration to alternatives is possible, and such should be provided if possible + - At least one NixOS release cycle should be the minimum +4. Finally remove the feature entirely, only keeping the error message for those still using it. + +# Relation to language versioning + +Obviously, removing anything breaks backwards compatibility. +In an ideal world, we'd have SemVer controls over the language and its features, cleanly allowing us to make breaking changes. +See https://wiki.lix.systems/books/lix-contributors/page/language-versioning and [RFC 137](https://github.com/nixos/rfcs/pull/137) for efforts on that. +However, we do not live in such an ideal world, and currently this goal is so far away, that "just disable it with some back-compat for a couple of years" is the most realistic solution, especially for comparatively minor changes. + +# Currently available deprecated features + +{{#include @generated@/contributing/deprecated-feature-descriptions.md}} diff --git a/doc/manual/src/contributing/meson.build b/doc/manual/src/contributing/meson.build index 2929578c8..8c76a6206 100644 --- a/doc/manual/src/contributing/meson.build +++ b/doc/manual/src/contributing/meson.build @@ -4,12 +4,25 @@ experimental_feature_descriptions_md = custom_target( command : nix_eval_for_docs + [ '--expr', - 'import @INPUT0@ (builtins.fromJSON (builtins.readFile @INPUT1@))', + 'import @INPUT0@ "experimental" "xp" (builtins.fromJSON (builtins.readFile @INPUT1@))', ], input : [ - '../../generate-xp-features.nix', + '../../generate-features.nix', xp_features_json, ], capture : true, output : 'experimental-feature-descriptions.md', ) + +deprecated_feature_descriptions_md = custom_target( + command : nix_eval_for_docs + [ + '--expr', + 'import @INPUT0@ "deprecated" "dp" (builtins.fromJSON (builtins.readFile @INPUT1@))', + ], + input : [ + '../../generate-features.nix', + dp_features_json, + ], + capture : true, + output : 'deprecated-feature-descriptions.md', +) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 702b9b6ac..fcc28d1ca 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2710,20 +2710,29 @@ Expr & EvalState::parseExprFromFile(const SourcePath & path, std::shared_ptr & staticEnv, const ExperimentalFeatureSettings & xpSettings) +Expr & EvalState::parseExprFromString( + std::string s_, + const SourcePath & basePath, + std::shared_ptr & staticEnv, + const FeatureSettings & featureSettings +) { // NOTE this method (and parseStdin) must take care to *fully copy* their input // into their respective Pos::Origin until the parser stops overwriting its input // data. auto s = make_ref(s_); s_.append("\0\0", 2); - return *parse(s_.data(), s_.size(), Pos::String{.source = s}, basePath, staticEnv, xpSettings); + return *parse(s_.data(), s_.size(), Pos::String{.source = s}, basePath, staticEnv, featureSettings); } -Expr & EvalState::parseExprFromString(std::string s, const SourcePath & basePath, const ExperimentalFeatureSettings & xpSettings) +Expr & EvalState::parseExprFromString( + std::string s, + const SourcePath & basePath, + const FeatureSettings & featureSettings +) { - return parseExprFromString(std::move(s), basePath, staticBaseEnv, xpSettings); + return parseExprFromString(std::move(s), basePath, staticBaseEnv, featureSettings); } diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index ff45efc08..eab1f22ef 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -344,8 +344,17 @@ public: /** * Parse a Nix expression from the specified string. */ - Expr & parseExprFromString(std::string s, const SourcePath & basePath, std::shared_ptr & staticEnv, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); - Expr & parseExprFromString(std::string s, const SourcePath & basePath, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); + Expr & parseExprFromString( + std::string s, + const SourcePath & basePath, + std::shared_ptr & staticEnv, + const FeatureSettings & xpSettings = featureSettings + ); + Expr & parseExprFromString( + std::string s, + const SourcePath & basePath, + const FeatureSettings & xpSettings = featureSettings + ); Expr & parseStdin(); @@ -569,7 +578,7 @@ private: Pos::Origin origin, const SourcePath & basePath, std::shared_ptr & staticEnv, - const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); + const FeatureSettings & xpSettings = featureSettings); /** * Current Nix call stack depth, used with `max-call-depth` setting to throw stack overflow hopefully before we run out of system stack. diff --git a/src/libexpr/parser/parser.cc b/src/libexpr/parser/parser.cc index 6d496d141..e45776ca6 100644 --- a/src/libexpr/parser/parser.cc +++ b/src/libexpr/parser/parser.cc @@ -115,7 +115,7 @@ struct ExprState std::unique_ptr pipe(PosIdx pos, State & state, bool flip = false) { - if (!state.xpSettings.isEnabled(Xp::PipeOperator)) + if (!state.featureSettings.isEnabled(Xp::PipeOperator)) throw ParseError({ .msg = HintFmt("Pipe operator is disabled"), .pos = state.positions[pos] @@ -656,7 +656,7 @@ template<> struct BuildAST : p::maybe_nothing {}; template<> struct BuildAST { static void apply(const auto & in, ExprState & s, State & ps) { - bool noURLLiterals = ps.xpSettings.isEnabled(Xp::NoUrlLiterals); + bool noURLLiterals = ps.featureSettings.isEnabled(Xp::NoUrlLiterals); if (noURLLiterals) throw ParseError({ .msg = HintFmt("URL literals are disabled"), @@ -858,7 +858,7 @@ Expr * EvalState::parse( Pos::Origin origin, const SourcePath & basePath, std::shared_ptr & staticEnv, - const ExperimentalFeatureSettings & xpSettings) + const FeatureSettings & featureSettings) { parser::State s = { symbols, @@ -866,7 +866,7 @@ Expr * EvalState::parse( basePath, positions.addOrigin(origin, length), exprSymbols, - xpSettings + featureSettings, }; parser::ExprState x; diff --git a/src/libexpr/parser/state.hh b/src/libexpr/parser/state.hh index 30803a37e..9dddd28e1 100644 --- a/src/libexpr/parser/state.hh +++ b/src/libexpr/parser/state.hh @@ -19,7 +19,7 @@ struct State SourcePath basePath; PosTable::Origin origin; const Expr::AstSymbols & s; - const ExperimentalFeatureSettings & xpSettings; + const FeatureSettings & featureSettings; void dupAttr(const AttrPath & attrPath, const PosIdx pos, const PosIdx prevPos); void dupAttr(Symbol attr, const PosIdx pos, const PosIdx prevPos); diff --git a/src/libutil/config-impl.hh b/src/libutil/config-impl.hh index 8e3a1e408..bc88b5504 100644 --- a/src/libutil/config-impl.hh +++ b/src/libutil/config-impl.hh @@ -34,6 +34,10 @@ template<> struct BaseSetting>::trait { static constexpr bool appendable = true; }; +template<> struct BaseSetting>::trait +{ + static constexpr bool appendable = true; +}; template struct BaseSetting::trait @@ -51,6 +55,7 @@ template<> void BaseSetting::appendOrSet(Strings newValue, bool append) template<> void BaseSetting::appendOrSet(StringSet newValue, bool append); template<> void BaseSetting::appendOrSet(StringMap newValue, bool append); template<> void BaseSetting>::appendOrSet(std::set newValue, bool append); +template<> void BaseSetting>::appendOrSet(std::set newValue, bool append); template void BaseSetting::appendOrSet(T newValue, bool append) @@ -116,6 +121,7 @@ DECLARE_CONFIG_SERIALISER(Strings) DECLARE_CONFIG_SERIALISER(StringSet) DECLARE_CONFIG_SERIALISER(StringMap) DECLARE_CONFIG_SERIALISER(std::set) +DECLARE_CONFIG_SERIALISER(std::set) template T BaseSetting::parse(const std::string & str) const diff --git a/src/libutil/config.cc b/src/libutil/config.cc index 8180886ce..3371e0bb3 100644 --- a/src/libutil/config.cc +++ b/src/libutil/config.cc @@ -2,6 +2,7 @@ #include "args.hh" #include "abstract-setting-to-json.hh" #include "experimental-features.hh" +#include "deprecated-features.hh" #include "file-system.hh" #include "logging.hh" #include "strings.hh" @@ -355,6 +356,32 @@ template<> std::string BaseSetting>::to_string() c return concatStringsSep(" ", stringifiedXpFeatures); } +template<> std::set BaseSetting>::parse(const std::string & str) const +{ + std::set res; + for (auto & s : tokenizeString(str)) { + if (auto thisDpFeature = parseDeprecatedFeature(s); thisDpFeature) + res.insert(thisDpFeature.value()); + else + warn("unknown deprecated feature '%s'", s); + } + return res; +} + +template<> void BaseSetting>::appendOrSet(std::set newValue, bool append) +{ + if (!append) value.clear(); + value.insert(std::make_move_iterator(newValue.begin()), std::make_move_iterator(newValue.end())); +} + +template<> std::string BaseSetting>::to_string() const +{ + StringSet stringifiedDpFeatures; + for (const auto & feature : value) + stringifiedDpFeatures.insert(std::string(showDeprecatedFeature(feature))); + return concatStringsSep(" ", stringifiedDpFeatures); +} + template<> StringMap BaseSetting::parse(const std::string & str) const { StringMap res; @@ -391,6 +418,7 @@ template class BaseSetting; template class BaseSetting; template class BaseSetting; template class BaseSetting>; +template class BaseSetting>; static Path parsePath(const AbstractSetting & s, const std::string & str) { @@ -525,28 +553,52 @@ GlobalConfig::Register::Register(Config * config) configRegistrations->emplace_back(config); } -ExperimentalFeatureSettings experimentalFeatureSettings; +FeatureSettings experimentalFeatureSettings; + +FeatureSettings& featureSettings = experimentalFeatureSettings; static GlobalConfig::Register rSettings(&experimentalFeatureSettings); -bool ExperimentalFeatureSettings::isEnabled(const ExperimentalFeature & feature) const +bool FeatureSettings::isEnabled(const ExperimentalFeature & feature) const { auto & f = experimentalFeatures.get(); return std::find(f.begin(), f.end(), feature) != f.end(); } -void ExperimentalFeatureSettings::require(const ExperimentalFeature & feature) const +void FeatureSettings::require(const ExperimentalFeature & feature) const { if (!isEnabled(feature)) throw MissingExperimentalFeature(feature); } -bool ExperimentalFeatureSettings::isEnabled(const std::optional & feature) const +bool FeatureSettings::isEnabled(const std::optional & feature) const { return !feature || isEnabled(*feature); } -void ExperimentalFeatureSettings::require(const std::optional & feature) const +void FeatureSettings::require(const std::optional & feature) const +{ + if (feature) require(*feature); +} + +bool FeatureSettings::isEnabled(const DeprecatedFeature & feature) const +{ + auto & f = deprecatedFeatures.get(); + return std::find(f.begin(), f.end(), feature) != f.end(); +} + +void FeatureSettings::require(const DeprecatedFeature & feature) const +{ + if (!isEnabled(feature)) + throw MissingDeprecatedFeature(feature); +} + +bool FeatureSettings::isEnabled(const std::optional & feature) const +{ + return !feature || isEnabled(*feature); +} + +void FeatureSettings::require(const std::optional & feature) const { if (feature) require(*feature); } diff --git a/src/libutil/config.hh b/src/libutil/config.hh index d7bf9cdc9..a0ad125fb 100644 --- a/src/libutil/config.hh +++ b/src/libutil/config.hh @@ -9,6 +9,7 @@ #include "types.hh" #include "experimental-features.hh" +#include "deprecated-features.hh" namespace nix { @@ -441,7 +442,7 @@ struct GlobalConfig : public AbstractConfig extern GlobalConfig globalConfig; -struct ExperimentalFeatureSettings : Config { +struct FeatureSettings : Config { Setting> experimentalFeatures{ this, {}, "experimental-features", @@ -483,9 +484,47 @@ struct ExperimentalFeatureSettings : Config { * disabled, and so the function does nothing in that case. */ void require(const std::optional &) const; + + Setting> deprecatedFeatures{ + this, {}, "deprecated-features", + R"( + Deprecated features that are allowed. (Currently there are none.) + + The following deprecated feature features can be re-activated: + + {{#include @generated@/command-ref/deprecated-features-shortlist.md}} + + Deprecated features are [further documented in the manual](@docroot@/contributing/deprecated-features.md). + )"}; + + /** + * Check whether the given deprecated feature is enabled. + */ + bool isEnabled(const DeprecatedFeature &) const; + + /** + * Require an deprecated feature be enabled, throwing an error if it is + * not. + */ + void require(const DeprecatedFeature &) const; + + /** + * `std::nullopt` pointer means no feature, which means there is nothing that could be + * disabled, and so the function returns true in that case. + */ + bool isEnabled(const std::optional &) const; + + /** + * `std::nullopt` pointer means no feature, which means there is nothing that could be + * disabled, and so the function does nothing in that case. + */ + void require(const std::optional &) const; }; // FIXME: don't use a global variable. -extern ExperimentalFeatureSettings experimentalFeatureSettings; +extern FeatureSettings& featureSettings; +// Aliases to `featureSettings` for not having to change the name in the code everywhere +using ExperimentalFeatureSettings = FeatureSettings; +extern FeatureSettings experimentalFeatureSettings; } diff --git a/src/libutil/deprecated-features-json.hh b/src/libutil/deprecated-features-json.hh new file mode 100644 index 000000000..787be40dc --- /dev/null +++ b/src/libutil/deprecated-features-json.hh @@ -0,0 +1,29 @@ +#pragma once +///@file + +#include "deprecated-features.hh" +#include "json-utils.hh" + +namespace nix { + +/** + * Compute the documentation of all deprecated features. + * + * See `doc/manual` for how this information is used. + */ +nlohmann::json documentDeprecatedFeatures(); + +/** + * Semi-magic conversion to and from json. + * See the nlohmann/json readme for more details. + */ +void to_json(nlohmann::json &, const DeprecatedFeature &); +void from_json(const nlohmann::json &, DeprecatedFeature &); + +/** + * It is always rendered as a string + */ +template<> +struct json_avoids_null : std::true_type {}; + +}; diff --git a/src/libutil/deprecated-features.cc b/src/libutil/deprecated-features.cc new file mode 100644 index 000000000..7c59d8598 --- /dev/null +++ b/src/libutil/deprecated-features.cc @@ -0,0 +1,108 @@ +#include "deprecated-features.hh" +// Required for instances of to_json and from_json for DeprecatedFeature +#include "deprecated-features-json.hh" +#include "strings.hh" + +#include "nlohmann/json.hpp" + +namespace nix { + +struct DeprecatedFeatureDetails +{ + DeprecatedFeature tag; + std::string_view name; + std::string_view description; +}; + +/** + * If two different PRs both add a deprecated feature, and we just + * used a number for this, we *woudln't* get merge conflict and the + * counter will be incremented once instead of twice, causing a build + * failure. + * + * By instead defining this instead as 1 + the bottom deprecated + * feature, we either have no issue at all if few features are not added + * at the end of the list, or a proper merge conflict if they are. + */ +constexpr size_t numDepFeatures = 0; + +constexpr std::array depFeatureDetails = {{ +}}; + +static_assert( + []() constexpr { + for (auto [index, feature] : enumerate(depFeatureDetails)) + if (index != (size_t)feature.tag) + return false; + return true; + }(), + "array order does not match enum tag order"); + +const std::optional parseDeprecatedFeature(const std::string_view & name) +{ + using ReverseDepMap = std::map; + + static std::unique_ptr reverseDepMap = []() { + auto reverseDepMap = std::make_unique(); + for (auto & depFeature : depFeatureDetails) + (*reverseDepMap)[depFeature.name] = depFeature.tag; + return reverseDepMap; + }(); + + if (auto feature = get(*reverseDepMap, name)) + return *feature; + else + return std::nullopt; +} + +std::string_view showDeprecatedFeature(const DeprecatedFeature tag) +{ + assert((size_t)tag < depFeatureDetails.size()); + return depFeatureDetails[(size_t)tag].name; +} + +nlohmann::json documentDeprecatedFeatures() +{ + StringMap res; + for (auto & depFeature : depFeatureDetails) + res[std::string { depFeature.name }] = + trim(stripIndentation(depFeature.description)); + return (nlohmann::json) res; +} + +std::set parseDeprecatedFeatures(const std::set & rawFeatures) +{ + std::set res; + for (auto & rawFeature : rawFeatures) + if (auto feature = parseDeprecatedFeature(rawFeature)) + res.insert(*feature); + return res; +} + +MissingDeprecatedFeature::MissingDeprecatedFeature(DeprecatedFeature feature) + : Error("Lix feature '%1%' is deprecated and should not be used anymore; use '--extra-deprecated-features %1%' to disable this error", showDeprecatedFeature(feature)) + , missingFeature(feature) +{} + +std::ostream & operator <<(std::ostream & str, const DeprecatedFeature & feature) +{ + return str << showDeprecatedFeature(feature); +} + +void to_json(nlohmann::json & j, const DeprecatedFeature & feature) +{ + j = showDeprecatedFeature(feature); +} + +void from_json(const nlohmann::json & j, DeprecatedFeature & feature) +{ + const std::string input = j; + const auto parsed = parseDeprecatedFeature(input); + + if (parsed.has_value()) + feature = *parsed; + else + throw Error("Unknown deprecated feature '%s' in JSON input", input); +} + +} diff --git a/src/libutil/deprecated-features.hh b/src/libutil/deprecated-features.hh new file mode 100644 index 000000000..86a9b8a5a --- /dev/null +++ b/src/libutil/deprecated-features.hh @@ -0,0 +1,69 @@ +#pragma once +///@file + +#include "error.hh" +#include "types.hh" + +namespace nix { + +/** + * The list of available deprecated features. + * + * If you update this, don’t forget to also change the map defining + * their string representation and documentation in the corresponding + * `.cc` file as well. + * + * Reminder: New deprecated features should start out with a warning without throwing an error. + * See the developer documentation for details. + */ +enum struct DeprecatedFeature +{ +}; + +/** + * Just because writing `DeprecatedFeature::UrlLiterals` is way too long + */ +using Dep = DeprecatedFeature; + +/** + * Parse a deprecated feature (enum value) from its name. Deprecated + * feature flag names are hyphenated and do not contain spaces. + */ +const std::optional parseDeprecatedFeature( + const std::string_view & name); + +/** + * Show the name of a deprecated feature. This is the opposite of + * parseDeprecatedFeature(). + */ +std::string_view showDeprecatedFeature(const DeprecatedFeature); + +/** + * Shorthand for `str << showDeprecatedFeature(feature)`. + */ +std::ostream & operator<<( + std::ostream & str, + const DeprecatedFeature & feature); + +/** + * Parse a set of strings to the corresponding set of deprecated + * features, ignoring (but warning for) any unknown feature. + */ +std::set parseDeprecatedFeatures(const std::set &); + +/** + * A deprecated feature used for some + * operation, but was not enabled. + */ +class MissingDeprecatedFeature : public Error +{ +public: + /** + * The deprecated feature that was required but not enabled. + */ + DeprecatedFeature missingFeature; + + MissingDeprecatedFeature(DeprecatedFeature missingFeature); +}; + +} diff --git a/src/libutil/meson.build b/src/libutil/meson.build index 41336874b..3e10e4b63 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -7,6 +7,7 @@ libutil_sources = files( 'compute-levels.cc', 'config.cc', 'current-process.cc', + 'deprecated-features.cc', 'english.cc', 'environment-variables.cc', 'error.cc', @@ -64,6 +65,8 @@ libutil_headers = files( 'config-impl.hh', 'config.hh', 'current-process.hh', + 'deprecated-features.hh', + 'deprecated-features-json.hh', 'english.hh', 'environment-variables.hh', 'error.hh', diff --git a/src/nix/main.cc b/src/nix/main.cc index 9cbe303ac..5356a0d04 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -14,6 +14,7 @@ #include "loggers.hh" #include "markdown.hh" #include "experimental-features-json.hh" +#include "deprecated-features-json.hh" #include #include @@ -422,6 +423,11 @@ void mainWrapped(int argc, char * * argv) return; } + if (argc == 2 && std::string(argv[1]) == "__dump-dp-features") { + logger->cout(documentDeprecatedFeatures().dump()); + return; + } + Finally printCompletions([&]() { if (args.completions) { diff --git a/tests/unit/libexpr-support/tests/libexpr.hh b/tests/unit/libexpr-support/tests/libexpr.hh index 745aa168d..642632baa 100644 --- a/tests/unit/libexpr-support/tests/libexpr.hh +++ b/tests/unit/libexpr-support/tests/libexpr.hh @@ -26,9 +26,9 @@ namespace nix { , state({}, store) { } - Value eval(std::string input, bool forceValue = true, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings) { + Value eval(std::string input, bool forceValue = true, const FeatureSettings & fSettings = featureSettings) { Value v; - Expr & e = state.parseExprFromString(input, state.rootPath(CanonPath::root), xpSettings); + Expr & e = state.parseExprFromString(input, state.rootPath(CanonPath::root), fSettings); state.eval(e, v); if (forceValue) state.forceValue(v, noPos); diff --git a/tests/unit/libexpr/trivial.cc b/tests/unit/libexpr/trivial.cc index c984657fd..46f9b7499 100644 --- a/tests/unit/libexpr/trivial.cc +++ b/tests/unit/libexpr/trivial.cc @@ -214,36 +214,36 @@ namespace nix { // pipes are gated behind an experimental feature flag TEST_F(TrivialExpressionTest, pipeDisabled) { ASSERT_THROW(eval("let add = l: r: l + r; in ''a'' |> add ''b''"), Error); - ASSERT_THROW(eval("let add = l: r: l + r; in ''a'' <| add ''b''"), Error); + ASSERT_THROW(eval("let add = l: r: l + r; in add ''a'' <| ''b''"), Error); } TEST_F(TrivialExpressionTest, pipeRight) { - ExperimentalFeatureSettings mockXpSettings; - mockXpSettings.set("experimental-features", "pipe-operator"); + FeatureSettings mockFeatureSettings; + mockFeatureSettings.set("experimental-features", "pipe-operator"); - auto v = eval("let add = l: r: l + r; in ''a'' |> add ''b''", true, mockXpSettings); + auto v = eval("let add = l: r: l + r; in ''a'' |> add ''b''", true, mockFeatureSettings); ASSERT_THAT(v, IsStringEq("ba")); - v = eval("let add = l: r: l + r; in ''a'' |> add ''b'' |> add ''c''", true, mockXpSettings); + v = eval("let add = l: r: l + r; in ''a'' |> add ''b'' |> add ''c''", true, mockFeatureSettings); ASSERT_THAT(v, IsStringEq("cba")); } TEST_F(TrivialExpressionTest, pipeLeft) { - ExperimentalFeatureSettings mockXpSettings; - mockXpSettings.set("experimental-features", "pipe-operator"); + FeatureSettings mockFeatureSettings; + mockFeatureSettings.set("experimental-features", "pipe-operator"); - auto v = eval("let add = l: r: l + r; in add ''a'' <| ''b''", true, mockXpSettings); + auto v = eval("let add = l: r: l + r; in add ''a'' <| ''b''", true, mockFeatureSettings); ASSERT_THAT(v, IsStringEq("ab")); - v = eval("let add = l: r: l + r; in add ''a'' <| add ''b'' <| ''c''", true, mockXpSettings); + v = eval("let add = l: r: l + r; in add ''a'' <| add ''b'' <| ''c''", true, mockFeatureSettings); ASSERT_THAT(v, IsStringEq("abc")); } TEST_F(TrivialExpressionTest, pipeMixed) { - ExperimentalFeatureSettings mockXpSettings; - mockXpSettings.set("experimental-features", "pipe-operator"); + FeatureSettings mockFeatureSettings; + mockFeatureSettings.set("experimental-features", "pipe-operator"); - auto v = eval("let add = l: r: l + r; in add ''a'' <| ''b'' |> add ''c''", true, mockXpSettings); + auto v = eval("let add = l: r: l + r; in add ''a'' <| ''b'' |> add ''c''", true, mockFeatureSettings); ASSERT_THAT(v, IsStringEq("acb")); - v = eval("let add = l: r: l + r; in ''a'' |> add <| ''c''", true, mockXpSettings); + v = eval("let add = l: r: l + r; in ''a'' |> add <| ''c''", true, mockFeatureSettings); ASSERT_THAT(v, IsStringEq("ac")); } } /* namespace nix */