diff --git a/flake.lock b/flake.lock index 3eeb3f6..86af839 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,21 @@ { "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-parts": { "inputs": { "nixpkgs-lib": "nixpkgs-lib" @@ -18,13 +34,56 @@ "type": "github" } }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1747372754, + "narHash": "sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo+bnXU9D9k=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1745526057, - "narHash": "sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA=", + "lastModified": 1748460289, + "narHash": "sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f771eb401a46846c1aebd20552521b233dd7e18b", + "rev": "96ec055edbe5ee227f28cdbc3f1ddf1df5965102", "type": "github" }, "original": { @@ -62,11 +121,11 @@ ] }, "locked": { - "lastModified": 1744599653, - "narHash": "sha256-nysSwVVjG4hKoOjhjvE6U5lIKA8sEr1d1QzEfZsannU=", + "lastModified": 1748562898, + "narHash": "sha256-STk4QklrGpM3gliPKNJdBLSQvIrqRuwHI/rnYb/5rh8=", "owner": "pyproject-nix", "repo": "build-system-pkgs", - "rev": "7dba6dbc73120e15b558754c26024f6c93015dd7", + "rev": "33bd58351957bb52dd1700ea7eeefe34de06a892", "type": "github" }, "original": { @@ -82,11 +141,11 @@ ] }, "locked": { - "lastModified": 1743438845, - "narHash": "sha256-1GSaoubGtvsLRwoYwHjeKYq40tLwvuFFVhGrG8J9Oek=", + "lastModified": 1746540146, + "narHash": "sha256-QxdHGNpbicIrw5t6U3x+ZxeY/7IEJ6lYbvsjXmcxFIM=", "owner": "pyproject-nix", "repo": "pyproject.nix", - "rev": "8063ec98edc459571d042a640b1c5e334ecfca1e", + "rev": "e09c10c24ebb955125fda449939bfba664c467fd", "type": "github" }, "original": { @@ -98,12 +157,34 @@ "root": { "inputs": { "flake-parts": "flake-parts", + "hooks": "hooks", "nixpkgs": "nixpkgs", "pyproject-build-systems": "pyproject-build-systems", "pyproject-nix": "pyproject-nix", + "treefmt": "treefmt", "uv2nix": "uv2nix" } }, + "treefmt": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1748243702, + "narHash": "sha256-9YzfeN8CB6SzNPyPm2XjRRqSixDopTapaRsnTpXUEY8=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "1f3f7b784643d488ba4bf315638b2b0a4c5fb007", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + }, "uv2nix": { "inputs": { "nixpkgs": [ @@ -114,11 +195,11 @@ ] }, "locked": { - "lastModified": 1745697651, - "narHash": "sha256-r4A/fkiCenEapHkjJWPiNUZEfviuXMCr6mRozJ5dC4o=", + "lastModified": 1748398512, + "narHash": "sha256-99mf47Kjl/rj716cSjeA6ubZLlhNudmC4HRg/6UMfvs=", "owner": "pyproject-nix", "repo": "uv2nix", - "rev": "cb6508484d534dafd097713b575f2aebc3417de0", + "rev": "f006d191d4ff5894d2ead6299e2eaf3659bc46b0", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index ae00fd6..9db5c5f 100644 --- a/flake.nix +++ b/flake.nix @@ -4,6 +4,14 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; + hooks = { + url = "github:cachix/git-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + treefmt = { + url = "github:numtide/treefmt-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; pyproject-nix = { url = "github:pyproject-nix/pyproject.nix"; inputs.nixpkgs.follows = "nixpkgs"; @@ -21,42 +29,70 @@ }; }; - outputs = { - self, - nixpkgs, - flake-parts, - uv2nix, - pyproject-nix, - pyproject-build-systems, - ... - } @ inputs: let - workspace = uv2nix.lib.workspace.loadWorkspace {workspaceRoot = ./.;}; - overlay = workspace.mkPyprojectOverlay { - sourcePreference = "wheel"; - }; - in - flake-parts.lib.mkFlake {inherit inputs;} { - systems = ["x86_64-linux" "aarch64-linux"]; + outputs = + { + self, + nixpkgs, + flake-parts, + hooks, + treefmt, + uv2nix, + pyproject-nix, + pyproject-build-systems, + ... + }@inputs: + let + workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; }; + overlay = workspace.mkPyprojectOverlay { + sourcePreference = "wheel"; + }; + in + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + hooks.flakeModule + treefmt.flakeModule + ]; - perSystem = { - pkgs, - inputs', - lib, - ... - }: let - python = pkgs.python313; - pythonSet = - (pkgs.callPackage pyproject-nix.build.packages { - inherit python; - }).overrideScope - ( - lib.composeManyExtensions [ - pyproject-build-systems.overlays.default - overlay - ] - ); - in { - devShells.default = pkgs.mkShell { + systems = nixpkgs.lib.systems.flakeExposed; + + perSystem = + { + config, + pkgs, + inputs', + lib, + ... + }: + let + python = pkgs.python313; + pythonSet = + (pkgs.callPackage pyproject-nix.build.packages { + inherit python; + }).overrideScope + ( + lib.composeManyExtensions [ + pyproject-build-systems.overlays.default + overlay + ] + ); + in + { + treefmt = { + projectRootFile = "flake.nix"; + + programs.nixfmt = { + enable = true; + package = pkgs.nixfmt-rfc-style; + }; + + programs.ruff.enable = true; + }; + + pre-commit.settings.hooks = { + treefmt.enable = true; + }; + + devShells.default = pkgs.mkShell { packages = [ python pkgs.libffi @@ -71,11 +107,14 @@ LD_LIBRARY_PATH = lib.makeLibraryPath pkgs.pythonManylinuxPackages.manylinux1; }; shellHook = '' + ${config.pre-commit.installationScript} unset PYTHONPATH ''; }; - packages.default = pythonSet.mkVirtualEnv "forgesync" workspace.deps.default; - }; + packages.default = pythonSet.mkVirtualEnv "forgesync" workspace.deps.default; + }; + + flake.nixosModules.default = import ./module.nix self; }; } diff --git a/module.nix b/module.nix new file mode 100644 index 0000000..18ca141 --- /dev/null +++ b/module.nix @@ -0,0 +1,167 @@ +self: +{ + lib, + pkgs, + utils, + config, + ... +}: +let + cfg = config.services.forgesync; + inherit (lib) types; + inherit (utils.systemdUtils.unitOptions) unitOption; +in +{ + options.services.forgesync = { + enable = lib.mkEnableOption "Forgesync"; + + package = lib.mkPackageOption self.packages.${pkgs.system} "default" { }; + + jobs = lib.mkOption { + description = '' + Synchronization jobs to run. + ''; + default = { }; + type = types.attrsOf ( + types.submodule { + options = { + settings = lib.mkOption { + default = { }; + example = { + from-instance = "https://codeberg.org/api/v1"; + to = "github"; + to-instance = "https://api.github.com"; + remirror = true; + description-template = "{description} (Mirror of {url})"; + mirror-interval = "8h0m0s"; + immediate = true; + log = "INFO"; + }; + description = '' + Settings for this Forgesync job. + ''; + type = + let + simples = [ + types.bool + types.str + types.int + types.float + ]; + in + types.attrsOf ( + types.oneOf ( + simples + ++ [ + (types.listOf (types.oneOf simples)) + ] + ) + ); + + }; + + secretFile = lib.mkOption { + type = types.path; + description = '' + The EnvironmentFile for secrets required for Forgesync: `FROM_TOKEN`, `TO_TOKEN` and `MIRROR_TOKEN`. + ''; + }; + + timerConfig = lib.mkOption { + type = types.nullOr (types.attrsOf unitOption); + default = { + OnCalendar = "daily"; + Persistent = true; + }; + description = '' + When to run the job. + ''; + }; + + inhibit = lib.mkOption { + default = [ ]; + type = types.listOf (types.strMatching "^[^:]+$"); + example = [ + "sleep" + ]; + description = '' + Run the Forgesync process with an inhibition lock taken; + see {manpage}`systemd-inhibit(1)` for a list of possible operations. + ''; + }; + }; + } + ); + }; + }; + + config = lib.mkIf cfg.enable { + systemd = lib.mkMerge ( + lib.mapAttrsToList ( + jobName: job: + let + unitName = "forgesync-job-${jobName}"; + description = "Forgesync job ${jobName}"; + in + { + timers.${unitName} = { + wantedBy = [ "timers.target" ]; + inherit description; + inherit (job) timerConfig; + }; + + services.${unitName} = { + after = [ "network.target" ]; + inherit description; + + serviceConfig = { + Type = "oneshot"; + + DynamicUser = true; + + ExecStart = + let + inhibitArgs = [ + (lib.getExe' config.systemd.package "systemd-inhibit") + "--mode" + "block" + "--who" + description + "--what" + (lib.concatStringsSep ":" job.inhibit) + "--why" + "Scheduled Forgesync job ${jobName}" + "--" + ]; + + args = + (lib.optionals (job.inhibit != [ ]) inhibitArgs) + ++ [ (lib.getExe cfg.package) ] + ++ (lib.cli.toGNUCommandLine { mkOptionName = k: "--${k}"; } job.settings); + in + utils.escapeSystemdExecArgs args; + + EnvironmentFile = job.secretFile; + + NoNewPrivileges = true; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + MemoryDenyWriteExecute = true; + LockPersonality = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + DevicePolicy = "closed"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + }; + }; + } + ) cfg.jobs + ); + }; +} diff --git a/src/forgesync/mirror.py b/src/forgesync/mirror.py index ae8fb89..53825bc 100644 --- a/src/forgesync/mirror.py +++ b/src/forgesync/mirror.py @@ -20,11 +20,9 @@ class PushMirrorConfig: def overlay(self: Self, other: Self) -> Self: result = type(self)() for f in fields(self): - value = ( # pyright: ignore[reportAny] - getattr(other, f.name) - if getattr(other, f.name) is not None - else getattr(self, f.name) - ) + other_attr = getattr(other, f.name) # pyright: ignore[reportAny] + self_attr = getattr(self, f.name) # pyright: ignore[reportAny] + value = other_attr if other_attr is not None else self_attr # pyright: ignore[reportAny] setattr(result, f.name, value) return result