at 25.11-pre 8.3 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfgs = config.services.cgit; 9 10 settingType = 11 with lib.types; 12 oneOf [ 13 bool 14 int 15 str 16 ]; 17 repeatedSettingType = 18 with lib.types; 19 oneOf [ 20 settingType 21 (listOf settingType) 22 ]; 23 24 genAttrs' = names: f: lib.listToAttrs (map f names); 25 26 regexEscape = 27 let 28 # taken from https://github.com/python/cpython/blob/05cb728d68a278d11466f9a6c8258d914135c96c/Lib/re.py#L251-L266 29 special = [ 30 "(" 31 ")" 32 "[" 33 "]" 34 "{" 35 "}" 36 "?" 37 "*" 38 "+" 39 "-" 40 "|" 41 "^" 42 "$" 43 "\\" 44 "." 45 "&" 46 "~" 47 "#" 48 " " 49 "\t" 50 "\n" 51 "\r" 52 " " # \v / 0x0B 53 " " # \f / 0x0C 54 ]; 55 in 56 lib.replaceStrings special (map (c: "\\${c}") special); 57 58 stripLocation = cfg: lib.removeSuffix "/" cfg.nginx.location; 59 60 regexLocation = cfg: regexEscape (stripLocation cfg); 61 62 mkFastcgiPass = name: cfg: '' 63 ${ 64 if cfg.nginx.location == "/" then 65 '' 66 fastcgi_param PATH_INFO $uri; 67 '' 68 else 69 '' 70 fastcgi_split_path_info ^(${regexLocation cfg})(/.+)$; 71 fastcgi_param PATH_INFO $fastcgi_path_info; 72 '' 73 }fastcgi_pass unix:${config.services.fcgiwrap.instances."cgit-${name}".socket.address}; 74 ''; 75 76 cgitrcLine = 77 name: value: 78 "${name}=${ 79 if value == true then 80 "1" 81 else if value == false then 82 "0" 83 else 84 toString value 85 }"; 86 87 # list value as multiple lines (for "readme" for example) 88 cgitrcEntry = 89 name: value: if lib.isList value then map (cgitrcLine name) value else [ (cgitrcLine name value) ]; 90 91 mkCgitrc = 92 cfg: 93 pkgs.writeText "cgitrc" '' 94 # global settings 95 ${lib.concatStringsSep "\n" ( 96 lib.flatten ( 97 lib.mapAttrsToList cgitrcEntry ({ virtual-root = cfg.nginx.location; } // cfg.settings) 98 ) 99 )} 100 ${lib.optionalString (cfg.scanPath != null) (cgitrcLine "scan-path" cfg.scanPath)} 101 102 # repository settings 103 ${lib.concatStrings ( 104 lib.mapAttrsToList (url: settings: '' 105 ${cgitrcLine "repo.url" url} 106 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: cgitrcLine "repo.${name}") settings)} 107 '') cfg.repos 108 )} 109 110 # extra config 111 ${cfg.extraConfig} 112 ''; 113 114 fcgiwrapUnitName = name: "fcgiwrap-cgit-${name}"; 115 fcgiwrapRuntimeDir = name: "/run/${fcgiwrapUnitName name}"; 116 gitProjectRoot = 117 name: cfg: if cfg.scanPath != null then cfg.scanPath else "${fcgiwrapRuntimeDir name}/repos"; 118 119in 120{ 121 options = { 122 services.cgit = lib.mkOption { 123 description = "Configure cgit instances."; 124 default = { }; 125 type = lib.types.attrsOf ( 126 lib.types.submodule ( 127 { config, ... }: 128 { 129 options = { 130 enable = lib.mkEnableOption "cgit"; 131 132 package = lib.mkPackageOption pkgs "cgit" { }; 133 134 nginx.virtualHost = lib.mkOption { 135 description = "VirtualHost to serve cgit on, defaults to the attribute name."; 136 type = lib.types.str; 137 default = config._module.args.name; 138 example = "git.example.com"; 139 }; 140 141 nginx.location = lib.mkOption { 142 description = "Location to serve cgit under."; 143 type = lib.types.str; 144 default = "/"; 145 example = "/git/"; 146 }; 147 148 repos = lib.mkOption { 149 description = "cgit repository settings, see {manpage}`cgitrc(5)`"; 150 type = with lib.types; attrsOf (attrsOf settingType); 151 default = { }; 152 example = { 153 blah = { 154 path = "/var/lib/git/example"; 155 desc = "An example repository"; 156 }; 157 }; 158 }; 159 160 scanPath = lib.mkOption { 161 description = "A path which will be scanned for repositories."; 162 type = lib.types.nullOr lib.types.path; 163 default = null; 164 example = "/var/lib/git"; 165 }; 166 167 settings = lib.mkOption { 168 description = "cgit configuration, see {manpage}`cgitrc(5)`"; 169 type = lib.types.attrsOf repeatedSettingType; 170 default = { }; 171 example = lib.literalExpression '' 172 { 173 enable-follow-links = true; 174 source-filter = "''${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py"; 175 } 176 ''; 177 }; 178 179 extraConfig = lib.mkOption { 180 description = "These lines go to the end of cgitrc verbatim."; 181 type = lib.types.lines; 182 default = ""; 183 }; 184 185 user = lib.mkOption { 186 description = "User to run the cgit service as."; 187 type = lib.types.str; 188 default = "cgit"; 189 }; 190 191 group = lib.mkOption { 192 description = "Group to run the cgit service as."; 193 type = lib.types.str; 194 default = "cgit"; 195 }; 196 }; 197 } 198 ) 199 ); 200 }; 201 }; 202 203 config = lib.mkIf (lib.any (cfg: cfg.enable) (lib.attrValues cfgs)) { 204 assertions = lib.mapAttrsToList (vhost: cfg: { 205 assertion = !cfg.enable || (cfg.scanPath == null) != (cfg.repos == { }); 206 message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set."; 207 }) cfgs; 208 209 users = lib.mkMerge ( 210 lib.flip lib.mapAttrsToList cfgs ( 211 _: cfg: { 212 users.${cfg.user} = { 213 isSystemUser = true; 214 inherit (cfg) group; 215 }; 216 groups.${cfg.group} = { }; 217 } 218 ) 219 ); 220 221 services.fcgiwrap.instances = lib.flip lib.mapAttrs' cfgs ( 222 name: cfg: 223 lib.nameValuePair "cgit-${name}" { 224 process = { inherit (cfg) user group; }; 225 socket = { inherit (config.services.nginx) user group; }; 226 } 227 ); 228 229 systemd.services = lib.flip lib.mapAttrs' cfgs ( 230 name: cfg: 231 lib.nameValuePair (fcgiwrapUnitName name) ( 232 lib.mkIf (cfg.repos != { }) { 233 serviceConfig.RuntimeDirectory = fcgiwrapUnitName name; 234 preStart = '' 235 GIT_PROJECT_ROOT=${lib.escapeShellArg (gitProjectRoot name cfg)} 236 mkdir -p "$GIT_PROJECT_ROOT" 237 cd "$GIT_PROJECT_ROOT" 238 ${lib.concatLines ( 239 lib.flip lib.mapAttrsToList cfg.repos ( 240 name: repo: '' 241 ln -s ${lib.escapeShellArg repo.path} ${lib.escapeShellArg name} 242 '' 243 ) 244 )} 245 ''; 246 } 247 ) 248 ); 249 250 services.nginx.enable = true; 251 252 services.nginx.virtualHosts = lib.mkMerge ( 253 lib.mapAttrsToList (name: cfg: { 254 ${cfg.nginx.virtualHost} = { 255 locations = 256 (genAttrs' [ "cgit.css" "cgit.png" "favicon.ico" "robots.txt" ] ( 257 fileName: 258 lib.nameValuePair "= ${stripLocation cfg}/${fileName}" { 259 extraConfig = '' 260 alias ${cfg.package}/cgit/${fileName}; 261 ''; 262 } 263 )) 264 // { 265 "~ ${regexLocation cfg}/.+/(info/refs|git-upload-pack)" = { 266 fastcgiParams = rec { 267 SCRIPT_FILENAME = "${pkgs.git}/libexec/git-core/git-http-backend"; 268 GIT_HTTP_EXPORT_ALL = "1"; 269 GIT_PROJECT_ROOT = gitProjectRoot name cfg; 270 HOME = GIT_PROJECT_ROOT; 271 }; 272 extraConfig = mkFastcgiPass name cfg; 273 }; 274 "${stripLocation cfg}/" = { 275 fastcgiParams = { 276 SCRIPT_FILENAME = "${cfg.package}/cgit/cgit.cgi"; 277 QUERY_STRING = "$args"; 278 HTTP_HOST = "$server_name"; 279 CGIT_CONFIG = mkCgitrc cfg; 280 }; 281 extraConfig = mkFastcgiPass name cfg; 282 }; 283 }; 284 }; 285 }) cfgs 286 ); 287 }; 288}