summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVyacheslav Yurkov <uvv.mail@gmail.com>2025-03-01 22:27:52 +0100
committerRichard Purdie <richard.purdie@linuxfoundation.org>2025-03-09 20:10:06 +0000
commit03b8e2ec1e43c2ff69bfdb3339b329a6c95cc71c (patch)
tree60e6b036a372ed2910f0f6c7232564e6d502c574
parent452217a6796b7ebc29d4f5aeca94aacdc99d0fc9 (diff)
downloadpoky-03b8e2ec1e43c2ff69bfdb3339b329a6c95cc71c.tar.gz
systemd: Build the systemctl executable
Instead of the python re-implementation build the actual systemctl from the systemd source tree. The python script was used when systemd didn't provide an option to build individual executables. It is possible in the meantime, so instead of always adapting the script when there's a new functionality, we simply use upstream implementation. License-Update: Base recipe is used (From OE-Core rev: 7a580800db391891a3a0f838c4ae6e1513c710a2) Signed-off-by: Vyacheslav Yurkov <uvv.mail@gmail.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
-rw-r--r--meta/recipes-core/systemd/systemd-systemctl-native.bb23
-rwxr-xr-xmeta/recipes-core/systemd/systemd-systemctl/systemctl376
2 files changed, 11 insertions, 388 deletions
diff --git a/meta/recipes-core/systemd/systemd-systemctl-native.bb b/meta/recipes-core/systemd/systemd-systemctl-native.bb
index ffa024caef..73862b4e23 100644
--- a/meta/recipes-core/systemd/systemd-systemctl-native.bb
+++ b/meta/recipes-core/systemd/systemd-systemctl-native.bb
@@ -1,17 +1,16 @@
1SUMMARY = "Wrapper for enabling systemd services" 1SUMMARY = "Systemctl executable from systemd"
2 2
3LICENSE = "MIT" 3require systemd.inc
4LIC_FILES_CHKSUM = "file://${COREBASE}/meta/COPYING.MIT;md5=3da9cfbcb788c80a0384361b4de20420"
5 4
5DEPENDS = "gperf-native libcap-native util-linux-native python3-jinja2-native"
6 6
7inherit native 7inherit pkgconfig meson native
8 8
9SRC_URI = "file://systemctl" 9MESON_TARGET = "systemctl:executable"
10MESON_INSTALL_TAGS = "systemctl"
11EXTRA_OEMESON:append = " -Dlink-systemctl-shared=false"
10 12
11S = "${WORKDIR}/sources" 13# Systemctl is supposed to operate on target, but the target sysroot is not
12UNPACKDIR = "${S}" 14# determined at run-time, but rather set during configure
13 15# More details are here https://github.com/systemd/systemd/issues/35897#issuecomment-2665405887
14do_install() { 16EXTRA_OEMESON:append = " --sysconfdir ${sysconfdir_native}"
15 install -d ${D}${bindir}
16 install -m 0755 ${S}/systemctl ${D}${bindir}
17}
diff --git a/meta/recipes-core/systemd/systemd-systemctl/systemctl b/meta/recipes-core/systemd/systemd-systemctl/systemctl
deleted file mode 100755
index 65e3157859..0000000000
--- a/meta/recipes-core/systemd/systemd-systemctl/systemctl
+++ /dev/null
@@ -1,376 +0,0 @@
1#!/usr/bin/env python3
2"""systemctl: subset of systemctl used for image construction
3
4Mask/preset systemd units
5"""
6
7import argparse
8import fnmatch
9import os
10import re
11import sys
12
13from collections import namedtuple
14from itertools import chain
15from pathlib import Path
16
17version = 1.0
18
19ROOT = Path("/")
20SYSCONFDIR = Path("etc")
21BASE_LIBDIR = Path("lib")
22LIBDIR = Path("usr", "lib")
23
24locations = list()
25
26
27class SystemdFile():
28 """Class representing a single systemd configuration file"""
29
30 _clearable_keys = ['WantedBy']
31
32 def __init__(self, root, path, instance_unit_name, unit_type):
33 self.sections = dict()
34 self._parse(root, path)
35 dirname = os.path.basename(path.name) + ".d"
36 for location in locations:
37 files = (root / location / unit_type / dirname).glob("*.conf")
38 if instance_unit_name:
39 inst_dirname = instance_unit_name + ".d"
40 files = chain(files, (root / location / unit_type / inst_dirname).glob("*.conf"))
41 for path2 in sorted(files):
42 self._parse(root, path2)
43
44 def _parse(self, root, path):
45 """Parse a systemd syntax configuration file
46
47 Args:
48 path: A pathlib.Path object pointing to the file
49
50 """
51 skip_re = re.compile(r"^\s*([#;]|$)")
52 section_re = re.compile(r"^\s*\[(?P<section>.*)\]")
53 kv_re = re.compile(r"^\s*(?P<key>[^\s]+)\s*=\s*(?P<value>.*)")
54 section = None
55
56 if path.is_symlink():
57 try:
58 path.resolve()
59 except FileNotFoundError:
60 # broken symlink, try relative to root
61 path = root / Path(os.readlink(str(path))).relative_to(ROOT)
62
63 with path.open() as f:
64 for line in f:
65 if skip_re.match(line):
66 continue
67
68 line = line.strip()
69 m = section_re.match(line)
70 if m:
71 if m.group('section') not in self.sections:
72 section = dict()
73 self.sections[m.group('section')] = section
74 else:
75 section = self.sections[m.group('section')]
76 continue
77
78 while line.endswith("\\"):
79 line += f.readline().rstrip("\n")
80
81 m = kv_re.match(line)
82 k = m.group('key')
83 v = m.group('value')
84 if k not in section:
85 section[k] = list()
86
87 # If we come across a "key=" line for a "clearable key", then
88 # forget all preceding assignments. This works because we are
89 # processing files in correct parse order.
90 if k in self._clearable_keys and not v:
91 del section[k]
92 continue
93
94 section[k].extend(v.split())
95
96 def get(self, section, prop):
97 """Get a property from section
98
99 Args:
100 section: Section to retrieve property from
101 prop: Property to retrieve
102
103 Returns:
104 List representing all properties of type prop in section.
105
106 Raises:
107 KeyError: if ``section`` or ``prop`` not found
108 """
109 return self.sections[section][prop]
110
111
112class Presets():
113 """Class representing all systemd presets"""
114 def __init__(self, scope, root):
115 self.directives = list()
116 self._collect_presets(scope, root)
117
118 def _parse_presets(self, presets):
119 """Parse presets out of a set of preset files"""
120 skip_re = re.compile(r"^\s*([#;]|$)")
121 directive_re = re.compile(r"^\s*(?P<action>enable|disable)\s+(?P<unit_name>(.+))")
122
123 Directive = namedtuple("Directive", "action unit_name")
124 for preset in presets:
125 with preset.open() as f:
126 for line in f:
127 m = directive_re.match(line)
128 if m:
129 directive = Directive(action=m.group('action'),
130 unit_name=m.group('unit_name'))
131 self.directives.append(directive)
132 elif skip_re.match(line):
133 pass
134 else:
135 sys.exit("Unparsed preset line in {}".format(preset))
136
137 def _collect_presets(self, scope, root):
138 """Collect list of preset files"""
139 presets = dict()
140 for location in locations:
141 paths = (root / location / scope).glob("*.preset")
142 for path in paths:
143 # earlier names override later ones
144 if path.name not in presets:
145 presets[path.name] = path
146
147 self._parse_presets([v for k, v in sorted(presets.items())])
148
149 def state(self, unit_name):
150 """Return state of preset for unit_name
151
152 Args:
153 presets: set of presets
154 unit_name: name of the unit
155
156 Returns:
157 None: no matching preset
158 `enable`: unit_name is enabled
159 `disable`: unit_name is disabled
160 """
161 for directive in self.directives:
162 if fnmatch.fnmatch(unit_name, directive.unit_name):
163 return directive.action
164
165 return None
166
167
168def add_link(path, target):
169 try:
170 path.parent.mkdir(parents=True)
171 except FileExistsError:
172 pass
173 if not path.is_symlink():
174 print("ln -s {} {}".format(target, path))
175 path.symlink_to(target)
176
177
178class SystemdUnitNotFoundError(Exception):
179 def __init__(self, path, unit):
180 self.path = path
181 self.unit = unit
182
183
184class SystemdUnit():
185 def __init__(self, root, unit, unit_type):
186 self.root = root
187 self.unit = unit
188 self.unit_type = unit_type
189 self.config = None
190
191 def _path_for_unit(self, unit):
192 for location in locations:
193 path = self.root / location / self.unit_type / unit
194 if path.exists() or path.is_symlink():
195 return path
196
197 raise SystemdUnitNotFoundError(self.root, unit)
198
199 def _process_deps(self, config, service, location, prop, dirstem, instance):
200 systemdir = self.root / SYSCONFDIR / "systemd" / self.unit_type
201
202 target = ROOT / location.relative_to(self.root)
203 try:
204 for dependent in config.get('Install', prop):
205 # expand any %i to instance (ignoring escape sequence %%)
206 dependent = re.sub("([^%](%%)*)%i", "\\g<1>{}".format(instance), dependent)
207 wants = systemdir / "{}.{}".format(dependent, dirstem) / service
208 add_link(wants, target)
209
210 except KeyError:
211 pass
212
213 def enable(self, units_enabled=[]):
214 # if we're enabling an instance, first extract the actual instance
215 # then figure out what the template unit is
216 template = re.match(r"[^@]+@(?P<instance>[^\.]*)\.", self.unit)
217 instance_unit_name = None
218 if template:
219 instance = template.group('instance')
220 if instance != "":
221 instance_unit_name = self.unit
222 unit = re.sub(r"@[^\.]*\.", "@.", self.unit, 1)
223 else:
224 instance = None
225 unit = self.unit
226
227 if instance_unit_name is not None:
228 try:
229 # Try first with instance unit name. Systemd allows to create instance unit files
230 # e.g. `gnome-shell@wayland.service` which cause template unit file to be ignored
231 # for the instance for which instance unit file is present. In that case, there may
232 # not be any template file at all.
233 path = self._path_for_unit(instance_unit_name)
234 except SystemdUnitNotFoundError:
235 path = self._path_for_unit(unit)
236 else:
237 path = self._path_for_unit(unit)
238
239 if path.is_symlink():
240 # ignore aliases
241 return
242
243 config = SystemdFile(self.root, path, instance_unit_name, self.unit_type)
244 if instance == "":
245 try:
246 default_instance = config.get('Install', 'DefaultInstance')[0]
247 except KeyError:
248 # no default instance, so nothing to enable
249 return
250
251 service = self.unit.replace("@.",
252 "@{}.".format(default_instance))
253 else:
254 service = self.unit
255
256 self._process_deps(config, service, path, 'WantedBy', 'wants', instance)
257 self._process_deps(config, service, path, 'RequiredBy', 'requires', instance)
258
259 try:
260 for also in config.get('Install', 'Also'):
261 try:
262 units_enabled.append(unit)
263 if also not in units_enabled:
264 SystemdUnit(self.root, also, self.unit_type).enable(units_enabled)
265 except SystemdUnitNotFoundError as e:
266 sys.exit("Error: Systemctl also enable issue with %s (%s)" % (service, e.unit))
267
268 except KeyError:
269 pass
270
271 systemdir = self.root / SYSCONFDIR / "systemd" / self.unit_type
272 target = ROOT / path.relative_to(self.root)
273 try:
274 for dest in config.get('Install', 'Alias'):
275 alias = systemdir / dest
276 add_link(alias, target)
277
278 except KeyError:
279 pass
280
281 def mask(self):
282 systemdir = self.root / SYSCONFDIR / "systemd" / self.unit_type
283 add_link(systemdir / self.unit, "/dev/null")
284
285
286def collect_services(root, unit_type):
287 """Collect list of service files"""
288 services = set()
289 for location in locations:
290 paths = (root / location / unit_type).glob("*")
291 for path in paths:
292 if path.is_dir():
293 continue
294 services.add(path.name)
295
296 return services
297
298
299def preset_all(root, unit_type):
300 presets = Presets('{}-preset'.format(unit_type), root)
301 services = collect_services(root, unit_type)
302
303 for service in services:
304 state = presets.state(service)
305
306 if state == "enable" or state is None:
307 try:
308 SystemdUnit(root, service, unit_type).enable()
309 except SystemdUnitNotFoundError:
310 sys.exit("Error: Systemctl preset_all issue in %s" % service)
311
312 # If we populate the systemd links we also create /etc/machine-id, which
313 # allows systemd to boot with the filesystem read-only before generating
314 # a real value and then committing it back.
315 #
316 # For the stateless configuration, where /etc is generated at runtime
317 # (for example on a tmpfs), this script shouldn't run at all and we
318 # allow systemd to completely populate /etc.
319 (root / SYSCONFDIR / "machine-id").touch()
320
321
322def main():
323 if sys.version_info < (3, 4, 0):
324 sys.exit("Python 3.4 or greater is required")
325
326 parser = argparse.ArgumentParser()
327 parser.add_argument('command', nargs='?', choices=['enable', 'mask',
328 'preset-all'])
329 parser.add_argument('service', nargs=argparse.REMAINDER)
330 parser.add_argument('--root')
331 parser.add_argument('--preset-mode',
332 choices=['full', 'enable-only', 'disable-only'],
333 default='full')
334 parser.add_argument('--global', dest="opt_global", action="store_true", default=False)
335
336 args = parser.parse_args()
337
338 root = Path(args.root) if args.root else ROOT
339
340 locations.append(SYSCONFDIR / "systemd")
341 # Handle the usrmerge case by ignoring /lib when it's a symlink
342 if not (root / BASE_LIBDIR).is_symlink():
343 locations.append(BASE_LIBDIR / "systemd")
344 locations.append(LIBDIR / "systemd")
345
346 command = args.command
347 if not command:
348 parser.print_help()
349 return 0
350
351 unit_type = "user" if args.opt_global else "system"
352
353 if command == "mask":
354 for service in args.service:
355 try:
356 SystemdUnit(root, service, unit_type).mask()
357 except SystemdUnitNotFoundError as e:
358 sys.exit("Error: Systemctl main mask issue in %s (%s)" % (service, e.unit))
359 elif command == "enable":
360 for service in args.service:
361 try:
362 SystemdUnit(root, service, unit_type).enable()
363 except SystemdUnitNotFoundError as e:
364 sys.exit("Error: Systemctl main enable issue in %s (%s)" % (service, e.unit))
365 elif command == "preset-all":
366 if len(args.service) != 0:
367 sys.exit("Too many arguments.")
368 if args.preset_mode != "enable-only":
369 sys.exit("Only enable-only is supported as preset-mode.")
370 preset_all(root, unit_type)
371 else:
372 raise RuntimeError()
373
374
375if __name__ == '__main__':
376 main()