[RFC] Nix function that overrides a scope, with automatic inheritance propagation

So today I wanted to use kdePackages.overrideScope. Here’s how it looked like at first:

let
  kdePackages' = kdePackages.overrideScope(self: super: {
    # Fix for....
    qtdeclarative = super.qtdeclarative.overrideAttrs(new: old: {
      patches = old.patches ++ [
        (fetchpatch {
          url = "https://github.com/qt/qtdeclarative/commit/9d4d376726a6ce15c429128dc65b927e411e40da.patch";
          hash = "sha256-XhfliF5wZuN4/E55f8hfipIRjxBe9V7vL1cgn5p4xqA=";
        })
      ];
    });
    qtscxml = super.qtscxml.override {
      inherit (self) qtdeclarative;
    };
    qtwayland = super.qtwayland.override {
      inherit (self) qtdeclarative;
    };
    qt5compat = super.qt5compat.override {
      inherit (self) qtdeclarative;
    };
    qttools = super.qttools.override {
      inherit (self) qtdeclarative;
    };
  });
in

The hard part is to find the expressions of each kdePackages attribute used in the actual mkDerivation call below in, and decide whether it needs or not an .override { inherit (self) qtdeclarative; }; in the scope override function above. This experience is prone to produce a derivation that requires two different qtdeclarative derivations, and hence buggy.

Thankfully with .override.__functionArgs one can automatically detect which other attributes will need to inherit from (self) the overridden attribute. With an LLM I wrote:

  overrideScopeFully =
    s: scopeOverrideFunc:
    let
      partiallyOverriddenScope = s.overrideScope scopeOverrideFunc;
      directlyOverriddenAttrs = builtins.attrNames (scopeOverrideFunc partiallyOverriddenScope s);
    in
    builtins.mapAttrs (
      attrName: pkg:
      pkg.override (
        lib.pipe directlyOverriddenAttrs [
          (builtins.filter (
            oAttr:
            # Don't include in this filter the attribute `attrName` from the
            # full scope, if it is already part of the _directly_ overridden
            # attributes.
            !(builtins.elem attrName directlyOverriddenAttrs)
            && pkg ? override
            # Continue with the creation of the `.override` arguments only for
            # overridden attributes (`oAttr`) which are possible arguments to the
            # `.override` function of the `pkg` at hand.
            && pkg.override.__functionArgs ? ${oAttr}
          ))
          # Generate the `.override` argument using the attribute names `aNames`
          (aNames: lib.genAttrs aNames (oAttr: partiallyOverriddenScope.${oAttr}))
        ]
      )
    ) partiallyOverriddenScope;

Which makes the overrideScope example above be:

  kdePackages' = overrideScopeFully kdePackages (
    self: super: {
      # Fix for: https://github.com/NixOS/nixpkgs/issues/526825
      # reported upstream at: https://github.com/musescore/MuseScore/issues/33015
      qtdeclarative = super.qtdeclarative.overrideAttrs (
        new: old: {
          patches = old.patches ++ [
            (fetchpatch {
              url = "https://github.com/qt/qtdeclarative/commit/9d4d376726a6ce15c429128dc65b927e411e40da.patch";
              hash = "sha256-XhfliF5wZuN4/E55f8hfipIRjxBe9V7vL1cgn5p4xqA=";
            })
          ];
        }
      );
    }
  );

And there’s no worry to forget a (self) inheritance.

I wonder if there are edge cases where this doesn’t work, and I’d like to add this to nixpkgs.lib. What do you think?

(without looking up the definition of the scope) wai twhy did you have to explicitly override all of the other qt* attributes, are they really not defined by self.callPackage?

1 Like

Mhm I can’t immediately decipher addPackages and why the custom application of newScope instead of using the auto-generated callPackage definition, but aside from that I see e.g. qtwayland taking qtdeclarative as argument, and being defined simply as qtwayland = callPackage ./modules/qtwayland.nix { };, meaning all but the first .override in your snippet should be redundant, unless the weirdness in the beginning of addPackages is smh relevant? EDIT: Might be just the new scope preserving references to the old splicing scopes

1 Like

I have something similar but you would need to create a file for the override to be recognized (i have a local override of 30 pkgs in ocamlPackages scope)

The overall idea is to directly callPackags with overridden final fixedpoint and inject pkgsPrev (to prevent infinite recursion) + pkgsFinal (this is the callPackage arg, but injecting just in case) into the called file

here are the relevant files

p.s. im already in bed and just checking the forum one last time before sleep, i’ll add more context tomorrow

