From a0146fe807117c801e9f4a978b30d933dfed187f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 22 Jun 2026 19:04:04 +0200 Subject: [PATCH] core: embed essential units as built-in fallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 | 6 ++++ meson.build | 4 ++- src/core/generate-builtin-units.py | 33 +++++++++++++++++ src/core/load-fragment.c | 57 ++++++++++++++++++++++++++++-- src/core/meson.build | 11 ++++-- units/meson.build | 38 ++++++++++++++++++-- 6 files changed, 140 insertions(+), 9 deletions(-) create mode 100755 src/core/generate-builtin-units.py diff --git a/TODO.md b/TODO.md index 8ef085f693..5537ba86aa 100644 --- 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 diff --git a/meson.build b/meson.build index 423f921834..a5572301db 100644 --- a/meson.build +++ b/meson.build @@ -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 index 0000000000..6850a9e59b --- /dev/null +++ b/src/core/generate-builtin-units.py @@ -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)}" }},') diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index ce4517e85b..c9e270a668 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -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" @@ -73,6 +74,16 @@ #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. diff --git a/src/core/meson.build b/src/core/meson.build index 2d3ed4cff2..26c84fa78b 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -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] diff --git a/units/meson.build b/units/meson.build index 60ce3bf0ca..d61138bf7b 100644 --- a/units/meson.build +++ b/units/meson.build @@ -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('/')