Home Manager + NixGL + Wayland: Persistent Duplicate Firefox Derivations (Same Version)

Hello Nix community,

I’m using Ubuntu (Wayland) with Home Manager and NixGL, and I’m consistently running into an issue where Home Manager seems to be creating two distinct Firefox derivations in the Nix store, even when I explicitly try to unify them. Both derivations appear to be for the exact same Firefox version.

My Goal: To have a single Firefox derivation in the Nix store that is:

  1. Managed by programs.firefox in Home Manager.
  2. Properly referenced by a custom AppArmor profile script (which I’m also managing via home.file).

What I’m Observing: After running home-manager switch, I consistently find two different Firefox derivations in the Nix store, even when which firefox shows one and my AppArmor script points to another:

Example output:

user@user ~> nix-store --query --referrers /nix/store/xr0l8ncclcl4129xjw1ns8fd4xxz16sc-firefox-139.0/
/nix/store/xr0l8ncclcl4129xjw1ns8fd4xxz16sc-firefox-139.0
/nix/store/41c9jrdzcrjfd6f0g6zxxjpi00bzq6cw-home-manager-path
/nix/store/z8jackbd1gvs37bm673bqadzr3f8s4pf-mozilla-native-messaging-hosts

user@user ~> nix-store --query --referrers /nix/store/zfvb6my3xkqfm2z2a2w8pwkyi8cxw8dx-firefox-139.0/
/nix/store/zfvb6my3xkqfm2z2a2w8pwkyi8cxw8dx-firefox-139.0
/nix/store/azwqkhj2badvg3bbajp77ngvhh18pyrx-hm_binsetupfirefoxapparmor.sh

In this example, one Firefox derivation (the first one) is referenced by home-manager-path (my general environment), and the other (the second one) is referenced by my hm_binsetupfirefoxapparmor.sh script.

My home.nix configuration (current attempt to unify):


{ config, pkgs, nixGL, lib, ... }:
let
  # Define the specific Firefox package you want to use.
  # This makes it explicit and reusable.
  # Consider if you need a nixGL wrapper here, or if programs.firefox handles it.
  # For now, let's assume pkgs.firefox is the base.
  # If you need nixGL for Firefox, you'd wrap it here:
  # myFirefoxPackage = config.lib.nixGL.wrap pkgs.firefox;
  # But usually, Firefox runs fine on Wayland with MOZ_ENABLE_WAYLAND=1,
  # so a direct pkgs.firefox might be sufficient unless you have specific GPU issues.
  myFirefoxPackage = pkgs.firefox; # This is the main Firefox package we'll use

