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('/')