From e5db80ae487b59b4e9f950d68983ffb0575e26c6 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 7 May 2023 15:37:28 +0200 Subject: [PATCH] nixosModules.pkgsReadOnly: init --- flake.nix | 13 +++++ nixos/modules/misc/nixpkgs/read-only.nix | 74 ++++++++++++++++++++++++ nixos/modules/misc/nixpkgs/test.nix | 59 +++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 nixos/modules/misc/nixpkgs/read-only.nix diff --git a/flake.nix b/flake.nix index f9442d8ea2d2..fa00bffcdf92 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 000000000000..2a783216a9d5 --- /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 a6d8877ae070..0536cfc9624a 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; }