1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.uwsgi;
7
8 uwsgi = pkgs.uwsgi.override {
9 plugins = cfg.plugins;
10 };
11
12 buildCfg = name: c:
13 let
14 plugins =
15 if any (n: !any (m: m == n) cfg.plugins) (c.plugins or [])
16 then throw "`plugins` attribute in UWSGI configuration contains plugins not in config.services.uwsgi.plugins"
17 else c.plugins or cfg.plugins;
18
19 hasPython = v: filter (n: n == "python${v}") plugins != [];
20 hasPython2 = hasPython "2";
21 hasPython3 = hasPython "3";
22
23 python =
24 if hasPython2 && hasPython3 then
25 throw "`plugins` attribute in UWSGI configuration shouldn't contain both python2 and python3"
26 else if hasPython2 then uwsgi.python2
27 else if hasPython3 then uwsgi.python3
28 else null;
29
30 pythonPackages = pkgs.pythonPackages.override {
31 inherit python;
32 self = pythonPackages;
33 };
34
35 json = builtins.toJSON {
36 uwsgi =
37 if c.type == "normal"
38 then {
39 inherit plugins;
40 } // removeAttrs c [ "type" "pythonPackages" ]
41 // optionalAttrs (python != null) {
42 pythonpath = "@PYTHONPATH@";
43 env = (c.env or {}) // {
44 PATH = optionalString (c ? env.PATH) "${c.env.PATH}:" + "@PATH@";
45 };
46 }
47 else if c.type == "emperor"
48 then {
49 emperor = if builtins.typeOf c.vassals != "set" then c.vassals
50 else pkgs.buildEnv {
51 name = "vassals";
52 paths = mapAttrsToList buildCfg c.vassals;
53 };
54 } // removeAttrs c [ "type" "vassals" ]
55 else throw "`type` attribute in UWSGI configuration should be either 'normal' or 'emperor'";
56 };
57
58 in
59 if python == null || c.type != "normal"
60 then pkgs.writeTextDir "${name}.json" json
61 else pkgs.stdenv.mkDerivation {
62 name = "uwsgi-config";
63 inherit json;
64 passAsFile = [ "json" ];
65 nativeBuildInputs = [ pythonPackages.wrapPython ];
66 pythonInputs = (c.pythonPackages or (self: [])) pythonPackages;
67
68 buildCommand = ''
69 mkdir $out
70 declare -A pythonPathsSeen=()
71 program_PYTHONPATH=
72 program_PATH=
73 if [ -n "$pythonInputs" ]; then
74 for i in $pythonInputs; do
75 _addToPythonPath $i
76 done
77 fi
78 # A hack to replace "@PYTHONPATH@" with a JSON list
79 if [ -n "$program_PYTHONPATH" ]; then
80 program_PYTHONPATH="\"''${program_PYTHONPATH//:/\",\"}\""
81 fi
82 substitute $jsonPath $out/${name}.json \
83 --replace '"@PYTHONPATH@"' "[$program_PYTHONPATH]" \
84 --subst-var-by PATH "$program_PATH"
85 '';
86 };
87
88in {
89
90 options = {
91 services.uwsgi = {
92
93 enable = mkOption {
94 type = types.bool;
95 default = false;
96 description = "Enable uWSGI";
97 };
98
99 runDir = mkOption {
100 type = types.string;
101 default = "/run/uwsgi";
102 description = "Where uWSGI communication sockets can live";
103 };
104
105 instance = mkOption {
106 type = types.attrs;
107 default = {
108 type = "normal";
109 };
110 example = literalExample ''
111 {
112 type = "emperor";
113 vassals = {
114 moin = {
115 type = "normal";
116 pythonPackages = self: with self; [ moinmoin ];
117 socket = "${config.services.uwsgi.runDir}/uwsgi.sock";
118 };
119 };
120 }
121 '';
122 description = ''
123 uWSGI configuration. It awaits an attribute <literal>type</literal> inside which can be either
124 <literal>normal</literal> or <literal>emperor</literal>.
125
126 For <literal>normal</literal> mode you can specify <literal>pythonPackages</literal> as a function
127 from libraries set into a list of libraries. <literal>pythonpath</literal> will be set accordingly.
128
129 For <literal>emperor</literal> mode, you should use <literal>vassals</literal> attribute
130 which should be either a set of names and configurations or a path to a directory.
131
132 Other attributes will be used in configuration file as-is. Notice that you can redefine
133 <literal>plugins</literal> setting here.
134 '';
135 };
136
137 plugins = mkOption {
138 type = types.listOf types.str;
139 default = [];
140 description = "Plugins used with uWSGI";
141 };
142
143 user = mkOption {
144 type = types.str;
145 default = "uwsgi";
146 description = "User account under which uwsgi runs.";
147 };
148
149 group = mkOption {
150 type = types.str;
151 default = "uwsgi";
152 description = "Group account under which uwsgi runs.";
153 };
154 };
155 };
156
157 config = mkIf cfg.enable {
158 systemd.services.uwsgi = {
159 wantedBy = [ "multi-user.target" ];
160 preStart = ''
161 mkdir -p ${cfg.runDir}
162 chown ${cfg.user}:${cfg.group} ${cfg.runDir}
163 '';
164 serviceConfig = {
165 Type = "notify";
166 ExecStart = "${uwsgi}/bin/uwsgi --uid ${cfg.user} --gid ${cfg.group} --json ${buildCfg "server" cfg.instance}/server.json";
167 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
168 ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
169 NotifyAccess = "main";
170 KillSignal = "SIGQUIT";
171 };
172 };
173
174 users.extraUsers = optionalAttrs (cfg.user == "uwsgi") (singleton
175 { name = "uwsgi";
176 group = cfg.group;
177 uid = config.ids.uids.uwsgi;
178 });
179
180 users.extraGroups = optionalAttrs (cfg.group == "uwsgi") (singleton
181 { name = "uwsgi";
182 gid = config.ids.gids.uwsgi;
183 });
184 };
185}