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
This commit is contained in:
piegames 2024-07-13 05:24:41 +02:00
parent 1c080a8239
commit 49d61b2e4b
22 changed files with 468 additions and 54 deletions

View file

@ -1,9 +1,14 @@
# Usually "experimental" or "deprecated"
kind:
# "xp" or "dp"
kindShort:
with builtins; with builtins;
with import ./utils.nix; with import ./utils.nix;
let let
showExperimentalFeature = name: doc: '' showExperimentalFeature = name: doc: ''
- [`${name}`](@docroot@/contributing/experimental-features.md#xp-feature-${name}) - [`${name}`](@docroot@/contributing/${kind}-features.md#${kindShort}-feature-${name})
''; '';
in in
xps: indent " " (concatStrings (attrValues (mapAttrs showExperimentalFeature xps))) xps: indent " " (concatStrings (attrValues (mapAttrs showExperimentalFeature xps)))

View file

@ -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)))

View file

@ -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)))

View file

@ -72,9 +72,9 @@ generate_manual_deps = files(
# Generates builtins.md and builtin-constants.md. # Generates builtins.md and builtin-constants.md.
subdir('src/language') 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') subdir('src/command-ref')
# Generates experimental-feature-descriptions.md. # Generates {experimental,deprecated}-feature-descriptions.md.
subdir('src/contributing') subdir('src/contributing')
# Generates rl-next-generated.md. # Generates rl-next-generated.md.
subdir('src/release-notes') subdir('src/release-notes')
@ -106,6 +106,8 @@ manual = custom_target(
nix3_cli_files, nix3_cli_files,
experimental_features_shortlist_md, experimental_features_shortlist_md,
experimental_feature_descriptions_md, experimental_feature_descriptions_md,
deprecated_features_shortlist_md,
deprecated_feature_descriptions_md,
conf_file_md, conf_file_md,
builtins_md, builtins_md,
builtin_constants_md, builtin_constants_md,

View file

@ -192,6 +192,7 @@
- [Hacking](contributing/hacking.md) - [Hacking](contributing/hacking.md)
- [Testing](contributing/testing.md) - [Testing](contributing/testing.md)
- [Experimental Features](contributing/experimental-features.md) - [Experimental Features](contributing/experimental-features.md)
- [Deprecated Features](contributing/deprecated-features.md)
- [CLI guideline](contributing/cli-guideline.md) - [CLI guideline](contributing/cli-guideline.md)
- [C++ style guide](contributing/cxx.md) - [C++ style guide](contributing/cxx.md)
- [Release Notes](release-notes/release-notes.md) - [Release Notes](release-notes/release-notes.md)

View file

@ -7,10 +7,10 @@ xp_features_json = custom_target(
experimental_features_shortlist_md = custom_target( experimental_features_shortlist_md = custom_target(
command : nix_eval_for_docs + [ command : nix_eval_for_docs + [
'--expr', '--expr',
'import @INPUT0@ (builtins.fromJSON (builtins.readFile @INPUT1@))', 'import @INPUT0@ "experimental" "xp" (builtins.fromJSON (builtins.readFile @INPUT1@))',
], ],
input : [ input : [
'../../generate-xp-features-shortlist.nix', '../../generate-features-shortlist.nix',
xp_features_json, xp_features_json,
], ],
capture : true, capture : true,
@ -18,6 +18,26 @@ experimental_features_shortlist_md = custom_target(
env : nix_env_for_docs, 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. # Intermediate step for manpage generation.
# This splorks the output of generate-manpage.nix as JSON, # This splorks the output of generate-manpage.nix as JSON,
# which gets written as a directory tree below. # which gets written as a directory tree below.
@ -60,6 +80,7 @@ conf_file_md = custom_target(
'../../utils.nix', '../../utils.nix',
conf_file_json, conf_file_json,
experimental_features_shortlist_md, experimental_features_shortlist_md,
deprecated_features_shortlist_md,
], ],
output : 'conf-file.md', output : 'conf-file.md',
env : nix_env_for_docs, env : nix_env_for_docs,

View file

@ -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}}

View file

@ -4,12 +4,25 @@
experimental_feature_descriptions_md = custom_target( experimental_feature_descriptions_md = custom_target(
command : nix_eval_for_docs + [ command : nix_eval_for_docs + [
'--expr', '--expr',
'import @INPUT0@ (builtins.fromJSON (builtins.readFile @INPUT1@))', 'import @INPUT0@ "experimental" "xp" (builtins.fromJSON (builtins.readFile @INPUT1@))',
], ],
input : [ input : [
'../../generate-xp-features.nix', '../../generate-features.nix',
xp_features_json, xp_features_json,
], ],
capture : true, capture : true,
output : 'experimental-feature-descriptions.md', 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',
)

View file

@ -2710,20 +2710,29 @@ Expr & EvalState::parseExprFromFile(const SourcePath & path, std::shared_ptr<Sta
} }
Expr & EvalState::parseExprFromString(std::string s_, const SourcePath & basePath, std::shared_ptr<StaticEnv> & staticEnv, const ExperimentalFeatureSettings & xpSettings) Expr & EvalState::parseExprFromString(
std::string s_,
const SourcePath & basePath,
std::shared_ptr<StaticEnv> & staticEnv,
const FeatureSettings & featureSettings
)
{ {
// NOTE this method (and parseStdin) must take care to *fully copy* their input // 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 // into their respective Pos::Origin until the parser stops overwriting its input
// data. // data.
auto s = make_ref<std::string>(s_); auto s = make_ref<std::string>(s_);
s_.append("\0\0", 2); 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);
} }

View file

@ -344,8 +344,17 @@ public:
/** /**
* Parse a Nix expression from the specified string. * Parse a Nix expression from the specified string.
*/ */
Expr & parseExprFromString(std::string s, const SourcePath & basePath, std::shared_ptr<StaticEnv> & staticEnv, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); Expr & parseExprFromString(
Expr & parseExprFromString(std::string s, const SourcePath & basePath, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); std::string s,
const SourcePath & basePath,
std::shared_ptr<StaticEnv> & staticEnv,
const FeatureSettings & xpSettings = featureSettings
);
Expr & parseExprFromString(
std::string s,
const SourcePath & basePath,
const FeatureSettings & xpSettings = featureSettings
);
Expr & parseStdin(); Expr & parseStdin();
@ -569,7 +578,7 @@ private:
Pos::Origin origin, Pos::Origin origin,
const SourcePath & basePath, const SourcePath & basePath,
std::shared_ptr<StaticEnv> & staticEnv, std::shared_ptr<StaticEnv> & 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. * Current Nix call stack depth, used with `max-call-depth` setting to throw stack overflow hopefully before we run out of system stack.

View file

@ -115,7 +115,7 @@ struct ExprState
std::unique_ptr<Expr> pipe(PosIdx pos, State & state, bool flip = false) std::unique_ptr<Expr> pipe(PosIdx pos, State & state, bool flip = false)
{ {
if (!state.xpSettings.isEnabled(Xp::PipeOperator)) if (!state.featureSettings.isEnabled(Xp::PipeOperator))
throw ParseError({ throw ParseError({
.msg = HintFmt("Pipe operator is disabled"), .msg = HintFmt("Pipe operator is disabled"),
.pos = state.positions[pos] .pos = state.positions[pos]
@ -656,7 +656,7 @@ template<> struct BuildAST<grammar::expr::path> : p::maybe_nothing {};
template<> struct BuildAST<grammar::expr::uri> { template<> struct BuildAST<grammar::expr::uri> {
static void apply(const auto & in, ExprState & s, State & ps) { 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) if (noURLLiterals)
throw ParseError({ throw ParseError({
.msg = HintFmt("URL literals are disabled"), .msg = HintFmt("URL literals are disabled"),
@ -858,7 +858,7 @@ Expr * EvalState::parse(
Pos::Origin origin, Pos::Origin origin,
const SourcePath & basePath, const SourcePath & basePath,
std::shared_ptr<StaticEnv> & staticEnv, std::shared_ptr<StaticEnv> & staticEnv,
const ExperimentalFeatureSettings & xpSettings) const FeatureSettings & featureSettings)
{ {
parser::State s = { parser::State s = {
symbols, symbols,
@ -866,7 +866,7 @@ Expr * EvalState::parse(
basePath, basePath,
positions.addOrigin(origin, length), positions.addOrigin(origin, length),
exprSymbols, exprSymbols,
xpSettings featureSettings,
}; };
parser::ExprState x; parser::ExprState x;

View file

@ -19,7 +19,7 @@ struct State
SourcePath basePath; SourcePath basePath;
PosTable::Origin origin; PosTable::Origin origin;
const Expr::AstSymbols & s; const Expr::AstSymbols & s;
const ExperimentalFeatureSettings & xpSettings; const FeatureSettings & featureSettings;
void dupAttr(const AttrPath & attrPath, const PosIdx pos, const PosIdx prevPos); void dupAttr(const AttrPath & attrPath, const PosIdx pos, const PosIdx prevPos);
void dupAttr(Symbol attr, const PosIdx pos, const PosIdx prevPos); void dupAttr(Symbol attr, const PosIdx pos, const PosIdx prevPos);

View file

@ -34,6 +34,10 @@ template<> struct BaseSetting<std::set<ExperimentalFeature>>::trait
{ {
static constexpr bool appendable = true; static constexpr bool appendable = true;
}; };
template<> struct BaseSetting<std::set<DeprecatedFeature>>::trait
{
static constexpr bool appendable = true;
};
template<typename T> template<typename T>
struct BaseSetting<T>::trait struct BaseSetting<T>::trait
@ -51,6 +55,7 @@ template<> void BaseSetting<Strings>::appendOrSet(Strings newValue, bool append)
template<> void BaseSetting<StringSet>::appendOrSet(StringSet newValue, bool append); template<> void BaseSetting<StringSet>::appendOrSet(StringSet newValue, bool append);
template<> void BaseSetting<StringMap>::appendOrSet(StringMap newValue, bool append); template<> void BaseSetting<StringMap>::appendOrSet(StringMap newValue, bool append);
template<> void BaseSetting<std::set<ExperimentalFeature>>::appendOrSet(std::set<ExperimentalFeature> newValue, bool append); template<> void BaseSetting<std::set<ExperimentalFeature>>::appendOrSet(std::set<ExperimentalFeature> newValue, bool append);
template<> void BaseSetting<std::set<DeprecatedFeature>>::appendOrSet(std::set<DeprecatedFeature> newValue, bool append);
template<typename T> template<typename T>
void BaseSetting<T>::appendOrSet(T newValue, bool append) void BaseSetting<T>::appendOrSet(T newValue, bool append)
@ -116,6 +121,7 @@ DECLARE_CONFIG_SERIALISER(Strings)
DECLARE_CONFIG_SERIALISER(StringSet) DECLARE_CONFIG_SERIALISER(StringSet)
DECLARE_CONFIG_SERIALISER(StringMap) DECLARE_CONFIG_SERIALISER(StringMap)
DECLARE_CONFIG_SERIALISER(std::set<ExperimentalFeature>) DECLARE_CONFIG_SERIALISER(std::set<ExperimentalFeature>)
DECLARE_CONFIG_SERIALISER(std::set<DeprecatedFeature>)
template<typename T> template<typename T>
T BaseSetting<T>::parse(const std::string & str) const T BaseSetting<T>::parse(const std::string & str) const

View file

@ -2,6 +2,7 @@
#include "args.hh" #include "args.hh"
#include "abstract-setting-to-json.hh" #include "abstract-setting-to-json.hh"
#include "experimental-features.hh" #include "experimental-features.hh"
#include "deprecated-features.hh"
#include "file-system.hh" #include "file-system.hh"
#include "logging.hh" #include "logging.hh"
#include "strings.hh" #include "strings.hh"
@ -355,6 +356,32 @@ template<> std::string BaseSetting<std::set<ExperimentalFeature>>::to_string() c
return concatStringsSep(" ", stringifiedXpFeatures); return concatStringsSep(" ", stringifiedXpFeatures);
} }
template<> std::set<DeprecatedFeature> BaseSetting<std::set<DeprecatedFeature>>::parse(const std::string & str) const
{
std::set<DeprecatedFeature> res;
for (auto & s : tokenizeString<StringSet>(str)) {
if (auto thisDpFeature = parseDeprecatedFeature(s); thisDpFeature)
res.insert(thisDpFeature.value());
else
warn("unknown deprecated feature '%s'", s);
}
return res;
}
template<> void BaseSetting<std::set<DeprecatedFeature>>::appendOrSet(std::set<DeprecatedFeature> 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<std::set<DeprecatedFeature>>::to_string() const
{
StringSet stringifiedDpFeatures;
for (const auto & feature : value)
stringifiedDpFeatures.insert(std::string(showDeprecatedFeature(feature)));
return concatStringsSep(" ", stringifiedDpFeatures);
}
template<> StringMap BaseSetting<StringMap>::parse(const std::string & str) const template<> StringMap BaseSetting<StringMap>::parse(const std::string & str) const
{ {
StringMap res; StringMap res;
@ -391,6 +418,7 @@ template class BaseSetting<Strings>;
template class BaseSetting<StringSet>; template class BaseSetting<StringSet>;
template class BaseSetting<StringMap>; template class BaseSetting<StringMap>;
template class BaseSetting<std::set<ExperimentalFeature>>; template class BaseSetting<std::set<ExperimentalFeature>>;
template class BaseSetting<std::set<DeprecatedFeature>>;
static Path parsePath(const AbstractSetting & s, const std::string & str) static Path parsePath(const AbstractSetting & s, const std::string & str)
{ {
@ -525,28 +553,52 @@ GlobalConfig::Register::Register(Config * config)
configRegistrations->emplace_back(config); configRegistrations->emplace_back(config);
} }
ExperimentalFeatureSettings experimentalFeatureSettings; FeatureSettings experimentalFeatureSettings;
FeatureSettings& featureSettings = experimentalFeatureSettings;
static GlobalConfig::Register rSettings(&experimentalFeatureSettings); static GlobalConfig::Register rSettings(&experimentalFeatureSettings);
bool ExperimentalFeatureSettings::isEnabled(const ExperimentalFeature & feature) const bool FeatureSettings::isEnabled(const ExperimentalFeature & feature) const
{ {
auto & f = experimentalFeatures.get(); auto & f = experimentalFeatures.get();
return std::find(f.begin(), f.end(), feature) != f.end(); 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)) if (!isEnabled(feature))
throw MissingExperimentalFeature(feature); throw MissingExperimentalFeature(feature);
} }
bool ExperimentalFeatureSettings::isEnabled(const std::optional<ExperimentalFeature> & feature) const bool FeatureSettings::isEnabled(const std::optional<ExperimentalFeature> & feature) const
{ {
return !feature || isEnabled(*feature); return !feature || isEnabled(*feature);
} }
void ExperimentalFeatureSettings::require(const std::optional<ExperimentalFeature> & feature) const void FeatureSettings::require(const std::optional<ExperimentalFeature> & 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<DeprecatedFeature> & feature) const
{
return !feature || isEnabled(*feature);
}
void FeatureSettings::require(const std::optional<DeprecatedFeature> & feature) const
{ {
if (feature) require(*feature); if (feature) require(*feature);
} }

View file

@ -9,6 +9,7 @@
#include "types.hh" #include "types.hh"
#include "experimental-features.hh" #include "experimental-features.hh"
#include "deprecated-features.hh"
namespace nix { namespace nix {
@ -441,7 +442,7 @@ struct GlobalConfig : public AbstractConfig
extern GlobalConfig globalConfig; extern GlobalConfig globalConfig;
struct ExperimentalFeatureSettings : Config { struct FeatureSettings : Config {
Setting<std::set<ExperimentalFeature>> experimentalFeatures{ Setting<std::set<ExperimentalFeature>> experimentalFeatures{
this, {}, "experimental-features", this, {}, "experimental-features",
@ -483,9 +484,47 @@ struct ExperimentalFeatureSettings : Config {
* disabled, and so the function does nothing in that case. * disabled, and so the function does nothing in that case.
*/ */
void require(const std::optional<ExperimentalFeature> &) const; void require(const std::optional<ExperimentalFeature> &) const;
Setting<std::set<DeprecatedFeature>> 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<DeprecatedFeature> &) 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<DeprecatedFeature> &) const;
}; };
// FIXME: don't use a global variable. // 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;
} }

View file

@ -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<DeprecatedFeature> : std::true_type {};
};

View file

@ -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<DeprecatedFeatureDetails, numDepFeatures> 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<DeprecatedFeature> parseDeprecatedFeature(const std::string_view & name)
{
using ReverseDepMap = std::map<std::string_view, DeprecatedFeature>;
static std::unique_ptr<ReverseDepMap> reverseDepMap = []() {
auto reverseDepMap = std::make_unique<ReverseDepMap>();
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<DeprecatedFeature> parseDeprecatedFeatures(const std::set<std::string> & rawFeatures)
{
std::set<DeprecatedFeature> 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);
}
}

View file

@ -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, dont 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<DeprecatedFeature> 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<DeprecatedFeature> parseDeprecatedFeatures(const std::set<std::string> &);
/**
* 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);
};
}

View file

@ -7,6 +7,7 @@ libutil_sources = files(
'compute-levels.cc', 'compute-levels.cc',
'config.cc', 'config.cc',
'current-process.cc', 'current-process.cc',
'deprecated-features.cc',
'english.cc', 'english.cc',
'environment-variables.cc', 'environment-variables.cc',
'error.cc', 'error.cc',
@ -64,6 +65,8 @@ libutil_headers = files(
'config-impl.hh', 'config-impl.hh',
'config.hh', 'config.hh',
'current-process.hh', 'current-process.hh',
'deprecated-features.hh',
'deprecated-features-json.hh',
'english.hh', 'english.hh',
'environment-variables.hh', 'environment-variables.hh',
'error.hh', 'error.hh',

View file

@ -14,6 +14,7 @@
#include "loggers.hh" #include "loggers.hh"
#include "markdown.hh" #include "markdown.hh"
#include "experimental-features-json.hh" #include "experimental-features-json.hh"
#include "deprecated-features-json.hh"
#include <sys/types.h> #include <sys/types.h>
#include <sys/socket.h> #include <sys/socket.h>
@ -422,6 +423,11 @@ void mainWrapped(int argc, char * * argv)
return; return;
} }
if (argc == 2 && std::string(argv[1]) == "__dump-dp-features") {
logger->cout(documentDeprecatedFeatures().dump());
return;
}
Finally printCompletions([&]() Finally printCompletions([&]()
{ {
if (args.completions) { if (args.completions) {

View file

@ -26,9 +26,9 @@ namespace nix {
, state({}, store) , 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; 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); state.eval(e, v);
if (forceValue) if (forceValue)
state.forceValue(v, noPos); state.forceValue(v, noPos);

View file

@ -214,36 +214,36 @@ namespace nix {
// pipes are gated behind an experimental feature flag // pipes are gated behind an experimental feature flag
TEST_F(TrivialExpressionTest, pipeDisabled) { 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 ''a'' <| add ''b''"), Error); ASSERT_THROW(eval("let add = l: r: l + r; in add ''a'' <| ''b''"), Error);
} }
TEST_F(TrivialExpressionTest, pipeRight) { TEST_F(TrivialExpressionTest, pipeRight) {
ExperimentalFeatureSettings mockXpSettings; FeatureSettings mockFeatureSettings;
mockXpSettings.set("experimental-features", "pipe-operator"); 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")); 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")); ASSERT_THAT(v, IsStringEq("cba"));
} }
TEST_F(TrivialExpressionTest, pipeLeft) { TEST_F(TrivialExpressionTest, pipeLeft) {
ExperimentalFeatureSettings mockXpSettings; FeatureSettings mockFeatureSettings;
mockXpSettings.set("experimental-features", "pipe-operator"); 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")); 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")); ASSERT_THAT(v, IsStringEq("abc"));
} }
TEST_F(TrivialExpressionTest, pipeMixed) { TEST_F(TrivialExpressionTest, pipeMixed) {
ExperimentalFeatureSettings mockXpSettings; FeatureSettings mockFeatureSettings;
mockXpSettings.set("experimental-features", "pipe-operator"); 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")); 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")); ASSERT_THAT(v, IsStringEq("ac"));
} }
} /* namespace nix */ } /* namespace nix */