1
Fork 0
mirror of https://github.com/RGBCube/hjem synced 2025-10-13 13:12:16 +00:00

Merge pull request #45 from Lunarnovaa/xdgFiles

feat: xdg management
This commit is contained in:
raf 2025-08-03 21:09:09 +03:00 committed by GitHub
commit ee5c671eeb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 390 additions and 16 deletions

View file

@ -46,6 +46,8 @@
hjem-basic = import ./tests/basic.nix checkArgs;
hjem-special-args = import ./tests/special-args.nix checkArgs;
hjem-linker = import ./tests/linker.nix checkArgs;
hjem-xdg = import ./tests/xdg.nix checkArgs;
hjem-xdg-linker = import ./tests/xdg-linker.nix checkArgs;
});
devShells = forAllSystems (system: let

View file

@ -4,6 +4,7 @@
# be avoided here.
{
config,
options,
pkgs,
lib,
...
@ -189,6 +190,92 @@ in {
description = "Files to be managed by Hjem";
};
xdg = {
cache = {
directory = mkOption {
type = path;
default = "${cfg.directory}/.cache";
defaultText = "$HOME/.cache";
description = ''
The XDG cache directory for the user, to which files configured in
{option}`hjem.users.<name>.xdg.cache.files` will be relative to by default.
Adds {env}`XDG_CACHE_HOME` to {option}`environment.sessionVariables` for
this user if changed.
'';
};
files = mkOption {
default = {};
type = attrsOf (fileType cfg.xdg.cache.directory);
example = {"foo.txt".source = "Hello World";};
description = "Cache files to be managed by Hjem";
};
};
config = {
directory = mkOption {
type = path;
default = "${cfg.directory}/.config";
defaultText = "$HOME/.config";
description = ''
The XDG config directory for the user, to which files configured in
{option}`hjem.users.<name>.xdg.config.files` will be relative to by default.
Adds {env}`XDG_CONFIG_HOME` to {option}`environment.sessionVariables` for
this user if changed.
'';
};
files = mkOption {
default = {};
type = attrsOf (fileType cfg.xdg.config.directory);
example = {"foo.txt".source = "Hello World";};
description = "Config files to be managed by Hjem";
};
};
data = {
directory = mkOption {
type = path;
default = "${cfg.directory}/.local/share";
defaultText = "$HOME/.local/share";
description = ''
The XDG data directory for the user, to which files configured in
{option}`hjem.users.<name>.xdg.data.files` will be relative to by default.
Adds {env}`XDG_DATA_HOME` to {option}`environment.sessionVariables` for
this user if changed.
'';
};
files = mkOption {
default = {};
type = attrsOf (fileType cfg.xdg.data.directory);
example = {"foo.txt".source = "Hello World";};
description = "data files to be managed by Hjem";
};
};
state = {
directory = mkOption {
type = path;
default = "${cfg.directory}/.local/state";
defaultText = "$HOME/.local/share";
description = ''
The XDG state directory for the user, to which files configured in
{option}`hjem.users.<name>.xdg.state.files` will be relative to by default.
Adds {env}`XDG_STATE_HOME` to {option}`environment.sessionVariables` for
this user if changed.
'';
};
files = mkOption {
default = {};
type = attrsOf (fileType cfg.xdg.state.directory);
example = {"foo.txt".source = "Hello World";};
description = "state files to be managed by Hjem";
};
};
};
packages = mkOption {
type = listOf package;
default = [];
@ -225,18 +312,25 @@ in {
};
config = {
environment.loadEnv = let
toEnv = env:
if isList env
then concatMapStringsSep ":" toString env
else toString env;
in
lib.pipe cfg.environment.sessionVariables [
(mapAttrsToList (name: value: "export ${name}=\"${toEnv value}\""))
concatLines
(pkgs.writeShellScript "load-env")
];
environment = {
sessionVariables = {
XDG_CACHE_HOME = mkIf (cfg.xdg.cache.directory != options.xdg.cache.directory.default) cfg.xdg.cache.directory;
XDG_CONFIG_HOME = mkIf (cfg.xdg.config.directory != options.xdg.config.directory.default) cfg.xdg.config.directory;
XDG_DATA_HOME = mkIf (cfg.xdg.data.directory != options.xdg.data.directory.default) cfg.xdg.data.directory;
XDG_STATE_HOME = mkIf (cfg.xdg.state.directory != options.xdg.state.directory.default) cfg.xdg.state.directory;
};
loadEnv = let
toEnv = env:
if isList env
then concatMapStringsSep ":" toString env
else toString env;
in
lib.pipe cfg.environment.sessionVariables [
(mapAttrsToList (name: value: "export ${name}=\"${toEnv value}\""))
concatLines
(pkgs.writeShellScript "load-env")
];
};
assertions = [
{
assertion = cfg.user != "";

View file

@ -18,10 +18,18 @@
enabledUsers = filterAttrs (_: u: u.enable) cfg.users;
disabledUsers = filterAttrs (_: u: !u.enable) cfg.users;
userFiles = user: [
user.files
user.xdg.cache.files
user.xdg.config.files
user.xdg.data.files
user.xdg.state.files
];
linker = getExe cfg.linker;
manifests = let
mapFiles = _: files:
mapFiles = files:
lib.attrsets.foldlAttrs (
accum: _: value:
if value.enable -> value.source == null
@ -44,7 +52,9 @@
text = builtins.toJSON {
clobber_by_default = cfg.users."${username}".clobberFiles;
version = 1;
files = mapFiles username cfg.users."${username}".files;
files = concatMap mapFiles (
userFiles cfg.users."${username}"
);
};
checkPhase = ''
set -e
@ -210,8 +220,8 @@ in {
systemd.user.tmpfiles.users =
mapAttrs (_: u: {
rules = pipe u.files [
attrValues
rules = pipe (userFiles u) [
(concatMap attrValues)
(filter (f: f.enable && f.source != null))
(map (
file:

163
tests/xdg-linker.nix Normal file
View file

@ -0,0 +1,163 @@
let
userHome = "/home/alice";
in
(import ./lib) {
name = "hjem-xdg-linker";
nodes = {
node1 = {
self,
pkgs,
inputs,
lib,
...
}: let
inherit (lib.modules) mkIf;
inherit (lib.strings) optionalString;
xdg = {
clobber,
altLocation,
}: {
cache = {
directory = mkIf altLocation (userHome + "/customCacheDirectory");
files = {
"foo" = {
text = "Hello ${optionalString clobber "new "}world!";
inherit clobber;
};
};
};
config = {
directory = mkIf altLocation (userHome + "/customConfigDirectory");
files = {
"bar.json" = {
generator = lib.generators.toJSON {};
value = {bar = "Hello ${optionalString clobber "new "}second world!";};
inherit clobber;
};
};
};
data = {
directory = mkIf altLocation (userHome + "/customDataDirectory");
files = {
"baz.toml" = {
generator = (pkgs.formats.toml {}).generate "baz.toml";
value = {baz = "Hello ${optionalString clobber "new "}third world!";};
inherit clobber;
};
};
};
state = {
directory = mkIf altLocation (userHome + "/customStateDirectory");
files = {
"foo" = {
source = pkgs.writeText "file-bar" "Hello ${optionalString clobber "new "}fourth world!";
inherit clobber;
};
};
};
};
in {
imports = [self.nixosModules.hjem];
system.switch.enable = true;
users.groups.alice = {};
users.users.alice = {
isNormalUser = true;
home = userHome;
password = "";
};
hjem = {
linker = inputs.smfh.packages.${pkgs.system}.default;
users = {
alice = {
enable = true;
};
};
};
specialisation = {
defaultFilesGetLinked.configuration = {
hjem.users.alice = {
xdg = xdg {
clobber = false;
altLocation = false;
};
};
};
altFilesGetLinked.configuration = {
hjem.users.alice = {
files.".config/foo".text = "Hello world!";
xdg = xdg {
clobber = false;
altLocation = true;
};
};
};
altFilesGetOverwritten.configuration = {
hjem.users.alice = {
files.".config/foo" = {
text = "Hello new world!";
clobber = true;
};
xdg = xdg {
clobber = true;
altLocation = true;
};
};
};
};
};
};
testScript = {nodes, ...}: let
baseSystem = nodes.node1.system.build.toplevel;
specialisations = "${baseSystem}/specialisation";
in
# py
''
node1.succeed("loginctl enable-linger alice")
with subtest("Default file locations get liked"):
node1.succeed("${specialisations}/defaultFilesGetLinked/bin/switch-to-configuration test")
node1.succeed("test -L ${userHome}/.cache/foo")
node1.succeed("grep \"Hello world!\" ~alice/.cache/foo")
node1.succeed("test -L ${userHome}/.config/bar.json")
node1.succeed("grep \"Hello second world!\" ~alice/.config/bar.json")
node1.succeed("test -L ${userHome}/.local/share/baz.toml")
node1.succeed("grep \"Hello third world!\" ~alice/.local/share/baz.toml")
node1.succeed("test -L ${userHome}/.local/state/foo")
node1.succeed("grep \"Hello fourth world!\" ~alice/.local/state/foo")
with subtest("Alternate file locations get linked"):
node1.succeed("${specialisations}/altFilesGetLinked/bin/switch-to-configuration test")
node1.succeed("test -L ${userHome}/customCacheDirectory/foo")
node1.succeed("grep \"Hello world!\" ~alice/customCacheDirectory/foo")
node1.succeed("test -L ${userHome}/customConfigDirectory/bar.json")
node1.succeed("grep \"Hello second world!\" ~alice/customConfigDirectory/bar.json")
node1.succeed("test -L ${userHome}/customDataDirectory/baz.toml")
node1.succeed("grep \"Hello third world!\" ~alice/customDataDirectory/baz.toml")
node1.succeed("test -L ${userHome}/customStateDirectory/foo")
node1.succeed("grep \"Hello fourth world!\" ~alice/customStateDirectory/foo")
# Same name as config test file to verify proper merging
node1.succeed("test -L ${userHome}/.config/foo")
node1.succeed("grep \"Hello world!\" ~alice/.config/foo")
with subtest("Alternate file locations get overwritten when changed"):
node1.succeed("${specialisations}/altFilesGetLinked/bin/switch-to-configuration test")
node1.succeed("${specialisations}/altFilesGetOverwritten/bin/switch-to-configuration test")
node1.succeed("test -L ${userHome}/customCacheDirectory/foo")
node1.succeed("grep \"Hello new world!\" ~alice/customCacheDirectory/foo")
node1.succeed("test -L ${userHome}/customConfigDirectory/bar.json")
node1.succeed("grep \"Hello new second world!\" ~alice/customConfigDirectory/bar.json")
node1.succeed("test -L ${userHome}/customDataDirectory/baz.toml")
node1.succeed("grep \"Hello new third world!\" ~alice/customDataDirectory/baz.toml")
node1.succeed("test -L ${userHome}/customStateDirectory/foo")
node1.succeed("grep \"Hello new fourth world!\" ~alice/customStateDirectory/foo")
# Same name as config test file to verify proper merging
node1.succeed("test -L ${userHome}/.config/foo")
node1.succeed("grep \"Hello new world!\" ~alice/.config/foo")
'';
}

105
tests/xdg.nix Normal file
View file

@ -0,0 +1,105 @@
let
userHome = "/home/alice";
in
(import ./lib) {
name = "hjem-xdg";
nodes = {
node1 = {
self,
lib,
pkgs,
...
}: {
imports = [self.nixosModules.hjem];
users.groups.alice = {};
users.users.alice = {
isNormalUser = true;
home = userHome;
password = "";
};
hjem.users = {
alice = {
enable = true;
files = {
"foo" = {
text = "Hello world!";
};
};
xdg = {
cache = {
directory = userHome + "/customCacheDirectory";
files = {
"foo" = {
text = "Hello world!";
};
};
};
config = {
directory = userHome + "/customConfigDirectory";
files = {
"bar.json" = {
generator = lib.generators.toJSON {};
value = {bar = "Hello second world!";};
};
};
};
data = {
directory = userHome + "/customDataDirectory";
files = {
"baz.toml" = {
generator = (pkgs.formats.toml {}).generate "baz.toml";
value = {baz = "Hello third world!";};
};
};
};
state = {
directory = userHome + "/customStateDirectory";
files = {
"foo" = {
source = pkgs.writeText "file-bar" "Hello fourth world!";
};
};
};
};
};
};
# Also test systemd-tmpfiles internally
systemd.user.tmpfiles = {
rules = [
"d %h/user_tmpfiles_created"
];
users.alice.rules = [
"d %h/only_alice"
];
};
};
};
testScript = ''
machine.succeed("loginctl enable-linger alice")
machine.wait_until_succeeds("systemctl --user --machine=alice@ is-active systemd-tmpfiles-setup.service")
# Test XDG files created by Hjem
with subtest("XDG files created by Hjem"):
machine.succeed("[ -L ~alice/customCacheDirectory/foo ]")
machine.succeed("grep \"Hello world!\" ~alice/customCacheDirectory/foo")
machine.succeed("[ -L ~alice/customConfigDirectory/bar.json ]")
machine.succeed("grep \"Hello second world!\" ~alice/customConfigDirectory/bar.json")
machine.succeed("[ -L ~alice/customDataDirectory/baz.toml ]")
machine.succeed("grep \"Hello third world!\" ~alice/customDataDirectory/baz.toml")
# Same name as config test file to verify proper merging
machine.succeed("[ -L ~alice/customStateDirectory/foo ]")
machine.succeed("grep \"Hello fourth world!\" ~alice/customStateDirectory/foo")
with subtest("Basic test file for Hjem"):
machine.succeed("[ -L ~alice/foo ]") # Same name as cache test file to verify proper merging
machine.succeed("grep \"Hello world!\" ~alice/foo")
# Test regular files, created by systemd-tmpfiles
machine.succeed("[ -d ~alice/user_tmpfiles_created ]")
machine.succeed("[ -d ~alice/only_alice ]")
'';
}