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

__cleanAttrs: Clean packages that don't expose all internals #217243

Closed
wants to merge 31 commits into from

Conversation

roberth
Copy link
Member

@roberth roberth commented Feb 19, 2023

Description of changes

What

Clean up the attribute zoo on packages, opt-in.

Why

Automatically exposing internals blurs the line between what's expected to be stable and what's not. Let's not do that.

How

Call derivationStrict instead of derivation. derivation is backed by derivationStrict anyway.

When you specify __cleanAttrs = true; in a mkDerivation call, you won't be exposing internals.

What else

This PR improves memory by some 1-2% and cpu time also improves.

Internals are still accessible via overrideAttrs, lib.inspectMkDerivationArgs and pkgs.inputDerivation.

This relates weakly to a proposed improvement to make Nix take outputs from package attrsets instead of store derivations, as both this PR and the proposal work towards a more accurate as an expression-level package concept.

Things done
  • Built on platform(s)
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin
  • For non-Linux: Is sandbox = true set in nix.conf? (See Nix manual)
  • Tested, as applicable:
  • Tested compilation of all packages that depend on this change using nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD". Note: all changes have to be committed, also see nixpkgs-review usage
  • Tested basic functionality of all binary files (usually in ./result/bin/)
  • 23.05 Release Notes (or backporting 22.11 Release notes)
    • (Package updates) Added a release notes entry if the change is major or breaking
    • (Module updates) Added a release notes entry if the change is significant
    • (Module addition) Added a release notes entry if adding a new NixOS module
  • Fits CONTRIBUTING.md.

@Artturin
Copy link
Member

Artturin commented Feb 21, 2023

Internals are still accessible via overrideAttrs, lib.inspectMkDerivationArgs and pkgs.inputDerivation.