Admittedly, the scope overriding for qt6/qt6Packages/kdePackages is a tangled mess, but why do this horrible dependency tracing when you can achieve the same thing with only this overlay?

self: super: {
  qt6 = super.qt6.overrideScope (qt6Self: qt6Super: {
    qtdeclarative = qt6Super.qtdeclarative.overrideAttrs (new: old: {
      patches = old.patches ++ [
        (self.fetchpatch {
          url = "https://github.com/qt/qtdeclarative/commit/9d4d376726a6ce15c429128dc65b927e411e40da.patch";
          hash = "sha256-XhfliF5wZuN4/E55f8hfipIRjxBe9V7vL1cgn5p4xqA=";
        })
      ];
    });
  });
}

Check the hashes on kdePackages.qtscxml.outPath, etc.; they are the same with this and with your solution.

3 Likes

OK so it was the stale splicing scopes, which you get rid of by reinstantiating nixpkgs with an overlay

I believe to you @rhendric that this overlay works when you use it outside of Nixpkgs, but how would you inject it into musescore/package.nix? Evidently the musescore derivations’ hashes are different if I only modify qtdeclarative inside kdePackages.overrideScope: Checkout the branch at #527324, and apply this diff:

diff --git i/pkgs/by-name/mu/musescore/package.nix w/pkgs/by-name/mu/musescore/package.nix
index d3ac130aa3d3..0e06d6baf46b 100644
--- i/pkgs/by-name/mu/musescore/package.nix
+++ w/pkgs/by-name/mu/musescore/package.nix
@@ -35,36 +35,7 @@
 }:
 
 let
-  # TODO(@doronbehar): Contribute this one day to lib/? See:
-  # https://discourse.nixos.org/t/rfc-nix-function-that-overrides-a-scope-with-automatic-inheritance-propagation/78025
-  overrideScopeFully =
-    s: scopeOverrideFunc:
-    let
-      partiallyOverriddenScope = s.overrideScope scopeOverrideFunc;
-      directlyOverriddenAttrs = builtins.attrNames (scopeOverrideFunc partiallyOverriddenScope s);
-    in
-    builtins.mapAttrs (
-      attrName: pkg:
-      pkg.override (
-        lib.pipe directlyOverriddenAttrs [
-          (builtins.filter (
-            oAttr:
-            # Don't include in this filter the attribute `attrName` from the
-            # full scope, if it is already part of the _directly_ overridden
-            # attributes.
-            !(builtins.elem attrName directlyOverriddenAttrs)
-            && pkg ? override
-            # Continue with the creation of the `.override` arguments only for
-            # overridden attributes (`oAttr`) which are possible arguments to the
-            # `.override` function of the `pkg` at hand.
-            && pkg.override.__functionArgs ? ${oAttr}
-          ))
-          # Generate the `.override` argument using the attribute names `aNames`
-          (aNames: lib.genAttrs aNames (oAttr: partiallyOverriddenScope.${oAttr}))
-        ]
-      )
-    ) partiallyOverriddenScope;
-  kdePackages' = overrideScopeFully kdePackages (
+  kdePackages' = kdePackages.overrideScope (
     self: super: {
       # Fix for: https://github.com/NixOS/nixpkgs/issues/526825
       # reported upstream at: https://github.com/musescore/MuseScore/issues/33015

You’d get a different hash (and the bug is not fixed).

overlay

maybe i was too sleepy lmao, i forgot everything is consumed in a overlay

Inside Nixpkgs, of course you’d want to add the patch to qt6 directly. I see you did that in a staging PR, and wanted to have your cake and eat it too.

Note that your solution doesn’t actually cover everything that indirectly depends on qtdeclarative. Compare the hashes for kdePackages'.wrapQtAppsHook with your solution versus patching qt6 directly. You’re only handling one narrow kind of dependency. Maybe that’s enough for musescore to work, but in general I wouldn’t promote this pattern.

qt6Packages and kdePackages depend on each other in a weird and circular way, and I don’t understand why it was written that way. Might be something the cross-compilation experts know about. In the ideal world, all you ought to need is kdePackages.override { qt6Packages.override { qt6 = ...; }; }, maybe with some extra kdePackages = kdePackages';s to tie the knot, but because of how weird qt6Packages is, that isn’t possible. I’d start with trying to untangle that rather than introducing a library function that does this hacky thing.

2 Likes

@rhendric thanks for the explanation, I think I understand why you think the pattern I showed is not necessarily good. However I have another evidence that it might be needed even for qt6Packages:

Take the current branch master, and apply this commit:

commit fd0a2f2a0984b5a67c67fa8ec8c0093a6d239e39
Author: Doron Behar <doron.behar@gmail.com>
Date:   Thu Jun 4 14:14:28 2026 +0300

    musescore: use qt6Packages
    
    As no kdePackages specific packages are needed

diff --git a/pkgs/by-name/mu/musescore/package.nix b/pkgs/by-name/mu/musescore/package.nix
index 0deadb2f1cfe..4fba53caaaa1 100644
--- a/pkgs/by-name/mu/musescore/package.nix
+++ b/pkgs/by-name/mu/musescore/package.nix
@@ -15,7 +15,7 @@
   alsa-plugins,
   flac,
   freetype,
-  kdePackages,
+  qt6Packages,
   lame,
   libjack2,
   libogg,
@@ -64,7 +64,7 @@ let
         ]
       )
     ) partiallyOverriddenScope;
