From d261304397e35264a67dff3c05e8d3af321b832e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 5 Feb 2026 00:11:20 +0100 Subject: [PATCH] add support for deprecated features Co-authored-by: piegames Co-authored-by: eldritch horrors --- doc/manual/generate-dp-features-shortlist.nix | 9 ++ doc/manual/generate-dp-features.nix | 14 +++ doc/manual/meson.build | 8 +- doc/manual/source/SUMMARY.md.in | 1 + doc/manual/source/command-ref/meson.build | 20 ++++ .../source/development/deprecated-features.md | 11 ++ doc/manual/source/development/meson.build | 13 +++ src/libutil/config-global.cc | 2 +- src/libutil/configuration.cc | 63 ++++++++++- src/libutil/deprecated-features.cc | 100 ++++++++++++++++++ src/libutil/include/nix/util/config-impl.hh | 9 ++ src/libutil/include/nix/util/configuration.hh | 43 +++++++- .../include/nix/util/deprecated-features.hh | 90 ++++++++++++++++ src/libutil/include/nix/util/meson.build | 1 + src/libutil/meson.build | 1 + src/nix/main.cc | 6 ++ 16 files changed, 383 insertions(+), 8 deletions(-) create mode 100644 doc/manual/generate-dp-features-shortlist.nix create mode 100644 doc/manual/generate-dp-features.nix create mode 100644 doc/manual/source/development/deprecated-features.md create mode 100644 src/libutil/deprecated-features.cc create mode 100644 src/libutil/include/nix/util/deprecated-features.hh diff --git a/doc/manual/generate-dp-features-shortlist.nix b/doc/manual/generate-dp-features-shortlist.nix new file mode 100644 index 00000000000..05226039953 --- /dev/null +++ b/doc/manual/generate-dp-features-shortlist.nix @@ -0,0 +1,9 @@ +with builtins; +with import ; + +let + showDeprecatedFeature = name: doc: '' + - [`${name}`](@docroot@/development/deprecated-features.md#dp-feature-${name}) + ''; +in +dps: indent " " (concatStrings (attrValues (mapAttrs showDeprecatedFeature dps))) diff --git a/doc/manual/generate-dp-features.nix b/doc/manual/generate-dp-features.nix new file mode 100644 index 00000000000..73eb9f806ae --- /dev/null +++ b/doc/manual/generate-dp-features.nix @@ -0,0 +1,14 @@ +with builtins; +with import ; + +let + showDeprecatedFeature = + name: doc: + squash '' + ## [`${name}`]{#dp-feature-${name}} + + ${doc} + ''; +in + +dps: (concatStringsSep "\n" (attrValues (mapAttrs showDeprecatedFeature dps))) diff --git a/doc/manual/meson.build b/doc/manual/meson.build index 6fd841e80cb..c2244a86314 100644 --- a/doc/manual/meson.build +++ b/doc/manual/meson.build @@ -154,6 +154,8 @@ if get_option('html-manual') nix3_cli_files, experimental_features_shortlist_md, experimental_feature_descriptions_md, + deprecated_features_shortlist_md, + deprecated_feature_descriptions_md, types_dir, conf_file_md, builtins_md, @@ -393,7 +395,11 @@ nix_manpages = [ 'nix.conf', 5, conf_file_md.full_path(), - [ conf_file_md, experimental_features_shortlist_md ], + [ + conf_file_md, + experimental_features_shortlist_md, + deprecated_features_shortlist_md, + ], ], [ 'nix-daemon', 8 ], [ 'nix-profiles', 5, 'files/profiles.md' ], diff --git a/doc/manual/source/SUMMARY.md.in b/doc/manual/source/SUMMARY.md.in index 05eed77837f..328690b88ce 100644 --- a/doc/manual/source/SUMMARY.md.in +++ b/doc/manual/source/SUMMARY.md.in @@ -150,6 +150,7 @@ - [JSON guideline](development/json-guideline.md) - [C++ style guide](development/cxx.md) - [Experimental Features](development/experimental-features.md) + - [Deprecated Features](development/deprecated-features.md) - [Contributing](development/contributing.md) - [Releases](release-notes/index.md) {{#include ./SUMMARY-rl-next.md}} diff --git a/doc/manual/source/command-ref/meson.build b/doc/manual/source/command-ref/meson.build index 06aed261a60..9d25b77238c 100644 --- a/doc/manual/source/command-ref/meson.build +++ b/doc/manual/source/command-ref/meson.build @@ -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', + env : nix_env_for_docs, +) + +deprecated_features_shortlist_md = custom_target( + command : nix_eval_for_docs + [ + '--expr', 'import @INPUT0@ (builtins.fromJSON (builtins.readFile ./@INPUT1@))', + ], + input : [ + '../../generate-dp-features-shortlist.nix', + dp_features_json, + ], + output : 'deprecated-features-shortlist.md', + capture : true, + env : nix_env_for_docs, +) + nix3_cli_files = custom_target( command : [ python.full_path(), '@INPUT0@', '@OUTPUT@', '--' ] + nix_eval_for_docs + [ '--expr', 'import @INPUT1@ true (builtins.readFile ./@INPUT2@)', diff --git a/doc/manual/source/development/deprecated-features.md b/doc/manual/source/development/deprecated-features.md new file mode 100644 index 00000000000..b766c336028 --- /dev/null +++ b/doc/manual/source/development/deprecated-features.md @@ -0,0 +1,11 @@ +This section describes the notion of *deprecated features*, and how it fits into the big picture of the development of Nix. + +# What are deprecated features? + +Deprecated features are legacy features that are scheduled for removal. +They are disabled by default but can be re-enabled by toggling the associated [deprecated feature flags](@docroot@/command-ref/conf-file.md#conf-deprecated-features). +This allows for a transition period where users can adapt their code. + +# Deprecated feature descriptions + +{{#include @generated@/development/deprecated-feature-descriptions.md}} diff --git a/doc/manual/source/development/meson.build b/doc/manual/source/development/meson.build index b3fb110230d..a474121e985 100644 --- a/doc/manual/source/development/meson.build +++ b/doc/manual/source/development/meson.build @@ -10,3 +10,16 @@ experimental_feature_descriptions_md = custom_target( env : nix_env_for_docs, output : 'experimental-feature-descriptions.md', ) + +deprecated_feature_descriptions_md = custom_target( + command : nix_eval_for_docs + [ + '--expr', 'import @INPUT0@ (builtins.fromJSON (builtins.readFile @INPUT1@))', + ], + input : [ + '../../generate-dp-features.nix', + dp_features_json, + ], + capture : true, + env : nix_env_for_docs, + output : 'deprecated-feature-descriptions.md', +) diff --git a/src/libutil/config-global.cc b/src/libutil/config-global.cc index b63b4aaa1bb..200c57e61c1 100644 --- a/src/libutil/config-global.cc +++ b/src/libutil/config-global.cc @@ -64,7 +64,7 @@ GlobalConfig::Register::Register(Config * config) configRegistrations().emplace_back(config); } -ExperimentalFeatureSettings experimentalFeatureSettings; +FeatureSettings experimentalFeatureSettings; static GlobalConfig::Register rSettings(&experimentalFeatureSettings); diff --git a/src/libutil/configuration.cc b/src/libutil/configuration.cc index 832099dab99..f52a623fa98 100644 --- a/src/libutil/configuration.cc +++ b/src/libutil/configuration.cc @@ -3,6 +3,7 @@ #include "nix/util/abstract-setting-to-json.hh" #include "nix/util/environment-variables.hh" #include "nix/util/experimental-features.hh" +#include "nix/util/deprecated-features.hh" #include "nix/util/util.hh" #include "nix/util/file-system.hh" @@ -425,6 +426,36 @@ std::string BaseSetting>::to_string() const 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 { @@ -505,6 +536,7 @@ template class BaseSetting; template class BaseSetting; template class BaseSetting; template class BaseSetting>; +template class BaseSetting>; template class BaseSetting; template class BaseSetting>; @@ -548,24 +580,47 @@ void OptionalPathSetting::operator=(const std::optional & v) this->assign(v); } -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, std::string reason) const +void FeatureSettings::require(const ExperimentalFeature & feature, std::string reason) const { if (!isEnabled(feature)) throw MissingExperimentalFeature(feature, std::move(reason)); } -bool ExperimentalFeatureSettings::isEnabled(const std::optional & feature) const +bool FeatureSettings::isEnabled(const std::optional & feature) const +{ + return !feature || isEnabled(*feature); +} + +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 ExperimentalFeatureSettings::require(const std::optional & feature) const +void FeatureSettings::require(const std::optional & feature) const { if (feature) require(*feature); diff --git a/src/libutil/deprecated-features.cc b/src/libutil/deprecated-features.cc new file mode 100644 index 00000000000..2cfd4a8265f --- /dev/null +++ b/src/libutil/deprecated-features.cc @@ -0,0 +1,100 @@ +#include "nix/util/deprecated-features.hh" +#include "nix/util/fmt.hh" +#include "nix/util/strings.hh" +#include "nix/util/util.hh" + +#include + +namespace nix { + +struct DeprecatedFeatureDetails +{ + DeprecatedFeature tag; + std::string_view name; + std::string_view description; +}; + +// Add features here +constexpr std::array depFeatureDetails = {{{ + .tag = DeprecatedFeature::UrlLiterals, + .name = "url-literals", + .description = R"( + Re-enable support for URL literals. + )", +}}}; + +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) +{ + for (const auto & detail : depFeatureDetails) { + if (detail.tag == tag) + return detail.name; + } + throw Error("Unknown deprecated feature tag"); +} + +nlohmann::json documentDeprecatedFeatures() +{ + std::map 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); + else + warn("unknown deprecated feature '%s'", rawFeature); + return res; +} + +MissingDeprecatedFeature::MissingDeprecatedFeature(DeprecatedFeature feature) + : Error( + "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); +} + +} // namespace nix diff --git a/src/libutil/include/nix/util/config-impl.hh b/src/libutil/include/nix/util/config-impl.hh index 8f6f9a358a4..68f5e89a5d2 100644 --- a/src/libutil/include/nix/util/config-impl.hh +++ b/src/libutil/include/nix/util/config-impl.hh @@ -44,6 +44,12 @@ struct BaseSetting>::trait static constexpr bool appendable = true; }; +template<> +struct BaseSetting>::trait +{ + static constexpr bool appendable = true; +}; + template struct BaseSetting::trait { @@ -64,6 +70,8 @@ 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) @@ -135,6 +143,7 @@ DECLARE_CONFIG_SERIALISER(Strings) DECLARE_CONFIG_SERIALISER(StringSet) DECLARE_CONFIG_SERIALISER(StringMap) DECLARE_CONFIG_SERIALISER(std::set) +DECLARE_CONFIG_SERIALISER(std::set) DECLARE_CONFIG_SERIALISER(std::filesystem::path) DECLARE_CONFIG_SERIALISER(std::optional) diff --git a/src/libutil/include/nix/util/configuration.hh b/src/libutil/include/nix/util/configuration.hh index 541febdb5f9..9ecf98e2e14 100644 --- a/src/libutil/include/nix/util/configuration.hh +++ b/src/libutil/include/nix/util/configuration.hh @@ -9,6 +9,7 @@ #include "nix/util/types.hh" #include "nix/util/experimental-features.hh" +#include "nix/util/deprecated-features.hh" namespace nix { @@ -431,7 +432,7 @@ public: void operator=(const std::optional & v); }; -struct ExperimentalFeatureSettings : Config +struct FeatureSettings : Config { Setting> experimentalFeatures{ @@ -489,9 +490,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 enabled. (Currently there are none.) + + The following deprecated feature features can be re-activated: + + {{#include deprecated-features-shortlist.md}} + + Deprecated features are [further documented in the manual](@docroot@/development/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 experimentalFeatureSettings; +using ExperimentalFeatureSettings = FeatureSettings; } // namespace nix diff --git a/src/libutil/include/nix/util/deprecated-features.hh b/src/libutil/include/nix/util/deprecated-features.hh new file mode 100644 index 00000000000..1167d8f8c1f --- /dev/null +++ b/src/libutil/include/nix/util/deprecated-features.hh @@ -0,0 +1,90 @@ +#pragma once +///@file + +#include "nix/util/error.hh" +#include "nix/util/types.hh" +#include "nix/util/json-non-null.hh" + +#include + +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 { + UrlLiterals, +}; + +/** + * 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); + +/** + * Compute the documentation of all deprecated features. + * + * See `doc/manual` for how this information is used. + */ +nlohmann::json documentDeprecatedFeatures(); + +/** + * 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 StringSet &); + +/** + * 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); +}; + +/** + * `DeprecatedFeature` is always rendered as a string. + */ +template<> +struct json_avoids_null : std::true_type +{}; + +/** + * 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 &); + +} // namespace nix diff --git a/src/libutil/include/nix/util/meson.build b/src/libutil/include/nix/util/meson.build index 1ab2c47e47d..ff2365f0c71 100644 --- a/src/libutil/include/nix/util/meson.build +++ b/src/libutil/include/nix/util/meson.build @@ -26,6 +26,7 @@ headers = files( 'config-impl.hh', 'configuration.hh', 'current-process.hh', + 'deprecated-features.hh', 'english.hh', 'environment-variables.hh', 'error.hh', diff --git a/src/libutil/meson.build b/src/libutil/meson.build index fdd310ad8e4..a02e65556ce 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -132,6 +132,7 @@ sources = [ config_priv_h ] + files( 'config-global.cc', 'configuration.cc', 'current-process.cc', + 'deprecated-features.cc', 'english.cc', 'environment-variables.cc', 'error.cc', diff --git a/src/nix/main.cc b/src/nix/main.cc index 811d1c495a6..ba5af5e1975 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -21,6 +21,7 @@ #include "nix/flake/flake.hh" #include "nix/flake/settings.hh" #include "nix/util/json-utils.hh" +#include "nix/util/deprecated-features.hh" #include "self-exe.hh" #include "crash-handler.hh" @@ -481,6 +482,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) { switch (args.completions->type) {