-
Notifications
You must be signed in to change notification settings - Fork 0
/
system.nix
298 lines (262 loc) · 11.9 KB
/
system.nix
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
{ nixpkgs
, pkgs
, lib
, stdenv
, ...
}:
{ name
, cfg
, env ? [ ]
, user ? null
, contents ? [ ]
, entrypoint ? null
, rootEntrypoint ? null
, systemdService ? null
}:
let
evaled = (
import "${nixpkgs}/nixos/lib/eval-config.nix" {
inherit pkgs;
# Override the base modules with our own limited set of modules
baseModules = (
map (p: "${nixpkgs}/nixos/modules/${p}") [
# Essential modules copied from clever's not-os
"system/etc/etc.nix"
"misc/nixpkgs.nix"
"misc/assertions.nix"
"misc/lib.nix"
"config/sysctl.nix"
# And these are included too as they are generally useful, we would add everything here but sometimes modules do things even when not enabled
"misc/extra-arguments.nix"
"misc/ids.nix"
"config/shells-environment.nix"
"config/system-environment.nix"
"config/fonts/fonts.nix"
"config/fonts/fontconfig.nix"
"programs/environment.nix"
"programs/shadow.nix"
"services/web-servers/apache-httpd/default.nix"
]
) ++ [
# Custom system-path module for a lighter system
./custom-modules/system-path.nix
# Custom users-groups that pregenerates passwd
./custom-modules/users-groups.nix
# Bunch of stuff to make modules not complain so much
./compat.nix
# Bunch of stuff to make stock Continix containers weight less
./lite.nix
] ++ (
if user == null && entrypoint != null then [
# Definition for the default user, if a user is not supplied but a user entrypoint is
./default-user.nix
] else [ ]
);
# We're sortof expected to _only_ supply modules, but we wanted to remove some things
# so lets just put the user config here
modules = [
cfg
];
}
);
sys = evaled.config.system;
userEntrypointScript =
if entrypoint != null then
(pkgs.writeScript "user-entrypoint.sh" ''
#!${pkgs.dash}/bin/dash
${entrypoint}
''
) else null;
rootEntrypointScript =
if rootEntrypoint != null then
(pkgs.writeScript "root-entrypoint.sh" ''
#!${pkgs.dash}/bin/dash
${rootEntrypoint}
''
) else null;
# Filter a list of service items ([ "something.target" "anotherthing.service" ]) into a list of a specific type ([ "something" ])
filterServiceItemsForType = (required: type: map (x: builtins.elemAt x 0) (builtins.filter (x: (builtins.elemAt x 1) == type) required));
# Helper method to recursively gather a list of dependencies for a given service
getRequiredBy = (
services: serviceName:
let
# Get the service by name
service = services.${serviceName};
# Helper methods for looking for possibly non-existent field names containing a list of services or targets on the service
# TODO: use better builtins for this?
maybe = (name: if builtins.hasAttr name service then service.${name} else [ ]);
maybes = (names: builtins.concatLists (map maybe names));
# Scrape the list of targets and services this service wants, and seperate into targets and services
required = map (x: lib.splitString "." x) (maybes [ "after" "wants" "requires" ]);
requiredTargets = filterServiceItemsForType required "target";
requiredServices = filterServiceItemsForType required "service";
# Filter over all other services, to find ones which want to run before this one
reverseRequired = builtins.filter
(
aServiceName:
let
# Get the other service by name
aService = services.${aServiceName};
# Helper methods for looking for possibly non-existent field names containing a list of services or targets on the other service
aMaybe = (name: if builtins.hasAttr name aService then aService.${name} else [ ]);
aMaybes = (names: builtins.concatLists (map aMaybe names));
# Scrape the list of targets and services this other service wants to run before
aServicePreceeds = map (x: lib.splitString "." x) (aMaybes [ "before" "wantedBy" ]);
aServicePreceedsTargets = filterServiceItemsForType aServicePreceeds "target";
aServicePreceedsServices = filterServiceItemsForType aServicePreceeds "service";
in
# If this other service wants to preceed the service, or if this other service wants to start before a target the service wants
builtins.elem serviceName aServicePreceedsServices || (builtins.length (builtins.filter (t: builtins.elem t requiredTargets) aServicePreceedsTargets)) != 0
)
(builtins.attrNames services);
# Filtr down the list of required services to only those which exist
existingRequiredServices = builtins.filter (n: builtins.hasAttr n services) requiredServices;
# Make a semi-full list of requirements from the requirements that exist and the requirements found from iterating other services
semiFullRequiredServices = existingRequiredServices ++ reverseRequired;
in
# Combine our semi-full list of requirements with the scraped requirements of those requirements
semiFullRequiredServices ++ (builtins.concatLists (map getRequiredBy semiFullRequiredServices))
);
# Convert a SystemD ExecStart into a bash line (they often begin with special operators that must be parsed)
reparseExecStart = (
x:
let
# Split the launch string by spaces (TODO: take (ba)sh? formatting into account, i.e. quotes)
split = lib.splitString " " x;
# Get the first component of the launch string
first = builtins.elemAt split 0;
# Helper to seperate prefix operators from the first part of the Exec command
collectVood = (
s:
let
# Define the actual function internally to expose a more reasonable API, while supporting recursion
_collectVood = (
ss: s:
# If the current string begins with one of the prefix operator symbols
if (builtins.elem (builtins.substring 0 1 s) [ "@" "-" ":" "!" ]) then
# Recurse this function with the first character (the operator) and the remainder
_collectVood (ss + (builtins.substring 0 1 s)) (builtins.substring 1 (builtins.stringLength s) s)
else [ ss s ] # Return the current string and remainder
);
in
_collectVood "" s
);
# Use the helper above to seperate the prefix operators from the launch string
vood = collectVood first;
prefixParts = builtins.elemAt vood 0;
firstParts = builtins.elemAt vood 1;
# Make a string from the remainder of the ExecStart line (not including the first part, which was parsed)
remainder = builtins.concatStringsSep " " (lib.drop 1 split);
# Compile a simple program that will replace argv[0] with the 2nd provided arg
modFirstArg = pkgs.runCommandCC "mod-first-arg" { } "echo \"int main(int c,char*v[]){execvp(v[1],&v[2]);perror(v[1]);return 127;}\" > r.c;gcc -o $out r.c;strip $out";
in
if (builtins.elem "@" (lib.stringToCharacters prefixParts)) then
"${modFirstArg} ${firstParts} ${remainder}"
else # TODO: support more operators
"${firstParts} ${remainder}"
);
# Helper copied from the SystemD NixOS module (TODO: import it?)
shellEscape = s: (lib.replaceChars [ "\\" ] [ "\\\\" ] s);
makeJobScript = name: text:
let
mkScriptName = s: "unit-script-" + (lib.replaceChars [ "\\" "@" ] [ "-" "_" ] (shellEscape s));
in
pkgs.writeTextFile { name = mkScriptName name; executable = true; inherit text; };
makeServiceLaunchScript = (
services: serviceName:
let
service = services.${serviceName};
preStart =
if builtins.hasAttr "preStart" service then
makeJobScript "${serviceName}-pre-start" ''
#! ${pkgs.runtimeShell} -e
${service.preStart}
''
else if builtins.hasAttr "serviceConfig" service && builtins.hasAttr "ExecStartPre" service.serviceConfig then
service.serviceConfig.ExecStartPre
else
"";
start =
if builtins.hasAttr "script" service then
makeJobScript "${serviceName}-start" ''
#! ${pkgs.runtimeShell} -e
${service.script}
'' + " " + service.scriptArgs
else if builtins.hasAttr "serviceConfig" service && builtins.hasAttr "ExecStart" service.serviceConfig then
reparseExecStart service.serviceConfig.ExecStart
else
"";
env = service.environment // {
# Make a PATH variable for all the service's packages
PATH = lib.makeBinPath (
service.path ++ [
pkgs.coreutils
pkgs.findutils
pkgs.gnugrep
pkgs.gnused
]
);
};
# Convert the service's specified environment variables into an env-setting script snippet
envLines = map
(
k:
"${k}=${env.${k}}"
)
(builtins.attrNames env);
in
pkgs.writeScript "service-launch-${serviceName}" ''
#! ${pkgs.runtimeShell} -e
${builtins.concatStringsSep "\n" envLines}
${preStart}
exec ${start}
'' + (
if (service.serviceConfig.Type == "forking") then "\n" + ''
# This will retry reading the PIDFile until success
while true; do
export PID=$(${pkgs.coreutils}/bin/cat ${service.serviceConfig.PIDFile})
[ ! -z "$PID" ] && break
done
# This will end when the process exits
exec ${pkgs.coreutils}/bin/tail -f /proc/$PID/fd/1 /proc/$PID/fd/2 --pid=$PID
'' else ""
)
);
in
{
inherit sys cfg name env contents evaled;
entrypoint =
let
entrypointScriptContents =
if (rootEntrypointScript != null || userEntrypointScript != null || systemdService != null) then
''
#!${pkgs.dash}/bin/dash
# If we're lucky, Docker will make us a real one, otherwise lets just make it
${pkgs.coreutils}/bin/mkdir -p /tmp
# These octets make me cringe but I think this actually correct
${pkgs.coreutils}/bin/chmod 777 /tmp
${if user == null && entrypoint != null then "${pkgs.coreutils}/bin/chown -R continix:continix /data" else ""}
${if user == null && entrypoint != null then "${pkgs.coreutils}/bin/chmod -R guo+rwX /data" else ""}
${if rootEntrypointScript != null then "exec " + rootEntrypointScript else ""}
${if userEntrypointScript != null then "exec ${pkgs.gosu}/bin/gosu ${if user != null then user else "continix"} ${userEntrypointScript}" else ""}
'' + (
if systemdService != null then
let
# Gather the requirements for the specified service
requirements = getRequiredBy evaled.config.systemd.services systemdService;
# Filter down to only oneshot services
shottableRequirements = builtins.filter (s: s.serviceConfig.Type == "oneshot") requirements;
# Make a list of all the services which should be ran
allServices = (shottableRequirements ++ [ systemdService ]);
# Map a makeServiceLaunchScript function with applied services over the services that should be ran
serviceLaunchScripts = map (makeServiceLaunchScript evaled.config.systemd.services) allServices;
in
# Finally, the actual service entrypoint contents
builtins.concatStringsSep "\n" serviceLaunchScripts
else ""
)
else null;
in
# Only define an entrypoint if contents for it were provided
if entrypointScriptContents != null then pkgs.writeScript "entrypoint.sh" entrypointScriptContents else null;
}