From: Zbigniew Jędrzejewski-Szmek Date: Mon, 22 Jun 2026 17:04:04 +0000 (+0200) Subject: core: embed essential units as built-in fallbacks X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=a0146fe807117c801e9f4a978b30d933dfed187f;p=thirdparty%2Fsystemd.git core: embed essential units as built-in fallbacks 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. --- diff --git a/TODO.md b/TODO.md index 8ef085f693e..5537ba86aa3 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 423f9218344..a5572301db0 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 00000000000..6850a9e59bf --- /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 ce4517e85bd..c9e270a6682 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 2d3ed4cff2a..26c84fa78b2 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 60ce3bf0cad..d61138bf7b2 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('/')