diff --git a/dev/README.md b/dev/README.md index 270517d2..c8f5e4bd 100644 --- a/dev/README.md +++ b/dev/README.md @@ -1,6 +1,10 @@ - # Separate `dev` flake Wouldn't recommend this pattern normally, but I'm trying to keep deps low for `flake-parts` until we have split dev inputs that don't carry over to dependent lock files. + +```sh +nix develop --impure -f './dev' 'mySystem.devShells.default' +nix repl -f './dev' +``` diff --git a/dev/default.nix b/dev/default.nix index df712a4d..bae0ed83 100644 --- a/dev/default.nix +++ b/dev/default.nix @@ -1,16 +1,36 @@ let + flake-parts = builtins.getFlake (toString ../.); + lib = flake-parts.inputs.nixpkgs-lib.lib; + sourceInfo = inputs.flake-parts.sourceInfo; # used by pre-commit module, etc flake = builtins.getFlake (toString ./.); - fmc-lib = (builtins.getFlake (toString ../.)).lib; - args = { - inherit self; - } // flake.inputs; - self = { - inherit (flake) inputs; - outPath = ../.; # used by pre-commit module, etc - outputs = self.config.flake; - } // - fmc-lib.mkFlake - { inputs = args; } - ./flake-module.nix; + inputs = flake.inputs // { inherit flake-parts; }; + makeResult = specialArgs: flakeModule: result: + let + outputs = flake.outputs // flake-parts.lib.mkFlake + { + inputs = inputs // { self = result; }; + # debugging tool + specialArgs = { + replaceSpecialArgs = newSpecialArgs: + let + newSpecialArgs' = + if lib.isFunction newSpecialArgs + then newSpecialArgs specialArgs + else newSpecialArgs; + newResult = makeResult newSpecialArgs' flakeModule newResult; + in + newResult; + } // specialArgs; + } + flakeModule; + in + outputs // sourceInfo // { + inherit inputs outputs sourceInfo; + _type = "flake"; + }; in -self.config.flake // { inherit (flake) inputs; } +let + # eagerly import to reproduce inline evaluation + result = makeResult { } (import ./flake-module.nix) result; +in +result diff --git a/dev/flake-module.nix b/dev/flake-module.nix index 5ac60b94..397c0ae0 100644 --- a/dev/flake-module.nix +++ b/dev/flake-module.nix @@ -1,19 +1,25 @@ -{ config, lib, inputs, withSystem, ... }: - -{ +{ config, inputs, lib, options, specialArgs, withSystem, ... } @ args: +let + rootArgs = args; + rootConfig = config; + rootOptions = options; + rootSpecialArgs = specialArgs; +in +# debugging tool +specialArgs.flakeModuleTransformer or (args: flakeModule: flakeModule) args { imports = [ inputs.pre-commit-hooks-nix.flakeModule inputs.hercules-ci-effects.flakeModule # herculesCI attr ]; - systems = [ "x86_64-linux" "aarch64-darwin" ]; + config.systems = [ "x86_64-linux" "aarch64-darwin" ]; - hercules-ci.flake-update = { + config.hercules-ci.flake-update = { enable = true; autoMergeMethod = "merge"; when.dayOfMonth = 1; }; - perSystem = { config, pkgs, ... }: { + config.perSystem = { config, pkgs, ... }: { devShells.default = pkgs.mkShell { nativeBuildInputs = [ @@ -38,11 +44,18 @@ in tests.runTests pkgs.emptyFile // { internals = tests; }; }; - flake = { - # for repl exploration / debug - config.config = config; - options.mySystem = lib.mkOption { default = config.allSystems.${builtins.currentSystem}; }; - config.effects = withSystem "x86_64-linux" ({ pkgs, hci-effects, ... }: { + config.flake = { config, options, specialArgs, ... } @ args: { + # for REPL exploration / debugging + config.allFlakeModuleArgs = args // config._module.args // specialArgs; + config.allRootModuleArgs = rootArgs // rootConfig._module.args // rootSpecialArgs; + config.transformFlakeModule = flakeModuleTransformer: + rootSpecialArgs.replaceSpecialArgs (prevSpecialArgs: prevSpecialArgs // { + inherit flakeModuleTransformer; + }); + options.mySystem = lib.mkOption { + default = rootConfig.allSystems.${builtins.currentSystem}; + }; + config.effects = withSystem "x86_64-linux" ({ hci-effects, pkgs, ... }: { tests = { template = pkgs.callPackage ./tests/template.nix { inherit hci-effects; }; }; diff --git a/dev/tests/eval-tests.nix b/dev/tests/eval-tests.nix index 437ee1a7..a1ebd5f0 100644 --- a/dev/tests/eval-tests.nix +++ b/dev/tests/eval-tests.nix @@ -2,17 +2,54 @@ # # nix build -f dev checks.x86_64-linux.eval-tests -rec { - f-p = builtins.getFlake (toString ../..); - flake-parts = f-p; +let + flake-parts = builtins.getFlake (toString ../..); + lib = flake-parts.inputs.nixpkgs-lib.lib; +in +(lib.makeExtensibleWithCustomName "extendEvalTests" (evalTests: { + inherit evalTests; + inherit flake-parts; + flake-parts-lib = evalTests.flake-parts.lib; + inherit lib; devFlake = builtins.getFlake (toString ../.); - nixpkgs = devFlake.inputs.nixpkgs; + nixpkgs = evalTests.devFlake.inputs.nixpkgs; - f-p-lib = f-p.lib; + inherit (evalTests.flake-parts-lib) mkFlake; + weakEvalTests.callFlake = { ... } @ flake: + let + sourceInfo = flake.sourceInfo or { }; + inputs = flake.inputs or { }; + outputs = flake.outputs inputs; + result = outputs; + in + result; + strongEvalTests.callFlake = { ... } @ flake: + let + sourceInfo = { outPath = "/unknown_eval-tests_flake"; } // + flake.sourceInfo or { }; + inputs = flake.inputs or { }; + outputs = flake.outputs (inputs // { self = result; }); + result = outputs // sourceInfo // { + inherit inputs outputs sourceInfo; + _type = "flake"; + }; + in + assert builtins.isFunction flake.outputs; + result; + + withWeakEvalTests = evalTests.extendEvalTests (finalEvalTests: prevEvalTests: + builtins.mapAttrs (name: value: finalEvalTests.weakEvalTests.${name}) + prevEvalTests.weakEvalTests + ); + withStrongEvalTests = evalTests.extendEvalTests (finalEvalTests: prevEvalTests: + builtins.mapAttrs (name: value: finalEvalTests.strongEvalTests.${name}) + prevEvalTests.strongEvalTests + ); - inherit (f-p-lib) mkFlake; - inherit (f-p.inputs.nixpkgs-lib) lib; + exhibitingInfiniteRecursion = false; + exhibitInfiniteRecursion = evalTests.extendEvalTests + (finalEvalTest: prevEvalTests: { exhibitingInfiniteRecursion = true; }); pkg = system: name: derivation { name = name; @@ -20,41 +57,202 @@ rec { system = system; }; - empty = mkFlake - { inputs.self = { }; } - { + empty = evalTests.callFlake { + inputs.flake-parts = evalTests.flake-parts; + inputs.self = { }; + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { systems = [ ]; }; + }; + weakEvalTests.emptyResult = { + apps = { }; + checks = { }; + devShells = { }; + formatter = { }; + legacyPackages = { }; + nixosConfigurations = { }; + nixosModules = { }; + overlays = { }; + packages = { }; + }; + strongEvalTests.emptyResult = let + _type = "flake"; + inputs.flake-parts = evalTests.flake-parts; + inputs.self = { }; + outputs = evalTests.weakEvalTests.emptyResult; + sourceInfo.outPath = "/unknown_eval-tests_flake"; + result = outputs // sourceInfo // { inherit _type inputs outputs sourceInfo; }; + in result; + runEmptyTests = ok: + assert evalTests.empty == evalTests.emptyResult; + ok; + emptyTestsResult = evalTests.runEmptyTests "ok"; - example1 = mkFlake - { inputs.self = { }; } - { + tooEmpty = evalTests.callFlake { + inputs.flake-parts = evalTests.flake-parts; + inputs.self = { }; + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { + }; + }; + # Shallow evaluation is successful… + weakEvalTests.tooEmptyResultTried0.success = true; + weakEvalTests.tooEmptyResultTried0.value = { }; + weakEvalTests.tooEmptyResultTried0TestTried.success = true; + weakEvalTests.tooEmptyResultTried0TestTried.value = false; + # …including for flake outputs… + strongEvalTests.tooEmptyResultTried0 = evalTests.weakEvalTests.tooEmptyResultTried0; + strongEvalTests.tooEmptyResultTried0TestTried = evalTests.weakEvalTests.tooEmptyResultTried0TestTried; + # …but any evaluations of attribute values (flake output values) are not. + weakEvalTests.tooEmptyResultTried1.success = true; + weakEvalTests.tooEmptyResultTried1.value = { + apps = { }; + checks = { }; + devShells = { }; + formatter = { }; + legacyPackages = { }; + nixosConfigurations = { }; + nixosModules = { }; + overlays = { }; + packages = { }; + }; + weakEvalTests.tooEmptyResultTried1TestTried.success = false; + weakEvalTests.tooEmptyResultTried1TestTried.value = false; + strongEvalTests.tooEmptyResultTried1.success = true; + strongEvalTests.tooEmptyResultTried1.value = let + _type = "flake"; + inputs.flake-parts = evalTests.flake-parts; + inputs.self = { }; + outputs = evalTests.weakEvalTests.tooEmptyResultTried1.value; + sourceInfo.outPath = "/unknown_eval-tests_flake"; + result = outputs // sourceInfo // { inherit _type inputs outputs sourceInfo; }; + in result; + strongEvalTests.tooEmptyResultTried1TestTried.success = false; + strongEvalTests.tooEmptyResultTried1TestTried.value = false; + runTooEmptyTests = ok: + let + tooEmptyResultTried = builtins.tryEval evalTests.tooEmpty; + tooEmptyResultTried0TestTried = builtins.tryEval (tooEmptyResultTried == evalTests.tooEmptyResultTried0); + tooEmptyResultTried1TestTried = builtins.tryEval (tooEmptyResultTried == evalTests.tooEmptyResultTried1); + in + assert tooEmptyResultTried0TestTried == evalTests.tooEmptyResultTried0TestTried; + assert tooEmptyResultTried1TestTried == evalTests.tooEmptyResultTried1TestTried; + ok; + tooEmptyTestsResult = evalTests.runTooEmptyTests "ok"; + + example1 = evalTests.callFlake { + inputs.flake-parts = evalTests.flake-parts; + inputs.self = { }; + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { systems = [ "a" "b" ]; perSystem = { system, ... }: { - packages.hello = pkg system "hello"; + packages.hello = evalTests.pkg system "hello"; }; }; + }; + weakEvalTests.example1Result = { + apps = { a = { }; b = { }; }; + checks = { a = { }; b = { }; }; + devShells = { a = { }; b = { }; }; + formatter = { }; + legacyPackages = { a = { }; b = { }; }; + nixosConfigurations = { }; + nixosModules = { }; + overlays = { }; + packages = { + a = { hello = evalTests.pkg "a" "hello"; }; + b = { hello = evalTests.pkg "b" "hello"; }; + }; + }; + strongEvalTests.example1Result = let + _type = "flake"; + inputs.flake-parts = evalTests.flake-parts; + inputs.self = { }; + outputs = evalTests.weakEvalTests.example1Result; + sourceInfo.outPath = "/unknown_eval-tests_flake"; + result = outputs // sourceInfo // { inherit _type inputs outputs sourceInfo; }; + in result; + runExample1Tests = ok: + assert evalTests.example1 == evalTests.example1Result; + ok; + example1TestsResult = evalTests.runExample1Tests "ok"; + + # This test case is a fun one. In the REPL, try `exhibitInfiniteRecursion.*`. + # In the case that `mkFlake` *isn't* called from a flake, `inputs.self` is + # unlikely to refer to the result of the `mkFlake` evaluation. If + # `inputs.self` isn't actually self-referential, evaluating attribute values + # of `self` is not divergent. Evaluation of `self.outPath` is useful for + # paths in documentation & error messages. However, if that evaluation occurs + # in a `builtins.addErrorContext` message forced by an erroring `self`, both + # `self` will never evaluate *and* `builtins.toString self.outPath` must + # evaluate, causing Nix to instead throw an infinite recursion error. Even + # just `inputs.self ? outPath` throws an infinite recursion error. + # (`builtins.tryEval` can only catch errors created by `builtins.throw` or + # `builtins.assert`, so evaluation is guarded with + # `exhibitingInfiniteRecursion` here to keep `runTests` from diverging.) + # In this particular case, `mkFlake` evaluates `self ? outPath` to know if the + # default module location it provides should be generic or specific. As + # explained, this evaluation is unsafe under an uncatchably divergent `self`. + # Thus, `outPath` cannot be safely sourced from `self` at the top-level. + # + # When tests are exhibititing infinite recursion, the abnormally correct + # `self` is provided. + weakEvalTests.nonexistentOption = let result = evalTests.callFlake { + inputs.flake-parts = evalTests.flake-parts; + inputs.self = if !evalTests.exhibitingInfiniteRecursion then { } else result; + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { + config.systems = [ ]; + config.nonexistentOption = null; + }; + }; in result; + # When using actual flakes, this test always diverges. Unless tests are + # exhibiting infinite recursion, the flake is made equivalent to `empty`. + strongEvalTests.nonexistentOption = evalTests.callFlake { + inputs.flake-parts = evalTests.flake-parts; + inputs.self = { }; + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } ({ + config.systems = [ ]; + } // (if !evalTests.exhibitingInfiniteRecursion then { } else { + config.nonexistentOption = null; + })); + }; + weakEvalTests.nonexistentOptionResultTried0.success = true; + weakEvalTests.nonexistentOptionResultTried0.value = { }; + weakEvalTests.nonexistentOptionResultTried0TestTried.success = true; + weakEvalTests.nonexistentOptionResultTried0TestTried.value = false; + strongEvalTests.nonexistentOptionResultTried0 = evalTests.weakEvalTests.nonexistentOptionResultTried0; + strongEvalTests.nonexistentOptionResultTried0TestTried = evalTests.weakEvalTests.nonexistentOptionResultTried0TestTried; + runNonexistentOptionTests = ok: + let + nonexistentOptionResultTried = builtins.tryEval evalTests.nonexistentOption; + nonexistentOptionResultTried0TestTried = builtins.tryEval (nonexistentOptionResultTried == evalTests.nonexistentOptionResultTried0); + in + assert nonexistentOptionResultTried0TestTried == evalTests.nonexistentOptionResultTried0TestTried; + ok; + nonexistentOptionTestsResult = evalTests.runNonexistentOptionTests "ok"; - packagesNonStrictInDevShells = mkFlake - { inputs.self = packagesNonStrictInDevShells; /* approximation */ } - { + packagesNonStrictInDevShells = evalTests.callFlake { + inputs.flake-parts = evalTests.flake-parts; + # approximation + inputs.self = evalTests.packagesNonStrictInDevShells; + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { systems = [ "a" "b" ]; - perSystem = { system, self', ... }: { - packages.hello = pkg system "hello"; + perSystem = { self', system, ... }: { + packages.hello = evalTests.pkg system "hello"; packages.default = self'.packages.hello; devShells = throw "can't be strict in perSystem.devShells!"; }; flake.devShells = throw "can't be strict in devShells!"; }; - easyOverlay = mkFlake - { inputs.self = { }; } - { - imports = [ flake-parts.flakeModules.easyOverlay ]; + easyOverlay = evalTests.callFlake { + inputs.flake-parts = evalTests.flake-parts; + inputs.self = { }; + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ inputs.flake-parts.flakeModules.easyOverlay ]; systems = [ "a" "aarch64-linux" ]; perSystem = { system, config, final, pkgs, ... }: { packages.default = config.packages.hello; - packages.hello = pkg system "hello"; + packages.hello = evalTests.pkg system "hello"; packages.hello_new = final.hello; overlayAttrs = { hello = config.packages.hello; @@ -63,11 +261,13 @@ rec { }; }; }; + }; - flakeModulesDeclare = mkFlake - { inputs.self = { outPath = ./.; }; } - ({ config, ... }: { - imports = [ flake-parts.flakeModules.flakeModules ]; + flakeModulesDeclare = evalTests.callFlake { + inputs.flake-parts = evalTests.flake-parts; + inputs.self = { outPath = ./.; }; + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } ({ config, inputs, lib, ... }: { + imports = [ inputs.flake-parts.flakeModules.flakeModules ]; systems = [ ]; flake.flakeModules.default = { lib, ... }: { options.flake.test123 = lib.mkOption { default = "option123"; }; @@ -77,92 +277,90 @@ rec { flake.test123 = "123test"; }; }); + }; - flakeModulesImport = mkFlake - { inputs.self = { }; } - { - imports = [ flakeModulesDeclare.flakeModules.default ]; + flakeModulesImport = evalTests.callFlake { + inputs.flake-parts = evalTests.flake-parts; + inputs.flakeModulesDeclare = evalTests.flakeModulesDeclare; + inputs.self = { }; + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ inputs.flakeModulesDeclare.flakeModules.default ]; }; + }; + runFlakeModulesImportTests = ok: + assert evalTests.flakeModulesImport.test123 == "123test"; + ok; + flakeModulesImportTestsResult = evalTests.runFlakeModulesImportTests "ok"; - flakeModulesDisable = mkFlake - { inputs.self = { }; } - { - imports = [ flakeModulesDeclare.flakeModules.default ]; - disabledModules = [ flakeModulesDeclare.flakeModules.extra ]; + flakeModulesDisable = evalTests.callFlake { + inputs.flake-parts = evalTests.flake-parts; + inputs.flakeModulesDeclare = evalTests.flakeModulesDeclare; + inputs.self = { }; + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ inputs.flakeModulesDeclare.flakeModules.default ]; + disabledModules = [ inputs.flakeModulesDeclare.flakeModules.extra ]; }; + }; + runFlakeModulesDisableTests = ok: + assert evalTests.flakeModulesDisable.test123 == "option123"; + ok; + flakeModulesDisableTestsResult = evalTests.runFlakeModulesDisableTests "ok"; - nixpkgsWithoutEasyOverlay = import nixpkgs { + nixpkgsWithoutEasyOverlay = import evalTests.nixpkgs { system = "x86_64-linux"; overlays = [ ]; config = { }; }; - nixpkgsWithEasyOverlay = import nixpkgs { + nixpkgsWithEasyOverlay = import evalTests.nixpkgs { # non-memoized system = "x86_64-linux"; - overlays = [ easyOverlay.overlays.default ]; + overlays = [ evalTests.easyOverlay.overlays.default ]; config = { }; }; - nixpkgsWithEasyOverlayMemoized = import nixpkgs { + nixpkgsWithEasyOverlayMemoized = import evalTests.nixpkgs { # memoized system = "aarch64-linux"; - overlays = [ easyOverlay.overlays.default ]; + overlays = [ evalTests.easyOverlay.overlays.default ]; config = { }; }; + tryEvalOutputs = outputs: builtins.seq (builtins.attrNames outputs) outputs; + runTests = ok: - assert empty == { - apps = { }; - checks = { }; - devShells = { }; - formatter = { }; - legacyPackages = { }; - nixosConfigurations = { }; - nixosModules = { }; - overlays = { }; - packages = { }; - }; + assert evalTests.runEmptyTests true; - assert example1 == { - apps = { a = { }; b = { }; }; - checks = { a = { }; b = { }; }; - devShells = { a = { }; b = { }; }; - formatter = { }; - legacyPackages = { a = { }; b = { }; }; - nixosConfigurations = { }; - nixosModules = { }; - overlays = { }; - packages = { - a = { hello = pkg "a" "hello"; }; - b = { hello = pkg "b" "hello"; }; - }; - }; + assert evalTests.runTooEmptyTests true; + + assert evalTests.runExample1Tests true; + + assert evalTests.runNonexistentOptionTests true; # - exported package becomes part of overlay. # - perSystem is invoked for the right system, when system is non-memoized - assert nixpkgsWithEasyOverlay.hello == pkg "x86_64-linux" "hello"; + assert evalTests.nixpkgsWithEasyOverlay.hello == evalTests.pkg "x86_64-linux" "hello"; # - perSystem is invoked for the right system, when system is memoized - assert nixpkgsWithEasyOverlayMemoized.hello == pkg "aarch64-linux" "hello"; + assert evalTests.nixpkgsWithEasyOverlayMemoized.hello == evalTests.pkg "aarch64-linux" "hello"; # - Non-exported package does not become part of overlay. - assert nixpkgsWithEasyOverlay.default or null != pkg "x86_64-linux" "hello"; + assert evalTests.nixpkgsWithEasyOverlay.default or null != evalTests.pkg "x86_64-linux" "hello"; # - hello_old comes from super - assert nixpkgsWithEasyOverlay.hello_old == nixpkgsWithoutEasyOverlay.hello; + assert evalTests.nixpkgsWithEasyOverlay.hello_old == evalTests.nixpkgsWithoutEasyOverlay.hello; # - `hello_new` shows that the `final` wiring works - assert nixpkgsWithEasyOverlay.hello_new == nixpkgsWithEasyOverlay.hello; + assert evalTests.nixpkgsWithEasyOverlay.hello_new == evalTests.nixpkgsWithEasyOverlay.hello; - assert flakeModulesImport.test123 == "123test"; + assert evalTests.runFlakeModulesImportTests true; - assert flakeModulesDisable.test123 == "option123"; + assert evalTests.runFlakeModulesDisableTests true; - assert packagesNonStrictInDevShells.packages.a.default == pkg "a" "hello"; + assert evalTests.packagesNonStrictInDevShells.packages.a.default == evalTests.pkg "a" "hello"; ok; - result = runTests "ok"; -} + result = evalTests.runTests "ok"; +})).withWeakEvalTests diff --git a/lib.nix b/lib.nix index deebf702..31871c49 100644 --- a/lib.nix +++ b/lib.nix @@ -37,8 +37,11 @@ let attrs@{ staticModules ? [ ] }: mkOptionType { name = "deferredModule"; description = "module"; + descriptionClass = "noun"; check = x: isAttrs x || isFunction x || path.check x; - merge = loc: defs: staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs; + merge = loc: defs: { + imports = staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs; + }; inherit (submoduleWith { modules = staticModules; }) getSubOptions getSubModules; diff --git a/modules/apps.nix b/modules/apps.nix index 0fd5d7c6..e3952387 100644 --- a/modules/apps.nix +++ b/modules/apps.nix @@ -15,7 +15,9 @@ let }; getExe = x: - "${lib.getBin x}/bin/${x.meta.mainProgram or (throw ''Package ${x.name or ""} does not have meta.mainProgram set, so I don't know how to find the main executable. You can set meta.mainProgram, or pass the full path to executable, e.g. program = "''${pkg}/bin/foo"'')}"; + "${lib.getBin x}/bin/${x.meta.mainProgram or (throw + ''Package ${x.name or ""} does not have meta.mainProgram set, so I don't know how to find the main executable. You can set `meta.mainProgram`, or pass the full path to executable, e.g. program = "''${pkg}/bin/foo"'' + )}"; appType = lib.types.submodule { options = { diff --git a/modules/moduleWithSystem.nix b/modules/moduleWithSystem.nix index e5c70083..e4d397f5 100644 --- a/modules/moduleWithSystem.nix +++ b/modules/moduleWithSystem.nix @@ -9,22 +9,23 @@ let system = config._module.args.system or - config._module.args.pkgs.stdenv.hostPlatform.system or - (throw "moduleWithSystem: Could not determine the configuration's system parameter for this module system application."); + config._module.args.pkgs.stdenv.hostPlatform.system or (throw + "moduleWithSystem: Could not determine the `system` parameter for this module set evaluation." + ); - allArgs = withSystem system (args: args); + allPerSystemArgs = withSystem system (args: args); - lazyArgsPerParameter = f: builtins.mapAttrs - (k: v: allArgs.${k} or (throw "moduleWithSystem: module argument `${k}` does not exist.")) + lazyPerSystemArgsPerParameter = f: builtins.mapAttrs + (k: v: allPerSystemArgs.${k} or (throw "moduleWithSystem: per-system argument `${k}` does not exist.")) (builtins.functionArgs f); # Use reflection to make the call lazy in the argument. # Restricts args to the ones declared. - callLazily = f: a: f (lazyArgsPerParameter f); + callLazily = f: a: f (lazyPerSystemArgsPerParameter f); in { imports = [ - (callLazily module allArgs) + (callLazily module allPerSystemArgs) ]; }; }; diff --git a/modules/nixpkgs.nix b/modules/nixpkgs.nix index 44df9158..e7fc642e 100644 --- a/modules/nixpkgs.nix +++ b/modules/nixpkgs.nix @@ -1,7 +1,7 @@ # # Nixpkgs module. The only exception to the rule. # -# Provides a `pkgs` argument in `perSystem`. +# Provides customizable `nixpkgs` and `pkgs` arguments in `perSystem`. # # Arguably, this shouldn't be in flake-parts, but in nixpkgs. # Nixpkgs could define its own module that does this, which would be @@ -11,14 +11,210 @@ # will be accepted into flake-parts, because it's against the # spirit of Flakes. # +{ config, flake-parts-lib, inputs, lib, options, ... }: +let + inherit (lib) + last + literalExpression + mapAttrs + mdDoc + mkDefault + mkOption + mkOptionDefault + mkOverride + toList + types + ; + inherit (flake-parts-lib) + mkPerSystemOption + mkSubmoduleOptions + ; + extendSubModules = type: modules: + type.substSubModules (type.getSubModules ++ modules); + getOptionSubOptions = locSuffix: opt: + let + loc = opt.loc ++ locSuffix; + type = extendSubModules opt.type [{ _module.args.name = last loc; }]; + in + type.getSubOptions loc; + mkSubmoduleOptionsWithShorthand = + options: mkOption { + type = types.submoduleWith { + modules = [{ inherit options; }]; + shorthandOnlyDefinesConfig = true; + }; + }; + # Shorthand for `types.submoduleWith`. + submoduleWithModules = + { ... }@attrs: + modules: + types.submoduleWith (attrs // { modules = toList modules; }); + rootConfig = config; + rootOptions = options; +in { + options = { + nixpkgs = { + evals = mkOption { + default = { default = { }; }; + description = '' + Configuration for Nixpkgs evaluations of {option}`perSystem.nixpkgs.evals`. + ''; + type = types.lazyAttrsOf (submoduleWithModules { } ({ config, name, options, ... }: { + _file = ./nixpkgs.nix; + options = { + input = mkOption { + description = mdDoc '' + Nixpkgs function for evaluation. + ''; + default = inputs.nixpkgs or (throw + "flake-parts: The flake does not have a `nixpkgs` input. Please add it, or set `${options.input}` yourself." + ); + defaultText = literalExpression ''inputs.nixpkgs''; + type = types.coercedTo types.path import (types.functionTo types.unspecified); + }; + settings = mkOption { + default = { }; + description = mdDoc '' + Settings argument for the Nixpkgs evaluations of {option}`perSystem.nixpkgs.evals.`. + ''; + type = rootOptions.nixpkgs.settings.type; + }; + }; + config = { + settings = mkDefault rootConfig.nixpkgs.settings; + }; + })); + }; + settings = mkOption { + default = { }; + description = mdDoc '' + Default settings argument for each Nixpkgs evaluations of {option}`nixpkgs.evals`. + ''; + # This submodule uses `shorthandOnlyDefinesConfig` because of the top-level `config` + # attribute and to make future upstreaming of this module to Nixpkgs easier. + type = submoduleWithModules { shorthandOnlyDefinesConfig = true; } ({ config, name, options, ... }: { + _file = ./nixpkgs.nix; + freeformType = types.lazyAttrsOf types.raw; + options = { + config = mkOption { + default = { }; + description = mdDoc '' + Config for this Nixpkgs evaluation. + ''; + type = submoduleWithModules { } { + _file = ./nixpkgs.nix; + freeformType = types.lazyAttrsOf types.raw; + }; + }; + crossOverlays = mkOption { + default = [ ]; + description = mdDoc '' + List of Nixpkgs overlays to apply to target packages only for this Nixpkgs evaluation. + ''; + type = types.listOf (types.uniq (types.functionTo (types.functionTo (types.lazyAttrsOf types.unspecified)))); + }; + overlays = mkOption { + default = [ ]; + description = mdDoc '' + List of Nixpkgs overlays for this Nixpkgs evaluation. + ''; + type = types.listOf (types.uniq (types.functionTo (types.functionTo (types.lazyAttrsOf types.unspecified)))); + }; + }; + }); + }; + }; + perSystem = mkPerSystemOption ({ config, system, ... }: { + _file = ./nixpkgs.nix; + options = { + nixpkgs = { + evals = mkOption { + default = { }; + description = '' + Configuration for Nixpkgs evaluations. + ''; + type = types.lazyAttrsOf (submoduleWithModules { } [ + ({ config, name, options, ... }: { + _file = ./nixpkgs.nix; + options = { + output = mkOption { + default = rootConfig.nixpkgs.evals.${name}.input config.settings; + defaultText = literalExpression + ''config.nixpkgs.evals.''${name}.input config.perSystem.nixpkgs.''${name}.settings''; + description = mdDoc '' + Evaluated Nixpkgs. + ''; + type = types.raw; + }; + settings = mkOption { + default = { }; + description = mdDoc '' + Settings argument for the Nixpkgs evaluations of {option}`perSystem.nixpkgs.evals.`. + ''; + type = (getOptionSubOptions [ name ] rootOptions.nixpkgs.evals).settings.type; + }; + }; + config = { + settings = mkDefault rootConfig.nixpkgs.evals.${name}.settings; + }; + }) + # Separate module, for type merging + ({ config, name, options, ... }: { + _file = ./nixpkgs.nix; + options = { + # `mkSubmoduleOptions` can't be used here due to `shorthandOnlyDefinesConfig`. + settings = mkSubmoduleOptionsWithShorthand { + localSystem = mkOption { + apply = lib.systems.elaborate; + default = config.settings.crossSystem; + defaultText = literalExpression ''config.perSystem.nixpkgs.evals.''${name}.crossSystem''; + description = mdDoc '' + Specifies the platform on which Nixpkgs packages should be built. + Also known as `buildPlatform`. + By default, Nixpkgs packages are built on the system where they run, but + you can change where it's built. Setting this option will cause NixOS to + be cross-compiled. + + For instance, if you're doing distributed multi-platform deployment, + or if you're building for machines, you can set this to match your + development system and/or build farm. + ''; + type = types.either types.str types.attrs; + }; + crossSystem = mkOption { + apply = lib.systems.elaborate; + default = system; + defaultText = literalExpression ''system''; + description = mdDoc '' + Specifies the system where packages from this Nixpkgs evaluation will run. + Also known as `hostPlatform`. + + To cross-compile, see also {option}`config.perSystem.nixpkgs.evals..localSystem`. + ''; + type = types.either types.str types.attrs; + }; + }; + }; + }) + ]); + }; + }; + }; + config = { + nixpkgs = { + evals = mapAttrs (name: genericConfig: { }) rootConfig.nixpkgs.evals; + }; + }; + }); + }; config = { - perSystem = { inputs', lib, ... }: { + perSystem = { config, nixpkgs, options, ... }: { + _file = ./nixpkgs.nix; config = { - _module.args.pkgs = lib.mkOptionDefault ( - builtins.seq - (inputs'.nixpkgs or (throw "flake-parts: The flake does not have a `nixpkgs` input. Please add it, or set `perSystem._module.args.pkgs` yourself.")) - inputs'.nixpkgs.legacyPackages + _module.args.nixpkgs = mkOptionDefault (mapAttrs (name: nixpkgs: nixpkgs.output) config.nixpkgs.evals); + _module.args.pkgs = mkOptionDefault ( + nixpkgs.default or (throw "flake-parts: The `perSystem` argument `nixpkgs` does not have a default attribute. Please configure `${options._module.args}.nixpkgs.default`, or set `${options._module.args}.nixpkgs` or `${options._module.args}.pkgs` yourself.") ); }; }; diff --git a/modules/perSystem.nix b/modules/perSystem.nix index 851b30a0..29a7fc52 100644 --- a/modules/perSystem.nix +++ b/modules/perSystem.nix @@ -1,4 +1,4 @@ -{ config, lib, flake-parts-lib, self, ... }: +{ config, lib, flake-parts-lib, inputs, self, ... }: let inherit (lib) genAttrs @@ -92,7 +92,7 @@ in type = mkPerSystemType ({ config, system, ... }: { _file = ./perSystem.nix; config = { - _module.args.inputs' = mapAttrs (k: rootConfig.perInput system) self.inputs; + _module.args.inputs' = mapAttrs (k: rootConfig.perInput system) inputs; _module.args.self' = rootConfig.perInput system self; # Custom error messages @@ -103,9 +103,9 @@ in _module.args.moduleWithSystem = throwAliasError "moduleWithSystem"; }; }); - apply = modules: system: + apply = module: system: (lib.evalModules { - inherit modules; + modules = [ module ]; prefix = [ "perSystem" system ]; specialArgs = { inherit system; diff --git a/template/multi-module/hello/flake-module.nix b/template/multi-module/hello/flake-module.nix index 0ddc6d30..e6817a74 100644 --- a/template/multi-module/hello/flake-module.nix +++ b/template/multi-module/hello/flake-module.nix @@ -1,7 +1,7 @@ # Definitions can be imported from a separate file like this one -{ self, lib, ... }: { - perSystem = { config, self', inputs', pkgs, ... }: { +{ config, lib, inputs, ... }: { + perSystem = { config, inputs', pkgs, ... }: { # Definitions like this are entirely equivalent to the ones # you may have directly in flake.nix. packages.hello = pkgs.hello; @@ -9,8 +9,8 @@ flake = { nixosModules.hello = { pkgs, ... }: { environment.systemPackages = [ - # or self.inputs.nixpkgs.legacyPackages.${pkgs.stdenv.hostPlatform.system}.hello - self.packages.${pkgs.stdenv.hostPlatform.system}.hello + # or inputs.nixpkgs.legacyPackages.${pkgs.stdenv.hostPlatform.system}.hello + config.flake.packages.${pkgs.stdenv.hostPlatform.system}.hello ]; }; }; diff --git a/template/unfree/flake.nix b/template/unfree/flake.nix index 7ec00c35..d5b0d3fd 100644 --- a/template/unfree/flake.nix +++ b/template/unfree/flake.nix @@ -3,16 +3,12 @@ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - outputs = inputs@{ flake-parts, nixpkgs, ... }: + outputs = inputs@{ flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { systems = [ "x86_64-linux" "aarch64-darwin" ]; + # This sets `pkgs` to a Nixpkgs with the `allowUnfree` option set. + nixpkgs.settings.config.allowUnfree = true; perSystem = { pkgs, system, ... }: { - # This sets `pkgs` to a nixpkgs with allowUnfree option set. - _module.args.pkgs = import nixpkgs { - inherit system; - config.allowUnfree = true; - }; - packages.default = pkgs.hello-unfree; }; };