diff --git a/modules/dream2nix/groups/group.nix b/modules/dream2nix/groups/group.nix index 5969e4bc..3e1fd492 100644 --- a/modules/dream2nix/groups/group.nix +++ b/modules/dream2nix/groups/group.nix @@ -1,21 +1,28 @@ -{commonModule}: { - lib, - dream2nix, +{ + commonModule, + globalOverrides, +}: { config, + dream2nix, + lib, specialArgs, ... }: let t = lib.types; - packageType = t.deferredModuleWith { - staticModules = [ - dream2nix.modules.dream2nix.core - {_module.args = specialArgs;} - # the top-level commonModule - commonModule - # the commonModule of the current group - config.commonModule - ]; - }; + packageType = name: + t.deferredModuleWith { + staticModules = [ + {_module.args = specialArgs;} + # the top-level commonModule + commonModule + # the commonModule of the current group + config.commonModule + # the global overrides + (globalOverrides.${name} or {}) + # the overrides of the current group + (config.overrides.${name} or {}) + ]; + }; in { options = { commonModule = lib.mkOption { @@ -26,10 +33,15 @@ in { default = {}; }; overrides = lib.mkOption { - type = t.attrs; + type = t.lazyAttrsOf (t.deferredModuleWith { + staticModules = [ + {_module.args = specialArgs;} + ]; + }); description = '' - A set of package overrides + Holds overrides for the packages in the current groups ''; + default = {}; }; packages = lib.mkOption { description = '' @@ -56,41 +68,51 @@ in { ``` ''; # name version options - type = t.lazyAttrsOf (t.lazyAttrsOf (t.submoduleWith { - modules = [ - ({config, ...}: { - options.module = lib.mkOption { - # this is a deferredModule type - type = packageType; - description = '' - The package configuration - ''; - default = {}; - }; - options.evaluated = lib.mkOption { - type = t.submoduleWith { - modules = [config.module]; - inherit specialArgs; + type = let + submoduleWithNameVersion = import ./submoduleWithNameVersion.nix { + inherit lib; + }; + in + t.lazyAttrsOf (t.lazyAttrsOf (submoduleWithNameVersion { + modules = [ + ({ + config, + name, + version, + ... + }: { + options.module = lib.mkOption { + # this is a deferredModule type + type = packageType name; + description = '' + The package configuration + ''; + default = {}; }; - description = '' - The evaluated dream2nix package modules - ''; - internal = true; - default = {}; - }; - options.public = lib.mkOption { - type = t.package; - description = '' - The evaluated package ready to consume - ''; - readOnly = true; - default = config.evaluated.public; - defaultText = lib.literalExpression "config.evaluated.public"; - }; - }) - ]; - inherit specialArgs; - })); + options.evaluated = lib.mkOption { + type = t.submoduleWith { + modules = [config.module]; + inherit specialArgs; + }; + description = '' + The evaluated dream2nix package modules + ''; + internal = true; + default = {}; + }; + options.public = lib.mkOption { + type = t.package; + description = '' + The evaluated package ready to consume + ''; + readOnly = true; + default = config.evaluated.public; + defaultText = lib.literalExpression "config.evaluated.public"; + }; + }) + ]; + inherit specialArgs; + })); }; }; } diff --git a/modules/dream2nix/groups/interface.nix b/modules/dream2nix/groups/interface.nix index 86cc06e9..8edfe2b7 100644 --- a/modules/dream2nix/groups/interface.nix +++ b/modules/dream2nix/groups/interface.nix @@ -8,7 +8,10 @@ t = lib.types; groupType = t.submoduleWith { modules = [ - (import ./group.nix {inherit (config) commonModule;}) + (import ./group.nix { + inherit (config) commonModule; + globalOverrides = config.overrides; + }) ]; inherit specialArgs; }; @@ -28,5 +31,21 @@ in { ''; default = {}; }; + overrides = lib.mkOption { + type = t.lazyAttrsOf (t.deferredModuleWith { + staticModules = [ + {_module.args = specialArgs;} + ]; + }); + description = '' + Holds overrides for all packages in all groups + ''; + default = {}; + example = { + hello.postPatch = '' + substituteInPlace Makefile --replace /usr/local /usr + ''; + }; + }; }; } diff --git a/modules/dream2nix/groups/submoduleWithNameVersion.nix b/modules/dream2nix/groups/submoduleWithNameVersion.nix new file mode 100644 index 00000000..9e7814c4 --- /dev/null +++ b/modules/dream2nix/groups/submoduleWithNameVersion.nix @@ -0,0 +1,164 @@ +# copied from nixpkgs with a single line added (see TODO) +{lib, ...}: let + inherit + (lib) + isAttrs + isFunction + optionalAttrs + last + mkOptionType + types + attrNames + ; + + inherit + (lib.types) + path + defaultFunctor + ; + + submoduleWith = { + modules, + specialArgs ? {}, + shorthandOnlyDefinesConfig ? false, + description ? null, + class ? null, + } @ attrs: let + inherit (lib.modules) evalModules; + + allModules = defs: + map ( + { + value, + file, + }: + if isAttrs value && shorthandOnlyDefinesConfig + then { + _file = file; + config = value; + } + else { + _file = file; + imports = [value]; + } + ) + defs; + + base = evalModules { + inherit class specialArgs; + modules = + [ + { + # This is a work-around for the fact that some sub-modules, + # such as the one included in an attribute set, expects an "args" + # attribute to be given to the sub-module. As the option + # evaluation does not have any specific attribute name yet, we + # provide a default for the documentation and the freeform type. + # + # This is necessary as some option declaration might use the + # "name" attribute given as argument of the submodule and use it + # as the default of option declarations. + # + # We use lookalike unicode single angle quotation marks because + # of the docbook transformation the options receive. In all uses + # > and < wouldn't be encoded correctly so the encoded values + # would be used, and use of `<` and `>` would break the XML document. + # It shouldn't cause an issue since this is cosmetic for the manual. + _module.args.name = lib.mkOptionDefault "‹name›"; + } + ] + ++ modules; + }; + + freeformType = base._module.freeformType; + + name = "submodule"; + in + mkOptionType { + inherit name; + description = + if description != null + then description + else freeformType.description or name; + check = x: isAttrs x || isFunction x || path.check x; + merge = loc: defs: + (base.extendModules { + modules = + [ + { + # this is the only line that was added + # TODO: think about ways to upstream this + _module.args.name = lib.elemAt loc ((lib.length loc) - 2); + _module.args.version = lib.last loc; + } + ] + ++ allModules defs; + prefix = loc; + }) + .config; + emptyValue = {value = {};}; + getSubOptions = prefix: + (base.extendModules + {inherit prefix;}) + .options + // optionalAttrs (freeformType != null) { + # Expose the sub options of the freeform type. Note that the option + # discovery doesn't care about the attribute name used here, so this + # is just to avoid conflicts with potential options from the submodule + _freeformOptions = freeformType.getSubOptions prefix; + }; + getSubModules = modules; + substSubModules = m: + submoduleWith (attrs + // { + modules = m; + }); + nestedTypes = lib.optionalAttrs (freeformType != null) { + freeformType = freeformType; + }; + functor = + defaultFunctor name + // { + type = types.submoduleWith; + payload = { + inherit modules class specialArgs shorthandOnlyDefinesConfig description; + }; + binOp = lhs: rhs: { + class = + # `or null` was added for backwards compatibility only. `class` is + # always set in the current version of the module system. + if lhs.class or null == null + then rhs.class or null + else if rhs.class or null == null + then lhs.class or null + else if lhs.class or null == rhs.class + then lhs.class or null + else throw "A submoduleWith option is declared multiple times with conflicting class values \"${toString lhs.class}\" and \"${toString rhs.class}\"."; + modules = lhs.modules ++ rhs.modules; + specialArgs = let + intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs; + in + if intersecting == {} + then lhs.specialArgs // rhs.specialArgs + else throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\""; + shorthandOnlyDefinesConfig = + if lhs.shorthandOnlyDefinesConfig == null + then rhs.shorthandOnlyDefinesConfig + else if rhs.shorthandOnlyDefinesConfig == null + then lhs.shorthandOnlyDefinesConfig + else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig + then lhs.shorthandOnlyDefinesConfig + else throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values"; + description = + if lhs.description == null + then rhs.description + else if rhs.description == null + then lhs.description + else if lhs.description == rhs.description + then lhs.description + else throw "A submoduleWith option is declared multiple times with conflicting descriptions"; + }; + }; + }; +in + submoduleWith diff --git a/tests/nix-unit/fixtures.nix b/tests/nix-unit/fixtures.nix index 88952f70..7bd5080e 100644 --- a/tests/nix-unit/fixtures.nix +++ b/tests/nix-unit/fixtures.nix @@ -7,6 +7,7 @@ ]; inherit name; + version = "1.0.0"; # set options builtins-derivation = { diff --git a/tests/nix-unit/test_groups/default.nix b/tests/nix-unit/test_groups/default.nix index 33ac82d3..66d902c1 100644 --- a/tests/nix-unit/test_groups/default.nix +++ b/tests/nix-unit/test_groups/default.nix @@ -35,4 +35,50 @@ in { expr = "${config.groups.my-group.packages.hello."1.0.0".public.name}"; expected = "hello-mod"; }; + + groups_overrides_global = let + config = eval { + groups.my-group.packages.foo."1.0.0".module = {...}: fixtures.basic-derivation; + groups.my-group.packages.bar."1.0.0".module = {...}: fixtures.basic-derivation; + overrides = {foo = {version = lib.mkForce "2.0.0";};}; + }; + in { + test_foo_changed = { + expr = "${config.groups.my-group.packages.foo."1.0.0".public.version}"; + expected = "2.0.0"; + }; + test_bar_unchanged = { + expr = "${config.groups.my-group.packages.bar."1.0.0".public.version}"; + expected = "1.0.0"; + }; + }; + + groups_overrides_local = let + config = eval { + groups.my-group.packages.foo."1.0.0".module = {...}: fixtures.basic-derivation; + groups.my-group.packages.bar."1.0.0".module = {...}: fixtures.basic-derivation; + groups.my-group.overrides = {foo = {version = lib.mkForce "2.0.0";};}; + }; + in { + test_foo_changed = { + expr = "${config.groups.my-group.packages.foo."1.0.0".public.version}"; + expected = "2.0.0"; + }; + test_bar_unchanged = { + expr = "${config.groups.my-group.packages.bar."1.0.0".public.version}"; + expected = "1.0.0"; + }; + }; + + test_groups_overrides_collision = let + config = eval { + groups.my-group.packages.foo."1.0.0".module = {...}: fixtures.basic-derivation; + groups.my-group.packages.bar."1.0.0".module = {...}: fixtures.basic-derivation; + overrides = {foo = {version = lib.mkForce "2.0.0";};}; + groups.my-group.overrides = {foo = {version = lib.mkForce "3.0.0";};}; + }; + in { + expr = "${config.groups.my-group.packages.foo."1.0.0".public.version}"; + expectedError.msg = ''The option `groups.my-group.packages.foo."1.0.0".evaluated.version' has conflicting definition values:''; + }; }