]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
core: embed essential units as built-in fallbacks
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Mon, 22 Jun 2026 17:04:04 +0000 (19:04 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Thu, 2 Jul 2026 15:19:18 +0000 (17:19 +0200)
Compile the contents of a few essential target unit files
(graphical.target, multi-user.target, rescue.target, etc.) into the
manager binary, with comments stripped, and fall back to those built-in
copies when no fragment is found on disk. This lets the manager reach a
usable state even on a system that ships none of these unit files, e.g.
in minimal environments or one-off chroots.

On-disk files always take precedence (masks included), as the fallback is
only consulted when nothing is found in the lookup paths. The unit file
list is defined in units/meson.build and embedded via a small generator
script; 'units' is now processed before 'src/core' so the list is
available there.

The set of units and services is selected that should allow for a normal
operation of starting/stopping/restarting, of the machine. The services
all use SuccessAction=…, so they don't require any binaries to be
installed.

sigpwr.target is included, even though it doesn't do anything useful.
The same is true in normal installations. We might want to change it
do something useful there too.

TODO.md
meson.build
src/core/generate-builtin-units.py [new file with mode: 0755]
src/core/load-fragment.c
src/core/meson.build
units/meson.build

diff --git a/TODO.md b/TODO.md
index 8ef085f693e40af6217fe6f3d88ef8666025d08b..5537ba86aa358825b26f2eb0c2bd77e7522edc69 100644 (file)
--- a/TODO.md
+++ b/TODO.md
@@ -2436,6 +2436,12 @@ SPDX-License-Identifier: LGPL-2.1-or-later
     operate in device mode
   - add NVMe authentication
 
+- **sigpwr.target** doesn't do anything useful. Consider hooking it up to
+  poweroff.target.
+
+- Provide a fallback in **rescue.service** that prints a fixed message
+  if sulogin-shell could not be started.
+
 - support boot into nvme-over-tcp: add generator that allows specifying nvme
   devices on kernel cmdline + credentials. Also maybe add interactive mode
   (where the user is prompted for nvme info), in order to boot from other
index 423f92183442eb072204a9554d1f1ffcc93118a2..a5572301db03bb956398c59b765b16ef900ca136 100644 (file)
@@ -1975,6 +1975,9 @@ module_additional_kwargs = {
 
 #####################################################################
 
+# 'src/core' embeds a built-in copy of some unit files, so the unit file list
+# from 'units' must be defined first.
+subdir('units')
 # systemd-analyze requires 'libcore'
 subdir('src/core')
 # systemd-networkd requires 'libsystemd_network'
@@ -2466,7 +2469,6 @@ subdir('shell-completion/zsh')
 subdir('sysctl.d')
 subdir('sysusers.d')
 subdir('tmpfiles.d')
-subdir('units')
 
 install_subdir('factory/etc',
                install_dir : factorydir)
diff --git a/src/core/generate-builtin-units.py b/src/core/generate-builtin-units.py
new file mode 100755 (executable)
index 0000000..6850a9e
--- /dev/null
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+# Embed the contents of unit files passed on the command line into a C array
+# initializer, so that the manager can fall back to a built-in copy when no
+# unit file is found on disk. Comment and empty lines are stripped to keep the
+# embedded strings small. The unit name is derived from each file's basename.
+
+import pathlib
+import string
+import sys
+
+PRINTABLE = set(string.printable) - set('\t\r\x0b\x0c')
+
+
+def strip_comments(text: str) -> str:
+    # Unit files use '#' and ';' as comment markers at the start of a line.
+    # fmt: off
+    lines = (line for line in text.splitlines()
+             if line and line[0] not in '#;')
+    return '\n'.join(lines) + '\n'
+
+
+def c_escape(text: str) -> str:
+    assert set(text) <= PRINTABLE
+    return text.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n')
+
+
+for path in sys.argv[1:]:
+    path = pathlib.Path(path)
+    data = strip_comments(path.read_text())
+    print(f'{{ "{path.name}", "{c_escape(data)}" }},')
index ce4517e85bd2950b2fb946ab9bd6515559b03cca..c9e270a6682faaa5f1cc3abb5bee5efea0cbc113 100644 (file)
@@ -30,6 +30,7 @@
 #include "execute.h"
 #include "extract-word.h"
 #include "fd-util.h"
+#include "fileio.h"
 #include "fstab-util.h"
 #include "hashmap.h"
 #include "hexdecoct.h"
 #include "user-util.h"
 #include "web-util.h"
 
+/* Built-in copies of a few essential unit files, embedded at build time. They are used as a fallback
+ * when no fragment for the unit is found on disk, so that the manager can reach a usable state even on
+ * a system that ships none of these unit files. */
+static const struct {
+        const char *name;
+        const char *data;
+} builtin_units[] = {
+#  include "builtin-units.inc"
+};
+
 static int parse_socket_protocol(const char *s) {
         int r;
 
@@ -6171,6 +6182,16 @@ static int merge_by_names(Unit *u, Set *names, const char *id) {
         return 0;
 }
 
+static const char* builtin_unit_lookup(const char *name) {
+        assert(name);
+
+        FOREACH_ELEMENT(i, builtin_units)
+                if (streq(i->name, name))
+                        return i->data;
+
+        return NULL;
+}
+
 int unit_load_fragment(Unit *u) {
         int r;
 
@@ -6252,14 +6273,44 @@ int unit_load_fragment(Unit *u) {
                         r = config_parse(u->id, fragment, f,
                                          UNIT_VTABLE(u)->sections,
                                          config_item_perf_lookup, load_fragment_gperf_lookup,
-                                         0,
-                                         u,
-                                         NULL);
+                                         /* flags= */ 0,
+                                         /* userdata= */ u,
+                                         /* ret_stat= */ NULL);
                         if (r == -ENOEXEC)
                                 log_unit_notice_errno(u, r, "Unit configuration has fatal error, unit will not be started.");
                         if (r < 0)
                                 return r;
                 }
+        } else if (u->manager->runtime_scope == RUNTIME_SCOPE_SYSTEM) {
+                /* No fragment found on disk. For system units, fall back to a built-in copy if we have one
+                 * embedded. This way the manager can reach a usable state even if none of these unit files
+                 * are installed. On-disk files always take precedence (including masks), since we only get
+                 * here when nothing was found in the lookup paths. */
+
+                const char *data = builtin_unit_lookup(u->id);
+                if (data) {
+                        _cleanup_fclose_ FILE *f = NULL;
+
+                        f = fmemopen_unlocked((void*) data, strlen(data), "re");
+                        if (!f)
+                                return log_oom();
+
+                        log_unit_debug(u, "Loading built-in fragment for %s.", u->id);
+
+                        u->load_state = UNIT_LOADED;
+                        u->fragment_mtime = 0;
+
+                        r = config_parse(u->id, u->id, f,
+                                         UNIT_VTABLE(u)->sections,
+                                         config_item_perf_lookup, load_fragment_gperf_lookup,
+                                         /* flags= */ 0,
+                                         /* userdata= */ u,
+                                         /* ret_stat= */ NULL);
+                        if (r == -ENOEXEC)
+                                log_unit_notice_errno(u, r, "Built-in unit configuration has fatal error, unit will not be started.");
+                        if (r < 0)
+                                return r;
+                }
         }
 
         /* Call merge_by_names with the name derived from the fragment path as the preferred name.
index 2d3ed4cff2a6ad2af7be9e105d4cba53e06abf33..26c84fa78b2f282e8cbe2605c9af914f8f2b5f6d 100644 (file)
@@ -135,6 +135,13 @@ load_fragment_gperf_nulstr_c = custom_target(
         command : [awk, '-f', '@INPUT0@', '@INPUT1@'],
         capture : true)
 
+generate_builtin_units = files('generate-builtin-units.py')
+builtin_units_inc = custom_target(
+        input : [generate_builtin_units, embedded_units],
+        output : 'builtin-units.inc',
+        command : [python, generate_builtin_units, embedded_units],
+        capture : true)
+
 generate_bpf_delegate_configs = files('generate-bpf-delegate-configs.py')
 bpf_delegate_configs_inc = custom_target(
         input : [generate_bpf_delegate_configs, bpf_delegate_sources],
@@ -155,8 +162,8 @@ bpf_delegate_xml = custom_target(
         capture : true)
 man_page_depends += bpf_delegate_xml
 
-generated_sources += [load_fragment_gperf_c, load_fragment_gperf_nulstr_c, bpf_delegate_configs_inc]
-libcore_sources += [load_fragment_gperf_c, load_fragment_gperf_nulstr_c, bpf_delegate_configs_inc]
+generated_sources += [load_fragment_gperf_c, load_fragment_gperf_nulstr_c, bpf_delegate_configs_inc, builtin_units_inc]
+libcore_sources += [load_fragment_gperf_c, load_fragment_gperf_nulstr_c, bpf_delegate_configs_inc, builtin_units_inc]
 libcore_build_dir = meson.current_build_dir()
 libcore_name = 'systemd-core-@0@'.format(shared_lib_tag)
 core_includes = [include_directories('.'), includes]
index 60ce3bf0cadab10aa3417dff56cc722b10d65bf6..d61138bf7b2ba5e3e578ba24215ea59f4f612057 100644 (file)
@@ -1070,6 +1070,33 @@ units = [
 udev_units = []
 hwdb_units = []
 
+# Unit files whose contents are embedded into the manager binary, so that it can
+# fall back to a built-in copy when the file is not present on disk. Referenced
+# from src/core/meson.build.
+embedded_unit_names = [
+        'basic.target',
+        'exit.target',
+        'final.target',
+        'graphical.target',
+        'halt.target',
+        'kexec.target',
+        'multi-user.target',
+        'poweroff.target',
+        'reboot.target',
+        'shutdown.target',
+        'sigpwr.target',
+        'soft-reboot.target',
+        'sysinit.target',
+        'systemd-exit.service',
+        'systemd-halt.service',
+        'systemd-kexec.service',
+        'systemd-poweroff.service',
+        'systemd-reboot.service',
+        'systemd-soft-reboot.service',
+        'umount.target',
+]
+embedded_units = []
+
 foreach unit : units
         source = unit.get('file')
 
@@ -1092,7 +1119,7 @@ foreach unit : units
         endforeach
 
         if needs_jinja
-                t = custom_target(
+                processed = custom_target(
                         input : source,
                         output : name,
                         command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'],
@@ -1100,9 +1127,9 @@ foreach unit : units
                         install_dir : systemunitdir,
                         install_tag : unit.get('install_tag', ''))
                 if unit.get('install_tag', '') == 'udev'
-                        udev_units += t
+                        udev_units += processed
                 elif unit.get('install_tag', '') == 'hwdb'
-                        hwdb_units += t
+                        hwdb_units += processed
                 endif
         elif install
                 install_data(source,
@@ -1110,6 +1137,11 @@ foreach unit : units
                              install_tag : unit.get('install_tag', ''))
         endif
 
+        if name in embedded_unit_names
+                # Embed either the processed output or the original file.
+                embedded_units += needs_jinja ? processed : source
+        endif
+
         if install
                 foreach target : unit.get('symlinks', [])
                         if target.endswith('/')