NixOS / nix

Nix, the purely functional package manager
https://nixos.org/
GNU Lesser General Public License v2.1
11.97k stars 1.47k forks source link

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

Open roberth opened 1 year ago

roberth commented 1 year ago

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

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

Describe the solution you'd like

From what I read and recall from the discussion of https://github.com/NixOS/nix/pull/6773#issuecomment-1381832767, we were close to an agreement to extend the cli instead of the flake core.

Compared to that PR

E.g. instead of searching only

... we could search:

... or:

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

Describe alternatives you've considered

Bad implementation ideas:

Not really acting on the issue:

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

Priorities

Add :+1: to issues you find important.

zimbatm commented 1 year ago

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 commented 1 year ago

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

edolstra commented 1 year ago

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 commented 1 year ago
{
  __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: https://github.com/NixOS/nix/compare/master...flox:nix:dynamic_select There are open questions around how to handle defaults, attrset updates, attrNames/attrValues.

alyssais commented 1 year ago

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 commented 1 year ago

@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:

Potential percieved disadvantages:

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 commented 1 year ago

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 commented 9 months ago

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:

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.