diff --git a/linux_configuration/nix-poc/configuration.nix b/linux_configuration/nix-poc/configuration.nix new file mode 100644 index 0000000..84daf98 --- /dev/null +++ b/linux_configuration/nix-poc/configuration.nix @@ -0,0 +1,28 @@ +{ pkgs, ... }: +{ + imports = [ + ./modules/arch-parity-poc.nix + ]; + + linuxConfigPoc = { + enable = true; + mainUser = "kuhy"; + + # Turn this on when you intentionally want to run the original imperative + # shell installers during activation (hosts install + guard setup, etc.). + # Keep it off by default for safe evaluation and incremental migration. + enableImperativeBootstrap = false; + }; + + # Minimal baseline the host config still needs while this remains a POC. + networking.hostName = "arch-parity-poc"; + time.timeZone = "Europe/Warsaw"; + + # Convenience tools for verification/debugging during migration. + environment.systemPackages = with pkgs; [ + git + jq + ]; + + system.stateVersion = "25.05"; +} diff --git a/linux_configuration/nix-poc/flake.nix b/linux_configuration/nix-poc/flake.nix new file mode 100644 index 0000000..59ed399 --- /dev/null +++ b/linux_configuration/nix-poc/flake.nix @@ -0,0 +1,24 @@ +{ + description = "NixOS proof-of-concept for linux_configuration parity"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + home-manager = { + url = "github:nix-community/home-manager"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { nixpkgs, home-manager, ... }: + let + system = "x86_64-linux"; + in { + nixosConfigurations.arch-parity-poc = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + home-manager.nixosModules.home-manager + ./configuration.nix + ]; + }; + }; +} diff --git a/linux_configuration/nix-poc/modules/arch-parity-poc.nix b/linux_configuration/nix-poc/modules/arch-parity-poc.nix new file mode 100644 index 0000000..8337b63 --- /dev/null +++ b/linux_configuration/nix-poc/modules/arch-parity-poc.nix @@ -0,0 +1,12 @@ +{ ... }: +{ + imports = [ + ./arch-parity/options.nix + ./arch-parity/packages.nix + ./arch-parity/report.nix + ./arch-parity/desktop.nix + ./arch-parity/system-maintenance.nix + ./arch-parity/hosts-guards.nix + ./arch-parity/bootstrap.nix + ]; +} diff --git a/linux_configuration/nix-poc/modules/arch-parity/bootstrap.nix b/linux_configuration/nix-poc/modules/arch-parity/bootstrap.nix new file mode 100644 index 0000000..4d99599 --- /dev/null +++ b/linux_configuration/nix-poc/modules/arch-parity/bootstrap.nix @@ -0,0 +1,17 @@ +{ config, lib, ... }: +let + cfg = config.linuxConfigPoc; + repo = toString cfg.repoPath; +in { + config = lib.mkIf (cfg.enable && cfg.enableImperativeBootstrap) { + system.activationScripts.linuxConfigImperativeBootstrap = { + text = '' + echo "[linuxConfigPoc] Running imperative bootstrap scripts" + bash ${repo}/hosts/install.sh || true + bash ${repo}/hosts/guard/setup_hosts_guard.sh || true + bash ${repo}/scripts/setup_periodic_system.sh || true + ''; + deps = [ "users" "groups" ]; + }; + }; +} diff --git a/linux_configuration/nix-poc/modules/arch-parity/desktop.nix b/linux_configuration/nix-poc/modules/arch-parity/desktop.nix new file mode 100644 index 0000000..9734246 --- /dev/null +++ b/linux_configuration/nix-poc/modules/arch-parity/desktop.nix @@ -0,0 +1,20 @@ +{ config, lib, ... }: +let + cfg = config.linuxConfigPoc; +in { + config = lib.mkIf cfg.enable { + services.xserver.enable = true; + services.xserver.windowManager.i3.enable = true; + services.xserver.displayManager.startx.enable = true; + + # Mirror i3 + i3blocks config deployment using Home Manager. + home-manager.useGlobalPkgs = true; + home-manager.useUserPackages = true; + home-manager.users.${cfg.mainUser} = { + xdg.configFile."i3/config".source = "${cfg.repoPath}/i3-configuration/i3/config"; + xdg.configFile."i3blocks/config".source = "${cfg.repoPath}/i3-configuration/i3blocks/config"; + xdg.configFile."i3blocks".recursive = true; + xdg.configFile."i3blocks".source = "${cfg.repoPath}/i3-configuration/i3blocks"; + }; + }; +} diff --git a/linux_configuration/nix-poc/modules/arch-parity/hosts-guards.nix b/linux_configuration/nix-poc/modules/arch-parity/hosts-guards.nix new file mode 100644 index 0000000..33e418a --- /dev/null +++ b/linux_configuration/nix-poc/modules/arch-parity/hosts-guards.nix @@ -0,0 +1,92 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.linuxConfigPoc; + repo = toString cfg.repoPath; + commonScriptPath = with pkgs; [ + bash + coreutils + findutils + gawk + gnugrep + gnused + util-linux + ]; + + customHostsEntries = pkgs.runCommand "linux-config-custom-hosts" { } '' + sed -n '/^# Custom blocking entries$/,/^EOF$/p' ${cfg.repoPath}/hosts/install.sh \ + | sed '$d' > "$out" + ''; +in { + config = lib.mkIf cfg.enable { + networking.extraHosts = builtins.readFile customHostsEntries; + + # Protect hosts lookup ordering against bypasses. + system.nssDatabases.hosts = [ "files" "myhostname" "dns" ]; + + systemd.services.hosts-guard = { + description = "Enforce canonical /etc/hosts contents"; + after = [ "local-fs.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${repo}/hosts/guard/enforce-hosts.sh"; + Nice = "10"; + IOSchedulingClass = "idle"; + }; + path = commonScriptPath; + }; + + systemd.paths.hosts-guard = { + description = "Watch /etc/hosts and trigger enforcement"; + wantedBy = [ "multi-user.target" ]; + pathConfig = { + PathChanged = [ "/etc/hosts" ]; + Unit = "hosts-guard.service"; + }; + }; + + systemd.services.nsswitch-guard = { + description = "Enforce canonical /etc/nsswitch.conf (prevents hosts bypass)"; + after = [ "local-fs.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${repo}/hosts/guard/enforce-nsswitch.sh"; + Nice = "10"; + IOSchedulingClass = "idle"; + }; + path = commonScriptPath; + }; + + systemd.paths.nsswitch-guard = { + description = "Watch /etc/nsswitch.conf for tampering"; + wantedBy = [ "multi-user.target" ]; + pathConfig = { + PathChanged = [ "/etc/nsswitch.conf" ]; + Unit = "nsswitch-guard.service"; + }; + }; + + systemd.services.resolved-guard = { + description = "Enforce canonical /etc/systemd/resolved.conf (prevents hosts bypass)"; + after = [ "local-fs.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${repo}/hosts/guard/enforce-resolved.sh"; + Nice = "10"; + IOSchedulingClass = "idle"; + }; + path = commonScriptPath; + }; + + systemd.paths.resolved-guard = { + description = "Watch /etc/systemd/resolved.conf for tampering"; + wantedBy = [ "multi-user.target" ]; + pathConfig = { + PathChanged = [ "/etc/systemd/resolved.conf" "/etc/systemd/resolved.conf.d" ]; + Unit = "resolved-guard.service"; + }; + }; + }; +} diff --git a/linux_configuration/nix-poc/modules/arch-parity/options.nix b/linux_configuration/nix-poc/modules/arch-parity/options.nix new file mode 100644 index 0000000..80aeb08 --- /dev/null +++ b/linux_configuration/nix-poc/modules/arch-parity/options.nix @@ -0,0 +1,117 @@ +{ lib, ... }: +{ + options.linuxConfigPoc = { + enable = lib.mkEnableOption "Arch linux_configuration parity proof-of-concept"; + + repoPath = lib.mkOption { + type = lib.types.path; + default = ../../..; + description = "Path to the linux_configuration repository root."; + }; + + mainUser = lib.mkOption { + type = lib.types.str; + default = "kuhy"; + description = "Primary desktop username for user-scoped config (i3/i3blocks)."; + }; + + enableImperativeBootstrap = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Run original shell installers at activation (hosts + guard + periodic setup)."; + }; + + enableResolutionReport = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Generate a package resolution report grouped by migration category."; + }; + + resolutionReportEtcPath = lib.mkOption { + type = lib.types.str; + default = "linux-config-poc/package-resolution-report.txt"; + description = "Path under /etc where the package resolution report is materialized."; + }; + + enableResolutionReportJson = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Generate a machine-readable JSON package resolution report."; + }; + + resolutionReportJsonEtcPath = lib.mkOption { + type = lib.types.str; + default = "linux-config-poc/package-resolution-report.json"; + description = "Path under /etc where the JSON package resolution report is materialized."; + }; + + packageMap = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { + # Fonts / desktop + "ttf-dejavu" = "dejavu_fonts"; + "noto-fonts" = "noto-fonts"; + "ttf-font-awesome" = "font-awesome"; + "adobe-source-sans-pro-fonts" = "source-sans"; + "ttf-liberation" = "liberation_ttf"; + + # Toolchain / language ecosystems + "go-tools" = "gotools"; + "cargo" = "cargo"; + "rust" = "rustc"; + "nodejs" = "nodejs"; + "npm" = "nodejs"; + "yarn" = "yarn"; + "node-gyp" = "nodePackages.node-gyp"; + "lua52" = "lua5_2"; + + # Python package translations + "python-nose" = "python3Packages.nose"; + "python-pyproject-metadata" = "python3Packages.pyproject-metadata"; + "meson-python" = "python3Packages.meson-python"; + "python-numpy" = "python3Packages.numpy"; + "python-markdown" = "python3Packages.markdown"; + "python-pyparsing" = "python3Packages.pyparsing"; + "python-pyqt5" = "python3Packages.pyqt5"; + "python-pefile" = "python3Packages.pefile"; + "python-booleanoperations" = "python3Packages.booleanoperations"; + "python-brotli" = "python3Packages.brotli"; + "python-defcon" = "python3Packages.defcon"; + "python-fontmath" = "python3Packages.fontmath"; + "python-fontpens" = "python3Packages.fontpens"; + "python-fonttools" = "python3Packages.fonttools"; + "python-fs" = "python3Packages.fs"; + "python-tqdm" = "python3Packages.tqdm"; + "python-unicodedata2" = "python3Packages.unicodedata2"; + "python-zopfli" = "python3Packages.zopfli"; + "python-pyaml" = "python3Packages.pyyaml"; + + # Java / Perl + "java-hamcrest" = "hamcrest"; + "perl-font-ttf" = "perlPackages.FontTTF"; + "perl-sort-versions" = "perlPackages.SortVersions"; + + # Multimedia / desktop mismatches + "pavucontrol-qt" = "pavucontrol"; + "qt5-wayland" = "qt5.qtwayland"; + "qt6-tools" = "qt6.full"; + "qt6-shadertools" = "qt6.qtshadertools"; + "ffmpeg" = "ffmpeg-full"; + "pyside6" = "python3Packages.pyside6"; + + # TeX stack consolidation + "texlive-plaingeneric" = "texliveFull"; + "texlive-latexextra" = "texliveFull"; + "texlive-bibtexextra" = "texliveFull"; + "texlive-pictures" = "texliveFull"; + "texlive-fontsextra" = "texliveFull"; + "texlive-formatsextra" = "texliveFull"; + "texlive-pstricks" = "texliveFull"; + "texlive-games" = "texliveFull"; + "texlive-humanities" = "texliveFull"; + "texlive-science" = "texliveFull"; + }; + description = "Mapping from Arch package names to nixpkgs attributes."; + }; + }; +} diff --git a/linux_configuration/nix-poc/modules/arch-parity/packages.nix b/linux_configuration/nix-poc/modules/arch-parity/packages.nix new file mode 100644 index 0000000..6f0939f --- /dev/null +++ b/linux_configuration/nix-poc/modules/arch-parity/packages.nix @@ -0,0 +1,55 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.linuxConfigPoc; + + readList = file: + let + lines = lib.splitString "\n" (builtins.readFile file); + trimmed = map lib.strings.trim lines; + in + builtins.filter (line: line != "" && !(lib.hasPrefix "#" line)) trimmed; + + pacmanPackageFile = "${toString cfg.repoPath}/fresh-install/pacman_packages.txt"; + aurPackageFile = "${toString cfg.repoPath}/fresh-install/aur_packages.txt"; + + pacmanNamesRaw = readList pacmanPackageFile; + aurLinesRaw = readList aurPackageFile; + aurNames = map (line: builtins.head (lib.splitString " " line)) aurLinesRaw; + + mappedPacmanNames = map + (name: if builtins.hasAttr name cfg.packageMap then builtins.getAttr name cfg.packageMap else name) + pacmanNamesRaw; + + resolvePackage = name: lib.attrByPath (lib.splitString "." name) null pkgs; + + resolvedPacmanPkgs = builtins.filter (pkg: pkg != null) (map resolvePackage mappedPacmanNames); + missingPacmanPkgs = builtins.filter (name: (resolvePackage name) == null) mappedPacmanNames; +in { + config = lib.mkIf cfg.enable { + warnings = [ + "linuxConfigPoc: ${toString (builtins.length resolvedPacmanPkgs)} pacman packages resolved to nixpkgs attrs." + "linuxConfigPoc: ${toString (builtins.length missingPacmanPkgs)} pacman packages still need mapping/overlays." + "linuxConfigPoc: ${toString (builtins.length aurNames)} AUR packages detected and not yet represented as Nix derivations." + "linuxConfigPoc: pacman-wrapper policy/challenges remain imperative scripts; full Nix-native equivalent requires dedicated policy module." + ]; + + environment.systemPackages = + lib.unique ( + (with pkgs; [ + acpi + bc + dex + i3blocks + i3lock + i3status + iw + jq + networkmanagerapplet + pavucontrol + terminator + xss-lock + ]) + ++ resolvedPacmanPkgs + ); + }; +} diff --git a/linux_configuration/nix-poc/modules/arch-parity/report.nix b/linux_configuration/nix-poc/modules/arch-parity/report.nix new file mode 100644 index 0000000..e7f3c2d --- /dev/null +++ b/linux_configuration/nix-poc/modules/arch-parity/report.nix @@ -0,0 +1,169 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.linuxConfigPoc; + + readList = file: + let + lines = lib.splitString "\n" (builtins.readFile file); + trimmed = map lib.strings.trim lines; + in + builtins.filter (line: line != "" && !(lib.hasPrefix "#" line)) trimmed; + + pacmanPackageFile = "${toString cfg.repoPath}/fresh-install/pacman_packages.txt"; + aurPackageFile = "${toString cfg.repoPath}/fresh-install/aur_packages.txt"; + + pacmanNamesRaw = readList pacmanPackageFile; + aurLinesRaw = readList aurPackageFile; + aurNames = map (line: builtins.head (lib.splitString " " line)) aurLinesRaw; + + resolvePackage = path: lib.attrByPath (lib.splitString "." path) null pkgs; + existsPath = path: (resolvePackage path) != null; + + mappedRecords = map + (original: + let + mapped = if builtins.hasAttr original cfg.packageMap then builtins.getAttr original cfg.packageMap else original; + in { + inherit original mapped; + resolved = (resolvePackage mapped) != null; + }) + pacmanNamesRaw; + + unresolvedRecords = builtins.filter (record: !record.resolved) mappedRecords; + + sortStrings = builtins.sort (a: b: a < b); + sortRecordsByOriginal = builtins.sort ( + a: b: + if a.original == b.original + then a.mapped < b.mapped + else a.original < b.original + ); + + candidatePaths = record: + let + pythonCandidate = + if lib.hasPrefix "python-" record.original + then "python3Packages.${lib.removePrefix "python-" record.original}" + else ""; + underscoreFromOriginal = lib.replaceStrings [ "-" ] [ "_" ] record.original; + dashFromOriginal = lib.replaceStrings [ "_" ] [ "-" ] record.original; + underscoreFromMapped = lib.replaceStrings [ "-" ] [ "_" ] record.mapped; + dashFromMapped = lib.replaceStrings [ "_" ] [ "-" ] record.mapped; + candidates = [ + record.original + record.mapped + underscoreFromOriginal + dashFromOriginal + underscoreFromMapped + dashFromMapped + pythonCandidate + ]; + in + lib.unique (builtins.filter (candidate: candidate != "") candidates); + + easySuggestions = record: + builtins.filter + (candidate: existsPath candidate) + (candidatePaths record); + easySuggestionsSorted = record: sortStrings (easySuggestions record); + + isAurOrOverlayLikely = record: + (builtins.elem record.original aurNames) + || (lib.hasSuffix "-git" record.original) + || (lib.hasPrefix "lib32-" record.original) + || (lib.hasSuffix "-bin" record.original); + + easyMapRecords = builtins.filter (record: (builtins.length (easySuggestions record)) > 0) unresolvedRecords; + needsOverlayRecords = builtins.filter + (record: ((builtins.length (easySuggestions record)) == 0) && (isAurOrOverlayLikely record)) + unresolvedRecords; + likelyUnavailableRecords = builtins.filter + (record: ((builtins.length (easySuggestions record)) == 0) && !(isAurOrOverlayLikely record)) + unresolvedRecords; + + easyMapRecordsSorted = sortRecordsByOriginal easyMapRecords; + needsOverlayRecordsSorted = sortRecordsByOriginal needsOverlayRecords; + likelyUnavailableRecordsSorted = sortRecordsByOriginal likelyUnavailableRecords; + aurNamesSorted = sortStrings aurNames; + + formatSimpleRecord = record: "- ${record.original} -> ${record.mapped}"; + formatEasyRecord = record: + "- ${record.original} -> ${record.mapped} | suggestions: ${lib.concatStringsSep ", " (easySuggestionsSorted record)}"; + + renderLines = lines: + if (builtins.length lines) == 0 + then "- none" + else lib.concatStringsSep "\n" lines; + + toEasyJsonRecord = record: { + original = record.original; + mapped = record.mapped; + suggestions = easySuggestionsSorted record; + }; + + toSimpleJsonRecord = record: { + original = record.original; + mapped = record.mapped; + }; + + reportText = '' + linux_configuration nix-poc package resolution report + ================================================ + + Source files: + - ${pacmanPackageFile} + - ${aurPackageFile} + + Summary: + - Total pacman package entries: ${toString (builtins.length pacmanNamesRaw)} + - Unresolved pacman entries after current mapping: ${toString (builtins.length unresolvedRecords)} + - AUR package entries: ${toString (builtins.length aurNames)} + + Category: easy-map (likely solvable by small mapping/path fixes) + --------------------------------------------------------------- + ${renderLines (map formatEasyRecord easyMapRecordsSorted)} + + Category: needs-overlay (AUR/multilib/git/bin packages) + ------------------------------------------------------- + ${renderLines (map formatSimpleRecord needsOverlayRecordsSorted)} + + Category: likely-unavailable (needs manual investigation) + --------------------------------------------------------- + ${renderLines (map formatSimpleRecord likelyUnavailableRecordsSorted)} + + AUR inventory + ------------- + ${renderLines (map (name: "- ${name}") aurNamesSorted)} + ''; + + reportJson = { + schemaVersion = "1.0"; + sourceFiles = { + pacman = pacmanPackageFile; + aur = aurPackageFile; + }; + summary = { + pacmanEntries = builtins.length pacmanNamesRaw; + unresolvedPacmanEntries = builtins.length unresolvedRecords; + aurEntries = builtins.length aurNames; + }; + categories = { + easyMap = map toEasyJsonRecord easyMapRecordsSorted; + needsOverlay = map toSimpleJsonRecord needsOverlayRecordsSorted; + likelyUnavailable = map toSimpleJsonRecord likelyUnavailableRecordsSorted; + }; + aurInventory = aurNamesSorted; + }; +in { + config = lib.mkIf (cfg.enable && cfg.enableResolutionReport) { + warnings = [ + "linuxConfigPoc: package resolution report generated at /etc/${cfg.resolutionReportEtcPath}${lib.optionalString cfg.enableResolutionReportJson " and /etc/${cfg.resolutionReportJsonEtcPath}"}" + ]; + + environment.etc = { + "${cfg.resolutionReportEtcPath}".text = reportText; + } // lib.optionalAttrs cfg.enableResolutionReportJson { + "${cfg.resolutionReportJsonEtcPath}".text = builtins.toJSON reportJson; + }; + }; +} diff --git a/linux_configuration/nix-poc/modules/arch-parity/system-maintenance.nix b/linux_configuration/nix-poc/modules/arch-parity/system-maintenance.nix new file mode 100644 index 0000000..079f17a --- /dev/null +++ b/linux_configuration/nix-poc/modules/arch-parity/system-maintenance.nix @@ -0,0 +1,119 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.linuxConfigPoc; + repo = toString cfg.repoPath; + commonScriptPath = with pkgs; [ + bash + coreutils + curl + findutils + gawk + gnugrep + gnused + procps + util-linux + ]; +in { + config = lib.mkIf cfg.enable { + systemd.services.periodic-system-maintenance = { + description = "Periodic System Maintenance (Pacman Wrapper & Hosts File)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + User = "root"; + ExecStart = "${repo}/scripts/system-maintenance/bin/periodic-system-maintenance.sh"; + StandardOutput = "journal"; + StandardError = "journal"; + TimeoutStartSec = "300"; + TimeoutStopSec = "30"; + Restart = "no"; + }; + path = commonScriptPath; + }; + + systemd.timers.periodic-system-maintenance = { + description = "Run Periodic System Maintenance every hour"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "hourly"; + OnBootSec = "5min"; + RandomizedDelaySec = "300"; + Persistent = true; + }; + unitConfig = { + Requires = "periodic-system-maintenance.service"; + }; + }; + + systemd.services.periodic-system-startup = { + description = "System Maintenance on Startup (Pacman Wrapper & Hosts File)"; + after = [ "network-online.target" "systemd-resolved.service" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + User = "root"; + ExecStart = "${repo}/scripts/system-maintenance/bin/periodic-system-maintenance.sh"; + StandardOutput = "journal"; + StandardError = "journal"; + RemainAfterExit = true; + TimeoutStartSec = "300"; + TimeoutStopSec = "30"; + }; + path = commonScriptPath; + }; + + systemd.services.hosts-file-monitor = { + description = "Hosts File Monitor and Auto-Restore Service"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + User = "root"; + ExecStart = "${repo}/scripts/system-maintenance/bin/hosts-file-monitor.sh"; + Restart = "always"; + RestartSec = "10"; + StandardOutput = "journal"; + StandardError = "journal"; + NoNewPrivileges = false; + PrivateTmp = true; + MemoryMax = "50M"; + CPUQuota = "10%"; + }; + path = commonScriptPath; + }; + + systemd.services.auto-system-update = { + description = "Automatic System Update (pacman + yay AUR)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + User = "root"; + ExecStart = "${repo}/scripts/system-maintenance/bin/auto-system-update.sh"; + StandardOutput = "journal"; + StandardError = "journal"; + TimeoutStartSec = "1800"; + TimeoutStopSec = "30"; + Restart = "no"; + }; + path = commonScriptPath; + }; + + systemd.timers.auto-system-update = { + description = "Run Automatic System Update daily"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "*-*-* 04:00:00"; + RandomizedDelaySec = "1800"; + Persistent = true; + }; + unitConfig = { + Requires = "auto-system-update.service"; + }; + }; + }; +}