In mkDerivation(finalAttrs: too?

@winterqt
Copy link
Member

winterqt commented Feb 21, 2023

Do we want to make cleanAttrs opt-out any time soon (with perhaps defaults for allowing pname, version, and src to passthru), or will it take a lot of effort (do a lot of things in-tree depend on internal attrs)?

@roberth
Copy link
Member Author

roberth commented Feb 21, 2023

Internals are still accessible via overrideAttrs, lib.inspectMkDerivationArgs and pkgs.inputDerivation.

In mkDerivation(finalAttrs: too?

  • finalAttrs and prevAttrs behave as before, except finalAttrs.finalPackage is smaller, as is to be expected because it is defined to match the return value of mkDerivation.

  • Fixes for overrideAttrs (split from #201734) #211685 sets more precedent for extending finalAttrs with more special attributes is useful and/or necessary, so I can imagine adding the complete drvAttrs there, if not cost prohibitive for some reason. I think this would be an extra feature that does not have to be implemented here.

Do we want to make cleanAttrs opt-out any time soon (with perhaps defaults for allowing pname, version, and src to passthru), or will it take a lot of effort (do a lot of things in-tree depend on internal attrs)?

I don't think we'll do this anytime soon, if at all. Applying that change too soon would mean that we fail to pay attention to the actual usage of some packages' attributes, which would be bad for users. cleanAttrs may never be suitable for being enabled by default in mkDerivation, but a successor to mkDerivation may enable it by default or its only behavior. The existence of cleanAttrs provides an anchor point for conversations about such a successor, and helps with experimentation.

@SuperSandro2000
Copy link
Member

So we should document that it should never be set in any package in nixpkgs without strong reasoning?

The implementation with dynamic attributes is a little weird, but
it performs better by avoiding some attrset copying and sorting, `//`.
@roberth
Copy link
Member Author

roberth commented Mar 16, 2023

I think we can just make __cleanAttrs = true warn for one release

Then every package that wants to opt in would have to do it in the first release. I don't think that's a good approach, and the cost of "warn" is low enough.

I'd like to avoid blowing up the scope and merge this soon. It adds the mechanism, but avoids changing the policy, so that we can

  • start applying the solution on a small scale
  • start solving the engineering problem
  • gain data and experience to inform policy decisions

@SuperSandro2000
Copy link
Member

SuperSandro2000 commented Apr 1, 2023

Making the interface of a package explicit helps with maintenance by reducing the surface area that a maintainer has to worry about. This also benefits users, as cleanAttrs starts the conversation about what the public interface should be. Furthermore, we can expect a further performance benefit from having smaller attrsets when applied at a larger scale.
For a single package, I don't see what specifics would make this reasoning stronger, making "strong reasons" a rather tough requirement.

For me this translates into more work and discussions. Sometimes I know that the solution I am drafting is hacky and I decided for myself that it is better than the available alternatives. I don't want to contact another person to expose some variable(s) for that though. I personally would want a setting to just disable cleanAttrs everywhere and give me access to everything, as I don't really care if something is marked as an interface or not.
Or do we now need to start doing reflections?


For example this would majorly break nix-update and complicate it.

https://github.com/Mic92/nix-update/blob/f7eba589cc99a0cb6f0bab298e194c7c5705be13/nix_update/eval.py#L106-L118

@@ -165,6 +165,10 @@ In addition to numerous new and upgraded packages, this release has the followin

- Pantheon now defaults to Mutter 42 and GNOME settings daemon 42, all Pantheon packages are now tracking elementary OS 7 updates.

- `mkDerivation` does not have to leak implementation details anymore.

Passing [`__cleanAttrs = true`](https://nixos.org/manual/nixpkgs/unstable/#var-__cleanAttrs) to `mkDerivation` returns a minimal set of package attributes that can be extended via [`passthru`](https://nixos.org/manual/nixpkgs/unstable/#var-stdenv-passthru). This reduces confusion for package users and allows for confident use of non-standard attributes provided by a package. It also gives package authors more confidence in modifying the builder environment without breaking consumers of their package, and it is slightly more efficient.
Copy link
Member

Choose a reason for hiding this comment

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

I really don't like the wording of this. It encourages people to just set cleanAttrs for all their packages, only to later get feedback from people that they need x, y, z back.

Copy link
Member Author

Choose a reason for hiding this comment

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

How about this?

Suggested change
Passing [`__cleanAttrs = true`](https://nixos.org/manual/nixpkgs/unstable/#var-__cleanAttrs) to `mkDerivation` returns a minimal set of package attributes that can be extended via [`passthru`](https://nixos.org/manual/nixpkgs/unstable/#var-stdenv-passthru). This reduces confusion for package users and allows for confident use of non-standard attributes provided by a package. It also gives package authors more confidence in modifying the builder environment without breaking consumers of their package, and it is slightly more efficient.
Passing [`__cleanAttrs = true`](https://nixos.org/manual/nixpkgs/unstable/#var-__cleanAttrs) to `mkDerivation` returns a minimal set of package attributes that can be extended via [`passthru`](https://nixos.org/manual/nixpkgs/unstable/#var-stdenv-passthru). For new packages, this reduces confusion for package users and allows for confident use of non-standard attributes provided by a package. It also gives package authors more confidence in modifying the builder environment without breaking consumers of their package, and it is slightly more efficient. It may be applied to existing packages with great care, as other users may unknowingly rely on internals. To uncover such usages without breaking their code, you may first pass `__cleanAttrs = "warn"` instead.

Copy link
Member

Choose a reason for hiding this comment

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

That sounds better. 👍🏼

Comment on lines +42 to +43
# We allow `hello.src` to be used in tests and examples, despite __cleanAttrs
passthru.src = finalAttrs.src;
Copy link
Member

Choose a reason for hiding this comment

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

IMO src should always be passed through. eg you sometimes want to (re-)build it and as I understand it, that would after that no longer be possible which would suck.

Copy link
Member Author

Choose a reason for hiding this comment

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

src -> internals.src should be fine for individual "override" packages you write.

There is no general way to get all sources in using just the expression language, so we don't lose the capability to generically retrieve all sources, because we never had that in the language. Simply put, not all packages have a src, and not all sources appear in src, and you can hide "anything" in a dependency of a derivation a string context.
This task is better achieved by analyzing the .drv graph, you can find all the derivation input sources (file references and builtin fetcher results) and fixed output derivations. You may even do this from within a derivation.

@roberth
Copy link
Member Author

roberth commented Apr 1, 2023

For me this translates into more work and discussions. Sometimes I know that the solution I am drafting is hacky and I decided for myself that it is better than the available alternatives. I don't want to contact another person to expose some variable(s) for that though.

You'll still be able to do this, by using attributes such as .internals.src. If you don't think your use case should be explicitly supported by the package, you don't need to do anything.

Also if you're very confident about your use case, you could just open a PR. It doesn't have to some sort of drawn out discussion.

I personally would want a setting to just disable cleanAttrs everywhere and give me access to everything, as I don't really care if something is marked as an interface or not. Or do we now need to start doing reflections?

What do you mean with reflections?

I'm not in favor of such a global flag, because it is not necessary and it will make some expressions that do or do not use the flag incompatible. (Configuration should always be a last resort)

For example this would majorly break nix-update and complicate it.

https://github.com/Mic92/nix-update/blob/f7eba589cc99a0cb6f0bab298e194c7c5705be13/nix_update/eval.py#L106-L118

It'd be fine for nix-update to peek inside the internals and then carry on as it did.

It would be nicer if the language integrations and nix-update agreed on some small but general interface, such as a package attribute, so that nix-update isn't coupled to each of the language integrations. More concretely, if the language integrations provided a default updateScript attribute, nix-update would be trivial. That's a separate project though that I don't really want to delve into here, but I think it shows that internals highlights the places where the design could be improved.

@SuperSandro2000
Copy link
Member

SuperSandro2000 commented Apr 1, 2023

You'll still be able to do this, by using attributes such as .internals.src.

Totally forgot about that. 😓 Then this should be okay merge.

It would be nicer if the language integrations and nix-update agreed on some small but general interface, such as a package attribute, so that nix-update isn't coupled to each of the language integrations. More concretely, if the language integrations provided a default updateScript attribute, nix-update would be trivial. That's a separate project though that I don't really want to delve into here, but I think it shows that internals highlights the places where the design could be improved.

I think nix-update exactly was build out of that problem that no such general attribute existed.

Copy link
Member

@infinisil infinisil left a comment

Choose a reason for hiding this comment

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

I'm a bit worried that people will start using __cleanAttrs = true on existing packages, breaking a lot of things without warnings downstream (and for every user complaining about an attribute gone missing there's 10 more in the back that never reported it). We'd have to trust reviewers to not merge such changes. Better of course would be CI, but that adds a lot of complexity.

Other than that this looks pretty good to me, I like the salve mundi!


New packages may pass `__cleanAttrs = true;` to `mkDerivation`, so that it will return a minimal set of package attributes, which package authors can extend via [`passthru`](#var-stdenv-passthru).

Existing packages may be modified to pass `__cleanAttrs = "warn";`, so that the legacy attributes remain available, but they will print a helpful warning when they are accessed. Doing this is best avoided until the packaging function used supports recursively defined arguments like [`mkDerivation` does](#mkderivation-recursive-attributes).
Copy link
Member

Choose a reason for hiding this comment

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

Doing this is best avoided until the packaging function used supports recursively defined arguments like mkDerivation does

Which packaging function?

Comment on lines +325 to +326
# optional, but specified in our case
"version"
Copy link
Member

Choose a reason for hiding this comment

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

I don't see why version should get propagated here.

in

lib.extendDerivation
validity.handled
({
(
lib.optionalAttrs (__cleanAttrs != true) (maybeWarnCleanAttrs {
Copy link
Member

Choose a reason for hiding this comment

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

This removes inputDerivation when __cleanAttrs == true, I'm not sure if that makes sense, since .inputDerivation is meant to be a public stable interface. While it might not be needed anymore since mkShell is now buildable, I'm not sure if this should be decided here.

(
(
if __cleanAttrs == true
then builtins.intersectAttrs { version = null; } drvAttrs
Copy link
Member

Choose a reason for hiding this comment

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

Why is version propagated? I guess it's somewhat important and stable enough. Should definitely be a comment here

Comment on lines +75 to +81
restore-default-greeting = callPackage ./test.nix {
hello = finalAttrs.finalPackage.overrideAttrs (o: {
passthru = o.passthru // {
greeting = null;
};
});
};
Copy link
Member

Choose a reason for hiding this comment

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

What's this for?

Copy link
Contributor

Choose a reason for hiding this comment

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

not everyone speaks latin 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

That is a good point. It could come across as elitist, especially in this context now that I think about it more.
Something like Esperanto would be nicer, or Lojban, or Tolkien's Elvish. Or maybe it should just be hello_with_bells_and_whistles, although I don't know if _ for variations should be followed by snake case or camelCase.
Why is this hard...

May the first confirmation win:

  • suilad-ambar: is this a correct Elvish package name?
  • coi-munje: is this a correct Lojban package name?

I don't like the Esperanto version because it's basically Romance language vocabulary. Nothing wrong with that, but if it's not going to be English, I don't want to bias it. Sue me. ;)

@@ -362,6 +364,42 @@ Unless set to `false`, some build systems with good support for parallel buildin

### Special variables {#special-variables}

#### `__cleanAttrs` {#var-__cleanAttrs}

By default, `mkDerivation` will expose its arguments in the returned package attribute set. This is unnecessary and leads to some confusion and doubt.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
By default, `mkDerivation` will expose its arguments in the returned package attribute set. This is unnecessary and leads to some confusion and doubt.
By default, `mkDerivation` will expose its arguments in the returned package attribute set.
This is often unnecessary and may lead to confusion about what's relevant for consumers.

We should soften that a bit, because I don't see evidence for the originally definitive-sounding statement.


##### What to do when warned {#warning-package-attr-impl-detail}

If you encounter the warning `The attribute ... of package ... is an implementation detail`, you are invited to help us explicitly support ways in which a package attribute set may used, so that Nixpkgs contributors and users will be aware of your use case.
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be phrased to be more immediately actionable.

Suggested change
If you encounter the warning `The attribute ... of package ... is an implementation detail`, you are invited to help us explicitly support ways in which a package attribute set may used, so that Nixpkgs contributors and users will be aware of your use case.
If you encounter the warning `The attribute ... of package ... is an implementation detail`, please make Nixpkgs contributors and users aware of your particular use case, and help providing explicit ways to use package attribute sets, by opening an [issue](https://github.com/NixOS/nixpkgs/issues) or (https://github.com/NixOS/nixpkgs/pulls).

@infinisil infinisil added the significant Novel ideas, large API changes, notable refactorings, issues with RFC potential, etc. label Apr 19, 2023
@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/2023-05-16-nixpkgs-architecture-team-meeting-38/28223/1

@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/zurich-23-05-zhf-hackathon-and-workshop-report/29093/1

@roberth
Copy link
Member Author

roberth commented Dec 1, 2023

#271241 (comment) shows that access to values computed by mkDerivations can be important for the package definition itself. Ideally this would not go through the public interface, but I'm not sure that I would want to impose finalAttrs.finalPackage.internals.doCheck as a best practice...

Perhaps it'd be better to avoid cleanAttrs and instead move to a module-like situation, where the functions are flattened to a single fixed-point, and the attrsets are unflattened so that what used to be layers between the functions are just groups of attributes.

mk2Derivation({ pythonAttrs, setup, drvAttrs, public }:
  # Don't need to get stuff from four levels deep
  # setup: the generic shell script from "stdenv"
  setup.configureFlags = ... (optional drvAttrs.doCheck "--enable-tests") ...;
  setup.doCheck = true; # like mkDerivation { doCheck } argument
  # No more passthru. It's symmetric now.
  public.tests = callPackage ./tests { mypkg = public; };
)

Is the overlay pattern (with 1 layer of hardcoded merging) good enough for this, or do we need more module system-like features...

@roberth roberth marked this pull request as draft December 1, 2023 01:00
@DavHau
Copy link
Member

DavHau commented Dec 1, 2023

How about removing the explicit call to mk2Derivation from the definition and instead introduce a new module __class:

{ pythonAttrs, setup, drvAttrs, public }: {
  __class = "mk2Derivation";
  # Don't need to get stuff from four levels deep
  # setup: the generic shell script from "stdenv"
  setup.configureFlags = ... (optional drvAttrs.doCheck "--enable-tests") ...;
  setup.doCheck = true; # like mkDerivation { doCheck } argument
  # No more passthru. It's symmetric now.
  public.tests = callPackage ./tests { mypkg = public; };
}

That way it would be discoverable how to import this package and it feels like getting one step closer towards better integration with nixos modules.

And of course, automatic merging of values would be nice, but maybe it's not a must for the start.

I think this would already be a huge improvement over the current situation.

@roberth
Copy link
Member Author

roberth commented Dec 4, 2023

automatic merging of values would be nice, but maybe it's not a must for the start.

Not merging those "top level" attributes, of which we have only few, will make any sort of overriding incredibly needlessly labor intensive, while gaining very little in flexibility or performance (if at all).
I believe we may allow a restricted form of module system-like merging, as the merging implemented in minimod appears quite cheap, especially if we can get rid of priorities, thanks to the implicit priority encoded by overlay layer ordering.

__class

We already have _class in the module system (newish feature) which is named too similarly, yet must be quite different in interpretation to coexist.

Whether similarity - let alone compatibility - with the module system is a valid requirement remains to be seen, because we can not degrade performance by any significant amount, due to the scale of a typical package's dependency closure.

@roberth
Copy link
Member Author

roberth commented Dec 12, 2023

This idea was good, but mkDerivation can not sustain it.
It appeared as low hanging fruit, but it was ever so slightly too much, as exemplified by #217243 (comment).

EDIT: last discussion may resume in #273815

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
0.kind: enhancement Add something new 6.topic: architecture Relating to code and API architecture of Nixpkgs 6.topic: hygiene 6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 6.topic: stdenv Standard environment 8.has: changelog 8.has: documentation 8.has: package (new) This PR adds a new package 10.rebuild-darwin: 1-10 10.rebuild-darwin: 1 10.rebuild-linux: 1-10 11.by: package-maintainer This PR was created by the maintainer of the package it changes significant Novel ideas, large API changes, notable refactorings, issues with RFC potential, etc.
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

9 participants