Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

installer: add support for flatpakref. #80

Merged
merged 9 commits into from
Oct 15, 2024
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ You can pin a specific commit setting `commit=<hash>` attribute.

Rebuild your system (or home-manager) for changes to take place.

#### Flatpakref files
[Flatpakref](https://docs.flatpak.org/en/latest/repositories.html#flatpakref-files) files can be installed by setting the `flatpakref` attribute to :
```nix
services.flatpak.packages = [
{ flatpakref = "<uri>"; sha256="<hash>"; }
];
```

A `sha256` hash is required for the flatpakref file. This can be generated with `nix-prefetch-url <uri>`.
Omitting the `sha256` attribute will require an `impure` evaluation of the flake.

When installing an application from a `flatpakref` it's remote will be added with the
`SuggestRemoteName` attributed declared in the flatpakref file.
gmodena marked this conversation as resolved.
Show resolved Hide resolved

##### Unmanaged packages and remotes

By default `nix-flatpak` will only manage (install/uninstall/update) packages declared in the
Expand Down
96 changes: 76 additions & 20 deletions modules/installer.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
{ cfg, pkgs, lib, installation ? "system", ... }:

let
utils = import ./ref.nix { inherit pkgs lib; };

flatpakrefCache = builtins.foldl'
(acc: package:
acc // utils.flatpakrefToAttrSet package acc
)
{ }
(builtins.filter (package: utils.isFlatpakref package) cfg.packages);

getAppIdOrRef = flatpakrefUrl: installation:
let
appId = flatpakrefCache.${(utils.sanitizeUrl flatpakrefUrl)}.Name;
in
''
$(if ${pkgs.flatpak}/bin/flatpak --${installation} list --app --columns=application | ${pkgs.gnugrep}/bin/grep -q ${appId}; then
echo "${appId}"
else
echo "--from ${flatpakrefUrl}"
fi)
'';

# 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
Expand All @@ -23,10 +44,28 @@ let
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;
packages = (map
(package:
if utils.isFlatpakref package
then package // {
appId = flatpakrefCache.${(utils.sanitizeUrl package.flatpakref)}.Name;
origin = flatpakrefCache.${(utils.sanitizeUrl package.flatpakref)}.SuggestRemoteName;
}
else package
)
cfg.packages);
overrides = cfg.overrides;
remotes = map (builtins.getAttr "name") cfg.remotes;
# Iterate over remotes and handle remotes installed from flatpakref URLs
remotes =
# Existing remotes (not from flatpakref)
(map (builtins.getAttr "name") cfg.remotes) ++
# Add remotes extracted from flatpakref URLs in packages
map
(package:
flatpakrefCache.${(utils.sanitizeUrl package.flatpakref)}.SuggestRemoteName)
(builtins.filter (package: utils.isFlatpakref package) cfg.packages);
});

statePath = "${gcroots}/${stateFile.name}";
Expand All @@ -46,7 +85,7 @@ let
--arg installed_packages "$INSTALLED_PACKAGES" \
'$old + { "packages" : $installed_packages | split("\n") }')

# Add all configured remoted to the old state, so that only managed ones will be kept across generations.
# Add all configured remote to the old state, so that only managed ones will be kept across generations.
MANAGED_REMOTES=$(${pkgs.flatpak}/bin/flatpak --${installation} remotes --columns=name)

OLD_STATE=$(${pkgs.jq}/bin/jq -r -n \
Expand All @@ -72,6 +111,7 @@ let
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)
${pkgs.coreutils}/bin/mkdir -p ${overridesDir}
Expand Down Expand Up @@ -102,31 +142,47 @@ let
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}
flatpakCmdBuilder = installation: action: args:
"${pkgs.flatpak}/bin/flatpak --${installation} --noninteractive ${args} ${action} ";

${if commit == null
then '' ''
else ''${pkgs.flatpak}/bin/flatpak --${installation} update --noninteractive --commit="${commit}" ${appId}
''}
'';
installCmdBuilder = installation: update: appId: flatpakref: origin:
flatpakCmdBuilder installation " install "
(if update then " --or-update " else " ") +
(if utils.isFlatpakref { flatpakref = flatpakref; }
then getAppIdOrRef flatpakref installation # If the appId is a flatpakref URL, get the appId from the flatpakref file
else " ${origin} ${appId} ");

