Compare commits

...

10 Commits

14 changed files with 714 additions and 18 deletions

View File

@@ -0,0 +1,49 @@
{
config,
lib,
...
}:
let
cfg = config.guests.adguard;
in
{
options.guests.adguard.adminPasswordHash = lib.mkOption {
type = lib.types.str;
description = "Bcrypt verifier for the AdGuard Home administrator password.";
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.address != null;
message = "guests.adguard.address must identify the AdGuard Home guest.";
}
];
registry.homepage.${config.networking.hostName}.adguard = {
tab = "System";
section = "Network";
name = "AdGuard Home";
description = "Internal DNS";
icon = "adguard-home.svg";
href = "http://${cfg.address}";
};
guests.adguard = {
interior = {
modules.services.adguardhome = {
enable = true;
inherit (cfg) adminPasswordHash;
};
services.resolved.settings.Resolve.DNSStubListener = false;
};
limits = {
memory = lib.mkDefault "256M";
cpu = lib.mkDefault "100%";
tasksMax = lib.mkDefault 128;
};
};
};
}

View File

@@ -94,6 +94,14 @@ in
}
];
registry.homepage.${config.networking.hostName}.romm = {
tab = "Apps";
section = "Media";
name = "RomM";
description = "Games";
icon = "romm.png";
};
guests.romm = {
interior = {
modules.services.romm.enable = true;

View File

@@ -29,6 +29,14 @@
registry.address.pikachu.self = "10.23.10.102";
guests.adguard = {
enable = true;
vlan = 20;
mac = "bc:24:11:91:60:bf";
address = "10.23.20.33";
adminPasswordHash = "$2b$12$wariglnrCvJOnh4b8KnB9eDX4RdQRaaTZtyXMUOilaYSn7xTR0g1e";
};
guests.valheim = {
enable = true;
vlan = 20;
@@ -104,6 +112,7 @@
domain = "games.alexion.dev";
protocol = "https";
port = 80;
clientMaxBodySize = "0";
};
};
};

View File

@@ -44,6 +44,7 @@
modules.desktop.enable = true;
modules.desktop.apps.obsidian.enable = true;
modules.desktop.apps.r2modman.enable = true;
modules.desktop.emulation.enable = true;
modules.desktop.apps.signal.enable = true;
modules.desktop.apps.steam.enable = true;
modules.desktop.apps.vencord.enable = true;

View File

@@ -0,0 +1,36 @@
{
config,
lib,
...
}:
# Shared directories for emulators.
let
user = config.user.name;
directory = "/home/${user}/emulation";
in
{
options.modules.desktop.emulation.enable = lib.mkEnableOption "shared emulation directories";
config = lib.mkIf config.modules.desktop.emulation.enable {
modules.desktop.emulation.retroarch = {
enable = lib.mkDefault true;
browserDirectory = "${directory}/roms";
};
home-manager.users.${user} = hm: {
home.file = {
".config/retroarch/saves".source = hm.config.lib.file.mkOutOfStoreSymlink "${directory}/saves";
".config/retroarch/states".source = hm.config.lib.file.mkOutOfStoreSymlink "${directory}/states";
".config/retroarch/system".source = hm.config.lib.file.mkOutOfStoreSymlink "${directory}/bios";
};
systemd.user.tmpfiles.rules = map (path: "d ${path} 0700 - - -") [
directory
"${directory}/bios"
"${directory}/roms"
"${directory}/saves"
"${directory}/states"
];
};
};
}

View File

