Skip to content
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

Filter away unset output attributes #59

Closed
wants to merge 1 commit into from

Conversation

L-as
Copy link

@L-as L-as commented Sep 28, 2022

…hello`)

This in addition also strips away unset attributes set using flake.

The core issue with IFD in this context, is that we want to strip away unused attributes. AFAICT, flake-parts did this in a buggy way before. Removing this entirely while fixing support for IFD was easier, but to keep support for filtering away unset attributes, a bit more complexity was needed.

The issue is that we can't use null as a special value to mean "unset". The reason is that while filtering over allSystems.${system}, checking whether packages has been set, doing null == packages will cause packages, an attribute set, to be evaluated to WHNF. WHNF for an attribute set entails evaluating the keys, and if the keys are generated using IFD, then this will fail, even if we know that no matter what the key is, this check will return false.

If the Nix evaluator is ever fixed, this feature can be somewhat cleaned up by using null checks rather than checking isDefined.

To make use of this feature, you can not simply do

packages = import drv;

You must do

packages = mkOverride 100 (mkOrder 1000 (import drv));

The reason is that the module system internally will try to check ._type, but this of course will cause all of the other keys to also be evaluated.

This would also be unnecessary if the Nix evaluator were fixed.

Closes #57

…hello`)

This in addition also strips away unset attributes set using `flake`.

The core issue with IFD in this context, is that we want to strip away unused attributes.
AFAICT, flake-parts did this in a buggy way before. Removing this entirely while
fixing support for IFD was easier, but to keep support for filtering away unset
attributes, a bit more complexity was needed.

The issue is that we can't use `null` as a special value to mean "unset".
The reason is that while filtering over `allSystems.${system}`, checking
whether `packages` has been set, doing `null == packages` will cause `packages`,
an attribute set, to be evaluated to WHNF. WHNF for an attribute set entails
evaluating the _keys_, and if the keys are generated using IFD, then this will
fail, even if we know that no matter what the key is, this check will return false.

If the Nix evaluator is ever fixed, this feature can be somewhat cleaned up by using
null checks rather than checking `isDefined`.

To make use of this feature, you can not simply do
```nix
packages = import drv;
```

You must do
```nix
packages = mkOverride 100 (mkOrder 1000 (import drv));
```

The reason is that the module system internally will try to
check `._type`, but this of course will cause all of the other
keys to also be evaluated.

This would also be unnecessary if the Nix evaluator were fixed.

Closes hercules-ci#57
@roberth
Copy link
Member

roberth commented Sep 28, 2022

Good analysis of the strictness issue.

to keep support for filtering away unset attributes, a bit more complexity was needed.

The code was purely wishful thinking that I've glossed over. It didn't work, but I don't think it was a problem. By adding more filterAttrs, one layer higher, we'd introduce more strictness once again. I don't think mkIf will help us out either, because that only really works with attrsOf, whereas flake-parts only uses lazyAttrsOf (again to avoid filterAttrs-like strictness-inducing behavior).
Maybe there's something I'm not seeing? If so, could you give me a concrete example of a real world problem that is solved by the extra filterAttrs?

This would also be unnecessary if the Nix evaluator were fixed.

I don't think the operational semantics were specified to include laziness at such a fine granularity, so calling this change (whatever it is) a fix would be an exaggeration.

@L-as
Copy link
Author

L-as commented Sep 28, 2022

I'm using this code in practice here https://github.com/mlabs-haskell/mlabs-tooling.nix/blob/las/work/flake.nix#L156

The extra filterAttrs that runs on the output of the module evaluation just evaluates all top-level attributes to WHNF, and I think that's fine since that should be system-independent in the first place.

The older code not working wasn't a problem (ignoring IFD), but it seemed to me that you intended to not set attributes that the user didn't mean to set, and I think that would be a very useful features. For most flakes, exposing an empty nixosModules makes little sense.

@roberth
Copy link
Member

roberth commented Sep 28, 2022

you intended to not set attributes that the user didn't mean to set

While that was my intent, I am not convinced that it is important.

It wouldn't be the first time that a project starts with some bad, redundant code.

For most flakes, exposing an empty nixosModules makes little sense.

The other day, I answered a strictness question from someone who was doing something like options = f myPkgs. I'd never thought of doing that, so to me it seems like a question of when rather than whether someone will make nixosModules and packages mutually recursive by accident. It's crazy until it's not.

I agree on the fact, but I can't act on it; especially because the infinite recursion errors are as bad as they are, and when the goal is for the project to be general.