updateCmdBuilder = installation: commit: appId:
flatpakCmdBuilder installation "update"
"--no-auto-pin --commit=\"${commit}\" ${appId}";

flatpakInstallCmd = installation: update: { appId, origin ? "flathub", commit ? null, flatpakref ? null, ... }:
let
installCmd = installCmdBuilder installation update appId flatpakref origin;

# pin the commit if it is provided
pinCommitOrUpdate =
if commit != null
then updateCmdBuilder installation commit appId
else "";
in
installCmd + "\n" + pinCommitOrUpdate;

flatpakAddRemotesCmd = installation: { name, location, args ? null, ... }: ''
${pkgs.flatpak}/bin/flatpak remote-add --${installation} --if-not-exists ${if args == null then "" else args} ${name} ${location}
'';
flatpakAddRemote = installation: remotes: map (flatpakAddRemotesCmd installation) remotes;

flatpakDeleteRemotesCmd = installation: {}: ''
# Delete all remotes that are present in the old state but not the new one
# $OLD_STATE and $NEW_STATE are globals, declared in the output of pkgs.writeShellScript.
${pkgs.jq}/bin/jq -r -n \
--argjson old "$OLD_STATE" \
--argjson new "$NEW_STATE" \
'(($old.remotes // []) - ($new.remotes // []))[]' \
| while read -r REMOTE_NAME; do
${pkgs.flatpak}/bin/flatpak remote-delete --${installation} $REMOTE_NAME
done
# Delete all remotes that are present in the old state but not the new one
# $OLD_STATE and $NEW_STATE are globals, declared in the output of pkgs.writeShellScript.
${pkgs.jq}/bin/jq -r -n \
--argjson old "$OLD_STATE" \
--argjson new "$NEW_STATE" \
'(($old.remotes // []) - ($new.remotes // []))[]' \
| while read -r REMOTE_NAME; do
${pkgs.flatpak}/bin/flatpak remote-delete --${installation} $REMOTE_NAME
done
'';


Expand Down
11 changes: 11 additions & 0 deletions modules/options.nix
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ let
default = "flathub";
description = lib.mdDoc "App repository origin (default: flathub).";
};

flatpakref = mkOption {
type = types.nullOr types.str;
description = lib.mdDoc "The flakeref URI of the app to install. ";
default = null;
};
sha256 = mkOption {
type = types.nullOr types.str;
description = lib.mdDoc "The sha256 hash of the URI to install. ";
default = null;
};
};
};

Expand Down
43 changes: 43 additions & 0 deletions modules/ref.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# utiliy function to manage flatpakref files
{ pkgs, lib, ... }:
let
# check if a value is a string
isString = value: builtins.typeOf value == "string";

# Check if a package declares a flatpakref
isFlatpakref = { flatpakref, ... }:
flatpakref != null && isString flatpakref;

# sanitize a URL to be used as a key in an attrset.
sanitizeUrl = url: builtins.replaceStrings [ "https://" "/" "." ":" ] [ "https_" "_" "_" "_" ] url;

# Fetch and convert an ini-like flatpakref file into an attrset, and cache it for future use
# within the same activation.
# We piggyback on builtins.fetchurl to fetch and cache flatpakref file. Pure nix evaluations
# requrie a sha256 hash to be provided.
# TODO: extract a generic ini-to-attrset function.
flatpakrefToAttrSet = { flatpakref, sha256, ... }: cache:
let
updatedCache =
if builtins.hasAttr (sanitizeUrl flatpakref) cache then
cache
else
let
fetchurlArgs =
if sha256 != null
then { url = flatpakref; sha256 = sha256; }
else { url = flatpakref; };
iniContent = builtins.readFile (builtins.fetchurl fetchurlArgs);
lines = builtins.split "\r?\n" iniContent;
parsed = builtins.filter (line: line != null) (map (line: builtins.match "(.*)=(.*)" (builtins.toString line)) lines);

# Convert the list of key-value pairs into an attrset
attrSet = builtins.listToAttrs (map (pair: { name = builtins.elemAt pair 0; value = builtins.elemAt pair 1; }) parsed);
in
cache // { ${(sanitizeUrl flatpakref)} = attrSet; };
in
updatedCache;
in
{
inherit isFlatpakref sanitizeUrl flatpakrefToAttrSet;
}
Loading