Revert "Module-builtin assertions, disabling assertions and submodule assertions"

This commit is contained in:
Silvan Mosberger 2020-12-18 16:42:42 +01:00
parent fd1cc29974
commit 9e6737710c
16 changed files with 96 additions and 442 deletions

View File

@ -46,7 +46,6 @@ let
showFiles showFiles
showOption showOption
unknownModule unknownModule
literalExample
; ;
in in
@ -73,20 +72,14 @@ rec {
check ? true check ? true
}: }:
let let
# An internal module that's always added, defining special options which # This internal module declare internal options under the `_module'
# change the behavior of the module evaluation itself. This is under a # attribute. These options are fragile, as they are used by the
# `_`-prefixed namespace in order to prevent name clashes with # module system to change the interpretation of modules.
# user-defined options
internalModule = rec { internalModule = rec {
# FIXME: Using ./modules.nix directly breaks the doc for some reason _file = ./modules.nix;
_file = "lib/modules.nix";
key = _file; key = _file;
# Most of these options are set to be internal only for prefix != [],
# aka it's a submodule evaluation. This way their docs are displayed
# only once as a top-level NixOS option, but will be hidden for all
# submodules, even though they are available there too
options = { options = {
_module.args = mkOption { _module.args = mkOption {
# Because things like `mkIf` are entirely useless for # Because things like `mkIf` are entirely useless for
@ -96,7 +89,7 @@ rec {
# a `_module.args.pkgs = import (fetchTarball { ... }) {}` won't # a `_module.args.pkgs = import (fetchTarball { ... }) {}` won't
# start a download when `pkgs` wasn't evaluated. # start a download when `pkgs` wasn't evaluated.
type = types.lazyAttrsOf types.unspecified; type = types.lazyAttrsOf types.unspecified;
internal = prefix != []; internal = true;
description = "Arguments passed to each module."; description = "Arguments passed to each module.";
}; };
@ -104,19 +97,13 @@ rec {
type = types.bool; type = types.bool;
internal = true; internal = true;
default = check; default = check;
description = '' description = "Whether to check whether all option definitions have matching declarations.";
Whether to check whether all option definitions have matching
declarations.
Note that this has nothing to do with the similarly named
<option>_module.checks</option> option
'';
}; };
_module.freeformType = mkOption { _module.freeformType = mkOption {
# Disallow merging for now, but could be implemented nicely with a `types.optionType` # Disallow merging for now, but could be implemented nicely with a `types.optionType`
type = types.nullOr (types.uniq types.attrs); type = types.nullOr (types.uniq types.attrs);
internal = prefix != []; internal = true;
default = null; default = null;
description = '' description = ''
If set, merge all definitions that don't have an associated option If set, merge all definitions that don't have an associated option
@ -129,75 +116,6 @@ rec {
turned off. turned off.
''; '';
}; };
_module.checks = mkOption {
description = ''
Evaluation checks to trigger during module evaluation. The
attribute name will be displayed when it is triggered, allowing
users to disable/change these checks if necessary. See
the section on Warnings and Assertions in the manual for more
information.
'';
example = literalExample ''
{
gpgSshAgent = {
enable = config.programs.gnupg.agent.enableSSHSupport && config.programs.ssh.startAgent;
message = "You can't use ssh-agent and GnuPG agent with SSH support enabled at the same time!";
};
grafanaPassword = {
enable = config.services.grafana.database.password != "";
message = "Grafana passwords will be stored as plaintext in the Nix store!";
type = "warning";
};
}
'';
default = {};
internal = prefix != [];
type = types.attrsOf (types.submodule {
options.enable = mkOption {
description = ''
Whether to enable this check. Set this to false to not trigger
any errors or warning messages. This is useful for ignoring a
check in case it doesn't make sense in certain scenarios.
'';
default = true;
type = types.bool;
};
options.check = mkOption {
description = ''
The condition that must succeed in order for this check to be
successful and not trigger a warning or error.
'';
readOnly = true;
type = types.bool;
};
options.type = mkOption {
description = ''
The type of the check. The default
<literal>"error"</literal> type will cause evaluation to fail,
while the <literal>"warning"</literal> type will only show a
warning.
'';
type = types.enum [ "error" "warning" ];
default = "error";
example = "warning";
};
options.message = mkOption {
description = ''
The message to display if this check triggers.
To display option names in the message, add
<literal>options</literal> to the module function arguments
and use <literal>''${options.path.to.option}</literal>.
'';
type = types.str;
example = "Enabling both \${options.services.foo.enable} and \${options.services.bar.enable} is not possible.";
};
});
};
}; };
config = { config = {
@ -236,35 +154,6 @@ rec {
# paths, meaning recursiveUpdate will never override any value # paths, meaning recursiveUpdate will never override any value
else recursiveUpdate freeformConfig declaredConfig; else recursiveUpdate freeformConfig declaredConfig;
# Triggers all checks defined by _module.checks before returning its argument
triggerChecks = let
handleCheck = errors: name:
let
value = config._module.checks.${name};
show =
# Assertions with a _ prefix aren't meant to be configurable
if lib.hasPrefix "_" name then value.message
else "[${showOption prefix}${optionalString (prefix != []) "/"}${name}] ${value.message}";
in
if value.enable -> value.check then errors
else if value.type == "warning" then lib.warn show errors
else if value.type == "error" then errors ++ [ show ]
else abort "Unknown check type ${value.type}";
errors = lib.foldl' handleCheck [] (lib.attrNames config._module.checks);
errorMessage = ''
Failed checks:
${lib.concatMapStringsSep "\n" (a: "- ${a}") errors}
'';
trigger = if errors == [] then null else throw errorMessage;
in builtins.seq trigger;
finalConfig = triggerChecks (removeAttrs config [ "_module" ]);
checkUnmatched = checkUnmatched =
if config._module.check && config._module.freeformType == null && merged.unmatchedDefns != [] then if config._module.check && config._module.freeformType == null && merged.unmatchedDefns != [] then
let let
@ -284,7 +173,7 @@ rec {
result = builtins.seq checkUnmatched { result = builtins.seq checkUnmatched {
inherit options; inherit options;
config = finalConfig; config = removeAttrs config [ "_module" ];
inherit (config) _module; inherit (config) _module;
}; };
in result; in result;
@ -625,8 +514,6 @@ rec {
definitions = map (def: def.value) res.defsFinal; definitions = map (def: def.value) res.defsFinal;
files = map (def: def.file) res.defsFinal; files = map (def: def.file) res.defsFinal;
inherit (res) isDefined; inherit (res) isDefined;
# This allows options to be correctly displayed using `${options.path.to.it}`
__toString = _: showOption loc;
}; };
# Merge definitions of a value of a given type. # Merge definitions of a value of a given type.
@ -886,15 +773,14 @@ rec {
visible = false; visible = false;
apply = x: throw "The option `${showOption optionName}' can no longer be used since it's been removed. ${replacementInstructions}"; apply = x: throw "The option `${showOption optionName}' can no longer be used since it's been removed. ${replacementInstructions}";
}); });
config._module.checks = config.assertions =
let opt = getAttrFromPath optionName options; in { let opt = getAttrFromPath optionName options; in [{
${"removed-" + showOption optionName} = lib.mkIf opt.isDefined { assertion = !opt.isDefined;
message = '' message = ''
The option definition `${showOption optionName}' in ${showFiles opt.files} no longer has any effect; please remove it. The option definition `${showOption optionName}' in ${showFiles opt.files} no longer has any effect; please remove it.
${replacementInstructions} ${replacementInstructions}
''; '';
}; }];
};
}; };
/* Return a module that causes a warning to be shown if the /* Return a module that causes a warning to be shown if the
@ -955,18 +841,14 @@ rec {
})) from); })) from);
config = { config = {
_module.checks = warnings = filter (x: x != "") (map (f:
let warningMessages = map (f: let val = getAttrFromPath f config;
let val = getAttrFromPath f config; opt = getAttrFromPath f options;
opt = getAttrFromPath f options; in
in { optionalString
${"merged" + showOption f} = lib.mkIf (val != "_mkMergedOptionModule") { (val != "_mkMergedOptionModule")
type = "warning"; "The option `${showOption f}' defined in ${showFiles opt.files} has been changed to `${showOption to}' that has a different type. Please read `${showOption to}' documentation and update your configuration accordingly."
message = "The option `${showOption f}' defined in ${showFiles opt.files} has been changed to `${showOption to}' that has a different type. Please read `${showOption to}' documentation and update your configuration accordingly."; ) from);
};
}
) from;
in mkMerge warningMessages;
} // setAttrByPath to (mkMerge } // setAttrByPath to (mkMerge
(optional (optional
(any (f: (getAttrFromPath f config) != "_mkMergedOptionModule") from) (any (f: (getAttrFromPath f config) != "_mkMergedOptionModule") from)
@ -1025,10 +907,8 @@ rec {
}); });
config = mkMerge [ config = mkMerge [
{ {
_module.checks.${"renamed-" + showOption from} = mkIf (warn && fromOpt.isDefined) { warnings = optional (warn && fromOpt.isDefined)
type = "warning"; "The option `${showOption from}' defined in ${showFiles fromOpt.files} has been renamed to `${showOption to}'.";
message = "The option `${showOption from}' defined in ${showFiles fromOpt.files} has been renamed to `${showOption to}'.";
};
} }
(if withPriority (if withPriority
then mkAliasAndWrapDefsWithPriority (setAttrByPath to) fromOpt then mkAliasAndWrapDefsWithPriority (setAttrByPath to) fromOpt

View File

@ -192,7 +192,7 @@ rec {
let ss = opt.type.getSubOptions opt.loc; let ss = opt.type.getSubOptions opt.loc;
in if ss != {} then optionAttrSetToDocList' opt.loc ss else []; in if ss != {} then optionAttrSetToDocList' opt.loc ss else [];
in in
[ docOption ] ++ optionals (docOption.visible && ! docOption.internal) subOptions) (collect isOption options); [ docOption ] ++ optionals docOption.visible subOptions) (collect isOption options);
/* This function recursively removes all derivation attributes from /* This function recursively removes all derivation attributes from

View File

@ -655,7 +655,7 @@ runTests {
modules = [ module ]; modules = [ module ];
}).options; }).options;
locs = filter (o: ! o.internal) (optionAttrSetToDocList (removeAttrs options [ "_module" ])); locs = filter (o: ! o.internal) (optionAttrSetToDocList options);
in map (o: o.loc) locs; in map (o: o.loc) locs;
expected = [ [ "foo" ] [ "foo" "<name>" "bar" ] [ "foo" "bar" ] ]; expected = [ [ "foo" ] [ "foo" "<name>" "bar" ] [ "foo" "bar" ] ];
}; };

View File

@ -27,50 +27,37 @@ reportFailure() {
fail=$((fail + 1)) fail=$((fail + 1))
} }
checkConfigCodeOutErr() {
local expectedExit=$1
shift;
local outputContains=$1
shift;
local errorContains=$1
shift;
out=$(mktemp)
err=$(mktemp)
evalConfig "$@" 1>"$out" 2>"$err"
if [[ "$?" -ne "$expectedExit" ]]; then
echo 2>&1 "error: Expected exit code $expectedExit while evaluating"
reportFailure "$@"
return 1
fi
if [[ -n "$outputContains" ]] && ! grep -zP --silent "$outputContains" "$out"; then
echo 2>&1 "error: Expected output matching '$outputContains', while evaluating"
reportFailure "$@"
return 1
fi
if [[ -n "$errorContains" ]] && ! grep -zP --silent "$errorContains" "$err"; then
echo 2>&1 "error: Expected error matching '$errorContains', while evaluating"
reportFailure "$@"
return 1
fi
pass=$((pass + 1))
# Clean up temp files
rm "$out" "$err"
}
checkConfigOutput() { checkConfigOutput() {
local outputContains=$1 local outputContains=$1
shift; shift;
checkConfigCodeOutErr 0 "$outputContains" "" "$@" if evalConfig "$@" 2>/dev/null | grep --silent "$outputContains" ; then
pass=$((pass + 1))
return 0;
else
echo 2>&1 "error: Expected result matching '$outputContains', while evaluating"
reportFailure "$@"
return 1
fi
} }
checkConfigError() { checkConfigError() {
local errorContains=$1 local errorContains=$1
local err=""
shift; shift;
checkConfigCodeOutErr 1 "" "$errorContains" "$@" if err==$(evalConfig "$@" 2>&1 >/dev/null); then
echo 2>&1 "error: Expected error code, got exit code 0, while evaluating"
reportFailure "$@"
return 1
else
if echo "$err" | grep -zP --silent "$errorContains" ; then
pass=$((pass + 1))
return 0;
else
echo 2>&1 "error: Expected error matching '$errorContains', while evaluating"
reportFailure "$@"
return 1
fi
fi
} }
# Check boolean option. # Check boolean option.
@ -275,29 +262,6 @@ checkConfigOutput true config.value.mkbefore ./types-anything/mk-mods.nix
checkConfigOutput 1 config.value.nested.foo ./types-anything/mk-mods.nix checkConfigOutput 1 config.value.nested.foo ./types-anything/mk-mods.nix
checkConfigOutput baz config.value.nested.bar.baz ./types-anything/mk-mods.nix checkConfigOutput baz config.value.nested.bar.baz ./types-anything/mk-mods.nix
## Module assertions
# Check that assertions are triggered by default for just evaluating config
checkConfigError 'Failed checks:\n- \[test\] Assertion failed' config ./assertions/simple.nix
# Assertion is not triggered when enable is false or condition is true
checkConfigOutput '{ }' config ./assertions/condition-true.nix
checkConfigOutput '{ }' config ./assertions/enable-false.nix
# Warnings should be displayed on standard error
checkConfigCodeOutErr 0 '{ }' 'warning: \[test\] Warning message' config ./assertions/warning.nix
# Check that multiple assertions and warnings can be triggered at once
checkConfigError 'Failed checks:\n- \[test1\] Assertion 1 failed\n- \[test2\] Assertion 2 failed' config ./assertions/multi.nix
checkConfigError 'trace: warning: \[test3\] Warning 3 failed\ntrace: warning: \[test4\] Warning 4 failed' config ./assertions/multi.nix
# Submodules should be able to trigger assertions and display the submodule prefix in their error
checkConfigError 'Failed checks:\n- \[foo/test\] Assertion failed' config.foo ./assertions/submodule.nix
checkConfigError 'Failed checks:\n- \[foo.bar/test\] Assertion failed' config.foo.bar ./assertions/submodule-attrsOf.nix
checkConfigError 'Failed checks:\n- \[foo.bar.baz/test\] Assertion failed' config.foo.bar.baz ./assertions/submodule-attrsOf-attrsOf.nix
# Assertions with an attribute starting with _ shouldn't have their name displayed
checkConfigError 'Failed checks:\n- Assertion failed' config ./assertions/underscore-attributes.nix
cat <<EOF cat <<EOF
====== module tests ====== ====== module tests ======
$pass Pass $pass Pass

View File

@ -1,8 +0,0 @@
{
_module.checks.test = {
check = true;
message = "Assertion failed";
};
}

View File

@ -1,9 +0,0 @@
{
_module.checks.test = {
enable = false;
check = false;
message = "Assertion failed";
};
}

View File

@ -1,23 +0,0 @@
{
_module.checks = {
test1 = {
check = false;
message = "Assertion 1 failed";
};
test2 = {
check = false;
message = "Assertion 2 failed";
};
test3 = {
check = false;
message = "Warning 3 failed";
type = "warning";
};
test4 = {
check = false;
message = "Warning 4 failed";
type = "warning";
};
};
}

View File

@ -1,6 +0,0 @@
{
_module.checks.test = {
check = false;
message = "Assertion failed";
};
}

View File

@ -1,13 +0,0 @@
{ lib, ... }: {
options.foo = lib.mkOption {
default = { bar.baz = {}; };
type = lib.types.attrsOf (lib.types.attrsOf (lib.types.submodule {
_module.checks.test = {
check = false;
message = "Assertion failed";
};
}));
};
}

View File

@ -1,13 +0,0 @@
{ lib, ... }: {
options.foo = lib.mkOption {
default = { bar = {}; };
type = lib.types.attrsOf (lib.types.submodule {
_module.checks.test = {
check = false;
message = "Assertion failed";
};
});
};
}

View File

@ -1,13 +0,0 @@
{ lib, ... }: {
options.foo = lib.mkOption {
default = {};
type = lib.types.submodule {
_module.checks.test = {
check = false;
message = "Assertion failed";
};
};
};
}

View File

@ -1,8 +0,0 @@
{
_module.checks._test = {
check = false;
message = "Assertion failed";
};
}

View File

@ -1,9 +0,0 @@
{
_module.checks.test = {
check = false;
type = "warning";
message = "Warning message";
};
}

View File

@ -3,155 +3,72 @@
xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:xi="http://www.w3.org/2001/XInclude"
version="5.0" version="5.0"
xml:id="sec-assertions"> xml:id="sec-assertions">
<title>Evaluation Checks</title> <title>Warnings and Assertions</title>
<para> <para>
When configuration problems are detectable in a module, it is a good idea to When configuration problems are detectable in a module, it is a good idea to
write a check for catching it early. Doing so can provide clear feedback to write an assertion or warning. Doing so provides clear feedback to the user
the user and can prevent errors before the build. and prevents errors after the build.
</para> </para>
<para> <para>
Although Nix has the <literal>abort</literal> and Although Nix has the <literal>abort</literal> and
<literal>builtins.trace</literal> <literal>builtins.trace</literal>
<link xlink:href="https://nixos.org/nix/manual/#ssec-builtins">functions</link> <link xlink:href="https://nixos.org/nix/manual/#ssec-builtins">functions</link>
to perform such tasks generally, they are not ideally suited for NixOS to perform such tasks, they are not ideally suited for NixOS modules. Instead
modules. Instead of these functions, you can declare your evaluation checks of these functions, you can declare your warnings and assertions using the
using the NixOS module system. NixOS module system.
</para> </para>
<section xml:id="sec-assertions-define"> <section xml:id="sec-assertions-warnings">
<title>Defining Checks</title> <title>Warnings</title>
<para> <para>
Checks can be defined using the <xref linkend="opt-_module.checks"/> option. This is an example of using <literal>warnings</literal>.
Each check needs an attribute name, under which you can define a trigger
assertion using <xref linkend="opt-_module.checks._name_.check"/> and a
message using <xref linkend="opt-_module.checks._name_.message"/>.
For the message, you can add
<literal>options</literal> to the module arguments and use
<literal>${options.path.to.option}</literal> to print a context-aware string
representation of an option path. Here is an example showing how this can be
done.
</para> </para>
<programlisting> <programlisting>
{ config, options, ... }: { <![CDATA[
_module.checks.gpgSshAgent = { { config, lib, ... }:
check = config.programs.gnupg.agent.enableSSHSupport -> !config.programs.ssh.startAgent; {
message = "If you have ${options.programs.gnupg.agent.enableSSHSupport} enabled," config = lib.mkIf config.services.foo.enable {
+ " you can't enable ${options.programs.ssh.startAgent} as well!"; warnings =
}; if config.services.foo.bar
then [ ''You have enabled the bar feature of the foo service.
_module.checks.grafanaPassword = { This is known to cause some specific problems in certain situations.
check = config.services.grafana.database.password == ""; '' ]
message = "The grafana password defined with ${options.services.grafana.database.password}" else [];
+ " will be stored as plaintext in the Nix store!"; }
# This is a non-fatal warning
type = "warning";
};
} }
]]>
</programlisting> </programlisting>
</section> </section>
<section xml:id="sec-assertions-ignoring"> <section xml:id="sec-assertions-assertions">
<title>Ignoring Checks</title> <title>Assertions</title>
<para> <para>
Sometimes you can get failing checks that don't apply to your specific case This example, extracted from the
and you wish to ignore them, or at least make errors non-fatal. You can do so <link xlink:href="https://github.com/NixOS/nixpkgs/blob/release-17.09/nixos/modules/services/logging/syslogd.nix">
for all checks defined using <xref linkend="opt-_module.checks"/> by <literal>syslogd</literal> module </link> shows how to use
using the attribute name of the definition, which is conveniently printed <literal>assertions</literal>. Since there can only be one active syslog
using <literal>[...]</literal> when the check is triggered. For above daemon at a time, an assertion is useful to prevent such a broken system
example, the evaluation output when the checks are triggered looks as from being built.
follows:
</para>
<programlisting>
trace: warning: [grafanaPassword] The grafana password defined with
services.grafana.database.password will be stored as plaintext in the Nix store!
error: Failed checks:
- [gpgSshAgent] If you have programs.gnupg.agent.enableSSHSupport
enabled, you can't enable programs.ssh.startAgent as well!
</programlisting>
<para>
The <literal>[grafanaPassword]</literal> and <literal>[gpgSshAgent]</literal>
strings tell you that these were defined under the <literal>grafanaPassword
</literal> and <literal>gpgSshAgent</literal> attributes of
<xref linkend="opt-_module.checks"/> respectively. With this knowledge
you can adjust them to your liking:
</para> </para>
<programlisting> <programlisting>
<![CDATA[
{ config, lib, ... }:
{ {
# Change the error into a non-fatal warning config = lib.mkIf config.services.syslogd.enable {
_module.checks.gpgSshAgent.type = "warning"; assertions =
[ { assertion = !config.services.rsyslogd.enable;
# We don't care about this warning, disable it message = "rsyslogd conflicts with syslogd";
_module.checks.grafanaPassword.enable = false; }
];
}
} }
]]>
</programlisting> </programlisting>
</section>
<section xml:id="sec-assertions-submodules">
<title>Checks in Submodules</title>
<para>
Evaluation checks can be defined within submodules in the same way. Here is an example:
</para>
<programlisting>
{ lib, ... }: {
options.myServices = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({ config, options, ... }: {
options.port = lib.mkOption {};
config._module.checks.portConflict = {
check = config.port != 80;
message = "Port ${toString config.port} defined using"
+ " ${options.port} is usually used for HTTP";
type = "warning";
};
}));
};
}
</programlisting>
<para>
When this check is triggered, it shows both the submodule path along with
the check attribute within that submodule, joined by a
<literal>/</literal>. Note also how <literal>${options.port}</literal>
correctly shows the context of the option.
</para>
<programlisting>
trace: warning: [myServices.foo/portConflict] Port 80 defined using
myServices.foo.port is usually used for HTTP
</programlisting>
<para>
Therefore to disable such a check, you can do so by changing the
<xref linkend="opt-_module.checks"/> option within the
<literal>myServices.foo</literal> submodule:
</para>
<programlisting>
{
myServices.foo._module.checks.portConflict.enable = false;
}
</programlisting>
<note>
<para>
Checks defined in submodules under <literal>types.listOf</literal> can't be
ignored, since there's no way to change previously defined list items.
</para>
</note>
</section> </section>
</section> </section>

View File

@ -1,4 +1,4 @@
{ lib, config, ... }: { lib, ... }:
with lib; with lib;
@ -30,18 +30,5 @@ with lib;
}; };
}; };
config._module.checks = lib.listToAttrs (lib.imap1 (n: value:
let
name = "_${toString n}";
isWarning = lib.isString value;
result = {
check = if isWarning then false else value.assertion;
type = if isWarning then "warning" else "error";
message = if isWarning then value else value.message;
};
in nameValuePair name result
) (config.assertions ++ config.warnings));
# impl of assertions is in <nixpkgs/nixos/modules/system/activation/top-level.nix> # impl of assertions is in <nixpkgs/nixos/modules/system/activation/top-level.nix>
} }

View File

@ -117,10 +117,18 @@ let
perl = "${pkgs.perl}/bin/perl " + (concatMapStringsSep " " (lib: "-I${lib}/${pkgs.perl.libPrefix}") (with pkgs.perlPackages; [ FileSlurp NetDBus XMLParser XMLTwig ])); perl = "${pkgs.perl}/bin/perl " + (concatMapStringsSep " " (lib: "-I${lib}/${pkgs.perl.libPrefix}") (with pkgs.perlPackages; [ FileSlurp NetDBus XMLParser XMLTwig ]));
}; };
# Handle assertions and warnings
failedAssertions = map (x: x.message) (filter (x: !x.assertion) config.assertions);
baseSystemAssertWarn = if failedAssertions != []
then throw "\nFailed assertions:\n${concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
else showWarnings config.warnings baseSystem;
# Replace runtime dependencies # Replace runtime dependencies
system = fold ({ oldDependency, newDependency }: drv: system = fold ({ oldDependency, newDependency }: drv:
pkgs.replaceDependency { inherit oldDependency newDependency drv; } pkgs.replaceDependency { inherit oldDependency newDependency drv; }
) baseSystem config.system.replaceRuntimeDependencies; ) baseSystemAssertWarn config.system.replaceRuntimeDependencies;
in in