Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
/modules/i18n/input-method @Kranzes
/tests/modules/i18n/input-method @Kranzes

/modules/launchd @midchildan

/modules/misc/dconf.nix @rycee

/modules/misc/fontconfig.nix @rycee
Expand Down
3 changes: 3 additions & 0 deletions docs/release-notes/rl-2205.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ such as the `home-manager` command line tool and the activation script.
If you would like to contribute to the translation effort
then you can do so through the {hm-weblate}[Home Manager Weblate project].

* A new module, `launchd.agents` was added.
Use this to enable services based on macOS LaunchAgents.

[[sec-release-22.05-state-version-changes]]
=== State Version Changes

Expand Down
1 change: 1 addition & 0 deletions format
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ find . -name '*.nix' \
! -path ./modules/default.nix \
! -path ./modules/files.nix \
! -path ./modules/home-environment.nix \
! -path ./modules/launchd/launchd.nix \
! -path ./modules/lib/default.nix \
! -path ./modules/lib/file-type.nix \
! -path ./modules/manual.nix \
Expand Down
211 changes: 211 additions & 0 deletions modules/launchd/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
{ config, lib, pkgs, ... }:

with lib;

let
inherit (pkgs.stdenv.hostPlatform) isDarwin;
inherit (lib.generators) toPlist;

cfg = config.launchd;
labelPrefix = "org.nix-community.home.";
dstDir = "${config.home.homeDirectory}/Library/LaunchAgents";

launchdConfig = { config, name, ... }: {
options = {
enable = mkEnableOption name;
config = mkOption {
type = types.submodule (import ./launchd.nix);
default = { };
example = literalExpression ''
{
ProgramArguments = [ "/usr/bin/say" "Good afternoon" ];
StartCalendarInterval = {
Hour = 12;
Minute = 0;
};
}
'';
description = ''
Define a launchd job. See <citerefentry>
<refentrytitle>launchd.plist</refentrytitle><manvolnum>5</manvolnum>
</citerefentry> for details.
'';
};
};

config = { config.Label = mkDefault "${labelPrefix}${name}"; };
};

toAgent = config: pkgs.writeText "${config.Label}.plist" (toPlist { } config);

agentPlists =
mapAttrs' (n: v: nameValuePair "${v.config.Label}.plist" (toAgent v.config))
(filterAttrs (n: v: v.enable) cfg.agents);

agentsDrv = pkgs.runCommand "home-manager-agents" {
srcs = attrValues agentPlists;
dsts = attrNames agentPlists;
} ''
mkdir -p "$out"

