| 1 | { |
| 2 | description = "fabrica — a self-hosted git server (single binary, HTTPS + SSH, themeable web UI)"; |
| 3 | |
| 4 | inputs = { |
| 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; |
| 6 | flake-parts.url = "github:hercules-ci/flake-parts"; |
| 7 | fenix = { |
| 8 | url = "github:nix-community/fenix"; |
| 9 | inputs.nixpkgs.follows = "nixpkgs"; |
| 10 | }; |
| 11 | crane.url = "github:ipetkov/crane"; |
| 12 | }; |
| 13 | |
| 14 | outputs = |
| 15 | inputs@{ flake-parts, ... }: |
| 16 | flake-parts.lib.mkFlake { inherit inputs; } { |
| 17 | systems = [ |
| 18 | "x86_64-linux" |
| 19 | "aarch64-linux" |
| 20 | "x86_64-darwin" |
| 21 | "aarch64-darwin" |
| 22 | ]; |
| 23 | |
| 24 | |
| 25 | |
| 26 | |
| 27 | flake.nixosModules.default = |
| 28 | { config, lib, pkgs, ... }: |
| 29 | let |
| 30 | cfg = config.services.fabrica; |
| 31 | format = pkgs.formats.toml { }; |
| 32 | configFile = format.generate "fabrica.toml" cfg.settings; |
| 33 | in |
| 34 | { |
| 35 | options.services.fabrica = { |
| 36 | enable = lib.mkEnableOption "the fabrica git server"; |
| 37 | |
| 38 | package = lib.mkOption { |
| 39 | type = lib.types.package; |
| 40 | default = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.default; |
| 41 | description = "The fabrica package to run."; |
| 42 | }; |
| 43 | |
| 44 | user = lib.mkOption { |
| 45 | type = lib.types.str; |
| 46 | default = "fabrica"; |
| 47 | description = "User the service runs as."; |
| 48 | }; |
| 49 | |
| 50 | group = lib.mkOption { |
| 51 | type = lib.types.str; |
| 52 | default = "fabrica"; |
| 53 | description = "Group the service runs as."; |
| 54 | }; |
| 55 | |
| 56 | dataDir = lib.mkOption { |
| 57 | type = lib.types.path; |
| 58 | default = "/var/lib/fabrica"; |
| 59 | description = "State directory (repos, database, host key, secret)."; |
| 60 | }; |
| 61 | |
| 62 | environmentFile = lib.mkOption { |
| 63 | type = lib.types.nullOr lib.types.path; |
| 64 | default = null; |
| 65 | description = '' |
| 66 | Path to an EnvironmentFile with secrets as FABRICA__* variables |
| 67 | (e.g. agenix/systemd-creds output). Kept out of the Nix store. |
| 68 | ''; |
| 69 | }; |
| 70 | |
| 71 | openFirewall = lib.mkOption { |
| 72 | type = lib.types.bool; |
| 73 | default = false; |
| 74 | description = "Open the HTTP and SSH ports in the firewall."; |
| 75 | }; |
| 76 | |
| 77 | settings = lib.mkOption { |
| 78 | inherit (format) type; |
| 79 | default = { }; |
| 80 | description = "Configuration rendered to fabrica.toml."; |
| 81 | example = { |
| 82 | instance.url = "https://git.example.com"; |
| 83 | server.port = 8080; |
| 84 | ssh.port = 2222; |
| 85 | }; |
| 86 | }; |
| 87 | }; |
| 88 | |
| 89 | config = lib.mkIf cfg.enable { |
| 90 | users.users.${cfg.user} = { |
| 91 | isSystemUser = true; |
| 92 | group = cfg.group; |
| 93 | home = cfg.dataDir; |
| 94 | }; |
| 95 | users.groups.${cfg.group} = { }; |
| 96 | |
| 97 | networking.firewall = lib.mkIf cfg.openFirewall { |
| 98 | allowedTCPPorts = [ |
| 99 | (cfg.settings.server.port or 8080) |
| 100 | (cfg.settings.ssh.port or 2222) |
| 101 | ]; |
| 102 | }; |
| 103 | |
| 104 | systemd.services.fabrica = { |
| 105 | description = "fabrica git server"; |
| 106 | wantedBy = [ "multi-user.target" ]; |
| 107 | after = [ "network.target" ]; |
| 108 | serviceConfig = { |
| 109 | ExecStart = "${cfg.package}/bin/fabrica --config ${configFile} serve"; |
| 110 | User = cfg.user; |
| 111 | Group = cfg.group; |
| 112 | StateDirectory = "fabrica"; |
| 113 | WorkingDirectory = cfg.dataDir; |
| 114 | Restart = "on-failure"; |
| 115 | EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; |
| 116 | |
| 117 | |
| 118 | DynamicUser = false; |
| 119 | NoNewPrivileges = true; |
| 120 | ProtectSystem = "strict"; |
| 121 | ProtectHome = true; |
| 122 | PrivateTmp = true; |
| 123 | PrivateDevices = true; |
| 124 | ProtectKernelTunables = true; |
| 125 | ProtectKernelModules = true; |
| 126 | ProtectControlGroups = true; |
| 127 | RestrictSUIDSGID = true; |
| 128 | LockPersonality = true; |
| 129 | ReadWritePaths = [ cfg.dataDir ]; |
| 130 | |
| 131 | AmbientCapabilities = lib.mkIf |
| 132 | ((cfg.settings.server.port or 8080) < 1024 |
| 133 | || (cfg.settings.ssh.port or 2222) < 1024) |
| 134 | [ "CAP_NET_BIND_SERVICE" ]; |
| 135 | }; |
| 136 | }; |
| 137 | }; |
| 138 | }; |
| 139 | |
| 140 | perSystem = |
| 141 | { self' |
| 142 | , pkgs |
| 143 | , system |
| 144 | , ... |
| 145 | }: |
| 146 | let |
| 147 | |
| 148 | |
| 149 | fenixToolchain = inputs.fenix.packages.${system}.complete.toolchain; |
| 150 | |
| 151 | craneLib = (inputs.crane.mkLib pkgs).overrideToolchain fenixToolchain; |
| 152 | |
| 153 | |
| 154 | |
| 155 | |
| 156 | |
| 157 | src = pkgs.lib.cleanSourceWith { |
| 158 | src = ./.; |
| 159 | name = "source"; |
| 160 | filter = |
| 161 | path: type: |
| 162 | (builtins.match ".*\\.(sql|snap|css|js|svg|ico|png|woff2)$" path != null) |
| 163 | || (craneLib.filterCargoSources path type); |
| 164 | }; |
| 165 | |
| 166 | commonArgs = { |
| 167 | inherit src; |
| 168 | strictDeps = true; |
| 169 | nativeBuildInputs = [ |
| 170 | pkgs.pkg-config |
| 171 | pkgs.makeWrapper |
| 172 | |
| 173 | pkgs.cmake |
| 174 | ]; |
| 175 | buildInputs = [ |
| 176 | pkgs.libgit2 |
| 177 | pkgs.zlib |
| 178 | pkgs.openssl |
| 179 | |
| 180 | |
| 181 | |
| 182 | pkgs.stdenv.cc.cc.lib |
| 183 | ]; |
| 184 | |
| 185 | LIBGIT2_NO_VENDOR = "1"; |
| 186 | }; |
| 187 | |
| 188 | cargoArtifacts = craneLib.buildDepsOnly commonArgs; |
| 189 | |
| 190 | fabrica = craneLib.buildPackage ( |
| 191 | commonArgs |
| 192 | // { |
| 193 | inherit cargoArtifacts; |
| 194 | doCheck = false; |
| 195 | |
| 196 | postInstall = '' |
| 197 | wrapProgram $out/bin/fabrica \ |
| 198 | --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.git ]} |
| 199 | ''; |
| 200 | } |
| 201 | ); |
| 202 | in |
| 203 | { |
| 204 | packages = { |
| 205 | default = fabrica; |
| 206 | fabrica = fabrica; |
| 207 | |
| 208 | container = pkgs.dockerTools.buildLayeredImage { |
| 209 | name = "fabrica"; |
| 210 | tag = "latest"; |
| 211 | contents = [ |
| 212 | fabrica |
| 213 | pkgs.git |
| 214 | pkgs.cacert |
| 215 | ]; |
| 216 | config = { |
| 217 | Entrypoint = [ "${fabrica}/bin/fabrica" ]; |
| 218 | Cmd = [ "serve" ]; |
| 219 | ExposedPorts = { |
| 220 | "8080/tcp" = { }; |
| 221 | "2222/tcp" = { }; |
| 222 | }; |
| 223 | Volumes = { |
| 224 | "/var/lib/fabrica" = { }; |
| 225 | }; |
| 226 | }; |
| 227 | }; |
| 228 | }; |
| 229 | |
| 230 | checks = { |
| 231 | inherit fabrica; |
| 232 | |
| 233 | clippy = craneLib.cargoClippy ( |
| 234 | commonArgs |
| 235 | // { |
| 236 | inherit cargoArtifacts; |
| 237 | cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings"; |
| 238 | } |
| 239 | ); |
| 240 | |
| 241 | fmt = craneLib.cargoFmt { |
| 242 | src = craneLib.cleanCargoSource ./.; |
| 243 | }; |
| 244 | |
| 245 | test = craneLib.cargoNextest ( |
| 246 | commonArgs |
| 247 | // { |
| 248 | inherit cargoArtifacts; |
| 249 | partitions = 1; |
| 250 | partitionType = "count"; |
| 251 | |
| 252 | |
| 253 | cargoNextestExtraArgs = "--no-tests=pass"; |
| 254 | } |
| 255 | ); |
| 256 | |
| 257 | deny = craneLib.cargoDeny { |
| 258 | src = craneLib.cleanCargoSource ./.; |
| 259 | }; |
| 260 | |
| 261 | doc = craneLib.cargoDoc ( |
| 262 | commonArgs |
| 263 | // { |
| 264 | inherit cargoArtifacts; |
| 265 | } |
| 266 | ); |
| 267 | }; |
| 268 | |
| 269 | devShells.default = craneLib.devShell { |
| 270 | checks = self'.checks; |
| 271 | packages = [ |
| 272 | pkgs.sqlx-cli |
| 273 | pkgs.sqlite |
| 274 | pkgs.postgresql |
| 275 | pkgs.git |
| 276 | pkgs.openssh |
| 277 | pkgs.gnupg |
| 278 | pkgs.cargo-nextest |
| 279 | pkgs.cargo-deny |
| 280 | pkgs.just |
| 281 | pkgs.pkg-config |
| 282 | pkgs.cmake |
| 283 | ]; |
| 284 | buildInputs = commonArgs.buildInputs; |
| 285 | LIBGIT2_NO_VENDOR = "1"; |
| 286 | |
| 287 | |
| 288 | |
| 289 | |
| 290 | LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath commonArgs.buildInputs; |
| 291 | }; |
| 292 | |
| 293 | formatter = pkgs.nixpkgs-fmt; |
| 294 | }; |
| 295 | }; |
| 296 | } |