Skip to content

Commit

Permalink
installer: only uninstall packages managed by this module (#23)
Browse files Browse the repository at this point in the history
Allow managed and unmanaged packages to coexist.

This PR introduces a state file that keeps track of the module's modifications, so they can be managed separately from imperatively introduced modifications.

A new uninstallUnmanagedPackages option has been added. When set to true (default), unmanaged flatpaks won't be uninstalled on activation. When set to false, all flatpacks will be overridden on module activation.
  • Loading branch information
Tomaszal authored Jan 20, 2024
1 parent f43640e commit 09d2dd7
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 12 deletions.
2 changes: 1 addition & 1 deletion modules/home-manager.nix
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ in
};

home.activation = {
start-service = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
flatpak-managed-install = lib.hm.dag.entryAfter [ "reloadSystemd" ] ''
export PATH=${lib.makeBinPath (with pkgs; [ systemd ])}:$PATH
$DRY_RUN_CMD systemctl is-system-running -q && \
Expand Down
58 changes: 47 additions & 11 deletions modules/installer.nix
Original file line number Diff line number Diff line change
@@ -1,20 +1,42 @@
{ cfg, pkgs, lib, installation ? "system", ... }:

let
# Put the state file in the `gcroots` folder of the respective installation,
# which prevents it from being garbage collected. This could probably be
# improved in the future if there are better conventions for how this should
# be handled. Right now it introduces a small issue of the state file derivation
# not being garbage collected even when this module is removed. You can find
# more details on this design drawback in PR#23
gcroots =
if (installation == "system")
then "/nix/var/nix/gcroots/"
else "\${XDG_STATE_HOME:-$HOME/.local/state}/home-manager/gcroots";
stateFile = pkgs.writeText "flatpak-state.json" (builtins.toJSON {
packages = map (builtins.getAttr "appId") cfg.packages;
});
statePath = "${gcroots}/${stateFile.name}";

updateApplications = cfg.update.onActivation || cfg.update.auto.enable;
applicationsToKeep = lib.strings.concatStringsSep " " (map (builtins.getAttr "appId") cfg.packages);
flatpakUninstallCmd = installation: {}: ''
APPS_TO_KEEP=("${applicationsToKeep}")
# Get a list of currently installed Flatpak application IDs
INSTALLED_APPS=$(${pkgs.flatpak}/bin/flatpak --${installation} list --app --columns=application | ${pkgs.gawk}/bin/awk '{print ''$1}')

# Iterate through the installed apps and uninstall those not present in the to keep list
for APP_ID in $INSTALLED_APPS; do
if [[ ! " ''${APPS_TO_KEEP[@]} " =~ " ''${APP_ID} " ]]; then
${pkgs.flatpak}/bin/flatpak uninstall --${installation} -y ''$APP_ID
fi
done
handleUnmanagedPackagesCmd = installation: uninstallUnmanagedPackages:
lib.optionalString uninstallUnmanagedPackages ''
# Add all installed Flatpak packages to the old state, so only the managed ones (new state) will be kept
INSTALLED_PACKAGES=$(${pkgs.flatpak}/bin/flatpak --${installation} list --app --columns=application)
OLD_STATE=$(${pkgs.jq}/bin/jq -r -n \
--argjson old "$OLD_STATE" \
--arg installed_packages "$INSTALLED_PACKAGES" \
'$old + { "packages" : $installed_packages | split("\n") }')
'';

flatpakUninstallCmd = installation: {}: ''
# Uninstall all packages that are present in the old state but not the new one
${pkgs.jq}/bin/jq -r -n \
--argjson old "$OLD_STATE" \
--argjson new "$NEW_STATE" \
'($old.packages - $new.packages)[]' \
| while read -r APP_ID; do
${pkgs.flatpak}/bin/flatpak uninstall --${installation} -y $APP_ID
done
'';

flatpakInstallCmd = installation: update: { appId, origin ? "flathub", commit ? null, ... }: ''
Expand All @@ -40,6 +62,17 @@ pkgs.writeShellScript "flatpak-managed-install" ''
# This script is triggered at build time by a transient systemd unit.
set -eu
# Setup state variables
NEW_STATE=$(cat ${stateFile})
if [[ -f ${statePath} ]]; then
OLD_STATE=$(cat ${statePath})
else
OLD_STATE={}
fi
# Handle unmanaged packages
${handleUnmanagedPackagesCmd installation cfg.uninstallUnmanagedPackages}
# Configure remotes
${mkFlatpakAddRemoteCmd installation cfg.remotes}
Expand All @@ -49,4 +82,7 @@ pkgs.writeShellScript "flatpak-managed-install" ''
# Install packages
${mkFlatpakInstallCmd installation updateApplications cfg.packages}
# Save state
ln -sf ${stateFile} ${statePath}
''
10 changes: 10 additions & 0 deletions modules/options.nix
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,14 @@ in
};
'';
};

uninstallUnmanagedPackages = mkOption {
type = with types; bool;
default = false;
description = lib.mdDoc ''
If enabled, uninstall packages not managed by this module on activation.
I.e. if packages were installed via Flatpak directly instead of this module,
they would get uninstalled on the next activation
'';
};
}

0 comments on commit 09d2dd7

Please sign in to comment.