hercules-ci / flake-parts

❄️ Simplify Nix Flakes with the module system
https://flake.parts
MIT License
699 stars 38 forks source link

Per system option with a different name? #205

Open khaled opened 6 months ago

khaled commented 6 months ago

Building on flake-parts, I'd like to create some base modules that allow developers to define "projects", like so:

{
  some-config = "some-value";
  projects.foo = {
    go.enable = true;
  };
}

Projects will translate to a set of packages on the flake, along with a devshell, based on the language choice. Customization will be possible, e.g.:

{
  some-config = "some-value"; 
  projects.foo = {pkgs, ...}: {
    go.enable = true;
    shell.packages = [pkgs.bar];
  };
}

So far, using flake-parts, the best I can come up with is:

{
  some-config = "some-value";
  perSystem = {pkgs, ...}: {
    projects.foo = {
      shell.packages = [pkgs.bar];
    };
  };
}

This works OK, but I'm hoping to remove the need for perSystem here, as projects are inherently perSystem. I'm guessing I might be able to use mkPerSystem option in some way, but thus far haven't been able to figure out how. Suggestions would be appreciated!

roberth commented 6 months ago

What behavior do you intend for projects, or what are the problems you want to solve with it?

I have an idea of what a project could be, so excuse me if I'm going off in a wildly different direction with the following...

So I do know of a pattern, such as in haskell-flake where you can have multiple perSystem.haskellProjects.<name>, and basically all the action happens within the context of those. Haskell-flake then takes care of adding prefixes to the generated flake outputs, to disambiguate them when you have multiple projects (except for haskellProjects.default, which does not receive a prefix). Probably this pattern could be factored out, reducing the boilerplate that modules like haskell-flake need to reinvent, and making language integrations more consistent. Not sure if that's what you have in mind, whether it helps with your use case.

Alternatively, maybe perSystem.projects should be something akin to NixOS specialisations, but with inheritParentConfig turned off, and instead of symlinking toplevels, it could perform the prefixing and merging similar to what I've described for haskell-flake. Instead of e.g.

haskellProjects.default.basePackages = pkgs.haskell.packages.ghc948;

users could write

projects.default = {
  imports = [ inputs.haskell-flake.flakeProjectModule ]; # typically just one import. different projects for each language?
  basePackages = pkgs.haskell.packages.ghc948;
};

Meanwhile haskell-flake or any other reusable project / perSystem-level module could write carelessly to options such as packages.default, because merging projects isn't its concern anymore.

The latter syntax is a bit more verbose, but might become very familiar if a module-based solution to the portable service layer takes off. What's nice though is that it makes a bit more intuitive that all projects eventually share a namespace, of flake output prefixes. Nonetheless, the prior syntax could be preserved as an alias for doing the latter.

What's not immediately obvious to me is how project should refer to one another, as what I've described so far basically jails those modules. Maybe it should just be up to the "user" to link up any projects if needed.

perSystem = { config, ... }: {
  projects.foo.extraDeps = [ config.projects.bar.packages.default ];
  projects.bar = { imports = ...; };
};

Projects could also be nested. Their behavior is like a function that siphons stuff from one perSystem-like context to another perSystem-like context - no reason why that wouldn't compose, or even compose automatically without implementing anything extra. Interesting.

khaled commented 6 months ago

@roberth thanks for the thoughtful response!

So I do know of a pattern, such as in haskell-flake where you can have multiple perSystem.haskellProjects., and basically all the action happens within the context of those.

Haskell-flake looks similar to what I've implemented thus far! The difference is that, as you picked up on and discussed above, the project concept is generic, and submodules define toolchain-specific options and functionality. I've written a few of these modules - e.g. for python+poetry, rust, and golang. Currently they are somewhat rudimentary and opinionated :)

Each project produces a single "top-level" package named project-name, and optionally, any number of "sub-packages", namespaced as project-name/sub-name. Various modules define such sub-packages, leading to, for example, being able to set oci-image.enable = true in a project and get a package named project-name/oci-image. Similarly, setting k8s.enable = true gets you a project-name/k8s-manifests.

devShells build on devenv (using its flake module) and set appropriate options depending on the chosen toolchain. They also give you a few standard commands for iterative development (e.g. watch + incremental rebuild + restart).

Meanwhile haskell-flake or any other reusable project / perSystem-level module could write carelessly to options such as packages.default, because merging projects isn't its concern anymore.

If I'm understanding you correctly, this sounds similar to what my project-level <toolchain.nix> modules do now. That is, they set a project level config.package attribute. This is the single "top-level" package I described before. Other modules define sub-packages; for example oci-image.nix defines config.subPackages.oci-image. Together package and subPackages get reduced into a flattened slash-delimited package set by the "root" project module.

What's not immediately obvious to me is how project should refer to one another

Your subsequent example looks good to me :)

Projects could also be nested. Their behavior is like a function that siphons stuff from one perSystem-like context to another perSystem-like context

Now that is very interesting, kind of an expansion upon sub-packages. If projects define shells, do nested projects result in namespaced devShells?

roberth commented 6 months ago

If projects define shells, do nested projects result in namespaced devShells?

Yes, I think all output-inspired options should translate to actual flake outputs, whether that's packages, devShells or whatnot. For legacyPackages I think it should be nesting instead of prefixing to achieve namespacing.

khaled commented 5 months ago

Implemented a proof of concept of the approach I was aiming for, where you can do something like:

fp.lib.mkFlake {inherit inputs;} {
  projects.sample = {pkgs, ...}: {
    go.enable = true;
    shell.packages = [pkgs.delve];
  };
}

So projects.sample here is like a perSystem option that gets evaluated in its own scope. Feedback welcomed...

lenianiva commented 9 hours ago

Is there a way to achieve this with the existing flake parts infrastructure? If I have a project output and I put it under packages, flake parts will complain:

       error: A definition for option `perSystem.x86_64-linux.packages.project' is not of type `package'. Definition values:
       - In `/nix/store/0s79b111100r9zpcx8cpmw3xdgrx1y33-source/flake.nix, via option perSystem':
           {
             allExternalDeps = [
               {
                 allExternalDeps = [
                   {