-  kdePackages' = overrideScopeFully kdePackages (
+  qt6Packages' = overrideScopeFully qt6Packages (
     self: super: {
       # Fix for: https://github.com/NixOS/nixpkgs/issues/526825
       # reported upstream at: https://github.com/musescore/MuseScore/issues/33015
@@ -142,8 +142,8 @@ stdenv.mkDerivation (finalAttrs: {
 
   nativeBuildInputs = [
     cmake
-    kdePackages'.qttools
-    kdePackages'.wrapQtAppsHook
+    qt6Packages'.qttools
+    qt6Packages'.wrapQtAppsHook
     ninja
     pkg-config
   ]
@@ -156,12 +156,12 @@ stdenv.mkDerivation (finalAttrs: {
   buildInputs = [
     flac
     freetype
-    kdePackages'.qt5compat
-    kdePackages'.qtbase
-    kdePackages'.qtdeclarative
-    kdePackages'.qtnetworkauth
-    kdePackages'.qtscxml
-    kdePackages'.qtsvg
+    qt6Packages'.qt5compat
+    qt6Packages'.qtbase
+    qt6Packages'.qtdeclarative
+    qt6Packages'.qtnetworkauth
+    qt6Packages'.qtscxml
+    qt6Packages'.qtsvg
     lame
     libjack2
     libogg
@@ -178,7 +178,7 @@ stdenv.mkDerivation (finalAttrs: {
   ]
   ++ lib.optionals stdenv.hostPlatform.isLinux [
     alsa-lib
-    kdePackages'.qtwayland
+    qt6Packages'.qtwayland
   ];
 
   strictDeps = true;

And then note the musescore.drvPath is the same as before:

/nix/store/rz8hghkliz9v10c5ay4xm4nh4swvsgsr-musescore-4.7.2.drv

And if you’d apply:

diff --git i/pkgs/by-name/mu/musescore/package.nix w/pkgs/by-name/mu/musescore/package.nix
index 4fba53caaaa1..0e52f1aae89d 100644
--- i/pkgs/by-name/mu/musescore/package.nix
+++ w/pkgs/by-name/mu/musescore/package.nix
@@ -35,36 +35,7 @@
 }:
 
 let
-  # TODO(@doronbehar): Contribute this one day to lib/? See:
-  # https://discourse.nixos.org/t/rfc-nix-function-that-overrides-a-scope-with-automatic-inheritance-propagation/78025
-  overrideScopeFully =
-    s: scopeOverrideFunc:
-    let
-      partiallyOverriddenScope = s.overrideScope scopeOverrideFunc;
-      directlyOverriddenAttrs = builtins.attrNames (scopeOverrideFunc partiallyOverriddenScope s);
-    in
-    builtins.mapAttrs (
-      attrName: pkg:
-      pkg.override (
-        lib.pipe directlyOverriddenAttrs [
-          (builtins.filter (
-            oAttr:
-            # Don't include in this filter the attribute `attrName` from the
-            # full scope, if it is already part of the _directly_ overridden
-            # attributes.
-            !(builtins.elem attrName directlyOverriddenAttrs)
-            && pkg ? override
-            # Continue with the creation of the `.override` arguments only for
-            # overridden attributes (`oAttr`) which are possible arguments to the
-            # `.override` function of the `pkg` at hand.
-            && pkg.override.__functionArgs ? ${oAttr}
-          ))
-          # Generate the `.override` argument using the attribute names `aNames`
-          (aNames: lib.genAttrs aNames (oAttr: partiallyOverriddenScope.${oAttr}))
-        ]
-      )
-    ) partiallyOverriddenScope;
-  qt6Packages' = overrideScopeFully qt6Packages (
+  qt6Packages' = qt6Packages.overrideScope (
     self: super: {
       # Fix for: https://github.com/NixOS/nixpkgs/issues/526825
       # reported upstream at: https://github.com/musescore/MuseScore/issues/33015

You’d get a different hash:

/nix/store/0ah934n1y6q4q10gnyibif4cy6wiyvqg-musescore-4.7.2.drv

Why is qt6Packages.overrideScope not overriding all packages that use qtdeclarative?

When in current branch master, I applied:

diff --git i/pkgs/by-name/mu/musescore/package.nix w/pkgs/by-name/mu/musescore/package.nix
index 0deadb2f1cfe..c742a757eb82 100644
--- i/pkgs/by-name/mu/musescore/package.nix
+++ w/pkgs/by-name/mu/musescore/package.nix
@@ -34,53 +34,6 @@
   nixosTests,
 }:
 
-let
-  # TODO(@doronbehar): Contribute this one day to lib/? See:
-  # https://discourse.nixos.org/t/rfc-nix-function-that-overrides-a-scope-with-automatic-inheritance-propagation/78025
-  overrideScopeFully =
-    s: scopeOverrideFunc:
-    let
-      partiallyOverriddenScope = s.overrideScope scopeOverrideFunc;
-      directlyOverriddenAttrs = builtins.attrNames (scopeOverrideFunc partiallyOverriddenScope s);
-    in
-    builtins.mapAttrs (
-      attrName: pkg:
-      pkg.override (
-        lib.pipe directlyOverriddenAttrs [
-          (builtins.filter (
-            oAttr:
-            # Don't include in this filter the attribute `attrName` from the
-            # full scope, if it is already part of the _directly_ overridden
-            # attributes.
-            !(builtins.elem attrName directlyOverriddenAttrs)
-            && pkg ? override
-            # Continue with the creation of the `.override` arguments only for
-            # overridden attributes (`oAttr`) which are possible arguments to the
-            # `.override` function of the `pkg` at hand.
-            && pkg.override.__functionArgs ? ${oAttr}
-          ))
-          # Generate the `.override` argument using the attribute names `aNames`
-          (aNames: lib.genAttrs aNames (oAttr: partiallyOverriddenScope.${oAttr}))
-        ]
-      )
-    ) partiallyOverriddenScope;
-  kdePackages' = overrideScopeFully kdePackages (
-    self: super: {
-      # Fix for: https://github.com/NixOS/nixpkgs/issues/526825
-      # reported upstream at: https://github.com/musescore/MuseScore/issues/33015
-      qtdeclarative = super.qtdeclarative.overrideAttrs (
-        new: old: {
-          patches = old.patches ++ [
-            (fetchpatch {
-              url = "https://github.com/qt/qtdeclarative/commit/9d4d376726a6ce15c429128dc65b927e411e40da.patch";
-              hash = "sha256-XhfliF5wZuN4/E55f8hfipIRjxBe9V7vL1cgn5p4xqA=";
-            })
-          ];
-        }
-      );
-    }
-  );
-in
 stdenv.mkDerivation (finalAttrs: {
   pname = "musescore";
   version = "4.7.2";
@@ -142,8 +95,8 @@ stdenv.mkDerivation (finalAttrs: {
 
   nativeBuildInputs = [
     cmake
-    kdePackages'.qttools
-    kdePackages'.wrapQtAppsHook
+    kdePackages.qttools
+    kdePackages.wrapQtAppsHook
     ninja
     pkg-config
   ]
@@ -156,12 +109,12 @@ stdenv.mkDerivation (finalAttrs: {
   buildInputs = [
     flac
     freetype
-    kdePackages'.qt5compat
-    kdePackages'.qtbase
-    kdePackages'.qtdeclarative
-    kdePackages'.qtnetworkauth
-    kdePackages'.qtscxml
-    kdePackages'.qtsvg
+    kdePackages.qt5compat
+    kdePackages.qtbase
+    kdePackages.qtdeclarative
+    kdePackages.qtnetworkauth
+    kdePackages.qtscxml
+    kdePackages.qtsvg
     lame
     libjack2
     libogg
@@ -178,7 +131,7 @@ stdenv.mkDerivation (finalAttrs: {
   ]
   ++ lib.optionals stdenv.hostPlatform.isLinux [
     alsa-lib
-    kdePackages'.qtwayland
+    kdePackages.qtwayland
   ];
 
   strictDeps = true;
diff --git i/pkgs/development/libraries/qt-6/modules/qtdeclarative/default.nix w/pkgs/development/libraries/qt-6/modules/qtdeclarative/default.nix
index 786229c5651b..26a55c54d455 100644
--- i/pkgs/development/libraries/qt-6/modules/qtdeclarative/default.nix
+++ w/pkgs/development/libraries/qt-6/modules/qtdeclarative/default.nix
@@ -43,6 +43,10 @@ qtModule {
       hash = "sha256-ESy35OlmsvI4yFQ/rFT8oelOUBCwCmlcbQJvwcTrCig=";
       revert = true;
     })
+    (fetchpatch {
+      url = "https://github.com/qt/qtdeclarative/commit/9d4d376726a6ce15c429128dc65b927e411e40da.patch";
+      hash = "sha256-XhfliF5wZuN4/E55f8hfipIRjxBe9V7vL1cgn5p4xqA=";
+    })
   ];
 
   cmakeFlags = [

And got the same derivation path:

/nix/store/rz8hghkliz9v10c5ay4xm4nh4swvsgsr-musescore-4.7.2.drv

However to be honest, before checking your claim, I did notice that my overrideScopeFully function indeed can forget to apply overrides in the following case: If A depends on B which depends on qtdeclarative, A’s .override call only gets direct scope members - it doesn’t see that B itself was transitively affected. This however doesn’t affect wrapQtAppsHook because it doesn’t really use qtwayland, although it is part of its override.__functionArgs.

The fix is to recursively look for packages that depend on the directly overridden qtdeclarative. I tried to write something with Claude, but I’m not sure it is good so I won’t post it.

Because qtdeclarative isn’t actually part of the qt6Packages scope or the kdePackages scope. It’s part of the qt6 scope.

When scopes ‘extend’ each other like this, an overrideScope on the extension scope only affects the things defined in the extension, not the things in the scope it extends. So using qt6Packages.overrideScope won’t override anything in the qt6 scope that depends on qtdeclarative. It only overrides the things newly added in the qt6Packages scope that do.

And then kdePackages further extends things.

That’s why overrideScope is the wrong tool for anything other than qt6. You want to be able to use override on those extension scopes, so that the qt6 that they get as an input and merge into their own scopes is already overridden.

Sadly, you can’t, because of the weirdness. So fix the weirdness.

I see your point now! Thanks for insisting on making that clear. Here’s how to do it your way:

  kdePackages' = kdePackages.override {
    qt6Packages = qt6Packages.override {
      pkgsHostTarget = pkgsHostTarget // {
        qt6 = pkgsHostTarget.qt6.overrideScope (
          self: super: {
            # Fix for: https://github.com/NixOS/nixpkgs/issues/526825
            # reported upstream at: https://github.com/musescore/MuseScore/issues/33015
            qtdeclarative = super.qtdeclarative.overrideAttrs (
              new: old: {
                patches = old.patches ++ [
                  (fetchpatch {
                    url = "https://github.com/qt/qtdeclarative/commit/9d4d376726a6ce15c429128dc65b927e411e40da.patch";
                    hash = "sha256-XhfliF5wZuN4/E55f8hfipIRjxBe9V7vL1cgn5p4xqA=";
                  })
                ];
              }
            );
          }
        );
      };
    };
  };

However that requires:

diff --git i/pkgs/top-level/all-packages.nix w/pkgs/top-level/all-packages.nix
index 83d919288d1d..a42c1ebdf214 100644
--- i/pkgs/top-level/all-packages.nix
+++ w/pkgs/top-level/all-packages.nix
@@ -6934,20 +6934,7 @@ with pkgs;
 
   qt6 = recurseIntoAttrs (callPackage ../development/libraries/qt-6 { });
 
-  qt6Packages = recurseIntoAttrs (
-    import ./qt6-packages.nix {
-      inherit
-        lib
-        config
-        __splicedPackages
-        makeScopeWithSplicing'
-        generateSplicesForMkScope
-        pkgsHostTarget
-        kdePackages
-        ;
-      inherit stdenv;
-    }
-  );
+  qt6Packages = recurseIntoAttrs (callPackage ./qt6-packages.nix { });
 
   readline70 = callPackage ../development/libraries/readline/7.0.nix { };
 

And indeed you get the same hash as with my original kdePackages'.

I created a PR for the above diff:

Which introduces 0 rebuilds, as expected.

1 Like