in
{
  home.username = "user";
  home.homeDirectory = "/home/user";

  # Enable Graphical Services
  xsession.enable = true;
  xsession.windowManager.command = "…";

  nixGL.packages = import <nixgl> { inherit pkgs; };
  nixGL.defaultWrapper = "mesa";  # Default wrapper for general use
  nixGL.offloadWrapper = "nvidiaPrime";  # Wrapper for NVIDIA GPU offloading
  nixGL.installScripts = [ "mesa" "nvidiaPrime" ];

  home.packages = [
  ];

  programs.vscode = {
    enable = true;
    package = config.lib.nixGL.wrapOffload pkgs.vscode;
  };

  programs.ghostty = {
    enable = true;
    package = config.lib.nixGL.wrap pkgs.ghostty;
    settings = {
        command = "fish";
    };
  };

  programs.fish = {
    enable = true;
    shellAbbrs = {
      code = "code --no-sandbox";
    };
  };

  programs.bash = {
    enable = true;
    shellAliases = {
      code = "code --no-sandbox";
    };
  };

  programs.firefox = {
    enable = true;
    # Explicitly tell Home Manager to use our defined Firefox package
    package = myFirefoxPackage;
    policies = {
      cookies = {
        Allow = ["https://github.com" "http://github.com"];
      };
    };
  };

  home.stateVersion = "25.05"; # Please read the comment before changing.

    xdg.desktopEntries.code = { # The attribute name 'code' maps to 'code.desktop'
    name = "Code - OSS";
    comment = "Develop with pleasure!";
    exec = "${pkgs.vscode}/bin/code --no-sandbox %F";
    icon = "vscode";
    type = "Application";
    startupNotify = true;
    categories = [ "Development" "IDE" ];
    mimeType = [ "text/plain" "inode/directory" ];
    actions.new-window.exec = "${pkgs.vscode}/bin/code --no-sandbox --new-window %F";
    actions.new-window.name = "New Window";
    actions.new-window.icon = "vscode";
    # You can add other desktop entry fields as needed
    # For example, if you want to explicitly hide it from some environments:
    # notShowIn = [ "GNOME" ];
  };

  # Set default applications for various MIME types
  xdg.mimeApps = {
    enable = true;
    defaultApplications = {
      "text/plain" = "code.desktop";
      "text/markdown" = "code.desktop";
      "text/x-shellscript" = "code.desktop";
      "application/json" = "code.desktop";
      "application/xml" = "code.desktop";
      # Add more MIME types as needed for files you want to open in VS Code
      "inode/directory" = "code.desktop"; # To open folders in VS Code
    };
    # You can also use associations.add to add applications that can open certain types
    # without making them the default.
    # associations.add = {
    #   "image/jpeg" = [ "gimp.desktop" "eog.desktop" ];
    # };
  };

  home.file = {
    # Define the AppArmor setup script
    "bin/setup-firefox-apparmor.sh" = {
      executable = true;
      text = ''
        #!/bin/bash

        # The Firefox path is determined at Nix evaluation time and embedded here.
        # This ensures it's always the Nix store path, not a system-wide binary.
        FIREFOX_PATH="${myFirefoxPackage}/bin/firefox" # Use the explicitly defined package

        echo "Using Firefox path: $FIREFOX_PATH"

        # Ensure the directory exists
        sudo mkdir -p /etc/apparmor.d/

        # Write the AppArmor profile content
        sudo tee /etc/apparmor.d/firefox-local > /dev/null << EOF
        # This profile allows everything and only exists to give the
        # application a name instead of having the label "unconfined"
        abi <abi/4.0>,
        include <tunables/global>

        profile firefox-local ${myFirefoxPackage}/bin/firefox flags=(unconfined) {
          userns,

          # Allow read access to the Nix store for Firefox and its dependencies
          /nix/store/** r,

          # Paths commonly needed for graphics drivers and other system components
          /run/opengl-driver/** r, # Common on NixOS, might be needed on other distros if drivers are symlinked here
          /dev/dri/** rw,           # Access to DRM devices for graphics
          /dev/shm/** rw,           # Shared memory for IPC
          /etc/ssl/certs/ca-certificates.crt r, # Often needed for TLS/SSL

          # Site-specific See local/README for details.
          include if exists <local/firefox>
        }
        EOF

        # Reload AppArmor profiles
        sudo apparmor_parser -r /etc/apparmor.d/firefox-local || true
        echo "Firefox AppArmor profile setup script completed."
        echo "You may need to restart Firefox for changes to take effect."
      '';
    };
  };

  # Add activation script to provide instructions
  home.activation.firefoxAppArmorInstructions = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
    echo "======================================================================="
    echo "                 Firefox AppArmor Setup Required                     "
    echo "======================================================================="
    echo "To enable full Firefox security features (and remove the warning),"
    echo "you need to create an AppArmor profile. Home Manager has placed a "
    echo "script for this at: ${config.home.homeDirectory}/bin/setup-firefox-apparmor.sh"
    echo ""
    echo "THIS REQUIRES ROOT PRIVILEGES (sudo)."
    echo ""
    echo "STEPS TO COMPLETE THE SETUP:"
    echo "1. **Inspect the script (HIGHLY RECOMMENDED):**"
    echo "   cat ${config.home.homeDirectory}/bin/setup-firefox-apparmor.sh"
    echo ""
    echo "2. **Configure Sudoers (CAREFUL!):**"
    echo "   This allows you to run the script without a password."
    echo "   Run: sudo visudo"
    echo "   Add the following line to the end of the file, replacing 'vandy' with your username:"
    echo "   ${config.home.username} ALL=(root) NOPASSWD: ${config.home.homeDirectory}/bin/setup-firefox-apparmor.sh"
    echo "   Save and exit (Ctrl+X, Y, Enter for nano)."
    echo ""
    echo "3. **Run the setup script:**"
    echo "   ${config.home.homeDirectory}/bin/setup-firefox-apparmor.sh"
    echo ""
    echo "After running the script, restart Firefox to see the changes."
    echo "======================================================================="
  '';
  
  home.sessionVariables = {
    NIXOS_OZONE_WL=1;
    EDITOR="code";
    MOZ_FORCE_ENABLE_POLICY = "1";
  };

programs.home-manager.enable = true;
}

Steps I’ve taken (after each home.nix modification):

  1. Removed Firefox entries from home.nix.
  2. Cleaned garbage collection (nix-collect-garbage -d) to ensure no Firefox derivations were left.
  3. Added Firefox and the AppArmor script back to home.nix as shown above.
  4. Run home-manager switch.
  5. Run sudo /home/vandy/bin/setup-firefox-apparmor.sh.
  6. Verified with nix-store --query --referrers and which firefox.

Question: Why am I still getting two distinct Firefox derivations, even when explicitly defining myFirefoxPackage and using it for both programs.firefox.package and embedding its path into the AppArmor script? Is there an implicit wrapping or derivation difference I’m missing with programs.firefox?

Yeah, that’s what home-manager does under the hood to change stuff:

If you use config.programs.firefox.finalPackage in your script, instead of a separate let binding, it will resolve to the correct derivation.

In general, you should use config. variables instead of hoping your binding isn’t touched by the module system, and always keep an eye out for wrappers in upstream modules like this one.