@@ -0,0 +1,62 @@
{
config,
lib,
pkgs,
util,
...
}:
# RetroArch with the selected PlayStation and Sega Saturn cores.
let
cfg = config.modules.desktop.emulation.retroarch;
user = config.user.name;
in
{
options.modules.desktop.emulation.retroarch = {
enable = lib.mkEnableOption "RetroArch with PlayStation and Sega Saturn cores";
browserDirectory = lib.mkOption {
type = lib.types.str;
default = "";
description = "Directory where RetroArch starts its content browser.";
};
};
config = lib.mkIf cfg.enable {
home-manager.users.${user} = {
home.packages = [
(pkgs.wrapRetroArch {
cores = with pkgs.libretro; [
beetle-psx-hw
beetle-saturn
];
settings = {
savefiles_in_content_dir = "false";
savestates_in_content_dir = "false";
system_directory = "/home/${user}/.config/retroarch/system";
video_fullscreen = "true";
sort_savefiles_enable = "true";
sort_savestates_enable = "true";
cloud_sync_enable = "true";
cloud_sync_driver = "webdav";
webdav_url = "https://games.alexion.dev/api/sync/retroarch/";
cloud_sync_sync_saves = "true";
cloud_sync_sync_configs = "true";
cloud_sync_sync_thumbs = "false";
cloud_sync_sync_system = "false";
cloud_sync_sync_mode = "0";
cloud_sync_destructive = "false";
}
// lib.optionalAttrs (cfg.browserDirectory != "") {
rgui_browser_directory = cfg.browserDirectory;
};
})
];
};
registry.window-icon.${config.networking.hostName}.retroarch = {
class = "com.libretro.RetroArch";
icon = util.glyph "f11b";
};
};
}

View File

@@ -0,0 +1,272 @@
{ config, lib, ... }:
let
cfg = config.modules.services.adguardhome;
in
{
options.modules.services.adguardhome = {
enable = lib.mkEnableOption "AdGuard Home";
adminPasswordHash = lib.mkOption {
type = lib.types.str;
description = "Bcrypt verifier for the AdGuard Home administrator password.";
};
};
config = lib.mkIf cfg.enable {
services.adguardhome = {
enable = true;
mutableSettings = false;
openFirewall = true;
settings = {
http = {
pprof = {
port = 6060;
enabled = false;
};
address = "0.0.0.0:80";
session_ttl = "720h";
};
users = [
{
name = config.user.name;
password = cfg.adminPasswordHash;
}
];
auth_attempts = 5;
block_auth_min = 15;
http_proxy = "";
language = "";
theme = "auto";
dns = {
bind_hosts = [ "0.0.0.0" ];
port = 53;
anonymize_client_ip = false;
ratelimit = 20;
ratelimit_subnet_len_ipv4 = 24;
ratelimit_subnet_len_ipv6 = 56;
ratelimit_whitelist = [ ];
refuse_any = true;
upstream_dns = [
"https://dns.cloudflare.com/dns-query"
"tls://1.1.1.1"
"https://dns.google/dns-query"
"tls://8.8.8.8"
"https://dns10.quad9.net/dns-query"
"tls://9.9.9.9"
];
upstream_dns_file = "";
bootstrap_dns = [
"9.9.9.10"
"149.112.112.10"
"2620:fe::10"
"2620:fe::fe:10"
];
fallback_dns = [ ];
upstream_mode = "parallel";
fastest_timeout = "1s";
allowed_clients = [ ];
disallowed_clients = [ ];
blocked_hosts = [
"version.bind"
"id.server"
"hostname.bind"
];
trusted_proxies = [
"127.0.0.0/8"
"::1/128"
];
cache_enabled = true;
cache_size = 4194304;
cache_ttl_min = 0;
cache_ttl_max = 0;
cache_optimistic = false;
cache_optimistic_answer_ttl = "30s";
cache_optimistic_max_age = "12h";
bogus_nxdomain = [ ];
aaaa_disabled = false;
enable_dnssec = false;
edns_client_subnet = {
custom_ip = "";
enabled = false;
use_custom = false;
};
max_goroutines = 300;
handle_ddr = true;
ipset = [ ];
ipset_file = "";
bootstrap_prefer_ipv6 = false;
upstream_timeout = "10s";
private_networks = [ ];
use_private_ptr_resolvers = true;
local_ptr_upstreams = [ ];
use_dns64 = false;
dns64_prefixes = [ ];
serve_http3 = false;
use_http3_upstreams = false;
serve_plain_dns = true;
hostsfile_enabled = true;
pending_requests.enabled = true;
};
tls = {
enabled = false;
server_name = "";
force_https = false;
port_https = 443;
port_dns_over_tls = 853;
port_dns_over_quic = 853;
port_dnscrypt = 0;
dnscrypt_config_file = "";
allow_unencrypted_doh = false;
certificate_chain = "";
private_key = "";
certificate_path = "";
private_key_path = "";
strict_sni_check = false;
};
querylog = {
dir_path = "";
ignored = [ ];
interval = "2160h";
size_memory = 1000;
enabled = true;
ignored_enabled = false;
file_enabled = true;
};
statistics = {
dir_path = "";
ignored = [ ];
interval = "24h";
enabled = true;
ignored_enabled = false;
};
filters = [
{
enabled = true;
url = "https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt";
name = "AdGuard DNS filter";
id = 1;
}
{
enabled = false;
url = "https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt";
name = "AdAway Default Blocklist";
id = 2;
}
];
whitelist_filters = [ ];
user_rules = [ ];
dhcp = {
enabled = false;
interface_name = "";
local_domain_name = "lan";
dhcpv4 = {
gateway_ip = "";
subnet_mask = "";
range_start = "";
range_end = "";
lease_duration = 86400;
icmp_timeout_msec = 1000;
options = [ ];
};
dhcpv6 = {
range_start = "";
lease_duration = 86400;
ra_slaac_only = false;
ra_allow_slaac = false;
};
};
filtering = {
blocking_ipv4 = "";
blocking_ipv6 = "";
blocked_services = {
schedule.time_zone = "Local";
ids = [ ];
};
protection_disabled_until = null;
safe_search = {
enabled = false;
bing = true;
duckduckgo = true;
ecosia = true;
google = true;
pixabay = true;
yandex = true;
youtube = true;
};
blocking_mode = "default";
parental_block_host = "family-block.dns.adguard.com";
safebrowsing_block_host = "standard-block.dns.adguard.com";
rewrites = [
{
domain = "*.home";
answer = "10.23.20.32";
enabled = true;
}
{
domain = "*.alexion.dev";
answer = "10.23.20.109";
enabled = true;
}
{
domain = "home";
answer = "10.23.20.109";
enabled = true;
}
];
safe_fs_patterns = [ "/opt/AdGuardHome/userfilters/*" ];
safebrowsing_cache_size = 1048576;
safesearch_cache_size = 1048576;
parental_cache_size = 1048576;
cache_time = 30;
filters_update_interval = 24;
blocked_response_ttl = 10;
filtering_enabled = true;
rewrites_enabled = true;
parental_enabled = false;
safebrowsing_enabled = false;
protection_enabled = true;
};
clients = {
runtime_sources = {
whois = true;
arp = true;
rdns = true;
dhcp = true;
hosts = true;
};
persistent = [ ];
};
log = {
enabled = true;
file = "";
max_backups = 0;
max_size = 100;
max_age = 3;
compress = false;
local_time = false;
verbose = false;
};
os = {
group = "";
user = "";
rlimit_nofile = 0;
};
};
};
networking.firewall = {
allowedTCPPorts = [ 53 ];
allowedUDPPorts = [ 53 ];
};
};
}

