about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--flake.nix13
-rw-r--r--nixos/modules/misc/nixpkgs/read-only.nix74
-rw-r--r--nixos/modules/misc/nixpkgs/test.nix59
3 files changed, 146 insertions, 0 deletions
diff --git a/flake.nix b/flake.nix
index f9442d8ea2d2c..fa00bffcdf92f 100644
--- a/flake.nix
+++ b/flake.nix
@@ -57,6 +57,19 @@
 
       nixosModules = {
         notDetected = ./nixos/modules/installer/scan/not-detected.nix;
+
+        /*
+          Make the `nixpkgs.*` configuration read-only. Guarantees that `pkgs`
+          is the way you initialize it.
+
+          Example:
+
+              {
+                imports = [ nixpkgs.nixosModules.readOnlyPkgs ];
+                nixpkgs.pkgs = nixpkgs.legacyPackages.x86_64-linux;
+              }
+        */
+        readOnlyPkgs = ./nixos/modules/misc/nixpkgs/read-only.nix;
       };
     };
 }
diff --git a/nixos/modules/misc/nixpkgs/read-only.nix b/nixos/modules/misc/nixpkgs/read-only.nix
new file mode 100644
index 0000000000000..2a783216a9d54
--- /dev/null
+++ b/nixos/modules/misc/nixpkgs/read-only.nix
@@ -0,0 +1,74 @@
+# A replacement for the traditional nixpkgs module, such that none of the modules
+# can add their own configuration. This ensures that the Nixpkgs configuration is
+# exactly as the user intends.
+# This may also be used as a performance optimization when evaluating multiple
+# configurations at once, with a shared `pkgs`.
+
+# This is a separate module, because merging this logic into the nixpkgs module
+# is too burdensome, considering that it is already burdened with legacy.
+# Moving this logic into a module does not lose any composition benefits, because
+# its purpose is not something that composes anyway.
+
+{ lib, config, ... }:
+
+let
+  cfg = config.nixpkgs;
+  inherit (lib) mkOption types;
+
+in
+{
+  disabledModules = [
+    ../nixpkgs.nix
+  ];
+  options = {
+    nixpkgs = {
+      pkgs = mkOption {
+        type = lib.types.pkgs;
+        description = lib.mdDoc ''The pkgs module argument.'';
+      };
+      config = mkOption {
+        internal = true;
+        type = types.unique { message = "nixpkgs.config is set to read-only"; } types.anything;
+        description = lib.mdDoc ''
+          The Nixpkgs `config` that `pkgs` was initialized with.
+        '';
+      };
+      overlays = mkOption {
+        internal = true;
+        type = types.unique { message = "nixpkgs.overlays is set to read-only"; } types.anything;
+        description = lib.mdDoc ''
+          The Nixpkgs overlays that `pkgs` was initialized with.
+        '';
+      };
+      hostPlatform = mkOption {
+        internal = true;
+        readOnly = true;
+        description = lib.mdDoc ''
+          The platform of the machine that is running the NixOS configuration.
+        '';
+      };
+      buildPlatform = mkOption {
+        internal = true;
+        readOnly = true;
+        description = lib.mdDoc ''
+          The platform of the machine that built the NixOS configuration.
+        '';
+      };
+      # NOTE: do not add the legacy options such as localSystem here. Let's keep
+      #       this module simple and let module authors upgrade their code instead.
+    };
+  };
+  config = {
+    _module.args.pkgs =
+      # find mistaken definitions
+      builtins.seq cfg.config
+      builtins.seq cfg.overlays
+      builtins.seq cfg.hostPlatform
+      builtins.seq cfg.buildPlatform
+      cfg.pkgs;
+    nixpkgs.config = cfg.pkgs.config;
+    nixpkgs.overlays = cfg.pkgs.overlays;
+    nixpkgs.hostPlatform = cfg.pkgs.stdenv.hostPlatform;
+    nixpkgs.buildPlatform = cfg.pkgs.stdenv.buildPlatform;
+  };
+}
diff --git a/nixos/modules/misc/nixpkgs/test.nix b/nixos/modules/misc/nixpkgs/test.nix
index a6d8877ae0700..0536cfc9624a2 100644
--- a/nixos/modules/misc/nixpkgs/test.nix
+++ b/nixos/modules/misc/nixpkgs/test.nix
@@ -1,3 +1,5 @@
+# [nixpkgs]$ nix-build -A nixosTests.nixpkgs --show-trace
+
 { evalMinimalConfig, pkgs, lib, stdenv }:
 let
   eval = mod: evalMinimalConfig {
@@ -27,6 +29,47 @@ let
     let
       uncheckedEval = lib.evalModules { modules = [ ../nixpkgs.nix module ]; };
     in map (ass: ass.message) (lib.filter (ass: !ass.assertion) uncheckedEval.config.assertions);
+
+  readOnlyUndefined = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+  };
+
+  readOnlyBad = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+    nixpkgs.pkgs = { };
+  };
+
+  readOnly = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+    nixpkgs.pkgs = pkgs;
+  };
+
+  readOnlyBadConfig = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+    nixpkgs.pkgs = pkgs;
+    nixpkgs.config.allowUnfree = true; # do in pkgs instead!
+  };
+
+  readOnlyBadOverlays = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+    nixpkgs.pkgs = pkgs;
+    nixpkgs.overlays = [ (_: _: {}) ]; # do in pkgs instead!
+  };
+
+  readOnlyBadHostPlatform = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+    nixpkgs.pkgs = pkgs;
+    nixpkgs.hostPlatform = "foo-linux"; # do in pkgs instead!
+  };
+
+  readOnlyBadBuildPlatform = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+    nixpkgs.pkgs = pkgs;
+    nixpkgs.buildPlatform = "foo-linux"; # do in pkgs instead!
+  };
+
+  throws = x: ! (builtins.tryEval x).success;
+
 in
 lib.recurseIntoAttrs {
   invokeNixpkgsSimple =
@@ -65,5 +108,21 @@ lib.recurseIntoAttrs {
         nixpkgs.pkgs = pkgs;
       } == [];
 
+
+    # Tests for the read-only.nix module
+    assert readOnly._module.args.pkgs.stdenv.hostPlatform.system == pkgs.stdenv.hostPlatform.system;
+    assert throws readOnlyBad._module.args.pkgs.stdenv;
+    assert throws readOnlyUndefined._module.args.pkgs.stdenv;
+    assert throws readOnlyBadConfig._module.args.pkgs.stdenv;
+    assert throws readOnlyBadOverlays._module.args.pkgs.stdenv;
+    assert throws readOnlyBadHostPlatform._module.args.pkgs.stdenv;
+    assert throws readOnlyBadBuildPlatform._module.args.pkgs.stdenv;
+    # read-only.nix does not provide legacy options, for the sake of simplicity
+    # If you're bothered by this, upgrade your configs to use the new *Platform
+    # options.
+    assert !readOnly.options.nixpkgs?system;
+    assert !readOnly.options.nixpkgs?localSystem;
+    assert !readOnly.options.nixpkgs?crossSystem;
+
     pkgs.emptyFile;
 }