--- /dev/null
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="systemd-sysinstall" conditional='ENABLE_SYSINSTALL'
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+ <refentryinfo>
+ <title>systemd-sysinstall</title>
+ <productname>systemd</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>systemd-sysinstall</refentrytitle>
+ <manvolnum>8</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>systemd-sysinstall</refname>
+ <refname>systemd-sysinstall.service</refname>
+ <refpurpose>Simple OS installer</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>systemd-sysinstall</command>
+ <arg choice="opt" rep="repeat">OPTIONS</arg>
+ <arg choice="opt">BLOCKDEVICE</arg>
+ </cmdsynopsis>
+
+ <para><filename>systemd-sysinstall.service</filename></para>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para><command>systemd-sysinstall</command> is a simple terminal and command line based operating system
+ installer tool. Its primary use-case is to act as an automatically started interactive interface when
+ booting from an installer medium (e.g. a USB stick), in order to install an OS onto a target
+ disk. However, it may also be invoked directly from a shell. It executes the following steps:</para>
+
+ <orderedlist>
+ <listitem><para>It prompts the user for the target disk to install the OS on. (Unless the block device
+ is already specified on the command line.)</para></listitem>
+
+ <listitem><para>It validates whether the disk is suitable (i.e. large enough, and with enough
+ free/unpartitioned space) for an OS installation. If it is generally suitable the user is prompted if they
+ want to erase the disk before installation, or if the OS shall be added to the existing partitions on
+ the disk (the latter only if enough free/unpartitioned disk space is available).</para></listitem>
+
+ <listitem><para>It prompts the user whether to register the newly installed OS with the firmware boot option menu.</para></listitem>
+
+ <listitem><para>It requests confirmation from the user, after showing a summary of the planned OS installation.</para></listitem>
+
+ <listitem><para>It invokes
+ <citerefentry><refentrytitle>systemd-creds</refentrytitle><manvolnum>1</manvolnum></citerefentry>'s
+ <command>encrypt</command> command in order to generate encrypted (TPM locked, if available) system
+ credential files for a few, very basic system settings of the currently booted system (locale, keymap,
+ timezone), which it will install on the target disk, parameterizing the invoked kernel. (Or in other
+ words, it prepares that some settings already in effect on the installer system are propagated securely
+ onto the new installation.)</para></listitem>
+
+ <listitem><para>It invokes
+ <citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry> with
+ a definitions directory of <filename>/usr/lib/repart.sysinstall.d/</filename> (only if populated – if
+ not will use the default of <filename>/usr/lib/repart.d/</filename>). This is supposed to set up the
+ basic OS partition structure on the target disk and copies in basic OS partitions (most importantly the
+ <filename>/usr/</filename> hierarchy).</para></listitem>
+
+ <listitem><para>It invokes
+ <citerefentry><refentrytitle>bootctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>'s
+ <command>link</command> command to install an OS kernel image onto the target disk's ESP/XBOOTLDR,
+ together with the credential files prepared earlier.</para></listitem>
+
+ <listitem><para>It invokes
+ <citerefentry><refentrytitle>bootctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>'s
+ <command>install</command> command to install the
+ <citerefentry><refentrytitle>systemd-boot</refentrytitle><manvolnum>7</manvolnum></citerefentry> boot
+ loader onto the target disk's ESP.</para></listitem>
+
+ <listitem><para>After confirmation, it reboots the system.</para></listitem>
+ </orderedlist>
+
+ <para>Note that the prompts/confirmation may be disabled via the command line, enabling fully automatic,
+ non-interactive installation. See below.</para>
+
+ <para>Note this tool does not interactively query the user for a user to create or a root password to be
+ set on the target system, under the assumption these questions are better prompted from within the newly
+ installed system's first boot process, for example via the
+ <citerefentry><refentrytitle>systemd-firstboot</refentrytitle><manvolnum>1</manvolnum></citerefentry> or
+ <filename>systemd-homed-firstboot.service</filename> components. Note that if required such settings
+ may be propagated explicitly via the <option>--load-credential=</option> switch below.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>Options</title>
+
+ <para>The following options are understood:</para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><option>--definitions=</option></term>
+
+ <listitem><para>Overrides the directory where <command>systemd-repart</command> shall read its
+ partition definitions from, in place of the default of
+ <filename>/usr/lib/repart.sysinstall.d/</filename>.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--welcome=</option></term>
+
+ <listitem><para>Takes a boolean argument. Controls whether to show the brief welcome text normally
+ displayed at the beginning of the installation. Defaults to true.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--chrome=</option></term>
+
+ <listitem><para>Takes a boolean argument. Controls whether to show the colored bars at the top and
+ bottom of the terminal interface. Defaults to true.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--erase=</option></term>
+
+ <listitem><para>Takes a boolean argument. Controls whether to erase the current contents of the
+ target disk. If this switch is not used the user is prompted.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--confirm=</option></term>
+
+ <listitem><para>Takes a boolean argument. Controls whether to interactively query the user for
+ confirmation before initiating the OS installation. Defaults to true.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--reboot=</option></term>
+
+ <listitem><para>Takes a boolean argument. Controls whether to reboot the system after completing the
+ installation. Defaults to false.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--variables=</option></term>
+
+ <listitem><para>Takes a boolean argument. Controls whether to register the installed boot loader in
+ the firmware's boot options database. If not specified the user will be prompted.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--summary=</option></term>
+
+ <listitem><para>Takes a boolean argument. Controls whether to show a summary of the choices made
+ before asking for confirmation to proceed with the OS installation. Defaults to true.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--kernel=</option></term>
+
+ <listitem><para>Takes a path to a unified kernel image (UKI). Explicitly selects the kernel image to
+ install on the target disk. If unspecified the currently booted kernel image is installed on the
+ target disk.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--set-credential=<replaceable>id</replaceable>:<replaceable>value</replaceable></option></term>
+
+ <listitem><para>Accepts an additional system credential to encrypt (with a key generated on the local
+ TPM, if available, and the null key otherwise) and place next to the installed kernel image in the
+ ESP. This may be used to parameterize the installed kernel with arbitrary system credentials. Do not
+ use this switch for sensitive data (such as passwords), use <option>--load-credential=</option>
+ instead, see below. May be used multiple times to configure multiple credentials.</para>
+
+ <para>Note that three system credentials are propagated in similar fashion to the target system:
+ the locale, keymap and timezone. This may be controlled by the relevant
+ <option>--copy-locale=</option>, <option>--copy-keymap=</option> and <option>--copy-timezone=</option>
+ options below.</para>
+
+ <para>See
+ <citerefentry><refentrytitle>systemd.system-credentials</refentrytitle><manvolnum>7</manvolnum></citerefentry>
+ for a list of well-known system credentials that may be propagated this way. (Note that you may pass
+ arbitrary additional credentials this way, that can be consumed by any service of your
+ choice, via the usual system credentials logic.)</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--load-credential=<replaceable>id</replaceable>:<replaceable>path</replaceable></option></term>
+
+ <listitem><para>Similar to <option>--set-credential=</option> but reads the credential value from a
+ file on disk or an <constant>AF_UNIX</constant> socket in the file system. This is generally
+ preferable for sensitive data, such as passwords.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--copy-locale=</option></term>
+ <term><option>--copy-keymap=</option></term>
+ <term><option>--copy-timezone=</option></term>
+
+ <listitem><para>These options take boolean parameters. They control whether the indicated system
+ settings shall be propagated from the currently running system into the new target OS
+ installation. These options default to true.</para>
+
+ <para>Typically, these three settings are the minimal settings that need to be configured during early
+ boot of an installer medium in order to make the installer tool accessible to the user. The
+ <citerefentry><refentrytitle>systemd-firstboot</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+ tool may be used to query the user interactively when the OS install medium is booted for these
+ properties. By propagating these settings to the target installation via system credentials they do
+ not need to be queried again on first boot of the new installation.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--mute-console=</option></term>
+
+ <listitem><para>Takes a boolean argument. Controls whether to disable kernel and service manager log
+ output to the console the installer is invoked on temporarily while running, in order to avoid
+ interleaved output. Defaults to false.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
+ <xi:include href="standard-options.xml" xpointer="help" />
+ <xi:include href="standard-options.xml" xpointer="version" />
+ </variablelist>
+ </refsect1>
+
+ <refsect1>
+ <title>Exit status</title>
+
+ <para>On success, 0 is returned, and a non-zero failure code otherwise.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>Example</title>
+
+ <example>
+ <title>Invoke the tool for a fully automatic non-interactive OS installation</title>
+
+ <programlisting>systemd-sysinstall \
+ /dev/disk/by-id/nvme-Micron_MTFDKBA1T0TFH_214532D0CDA5 \
+ --erase=yes \
+ --confirm=no \
+ --variables=yes \
+ --load-credential=ssh.authorized_keys.root:my-ssh-key
+ </programlisting>
+
+ <para>This installs the OS on the selected disk, erasing any previous contents, without confirmation,
+ registers it in the firmware, and drops in the SSH key for the root user, read from the
+ <filename>my-ssh-key</filename> file in the current directory.</para>
+ </example>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+ <para><simplelist type="inline">
+ <member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>systemd-creds</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>bootctl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>systemd-firstboot</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>systemd-boot</refentrytitle><manvolnum>7</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>systemd.system-credentials</refentrytitle><manvolnum>7</manvolnum></citerefentry></member>
+ </simplelist></para>
+ </refsect1>
+
+</refentry>
--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <locale.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "sd-varlink.h"
+
+#include "alloc-util.h"
+#include "ansi-color.h"
+#include "blockdev-list.h"
+#include "build.h"
+#include "build-path.h"
+#include "chase.h"
+#include "conf-files.h"
+#include "constants.h"
+#include "efi-loader.h"
+#include "efivars.h"
+#include "env-file.h"
+#include "escape.h"
+#include "fd-util.h"
+#include "find-esp.h"
+#include "format-table.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "glyph-util.h"
+#include "help-util.h"
+#include "image-policy.h"
+#include "json-util.h"
+#include "locale-setup.h"
+#include "log.h"
+#include "loop-util.h"
+#include "machine-credential.h"
+#include "main-func.h"
+#include "mount-util.h"
+#include "options.h"
+#include "os-util.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "prompt-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "varlink-util.h"
+
+static char *arg_node = NULL;
+static bool arg_welcome = true;
+static int arg_erase = -1; /* tri-state */
+static bool arg_confirm = true;
+static bool arg_summary = true;
+static char **arg_definitions = NULL;
+static char *arg_kernel_image = NULL;
+static bool arg_reboot = false;
+static int arg_touch_variables = -1; /* tri-state */
+static MachineCredentialContext arg_credentials = {};
+static bool arg_copy_locale = true;
+static bool arg_copy_keymap = true;
+static bool arg_copy_timezone = true;
+static bool arg_chrome = true;
+static bool arg_mute_console = false;
+
+STATIC_DESTRUCTOR_REGISTER(arg_node, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_definitions, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_kernel_image, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_credentials, machine_credential_context_done);
+
+static int help(void) {
+ int r;
+
+ _cleanup_(table_unrefp) Table *options = NULL;
+ r = option_parser_get_help_table(&options);
+ if (r < 0)
+ return r;
+
+ help_cmdline("[OPTIONS...] [DEVICE]");
+ help_abstract("Installs the OS to another block device.");
+ help_section("Options:");
+
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+
+ help_man_page_reference("systemd-sysinstall", "8");
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ int r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ OptionParser opts = { argc, argv };
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
+ switch (c) {
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION_COMMON_VERSION:
+ return version();
+
+ OPTION_LONG("welcome", "no", "Disable the welcome text"):
+ r = parse_boolean_argument("--welcome=", opts.arg, &arg_welcome);
+ if (r < 0)
+ return r;
+
+ break;
+
+ OPTION_LONG("erase", "BOOL", "Whether to erase the target disk"):
+ r = parse_tristate_argument_with_auto("--erase=", opts.arg, &arg_erase);
+ if (r < 0)
+ return r;
+ break;
+
+ OPTION_LONG("confirm", "no", "Disable query for confirmation"):
+ r = parse_boolean_argument("--confirm=", opts.arg, &arg_confirm);
+ if (r < 0)
+ return r;
+ break;
+
+ OPTION_LONG("summary", "no", "Disable summary before beginning operation"):
+ r = parse_boolean_argument("--summary=", opts.arg, &arg_summary);
+ if (r < 0)
+ return r;
+ break;
+
+ OPTION_LONG("definitions", "DIR", "Find partition definitions in specified directory"): {
+ _cleanup_free_ char *path = NULL;
+ r = parse_path_argument(opts.arg, /* suppress_root= */ false, &path);
+ if (r < 0)
+ return r;
+ if (strv_consume(&arg_definitions, TAKE_PTR(path)) < 0)
+ return log_oom();
+ break;
+ }
+
+ OPTION_LONG("reboot", "BOOL", "Whether to reboot after installation is complete"):
+ r = parse_boolean_argument("--reboot=", opts.arg, &arg_reboot);
+ if (r < 0)
+ return r;
+ break;
+
+ OPTION_LONG("variables", "BOOL", "Whether to modify EFI variables"):
+ r = parse_tristate_argument_with_auto("--variables=", opts.arg, &arg_touch_variables);
+ if (r < 0)
+ return r;
+ break;
+
+ OPTION_LONG("kernel", "IMAGE", "Explicitly pick kernel image to install"):
+ r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_kernel_image);
+ if (r < 0)
+ return r;
+ break;
+
+ OPTION_LONG("set-credential", "ID:VALUE", "Install a credential with literal value to target system"):
+ r = machine_credential_set(&arg_credentials, opts.arg);
+ if (r < 0)
+ return r;
+ break;
+
+ OPTION_LONG("load-credential", "ID:PATH", "Load a credential to install to new system from file or AF_UNIX stream socket"):
+ r = machine_credential_load(&arg_credentials, opts.arg);
+ if (r < 0)
+ return r;
+
+ break;
+
+ OPTION_LONG("copy-locale", "no", "Don't copy current locale to target system"):
+ r = parse_boolean_argument("--copy-locale=", opts.arg, &arg_copy_locale);
+ if (r < 0)
+ return r;
+ break;
+
+ OPTION_LONG("copy-keymap", "no", "Don't copy current keymap to target system"):
+ r = parse_boolean_argument("--copy-keymap=", opts.arg, &arg_copy_keymap);
+ if (r < 0)
+ return r;
+ break;
+
+ OPTION_LONG("copy-timezone", "no", "Don't copy current timezone to target system"):
+ r = parse_boolean_argument("--copy-timezone=", opts.arg, &arg_copy_timezone);
+ if (r < 0)
+ return r;
+ break;
+
+ OPTION_LONG("chrome", "no", "Whether to show a color bar at top and bottom of terminal"):
+ r = parse_boolean_argument("--chrome=", opts.arg, &arg_chrome);
+ if (r < 0)
+ return r;
+
+ break;
+
+ OPTION_LONG("mute-console", "BOOL", "Whether to disallow kernel/PID 1 writes to the console while running"):
+ r = parse_boolean_argument("--mute-console=", opts.arg, &arg_mute_console);
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ char **args = option_parser_get_args(&opts);
+
+ if (strv_length(args) > 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments.");
+ if (!strv_isempty(args)) {
+ arg_node = strdup(args[0]);
+ if (!arg_node)
+ return log_oom();
+ }
+
+ return 1;
+}
+
+static int print_welcome(sd_varlink **mute_console_link) {
+ _cleanup_free_ char *pretty_name = NULL, *os_name = NULL, *ansi_color = NULL;
+ const char *pn, *ac;
+ int r;
+
+ assert(mute_console_link);
+
+ if (!*mute_console_link && arg_mute_console)
+ (void) mute_console(mute_console_link);
+
+ if (!arg_welcome)
+ return 0;
+
+ r = parse_os_release(
+ /* root= */ NULL,
+ "PRETTY_NAME", &pretty_name,
+ "NAME", &os_name,
+ "ANSI_COLOR", &ansi_color);
+ if (r < 0)
+ log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
+ "Failed to read os-release file, ignoring: %m");
+
+ pn = os_release_pretty_name(pretty_name, os_name);
+ ac = isempty(ansi_color) ? "0" : ansi_color;
+
+ if (colors_enabled())
+ printf(ANSI_HIGHLIGHT "Welcome to the " ANSI_NORMAL "\x1B[%sm%s" ANSI_HIGHLIGHT " Installer!" ANSI_NORMAL "\n", ac, pn);
+ else
+ printf("Welcome to the %s Installer!\n", pn);
+
+ putchar('\n');
+
+ return 0;
+}
+
+static int connect_to_repart(sd_varlink **link) {
+ int r;
+
+ assert(link);
+
+ if (*link) {
+ /* Reset the time-out to default here, since we are reusing the connection, but might enqueue
+ * a different operation */
+ r = sd_varlink_set_relative_timeout(*link, 0);
+ if (r < 0)
+ return r;
+
+ return 0;
+ }
+
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_free_ char *repart = NULL;
+ fd = pin_callout_binary("systemd-repart", &repart);
+ if (fd < 0)
+ return log_error_errno(fd, "Failed to find systemd-repart binary: %m");
+
+ r = sd_varlink_connect_exec(link, repart, /* argv= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to systemd-repart: %m");
+
+ return 1;
+}
+
+static int acquire_device_list(
+ sd_varlink **link,
+ char ***ret_menu,
+ char ***ret_accepted) {
+ int r;
+
+ r = connect_to_repart(link);
+ if (r < 0)
+ return r;
+
+ _cleanup_strv_free_ char **menu = NULL, **accepted = NULL;
+
+ sd_json_variant *reply = NULL;
+ const char *error_id = NULL;
+ r = sd_varlink_collectbo(
+ *link,
+ "io.systemd.Repart.ListCandidateDevices",
+ &reply,
+ &error_id,
+ SD_JSON_BUILD_PAIR_BOOLEAN("ignoreRoot", true));
+ if (r < 0)
+ return log_error_errno(r, "Failed to issue io.systemd.Repart.ListCandidateDevices() varlink call: %m");
+ if (streq_ptr(error_id, "io.systemd.Repart.NoCandidateDevices"))
+ log_debug("No candidate devices found.");
+ else if (error_id) {
+ r = sd_varlink_error_to_errno(error_id, reply); /* If this is a system errno style error, output it with %m */
+ if (r != -EBADR)
+ return log_error_errno(r, "Failed to issue io.systemd.Repart.ListCandidateDevices() varlink call: %m");
+
+ return log_error_errno(r, "Failed to issue io.systemd.Repart.ListCandidateDevices() varlink call: %s", error_id);
+ } else {
+ sd_json_variant *i;
+ JSON_VARIANT_ARRAY_FOREACH(i, reply) {
+ _cleanup_(block_device_done) BlockDevice bd = BLOCK_DEVICE_NULL;
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "node", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(BlockDevice, node), SD_JSON_MANDATORY },
+ { "symlinks", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(BlockDevice, symlinks), 0 },
+ {}
+ };
+
+ r = sd_json_dispatch(i, dispatch_table, SD_JSON_LOG|SD_JSON_ALLOW_EXTENSIONS, &bd);
+ if (r < 0)
+ return r;
+
+ if (strv_extend(&accepted, bd.node) < 0)
+ return log_oom();
+ if (strv_extend_strv(&accepted, bd.symlinks, /* filter_duplicates= */ true) < 0)
+ return log_oom();
+
+ /* Prefer the by-id and by-loop-ref because they typically contain the strings most
+ * directly understood by the user */
+ const char *n = strv_find_prefix(bd.symlinks, "/dev/disk/by-id/");
+ if (!n)
+ n = strv_find_prefix(bd.symlinks, "/dev/disk/by-loop-ref/");
+ if (!n)
+ n = bd.node;
+
+ if (strv_extend(&menu, n) < 0)
+ return log_oom();
+ }
+ }
+
+ *ret_menu = TAKE_PTR(menu);
+ *ret_accepted = TAKE_PTR(accepted);
+ return 0;
+}
+
+static int device_is_valid(const char *node, void *userdata) {
+
+ if (!path_is_valid(node) || !path_is_absolute(node)) {
+ log_error("Not a valid absolute file system path, refusing: %s", node);
+ return false;
+ }
+
+ struct stat st;
+ if (stat(node, &st) < 0) {
+ log_error_errno(errno, "Failed to check if '%s' is a valid block device node: %m", node);
+ return false;
+ }
+ if (!S_ISBLK(st.st_mode)) {
+ log_error("Path '%s' does not refer to a valid block device node, refusing.", node);
+ return false;
+ }
+
+ return true;
+}
+
+static int refresh_devices(char ***ret_menu, char ***ret_accepted, void *userdata) {
+ sd_varlink **repart_link = ASSERT_PTR(userdata);
+
+ (void) acquire_device_list(repart_link, ret_menu, ret_accepted);
+ return 0;
+}
+
+static int prompt_block_device(sd_varlink **repart_link, char **ret_node) {
+ int r;
+
+ putchar('\n');
+
+ _cleanup_strv_free_ char **menu = NULL, **accepted = NULL;
+ (void) acquire_device_list(repart_link, &menu, &accepted);
+
+ r = prompt_loop("Please enter target disk device",
+ GLYPH_COMPUTER_DISK,
+ menu,
+ accepted,
+ /* ellipsize_percentage= */ 20,
+ /* n_columns= */ 1,
+ /* column_width= */ 80,
+ device_is_valid,
+ refresh_devices,
+ /* userdata= */ repart_link,
+ PROMPT_SHOW_MENU|PROMPT_SHOW_MENU_NOW|PROMPT_MAY_SKIP|PROMPT_HIDE_SKIP_HINT|PROMPT_HIDE_MENU_HINT,
+ ret_node);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation cancelled.");
+
+ return 0;
+}
+
+static int read_space_metrics(
+ sd_json_variant *v,
+ uint64_t *min_size,
+ uint64_t *current_size,
+ uint64_t *need_free) {
+
+ int r;
+
+ struct {
+ uint64_t min_size;
+ uint64_t current_size;
+ uint64_t need_free;
+ } p = {
+ .min_size = UINT64_MAX,
+ .current_size = UINT64_MAX,
+ .need_free = UINT64_MAX,
+ };
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "minimalSizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, min_size), 0 },
+ { "currentSizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, current_size), 0 },
+ { "needFreeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, need_free), 0 },
+ {}
+ };
+
+ r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
+ if (r < 0)
+ return r;
+
+ if (min_size)
+ *min_size = p.min_size;
+ if (current_size)
+ *current_size = p.current_size;
+ if (need_free)
+ *need_free = p.need_free;
+
+ return 0;
+}
+
+static int invoke_repart(
+ sd_varlink **link,
+ const char *node,
+ bool erase,
+ bool dry_run,
+ uint64_t *min_size, /* initialized both on success and error */
+ uint64_t *current_size, /* ditto */
+ uint64_t *need_free) { /* ditto */
+
+ int r;
+
+ assert(link);
+
+ /* Note, if dry_run is true, then ENOSPC, E2BIG, EHWPOISON will not be logged about beyond LOG_DEBUG,
+ * but all other errors will be */
+
+ r = connect_to_repart(link);
+ if (r < 0) {
+ read_space_metrics(/* v= */ NULL, min_size, current_size, need_free);
+ return r;
+ }
+
+ if (!dry_run) {
+ /* Seeding the partitions might be very slow, disable timeout */
+ r = sd_varlink_set_relative_timeout(*link, UINT64_MAX);
+ if (r < 0)
+ return log_error_errno(r, "Failed to disable IPC timeout: %m");
+ }
+
+ sd_json_variant *reply = NULL;
+ const char *error_id = NULL;
+ r = sd_varlink_callbo(
+ *link,
+ "io.systemd.Repart.Run",
+ &reply,
+ &error_id,
+ SD_JSON_BUILD_PAIR_STRING("node", node),
+ SD_JSON_BUILD_PAIR_STRING("empty", erase ? "force" : "allow"),
+ SD_JSON_BUILD_PAIR_BOOLEAN("dryRun", dry_run),
+ SD_JSON_BUILD_PAIR_CONDITION(!!arg_definitions, "definitions", SD_JSON_BUILD_STRV(arg_definitions)),
+ SD_JSON_BUILD_PAIR_BOOLEAN("deferPartitionsEmpty", true),
+ SD_JSON_BUILD_PAIR_BOOLEAN("deferPartitionsFactoryReset", true));
+ if (r < 0) {
+ read_space_metrics(/* v= */ NULL, min_size, current_size, need_free);
+ return log_error_errno(r, "Failed to issue io.systemd.Repart.Run() varlink call: %m");
+ }
+ if (error_id) {
+ if (streq(error_id, "io.systemd.Repart.InsufficientFreeSpace")) {
+ (void) read_space_metrics(reply, min_size, current_size, need_free);
+ return log_full_errno(
+ dry_run ? LOG_DEBUG : LOG_ERR,
+ SYNTHETIC_ERRNO(ENOSPC),
+ "Not enough free space on disk, cannot install.");
+ }
+ if (streq(error_id, "io.systemd.Repart.DiskTooSmall")) {
+ (void) read_space_metrics(reply, min_size, current_size, need_free);
+ return log_full_errno(
+ dry_run ? LOG_DEBUG : LOG_ERR,
+ SYNTHETIC_ERRNO(E2BIG),
+ "Disk too small for installation, cannot install.");
+ }
+
+ /* For all other errors reset the metrics */
+ read_space_metrics(/* v= */ NULL, min_size, current_size, need_free);
+
+ if (streq(error_id, "io.systemd.Repart.ConflictingDiskLabelPresent"))
+ return log_full_errno(
+ dry_run ? LOG_DEBUG : LOG_ERR,
+ SYNTHETIC_ERRNO(EHWPOISON),
+ "A conflicting disk label is already present on the target disk, cannot install unless disk is erased.");
+
+ r = sd_varlink_error_to_errno(error_id, reply); /* If this is a system errno style error, output it with %m */
+ if (r != -EBADR)
+ return log_error_errno(r, "Failed to issue io.systemd.Repart.Run() varlink call: %m");
+
+ return log_error_errno(r, "Failed to issue io.systemd.Repart.Run() varlink call: %s", error_id);
+ }
+
+ (void) read_space_metrics(reply, min_size, current_size, need_free);
+
+ return 0;
+}
+
+static int prompt_erase(
+ bool can_add,
+ int *ret_erase) {
+ int r;
+
+ assert(ret_erase);
+
+ putchar('\n');
+
+ char **l = can_add ? STRV_MAKE("keep", "erase") : STRV_MAKE("erase");
+
+ _cleanup_free_ char *reply = NULL;
+ r = prompt_loop(can_add ?
+ "Please type 'keep' to install the OS in addition to what the disk already contains, or 'erase' to erase all data on the disk" :
+ "Please type 'erase' to confirm that all data on the disk shall be erased",
+ GLYPH_BROOM,
+ /* menu= */ l,
+ /* accepted= */ l,
+ /* ellipsize_percentage= */ 20,
+ /* n_columns= */ 2,
+ /* column_width= */ 40,
+ /* is_valid= */ NULL,
+ /* refresh= */ NULL,
+ /* userdata= */ NULL,
+ PROMPT_SHOW_MENU|PROMPT_MAY_SKIP|PROMPT_HIDE_MENU_HINT|PROMPT_HIDE_SKIP_HINT,
+ &reply);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation cancelled.");
+
+ if (streq(reply, "erase"))
+ *ret_erase = true;
+ else if (streq(reply, "keep"))
+ *ret_erase = false;
+ else
+ assert_not_reached();
+
+ return 0;
+}
+
+static int prompt_touch_variables(void) {
+ int r;
+
+ if (arg_touch_variables >= 0)
+ return 0;
+
+ putchar('\n');
+
+ char **l = STRV_MAKE("yes", "no");
+
+ _cleanup_free_ char *reply = NULL;
+ r = prompt_loop("Type 'yes' to register OS installation in firmware variables of the local system, 'no' otherwise",
+ GLYPH_ROCKET,
+ /* menu= */ l,
+ /* accepted= */ l,
+ /* ellipsize_percentage= */ 20,
+ /* n_columns= */ 2,
+ /* column_width= */ 40,
+ /* is_valid= */ NULL,
+ /* refresh= */ NULL,
+ /* userdata= */ NULL,
+ PROMPT_SHOW_MENU|PROMPT_MAY_SKIP|PROMPT_HIDE_MENU_HINT|PROMPT_HIDE_SKIP_HINT,
+ &reply);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation cancelled.");
+
+ r = parse_boolean(reply);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse reply: %s", reply);
+
+ arg_touch_variables = r;
+
+ return 0;
+}
+
+static int prompt_confirm(void) {
+ int r;
+
+ if (!arg_confirm)
+ return 0;
+
+ putchar('\n');
+
+ char **l = STRV_MAKE("yes", "no");
+
+ _cleanup_free_ char *reply = NULL;
+ r = prompt_loop(arg_summary ? "Please type 'yes' to confirm the choices above and begin the installation" :
+ "Please type 'yes' to begin the installation",
+ GLYPH_WARNING_SIGN,
+ /* menu= */ l,
+ /* accepted= */ l,
+ /* ellipsize_percentage= */ 20,
+ /* n_columns= */ 2,
+ /* column_width= */ 40,
+ /* is_valid= */ NULL,
+ /* refresh= */ NULL,
+ /* userdata= */ NULL,
+ PROMPT_SHOW_MENU|PROMPT_MAY_SKIP|PROMPT_HIDE_MENU_HINT|PROMPT_HIDE_SKIP_HINT,
+ &reply);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation cancelled.");
+
+ if (!streq(reply, "yes"))
+ return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation not confirmed, cancelling.");
+
+ return 0;
+}
+
+static int validate_run(sd_varlink **repart_link, const char *node) {
+ int r;
+
+ assert(repart_link);
+ assert(node);
+
+ /* First loop: either with explicitly configured --erase= value, or false. A second loop only if not configured explicitly. */
+ bool try_erase = arg_erase > 0, conflicting_disk_label = false;
+ for (;;) {
+ uint64_t min_size = UINT64_MAX, current_size = UINT64_MAX, need_free = UINT64_MAX;
+ r = invoke_repart(
+ repart_link,
+ node,
+ try_erase,
+ /* dry_run= */ true,
+ &min_size,
+ ¤t_size,
+ &need_free);
+ if (r == -ENOSPC) {
+ /* The disk is large enough, but there's not enough unallocated space. Hence proceed, but require erasing */
+ if (try_erase || arg_erase >= 0)
+ return log_error_errno(r, "The selected disk is big enough for the installation but does not have enough free space.");
+
+ log_notice("The selected disk is big enough for the installation but does not have enough free space. Installation will require erasing.");
+ if (need_free != UINT64_MAX)
+ log_info("Required free space is %s.", FORMAT_BYTES(need_free));
+
+ try_erase = true;
+ } else if (r == -E2BIG) {
+ /* Won't fit, whatever we do */
+ log_error_errno(r, "The selected disk is not large enough for an OS installation.");
+ if (current_size != UINT64_MAX)
+ log_info("The size of the selected disk is %s, but a minimal size of %s is required.",
+ FORMAT_BYTES(current_size),
+ FORMAT_BYTES(min_size));
+ return r;
+ } else if (r == -EHWPOISON) {
+ if (try_erase || arg_erase >= 0)
+ return log_error_errno(r, "The selected disk contains a conflicting disk label, refusing.");
+
+ log_debug("Disk contains a conflicting disk label, checking if we could install the OS after erasing it.");
+ try_erase = true;
+ conflicting_disk_label = true;
+ continue;
+ } else if (r < 0)
+ /* invoke_repart() already logged about all other errors */
+ return r;
+ else
+ /* Nice, we can add the OS to the disk, without erasing anything. */
+ log_info("The selected disk has enough free space for an installation of the OS.");
+
+ if (conflicting_disk_label)
+ log_warning("A conflicting disk label has been found, and must be erased for installation.");
+
+ if (arg_erase < 0) {
+ r = prompt_erase(/* can_add= */ !try_erase, &arg_erase);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+ }
+}
+
+static int show_summary(void) {
+ int r;
+
+ if (!arg_summary)
+ return 0;
+
+ printf("\n"
+ "%sSummary:%s\n", ansi_underline(), ansi_normal());
+
+ _cleanup_(table_unrefp) Table *table = table_new_vertical();
+ if (!table)
+ return log_oom();
+
+ r = table_add_many(
+ table,
+ TABLE_FIELD, "Selected Disk",
+ TABLE_STRING, arg_node,
+ TABLE_FIELD, "Erase Disk",
+ TABLE_BOOLEAN, arg_erase,
+ TABLE_SET_COLOR, arg_erase ? ansi_highlight_red() : NULL,
+ TABLE_FIELD, "Register in Firmware",
+ TABLE_BOOLEAN, arg_touch_variables);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ static const char * const map[] = {
+ "firstboot.keymap", "Keyboard Map",
+ "firstboot.locale", "Locale",
+ "firstboot.locale-messages", "Locale (Messages)",
+ "firstboot.timezone", "Timezone",
+ NULL
+ };
+
+ STRV_FOREACH_PAIR(id, text, map) {
+ MachineCredential *c = machine_credential_find(&arg_credentials, *id);
+ if (!c)
+ continue;
+
+ _cleanup_free_ char *escaped = cescape_length(c->data, c->size);
+ if (!escaped)
+ return log_oom();
+
+ r = table_add_many(
+ table,
+ TABLE_FIELD, *text,
+ TABLE_STRING, escaped);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ unsigned n_extra_credentials = 0;
+ FOREACH_ARRAY(cred, arg_credentials.credentials, arg_credentials.n_credentials) {
+ bool covered = false;
+
+ STRV_FOREACH_PAIR(id, text, map)
+ if (streq(*id, cred->id)) {
+ covered = true;
+ break;
+ }
+
+ if (!covered)
+ n_extra_credentials++;
+ }
+
+ if (n_extra_credentials > 0) {
+ r = table_add_many(
+ table,
+ TABLE_FIELD, "Extra Credentials",
+ TABLE_UINT, n_extra_credentials);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_print(table);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int find_current_kernel(
+ char **ret_filename,
+ int *ret_fd) {
+
+ int r;
+
+ sd_id128_t uuid;
+ r = efi_stub_get_device_part_uuid(&uuid);
+ if (r == -ENOENT)
+ return log_error_errno(r, "Cannot find current kernel, no stub partition UUID passed via EFI variables.");
+ if (r < 0)
+ return log_error_errno(r, "Unable to determine stub partition UUID: %m");
+
+ _cleanup_free_ char *image = NULL;
+ r = efi_get_variable_path(EFI_LOADER_VARIABLE_STR("StubImageIdentifier"), &image);
+ if (r == -ENOENT)
+ return log_error_errno(r, "Cannot find current kernel, no stub EFI binary path passed.");
+ if (r < 0)
+ return log_error_errno(r, "Unable to determine stub EFI binary path: %m");
+
+ /* Note: we search for the *host* ESP here (i.e. the one the current EFI paths relate to), not the
+ * one of the target image */
+
+ _cleanup_free_ char *partition_path = NULL;
+ _cleanup_close_ int partition_fd = -EBADF;
+ sd_id128_t partition_uuid;
+ r = find_esp_and_warn_full(
+ /* root= */ NULL,
+ /* path= */ NULL,
+ /* unprivileged_mode= */ false,
+ &partition_path,
+ &partition_fd,
+ /* ret_part= */ NULL,
+ /* ret_pstart= */ NULL,
+ /* ret_psize= */ NULL,
+ &partition_uuid,
+ /* ret_devid= */ NULL);
+ if (r < 0 && r != -ENOKEY)
+ return r;
+ if (r < 0 || !sd_id128_equal(uuid, partition_uuid)) {
+ partition_path = mfree(partition_path);
+ partition_fd = safe_close(partition_fd);
+
+ r = find_xbootldr_and_warn_full(
+ /* root= */ NULL,
+ /* path= */ NULL,
+ /* unprivileged_mode= */ false,
+ &partition_path,
+ &partition_fd,
+ &partition_uuid,
+ /* ret_devid= */ NULL);
+ if (r < 0 && r != -ENOKEY)
+ return r;
+
+ if (r < 0 || !sd_id128_equal(uuid, partition_uuid))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Unable to find UKI on ESP/XBOOTLDR partitions.");
+ }
+
+ _cleanup_free_ char *resolved = NULL;
+ _cleanup_close_ int fd = chase_and_openat(
+ /* root_fd= */ partition_fd,
+ /* dir_fd= */ partition_fd,
+ image,
+ CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_REGULAR,
+ O_RDONLY|O_CLOEXEC,
+ &resolved);
+ if (fd < 0)
+ return log_error_errno(fd, "Failed to find EFI binary '%s' on partition '%s': %m", image, partition_path);
+
+ _cleanup_free_ char *fn = NULL;
+ r = path_extract_filename(resolved, &fn);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract UKI file name from '%s': %m", resolved);
+
+ if (ret_filename)
+ *ret_filename = TAKE_PTR(fn);
+ if (ret_fd)
+ *ret_fd = TAKE_FD(fd);
+
+ return 0;
+}
+
+static int connect_to_bootctl(sd_varlink **link) {
+ int r;
+
+ assert(link);
+
+ if (*link)
+ return 0;
+
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_free_ char *bootctl = NULL;
+ fd = pin_callout_binary("bootctl", &bootctl);
+ if (fd < 0)
+ return log_error_errno(fd, "Failed to find bootctl binary: %m");
+
+ r = sd_varlink_connect_exec(link, bootctl, /* argv= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to bootctl: %m");
+
+ r = sd_varlink_set_allow_fd_passing_output(*link, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enable fd passing to bootctl: %m");
+
+ return 1;
+}
+
+static int invoke_bootctl_install(
+ sd_varlink **link,
+ const char *root_dir,
+ int root_fd) {
+ int r;
+
+ assert(link);
+ assert(root_dir);
+ assert(root_fd >= 0);
+
+ r = connect_to_bootctl(link);
+ if (r < 0)
+ return r;
+
+ int fd_idx = sd_varlink_push_dup_fd(*link, root_fd);
+ if (fd_idx < 0)
+ return log_error_errno(fd_idx, "Failed to submit root fd onto Varlink connection: %m");
+
+ const char *error_id = NULL;
+ r = varlink_callbo_and_log(
+ *link,
+ "io.systemd.BootControl.Install",
+ /* reply= */ NULL,
+ &error_id,
+ SD_JSON_BUILD_PAIR_STRING("operation", "new"),
+ SD_JSON_BUILD_PAIR_INTEGER("rootFileDescriptor", fd_idx),
+ SD_JSON_BUILD_PAIR_STRING("rootDirectory", root_dir),
+ SD_JSON_BUILD_PAIR_BOOLEAN("touchVariables", arg_touch_variables));
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int invoke_bootctl_link(
+ sd_varlink **link,
+ const char *root_dir,
+ int root_fd,
+ char **encrypted_credentials) {
+ int r;
+
+ assert(link);
+ assert(root_dir);
+ assert(root_fd >= 0);
+
+ r = connect_to_bootctl(link);
+ if (r < 0)
+ return r;
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL;
+ STRV_FOREACH_PAIR(name, value, encrypted_credentials) {
+ _cleanup_free_ char *j = strjoin(*name, ".cred");
+ if (!j)
+ return log_oom();
+
+ r = sd_json_variant_append_arraybo(
+ &array,
+ SD_JSON_BUILD_PAIR_STRING("filename", j),
+ SD_JSON_BUILD_PAIR_BASE64("data", *value, strlen(*value)));
+ if (r < 0)
+ return log_error_errno(r, "Failed to append credential to message: %m");
+ }
+
+ int root_fd_idx = sd_varlink_push_dup_fd(*link, root_fd);
+ if (root_fd_idx < 0)
+ return log_error_errno(root_fd_idx, "Failed to submit root fd onto Varlink connection: %m");
+
+ _cleanup_free_ char *kernel_filename = NULL;
+ _cleanup_close_ int kernel_fd = -EBADF;
+ if (arg_kernel_image) {
+ r = path_extract_filename(arg_kernel_image, &kernel_filename);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract filename from kernel path '%s': %m", arg_kernel_image);
+ if (r == O_DIRECTORY)
+ return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Kernel path '%s' refers to directory, must be regular file, refusing.", arg_kernel_image);
+
+ kernel_fd = xopenat_full(XAT_FDROOT, arg_kernel_image, O_RDONLY|O_CLOEXEC, XO_REGULAR, MODE_INVALID);
+ if (kernel_fd < 0)
+ return log_error_errno(kernel_fd, "Failed to open kernel image '%s': %m", arg_kernel_image);
+
+ } else {
+ r = find_current_kernel(&kernel_filename, &kernel_fd);
+ if (r < 0)
+ return r;
+ }
+
+ int kernel_fd_idx = sd_varlink_push_dup_fd(*link, kernel_fd);
+ if (kernel_fd_idx < 0)
+ return log_error_errno(kernel_fd_idx, "Failed to submit kernel fd onto Varlink connection: %m");
+
+ const char *error_id = NULL;
+ r = varlink_callbo_and_log(
+ *link,
+ "io.systemd.BootControl.Link",
+ /* reply= */ NULL,
+ &error_id,
+ SD_JSON_BUILD_PAIR_INTEGER("rootFileDescriptor", root_fd_idx),
+ SD_JSON_BUILD_PAIR_STRING("rootDirectory", root_dir),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("kernelFilename", kernel_filename),
+ SD_JSON_BUILD_PAIR_INTEGER("kernelFileDescriptor", kernel_fd_idx),
+ SD_JSON_BUILD_PAIR_CONDITION(!!array, "extraFiles", SD_JSON_BUILD_VARIANT(array)));
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int maybe_reboot(void) {
+ int r;
+
+ if (!arg_reboot)
+ return 0;
+
+ log_notice("%s%sSystem will reboot now.",
+ emoji_enabled() ? glyph(GLYPH_CIRCLE_ARROW) : "", emoji_enabled() ? " " : "");
+
+ if (!any_key_to_proceed())
+ return 0;
+
+ log_notice("%s%sInitiating reboot.",
+ emoji_enabled() ? glyph(GLYPH_CIRCLE_ARROW) : "", emoji_enabled() ? " " : "");
+
+ _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL;
+ r = sd_varlink_connect_address(&link, "/run/systemd/io.systemd.Shutdown");
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to systemd-logind: %m");
+
+ sd_json_variant *reply = NULL;
+ const char *error_id = NULL;
+ r = varlink_callbo_and_log(
+ link,
+ "io.systemd.Shutdown.Reboot",
+ &reply,
+ &error_id);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int read_credential_locale(void) {
+ int r;
+
+ if (!arg_copy_locale)
+ return 0;
+
+ if (machine_credential_find(&arg_credentials, "firstboot.locale") ||
+ machine_credential_find(&arg_credentials, "firstboot.locale-messages"))
+ return 0;
+
+ /* For the main locale we check the two env vars, and if neither is there, we use LC_NUMERIC, since
+ * it seems to be one of the most fundamental ones, and is not LC_MESSAGES for which we have a
+ * separate setting after all */
+ const char *l = getenv("LC_ALL") ?: getenv("LANG") ?: setlocale(LC_NUMERIC, NULL);
+ if (l) {
+ r = machine_credential_add(&arg_credentials, "firstboot.locale", l, /* size= */ SIZE_MAX);
+ if (r < 0)
+ return log_oom();
+ }
+
+ const char *m = setlocale(LC_MESSAGES, NULL);
+ if (m && !streq_ptr(m, l)) {
+ r = machine_credential_add(&arg_credentials, "firstboot.locale-messages", m, /* size= */ SIZE_MAX);
+ if (r < 0)
+ return log_oom();
+ }
+
+ return 0;
+}
+
+static int read_credential_keymap(void) {
+ int r;
+
+ if (!arg_copy_keymap)
+ return 0;
+
+ if (machine_credential_find(&arg_credentials, "firstboot.keymap"))
+ return 0;
+
+ _cleanup_free_ char *keymap = NULL;
+ r = parse_env_file(
+ /* f= */ NULL,
+ etc_vconsole_conf(),
+ "KEYMAP", &keymap);
+ if (r < 0 && r != -ENOENT)
+ return log_error_errno(r, "Failed to parse '%s': %m", etc_vconsole_conf());
+
+ if (!isempty(keymap)) {
+ r = machine_credential_add(&arg_credentials, "firstboot.keymap", keymap, /* size= */ SIZE_MAX);
+ if (r < 0)
+ return log_oom();
+ }
+
+ return 0;
+}
+
+static int read_credential_timezone(void) {
+ int r;
+
+ if (!arg_copy_timezone)
+ return 0;
+
+ if (machine_credential_find(&arg_credentials, "firstboot.timezone"))
+ return 0;
+
+ _cleanup_free_ char *tz = NULL;
+ r = get_timezone_prefer_env(&tz);
+ if (r < 0)
+ log_warning_errno(r, "Failed to read timezone, skipping timezone propagation: %m");
+ else {
+ r = machine_credential_add(&arg_credentials, "firstboot.timezone", tz, /* size= */ SIZE_MAX);
+ if (r < 0)
+ return log_oom();
+ }
+
+ return 0;
+}
+
+static int read_credentials(void) {
+ int r;
+
+ r = read_credential_locale();
+ if (r < 0)
+ return r;
+
+ r = read_credential_keymap();
+ if (r < 0)
+ return r;
+
+ r = read_credential_timezone();
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int connect_to_creds(sd_varlink **link) {
+ int r;
+
+ assert(link);
+
+ if (*link)
+ return 0;
+
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_free_ char *creds = NULL;
+ fd = pin_callout_binary("systemd-creds", &creds);
+ if (fd < 0)
+ return log_error_errno(fd, "Failed to find systemd-creds binary: %m");
+
+ r = sd_varlink_connect_exec(link, creds, /* argv= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to systemd-creds: %m");
+
+ return 1;
+}
+
+static int encrypt_one_credential(sd_varlink **link, const MachineCredential *input, char ***encrypted) {
+ int r;
+
+ assert(link);
+ assert(input);
+ assert(encrypted);
+
+ log_info("Encrypting credential '%s'...", input->id);
+
+ r = connect_to_creds(link);
+ if (r < 0)
+ return r;
+
+ sd_json_variant *reply = NULL;
+ const char *error_id = NULL;
+ r = varlink_callbo_and_log(
+ *link,
+ "io.systemd.Credentials.Encrypt",
+ &reply,
+ &error_id,
+ SD_JSON_BUILD_PAIR_STRING("name", input->id),
+ SD_JSON_BUILD_PAIR_BASE64("data", input->data, input->size),
+ SD_JSON_BUILD_PAIR_STRING("scope", "system"),
+ /* We pick the 'auto_initrd' key for this, since we want TPM if available, but are fine with NULL if not */
+ SD_JSON_BUILD_PAIR_STRING("withKey", "auto_initrd"));
+ if (r < 0)
+ return r;
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "blob", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, 0, 0 },
+ {}
+ };
+
+ const char *blob = NULL;
+ r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &blob);
+ if (r < 0)
+ return r;
+
+ r = strv_extend_many(encrypted, input->id, blob);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int encrypt_credentials(sd_varlink **link, char ***encrypted) {
+ int r;
+
+ assert(link);
+ assert(encrypted);
+
+ FOREACH_ARRAY(cred, arg_credentials.credentials, arg_credentials.n_credentials) {
+ r = encrypt_one_credential(link, cred, encrypted);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static const ImagePolicy image_policy = {
+ .n_policies = 4,
+ .policies = {
+ /* We mount / and /usr/ so that we can get access to /etc/machine-id and /etc/kernel/ */
+ { PARTITION_ROOT, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT },
+ { PARTITION_USR, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT },
+ { PARTITION_ESP, PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT },
+ { PARTITION_XBOOTLDR, PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT },
+ },
+ .default_flags = PARTITION_POLICY_IGNORE,
+};
+
+static int settle_definitions(void) {
+ int r;
+
+ if (arg_definitions)
+ return 0;
+
+ /* If /usr/lib/repart.sysinstall.d/ is populated, use it, otherwise use the regular definition
+ * files */
+
+ _cleanup_strv_free_ char **files = NULL;
+ r = conf_files_list_strv(
+ &files,
+ ".conf",
+ /* root= */ NULL,
+ CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN|CONF_FILES_DONT_PREFIX_ROOT,
+ (const char**) CONF_PATHS_STRV("repart.sysinstall.d"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate *.conf files: %m");
+
+ if (!strv_isempty(files)) {
+ arg_definitions = strv_copy(CONF_PATHS_STRV("repart.sysinstall.d"));
+ if (!arg_definitions)
+ return log_oom();
+ }
+
+ return 0;
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ setlocale(LC_ALL, "");
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ log_setup();
+
+ r = settle_definitions();
+ if (r < 0)
+ return r;
+
+ _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *mute_console_link = NULL;
+ if (arg_welcome) {
+ if (arg_mute_console)
+ (void) mute_console(&mute_console_link);
+
+ (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0);
+
+ if (arg_chrome)
+ chrome_show("Operating System Installer", /* bottom= */ NULL);
+ }
+
+ DEFER_VOID_CALL(chrome_hide);
+
+ _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *repart_link = NULL;
+ if (arg_node) {
+ r = print_welcome(&mute_console_link);
+ if (r < 0)
+ return r;
+
+ r = validate_run(&repart_link, arg_node);
+ if (r < 0)
+ return r;
+ } else {
+ /* Determine the minimum disk size */
+ uint64_t min_size = UINT64_MAX;
+ r = invoke_repart(
+ &repart_link,
+ /* node= */ NULL,
+ /* erase= */ true,
+ /* dry_run= */ true,
+ &min_size,
+ /* current_size= */ NULL,
+ /* need_free= */ NULL);
+ if (r < 0)
+ return r;
+
+ r = print_welcome(&mute_console_link);
+ if (r < 0)
+ return r;
+
+ log_info("Required minimal installation disk size is %s.", FORMAT_BYTES(min_size));
+
+ for (;;) {
+ _cleanup_free_ char *node = NULL;
+ r = prompt_block_device(&repart_link, &node);
+ if (r < 0)
+ return r;
+
+ r = validate_run(&repart_link, node);
+ if (IN_SET(r, -ENOSPC, -E2BIG, -EHWPOISON)) /* Device is no fit, pick other */
+ continue;
+ if (r < 0)
+ return r;
+
+ arg_node = TAKE_PTR(node);
+ break;
+ }
+ }
+
+ r = prompt_touch_variables();
+ if (r < 0)
+ return r;
+
+ r = read_credentials();
+ if (r < 0)
+ return r;
+
+ /* Verify we have everything we need */
+ assert(arg_node);
+ assert(arg_erase >= 0);
+ assert(arg_touch_variables >= 0);
+
+ r = show_summary();
+ if (r < 0)
+ return r;
+
+ r = prompt_confirm();
+ if (r < 0)
+ return r;
+
+ putchar('\n');
+
+ log_notice("%s%sEncrypting credentials...",
+ emoji_enabled() ? glyph(GLYPH_LOCK_AND_KEY) : "", emoji_enabled() ? " " : "");
+
+ _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *creds_link = NULL;
+ _cleanup_strv_free_ char **encrypted_credentials = NULL;
+ r = encrypt_credentials(&creds_link, &encrypted_credentials);
+ if (r < 0)
+ return r;
+
+ log_notice("%s%sInstalling partitions...",
+ emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : "");
+
+ /* Do the main part of the installation */
+ r = invoke_repart(
+ &repart_link,
+ arg_node,
+ arg_erase,
+ /* dry_run= */ false,
+ /* min_size= */ NULL,
+ /* current_size= */ NULL,
+ /* need_free= */ NULL);
+ if (r < 0)
+ return r;
+
+ log_notice("%s%sMounting partitions...",
+ emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : "");
+
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(umount_and_freep) char *root_dir = NULL;
+ _cleanup_close_ int root_fd = -EBADF;
+ r = mount_image_privately_interactively(
+ arg_node,
+ &image_policy,
+ DISSECT_IMAGE_REQUIRE_ROOT |
+ DISSECT_IMAGE_RELAX_VAR_CHECK |
+ DISSECT_IMAGE_ALLOW_USERSPACE_VERITY |
+ DISSECT_IMAGE_DISCARD_ANY |
+ DISSECT_IMAGE_GPT_ONLY |
+ DISSECT_IMAGE_FSCK |
+ DISSECT_IMAGE_USR_NO_ROOT |
+ DISSECT_IMAGE_ADD_PARTITION_DEVICES |
+ DISSECT_IMAGE_PIN_PARTITION_DEVICES,
+ &root_dir,
+ &root_fd,
+ &loop_device);
+ if (r < 0)
+ return log_error_errno(r, "Failed to mount new image: %m");
+
+ log_notice("%s%sInstalling kernel...",
+ emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : "");
+
+ _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *bootctl_link = NULL;
+ r = invoke_bootctl_link(&bootctl_link, root_dir, root_fd, encrypted_credentials);
+ if (r < 0)
+ return r;
+
+ log_notice("%s%sInstalling boot loader...",
+ emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : "");
+
+ r = invoke_bootctl_install(&bootctl_link, root_dir, root_fd);
+ if (r < 0)
+ return r;
+
+ log_notice("%s%sUnmounting partitions...",
+ emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : "");
+
+ root_fd = safe_close(root_fd);
+ r = umount_recursive(root_dir, /* flags= */ 0);
+ if (r < 0)
+ log_warning_errno(r, "Failed to unmount target disk, proceeding anyway: %m");
+ loop_device = loop_device_unref(loop_device);
+ sync();
+
+ log_notice("%s%sInstallation succeeded.",
+ emoji_enabled() ? glyph(GLYPH_SPARKLES) : "", emoji_enabled() ? " " : "");
+
+ r = maybe_reboot();
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);