system.services: Remove ambiguous, redundant pkgs module argument

Primary reasons: remove implicit dependencies and force uniformity.
See nixos/modules/system/service/README.md for detailed rationale.

Changed files
+174 -41
nixos
doc
manual
development
modules
system
service
tests
pkgs
by-name
gh
development
interpreters
+5 -1
nixos/README-modular-services.md
···
Provide it as the first attribute in the module:
```nix
+
# Non-module dependencies (`importApply`)
+
{ writeScript, runtimeShell }:
+
+
# Service module
{ lib, config, ... }:
{
_class = "service";
···
passthru = {
services = {
default = {
-
imports = [ ./service.nix ];
+
imports = [ (lib.modules.importApply ./service.nix { inherit pkgs; }) ];
example.package = finalAttrs.finalPackage;
# ...
};
+3 -1
nixos/doc/manual/development/modular-services.md
···
## Writing and Reviewing a Modular Service {#modular-service-review}
-
Refer to the contributor documentation in [`nixos/README-modular-services.md`](https://github.com/NixOS/nixpkgs/blob/master/nixos/README-modular-services.md).
+
A typical service module consists of the following:
+
+
For more details, refer to the contributor documentation in [`nixos/README-modular-services.md`](https://github.com/NixOS/nixpkgs/blob/master/nixos/README-modular-services.md).
## Portable Service Options {#modular-service-options-portable}
+90
nixos/modules/system/service/README.md
···
- **Simple attribute structure**: Unlike `environment.etc`, `configData` uses a simpler structure with just `enable`, `name`, `text`, `source`, and `path` attributes. Complex ownership options were omitted for simplicity and portability.
Per-service user creation is still TBD.
+
+
## No `pkgs` module argument
+
+
The modular service infrastructure avoids exposing `pkgs` as a module argument to service modules. Instead, derivations and builder functions are provided through lexical closure, making dependency relationships explicit and avoiding uncertainty about where dependencies come from.
+
+
### Benefits
+
+
- **Explicit dependencies**: Services declare what they need rather than implicitly depending on `pkgs`
+
- **No interference**: Service modules can be reused in different contexts without assuming a specific `pkgs` instance. An unexpected `pkgs` version is not a failure mode anymore.
+
- **Clarity**: With fewer ways to do things, there's no ambiguity about where dependencies come from (from the module, not the OS or service manager)
+
+
### Implementation
+
+
- **Portable layer**: Service modules in `portable/` do not receive `pkgs` as a module argument. Any required derivations must be provided by the caller.
+
+
- **Systemd integration**: The `systemd/system.nix` module imports `config-data.nix` as a function, providing `pkgs` in lexical closure:
+
```nix
+
(import ../portable/config-data.nix { inherit pkgs; })
+
```
+
+
- **Service modules**:
+
1. Should explicitly declare their package dependencies as options rather than using `pkgs` defaults:
+
```nix
+
{
+
# Bad: uses pkgs module argument
+
foo.package = mkOption {
+
default = pkgs.python3;
+
# ...
+
};
+
}
+
```
+
+
```nix
+
{
+
# Good: caller provides the package
+
foo.package = mkOption {
+
type = types.package;
+
description = "Python package to use";
+
defaultText = lib.literalMD "The package that provided this module.";
+
};
+
}
+
```
+
+
2. `passthru.services` can still provide a complete module using the package's lexical scope, making the module truly self-contained:
+
+
**Package (`package.nix`):**
+
```nix
+
{
+
lib,
+
writeScript,
+
runtimeShell,
+
# ... other dependencies
+
}:
+
stdenv.mkDerivation (finalAttrs: {
+
# ... package definition
+
+
passthru.services.default = {
+
imports = [
+
(lib.modules.importApply ./service.nix {
+
inherit writeScript runtimeShell;
+
})
+
];
+
someService.package = finalAttrs.finalPackage;
+
};
+
})
+
```
+
+
**Service module (`service.nix`):**
+
```nix
+
# Non-module dependencies (importApply)
+
{ writeScript, runtimeShell }:
+
+
# Service module
+
{
+
lib,
+
config,
+
options,
+
...
+
}:
+
{
+
# Service definition using writeScript, runtimeShell from lexical scope
+
process.argv = [
+
(writeScript "wrapper" ''
+
#!${runtimeShell}
+
# ... wrapper logic
+
'')
+
# ... other args
+
];
+
}
+
```
+4 -1
nixos/modules/system/service/portable/config-data.nix
···
# Tests in: ../../../../tests/modular-service-etc/test.nix
+
+
# Non-modular context provided by the modular services integration.
+
{ pkgs }:
+
# Configuration data support for portable services
# This module provides configData for services, enabling configuration reloading
# without terminating and restarting the service process.
{
lib,
-
pkgs,
...
}:
let
-1
nixos/modules/system/service/portable/service.nix
···
_class = "service";
imports = [
../../../misc/assertions.nix
-
./config-data.nix
];
options = {
services = mkOption {
+4 -7
nixos/modules/system/service/systemd/system.nix
···
modules = [
./service.nix
./config-data-path.nix
+
(lib.modules.importApply ../portable/config-data.nix { inherit pkgs; })
-
# TODO: Consider removing pkgs. Service modules can provide their own
-
# dependencies.
{
# Extend portable services option
options.services = lib.mkOption {
type = types.attrsOf (
types.submoduleWith {
-
specialArgs.pkgs = pkgs;
-
modules = [ ];
+
modules = [
+
(lib.modules.importApply ../portable/config-data.nix { inherit pkgs; })
+
];
}
);
};
···
];
specialArgs = {
# perhaps: features."systemd" = { };
-
# TODO: Consider removing pkgs. Service modules can provide their own
-
# dependencies.
-
inherit pkgs;
systemdPackage = config.systemd.package;
};
}
+3 -17
nixos/tests/modular-service-etc/python-http-server.nix
···
{
config,
lib,
-
pkgs,
...
}:
let
···
python-http-server = {
package = mkOption {
type = types.package;
-
default = pkgs.python3;
description = "Python package to use for the web server";
};
···
];
configData = {
-
# This should probably just be {} if we were to put this module in production.
-
"webroot" = lib.mkDefault {
-
source = pkgs.runCommand "default-webroot" { } ''
-
mkdir -p $out
-
cat > $out/index.html << 'EOF'
-
<!DOCTYPE html>
-
<html>
-
<head><title>Python Web Server</title></head>
-
<body>
-
<h1>Welcome to the Python Web Server</h1>
-
<p>Serving from port ${toString config.python-http-server.port}</p>
-
</body>
-
</html>
-
EOF
-
'';
+
"webroot" = {
+
# Enable only if directory is set to use this path
+
enable = lib.mkDefault (config.python-http-server.directory == config.configData."webroot".path);
};
};
};
+28 -2
nixos/tests/modular-service-etc/test.nix
···
nodes = {
server =
{ pkgs, ... }:
+
let
+
# Normally the package services.default attribute combines this, but we
+
# don't have that, because this is not a production service. Should it be?
+
python-http-server = {
+
imports = [ ./python-http-server.nix ];
+
python-http-server.package = pkgs.python3;
+
};
+
in
{
system.services.webserver = {
# The python web server is simple enough that it doesn't need a reload signal.
# Other services may need to receive a signal in order to re-read what's in `configData`.
-
imports = [ ./python-http-server.nix ];
+
imports = [ python-http-server ];
python-http-server = {
port = 8080;
};
+
configData = {
+
"webroot" = {
+
source = pkgs.runCommand "webroot" { } ''
+
mkdir -p $out
+
cat > $out/index.html << 'EOF'
+
<!DOCTYPE html>
+
<html>
+
<head><title>Python Web Server</title></head>
+
<body>
+
<h1>Welcome to the Python Web Server</h1>
+
<p>Serving from port 8080</p>
+
</body>
+
</html>
+
EOF
+
'';
+
};
+
};
+
# Add a sub-service
services.api = {
-
imports = [ ./python-http-server.nix ];
+
imports = [ python-http-server ];
python-http-server = {
port = 8081;
};
+2
nixos/tests/php/fpm-modular.nix
···
+
# Run with:
+
# nix-build -A nixosTests.php.fpm-modular
{ lib, php, ... }:
{
name = "php-${php.version}-fpm-modular-nginx-test";
+7 -1
pkgs/by-name/gh/ghostunnel/package.nix
···
ghostunnel,
apple-sdk_12,
darwinMinVersionHook,
+
writeScript,
+
runtimeShell,
}:
buildGoModule rec {
···
};
passthru.services.default = {
-
imports = [ ./service.nix ];
+
imports = [
+
(lib.modules.importApply ./service.nix {
+
inherit writeScript runtimeShell;
+
})
+
];
ghostunnel.package = ghostunnel; # FIXME: finalAttrs.finalPackage
};
+7 -3
pkgs/by-name/gh/ghostunnel/service.nix
···
+
# Non-module dependencies (`importApply`)
+
{ writeScript, runtimeShell }:
+
+
# Service module
{
lib,
config,
options,
-
pkgs,
...
}:
let
···
ghostunnel = {
package = mkOption {
description = "Package to use for ghostunnel";
+
defaultText = "The ghostunnel package that provided this module.";
type = types.package;
};
···
cfg.cacert
])
(
-
pkgs.writeScript "load-credentials" ''
-
#!${pkgs.runtimeShell}
+
writeScript "load-credentials" ''
+
#!${runtimeShell}
exec $@ ${
concatStringsSep " " (
optional (cfg.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore"
+7 -1
pkgs/development/interpreters/php/generic.nix
···
common-updater-scripts,
curl,
jq,
+
coreutils,
+
formats,
version,
phpSrc ? null,
···
inherit ztsSupport;
services.default = {
-
imports = [ ./service.nix ];
+
imports = [
+
(lib.modules.importApply ./service.nix {
+
inherit formats coreutils;
+
})
+
];
php-fpm.package = lib.mkDefault finalAttrs.finalPackage;
};
};
+14 -6
pkgs/development/interpreters/php/service.nix
···
+
# Tests in: nixos/tests/php/fpm-modular.nix
+
+
# Non-module dependencies (importApply)
+
{ formats, coreutils }:
+
+
# Service module
{
options,
config,
-
pkgs,
lib,
...
}:
let
cfg = config.php-fpm;
-
format = pkgs.formats.iniWithGlobalSection { };
+
format = formats.iniWithGlobalSection { };
configFile = format.generate "php-fpm.conf" {
globalSection = lib.filterAttrs (_: v: !lib.isAttrs v) cfg.settings;
sections = lib.filterAttrs (_: lib.isAttrs) cfg.settings;
···
_class = "service";
options.php-fpm = {
-
package = lib.mkPackageOption pkgs "php" {
-
example = ''
+
package = lib.mkOption {
+
type = lib.types.package;
+
description = "PHP package to use for php-fpm";
+
defaultText = lib.literalMD ''The PHP package that provided this module.'';
+
example = lib.literalExpression ''
php.buildEnv {
extensions =
{ all, ... }:
···
serviceConfig = {
Type = "notify";
-
ExecReload = "${pkgs.coreutils}/bin/kill -USR2 $MAINPID";
+
ExecReload = "${coreutils}/bin/kill -USR2 $MAINPID";
RuntimeDirectory = "php-fpm";
RuntimeDirectoryPreserve = true;
Restart = "always";
···
finit.service = {
conditions = [ "service/syslogd/ready" ];
-
reload = "${pkgs.coreutils}/bin/kill -USR2 $MAINPID";
+
reload = "${coreutils}/bin/kill -USR2 $MAINPID";
notify = "systemd";
};
};