From c90b96b719f3d386f6e66eff3f8a3f77bec1b30a Mon Sep 17 00:00:00 2001 From: Tomas Zaluckij Date: Mon, 11 Dec 2023 06:22:21 +0000 Subject: [PATCH 1/2] installer: only uninstall packages managed by this module --- modules/home-manager.nix | 2 +- modules/installer.nix | 58 ++++++++++++++++++++++++++++++++-------- modules/options.nix | 10 +++++++ 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/modules/home-manager.nix b/modules/home-manager.nix index 2971534..c1b2d7f 100644 --- a/modules/home-manager.nix +++ b/modules/home-manager.nix @@ -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 && \ diff --git a/modules/installer.nix b/modules/installer.nix index 26eada7..bc3d583 100644 --- a/modules/installer.nix +++ b/modules/installer.nix @@ -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, ... }: '' @@ -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} @@ -49,4 +82,7 @@ pkgs.writeShellScript "flatpak-managed-install" '' # Install packages ${mkFlatpakInstallCmd installation updateApplications cfg.packages} + + # Save state + ln -sf ${stateFile} ${statePath} '' diff --git a/modules/options.nix b/modules/options.nix index 4260d72..e061b99 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -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 + ''; + }; } From bb65a8c11639668e8434d442d6ea03a7b261b75e Mon Sep 17 00:00:00 2001 From: Tomas Zaluckij Date: Tue, 12 Dec 2023 08:49:09 +0000 Subject: [PATCH 2/2] modules: add overrides management --- modules/installer.nix | 38 ++++++++++++++++++++++++++++++++++++++ modules/options.nix | 17 +++++++++++++++++ modules/overrides.jq | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 modules/overrides.jq diff --git a/modules/installer.nix b/modules/installer.nix index bc3d583..80aa0c7 100644 --- a/modules/installer.nix +++ b/modules/installer.nix @@ -13,6 +13,7 @@ let 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; + overrides = cfg.overrides; }); statePath = "${gcroots}/${stateFile.name}"; @@ -39,6 +40,40 @@ let done ''; + overridesDir = + if (installation == "system") + then "/var/lib/flatpak/overrides" + else "\${XDG_DATA_HOME:-$HOME/.local/share}/flatpak/overrides"; + flatpakOverridesCmd = installation: {}: '' + # Update overrides that are managed by this module (both old and new) + mkdir -p ${overridesDir} + ${pkgs.jq}/bin/jq -r -n \ + --argjson old "$OLD_STATE" \ + --argjson new "$NEW_STATE" \ + '$new.overrides + $old.overrides | keys[]' \ + | while read -r APP_ID; do + OVERRIDES_PATH=${overridesDir}/$APP_ID + + # Transform the INI-like Flatpak overrides file into a workable JSON + if [[ -f $OVERRIDES_PATH ]]; then + ACTIVE=$(cat $OVERRIDES_PATH \ + | ${pkgs.jc}/bin/jc --ini \ + | ${pkgs.jq}/bin/jq 'map_values(map_values(split(";") | select(. != []) // ""))') + else + ACTIVE={} + fi + + # Generate and save the updated overrides file + ${pkgs.jq}/bin/jq -r -n \ + --arg app_id "$APP_ID" \ + --argjson active "$ACTIVE" \ + --argjson old_state "$OLD_STATE" \ + --argjson new_state "$NEW_STATE" \ + --from-file ${./overrides.jq} \ + >$OVERRIDES_PATH + done + ''; + flatpakInstallCmd = installation: update: { appId, origin ? "flathub", commit ? null, ... }: '' ${pkgs.flatpak}/bin/flatpak --${installation} --noninteractive --no-auto-pin install \ ${if update && commit == null then ''--or-update'' else ''''} ${origin} ${appId} @@ -83,6 +118,9 @@ pkgs.writeShellScript "flatpak-managed-install" '' # Install packages ${mkFlatpakInstallCmd installation updateApplications cfg.packages} + # Configure overrides + ${flatpakOverridesCmd installation {}} + # Save state ln -sf ${stateFile} ${statePath} '' diff --git a/modules/options.nix b/modules/options.nix index e061b99..62fff40 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -119,6 +119,23 @@ in ''; }; + overrides = mkOption { + type = with types; attrsOf (attrsOf (attrsOf (either str (listOf str)))); + default = {}; + description = lib.mdDoc '' + Applies the provided attribute set into a Flatpak overrides file with the + same structure, keeping externally applied changes + ''; + example = literalExpression '' + { + # Array entries will be merged with externally applied values + "com.visualstudio.code".Context.sockets = ["wayland" "!x11" "!fallback-x11"]; + # String entries will override externally applied values + global.Environment.LC_ALL = "C.UTF-8"; + }; + ''; + }; + update = mkOption { type = with types; submodule updateOptions; default = { onActivation = false; auto = { enable = false; onCalendar = "weekly"; }; }; diff --git a/modules/overrides.jq b/modules/overrides.jq new file mode 100644 index 0000000..0da9f02 --- /dev/null +++ b/modules/overrides.jq @@ -0,0 +1,42 @@ +# Convert entry value into array +def values($value): if ($value | type) == "string" then [$value] else ($value // []) end; + +# State aliases + ($old_state.overrides[$app_id] // {}) as $old +| ($new_state.overrides[$app_id] // {}) as $new + +# Map sections that exist in either active or new state (ignore old) +| $active + $new | keys | map ( + . as $section | {"section_key": $section, "section_value": ( + + # Map entries that exist in either active or new state (ignore old) + ($active[$section] // {}) + ($new[$section] // {}) | keys | map ( + . as $entry | { "entry_key": $entry, "entry_value": ( + + # Entry value aliases + $active[$section][$entry] as $active_value + | $new[$section][$entry] as $new_value + | $old[$section][$entry] as $old_value + + # Use new value if it is a string + | if ($new_value | type) == "string" then $new_value + else + # Otherwise remove old values from the active ones, and add the new ones + values($active_value) - values($old_value) + values($new_value) + + # Remove empty arrays and duplicate values + | select(. != []) | unique + + # Convert array into Flatpak string array format + | join(";") + end + )} + ) + + # Remove empty arrays + | select(. != []) + )} + )[] + +# Generate the final overrides file +| "[\(.section_key)]", (.section_value[] | "\(.entry_key)=\(.entry_value)"), ""