@roberth roberth changed the title Allow using IFD to generate attribute names (e.g. `package.${system}.… Filter away unset output attributes Sep 28, 2022
@roberth
Copy link
Member

roberth commented Sep 28, 2022

I've updated the title to (what I believe to) better reflect the effect of the proposed change. That is, assuming #58 did its job for #57.

Copy link
Member

@roberth roberth left a comment

Choose a reason for hiding this comment

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

I don't think I can make this change happen, but I hope you can appreciate the ideas in my comments.

# - Doesn't handle undefined options in submodules.
# - Doesn't handle undefined options that have a prefix, e.g. `_module.args`.
# TODO: Upstream a good version of this to Nixpkgs, probably as an extra result
# from `evalModules`, e.g. `result.configFiltered`.
Copy link
Member

Choose a reason for hiding this comment

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

We should be careful about adding attributes. They're not like methods that have no per-object overhead.

# and only check whether they're defined. Hence we can't check for nullness,
# because that will evaluate a lot of stuff unnecessarily and cause errors
# with IFD. A more proper fix would perhaps be fixing the Nix evaluator to
# short circuit more, specifically, in an expression like `null != { ${x} = ...; }`,
Copy link
Member

Choose a reason for hiding this comment

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

This requires a Value to have an intermediate state between thunk and attrset, and it will cause make the != operator asymmetric. Neither is terrible in itself, but nix has to strike a balance. If you want to make this happens, please contribute to the test suite or other quality control measures such as combining boehmgc and c++ sanitizers.

# short circuit more, specifically, in an expression like `null != { ${x} = ...; }`,
# `x` should not be validated, since it's a set regardless.
#
# In general though, Nix's evaluator sucks.
Copy link
Member

Choose a reason for hiding this comment

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

Not very constructive, is it?

@L-as
Copy link
Author

L-as commented Sep 28, 2022

I agree on the fact, but I can't act on it; especially because the infinite recursion errors are as bad as they are, and when the goal is for the project to be general.

I don't think the code here makes it any worse than it was before. isDefined is safe to use in this context AFAICT.

Comment on lines +85 to +91
mkIfNonEmptySet
(mapAttrs
(k: v: v.apps)
(filterAttrs
(k: v: v ? apps)
config.allSystems
));
Copy link
Member

Choose a reason for hiding this comment

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

This technique makes attributes such as devShell.x86_64-linux.default depend on checks.aarch64-darwin. (To pick a highly unrelated example)

You can see this when you define

  systems = [ "x86_64-linux" "aarch64-darwin" ];
  perSystem = { system, ... }: {
    checks = if system == "aarch64-darwin" then throw "not ok" else {};
  }

and then instantiate devShell.x86_64-linux.default.

Usually checks.<system> isn't too slow or fragile, but that's not an assumption we can make. Also strictness can lead to infinite recursions, which are horrible.

Copy link
Author

@L-as L-as Dec 20, 2022

Choose a reason for hiding this comment

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

Thanks, yes, optimally this shouldn't happen.

@roberth
Copy link
Member

roberth commented Nov 12, 2022

What is your reason to filter attributes with empty value?

@shanesveller
Copy link

shanesveller commented Nov 12, 2022

Not taking a stance on whether this should or shouldn't be handled upstream, but couldn't we already use the disabledModules syntax to locally omit any flake-parts content we don't intend to ever use? A pseudocode example might be:

disabledModules = ["nixosConfigurations.nix" "nixosModules.nix" "darwinModules.nix" "legacyPackages.nix"];

for a project-specific dev shell that doesn't make sense for it to deal in NixOS/Darwin modules or nixpkgs-style output trees?

@L-as
Copy link
Author

L-as commented Dec 20, 2022

@shanesveller That does seem possible, thanks for informing me. However, this still seems more ergonomic.

@roberth Because it blows up the size of the output and adds a bunch of unnecessary information to e.g. nix flake show. Any time someone wants to type some output attribute, it bloats up every flake using flake parts more.
Ideally, the output shouldn't be any different compared to not using flake parts.

@roberth roberth added requires upstream change Issues that need a change in Nix, Nixpkgs, etc undecided Depends on postponed design decision labels Feb 1, 2023
@L-as
Copy link
Author

L-as commented Mar 3, 2023

The only remaining benefit of this is that the attributes are truly not there, meaning tab-completion in nix repl won't see them either. I'm not sure whether that's worth it.

@roberth roberth removed the requires upstream change Issues that need a change in Nix, Nixpkgs, etc label May 8, 2023
@roberth
Copy link
Member

roberth commented May 8, 2023

nix repl is still a fair point, but I agree that it doesn't weigh up against the potential for infinite recursions or slower evals.

@shanesveller's suggestion still holds some potential. I think the nixos* modules could be moved to nixpkgs and perhaps some other modules could be made opt-in, like flakeModules.

@roberth roberth closed this May 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
undecided Depends on postponed design decision
Projects
None yet
Development

Successfully merging this pull request may close these issues.

perSystem seemingly breaks with IFD
3 participants