Merge pull request 'docs(service-modules): add description and docs for options' (#3848) from doc-1 into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3848
This commit is contained in:
hsjobeki
2025-06-04 12:44:32 +00:00
10 changed files with 397 additions and 119 deletions

View File

@@ -80,7 +80,7 @@ nav:
- macOS: guides/macos.md - macOS: guides/macos.md
- Reference: - Reference:
- Overview: reference/index.md - Overview: reference/index.md
- Clan Services: - Services:
- Overview: reference/clanServices/index.md - Overview: reference/clanServices/index.md
- reference/clanServices/admin.md - reference/clanServices/admin.md
- reference/clanServices/auto-upgrade.md - reference/clanServices/auto-upgrade.md
@@ -93,7 +93,8 @@ nav:
- reference/clanServices/hello-world.md - reference/clanServices/hello-world.md
- reference/clanServices/wifi.md - reference/clanServices/wifi.md
- reference/clanServices/zerotier.md - reference/clanServices/zerotier.md
- Clan Modules: - Interface for making Services: reference/clanServices/clan-service-author-interface.md
- Modules:
- Overview: reference/clanModules/index.md - Overview: reference/clanModules/index.md
- reference/clanModules/frontmatter/index.md - reference/clanModules/frontmatter/index.md
# TODO: display the docs of the clan.service modules # TODO: display the docs of the clan.service modules

View File

@@ -90,6 +90,7 @@
export CLAN_MODULES_VIA_ROLES=${clanModulesViaRoles} export CLAN_MODULES_VIA_ROLES=${clanModulesViaRoles}
export CLAN_MODULES_VIA_SERVICE=${clanModulesViaService} export CLAN_MODULES_VIA_SERVICE=${clanModulesViaService}
export CLAN_MODULES_VIA_NIX=${clanModulesViaNix} export CLAN_MODULES_VIA_NIX=${clanModulesViaNix}
export CLAN_SERVICE_INTERFACE=${self'.legacyPackages.clan-service-module-interface}/share/doc/nixos/options.json
# Frontmatter format for clanModules # Frontmatter format for clanModules
export CLAN_MODULES_FRONTMATTER_DOCS=${clanModulesFrontmatter}/share/doc/nixos/options.json export CLAN_MODULES_FRONTMATTER_DOCS=${clanModulesFrontmatter}/share/doc/nixos/options.json

View File

@@ -49,6 +49,10 @@ CLAN_MODULES_VIA_NIX = os.environ.get("CLAN_MODULES_VIA_NIX")
# Some modules can be imported via inventory # Some modules can be imported via inventory
CLAN_MODULES_VIA_ROLES = os.environ.get("CLAN_MODULES_VIA_ROLES") CLAN_MODULES_VIA_ROLES = os.environ.get("CLAN_MODULES_VIA_ROLES")
# Options how to author clan.modules
# perInstance, perMachine, ...
CLAN_SERVICE_INTERFACE = os.environ.get("CLAN_SERVICE_INTERFACE")
CLAN_MODULES_VIA_SERVICE = os.environ.get("CLAN_MODULES_VIA_SERVICE") CLAN_MODULES_VIA_SERVICE = os.environ.get("CLAN_MODULES_VIA_SERVICE")
OUT = os.environ.get("out") OUT = os.environ.get("out")
@@ -309,7 +313,7 @@ def produce_clan_core_docs() -> None:
core_outputs[indexfile] += """!!! info "Submodules"\n""" core_outputs[indexfile] += """!!! info "Submodules"\n"""
for submodule_name, split_options in split.items(): for submodule_name, split_options in split.items():
root = options_to_tree(split_options, debug=True) root = options_to_tree(split_options)
module = root.suboptions[0] module = root.suboptions[0]
module_type = module.info.get("type") module_type = module.info.get("type")
if module_type is not None and "submodule" not in module_type: if module_type is not None and "submodule" not in module_type:
@@ -798,6 +802,44 @@ def split_options_by_root(options: dict[str, Any]) -> dict[str, dict[str, Any]]:
return res return res
def produce_clan_service_author_docs() -> None:
if not CLAN_SERVICE_INTERFACE:
msg = f"Environment variables are not set correctly: CLAN_SERVICE_INTERFACE={CLAN_SERVICE_INTERFACE}. Expected a path to the optionsJSON"
raise ClanError(msg)
if not OUT:
msg = f"Environment variables are not set correctly: $out={OUT}"
raise ClanError(msg)
output = """
This document describes the structure and configurable attributes of a `clan.service` module.
Typically needed by module authors to define roles, behavior and metadata for distributed services.
!!! Note
This is not a user-facing documentation, but rather meant as a reference for *module authors*
See: [clanService Authoring Guide](../../guides/authoring/clanServices/index.md)
"""
# Inventory options are already included under the buildClan attribute
# We just omitted them in the buildClan docs, because we want a separate output for the inventory model
with Path(CLAN_SERVICE_INTERFACE).open() as f:
options: dict[str, dict[str, Any]] = json.load(f)
options_tree = options_to_tree(options, debug=True)
# Find the inventory options
# Render the inventory options
# This for loop excludes the root node
# for option in options_tree.suboptions:
output += options_docs_from_tree(options_tree, init_level=2)
outfile = Path(OUT) / "clanServices/clan-service-author-interface.md"
outfile.parent.mkdir(parents=True, exist_ok=True)
with Path.open(outfile, "w") as of:
of.write(output)
@dataclass @dataclass
class Option: class Option:
name: str name: str
@@ -959,6 +1001,8 @@ if __name__ == "__main__": #
produce_build_clan_docs() produce_build_clan_docs()
produce_inventory_docs() produce_inventory_docs()
produce_clan_service_author_docs()
produce_clan_modules_docs() produce_clan_modules_docs()
produce_clan_service_docs() produce_clan_service_docs()

View File

@@ -8,7 +8,9 @@
## Service Module Specification ## Service Module Specification
This section explains how to author a clan service module. This section explains how to author a clan service module.
We discussed the initial architecture in [01-clan-service-modules](../../../decisions/01-ClanModules.md) and decided to rework the format as follows: We discussed the initial architecture in [01-clan-service-modules](../../../decisions/01-ClanModules.md) and decided to rework the format.
For the full specification and current state see: **[Service Author Reference](../../../reference/clanServices/clan-service-author-interface.md)**
### A Minimal module ### A Minimal module
@@ -49,6 +51,8 @@ The imported module file must fulfill at least the following requirements:
} }
``` ```
For more attributes see: **[Service Author Reference](../../../reference/clanServices/clan-service-author-interface.md)**
### Adding functionality to the module ### Adding functionality to the module
While the very minimal module is valid in itself it has no way of adding any machines to it, because it doesn't specify any roles. While the very minimal module is valid in itself it has no way of adding any machines to it, because it doesn't specify any roles.
@@ -254,3 +258,11 @@ outputs = inputs: flake-parts.lib.mkFlake { inherit inputs; } ({self, lib, ...}:
``` ```
The benefit of this approach is that downstream users can override the value of `myClan` by using `mkForce` or other priority modifiers. The benefit of this approach is that downstream users can override the value of `myClan` by using `mkForce` or other priority modifiers.
---
## Further
- [Reference Documentation for Service Authors](../../../reference/clanServices/clan-service-author-interface.md)
- [Migration Guide from ClanModules to ClanServices](../../migrations/migrate-inventory-services.md)
- [Decision that lead to ClanServices](../../../decisions/01-ClanModules.md)

View File

@@ -10,6 +10,7 @@ in
{ lib, config, ... }: { lib, config, ... }:
{ {
options.result.api = lib.mkOption { options.result.api = lib.mkOption {
visible = false;
default = { }; default = { };
type = lib.types.submodule ({ type = lib.types.submodule ({
options.schema = lib.mkOption { options.schema = lib.mkOption {

View File

@@ -26,13 +26,6 @@ let
${builtins.toJSON (lib.attrNames config.roles)} ${builtins.toJSON (lib.attrNames config.roles)}
''; '';
# checkInstanceSettings =
# instanceName: instanceSettings:
# let
# unmatchedRoles = 1;
# in
# unmatchedRoles;
/** /**
Merges the role- and machine-settings using the role interface Merges the role- and machine-settings using the role interface
@@ -154,11 +147,29 @@ let
in in
{ {
options = { options = {
# TODO: deduplicate this with inventory.instances
# Although inventory has stricter constraints
instances = mkOption { instances = mkOption {
# Instances are created in the inventory
visible = false;
defaultText = "Throws: 'The service must define its instances' when not defined";
default = throw '' default = throw ''
The clan service module ${config.manifest.name} doesn't define any instances. The clan service module ${config.manifest.name} doesn't define any instances.
Did you forget to create instances via 'inventory.instances' ? Did you forget to create instances via 'inventory.instances'?
'';
description = ''
Instances of the service.
An Instance is a user-specific deployment or configuration of a service.
It represents the active usage of the service configured to the user's settings or use case.
The `<instanceName>` of the instance is arbitrary, but must be unique.
A common best practice is to name the instance after the 'service' and the 'use-case'.
For example:
- 'instances.zerotier-homelab = ...' for a zerotier instance that connects all machines of a homelab
''; '';
type = attrsWith { type = attrsWith {
@@ -174,6 +185,21 @@ in
# apply = v: lib.seq (checkInstanceSettings name v) v; # apply = v: lib.seq (checkInstanceSettings name v) v;
# }; # };
options.roles = mkOption { options.roles = mkOption {
description = ''
Roles of the instance.
A role is a specific behavior or configuration of the service.
It defines how the service should behave in the context of this instance.
The `<roleName>` must match one of the roles defined in the service
For example:
- 'roles.client = ...' for a client role that connects to the service
- 'roles.server = ...' for a server role that provides the service
Throws an error if empty, since this would mean that the service has no members.
'';
defaultText = "Throws: 'The service must define members via roles' when not defined";
default = throw '' default = throw ''
Instance '${name}' of service '${config.manifest.name}' mut define members via 'roles'. Instance '${name}' of service '${config.manifest.name}' mut define members via 'roles'.
@@ -184,43 +210,51 @@ in
placeholder = "roleName"; placeholder = "roleName";
elemType = submoduleWith { elemType = submoduleWith {
modules = [ modules = [
( ({
{ ... }: # instances.{instanceName}.roles.{roleName}.machines
{ options.machines = mkOption {
# instances.{instanceName}.roles.{roleName}.machines description = ''
options.machines = mkOption { Machines of the role.
type = attrsWith {
placeholder = "machineName"; A machine is a physical or virtual machine that is part of the instance.
elemType = submoduleWith { The `<machineName>` must match the name of any machine defined in the clan.
modules = [
(m: { For example:
options.settings = mkOption {
type = types.raw; - 'machines.my-machine = { ...; }' for a machine that is part of the instance
description = "Settings of '${name}-machine': ${m.name}."; - 'machines.my-other-machine = { ...; }' for another machine that is part of the instance
default = { }; '';
}; type = attrsWith {
}) placeholder = "machineName";
]; elemType = submoduleWith {
}; modules = [
(m: {
options.settings = mkOption {
type = types.raw;
description = "Settings of '${name}-machine': ${m.name or "<machineName>"}.";
default = { };
};
})
];
}; };
}; };
};
# instances.{instanceName}.roles.{roleName}.settings # instances.{instanceName}.roles.{roleName}.settings
# options._settings = mkOption { }; # options._settings = mkOption { };
# options._settingsViaTags = mkOption { }; # options._settingsViaTags = mkOption { };
# A deferred module that combines _settingsViaTags with _settings # A deferred module that combines _settingsViaTags with _settings
options.settings = mkOption { options.settings = mkOption {
type = types.raw; type = types.raw;
description = "Settings of 'role': ${name}"; description = "Settings of 'role': ${name}";
default = { }; default = { };
}; };
options.extraModules = lib.mkOption { options.extraModules = lib.mkOption {
default = [ ]; default = [ ];
type = types.listOf (types.deferredModule); type = types.listOf (types.deferredModule);
}; };
} })
)
]; ];
}; };
}; };
@@ -242,6 +276,22 @@ in
}; };
}; };
roles = mkOption { roles = mkOption {
description = ''
Roles of the service.
A role is a specific behavior or configuration of the service.
It defines how the service should behave in the context of the clan.
The `<roleName>`s of the service are defined here. Later usage of the roles must match one of the `roleNames`.
For example:
- 'roles.client = ...' for a client role that connects to the service
- 'roles.server = ...' for a server role that provides the service
Throws an error if empty, since this would mean that the service has no way of adding members.
'';
defaultText = "Throws: 'The service must define its roles' when not defined";
default = throw '' default = throw ''
Role behavior of service '${config.manifest.name}' must be defined. Role behavior of service '${config.manifest.name}' must be defined.
A 'clan.service' module should always define its behavior via 'roles' A 'clan.service' module should always define its behavior via 'roles'
@@ -263,32 +313,138 @@ in
in in
{ {
options.interface = mkOption { options.interface = mkOption {
description = ''
Abstract interface of the role.
This is an abstract module which should define 'options' for the role's settings.
Example:
```nix
{
options.timeout = mkOption {
type = types.int;
default = 30;
description = "Timeout in seconds";
};
}
```
Note:
- `machine.config` is not available here, since the role is definition is abstract.
- *defaults* that depend on the *machine* or *instance* should be added to *settings* later in 'perInstance' or 'perMachine'
'';
type = types.deferredModule; type = types.deferredModule;
# TODO: Default to an empty module # TODO: Default to an empty module
# need to test that an the empty module can be evaluated to empty settings # need to test that an the empty module can be evaluated to empty settings
default = { }; default = { };
}; };
options.perInstance = mkOption { options.perInstance = mkOption {
type = types.deferredModuleWith { description = ''
staticModules = [ Per-instance configuration of the role.
# Common output format
# As described by adr This option is used to define instance-specific behavior for the service-role. (Example below)
# { nixosModule, services, ... }
( Although the type is a `deferredModule`, it helps to think of it as a function.
{ ... }: The 'function' takes the `instance-name` and some other `arguments`.
*Arguments*:
- `instanceName` (`string`): The name of the instance.
- `machine`: Machine information, containing:
```nix
{ {
options.nixosModule = mkOption { default = { }; }; name = "machineName";
options.services = mkOption { roles = ["client" "server" ... ];
type = attrsWith { }
placeholder = "serviceName"; ```
elemType = submoduleWith { - `roles`: Attribute set of all roles of the instance, in the form:
modules = [ ./service-module.nix ]; ```nix
roles = {
client = {
machines = {
jon = {
settings = {
timeout = 60;
}; };
}; };
default = { }; # ...
}; };
} settings = {
) timeout = 30;
};
};
# ...
};
```
- `settings`: The settings of the role, as defined in `inventory`
```nix
{
timeout = 30;
}
```
- `extendSettings`: A function that takes a module and returns a new module with extended settings.
```nix
extendSettings {
timeout = mkForce 60;
};
->
{
timeout = 60;
}
```
*Returns* an `attribute set` containing:
- `nixosModule`: The NixOS module for the instance.
'';
type = types.deferredModuleWith {
staticModules = [
({
options.nixosModule = mkOption {
type = types.deferredModule;
default = { };
description = ''
This module is later imported to configure the machine with the config derived from service's settings.
Example:
```nix
roles.client.perInstance = { instanceName, ... }:
{
# Keep in mind that this module is produced once per-instance
# Meaning you might end up with multiple of these modules.
# Make sure they can be imported all together without conflicts
#
# nixos-config
nixosModule = { config ,... }: {
# create one systemd service per instance
# It is a common practice to concatenate the *service-name* and *instance-name*
# To ensure globally unique systemd-units for the target machine
systemd.services."webly-''${instanceName}" = {
...
};
};
}
```
'';
};
# TODO: Recursive services
options.services = mkOption {
visible = false;
type = attrsWith {
placeholder = "serviceName";
elemType = submoduleWith {
modules = [ ./service-module.nix ];
};
};
apply = _: throw "Not implemented yet";
default = { };
};
})
]; ];
}; };
default = { }; default = { };
@@ -333,26 +489,78 @@ in
}; };
perMachine = mkOption { perMachine = mkOption {
description = ''
Per-machine configuration of the service.
This option is used to define machine-specific settings for the service **once**, if any service-instance is used.
Although the type is a `deferredModule`, it helps to think of it as a function.
The 'function' takes the `machine-name` and some other 'arguments'
*Arguments*:
- `machine`: `{ name :: string; roles :: listOf String }`
- `instances`: The scope of the machine, containing all instances and roles that the machine is part of.
```nix
{
instances = {
<instanceName> = {
roles = {
<roleName> = {
# Per-machine settings
machines = { <machineName> = { settings = { ... }; }; }; };
# Per-role settings
settings = { ... };
};
};
};
}
```
*Returns* an `attribute set` containing:
- `nixosModule`: The NixOS module for the machine.
'';
type = types.deferredModuleWith { type = types.deferredModuleWith {
staticModules = [ staticModules = [
# Common output format ({
# As described by adr options.nixosModule = mkOption {
# { nixosModule, services, ... } type = types.deferredModule;
( default = { };
{ ... }: description = ''
{ A single NixOS module for the machine.
options.nixosModule = mkOption { default = { }; };
options.services = mkOption { This module is later imported to configure the machine with the config derived from service's settings.
type = attrsWith {
placeholder = "serviceName"; Example:
elemType = submoduleWith {
modules = [ ./service-module.nix ]; ```nix
}; # machine.roles ...
perMachine = { machine, ... }:
{ # nixos-config
nixosModule = { config ,... }: {
systemd.services.foo = {
enable = true;
};
}
}
```
'';
};
# TODO: Recursive services
options.services = mkOption {
visible = false;
type = attrsWith {
placeholder = "serviceName";
elemType = submoduleWith {
modules = [ ./service-module.nix ];
}; };
default = { };
}; };
} apply = _: throw "Not implemented yet";
) default = { };
};
})
]; ];
}; };
default = { }; default = { };
@@ -428,6 +636,7 @@ in
} }
*/ */
result.allRoles = mkOption { result.allRoles = mkOption {
visible = false;
readOnly = true; readOnly = true;
default = lib.mapAttrs (roleName: roleCfg: { default = lib.mapAttrs (roleName: roleCfg: {
allInstances = lib.mapAttrs (instanceName: instanceCfg: { allInstances = lib.mapAttrs (instanceName: instanceCfg: {
@@ -436,15 +645,14 @@ in
let let
instanceRes = roleCfg.perInstance instanceName machineName; instanceRes = roleCfg.perInstance instanceName machineName;
in in
{ instanceRes
// {
nixosModule = { nixosModule = {
imports = [ imports = [
# Result of the applied 'perInstance = {...}: { nixosModule = { ... }; }' # Result of the applied 'perInstance = {...}: { nixosModule = { ... }; }'
instanceRes.nixosModule instanceRes.nixosModule
] ++ instanceCfg.roles.${roleName}.extraModules; ] ++ instanceCfg.roles.${roleName}.extraModules;
}; };
# TODO: nested services
services = { };
} }
) instanceCfg.roles.${roleName}.machines or { }; ) instanceCfg.roles.${roleName}.machines or { };
@@ -454,10 +662,12 @@ in
result.assertions = mkOption { result.assertions = mkOption {
default = { }; default = { };
visible = false;
type = types.attrsOf types.raw; type = types.attrsOf types.raw;
}; };
result.allMachines = mkOption { result.allMachines = mkOption {
visible = false;
readOnly = true; readOnly = true;
default = default =
let let
@@ -505,6 +715,7 @@ in
}; };
result.final = mkOption { result.final = mkOption {
visible = false;
readOnly = true; readOnly = true;
default = lib.mapAttrs ( default = lib.mapAttrs (
machineName: machineResult: machineName: machineResult:

View File

@@ -37,17 +37,19 @@ let
}; };
in in
{ {
nixosModule = { options.passthru = lib.mkOption {
inherit default = {
instanceName inherit
settings instanceName
machine settings
roles machine
; roles
;
# We are double vendoring the settings # We are double vendoring the settings
# To test that we can do it indefinitely # To test that we can do it indefinitely
vendoredSettings = finalSettings; vendoredSettings = finalSettings;
};
}; };
}; };
}; };
@@ -96,30 +98,26 @@ let
. "foo-peer"; }; }; }; settings = { timeout = "foo-peer-jon"; }; vendoredSettings = { timeout = "conf . . "foo-peer"; }; }; }; settings = { timeout = "foo-peer-jon"; }; vendoredSettings = { timeout = "conf .
. ig.thing"; }; } ]; } . . ig.thing"; }; } ]; } .
*/ */
unwrapModule = m: (builtins.head m.imports);
in in
{ {
# settings should evaluate # settings should evaluate
test_per_instance_arguments = { test_per_instance_arguments = {
expr = expr = {
let instanceName =
m = ( res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.passthru.instanceName;
unwrapModule
res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.nixosModule
);
in
{
instanceName = m.instanceName;
# settings are specific. # settings are specific.
# Below we access: # Below we access:
# instance = instance_foo # instance = instance_foo
# roles = peer # roles = peer
# machines = jon # machines = jon
settings = m.settings; settings =
machine = m.machine; res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.settings;
roles = m.roles; machine =
}; res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.machine;
roles =
res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.roles;
};
expected = { expected = {
instanceName = "instance_foo"; instanceName = "instance_foo";
settings = { settings = {
@@ -161,10 +159,9 @@ in
# TODO: Cannot be tested like this anymore # TODO: Cannot be tested like this anymore
test_per_instance_settings_vendoring = { test_per_instance_settings_vendoring = {
x = res.importedModulesEvaluated.self-A.config;
expr = expr =
(unwrapModule res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.passthru.vendoredSettings;
res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances."instance_foo".allMachines.jon.nixosModule
).vendoredSettings;
expected = { expected = {
timeout = "config.thing"; timeout = "config.thing";
}; };

View File

@@ -28,8 +28,10 @@ let
perMachine = perMachine =
{ instances, machine, ... }: { instances, machine, ... }:
{ {
nixosModule = { options.passthru = lib.mkOption {
inherit instances machine; default = {
inherit instances machine;
};
}; };
}; };
}; };
@@ -76,7 +78,7 @@ in
inherit res; inherit res;
expr = { expr = {
hasMachineSettings = hasMachineSettings =
res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instances.instance_foo.roles.peer.machines.jon res.importedModulesEvaluated.self-A.config.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon
? settings; ? settings;
# settings are specific. # settings are specific.
@@ -84,10 +86,10 @@ in
# instance = instance_foo # instance = instance_foo
# roles = peer # roles = peer
# machines = jon # machines = jon
specificMachineSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instances.instance_foo.roles.peer.machines.jon.settings; specificMachineSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.machines.jon.settings;
hasRoleSettings = hasRoleSettings =
res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instances.instance_foo.roles.peer res.importedModulesEvaluated.self-A.config.result.allMachines.jon.passthru.instances.instance_foo.roles.peer
? settings; ? settings;
# settings are specific. # settings are specific.
@@ -95,7 +97,7 @@ in
# instance = instance_foo # instance = instance_foo
# roles = peer # roles = peer
# machines = * # machines = *
specificRoleSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instances.instance_foo.roles.peer.settings; specificRoleSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.passthru.instances.instance_foo.roles.peer.settings;
}; };
expected = { expected = {
hasMachineSettings = true; hasMachineSettings = true;

View File

@@ -43,6 +43,16 @@ in
} }
); );
legacyPackages.clan-service-module-interface =
(pkgs.nixosOptionsDoc {
options =
(self.clanLib.inventory.evalClanService {
modules = [ ];
prefix = [ ];
}).options;
warningsAreErrors = true;
}).optionsJSON;
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests # Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests
legacyPackages.evalTests-inventory = import ./tests { legacyPackages.evalTests-inventory = import ./tests {
inherit lib; inherit lib;

View File

@@ -20,6 +20,5 @@
in in
{ {
imports = [ test-types-module ]; imports = [ test-types-module ];
legacyPackages.xxx = { };
}; };
} }