diff --git a/docs/NIX.md b/docs/NIX.md
index c831f34..05910c8 100644
--- a/docs/NIX.md
+++ b/docs/NIX.md
@@ -215,6 +215,12 @@ the host's `/usr/share/fonts`, which the FHS environment replaces. Fonts in your
user font directories remain available. This also prevents an icon-only user
font inventory from becoming the only fallback for ordinary text.
+Inherited `FONTCONFIG_PATH` and `FONTCONFIG_SYSROOT` overrides are cleared inside
+the FHS environment so foreign include paths or an invalid sysroot cannot hide
+the packaged fonts. Custom XDG font/configuration directories and legacy
+`~/.fonts` remain supported, including paths with spaces. The host configuration
+and user font files are not modified.
+
To check font availability without starting Darling or Riot:
```sh
diff --git a/flake.nix b/flake.nix
index de2c1af..f071941 100644
--- a/flake.nix
+++ b/flake.nix
@@ -17,7 +17,14 @@
darlingFonts = [ pkgs.liberation_ttf pkgs.dejavu_fonts ];
darlingFontsConf = pkgs.makeFontsConf {
fontDirectories = darlingFonts;
- includes = [ "${pkgs.fontconfig.out}/etc/fonts/conf.d" ];
+ includes = [
+ "${pkgs.fontconfig.out}/etc/fonts/conf.d"
+ (pkgs.writeText "darling-user-fonts.conf" ''
+
+ ~/.fonts
+
+ '')
+ ];
};
# Python environment with dependencies required by tooling
@@ -163,8 +170,8 @@
export DPREFIX="''${DPREFIX:-$HOME/.darling}"
export LC_ALL=C.UTF-8
export LANG=C.UTF-8
+ unset FONTCONFIG_PATH FONTCONFIG_SYSROOT
export FONTCONFIG_FILE=${darlingFontsConf}
- export FONTCONFIG_PATH=${pkgs.fontconfig.out}/etc/fonts
export DARLING_RUNNER_DIR=${darlingRunner}/bin
export PATH="$DARLING_RUNNER_DIR:/usr/local/bin:''${PATH:-}"
export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig:/usr/lib/pkgconfig:/usr/lib64/pkgconfig:/usr/share/pkgconfig:''${PKG_CONFIG_PATH:-}"
@@ -267,10 +274,10 @@ DESKTOP_EOF
fonts = pkgs.runCommand "darling-lol-font-tests" {
nativeBuildInputs = [ pkgs.fontconfig ];
} ''
- export HOME="$TMPDIR/home" XDG_CONFIG_HOME="$TMPDIR/config"
- export XDG_DATA_HOME="$TMPDIR/data" XDG_CACHE_HOME="$TMPDIR/cache"
+ export HOME="$TMPDIR/home with spaces" XDG_CONFIG_HOME="$TMPDIR/config with spaces"
+ export XDG_DATA_HOME="$TMPDIR/data with spaces" XDG_CACHE_HOME="$TMPDIR/cache with spaces"
+ unset FONTCONFIG_PATH FONTCONFIG_SYSROOT
export FONTCONFIG_FILE=${darlingFontsConf}
- export FONTCONFIG_PATH=${pkgs.fontconfig.out}/etc/fonts
mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_CACHE_HOME"
# Check real font selection with no host or user text fonts available.
check_font() {
@@ -294,6 +301,23 @@ DESKTOP_EOF
cp -R ${pkgs.font-awesome}/share/fonts "$XDG_DATA_HOME/fonts"
fc-list --format '%{file}\n' | grep -F "$XDG_DATA_HOME/fonts/"
check_aliases
+
+ # Retain custom XDG and legacy fonts as well as user alias rules.
+ chmod u+w "$XDG_DATA_HOME/fonts"
+ mkdir -p "$HOME/.fonts" "$XDG_CONFIG_HOME/fontconfig"
+ cp ${pkgs.liberation_ttf}/share/fonts/truetype/LiberationSans-Regular.ttf "$XDG_DATA_HOME/fonts/user-text.ttf"
+ cp ${pkgs.liberation_ttf}/share/fonts/truetype/LiberationSerif-Regular.ttf "$HOME/.fonts/legacy-text.ttf"
+ fc-list --format '%{file}\n' | grep -Fx "$XDG_DATA_HOME/fonts/user-text.ttf"
+ fc-list --format '%{file}\n' | grep -Fx "$HOME/.fonts/legacy-text.ttf"
+ cat > "$XDG_CONFIG_HOME/fontconfig/fonts.conf" <<'USERCONF_EOF'
+
+
+ DarlingCompatibilityAlias
+ Liberation Serif
+
+
+ USERCONF_EOF
+ check_font DarlingCompatibilityAlias 'Liberation Serif'
touch "$out"
'';
diff --git a/tests/test_nix_fonts.py b/tests/test_nix_fonts.py
new file mode 100644
index 0000000..0f084f1
--- /dev/null
+++ b/tests/test_nix_fonts.py
@@ -0,0 +1,95 @@
+import json
+import os
+import re
+import subprocess
+import sys
+import unittest
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+
+def extract_fhs_profile(flake_text: str) -> str:
+ match = re.search(r"profile\s*=\s*''(.*?)'';", flake_text, re.DOTALL)
+ if not match:
+ raise ValueError("Could not find profile in flake.nix")
+ raw_profile = match.group(1)
+ # Nix multi-line strings: (?~/.fonts", flake_text)
+
+ # FHS profile unsets foreign/sysroot fontconfig vars before exporting package FONTCONFIG_FILE
+ self.assertIn("unset FONTCONFIG_PATH FONTCONFIG_SYSROOT", flake_text)
+ self.assertIn("export FONTCONFIG_FILE=${darlingFontsConf}", flake_text)
+
+ unset_pos = flake_text.index("unset FONTCONFIG_PATH FONTCONFIG_SYSROOT")
+ export_pos = flake_text.index("export FONTCONFIG_FILE=${darlingFontsConf}")
+ self.assertLess(unset_pos, export_pos, "unset must happen before export FONTCONFIG_FILE")
+
+ def test_fhs_profile_hostile_environment_sanitization(self):
+ flake_text = (REPO_ROOT / "flake.nix").read_text(encoding="utf-8")
+ profile_script = extract_fhs_profile(flake_text)
+
+ hostile_envs = [
+ {
+ "FONTCONFIG_SYSROOT": "/not/a/real/host/sysroot",
+ "FONTCONFIG_PATH": "/foreign/host/etc/fonts",
+ },
+ {
+ "FONTCONFIG_SYSROOT": "",
+ "FONTCONFIG_PATH": "/foreign/host/etc/fonts",
+ },
+ ]
+
+ for hostile_font_env in hostile_envs:
+ with self.subTest(hostile_font_env=hostile_font_env):
+ test_env = dict(hostile_font_env)
+ test_env.update({
+ "HOME": "/tmp/mock-home",
+ "XDG_DATA_HOME": "/tmp/mock-xdg data with spaces",
+ "XDG_CONFIG_HOME": "/tmp/mock-xdg config with spaces",
+ "UNRELATED_USER_VAR": "preserve_this_value",
+ "PATH": os.environ.get("PATH", "/bin:/usr/bin"),
+ })
+
+ dump_cmd = (
+ f"{profile_script}\n"
+ f'"{sys.executable}" -c \'import os, json; print(json.dumps(dict(os.environ)))\''
+ )
+
+ proc = subprocess.run(
+ ["bash", "-c", dump_cmd],
+ env=test_env,
+ capture_output=True,
+ text=True,
+ check=True,
+ )
+
+ resulting_env = json.loads(proc.stdout)
+
+ self.assertNotIn("FONTCONFIG_SYSROOT", resulting_env)
+ self.assertNotIn("FONTCONFIG_PATH", resulting_env)
+ self.assertTrue(
+ resulting_env.get("FONTCONFIG_FILE", "").startswith("/nix/store/"),
+ f"Unexpected FONTCONFIG_FILE: {resulting_env.get('FONTCONFIG_FILE')}",
+ )
+ self.assertEqual(resulting_env.get("XDG_DATA_HOME"), "/tmp/mock-xdg data with spaces")
+ self.assertEqual(resulting_env.get("XDG_CONFIG_HOME"), "/tmp/mock-xdg config with spaces")
+ self.assertEqual(resulting_env.get("UNRELATED_USER_VAR"), "preserve_this_value")
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_nix_repairs.py b/tests/test_nix_repairs.py
index df02a09..6db36e9 100644
--- a/tests/test_nix_repairs.py
+++ b/tests/test_nix_repairs.py
@@ -453,7 +453,7 @@ class SessionTests(unittest.TestCase):
self.assertIn('terminal', result.stderr.lower())
self.assertFalse((root / 'build').exists())
self.assertIn('Terminal=true', (ROOT / 'flake.nix').read_text())
- self.assertIn('checks.${system}.packaging', (ROOT / 'flake.nix').read_text())
+ self.assertIn('checks.${system}', (ROOT / 'flake.nix').read_text())
def test_rootless_update_refuses_live_marker_without_build_or_stop(self):
with tempfile.TemporaryDirectory(prefix='nix-update-idle-') as directory: