-
-
Notifications
You must be signed in to change notification settings - Fork 321
Add recipe: Emit an evaluation warning only once #1170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,205 @@ | ||||||
| (emit-a-warning-only-once)= | ||||||
| # Emit an evaluation warning only once | ||||||
|
|
||||||
| The Nix language provides a `warn` function that lets expression authors provide feedback to their code's callers/users. | ||||||
| Since such warnings are tied to the flow of evaluation, it may happen that the warning you wish to add is trigger too many times. | ||||||
| This document shows a technique to solve that problem, and documents its requirements and limitations. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pure meta-commentary. I would cut it. Replace with a one-sentence statement of the technique itself or drop entirely. The document works without it. |
||||||
|
|
||||||
| ## Working principle | ||||||
|
|
||||||
| Nix evaluates the root value of an expression file only once. | ||||||
| We will exploit this property to emit fewer warnings. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No future tense please. We are going to add a technical writing guide. Use present tense to describe how things happen. |
||||||
|
|
||||||
| ## Example | ||||||
|
|
||||||
| Suppose we have the following little library: | ||||||
|
|
||||||
| ```nix | ||||||
| { lib, pkgs }: | ||||||
|
|
||||||
| { | ||||||
| makeWidgetScript = { widgetType, useLegacyMethod ? true }: | ||||||
| pkgs.writeScriptBin "make-widget" '' | ||||||
| #!${pkgs.runtimeShell} | ||||||
| echo "Just made a" ${lib.escapeShellArg widgetType} ${lib.optionalString (!useLegacyMethod) "efficiently"} | ||||||
| ''; | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| Since we want widget production to be efficient, we want to phase out `useLegacyMethod`, but without causing unnecessary disruption, so we add a warning: | ||||||
|
|
||||||
| ```nix | ||||||
| { lib, pkgs }: | ||||||
|
|
||||||
| { | ||||||
| makeWidgetScript = args@{ widgetType, useLegacyMethod ? true }: | ||||||
| lib.warnIf | ||||||
| (useLegacyMethod && | ||||||
| # not explicitly provided by the caller | ||||||
| !(args ? useLegacyMethod)) | ||||||
| '' | ||||||
| ACME Inc is in the process of phasing out the legacy widget production method, | ||||||
| but `makeWidgetScript` is still invoked without disabling the legacy method. | ||||||
| If this is intentional, and you are not yet ready to upgrade to the efficient method, | ||||||
| you may specify `useLegacyMethod = true;` to temporarily prolong your use during | ||||||
| the phase-out. | ||||||
| '' | ||||||
| (pkgs.writeScriptBin "make-widget" '' | ||||||
| #!${pkgs.runtimeShell} | ||||||
| echo "Just made a" ${lib.escapeShellArg widgetType} ${lib.optionalString (!useLegacyMethod) "efficiently"} | ||||||
| ''); | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| This will cause each `makeWidgetScript` invocation to emit a sizable message, every time it is invoked in the old way. | ||||||
|
|
||||||
| Now let's use a top level scope to reduce our warnings to just one. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove filler intro
Suggested change
|
||||||
|
|
||||||
| We'll start by adding a `let` binding to the top of the file. | ||||||
| The bindings in it will only be evaluated once, as the evaluator will cache the value it leads up to. | ||||||
|
|
||||||
| ```nix | ||||||
| let | ||||||
| widgetScriptWarning = <...>; # TODO | ||||||
| in | ||||||
| { lib, pkgs }: | ||||||
|
|
||||||
| <...> # The rest of the code | ||||||
| ``` | ||||||
|
|
||||||
| Unfortunately, this means we don't have access to the `lib.warn` family of functions, but fortunately we can usually rely on `builtins.warn` nowadays (more on that later). | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't hedge a fact that has a precise answer. builtins.warn was introduced in a precise version. This sentence adds vagueness. As a user i would not know if i can use builtins.warn or not. Sugggestion: drop the |
||||||
|
|
||||||
| Now we can start writing the warning in the `let` binding, although we have nothing to return. | ||||||
| We could simply omit the second argument, but then we'd be defining a helper function that behaves just like before. | ||||||
| Instead, we'll pass `null` for the value. | ||||||
|
|
||||||
| <!-- the comment may seem redundant for instructional purposes, but makes for good copy-pasting --> | ||||||
| ```nix | ||||||
| # this let must be top-level; see https://nix.dev/guides/recipes/emit-a-warning-only-once | ||||||
| let | ||||||
| inherit (builtins) warn; | ||||||
| widgetScriptWarning = warn '' | ||||||
| ACME Inc is in the process of phasing out the legacy widget production method, | ||||||
| but `makeWidgetScript` is still invoked without disabling the legacy method. | ||||||
| If this is intentional, and you are not yet ready to upgrade to the efficient method, | ||||||
| you may specify `useLegacyMethod = true;` to temporarily prolong your use during | ||||||
| the phase-out. | ||||||
| '' null; # Note the `null` here | ||||||
| in | ||||||
| { lib, pkgs }: | ||||||
|
|
||||||
| <...> # The rest of the code | ||||||
| ``` | ||||||
|
|
||||||
| Now we have a binding that triggers at most once, but we haven't triggered it yet. | ||||||
| This is where `builtins.seq` comes in. It causes its first argument to be evaluated, but only returns its second argument. | ||||||
|
|
||||||
| Let's see the whole example: | ||||||
|
|
||||||
| <!-- the comment may seem redundant for instructional purposes, but makes for good copy-pasting --> | ||||||
| ```nix | ||||||
| # this let must be top-level; see https://nix.dev/guides/recipes/emit-a-warning-only-once | ||||||
| let | ||||||
| inherit (builtins) warn seq; | ||||||
| widgetScriptWarning = warn '' | ||||||
| ACME Inc is in the process of phasing out the legacy widget production method, | ||||||
| but `makeWidgetScript` is still invoked without disabling the legacy method. | ||||||
| If this is intentional, and you are not yet ready to upgrade to the efficient method, | ||||||
| you may specify `useLegacyMethod = true;` to temporarily prolong your use during | ||||||
| the phase-out. | ||||||
| '' null; # Note the `null` here | ||||||
| in | ||||||
| { lib, pkgs }: | ||||||
|
|
||||||
| { | ||||||
| makeWidgetScript = args@{ widgetType, useLegacyMethod ? true }: | ||||||
| (if useLegacyMethod && | ||||||
| # not explicitly provided by the caller | ||||||
| !(args ? useLegacyMethod) | ||||||
| then seq widgetScriptWarning | ||||||
| else x: x | ||||||
| ) | ||||||
| pkgs.writeScriptBin "make-widget" '' | ||||||
| #!${pkgs.runtimeShell} | ||||||
| echo "Just made a" ${lib.escapeShellArg widgetType} ${lib.optionalString (!useLegacyMethod) "efficiently"} | ||||||
| ''; | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ## Best of both worlds | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would drop this. Most people don't need this. |
||||||
|
|
||||||
| You may combine both techniques to reduce, but not eliminate duplication. | ||||||
| Use the described technique to provide context in a single warning, and then trigger it right before a brief warning. | ||||||
| This lets you provide context about the call sites without polluting the log as much. | ||||||
|
|
||||||
| Here is how you may trigger a context warning: | ||||||
|
|
||||||
| <!-- just the triggering snippet for brevity; we already have plenty of duplicate-ish code --> | ||||||
| ```nix | ||||||
| lib.warnIf | ||||||
| (useLegacyMethod && | ||||||
| # not explicitly provided by the caller | ||||||
| !(args ? useLegacyMethod)) | ||||||
| (seq widgetScriptWarning "Implicit use of legacy method for widget ${widgetType}; see prior warning.") | ||||||
| pkgs.writeScriptBin # ... | ||||||
| ``` | ||||||
|
|
||||||
| Note that we use `warnIf` again, and the warning message provides a good opportunity for `seq` to trigger our contextual warning. | ||||||
|
|
||||||
| ## Finding call sites | ||||||
|
|
||||||
| If the evaluator does not produce an unrelated warning first, a quick method to find a call site is to pass `--show-trace --abort-on-warn` to your Nix command invocation. | ||||||
| Otherwise, `--debugger-on-warn` is more suitable, as it lets you resume evaluation after warnings you ignore. | ||||||
|
|
||||||
| ## `builtins.warn` availability | ||||||
|
|
||||||
| `builtins.warn` was introduced in Nix 2.23. | ||||||
| Most users have upgraded far beyond 2.22, but if your code is in the upgrade path for users who may not have, use this polyfill: | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just state the polyfill.
Suggested change
Most users have upgraded far beyond 2.22, statement Is not necessary/verified. |
||||||
|
|
||||||
| ```nix | ||||||
| let | ||||||
| warn = builtins.warn or builtins.trace; | ||||||
| # ... | ||||||
| in | ||||||
| # ... | ||||||
| ``` | ||||||
|
|
||||||
| The `. or` operator will take care of the potentially missing `warn`. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will take is future tense.
Suggested change
|
||||||
|
|
||||||
| When applying this pattern in Nixpkgs, please use this polyfill. | ||||||
|
|
||||||
| (broader-application)= | ||||||
| ## Broader application, e.g. NixOS | ||||||
|
|
||||||
| This technique of `let` + `warn` + `seq` could also be applied in places where the `let` isn't truly top level in a file. | ||||||
| Instead, the warning could be attached to something else that is quite central and low in number, such as the "top level" of a NixOS configuration. | ||||||
| You could create an internal option whose value emits the message once, but this technique is largely unnecessary as NixOS's own [`warnings` option] provides plenty of control. | ||||||
|
|
||||||
| (limitations)= | ||||||
| ## Limitations | ||||||
|
|
||||||
| Nix will only cache a value or let binding when it's the same file, and it will only do so within a single evaluator process. | ||||||
| Technically it's not "once", but at most once _per version of the file_, _per evaluator invocation_. | ||||||
|
|
||||||
| ## Troubleshooting | ||||||
|
|
||||||
| ### It didn't print at all | ||||||
|
|
||||||
| Make sure that | ||||||
| - `warn` is called with two arguments | ||||||
| - the `seq` trigger will be reached. | ||||||
|
|
||||||
| ### It printed more than once | ||||||
|
|
||||||
| Make sure that the binding for the warning contains the whole `warn` invocation with *both* arguments: the message and `null`. | ||||||
|
|
||||||
| Alternatively, this may be unavoidable; see [Limitations](#limitations). | ||||||
|
|
||||||
| ## See also | ||||||
|
|
||||||
| - [`builtins.warn`](https://nix.dev/manual/nix/stable/language/builtins#builtins-warn) | ||||||
| - [`builtins.seq`](https://nix.dev/manual/nix/stable/language/builtins#builtins-seq) | ||||||
| - [`builtins.trace`](https://nix.dev/manual/nix/stable/language/builtins#builtins-trace) | ||||||
| - [NixOS `warnings` option][`warnings` option] | ||||||
|
|
||||||
| [`warnings` option]: https://nixos.org/manual/nixos/stable/#sec-assertions | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simplify and fix typo