launchd: add agent domain option
Allow Darwin LaunchAgents to target either the GUI or user launchd domain while preserving gui as the compatibility default.
This commit is contained in:
@@ -14,6 +14,12 @@ This release has the following notable changes:
|
||||
and `programs.uv.python.prune` / `programs.uv.tool.prune` make the managed set
|
||||
fully declarative by removing versions and tools that are no longer listed.
|
||||
|
||||
- On Darwin, Home Manager launchd agents now support
|
||||
[](#opt-launchd.agents._name_.domain) to choose either the user's GUI or
|
||||
background launchd domain. Background services provided by Home Manager now
|
||||
use the user domain, which allows activations from contexts such as SSH
|
||||
sessions without requiring an active graphical login session.
|
||||
|
||||
## State Version Changes {#sec-release-26.11-state-version-changes}
|
||||
|
||||
The state version in this release includes the changes below. These
|
||||
|
||||
+104
-11
@@ -18,6 +18,22 @@ let
|
||||
{
|
||||
options = {
|
||||
enable = lib.mkEnableOption name;
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"gui"
|
||||
"user"
|
||||
];
|
||||
default = "gui";
|
||||
example = "user";
|
||||
description = ''
|
||||
The launchd domain to bootstrap this agent into.
|
||||
|
||||
The `gui` domain is appropriate for agents that need the user's
|
||||
Aqua session, such as window managers, hotkey daemons, and other
|
||||
graphical tools. The `user` domain is appropriate for background
|
||||
services that do not require a graphical login session.
|
||||
'';
|
||||
};
|
||||
config = lib.mkOption {
|
||||
type = lib.types.submodule (import ./launchd.nix);
|
||||
default = { };
|
||||
@@ -74,6 +90,13 @@ let
|
||||
_n: v: lib.nameValuePair "${v.config.Label}.plist" (toAgent v.config)
|
||||
) (lib.filterAttrs (_n: v: v.enable) cfg.agents);
|
||||
|
||||
agentDomains = lib.mapAttrs' (
|
||||
_n: v:
|
||||
lib.nameValuePair "${v.config.Label}.domain" (
|
||||
pkgs.writeText "${v.config.Label}.domain" "${v.domain}\n"
|
||||
)
|
||||
) (lib.filterAttrs (_n: v: v.enable) cfg.agents);
|
||||
|
||||
agentsDrv = pkgs.runCommand "home-manager-agents" { } ''
|
||||
mkdir -p "$out"
|
||||
|
||||
@@ -87,6 +110,20 @@ let
|
||||
ln -s "$src" "$out/$dest"
|
||||
done
|
||||
'';
|
||||
|
||||
agentDomainsDrv = pkgs.runCommand "home-manager-agent-domains" { } ''
|
||||
mkdir -p "$out"
|
||||
|
||||
declare -A domains
|
||||
domains=(${
|
||||
lib.concatStringsSep " " (lib.mapAttrsToList (name: value: "['${name}']='${value}'") agentDomains)
|
||||
})
|
||||
|
||||
for dest in "''${!domains[@]}"; do
|
||||
src="''${domains[$dest]}"
|
||||
ln -s "$src" "$out/$dest"
|
||||
done
|
||||
'';
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
@@ -129,6 +166,7 @@ in
|
||||
(lib.mkIf isDarwin {
|
||||
home.extraBuilderCommands = ''
|
||||
ln -s "${agentsDrv}" $out/LaunchAgents
|
||||
ln -s "${agentDomainsDrv}" $out/LaunchAgentDomains
|
||||
'';
|
||||
|
||||
# NOTE: Launch Agent configurations can't be symlinked from the Nix store
|
||||
@@ -139,6 +177,40 @@ in
|
||||
# Disable errexit to ensure we process all agents even if some fail
|
||||
set +e
|
||||
|
||||
readAgentDomain() {
|
||||
local domainsDir="$1"
|
||||
local agentName="$2"
|
||||
local domainFile="$domainsDir/$agentName.domain"
|
||||
|
||||
if [[ -n "$domainsDir" && -f "$domainFile" ]]; then
|
||||
local domainName
|
||||
domainName="$(<"$domainFile")"
|
||||
case "$domainName" in
|
||||
gui|user)
|
||||
printf '%s\n' "$domainName"
|
||||
;;
|
||||
*)
|
||||
printf 'gui\n'
|
||||
;;
|
||||
esac
|
||||
else
|
||||
printf 'gui\n'
|
||||
fi
|
||||
}
|
||||
|
||||
resolveDomain() {
|
||||
local domainName="$1"
|
||||
|
||||
case "$domainName" in
|
||||
gui)
|
||||
printf 'gui/%s\n' "$UID"
|
||||
;;
|
||||
user)
|
||||
printf 'user/%s\n' "$UID"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Stop an agent if it's running
|
||||
bootoutAgent() {
|
||||
local domain="$1"
|
||||
@@ -190,26 +262,36 @@ in
|
||||
processAgent() {
|
||||
local srcPath="$1"
|
||||
local dstDir="$2"
|
||||
local domain="$3"
|
||||
local oldDomainsDir="$3"
|
||||
local newDomainsDir="$4"
|
||||
|
||||
local agentFile="''${srcPath##*/}"
|
||||
local agentName="''${agentFile%.plist}"
|
||||
local dstPath="$dstDir/$agentFile"
|
||||
local oldDomainName
|
||||
local newDomainName
|
||||
local oldDomain
|
||||
local newDomain
|
||||
|
||||
oldDomainName="$(readAgentDomain "$oldDomainsDir" "$agentName")"
|
||||
newDomainName="$(readAgentDomain "$newDomainsDir" "$agentName")"
|
||||
oldDomain="$(resolveDomain "$oldDomainName")"
|
||||
newDomain="$(resolveDomain "$newDomainName")"
|
||||
|
||||
# Skip if unchanged
|
||||
if cmp -s "$srcPath" "$dstPath"; then
|
||||
verboseEcho "Agent '$agentName' is already up-to-date"
|
||||
if cmp -s "$srcPath" "$dstPath" && [[ "$oldDomainName" == "$newDomainName" ]]; then
|
||||
verboseEcho "Agent '$newDomain/$agentName' is already up-to-date"
|
||||
return 0
|
||||
fi
|
||||
|
||||
verboseEcho "Processing agent '$agentName'"
|
||||
verboseEcho "Processing agent '$newDomain/$agentName'"
|
||||
|
||||
# Stop/Unload agent if it's already running
|
||||
if [[ -f "$dstPath" ]]; then
|
||||
bootoutAgent "$domain" "$agentName"
|
||||
bootoutAgent "$oldDomain" "$agentName"
|
||||
fi
|
||||
|
||||
installAndBootstrapAgent "$srcPath" "$dstPath" "$domain" "$agentName"
|
||||
installAndBootstrapAgent "$srcPath" "$dstPath" "$newDomain" "$agentName"
|
||||
# Note: We continue processing even if this agent fails
|
||||
return 0
|
||||
}
|
||||
@@ -218,11 +300,16 @@ in
|
||||
local srcPath="$1"
|
||||
local dstDir="$2"
|
||||
local newDir="$3"
|
||||
local domain="$4"
|
||||
local oldDomainsDir="$4"
|
||||
|
||||
local agentFile="''${srcPath##*/}"
|
||||
local agentName="''${agentFile%.plist}"
|
||||
local dstPath="$dstDir/$agentFile"
|
||||
local domainName
|
||||
local domain
|
||||
|
||||
domainName="$(readAgentDomain "$oldDomainsDir" "$agentName")"
|
||||
domain="$(resolveDomain "$domainName")"
|
||||
|
||||
if [[ -e "$newDir/$agentFile" ]]; then
|
||||
verboseEcho "Agent '$agentName' still exists in new generation, skipping cleanup"
|
||||
@@ -253,11 +340,11 @@ in
|
||||
}
|
||||
|
||||
setupLaunchAgents() {
|
||||
local oldDir newDir dstDir domain
|
||||
local oldDir newDir oldDomainsDir newDomainsDir dstDir
|
||||
|
||||
newDir="$(readlink -m "$newGenPath/LaunchAgents")"
|
||||
newDomainsDir="$(readlink -m "$newGenPath/LaunchAgentDomains")"
|
||||
dstDir=${lib.escapeShellArg dstDir}
|
||||
domain="gui/$UID"
|
||||
|
||||
if [[ -n "''${oldGenPath:-}" ]]; then
|
||||
oldDir="$(readlink -m "$oldGenPath/LaunchAgents")"
|
||||
@@ -265,8 +352,14 @@ in
|
||||
verboseEcho "No previous LaunchAgents directory found"
|
||||
oldDir=""
|
||||
fi
|
||||
|
||||
oldDomainsDir="$(readlink -m "$oldGenPath/LaunchAgentDomains")"
|
||||
if [[ ! -d "$oldDomainsDir" ]]; then
|
||||
oldDomainsDir=""
|
||||
fi
|
||||
else
|
||||
oldDir=""
|
||||
oldDomainsDir=""
|
||||
fi
|
||||
|
||||
verboseEcho "Setting up LaunchAgents in $dstDir"
|
||||
@@ -274,7 +367,7 @@ in
|
||||
|
||||
verboseEcho "Processing new/updated LaunchAgents..."
|
||||
find -L "$newDir" -maxdepth 1 -name '*.plist' -type f | while read -r srcPath; do
|
||||
processAgent "$srcPath" "$dstDir" "$domain"
|
||||
processAgent "$srcPath" "$dstDir" "$oldDomainsDir" "$newDomainsDir"
|
||||
done
|
||||
|
||||
# Skip cleanup if there's no previous generation
|
||||
@@ -285,7 +378,7 @@ in
|
||||
|
||||
verboseEcho "Cleaning up removed LaunchAgents..."
|
||||
find -L "$oldDir" -maxdepth 1 -name '*.plist' -type f | while read -r srcPath; do
|
||||
removeAgent "$srcPath" "$dstDir" "$newDir" "$domain"
|
||||
removeAgent "$srcPath" "$dstDir" "$newDir" "$oldDomainsDir"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
time = "2026-06-18T02:02:00+00:00";
|
||||
condition = pkgs.stdenv.hostPlatform.isDarwin && config.launchd.enable;
|
||||
message = ''
|
||||
|
||||
Home Manager launchd agents now support the
|
||||
`launchd.agents.<name>.domain` option. Background services provided by Home
|
||||
Manager use the user launchd domain by default, so they can be managed from
|
||||
SSH and other non-graphical sessions. Set
|
||||
`launchd.agents.<name>.domain = "gui"` for agents that need the graphical
|
||||
session.
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
config = {
|
||||
launchd.agents."user-service" = {
|
||||
enable = true;
|
||||
domain = "user";
|
||||
config.ProgramArguments = [
|
||||
"/some/command"
|
||||
];
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
serviceFile=LaunchAgents/org.nix-community.home.user-service.plist
|
||||
assertFileExists $serviceFile
|
||||
|
||||
domainFile=LaunchAgentDomains/org.nix-community.home.user-service.domain
|
||||
assertFileExists $domainFile
|
||||
assertFileContent $domainFile ${builtins.toFile "expected-domain" "user\n"}
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -22,6 +22,16 @@
|
||||
serviceFile=LaunchAgents/org.nix-community.home.test-service.plist
|
||||
assertFileExists $serviceFile
|
||||
assertFileContent $serviceFile ${./expected-agent.plist}
|
||||
|
||||
domainFile=LaunchAgentDomains/org.nix-community.home.test-service.domain
|
||||
assertFileExists $domainFile
|
||||
assertFileContent $domainFile ${builtins.toFile "expected-domain" "gui\n"}
|
||||
|
||||
assertFileExists activate
|
||||
assertFileContains activate 'readAgentDomain'
|
||||
assertFileContains activate 'resolveDomain'
|
||||
assertFileContains activate "printf 'gui/%s"
|
||||
assertFileContains activate "printf 'user/%s"
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
{ launchd-agents = ./agents.nix; }
|
||||
{
|
||||
launchd-agent-domain = ./agent-domain.nix;
|
||||
launchd-agents = ./agents.nix;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user