{ description = "fabrica — a self-hosted git server (single binary, HTTPS + SSH, themeable web UI)"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; fenix = { url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; }; crane.url = "github:ipetkov/crane"; }; outputs = inputs@{ flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; # NixOS module: services.fabrica. A hardened systemd unit, a TOML-typed # `settings` option, secrets via `environmentFile`, and optional firewall # opening. `git` is wrapped onto the service PATH by the package itself. flake.nixosModules.default = { config, lib, pkgs, ... }: let cfg = config.services.fabrica; format = pkgs.formats.toml { }; configFile = format.generate "fabrica.toml" cfg.settings; in { options.services.fabrica = { enable = lib.mkEnableOption "the fabrica git server"; package = lib.mkOption { type = lib.types.package; default = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.default; description = "The fabrica package to run."; }; user = lib.mkOption { type = lib.types.str; default = "fabrica"; description = "User the service runs as."; }; group = lib.mkOption { type = lib.types.str; default = "fabrica"; description = "Group the service runs as."; }; dataDir = lib.mkOption { type = lib.types.path; default = "/var/lib/fabrica"; description = "State directory (repos, database, host key, secret)."; }; environmentFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = '' Path to an EnvironmentFile with secrets as FABRICA__* variables (e.g. agenix/systemd-creds output). Kept out of the Nix store. ''; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Open the HTTP and SSH ports in the firewall."; }; settings = lib.mkOption { inherit (format) type; default = { }; description = "Configuration rendered to fabrica.toml."; example = { instance.url = "https://git.example.com"; server.port = 8080; ssh.port = 2222; }; }; }; config = lib.mkIf cfg.enable { users.users.${cfg.user} = { isSystemUser = true; group = cfg.group; home = cfg.dataDir; }; users.groups.${cfg.group} = { }; networking.firewall = lib.mkIf cfg.openFirewall { allowedTCPPorts = [ (cfg.settings.server.port or 8080) (cfg.settings.ssh.port or 2222) ]; }; systemd.services.fabrica = { description = "fabrica git server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; serviceConfig = { ExecStart = "${cfg.package}/bin/fabrica --config ${configFile} serve"; User = cfg.user; Group = cfg.group; StateDirectory = "fabrica"; WorkingDirectory = cfg.dataDir; Restart = "on-failure"; EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; # Hardening. DynamicUser = false; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; PrivateDevices = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictSUIDSGID = true; LockPersonality = true; ReadWritePaths = [ cfg.dataDir ]; # Only if a privileged port is configured. AmbientCapabilities = lib.mkIf ((cfg.settings.server.port or 8080) < 1024 || (cfg.settings.ssh.port or 2222) < 1024) [ "CAP_NET_BIND_SERVICE" ]; }; }; }; }; perSystem = { self' , pkgs , system , ... }: let # Nightly toolchain via fenix, pinned through flake.lock. The `complete` # profile carries clippy, rustfmt, rust-src, and rust-analyzer. fenixToolchain = inputs.fenix.packages.${system}.complete.toolchain; craneLib = (inputs.crane.mkLib pkgs).overrideToolchain fenixToolchain; # Keep files crane's default cargo-source filter would strip but the build # needs: `.sql` migrations (embedded by `store` via `sqlx::migrate!`), # `.snap` insta snapshots (read by `highlight`), and the web `assets/` # (css/js/svg/… embedded by `web` via `rust-embed` at compile time). src = pkgs.lib.cleanSourceWith { src = ./.; name = "source"; filter = path: type: (builtins.match ".*\\.(sql|snap|css|js|svg|ico|png|woff2)$" path != null) || (craneLib.filterCargoSources path type); }; commonArgs = { inherit src; strictDeps = true; nativeBuildInputs = [ pkgs.pkg-config pkgs.makeWrapper # aws-lc-sys (russh's crypto backend) builds AWS-LC via cmake. pkgs.cmake ]; buildInputs = [ pkgs.libgit2 pkgs.zlib pkgs.openssl # inkjet compiles tree-sitter grammars (some C++), whose objects link # libstdc++ dynamically; keep it in the closure so the built binary # rpaths it and interactive `cargo test` finds it via LD_LIBRARY_PATH. pkgs.stdenv.cc.cc.lib ]; # Use the system libgit2 rather than the vendored copy. LIBGIT2_NO_VENDOR = "1"; }; cargoArtifacts = craneLib.buildDepsOnly commonArgs; fabrica = craneLib.buildPackage ( commonArgs // { inherit cargoArtifacts; doCheck = false; # tests run as their own flake check # The git plumbing must always be reachable at runtime. postInstall = '' wrapProgram $out/bin/fabrica \ --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.git ]} ''; } ); in { packages = { default = fabrica; fabrica = fabrica; container = pkgs.dockerTools.buildLayeredImage { name = "fabrica"; tag = "latest"; contents = [ fabrica pkgs.git pkgs.cacert ]; config = { Entrypoint = [ "${fabrica}/bin/fabrica" ]; Cmd = [ "serve" ]; ExposedPorts = { "8080/tcp" = { }; "2222/tcp" = { }; }; Volumes = { "/var/lib/fabrica" = { }; }; }; }; }; checks = { inherit fabrica; clippy = craneLib.cargoClippy ( commonArgs // { inherit cargoArtifacts; cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings"; } ); fmt = craneLib.cargoFmt { src = craneLib.cleanCargoSource ./.; }; test = craneLib.cargoNextest ( commonArgs // { inherit cargoArtifacts; partitions = 1; partitionType = "count"; # Crates have no tests during early scaffolding; do not treat an # empty test set as a failure. cargoNextestExtraArgs = "--no-tests=pass"; } ); deny = craneLib.cargoDeny { src = craneLib.cleanCargoSource ./.; }; doc = craneLib.cargoDoc ( commonArgs // { inherit cargoArtifacts; } ); }; devShells.default = craneLib.devShell { checks = self'.checks; packages = [ pkgs.sqlx-cli pkgs.sqlite pkgs.postgresql pkgs.git pkgs.openssh pkgs.gnupg pkgs.cargo-nextest pkgs.cargo-deny pkgs.just pkgs.pkg-config pkgs.cmake ]; buildInputs = commonArgs.buildInputs; LIBGIT2_NO_VENDOR = "1"; # Test/bin artifacts built interactively link libgit2 dynamically but # do not get the build sandbox's rpath, so the loader needs the lib # dirs on the search path for `cargo test`/`cargo run` to work in the # shell. (The Nix build embeds rpath itself, so its checks don't.) LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath commonArgs.buildInputs; }; formatter = pkgs.nixpkgs-fmt; }; }; }