diff --git a/nix/modules/darwin.nix b/nix/modules/darwin.nix index a06d9033..428b0387 100644 --- a/nix/modules/darwin.nix +++ b/nix/modules/darwin.nix @@ -25,10 +25,13 @@ if cfg.darwin.paths.blocksRoot != null then cfg.darwin.paths.blocksRoot else "${stateDir}/blocks"; + fileLoggingEnabled = cfg.configSchema.logTarget == "file" || cfg.configSchema.logTarget == "both"; logsPath = if cfg.darwin.paths.logsDir != null then cfg.darwin.paths.logsDir - else defaultLogsPath; + else if fileLoggingEnabled + then defaultLogsPath + else "${stateDir}/logs"; configSource = shared.mkConfigToml { inherit cfg; inherit accountsPath; @@ -103,14 +106,10 @@ in { ]; config = lib.mkIf cfg.enable { - assertions = [ - { - assertion = !(cfg.configFile != null && cfg.environmentFile != null); - message = "services.mithril.configFile and services.mithril.environmentFile cannot both be set."; - } - ]; warnings = - lib.optional (!darwinExternalDiskUsed) + lib.optional (cfg.configFile != null && cfg.environmentFile != null) + (builtins.throw "services.mithril.configFile and services.mithril.environmentFile cannot both be set.") + ++ lib.optional (!darwinExternalDiskUsed) "Mithril on macOS writes heavily; use external storage (e.g. /Volumes/...) via services.mithril.darwin.paths.* or services.mithril.configSchema.storage* to reduce SSD wear."; services.mithril.configFile = lib.mkForce configFile; diff --git a/nix/modules/home-manager/service.nix b/nix/modules/home-manager/service.nix index 26415322..28a35b56 100644 --- a/nix/modules/home-manager/service.nix +++ b/nix/modules/home-manager/service.nix @@ -20,8 +20,11 @@ if pkgs.stdenv.isDarwin && cfg.darwin.paths.blocksRoot != null then cfg.darwin.paths.blocksRoot else "${stateDir}/blocks"; + fileLoggingEnabled = cfg.configSchema.logTarget == "file" || cfg.configSchema.logTarget == "both"; logsPath = - if pkgs.stdenv.isDarwin && cfg.darwin.paths.logsDir != null + if !fileLoggingEnabled + then null + else if pkgs.stdenv.isDarwin && cfg.darwin.paths.logsDir != null then cfg.darwin.paths.logsDir else cfg.configSchema.storageLogs; shared = import ../shared/lib.nix {inherit lib pkgs;}; @@ -29,13 +32,16 @@ inherit cfg; accountsPath = "@STATE_DIRECTORY@/accounts"; blocksRoot = "@STATE_DIRECTORY@/blocks"; - logsPath = "@LOGS_DIRECTORY@"; + logsPath = + if fileLoggingEnabled + then "@LOGS_DIRECTORY@" + else "@STATE_DIRECTORY@/logs"; }; configInitScript = pkgs.writeShellScript "mithril-generate-config-user" '' set -euo pipefail config_dir="$CONFIGURATION_DIRECTORY" state_dir="$STATE_DIRECTORY" - logs_dir="$LOGS_DIRECTORY" + logs_dir="''${LOGS_DIRECTORY:-}" runtime_dir="$RUNTIME_DIRECTORY" mkdir -p "$config_dir" mkdir -p "$runtime_dir" @@ -132,12 +138,9 @@ in { config = lib.mkIf cfg.enable ( lib.mkMerge [ { - assertions = [ - { - assertion = !(cfg.configFile != null && cfg.environmentFile != null); - message = "services.mithril.configFile and services.mithril.environmentFile cannot both be set."; - } - ]; + warnings = + lib.optional (cfg.configFile != null && cfg.environmentFile != null) + (builtins.throw "services.mithril.configFile and services.mithril.environmentFile cannot both be set."); } (lib.mkIf (effectiveGenerate && pkgs.stdenv.isDarwin) { xdg.configFile."mithril/config.toml".source = shared.mkConfigToml { diff --git a/nix/modules/nixos/service.nix b/nix/modules/nixos/service.nix index 17afae5c..8db348a1 100644 --- a/nix/modules/nixos/service.nix +++ b/nix/modules/nixos/service.nix @@ -23,30 +23,61 @@ if cfg.storage.singleDisk.mountPoint != null then "${escapeSystemdPath cfg.storage.singleDisk.mountPoint}.mount" else null; + fileLoggingEnabled = cfg.configSchema.logTarget == "file" || cfg.configSchema.logTarget == "both"; + hasExternalStorage = + cfg.storage.singleDisk.enable + || cfg.storage.accounts.device != null + || cfg.storage.blocks.device != null; shared = import ../shared/lib.nix {inherit lib pkgs;}; configTemplate = shared.mkConfigTomlTemplate { inherit cfg; - accountsPath = "@STATE_DIRECTORY@/accounts"; - blocksRoot = "@STATE_DIRECTORY@/blocks"; - logsPath = "@LOGS_DIRECTORY@"; + accountsPath = + if cfg.storage.accounts.mountPoint != null + then cfg.storage.accounts.mountPoint + else "@STATE_DIRECTORY@/accounts"; + blocksRoot = + if cfg.storage.blocks.mountPoint != null + then cfg.storage.blocks.mountPoint + else "@STATE_DIRECTORY@/blocks"; + logsPath = + if fileLoggingEnabled + then "@LOGS_DIRECTORY@" + else "@STATE_DIRECTORY@/logs"; }; mkdirsScript = pkgs.writeShellScript "mithril-mkdirs" '' set -euo pipefail + ${lib.optionalString hasExternalStorage '' + # External storage mounts are root-owned after mkfs. + # Chown mount point roots so the DynamicUser can write. + if [ -n "${singleDiskMountPoint}" ]; then + chown "${cfg.user}:${cfg.group}" "${singleDiskMountPoint}" + fi + ${lib.optionalString (cfg.storage.accounts.device != null) '' + if [ -n "${accountsMountPoint}" ]; then + chown "${cfg.user}:${cfg.group}" "${accountsMountPoint}" + fi + ''} + ${lib.optionalString (cfg.storage.blocks.device != null) '' + if [ -n "${blocksMountPoint}" ]; then + chown "${cfg.user}:${cfg.group}" "${blocksMountPoint}" + fi + ''} + ''} if [ -n "${accountsMountPoint}" ]; then - install -d -m 0755 -o ${cfg.user} -g ${cfg.group} "${accountsMountPoint}" + install -d -m 0755 ${lib.optionalString hasExternalStorage "-o ${cfg.user} -g ${cfg.group}"} "${accountsMountPoint}" fi if [ -n "${blocksMountPoint}" ]; then - install -d -m 0755 -o ${cfg.user} -g ${cfg.group} "${blocksMountPoint}" + install -d -m 0755 ${lib.optionalString hasExternalStorage "-o ${cfg.user} -g ${cfg.group}"} "${blocksMountPoint}" fi if [ -n "${logsMountPoint}" ]; then - install -d -m 0755 -o ${cfg.user} -g ${cfg.group} "${logsMountPoint}" + install -d -m 0755 ${lib.optionalString hasExternalStorage "-o ${cfg.user} -g ${cfg.group}"} "${logsMountPoint}" fi ''; configInitScript = pkgs.writeShellScript "mithril-generate-config" '' set -euo pipefail config_dir="$CONFIGURATION_DIRECTORY" state_dir="$STATE_DIRECTORY" - logs_dir="$LOGS_DIRECTORY" + logs_dir="''${LOGS_DIRECTORY:-}" runtime_dir="$RUNTIME_DIRECTORY" mkdir -p "$config_dir" mkdir -p "$runtime_dir" @@ -90,6 +121,10 @@ ' "$config_dir/config.toml" > "$runtime_dir/config.toml.tmp" mv "$runtime_dir/config.toml.tmp" "$config_dir/config.toml" ''; + singleDiskMountPoint = + if cfg.storage.singleDisk.mountPoint != null + then cfg.storage.singleDisk.mountPoint + else ""; accountsMountPoint = if cfg.storage.accounts.mountPoint != null then cfg.storage.accounts.mountPoint @@ -99,7 +134,7 @@ then cfg.storage.blocks.mountPoint else ""; logsMountPoint = - if cfg.configSchema.storageLogs != null + if fileLoggingEnabled && cfg.configSchema.storageLogs != null then cfg.configSchema.storageLogs else ""; in { @@ -131,8 +166,8 @@ in { ++ lib.optional (cfg.storage.blocks.device != null && blocksMountUnit != null) blocksMountUnit; serviceConfig.ExecStartPre = - [mkdirsScript] - ++ lib.optionals (effectiveGenerate && cfg.configFile == null) [configInitScript]; + ["+${mkdirsScript}"] + ++ lib.optionals (effectiveGenerate && cfg.configFile == null) ["+${configInitScript}"]; }; mithril-thp = lib.mkIf cfg.performance.enable { diff --git a/nix/modules/nixos/storage.nix b/nix/modules/nixos/storage.nix index 1a0248b5..a8ed68c9 100644 --- a/nix/modules/nixos/storage.nix +++ b/nix/modules/nixos/storage.nix @@ -10,8 +10,7 @@ escaped = lib.replaceStrings ["-"] ["\\x2d"] stripped; in lib.replaceStrings ["/"] ["-"] escaped; - dirName = "mithril"; - stateDir = "/var/lib/${dirName}"; + dataDir = cfg.storage.dataDir; accountsMountUnit = if cfg.storage.accounts.mountPoint != null then "${escapeSystemdPath cfg.storage.accounts.mountPoint}.mount" @@ -24,6 +23,18 @@ if cfg.storage.singleDisk.mountPoint != null then "${escapeSystemdPath cfg.storage.singleDisk.mountPoint}.mount" else null; + singleDeviceUnit = + if cfg.storage.singleDisk.device != null + then "${escapeSystemdPath cfg.storage.singleDisk.device}.device" + else null; + accountsDeviceUnit = + if cfg.storage.accounts.device != null + then "${escapeSystemdPath cfg.storage.accounts.device}.device" + else null; + blocksDeviceUnit = + if cfg.storage.blocks.device != null + then "${escapeSystemdPath cfg.storage.blocks.device}.device" + else null; in { config = lib.mkIf cfg.enable { assertions = [ @@ -51,21 +62,25 @@ in { assertion = !(cfg.storage.blocks.device != null && cfg.storage.blocks.mountPoint == null); message = "services.mithril.storage.blocks.device requires storage.blocks.mountPoint."; } + { + assertion = !lib.hasPrefix "/var/lib/mithril" cfg.storage.dataDir; + message = "services.mithril.storage.dataDir must not be under /var/lib/mithril — it conflicts with the systemd StateDirectory used by DynamicUser."; + } ]; services.mithril.storage = { singleDisk.mountPoint = lib.mkIf cfg.storage.singleDisk.enable ( - lib.mkDefault stateDir + lib.mkDefault dataDir ); accounts.mountPoint = lib.mkDefault ( if cfg.storage.singleDisk.enable then "${cfg.storage.singleDisk.mountPoint}/accounts" - else "${stateDir}/accounts" + else "${dataDir}/accounts" ); blocks.mountPoint = lib.mkDefault ( if cfg.storage.singleDisk.enable then "${cfg.storage.singleDisk.mountPoint}/blocks" - else "${stateDir}/blocks" + else "${dataDir}/blocks" ); }; @@ -102,139 +117,124 @@ in { }) ]; - systemd.services = lib.mkMerge [ - (lib.mkIf (cfg.storage.singleDisk.enable && cfg.storage.singleDisk.format.enable) { - mithril-format-single = { - description = "Format Mithril single disk if needed"; - before = [singleMountUnit]; - requiredBy = [singleMountUnit]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - path = [ - pkgs.util-linux - pkgs.e2fsprogs - pkgs.xfsprogs - pkgs.f2fs-tools - ]; - script = '' - set -euo pipefail - device="${cfg.storage.singleDisk.device}" - fstype="${cfg.storage.singleDisk.fsType}" - label="${cfg.storage.singleDisk.format.label}" - existing="$(blkid -o value -s TYPE "$device" || true)" - if [ -n "$existing" ] && [ "${ - if cfg.storage.singleDisk.format.force - then "true" - else "false" - }" != "true" ]; then - echo "Single disk already formatted as $existing. Skipping." - exit 0 - fi - if [ "${ - if cfg.storage.singleDisk.format.force - then "true" - else "false" - }" = "true" ]; then - wipefs -a "$device" || true + systemd.services = let + # blkid exit codes: 0 = found, 2 = no filesystem, anything else = error. + # The old script used `|| true` which swallowed all errors, so if blkid + # failed (device not ready, permission denied, etc.) the script would + # see an empty $existing and reformat the drive on every boot. + mkFormatScript = { + device, + fsType, + label, + force, + diskLabel, + }: '' + set -euo pipefail + device="${device}" + fstype="${fsType}" + label="${label}" + + if [ ! -b "$device" ]; then + echo "${diskLabel}: device $device not found, refusing to format" >&2 + exit 1 + fi + + rc=0 + existing="$(blkid -o value -s TYPE "$device")" || rc=$? + if [ "$rc" -ne 0 ] && [ "$rc" -ne 2 ]; then + echo "${diskLabel}: blkid failed (exit $rc), refusing to format" >&2 + exit 1 + fi + + ${ + if force + then '' + if [ -n "$existing" ]; then + echo "${diskLabel}: wiping existing $existing filesystem (force=true)" + wipefs -a "$device" fi - case "$fstype" in - ext4) mkfs.ext4 -F -L "$label" "$device" ;; - xfs) mkfs.xfs -f -L "$label" "$device" ;; - f2fs) mkfs.f2fs -f -l "$label" "$device" ;; - esac - ''; - }; - }) - (lib.mkIf (cfg.storage.accounts.device != null && cfg.storage.accounts.format.enable) { - mithril-format-accounts = { - description = "Format Mithril AccountsDB disk if needed"; - before = [accountsMountUnit]; - requiredBy = [accountsMountUnit]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - path = [ - pkgs.util-linux - pkgs.e2fsprogs - pkgs.xfsprogs - pkgs.f2fs-tools - ]; - script = '' - set -euo pipefail - device="${cfg.storage.accounts.device}" - fstype="${cfg.storage.accounts.fsType}" - label="${cfg.storage.accounts.format.label}" - existing="$(blkid -o value -s TYPE "$device" || true)" - if [ -n "$existing" ] && [ "${ - if cfg.storage.accounts.format.force - then "true" - else "false" - }" != "true" ]; then - echo "Accounts disk already formatted as $existing. Skipping." + '' + else '' + if [ -n "$existing" ]; then + echo "${diskLabel}: already formatted as $existing. Skipping." exit 0 fi - if [ "${ - if cfg.storage.accounts.format.force - then "true" - else "false" - }" = "true" ]; then - wipefs -a "$device" || true - fi - case "$fstype" in - ext4) mkfs.ext4 -F -L "$label" "$device" ;; - xfs) mkfs.xfs -f -L "$label" "$device" ;; - f2fs) mkfs.f2fs -f -l "$label" "$device" ;; - esac - ''; - }; - }) - (lib.mkIf (cfg.storage.blocks.device != null && cfg.storage.blocks.format.enable) { - mithril-format-blocks = { - description = "Format Mithril blocks disk if needed"; - before = [blocksMountUnit]; - requiredBy = [blocksMountUnit]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; + '' + } + + echo "${diskLabel}: formatting $device as $fstype (label=$label)" + case "$fstype" in + ext4) mkfs.ext4 -F -L "$label" "$device" ;; + xfs) mkfs.xfs -f -L "$label" "$device" ;; + f2fs) mkfs.f2fs -f -l "$label" "$device" ;; + esac + ''; + formatServiceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + formatPath = [ + pkgs.util-linux + pkgs.e2fsprogs + pkgs.xfsprogs + pkgs.f2fs-tools + ]; + in + lib.mkMerge [ + (lib.mkIf (cfg.storage.singleDisk.enable && cfg.storage.singleDisk.format.enable) { + mithril-format-single = { + description = "Format Mithril single disk if needed"; + after = [singleDeviceUnit]; + requires = [singleDeviceUnit]; + before = [singleMountUnit]; + requiredBy = [singleMountUnit]; + serviceConfig = formatServiceConfig; + path = formatPath; + script = mkFormatScript { + device = cfg.storage.singleDisk.device; + fsType = cfg.storage.singleDisk.fsType; + label = cfg.storage.singleDisk.format.label; + force = cfg.storage.singleDisk.format.force; + diskLabel = "single-disk"; + }; }; - path = [ - pkgs.util-linux - pkgs.e2fsprogs - pkgs.xfsprogs - pkgs.f2fs-tools - ]; - script = '' - set -euo pipefail - device="${cfg.storage.blocks.device}" - fstype="${cfg.storage.blocks.fsType}" - label="${cfg.storage.blocks.format.label}" - existing="$(blkid -o value -s TYPE "$device" || true)" - if [ -n "$existing" ] && [ "${ - if cfg.storage.blocks.format.force - then "true" - else "false" - }" != "true" ]; then - echo "Blocks disk already formatted as $existing. Skipping." - exit 0 - fi - if [ "${ - if cfg.storage.blocks.format.force - then "true" - else "false" - }" = "true" ]; then - wipefs -a "$device" || true - fi - case "$fstype" in - ext4) mkfs.ext4 -F -L "$label" "$device" ;; - xfs) mkfs.xfs -f -L "$label" "$device" ;; - f2fs) mkfs.f2fs -f -l "$label" "$device" ;; - esac - ''; - }; - }) - ]; + }) + (lib.mkIf (cfg.storage.accounts.device != null && cfg.storage.accounts.format.enable) { + mithril-format-accounts = { + description = "Format Mithril AccountsDB disk if needed"; + after = [accountsDeviceUnit]; + requires = [accountsDeviceUnit]; + before = [accountsMountUnit]; + requiredBy = [accountsMountUnit]; + serviceConfig = formatServiceConfig; + path = formatPath; + script = mkFormatScript { + device = cfg.storage.accounts.device; + fsType = cfg.storage.accounts.fsType; + label = cfg.storage.accounts.format.label; + force = cfg.storage.accounts.format.force; + diskLabel = "accounts"; + }; + }; + }) + (lib.mkIf (cfg.storage.blocks.device != null && cfg.storage.blocks.format.enable) { + mithril-format-blocks = { + description = "Format Mithril blocks disk if needed"; + after = [blocksDeviceUnit]; + requires = [blocksDeviceUnit]; + before = [blocksMountUnit]; + requiredBy = [blocksMountUnit]; + serviceConfig = formatServiceConfig; + path = formatPath; + script = mkFormatScript { + device = cfg.storage.blocks.device; + fsType = cfg.storage.blocks.fsType; + label = cfg.storage.blocks.format.label; + force = cfg.storage.blocks.format.force; + diskLabel = "blocks"; + }; + }; + }) + ]; }; } diff --git a/nix/modules/nixos/systemd.nix b/nix/modules/nixos/systemd.nix index bd41a5e8..d5c5f61a 100644 --- a/nix/modules/nixos/systemd.nix +++ b/nix/modules/nixos/systemd.nix @@ -12,22 +12,13 @@ }; in { config = lib.mkIf (cfg.enable && !pkgs.stdenv.isDarwin) { - users.users.${cfg.user} = lib.mkIf (cfg.user != null) { - isSystemUser = true; - group = cfg.group; - }; - users.groups.${cfg.group} = lib.mkIf (cfg.group != null) {}; - systemd.services.mithril = baseUnit // { serviceConfig = baseUnit.serviceConfig // { - User = cfg.user; - Group = cfg.group; - DynamicUser = false; - PermissionsStartOnly = true; + DynamicUser = true; }; wantedBy = ["multi-user.target"]; }; diff --git a/nix/modules/shared/lib.nix b/nix/modules/shared/lib.nix index 8e25576f..7d4701f9 100644 --- a/nix/modules/shared/lib.nix +++ b/nix/modules/shared/lib.nix @@ -21,6 +21,7 @@ if cfg.configSchema.storageSnapshots != null then cfg.configSchema.storageSnapshots else "${blocksRoot}/snapshots"; + fileLoggingEnabled = cfg.configSchema.logTarget == "file" || cfg.configSchema.logTarget == "both"; storageLogs = if cfg.configSchema.storageLogs != null then cfg.configSchema.storageLogs @@ -99,12 +100,24 @@ max_snapshot_url_attempts = cfg.configSchema.snapshotMaxSnapshotUrlAttempts; min_incremental_speed_mbs = cfg.configSchema.snapshotMinIncrementalSpeedMbs; }; - log = { + log = let + fileEnabled = cfg.configSchema.logTarget == "file" || cfg.configSchema.logTarget == "both"; + stdoutEnabled = cfg.configSchema.logTarget == "journald" || cfg.configSchema.logTarget == "both"; + in { level = cfg.configSchema.logLevel; - to_stdout = cfg.configSchema.logToStdout; - max_size_mb = cfg.configSchema.logMaxSizeMb; - max_age_days = cfg.configSchema.logMaxAgeDays; - max_backups = cfg.configSchema.logMaxBackups; + to_stdout = stdoutEnabled; + max_size_mb = + if fileEnabled + then cfg.configSchema.logMaxSizeMb + else null; + max_age_days = + if fileEnabled + then cfg.configSchema.logMaxAgeDays + else null; + max_backups = + if fileEnabled + then cfg.configSchema.logMaxBackups + else null; }; }; finalConfig = removeNulls (lib.recursiveUpdate baseConfig cfg.config.settings); diff --git a/nix/modules/shared/options.nix b/nix/modules/shared/options.nix index bc1aee9b..c716b701 100644 --- a/nix/modules/shared/options.nix +++ b/nix/modules/shared/options.nix @@ -19,6 +19,12 @@ }; storage = { + dataDir = lib.mkOption { + type = lib.types.str; + default = "/var/mithril"; + description = "Base directory for Mithril data when using external storage."; + }; + singleDisk = { enable = lib.mkOption { type = lib.types.bool; @@ -512,28 +518,33 @@ description = "Log level."; }; - logToStdout = lib.mkOption { - type = lib.types.bool; - default = true; - description = "Log to stdout."; + logTarget = lib.mkOption { + type = lib.types.enum ["file" "journald" "both"]; + default = "both"; + description = '' + Where to send logs. + - "file": write log files only (no stdout). + - "journald": log to stdout only (captured by journald on NixOS, system log on macOS). + - "both": log to both stdout and files. + ''; }; logMaxSizeMb = lib.mkOption { type = lib.types.int; default = 100; - description = "Max log file size (MB)."; + description = "Max log file size (MB). Only applies when logTarget includes file logging."; }; logMaxAgeDays = lib.mkOption { type = lib.types.int; default = 30; - description = "Max log file age (days)."; + description = "Max log file age (days). Only applies when logTarget includes file logging."; }; logMaxBackups = lib.mkOption { type = lib.types.int; default = 100; - description = "Max log backups (0 = unlimited)."; + description = "Max log backups (0 = unlimited). Only applies when logTarget includes file logging."; }; }; diff --git a/nix/modules/shared/systemd-base.nix b/nix/modules/shared/systemd-base.nix index fe8503c6..e678bcae 100644 --- a/nix/modules/shared/systemd-base.nix +++ b/nix/modules/shared/systemd-base.nix @@ -25,6 +25,7 @@ "-c" "${cfg.package}/bin/mithril run --config \"${configPath}\"" ]; + fileLoggingEnabled = cfg.configSchema.logTarget == "file" || cfg.configSchema.logTarget == "both"; rwPaths = lib.filter (p: p != null) ( if cfg.storage.singleDisk.enable @@ -34,7 +35,7 @@ cfg.storage.blocks.mountPoint ] ) - ++ lib.optional (cfg.configSchema.storageLogs != null) cfg.configSchema.storageLogs; + ++ lib.optional (fileLoggingEnabled && cfg.configSchema.storageLogs != null) cfg.configSchema.storageLogs; in { baseUnit = { unitConfig = { @@ -48,14 +49,16 @@ in { Restart = "on-failure"; RestartSec = "5s"; Environment = environmentList; + WorkingDirectory = "%S/mithril"; ConfigurationDirectory = "mithril"; StateDirectory = "mithril"; CacheDirectory = "mithril"; RuntimeDirectory = "mithril"; - CredentialsDirectory = "mithril"; - LogsDirectory = "mithril"; ReadWritePaths = rwPaths; } + // lib.optionalAttrs fileLoggingEnabled { + LogsDirectory = "mithril"; + } // lib.optionalAttrs (cfg.environmentFile != null) { EnvironmentFile = [cfg.environmentFile]; };