if [[ -n "$srcs" ]]; then
for (( i=0; i < "''${#srcs[@]}"; i+=1 )); do
src="''${srcs[i]}"
dst="''${dsts[i]}"
ln -s "$src" "$out/$dst"
done
fi
'';
in {
meta.maintainers = with maintainers; [ midchildan ];

options.launchd = {
enable = mkOption {
type = types.bool;
default = isDarwin;
Comment thread
rycee marked this conversation as resolved.
defaultText = literalExpression "pkgs.stdenv.hostPlatform.isDarwin";
description = ''
Whether to enable Home Manager to define per-user daemons by making use
of launchd's LaunchAgents.
'';
};

agents = mkOption {
type = with types; attrsOf (submodule launchdConfig);
default = { };
description = "Define LaunchAgents.";
};
};

config = mkMerge [
{
assertions = [{
assertion = (cfg.enable && agentPlists != { }) -> isDarwin;
message = let names = lib.concatStringsSep ", " (attrNames agentPlists);
in "Must use Darwin for modules that require Launchd: " + names;
}];
}

(mkIf isDarwin {
home.extraBuilderCommands = ''
ln -s "${agentsDrv}" $out/LaunchAgents
'';

home.activation.checkLaunchAgents =
hm.dag.entryBefore [ "writeBoundary" ] ''
checkLaunchAgents() {
local oldDir newDir dstDir err
oldDir=""
if [[ -n "''${oldGenPath:-}" ]]; then
oldDir="$(readlink -m "$oldGenPath/LaunchAgents")" || err=$?
if (( err )); then
oldDir=""
fi
fi
newDir=${escapeShellArg agentsDrv}
dstDir=${escapeShellArg dstDir}

local oldSrcPath newSrcPath dstPath agentFile agentName

find -L "$newDir" -maxdepth 1 -name '*.plist' -type f -print0 \
| while IFS= read -rd "" newSrcPath; do
agentFile="''${newSrcPath##*/}"
agentName="''${agentFile%.plist}"
dstPath="$dstDir/$agentFile"
oldSrcPath="$oldDir/$agentFile"

if [[ ! -e "$dstPath" ]]; then
continue
fi

if ! cmp --quiet "$oldSrcPath" "$dstPath"; then
errorEcho "Existing file '$dstPath' is in the way of '$newSrcPath'"
exit 1
fi
done
}

checkLaunchAgents
'';

# NOTE: Launch Agent configurations can't be symlinked from the Nix store
# because it needs to be owned by the user running it.
Comment on lines +131 to +132
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LaunchAgent configuration files are copied into the home directory because Launchd refuses to work with files not owned by the user running it. For safety reasons, the activation script would check if any changes are made to configurations files previously deployed by Home Manager.

home.activation.setupLaunchAgents =
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The activation script takes care of starting/restarting/stopping the agents automatically.

hm.dag.entryAfter [ "writeBoundary" ] ''
setupLaunchAgents() {
local oldDir newDir dstDir domain err
oldDir=""
if [[ -n "''${oldGenPath:-}" ]]; then
oldDir="$(readlink -m "$oldGenPath/LaunchAgents")" || err=$?
if (( err )); then
oldDir=""
fi
fi
newDir="$(readlink -m "$newGenPath/LaunchAgents")"
dstDir=${escapeShellArg dstDir}
domain="gui/$UID"
err=0

local srcPath dstPath agentFile agentName i bootout_retries
bootout_retries=10

find -L "$newDir" -maxdepth 1 -name '*.plist' -type f -print0 \
| while IFS= read -rd "" srcPath; do
agentFile="''${srcPath##*/}"
agentName="''${agentFile%.plist}"
dstPath="$dstDir/$agentFile"

if cmp --quiet "$srcPath" "$dstPath"; then
continue
fi
if [[ -f "$dstPath" ]]; then
for (( i = 0; i < bootout_retries; i++ )); do
$DRY_RUN_CMD launchctl bootout "$domain/$agentName" || err=$?
if [[ -v DRY_RUN ]]; then
break
fi
if (( err != 9216 )) &&
! launchctl print "$domain/$agentName" &> /dev/null; then
break
fi
sleep 1
done
if (( i == bootout_retries )); then
warnEcho "Failed to stop '$domain/$agentName'"
return 1
fi
fi
$DRY_RUN_CMD install -Dm444 -T "$srcPath" "$dstPath"
$DRY_RUN_CMD launchctl bootstrap "$domain" "$dstPath"
done

if [[ ! -e "$oldDir" ]]; then
return
fi

find -L "$oldDir" -maxdepth 1 -name '*.plist' -type f -print0 \
| while IFS= read -rd "" srcPath; do
agentFile="''${srcPath##*/}"
agentName="''${agentFile%.plist}"
dstPath="$dstDir/$agentFile"
if [[ -e "$newDir/$agentFile" ]]; then
continue
fi

$DRY_RUN_CMD launchctl bootout "$domain/$agentName" || :
if [[ ! -e "$dstPath" ]]; then
continue
fi
if ! cmp --quiet "$srcPath" "$dstPath"; then
warnEcho "Skipping deletion of '$dstPath', since its contents have diverged"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I like the "diverged" wording 👍

continue
fi
$DRY_RUN_CMD rm -f $VERBOSE_ARG "$dstPath"
done
}

setupLaunchAgents
'';
})
];
}
Loading