View File

@@ -71,7 +71,10 @@ let
extraConfig = lib.optionalString (route.clientMaxBodySize != null) ''
client_max_body_size ${route.clientMaxBodySize};
'';
locations."/".proxyPass = mkBackend address port;
locations."/" = {
proxyPass = mkBackend address port;
proxyWebsockets = true;
};
};
};

View File

@@ -1,6 +1,7 @@
{
config,
lib,
pkgs,
...
}:
let
@@ -31,13 +32,33 @@ in
services.romm = {
enable = true;
package = pkgs.alexion.romm;
group = "storage";
database.createLocally = true;
redis.createLocally = true;
watcher.enable = true;
extraEnvironment = {
HASHEOUS_API_ENABLED = "true";
PLAYMATCH_API_ENABLED = "true";
ROM_CONVERTO_ENABLED = "false";
};
inherit (cfg) environmentFile;
};
systemd.services.romm-worker.serviceConfig.ExecStart =
lib.mkForce "${lib.getExe' config.services.romm.package "romm-worker"} scans";
systemd.tmpfiles.settings."10-romm"."${config.services.romm.dataDir}/config/config.yml"."L+".argument =
toString (
(pkgs.formats.yaml { }).generate "romm-config.yml" {
emulatorjs.auto_save_sync = false;
filesystem.structure = {
default = "roms/{platform}/{game}";
firmware = "bios/{platform}";
};
}
);
users.users = {
romm.uid = config.registry."stable-uid".${config.networking.hostName}.romm;
redis-romm.uid = config.registry."stable-uid".${config.networking.hostName}."redis-romm";

View File

@@ -47,6 +47,8 @@ or begin
return $status
end
set remote_nar
if test (count $missing_paths) -gt 0
set nar_file (mktemp --suffix=.nar)
or return $status
@@ -70,26 +72,23 @@ if test (count $missing_paths) -gt 0
return $scp_status
end
echo "deploy-to: importing closure on $host" >&2
ssh -t $host "sudo sh -c 'nix-store --import < $remote_nar' && rm -f $remote_nar"
or begin
echo "deploy-to: closure import failed for $host" >&2
return $status
end
echo "deploy-to: importing the closure, recording $system_path, and activating it on $host" >&2
else
echo "deploy-to: $host already has the full closure for $system_path" >&2
echo "deploy-to: recording $system_path and activating it on $host" >&2
end
echo "deploy-to: recording $system_path as the system profile on $host" >&2
ssh -t $host sudo nix-env --profile /nix/var/nix/profiles/system --set $system_path
or begin
echo "deploy-to: system profile update failed for $host" >&2
return $status
end
set remote_script (
string join '; ' \
'system_path=$1' \
'remote_nar=$2' \
'if test -n "$remote_nar"; then nix-store --import < "$remote_nar"; rm -f "$remote_nar"; fi' \
'nix-env --profile /nix/var/nix/profiles/system --set "$system_path"' \
'"$system_path/bin/switch-to-configuration" switch'
)
echo "deploy-to: activating $system_path on $host" >&2
ssh -t $host sudo "$system_path/bin/switch-to-configuration" switch
ssh -t $host sudo sh -e -c (string escape -- $remote_script) sh (string escape -- $system_path $remote_nar)
or begin
echo "deploy-to: activation failed for $host" >&2
echo "deploy-to: deployment failed for $host" >&2
return $status
end

View File

@@ -0,0 +1,110 @@
# AdGuard migration to Pikachu: design tree
## Research facts
- The pinned Nixpkgs revision provides `adguardhome` version `0.107.79`.
- NixOS provides the `services.adguardhome` module.
- The module runs AdGuard Home as a hardened dynamic systemd user.
- It stores state in `/var/lib/AdGuardHome`.
- It grants `CAP_NET_BIND_SERVICE` for DNS port 53.
- It grants `CAP_NET_RAW` when AdGuard Home DHCP is enabled.
- The module supports an arbitrary YAML configuration through `settings`.
- The module supports fully declarative operation with `mutableSettings = false`.
- In fully declarative operation, the module replaces `/var/lib/AdGuardHome/AdGuardHome.yaml` on each service start.
- Fully declarative operation requires `dns.bootstrap_dns` as a list.
- Declarative settings pass an AdGuard configuration check during the build.
- `mutableSettings = true` merges declared settings into UI changes at service start.
- `mutableSettings = false` replaces the state configuration on every service start.
- `openFirewall` opens only the web interface port.
- The migration must explicitly open DNS ports 53/TCP and 53/UDP when clients need them.
- Pikachu has address `10.23.10.102` on its untagged network.
- Pikachu carries VLAN 20 and creates bridge `br-vlan20`.
- The guest framework runs each guest in its own systemd-nspawn container.
- Each guest attaches to a host VLAN bridge and receives an address through DHCP.
- Pikachu currently configures only VLAN 20 for guests.
- The repository has no AdGuard configuration, Proxmox inventory, or DHCP-provider configuration.
- The current DHCP provider remains unestablished.
- The current Proxmox AdGuard container uses MAC `bc:24:11:91:60:bf`.
- The current Proxmox AdGuard container uses address `10.23.20.33` on VLAN 20.
- This environment has no Proxmox SSH target or local Proxmox control utility.
- The current AdGuard configuration listens on all guest addresses at DNS port 53.
- The current AdGuard configuration has no allowed-client or disallowed-client restrictions.
- The current AdGuard configuration uses Cloudflare, Google, and Quad9 encrypted upstreams in parallel.
- The current AdGuard configuration disables DNSSEC.
- The current AdGuard configuration defines three local rewrites.
- The repository configures no host backup system or monitoring system.
- Hosts update through explicit `nixos-rebuild switch --flake` commands.
- The working tree has unrelated operator changes.
## Service tradeoffs
| Service | NixOS support | Domain filtering | Administration UI | DHCP | Encrypted DNS | Main tradeoff |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| AdGuard Home | `services.adguardhome`; checked YAML and mutable or immutable state | Native block lists, rules, and per-client policy | Native | Native | Upstream and client-facing encrypted DNS | Direct migration with the closest state and feature match. UI state conflicts with fully declarative operation unless configuration is immutable. |
| Blocky | `services.blocky`; checked declarative YAML | Native lists, allow lists, rewrites, and per-client groups | No UI; REST API and metrics only | No | Encrypted upstreams and client-facing DoH, DoT, and DoQ | Strong Git-managed configuration and low operational surface. It lacks an interactive administration interface and DHCP. |
| Pi-hole | `services.pihole-ftl` plus `services.pihole-web`; declarative TOML, lists, and dashboard | Native gravity lists, allow lists, regex rules, and groups | Native | Native | Use a companion for encrypted upstream DNS | Familiar filtering product with a dashboard. The migration changes product data and uses more coupled services. |
| Technitium DNS Server | `services.technitium-dns-server`; service and firewall options only | Native block lists and DNS applications | Native | Native | Upstream and client-facing encrypted DNS | Broadest DNS feature set, including authoritative DNS. NixOS does not expose its runtime configuration declaratively. |
| Unbound | `services.unbound`; structured server and free-form settings | Local zones or response-policy zones require custom policy management | No | No | DNS-over-TLS support | Mature validating recursive resolver. Filtering and user operations require separate design. |
| dnscrypt-proxy | `services.dnscrypt-proxy`; declarative TOML | Local block and allow rules, without a list-management UI | No | No | Encrypted upstream DNS, including DNSCrypt and DoH | Strong upstream privacy and resolver selection. It is not a full network filtering appliance. |
| Knot Resolver | `services.kresd`; structured listener settings and extra Lua | Custom policy modules | No | No | Client-facing DoH and TLS listeners | Flexible high-performance recursive resolver. Filtering requires resolver expertise and custom policy. |
| SmartDNS | `services.smartdns`; free-form settings | Domain and address rules | No NixOS-integrated UI | No | Encrypted upstream and client-facing DNS | Optimizes upstream selection and latency. It offers less complete filtering management than dedicated blockers. |
`Native` means that the product provides the feature without a companion service.
## Decision tree
- **D1 — DNS service product** [settled]
- Retain AdGuard Home on Pikachu.
- Constraint: Preserve AdGuard Home as the DNS service product.
- **D2A — AdGuard Home role** [settled]
- Provides DNS filtering and resolution only.
- Constraint: The existing DHCP provider continues to assign addresses and DHCP options.
- **D3A — Execution placement** [settled]
- Runs in a dedicated guest on Pikachu's existing VLAN 20.
- Constraint: Isolate AdGuard Home from the Pikachu host.
- **D4A — Guest network identity** [settled]
- Reuses MAC `bc:24:11:91:60:bf` and address `10.23.20.33` on VLAN 20.
- Constraint: Stop the Proxmox container before the guest starts.
- **D5A — Network exposure** [settled]
- Listens on all guest addresses at port 53.
- Accepts requests from every reachable internal device.
- **D6A — Resolver policy** [settled]
- Preserves the existing upstream resolvers and parallel resolver mode.
- Preserves disabled DNSSEC and the three local rewrites.
- **D7A — Configuration authority** [settled]
- Uses fully declarative Nix configuration.
- Constraint: Set `services.adguardhome.mutableSettings = false`.
- Constraint: Declare `dns.bootstrap_dns` as a list.
- Constraint: Web UI changes do not persist.
- Constraint: Match existing service behavior except for the administrator password.
- **D8A — Migration data** [settled]
- Migrate functional DNS configuration only.
- Constraint: Do not migrate query history or statistics.
- **D9A — Operations** [settled]
- Declares a newly generated bcrypt verifier in Nix.
- Constraint: Keep the plaintext password outside the repository and Nix store.
- Constraint: The verifier is readable from the Nix store.
- **D10A — Cutover and rollback** [settled]
- Uses a planned atomic cutover with rollback.
- Stop the Proxmox container before starting the guest.
- Validate the guest before restoring the Proxmox container during rollback.
- **D11A — Runtime-state backup** [settled]
- Do not back up rebuildable AdGuard Home runtime state.
- **D12A — Monitoring** [settled]
- Do not add DNS availability monitoring.
- **D13A — Upgrade ownership** [settled]
- The normal flake update and explicit NixOS rebuild workflow owns upgrades.
- Replace AdGuard Home. [not selected]
- **D2B — Replacement product** [not applicable]
- Choose Blocky, Pi-hole, Technitium, or a composed resolver.
## Frontier
None.
## Sources
- Pinned Nixpkgs module: `nixos/modules/services/networking/adguardhome.nix`.
- Pinned Nixpkgs modules: `blocky.nix`, `pihole-ftl.nix`, `pihole-web.nix`, `technitium-dns-server.nix`, `unbound.nix`, `dnscrypt-proxy.nix`, `knot-resolver.nix`, and `smartdns.nix`.
- AdGuard Home README, release `v0.107.79`.
- Technitium DNS Server README, current upstream.

View File

@@ -3,8 +3,8 @@ aliases:
- "0015"
- RomM 5.4 Package
date-created: 2026-10-19 19:40
date-modified: 2026-10-19 19:40
status: open
date-modified: 2026-10-21 00:01
status: closed
area: "[[homelab]]"
blocked-by:
- "[[0014-romm-guest]]"

View File

@@ -4,5 +4,7 @@
inherit root;
};
romm = pkgs.callPackage ./romm.nix { };
waybar = pkgs.callPackage ./waybar.nix { };
}

