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

Extend the flake / cli interface with a function for system-specific attributes #7709

Open
5 tasks
roberth opened this issue Jan 29, 2023 · 8 comments
Open
5 tasks
Labels
feature Feature request or proposal flakes new-cli Relating to the "nix" command

Comments

@roberth
Copy link
Member

roberth commented Jan 29, 2023

Is your feature request related to a problem? Please describe.

The flake schema suggested by the flake / cli interface has two usability problems

  • Does not support any systems that are not enumerated. Because of this, new system types are not usable with flakes, making the introduction of new system types harder than necessary.
  • All supported systems must be enumerated. This adds learning overhead and a false sense of reliability.

Describe the solution you'd like

From what I read and recall from the discussion of #6773 (comment), we were close to an agreement to extend the cli instead of the flake core.

Compared to that PR

  • do not touch call-flake.nix
  • extend the search path for e.g. packages to include the possibility of a function call

E.g. instead of searching only

  • packages.${system}.${installable}
  • legacyPackages.${system}.${installable}
  • ${installable}

... we could search:

  • (perSystem system).packages.${installable}
  • (perSystem system).${installable}
  • packages.${system}.${installable}
  • legacyPackages.${system}.${installable}
  • ${installable}

... or:

  • getPackage system installable if not null
  • (getPackages system).${installable} or null if not null
  • packages.${system}.${installable}
  • legacyPackages.${system}.${installable}
  • ${installable}

The latter search path allows for optimal laziness, which is most relevant for non-trivial flakes that use a framework, while also supporting enumeration for nix flake show and CI.

TODO

  • get this idea and implementation strategy approved in the Nix Maintainer Team
  • extend the cli installable lookup algorithm to call such functions
  • make it work
  • extend the eval cache to allow the calling of functions with simple arguments
  • consider reviving the memoization branch?
  • make nix flake show work, by invoking for the current system
  • make nix flake show --for-supported-systems work, by querying an new output attribute supportedSystems
  • document the extended schema

Describe alternatives you've considered

Bad implementation ideas:

  • Call packages as a function. This does not allow a flake to be compatible with both calling conventions, breaking compatibility with previous cli versions and (worse) breaking compatibility with flakes using the flake as a dependency.

Not really acting on the issue:

  • Only fix the "exotic system" problem by trying to convince users to implement a non-cli schema on their own. This will fail because there is little incentive, and no easy way to test correctness.

  • Only fix the enumeration problem by making all flakes regurgitate a list that Nixpkgs provides. This is only fixes half of the problem and makes apparent set of supported systems completely useless.

Additional context
Add any other context or screenshots about the feature request here.

Priorities

Add 👍 to issues you find important.

@zimbatm
Copy link
Member

zimbatm commented Jan 30, 2023

A good starting point would be to have the list of supported systems be part of the flake metadata. That way, it becomes inspectable with nix flake show. Inputs could be checked for systems compatibility. Systems could be overridden.

{
  systems = ["x86_64-linux" "aarch-64linux"];
  outputs = { self, nixpkgs, ... }:
    {
      # system would be attached to `self` 
      packages = nixpkgs.lib.genAttrs self.systems (system: /*...*/);
    };
}

I think your proposal is also nicely complementary to this one.

@roberth
Copy link
Member Author

roberth commented Jan 30, 2023

Team discussion: we've agreed that we want to experiment with function-like behavior for these attributes under a new feature flag.

@edolstra
Copy link
Member

edolstra commented Jan 30, 2023

Note from the team discussion:

It would be nice to have a functor-like interface for having attrset-like objects that are implemented by functions, i.e. allowing dynamically computed sets of attributes. Something like:

{
  __getAttr = key: ... return value ...;
  __listAttrs = [ ... list of keys ... ];
}

where __listAttrs could be optional for infinite or open-ended attrsets.

This way, a flake attribute like packages remains (morally) an attrset of packages per system type and could be defined like:

{
  packages = {
    __getAttr = system: {
      hello = (import nixpkgs { inherit system; }).hello;
    };
  };
```

@tomberek
Copy link
Contributor

tomberek commented Feb 2, 2023

{
  __getAttr = key: ... return value ...;
  __listAttrs = [ ... list of keys ... ];
}

where __listAttrs could be optional for infinite or open-ended attrsets.

A dynamic attrset would be a fairly large and powerful change, beyond the scope of this issue. Is this something feasible to add to the language without breakage? On its own it would provide a new avenue to expose better interfaces to users, at the cost of a complex feature. (Though I’d love to explore the consequences!)

Edit: Proof-of-concept: master...flox:nix:dynamic_select
There are open questions around how to handle defaults, attrset updates, attrNames/attrValues.

@alyssais
Copy link
Member

alyssais commented Feb 2, 2023

One problem I've been thinking about a lot recently, as I've been working heavily on cross-compilation, is that increasingly, Nixpkgs supports platforms that cannot be expressed as simple strings, but Nix hasn't really kept up with this. For example, how are we going to express { system = "armv5tel-linux"; gcc.fpu = "vfpv2"; useLLVM = true; } as an attribute name? Either we start exhaustively listing all possible systems that can be built for, which takes away the useful flexibility Nixpkgs' current setup provides, or we have to come up with some sort of string-based encoding for platforms, that a __getAttr implementation would have to parse.

With non-Flakes Nix, Nixpkgs can support these custom platforms just fine. They're easy to use with --arg on the CLI. I don't see a way to support the same level of functionality in Flakes without making something in the flake definiton a function that takes a platform specification. (But am open to being proven wrong!)

@roberth
Copy link
Member Author

roberth commented Feb 3, 2023

@tomberek maybe open an issue or draft pr?

A possible half-way point is to have a schema and/or convention that allows any pkgs to be injected. It's only a half-way point though because the cli shouldn't know how to construct a value for pkgs, making this idea expression-only.

By changing the implicit CLI expression to something like the following, we could immediately solve the cross problem for expressions that need cross. (leaving the CLI question somewhat unanswered for now, which might be desirable for this issue?)

# nix build installable =>
with calledOutputs;
(configure { system = system; }).packages.${installable}

This way, a flake expression can also accept a custom nixpkgs:

{
  outputs = { nixpkgs, ... }: {
    configure = { system, pkgs ? nixpkgs.legacyPackages.${system} }: {
      packages.installable = pkgs.stdenv.mkDerivation ...;
    };
  };
}

Benefits:

  • does not add or suggest a hint at a language feature that could be considered out of scope (even though I like the idea)
  • schema is readily suitable to wider configurability than just system (--argstr hostPlatform '{ .... }'?)
  • cli level caching still only needs to support arguments it wants to model (while leaving the schema open for more advanced usages, if desired later)
  • sharing between things that are configured in the same way is easy, as everything is in a single function body
  • configure is a nice division line between the realm of things that are concrete and things that are configurable

Potential percieved disadvantages:

  • introduction of a hostPlatform parameter (or such) may still require a each flake to adapt their pkgs default. For flakes that use a framework like flake-parts, this would only be an update away, so it doesn't seem like much of an issue. Part of this "problem" is inherent to the way manually written flakes are written anyway.
  • transposing the attribute path (e.g. ${system}.application <-> application.${system}) might feel like a complication for flakes that weren't already written in a transposed style with e.g. flake-utils forEachSystem, though I'm sure they won't miss the ${system} lookups that used to be sprinkled everywhere in their flake. (and again, such lookups wouldn't work with any sane cross compilation support)

Maybe relevant; flake-parts already benefits from treating perSystem in exactly the way I suggest to treat configure here. It allows an overlay to be derived without {,re}defining them in an overlay. The result is not an idiomatic overlay, as it typically won't use the final parameter a lot, but arguably that's a good thing. "Function from Nixpkgs pkgs to locally defined packages" was already a thing in the wild, proving that something like it is useful.

@nixos-discourse
Copy link

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

https://discourse.nixos.org/t/nix-systems-externally-extensible-flake-systems/27110/9

@jeff-hykin
Copy link

jeff-hykin commented Nov 5, 2023

This platform/system issue is the sole reason I haven't really adopted flakes, so I'm really glad to see it being discussed seriously (and if it takes 10 years then so be it; I'm totally in support of the team ensuring this done right rather than done quick).

Correct me if I'm missing one but I believe the current goals/design-constraints are:

  1. Not have systems-as-strings hard-coded as part of the flake spec
  2. Have system requirements be serializable without building the output. E.g. knowing a package "supports x86" both for flake metadata/lockfile reasons and search tools package-indexing reasons.
  3. Support "no system" packages (ex: an image dataset doesn't care about system)
  4. Support complex requirements (ex: requiring Intel AVX support)
  5. Allow gracefully adding new operating systems and chips (no complex override gymnastics or need to fork a package)
  6. Support partial requirements (ex: require Intel AVX without specifying darwin/linux/openBSD)
  7. Support cross-compilation (don't forcefully use the host system)

I don't see a way to support the same level of functionality in Flakes without making something in the flake definition a function that takes a platform specification. (But am open to being proven wrong!)

@alyssais Challenge accepted! I think I have a structure (and lots of viable variations) for you that meets all the goals, and does it without functions.

For a moment, imagine an alternative to system that is an attrSet. For example, linux_x86_64 would be something like {kernel="linux";cpu="x86";bitness=64;}. Then flakes could look something like:

{
  inputs = ...;
  outputs = { self, nixpkgs }:
    {
      defaultPackage = [
        {
          # NOTE: intentionally does not mention bit-ness
          systemMatch = { kernel="linux"; cpu = "arm"; };
          derivation = stdenv.mkDerivation {...}
        }
        {
          systemMatch = { kernel="darwin"; };
          derivation = stdenv.mkDerivation {...}
        }
      ]
    }
}

The list order matters, and the systemMatch is just checking if it's a valid subset of the host (or cross compile target). Meaning if someone put systemMatch = {}; at the top (e.g. "no system requirements"), then none of the other builds in the list would be evaluated.

With one extension, we can make this interface handle custom system/platform checks. We can do it using derivations:

  • Any name inside a systemMatch that is a valid nix store path is assumed to be path to an executable.
  • If the executable runs with an exit code of 0 then the system meets the custom constraint.
  • The "exit 0" can be permanently cached, making it extremely low overhead.

How that might look;

defaultPackage = [
  {
     systemMatch = {kernel="linux"; "${pkgs.checkAvxSupport}/out/check"=true;};
      derivation = stdenv.mkDerivation {...};
  }
]

I believe this meets all the design constraints, and is a balance between the 100% non-lazy approach (a function with a system argument), and the very-lazy but non-extensible/flawed-enumeration approach (${system}.packages) proposed at the top.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Feature request or proposal flakes new-cli Relating to the "nix" command
Projects
None yet
Development

No branches or pull requests

7 participants