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

change mechanism for deps; add depsRequired; add depsFrom. #10

Closed
wants to merge 2 commits into from

Conversation

DavHau
Copy link
Owner

@DavHau DavHau commented Dec 22, 2022

@roberth It would be amazing if you could review this. I really want to get this right.

With this commit, the user is forced to explicitly specify all required dependency names via depsRequired. options.deps contains generated options for all names of depsRequired. These options are populated with defaults from depsFrom, The examples under ./examples show how this is used.

The coercedTo type of deps had some weaknesses:

  1. It was not possible to specify required dependencies explicitly, because it was not possible to create nested options likeoptions.deps.bash = mkOption ....
  2. Because of (1.) I had to add a manual check in makeModule.nix to ensure that all required deps are populated by the user.

Having actual options for all required dependencies allows to reduce complexity and hand the checking off to the module system.

Also, when rendering the options for a package to a doc, all dependencies now appear as an option, which I think is great. It allows the user to see all inputs of a package without having to read the code.

This whole deps mechanism is generic enough to suit as a general inputs. It probably should be renamed to inputs in the future, but I don't want to clutter this PR too much.

EDIT:
options.stdenv can now probably be moved to options.deps.stdenv. It is basically just a dependency of the derivation and I see no need for special treatment.

With this commit, the user is forced to explicitly specify all required dependency names via `depsRequired`.
`options.deps` contains generated options for all names of `depsRequired`.
These options are populated with defaults from `depsFrom`,
The examples under `./examples` show how this is used.

The `coercedTo` type of `deps` had some weaknesses:
1. It was not possible to specify required dependencies explicitly, because it was not possible to create nested options like`options.deps.bash = mkOption ...`.
2. Because of (1.) I had to add a manual check in makeModule.nix to ensure that all required `deps` are populated by the user.

Having actual options for all required dependencies allows to reduce complexity and hand the `checking` off to the module system.

Also, when rendering the options for a package to a doc, all dependencies now appear as an option, which I think is great. It allows the user to see all inputs of a package without having to read the code.
Copy link
Contributor

@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 like killing the deps pattern. I think it does a pretty good job at steering towards a separation of concerns, steering package authors to a situation where all deps are overridable, potentially even improving on callPackage in this regard.

Of course it can be defeated, but package authors who never see pkgs outside of deps = { pkgs, ... }: will not think of referencing it directly. I'm also surprised to see that pkgs is even in the package module arguments. Was that needed for something? (before this PR)

