groups: implement global and local overrides

implement options:
- options.overrides
- options.groups.<name>.overrides

Having this, it should not be necessary anymore to override members of a group directly. Instead the logic can be bound to package names, either globally for all groups, or locally for a single group.

We should probably also rename :
- `options.commonModule` -> `options.overrideAll`
- `options.groups.<name>.commonModules` -> `options.groups.<name>.overrideAll`
This commit is contained in:
DavHau 2023-10-29 01:54:25 +00:00 committed by mergify[bot]
parent 4bd6022300
commit 752936f0c2
5 changed files with 302 additions and 50 deletions

View File

@ -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;
}));
};
};
}

View File

@ -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
'';
};
};
};
}

View File

@ -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
# &gt; and &lt; 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

View File

@ -7,6 +7,7 @@
];
inherit name;
version = "1.0.0";
# set options
builtins-derivation = {

View File

@ -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:'';
};
}