mirror of
https://github.com/nix-community/home-manager.git
synced 2026-06-05 21:02:51 +00:00
modules: add modular services support
Adds `home.services`, an attribute set of nixpkgs [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) sourced from `<nixpkgs/lib/services/lib.nix>`. Each service exposes `process.argv` and the upstream NixOS-style systemd schema (`systemd.lib`, `systemd.mainExecStart`, `systemd.service`, `systemd.services`, `systemd.sockets`) by re-exporting `nixos/modules/system/service/systemd/service.nix`. Service modules shipped with `_class = "service"` (e.g. `pkgs.<name>.passthru.services.default`) drop in unchanged -- service portability across module systems is the point of modular services. Lifted units are evaluated and translated from NixOS-style attrs (`wantedBy`, `serviceConfig`, `unitConfig`, `environment`, ...) into the section-based INI shape (`{ Unit; Service; Install; }`) that home-manager's `systemd.user.{services,sockets}` consumes; only the common keys are mapped, uncommon options remain reachable via `unitConfig` / `serviceConfig` / `socketConfig`. Sub-services and their units are dashed under the parent service name; `process.argv` becomes the default `ExecStart` for the service's primary unit, which defaults to `WantedBy=default.target`. Mirrors the surface of nixpkgs' portable systemd module (services + sockets only); other unit kinds home-manager supports natively (timers etc.) are intentionally not modeled until upstream grows them. Each service's `configData.<name>` entries are materialized at `$XDG_CONFIG_HOME/system-services/<service-prefix>/<name>` (mirroring how `nixos/modules/system/service/systemd/{config-data-path,system}.nix` lifts `configData` to `environment.etc`), with the absolute path injected back into `configData.<name>.path` so the service can refer to its files at a stable location. Includes nmt tests covering: a basic `process.argv`-only service, a service with a `configData` entry, and importing `pkgs.ghostunnel.passthru.services.default` to assert the lifted user unit contains the expected ExecStart flags and `LoadCredential` entries.
This commit is contained in:
committed by
Matthieu Coudron
parent
9c6f1307e1
commit
fb6a0c6d39
@@ -60,6 +60,7 @@ usage/rollbacks.md
|
||||
usage/dotfiles.md
|
||||
usage/graphical.md
|
||||
usage/gpu-non-nixos.md
|
||||
usage/modular-services.md
|
||||
usage/updating.md
|
||||
usage/upgrading.md
|
||||
```
|
||||
|
||||
77
docs/manual/usage/modular-services.md
Normal file
77
docs/manual/usage/modular-services.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Modular Services {#sec-usage-modular-services}
|
||||
|
||||
Home Manager supports nixpkgs
|
||||
[modular services](https://nixos.org/manual/nixos/unstable/#modular-services)
|
||||
under [](#opt-home.services). This is the home-manager analog to the
|
||||
NixOS `system.services` namespace: each entry is an abstract service
|
||||
sourced from `<nixpkgs/lib/services/lib.nix>` with the upstream portable
|
||||
systemd module loaded into it, so service modules shipped with packages
|
||||
(e.g. `pkgs.<name>.passthru.services.default`) drop in unchanged --
|
||||
the same module evaluates on NixOS and on Home Manager.
|
||||
|
||||
A minimal example -- run a one-shot user service from a package's
|
||||
modular service definition:
|
||||
|
||||
```nix
|
||||
{ pkgs, ... }: {
|
||||
home.services.tunnel = {
|
||||
imports = [ pkgs.ghostunnel.passthru.services.default ];
|
||||
ghostunnel = {
|
||||
listen = "127.0.0.1:8443";
|
||||
target = "127.0.0.1:8080";
|
||||
cert = "/run/secrets/cert.pem";
|
||||
key = "/run/secrets/key.pem";
|
||||
allowAll = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This produces `~/.config/systemd/user/tunnel.service` with the expected
|
||||
`ExecStart`, `LoadCredential`, and `WantedBy=default.target`.
|
||||
|
||||
Each service exposes the upstream NixOS-style schema: [`process.argv`],
|
||||
`systemd.lib`, `systemd.mainExecStart`, `systemd.service`,
|
||||
`systemd.services`, `systemd.sockets`. Lifted units are translated from
|
||||
NixOS-style attrs (`wantedBy`, `serviceConfig`, `unitConfig`,
|
||||
`environment`, ...) into the section-based INI shape
|
||||
(`{ Unit; Service; Install; }`) that home-manager's
|
||||
[](#opt-systemd.user.services) consumes. Only common keys are mapped
|
||||
explicitly; uncommon options remain reachable via `unitConfig`,
|
||||
`serviceConfig`, or `socketConfig`.
|
||||
|
||||
Sub-services (nested `services.<sub>` inside another service) and their
|
||||
units are dashed under the parent service name. The empty unit key
|
||||
`""` denotes the service's *primary* unit (lifted to a unit named
|
||||
after the service itself); [`process.argv`] becomes the default
|
||||
`ExecStart` for that unit, which defaults to `WantedBy=default.target`.
|
||||
|
||||
## Configuration data {#sec-usage-modular-services-configdata}
|
||||
|
||||
Each service can declare configuration files via `configData.<name>`.
|
||||
These are materialized at `$XDG_CONFIG_HOME/system-services/<service>/<name>`
|
||||
(mirroring how NixOS lifts `configData` to `environment.etc`), with the
|
||||
absolute path injected back into `configData.<name>.path` so the service
|
||||
can refer to its files at a stable location:
|
||||
|
||||
```nix
|
||||
{ config, ... }:
|
||||
{
|
||||
home.services.demo = {
|
||||
process.argv = [ "/bin/myapp" "--config" config.home.services.demo.configData."app.toml".path ];
|
||||
configData."app.toml".text = ''
|
||||
port = 1234
|
||||
'';
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Scope notes {#sec-usage-modular-services-scope}
|
||||
|
||||
Home Manager mirrors the surface of nixpkgs' portable systemd module:
|
||||
services and sockets only. Other unit kinds Home Manager supports
|
||||
natively under [](#opt-systemd.user.services) (timers, paths, mounts, ...)
|
||||
are intentionally not modeled on `home.services` until upstream grows them,
|
||||
to keep both surfaces aligned.
|
||||
|
||||
[`process.argv`]: https://nixos.org/manual/nixos/unstable/#opt-system.services._name_.process.argv
|
||||
@@ -19,6 +19,13 @@ This release has the following notable changes:
|
||||
range 1.0–2.0, previously it erroneously expected values in the
|
||||
range `0.0–1.0`.
|
||||
|
||||
- New [](#opt-home.services) namespace for nixpkgs
|
||||
[modular services](https://nixos.org/manual/nixos/unstable/#modular-services).
|
||||
Service modules shipped with packages (e.g.
|
||||
`pkgs.<name>.passthru.services.default`) drop in unchanged and are
|
||||
lifted to user systemd units. See
|
||||
[Modular Services](#sec-usage-modular-services) for details.
|
||||
|
||||
## State Version Changes {#sec-release-26.05-state-version-changes}
|
||||
|
||||
The state version in this release includes the changes below. These
|
||||
|
||||
11
modules/misc/news/2026/04/2026-04-29_15-39-41.nix
Normal file
11
modules/misc/news/2026/04/2026-04-29_15-39-41.nix
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
time = "2026-04-29T15:39:41+00:00";
|
||||
condition = true;
|
||||
message = ''
|
||||
A new `home.services` namespace has been added for nixpkgs
|
||||
modular services. Service modules shipped with packages (i.e.
|
||||
`pkgs.<name>.passthru.services.default`) drop in unchanged and are
|
||||
lifted to user systemd units. See the "Modular Services" chapter
|
||||
in the manual for details.
|
||||
'';
|
||||
}
|
||||
@@ -64,6 +64,7 @@ let
|
||||
./misc/xdg-user-dirs.nix
|
||||
./misc/xdg.nix
|
||||
./misc/xfconf.nix
|
||||
./services-modular
|
||||
./systemd.nix
|
||||
./targets/darwin
|
||||
./targets/generic-linux.nix
|
||||
|
||||
42
modules/services-modular/config-data-path.nix
Normal file
42
modules/services-modular/config-data-path.nix
Normal file
@@ -0,0 +1,42 @@
|
||||
# Sets `path` on each modular service's `configData.<name>` so that the
|
||||
# service can refer to its config files at a stable absolute location
|
||||
# inside the user's XDG config directory. Mirrors
|
||||
# `nixos/modules/system/service/systemd/config-data-path.nix`.
|
||||
let
|
||||
setPathsModule =
|
||||
prefix:
|
||||
{
|
||||
lib,
|
||||
name,
|
||||
xdgConfigHome,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib) mkOption types;
|
||||
servicePrefix = "${prefix}${name}";
|
||||
in
|
||||
{
|
||||
_class = "service";
|
||||
options = {
|
||||
configData = mkOption {
|
||||
type = types.lazyAttrsOf (
|
||||
types.submodule (
|
||||
{ config, ... }:
|
||||
{
|
||||
config.path = lib.mkDefault "${xdgConfigHome}/system-services/${servicePrefix}/${config.name}";
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
services = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submoduleWith {
|
||||
modules = [ (setPathsModule "${servicePrefix}-") ];
|
||||
specialArgs = { inherit xdgConfigHome; };
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
setPathsModule ""
|
||||
187
modules/services-modular/default.nix
Normal file
187
modules/services-modular/default.nix
Normal file
@@ -0,0 +1,187 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib) concatMapAttrs mkOption types;
|
||||
|
||||
portable-lib = import (pkgs.path + "/lib/services/lib.nix") { inherit lib; };
|
||||
|
||||
# Combine a service-name prefix with a unit name. The empty unit name
|
||||
# `""` is the convention from nixpkgs' portable services for the
|
||||
# service's *primary* unit (e.g. `services.foo.systemd.user.services.""`
|
||||
# is the unit named `foo.service`). Sub-units use a hyphenated suffix.
|
||||
dashed =
|
||||
before: after:
|
||||
if after == "" then
|
||||
before
|
||||
else if before == "" then
|
||||
after
|
||||
else
|
||||
"${before}-${after}";
|
||||
|
||||
# Translate a NixOS-style systemd unit attrset (wantedBy, serviceConfig,
|
||||
# unitConfig, environment, ...) into the section-based INI shape that
|
||||
# home-manager's `systemd.user.<unitType>` expects (Unit/Service/Install).
|
||||
# Only the common keys are mapped; uncommon options can still be set
|
||||
# explicitly via `unitConfig` / `serviceConfig` / `socketConfig`.
|
||||
unitAttrKeys = [
|
||||
"description"
|
||||
"documentation"
|
||||
"requires"
|
||||
"wants"
|
||||
"upholds"
|
||||
"after"
|
||||
"before"
|
||||
"bindsTo"
|
||||
"partOf"
|
||||
"conflicts"
|
||||
"requisite"
|
||||
"onFailure"
|
||||
"onSuccess"
|
||||
];
|
||||
pickSection =
|
||||
keys: src:
|
||||
lib.listToAttrs (
|
||||
lib.concatMap (
|
||||
k:
|
||||
lib.optional (src ? ${k} && src.${k} != null && src.${k} != [ ]) {
|
||||
name = lib.toSentenceCase k;
|
||||
value = src.${k};
|
||||
}
|
||||
) keys
|
||||
);
|
||||
envToList =
|
||||
env: lib.mapAttrsToList (k: v: "${k}=${toString v}") (lib.filterAttrs (_: v: v != null) env);
|
||||
installSection =
|
||||
u:
|
||||
lib.filterAttrs (_: v: v != [ ]) {
|
||||
WantedBy = u.wantedBy or [ ];
|
||||
RequiredBy = u.requiredBy or [ ];
|
||||
};
|
||||
toHmIni = unit: {
|
||||
Unit = pickSection unitAttrKeys unit // (unit.unitConfig or { });
|
||||
Service =
|
||||
(unit.serviceConfig or { })
|
||||
// lib.optionalAttrs (unit ? environment && unit.environment != { }) {
|
||||
Environment = envToList unit.environment;
|
||||
};
|
||||
Install = installSection unit;
|
||||
};
|
||||
toHmIniSocket = sock: {
|
||||
Unit = pickSection unitAttrKeys sock // (sock.unitConfig or { });
|
||||
Socket =
|
||||
(sock.socketConfig or { })
|
||||
// lib.optionalAttrs (sock ? listenStreams && sock.listenStreams != [ ]) {
|
||||
ListenStream = sock.listenStreams;
|
||||
}
|
||||
// lib.optionalAttrs (sock ? listenDatagrams && sock.listenDatagrams != [ ]) {
|
||||
ListenDatagram = sock.listenDatagrams;
|
||||
};
|
||||
Install = installSection sock;
|
||||
};
|
||||
|
||||
# Evaluate a deferredModule into attrs, then translate.
|
||||
evalDeferred =
|
||||
translator: unitModule:
|
||||
translator
|
||||
(lib.evalModules {
|
||||
modules = [
|
||||
(_: {
|
||||
# Loose schema: NixOS-style unit attrs include nested sub-attrsets
|
||||
# (e.g. `serviceConfig`) that need to merge across definitions.
|
||||
freeformType =
|
||||
with types;
|
||||
attrsOf (oneOf [
|
||||
(attrsOf raw)
|
||||
(listOf raw)
|
||||
raw
|
||||
]);
|
||||
})
|
||||
unitModule
|
||||
];
|
||||
}).config;
|
||||
|
||||
makeUnits =
|
||||
translator: unitType: prefix: service:
|
||||
concatMapAttrs (unitName: unitModule: {
|
||||
"${dashed prefix unitName}" = evalDeferred translator unitModule;
|
||||
}) service.systemd.${unitType}
|
||||
// concatMapAttrs (
|
||||
subName: subService: makeUnits translator unitType (dashed prefix subName) subService
|
||||
) service.services;
|
||||
|
||||
# Lift each service's `configData` entries into `xdg.configFile` paths.
|
||||
# Mirrors how `nixos/modules/system/service/systemd/system.nix` lifts
|
||||
# `configData` to `environment.etc`.
|
||||
makeConfigFiles =
|
||||
prefix: service:
|
||||
lib.mapAttrs' (_: cfg: {
|
||||
name = "system-services/${prefix}/${cfg.name}";
|
||||
value = lib.filterAttrs (_: v: v != null) {
|
||||
source = cfg.source or null;
|
||||
text = cfg.text or null;
|
||||
inherit (cfg) enable;
|
||||
};
|
||||
}) (lib.filterAttrs (_: cfg: cfg.enable) (service.configData or { }))
|
||||
// concatMapAttrs (
|
||||
subName: subService: makeConfigFiles (dashed prefix subName) subService
|
||||
) service.services;
|
||||
|
||||
modularServiceConfiguration = portable-lib.configure {
|
||||
serviceManagerPkgs = pkgs;
|
||||
extraRootModules = [
|
||||
./service.nix
|
||||
./config-data-path.nix
|
||||
];
|
||||
extraRootSpecialArgs = {
|
||||
systemdPackage = pkgs.systemd;
|
||||
nixpkgsPath = pkgs.path;
|
||||
xdgConfigHome = config.xdg.configHome;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.kiara ];
|
||||
|
||||
options.home.services = mkOption {
|
||||
description = ''
|
||||
Home-Manager [modular services](https://nixos.org/manual/nixos/unstable/#modular-services).
|
||||
|
||||
Each entry is an abstract service that may declare a {option}`process.argv`
|
||||
and home-manager-style {option}`systemd.user.{services,sockets}` units
|
||||
(INI section shape). Units are emitted under home-manager's
|
||||
{option}`systemd.user.services` (and friends) with the service name
|
||||
as a prefix. Mirrors {option}`system.services` in NixOS.
|
||||
'';
|
||||
type = types.attrsOf modularServiceConfiguration.serviceSubmodule;
|
||||
default = { };
|
||||
visible = "shallow";
|
||||
};
|
||||
|
||||
config = {
|
||||
assertions = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
name: cfg: portable-lib.getAssertions [ "home" "services" name ] cfg
|
||||
) config.home.services
|
||||
);
|
||||
|
||||
warnings = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
name: cfg: portable-lib.getWarnings [ "home" "services" name ] cfg
|
||||
) config.home.services
|
||||
);
|
||||
|
||||
systemd.user.services = concatMapAttrs (
|
||||
name: svc: makeUnits toHmIni "services" name svc
|
||||
) config.home.services;
|
||||
|
||||
systemd.user.sockets = concatMapAttrs (
|
||||
name: svc: makeUnits toHmIniSocket "sockets" name svc
|
||||
) config.home.services;
|
||||
|
||||
xdg.configFile = concatMapAttrs (name: svc: makeConfigFiles name svc) config.home.services;
|
||||
};
|
||||
}
|
||||
22
modules/services-modular/service.nix
Normal file
22
modules/services-modular/service.nix
Normal file
@@ -0,0 +1,22 @@
|
||||
# Per-service extension module loaded into every `home.services` entry.
|
||||
#
|
||||
# Re-exports the nixpkgs portable systemd service module so HM modular
|
||||
# services accept the same NixOS-style schema (`systemd.lib`,
|
||||
# `systemd.mainExecStart`, `systemd.service`, `systemd.services`,
|
||||
# `systemd.sockets`) as their NixOS counterparts. This is what lets
|
||||
# upstream service modules (e.g. `pkgs.<name>.passthru.services.default`)
|
||||
# drop in unchanged. Then overrides the primary unit's `wantedBy` default
|
||||
# to `default.target`, since user units typically attach to that instead
|
||||
# of `multi-user.target`.
|
||||
{ lib, nixpkgsPath, ... }:
|
||||
{
|
||||
imports = [
|
||||
(nixpkgsPath + "/nixos/modules/system/service/systemd/service.nix")
|
||||
];
|
||||
|
||||
# The empty key `""` is the modular service's *primary* unit (see
|
||||
# `dashed` in `default.nix`).
|
||||
config.systemd.services."" = {
|
||||
wantedBy = lib.mkOverride 950 [ "default.target" ];
|
||||
};
|
||||
}
|
||||
@@ -212,6 +212,7 @@ import nmtSrc {
|
||||
./modules/misc/qt
|
||||
./modules/misc/xdg/linux.nix
|
||||
./modules/misc/xsession
|
||||
./modules/services-modular
|
||||
./modules/systemd
|
||||
./modules/targets-linux
|
||||
# keep-sorted end
|
||||
|
||||
15
tests/modules/services-modular/basic.nix
Normal file
15
tests/modules/services-modular/basic.nix
Normal file
@@ -0,0 +1,15 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
home.services.demo = {
|
||||
process.argv = [
|
||||
"${pkgs.coreutils}/bin/echo"
|
||||
"hello"
|
||||
];
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.config/systemd/user/demo.service
|
||||
assertFileContains home-files/.config/systemd/user/demo.service '/bin/echo'
|
||||
assertFileContains home-files/.config/systemd/user/demo.service 'WantedBy=default.target'
|
||||
'';
|
||||
}
|
||||
15
tests/modules/services-modular/configdata.nix
Normal file
15
tests/modules/services-modular/configdata.nix
Normal file
@@ -0,0 +1,15 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
home.services.demo = {
|
||||
process.argv = [ "${pkgs.coreutils}/bin/true" ];
|
||||
configData."config.toml".text = ''
|
||||
[server]
|
||||
port = 1234
|
||||
'';
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.config/system-services/demo/config.toml
|
||||
assertFileContains home-files/.config/system-services/demo/config.toml 'port = 1234'
|
||||
'';
|
||||
}
|
||||
5
tests/modules/services-modular/default.nix
Normal file
5
tests/modules/services-modular/default.nix
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
home-services-basic = ./basic.nix;
|
||||
home-services-configdata = ./configdata.nix;
|
||||
home-services-ghostunnel = ./ghostunnel.nix;
|
||||
}
|
||||
20
tests/modules/services-modular/ghostunnel.nix
Normal file
20
tests/modules/services-modular/ghostunnel.nix
Normal file
@@ -0,0 +1,20 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
home.services.tunnel = {
|
||||
imports = [ pkgs.ghostunnel.passthru.services.default ];
|
||||
ghostunnel = {
|
||||
listen = "127.0.0.1:8443";
|
||||
target = "127.0.0.1:8080";
|
||||
cert = "/run/secrets/cert.pem";
|
||||
key = "/run/secrets/key.pem";
|
||||
allowAll = true;
|
||||
};
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.config/systemd/user/tunnel.service
|
||||
assertFileContains home-files/.config/systemd/user/tunnel.service '/bin/ghostunnel'
|
||||
assertFileContains home-files/.config/systemd/user/tunnel.service 'allow-all'
|
||||
assertFileContains home-files/.config/systemd/user/tunnel.service 'LoadCredential=cert:/run/secrets/cert.pem'
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user