Expose repo with npins and flakes compatibility at the same time

Personally, I do use npins instead of flakes. However, many people use flakes and for public repositories, I would also like to include a ready-to-use flake.nix file that exposes the content of my package.nix.

Is there a nice way to set up a repository that works both with npins and flakes, without having the complexity of maintaining two lock files?

Here is how I structure my own projects:

# package.nix: Classic nixpkgs style package
{ lib, stdenv, ... }:

stdenv.mkDerivation {
  pname = "...";
  version = "...";

  src = lib.cleanSource ./.;

  # rest of derivation 
}
# default.nix
let
  sources = import ./npins;
  pkgsDefault = import sources.nixpkgs { };
in

{ pkgs ? pkgsDefault }:

pkgs.callPackage ./package.nix { }
# shell.nix
let
  sources = import ./npins;
  pkgsDefault = import sources.nixpkgs { };
in

{ pkgs ? pkgsDefault }:

pkgs.mkShell {
  packages = [
    # packages dev environment
  ];
}

Depending on the complexity of the project, I can have multiple smaller package.nix files and the default.nix can do some more plumbing.

If I want to allow ingesting the repository as a flake, I can just hook up the individual parts in a small flake.nix like so:

# flake.nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    flake-parts.url = "github:hercules-ci/flake-parts";
  };

  outputs =
    inputs@{ flake-parts, ... }:
    flake-parts.lib.mkFlake { inherit inputs; } {
      systems = [
        "x86_64-linux"
        "aarch64-linux"
        "aarch64-darwin"
      ];

      perSystem = { pkgs, ... }: {
        packages.default = import ./default.nix { inherit pkgs; };
        devShells.default = import ./shell.nix { inherit pkgs; };
      };
    };
}

The only problem is that I now have to maintain two lock file mechanisms (npins and flake.lock). Is there any way to simplify this further?

One idea I had is that I could have a justfile command that somehow creates a flake.lock file from the npins lock. I noticed that there are nix flake lock and nix flake update commands, but I am unsure if they could help me here.

You can just get users to use the flake = false setting if they want to use your project as an input, and get them to do import <inputname> { pkgs = nixpkgs.legacyPackages.${system}; } to get a handle on your project objects.

As long as you allow passing in a custom args to default.nix to override your npins inputs there’s no real need to integrate with flake input/output semantics.

2 Likes

If you’re not using Flakes yourself the easiest solution is to gitignore flake.lock.

Also: flake-parts is completely unnecessary here. I’d simplify it to:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  };

  outputs =
    { nixpkgs }:
    let
      forAllSystems = lib.genAttrs lib.systems.flakeExposed;
      inherit (nixpkgs) lib;
    in
    {
      devShells = forAllSystems (
        system:
        let
          pkgs = nixpkgs.legacyPackages.${system};
        in
        {
          default = pkgs.callPackage ./shell.nix { };
        }
      );

      packages = forAllSystems (
        system:
        let
          pkgs = nixpkgs.legacyPackages.${system};
        in
        {
          default = pkgs.callPackage ./default.nix { };
        }
      );
    };
}
3 Likes

Wow, thanks to both of you! Initially, I thought my question was a bit weird, because one could also just update both lock files in lock step, but the answers really helped my understanding of flakes :slight_smile:

@adisbladis: Do I understand it correctly, that when people use my flake as an input, they anyway would not use the locked dependencies from my flake.lock file, but instead would record all the transitive dependencies in the lock file of the parent flake? In other words, this mirrors other programming languages’ tooling where one usually only commits the lock file for binary applications (roots of the dependency tree), but not necessarily for shared libraries (because libraries are not run standalone, but are imported from other parent projects)?

Also thanks for the recommendation of a version without flake-parts. One reason for sticking to npins is that flakes always look much more complicated to me, so seeing a more straight-forward version helps. I also did not know about lib.systems.flakeExposed (and it did not show up in noggle). I will switch to this version.

For reference, here is an example of @TLATER’s solution where a flake consumes a non-flake input by just passing through the package:

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
  
    demo.url = "...";
    demo.flake = false;
  };
  outputs = { self, nixpkgs, demo }:
    {
      packages.x86_64-linux.default = import demo { pkgs = nixpkgs.legacyPackages.x86_64-linux; };
    };
}

As my goal was to aid users of nix flakes while also supporting the “traditional” style, I think I will stick to @adisbladis’s solution and just add a small flake.nix file without bothering about committing the lock file.

Do I understand it correctly, that when people use my flake as an input, they anyway would not use the locked dependencies from my flake.lock file, but instead would record all the transitive dependencies in the lock file of the parent flake?

By default Flakes reuses the whole dependency graph of an input verbatim (as locked by the upstream). This is widely seen as one of the design warts of Flakes and why you’ll commonly see more follows lines in a flake.nix than actual dependency inputs.

This article is a decent explanation: 1000 instances of nixpkgs - zimbatm.
It talks more about overlays, but also talks a bit about Flake inputs & let’s you understand the issue structurally.

1 Like

That’s why it’s so surprising that not committing flake.lock even works.

I guess nix decides to simply resolve the url as if it weren’t locked in that case, and just grabs whatever nix flake lock would have locked the first time the downstream flake resolves the input. I’m assuming you’re right that it works, I personally think that this shouldn’t be a valid flake given the point of lockfiles.

Presumably @fgrsnau wanted to avoid un-pinned inputs, given how much they’re focused on syncing the lockfiles.

If that’s the case, @adisbladis ’ suggestion can still work, you’ll just have to manually add the correct ?rev to the input url, and probably find a way to sync that from your npins. Still not properly pinned, but it should be reproducible.