Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions source/guides/recipes/emit-a-warning-only-once.md
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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify and fix typo

Suggested change
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.
Since such warnings are tied to the flow of evaluation the warning may fire more times than intended.

This document shows a technique to solve that problem, and documents its requirements and limitations.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove filler intro

Suggested change
Now let's use a top level scope to reduce our warnings to just one.
Add a top-level ...


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).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 nowadays (more on that later) and state facts instead.


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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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, but if your code is in the upgrade path for users who may not have, use this polyfill:
builtins.warn was introduced in Nix 2.23. If you support older versions, use this polyfill:

Most users have upgraded far beyond 2.22, statement Is not necessary/verified.
Depends who your users are. Most of open source users? Most nixpkgs users? The sentence works without that claim.


```nix
let
warn = builtins.warn or builtins.trace;
# ...
in
# ...
```

The `. or` operator will take care of the potentially missing `warn`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will take is future tense. . or typo

Suggested change
The `. or` operator will take care of the potentially missing `warn`.
The `or` operator takes care of the potentially missing `warn`.


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
1 change: 1 addition & 0 deletions source/guides/recipes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ Managing remote sources <./dependency-management.md>
Python development environment <./python-environment.md>
post-build-hook.md
continuous-integration-github-actions.md
emit-a-warning-only-once.md
```