124
src/flake/packages/romm.nix Normal file
View File

@@ -0,0 +1,124 @@
{
buildNpmPackage,
fetchFromGitHub,
lib,
python3,
romm,
}:
romm.overrideAttrs (
finalAttrs: previousAttrs:
let
frontend = buildNpmPackage {
pname = "romm-frontend";
inherit (finalAttrs) version;
src = "${finalAttrs.src}/frontend";
npmDepsHash = "sha256-f5IcpOVv7JYFzUPkZIq5nbT3dS1f/KCSllh8Vkgl1P0=";
npmFlags = [ "--ignore-scripts" ];
makeCacheWritable = true;
installPhase = ''
runHook preInstall
cp -r dist $out
chmod u+w $out/assets
cp -r assets/. $out/assets/
runHook postInstall
'';
};
# RomM 5.4 requires packages absent from the Nixpkgs 5.2 environment.
pythonEnv = python3.withPackages (
ps: with ps; [
aiohttp
alembic
anyio
apprise
asyncssh
authlib
bcrypt
colorama
croniter
cryptography
defusedxml
email-validator
fastapi
gunicorn
httpx
httpx2
itsdangerous
jinja2
joserfc
mariadb
mutagen
mysql-connector-python
opentelemetry-distro
opentelemetry-exporter-otlp
opentelemetry-instrumentation-aiohttp-client
opentelemetry-instrumentation-fastapi
opentelemetry-instrumentation-httpx
opentelemetry-instrumentation-redis
opentelemetry-instrumentation-sqlalchemy
passlib
pillow
psycopg
pydantic
pydash
python-dotenv
python-magic
python-multipart
python-socketio
pyyaml
redis
rq
rq-scheduler
sentry-sdk
sqlalchemy
starlette
streaming-form-data
strsimpy
ua-parser
unidecode
uvicorn
uvicorn-worker
watchfiles
yarl
zipfile-inflate64
zstandard
]
);
in
{
version = "5.4.0";
src = fetchFromGitHub {
owner = "rommapp";
repo = "romm";
tag = finalAttrs.version;
hash = "sha256-oIYEdIAJgXkcqjZtWlWKD/NFiQ3gbo7gxwLhqZOCWpM=";
};
postPatch = previousAttrs.postPatch + ''
# Redis 8.0.1 removed this argument.
# RomM disables the related maintenance notifications.
substituteInPlace backend/handler/redis_handler.py \
--replace-fail 'from redis.maint_notifications import MaintNotificationsConfig' "" \
--replace-fail ' "maint_notifications_config": MaintNotificationsConfig(enabled=False),' ""
'';
installPhase =
lib.replaceStrings
[
"${previousAttrs.passthru.frontend}"
"${previousAttrs.passthru.pythonEnv}"
]
[
"${frontend}"
"${pythonEnv}"
]
previousAttrs.installPhase;
passthru = previousAttrs.passthru // {
inherit frontend pythonEnv;
};
}
)