systemd
;
# This must be complete, otherwise options would be missing from `deps`.
depsRequired = {
Copy link
Contributor

Choose a reason for hiding this comment

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

This rename seems like a step back.

callDeps
(t.lazyAttrsOf t.raw);
# Unspecified `deps` are taken from `depsFrom`. It's a source for defaults.
depsFrom = l.mkOption {
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like it should be this then

Suggested change
depsFrom = l.mkOption {
defaultDeps = l.mkOption {

Copy link
Contributor

Choose a reason for hiding this comment

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

or depsDefaults

;
# Define dependencies from the "outer world" only via `depsFrom`.
# This allows for easy overriding via `config.deps` later.
depsFrom = {inherit (pkgs) fetchurl python};
Copy link
Contributor

Choose a reason for hiding this comment

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

Where does this pkgs come from?
The point of having a function here is to avoid bringing pkgs into the larger scope and seducing people into using it directly.

Copy link
Owner Author

Choose a reason for hiding this comment

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

This example is bad. depsFrom should never be specified from within the package module itself. It should be set from outside.

Comment on lines +130 to +133
deps =
l.mapAttrs
mkDepOpt
(l.filterAttrs (_: enabled: enabled) config.depsRequired);
Copy link
Contributor

Choose a reason for hiding this comment

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

makeModule implementation concerns are leaking into the general interface. That's not good.

Shouldn't this be in a makeModule module?

Should deps be a freeform module?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Shouldn't this be in a makeModule module?

I thought explicitly specifying a list of required deps is something that we want everywhere. depsRequired seemed like the simplest way to do that. It's not supposed to be specific to makeModule.

Should deps be a freeform module?

Will this bring back the capability of specifying deps as a function?
I probably need to combine freeform and apply, I guess?

@roberth
Copy link
Contributor

roberth commented Dec 22, 2022

  1. It was not possible to specify required dependencies explicitly, because it was not possible to create nested options likeoptions.deps.bash = mkOption ....

Did you try with mkOption { apply } instead of type = coercedTo?

@roberth
Copy link
Contributor

roberth commented Dec 22, 2022

should be renamed to inputs

Those could be understood to be flake inputs. Arguably those shouldn't be in scope though, so perhaps shadowing is a good thing. Still potentially confusing though. Especially inputs = { inputs, ... }: inputs.foo is a bit odd.

@DavHau
Copy link
Owner Author

DavHau commented Dec 22, 2022

I'm also surprised to see that pkgs is even in the package module arguments. Was that needed for something? (before this PR)

Which package module arguments do you mean?

Did you try with mkOption { apply } instead of type = coercedTo?

Not yet, but I will experiment with it now.

@roberth
Copy link
Contributor

roberth commented Dec 22, 2022

I'm also surprised to see that pkgs is even in the package module arguments. Was that needed for something? (before this PR)

Which package module arguments do you mean?

I think I inferred this wrong when reading the code. Maybe got confused by what you said is a bad example.

@DavHau
Copy link
Owner Author

DavHau commented Dec 22, 2022

  1. It was not possible to specify required dependencies explicitly, because it was not possible to create nested options likeoptions.deps.bash = mkOption ....

Did you try with mkOption { apply } instead of type = coercedTo?

Which type would I pick that allows nested options? If I pick submodule, I cannot define a function for deps anymore, because it gets interpreted as a module, and errors out.

@roberth
Copy link
Contributor

roberth commented Dec 22, 2022

Which type would I pick that allows nested options? If I pick submodule, I cannot define a function for deps anymore, because it gets interpreted as a module, and errors out.

functionTo submodule? Or maybe the dependency sets could be specialArgs?

@DavHau
Copy link
Owner Author

DavHau commented Dec 22, 2022

To explain my though process a bit better:

One underlying question seems to be: Who gets to pick dependencies? The package maintainer, or the caller?
deps = { pkgs, ... }: puts the decision in the hand of the maintainer which makes sense in a mono repo like nixpkgs, where maintainers just want to add a file and have everything sorted.

Out in the decentralized wild, it might be more valuable to let the caller satisfy the dependencies, which requires that requirements are discoverable in some way.

Therefore I thought it's a good idea to enforce an option for each dependency.

Thinking about an end game where nix competes with language specific package managers, the decentralized fashion seems to be the more generic approach vs. nixpkgs is more specific. Maybe the logic of how dependencies are picked within nixpkgs, should be factored out into a separate module?


functionTo submodule? Or maybe the dependency sets could be specialArgs?

Genius! submoduleWith {modules = []; specialArgs = packageSets;} seems to work wonderfully. Thanks for that.

With that, I can get rid of depsDefault, as a mechanism to provide defaults.
depsRequired still seems necessary, if we want discoverable dependencies.

Maybe one can get rid of deps in most cases by providing a default implementation for deps that intersects depsRequired with a default dependencySet like pkgs.

A package module would then look like this:

{config, lib, ...}: {
  name = "test";
  # all required deps must always be declared
  depsRequired = {
    foo = true;
    bar = true;
    bash = true;
  };
  # `deps` is only required for picking non-default versions
  # This logic is specific to the repo the package resides in, but can easily
  #   be removed by a `mkForce`.
  deps = {pkgs, ...}: {
    # pick a special version for bar
    bar = pkgs.bar_2;
  };
}

@roberth
Copy link
Contributor

roberth commented Dec 22, 2022

Out in the decentralized wild, it might be more valuable to let the caller satisfy the dependencies, which requires that requirements are discoverable in some way.

I don't think the change will be huge. A lot of people already live in a decentralized wild (without Nix) and they rely on distributions to provide such things as docker base images. Even a gentoo user doesn't stitch together all dependencies by hand. (I think? It's been over a decade.)

depsRequired still seems necessary, if we want discoverable dependencies.

or mkOption, right?

depsRequired

types.enum [ true ];? types.submodule {}?
I never know what to make of sets of names.

  # `deps` is only required for picking non-default versions
  # This logic is specific to the repo the package resides in, but can easily
  #   be removed by a `mkForce`.
  deps = {pkgs, ...}: {
    # pick a special version for bar
    bar = pkgs.bar_2;
  };

Not sure if going in circles, but what about depsRequired = lib.attrNames (lib.lazyFunction config.deps (throw "The attribute set returned by depsmust have a static set of attribute names, so nooptionalAttrsor anything else that may depend on the arguments of thedeps function."))?

I should probably merge this, shouldn't I:

@DavHau
Copy link
Owner Author

DavHau commented Dec 24, 2022

I don't think the change will be huge. A lot of people already live in a decentralized wild (without Nix) and they rely on distributions to provide such things as docker base images. Even a gentoo user doesn't stitch together all dependencies by hand. (I think? It's been over a decade.)

Even if you have a community maintained package set, where the community picks one good version for everything, that approach only scales so far. There will always be the necessity to assemble environments with dependency versions different from the ones found in the community set. Language specific environments are the best example for that.

Customizing dependency trees can be done by hand or by a resolver, but in any case, to support this well, the requirements of a package should be discoverable.
Currently this is not the case in nixpkgs, which is one major reason for the big gap between the automated language2nix approaches and nixpkgs. Combining the two is hard. One is either in this world or in that world.

It would be nice if mixed approaches would be supported, so that users can satisfy their requirements, by taking some packages from nixpkgs and others from elsewhere (auto-generated, or different repos, etc...). Automatically.

Expanding a bit on the depsRequired idea, we could have, for example:

{
  depsRequired = {
    foo = "^2.0.0";
    bar = "*";
    baz = "~2.0";
  };
}

If packages had that, mixing dynamic language2nix approaches with nixpkgs would be a charm. Knowing more about a package is beneficial for automation. And automation is key to scale.

depsRequired still seems necessary, if we want discoverable dependencies.

or mkOption, right?

depsRequired is creating options for deps behind the scenes. It's equivalent of using mkOption. It just requires less typing.

Anyways, I might be biased by dealing with language2nix too much, and I do not want to pollute the core of drv-parts with anything that we don't agree is absolutely necessary.

@DavHau
Copy link
Owner Author

DavHau commented Dec 24, 2022

I now opened #11 which is a more minimal change, just allowing nested options for deps.
The new PR:

  • doesn't add any additional options like depsRequired etc.
  • keeps the deps = {pkgs, ...}: {inherit (deps) ...;} style intact

@blaggacao
Copy link

so this implementation / PR will be closed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants