]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sysinstall: new component
authorLennart Poettering <lennart@poettering.net>
Thu, 28 Aug 2025 09:51:11 +0000 (11:51 +0200)
committerLennart Poettering <lennart@amutable.com>
Tue, 5 May 2026 13:09:47 +0000 (15:09 +0200)
15 files changed:
man/rules/meson.build
man/systemd-sysinstall.xml [new file with mode: 0644]
meson.build
meson_options.txt
shell-completion/bash/meson.build
shell-completion/bash/systemd-sysinstall [new file with mode: 0644]
shell-completion/zsh/_systemd-sysinstall [new file with mode: 0644]
shell-completion/zsh/meson.build
src/basic/time-util.c
src/basic/time-util.h
src/sysinstall/meson.build [new file with mode: 0644]
src/sysinstall/sysinstall.c [new file with mode: 0644]
units/meson.build
units/system-install.target [new file with mode: 0644]
units/systemd-sysinstall.service [new file with mode: 0644]

index 719838064c02f8dc5a5c8675dc489c68f8e94a0e..c42a8d47f8e27e8e0ced4e2de0db8115d925356d 100644 (file)
@@ -1218,6 +1218,10 @@ manpages = [
    'systemd-sysext-sysroot.service',
    'systemd-sysext.service'],
   'ENABLE_SYSEXT'],
+ ['systemd-sysinstall',
+  '8',
+  ['systemd-sysinstall.service'],
+  'ENABLE_SYSINSTALL'],
  ['systemd-system-update-generator', '8', [], ''],
  ['systemd-system.conf',
   '5',
diff --git a/man/systemd-sysinstall.xml b/man/systemd-sysinstall.xml
new file mode 100644 (file)
index 0000000..228e7e2
--- /dev/null
@@ -0,0 +1,292 @@
+<?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>
index 325b954a78b24fb49dc8d5406f409db745f4f1c6..d6fbd7c2b7ea68e23833811c4d2326f0ddbe7697 100644 (file)
@@ -1582,6 +1582,7 @@ foreach tuple : [
                 ['rfkill'],
                 ['smack'],
                 ['sysext'],
+                ['sysinstall'],
                 ['sysusers'],
                 ['timedated'],
                 ['timesyncd'],
@@ -2144,6 +2145,7 @@ subdir('src/storagetm')
 subdir('src/sulogin-shell')
 subdir('src/sysctl')
 subdir('src/sysext')
+subdir('src/sysinstall')
 subdir('src/system-update-generator')
 subdir('src/systemctl')
 subdir('src/sysupdate')
@@ -2928,6 +2930,7 @@ foreach tuple : [
         ['resolve'],
         ['rfkill'],
         ['sysext'],
+        ['sysinstall'],
         ['systemd-analyze',        conf.get('ENABLE_ANALYZE') == 1],
         ['sysupdate'],
         ['sysupdated'],
index d61afac519d8470123633d31253d0b28c2e26311..1917268d2ce4d5adf45a0a62a3b815fab975b088 100644 (file)
@@ -109,6 +109,8 @@ option('sysupdate', type : 'feature', deprecated : { 'true' : 'enabled', 'false'
 option('sysupdated', type: 'combo', value : 'auto',
        choices : ['auto', 'enabled', 'disabled'],
        description : 'install the systemd-sysupdated service')
+option('sysinstall', type : 'boolean',
+       description : 'install the systemd-sysinstall tool')
 
 option('coredump', type : 'boolean',
        description : 'install the coredump handler')
index b0e56608e8f37def5c8eff478ce9ba0897d29993..cddf742059d5112e2c23d3a0bda8d85c60af71d1 100644 (file)
@@ -54,6 +54,7 @@ foreach item : [
         ['systemd-resolve',     'ENABLE_RESOLVE'],
         ['systemd-run',         ''],
         ['systemd-sysext',      'ENABLE_SYSEXT'],
+        ['systemd-sysinstall',  'ENABLE_SYSINSTALL'],
         ['systemd-vmspawn',     'ENABLE_VMSPAWN'],
         ['systemd-vpick',       ''],
         ['timedatectl',         'ENABLE_TIMEDATED'],
diff --git a/shell-completion/bash/systemd-sysinstall b/shell-completion/bash/systemd-sysinstall
new file mode 100644 (file)
index 0000000..600e2aa
--- /dev/null
@@ -0,0 +1,90 @@
+# shellcheck shell=bash
+# systemd-sysinstall(8) completion                    -*- shell-script -*-
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# systemd is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with systemd; If not, see <https://www.gnu.org/licenses/>.
+
+__contains_word() {
+    local w word=$1; shift
+    for w in "$@"; do
+        [[ $w = "$word" ]] && return
+    done
+}
+
+__get_block_devices() {
+    systemd-repart --list-devices 2>/dev/null
+}
+
+_systemd_sysinstall() {
+    local comps
+    local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} words cword
+    local -A OPTS=(
+        [STANDALONE]='-h --help --version'
+        [ARG]='--welcome
+               --chrome
+               --erase
+               --confirm
+               --summary
+               --reboot
+               --variables
+               --mute-console
+               --copy-locale
+               --copy-keymap
+               --copy-timezone
+               --definitions
+               --kernel
+               --set-credential
+               --load-credential'
+    )
+
+    _init_completion || return
+
+    if __contains_word "$prev" ${OPTS[ARG]}; then
+        case $prev in
+            --welcome|--chrome|--confirm|--summary|--reboot|--mute-console|--copy-locale|--copy-keymap|--copy-timezone)
+                comps='yes no'
+                ;;
+            --erase|--variables)
+                comps='yes no auto'
+                ;;
+            --definitions)
+                comps=$(compgen -A directory -- "$cur")
+                compopt -o filenames
+                ;;
+            --kernel|--load-credential)
+                comps=$(compgen -A file -- "$cur")
+                compopt -o filenames
+                ;;
+            --set-credential)
+                comps=''
+                ;;
+        esac
+        COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
+        return 0
+    fi
+
+    if [[ "$cur" = -* ]]; then
+        COMPREPLY=( $(compgen -W '${OPTS[*]}' -- "$cur") )
+        return 0
+    fi
+
+    comps=$(__get_block_devices)
+    COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
+    compopt -o filenames
+    return 0
+}
+
+complete -F _systemd_sysinstall systemd-sysinstall
diff --git a/shell-completion/zsh/_systemd-sysinstall b/shell-completion/zsh/_systemd-sysinstall
new file mode 100644 (file)
index 0000000..039e4bf
--- /dev/null
@@ -0,0 +1,29 @@
+#compdef systemd-sysinstall
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+(( $+functions[_systemd-sysinstall_devices] )) ||
+_systemd-sysinstall_devices() {
+    local -a _devices
+    _devices=( ${(f)"$(systemd-repart --list-devices 2>/dev/null)"} )
+    _wanted devices expl 'block device' compadd -a _devices
+}
+
+_arguments \
+    '(- *)'{-h,--help}'[Show help text]' \
+    '(- *)--version[Show package version]' \
+    '--welcome=[Show welcome text]:boolean:(yes no)' \
+    '--chrome=[Show colored bars at top and bottom of the terminal]:boolean:(yes no)' \
+    '--erase=[Erase target disk before installation]:boolean:(yes no auto)' \
+    '--confirm=[Query for confirmation before installation]:boolean:(yes no)' \
+    '--summary=[Show summary before installation]:boolean:(yes no)' \
+    '--reboot=[Reboot system after installation]:boolean:(yes no)' \
+    '--variables=[Register installation in firmware variables]:boolean:(yes no auto)' \
+    '--mute-console=[Mute kernel/PID 1 console output during installation]:boolean:(yes no)' \
+    '--copy-locale=[Copy current locale to target system]:boolean:(yes no)' \
+    '--copy-keymap=[Copy current keymap to target system]:boolean:(yes no)' \
+    '--copy-timezone=[Copy current timezone to target system]:boolean:(yes no)' \
+    '--definitions=[Find partition definitions in directory]:directory:_directories' \
+    '--kernel=[Kernel image to install]:kernel image:_files' \
+    '--set-credential=[Install a credential with a literal value]: : _message "ID:VALUE"' \
+    '--load-credential=[Load credential from a file or AF_UNIX socket]: : _message "ID:PATH"' \
+    '*::block device:_systemd-sysinstall_devices'
index 6cc8a2d57f83e3b1a830f3e3bef729112ef739ab..f10ba7be617cc994084191f14b54c56bd2bb45a5 100644 (file)
@@ -43,6 +43,7 @@ foreach item : [
         ['_systemd-nspawn',           ''],
         ['_systemd-path',             ''],
         ['_systemd-run',              ''],
+        ['_systemd-sysinstall',       'ENABLE_SYSINSTALL'],
         ['_systemd-tmpfiles',         'ENABLE_TMPFILES'],
         ['_timedatectl',              'ENABLE_TIMEDATED'],
         ['_udevadm',                  ''],
index 78c33c7553ce6be3abacc4d74170ec164ed44278..eb74de32c2db811a3df91e3212791b04c1aeac32 100644 (file)
@@ -1697,6 +1697,16 @@ int get_timezone(char **ret) {
         return strdup_to(ret, e);
 }
 
+int get_timezone_prefer_env(char **ret) {
+        assert(ret);
+
+        const char *e = getenv("TZ");
+        if (e && e[0] == ':' && timezone_is_valid(e + 1, LOG_DEBUG))
+                return strdup_to(ret, e + 1);
+
+        return get_timezone(ret);
+}
+
 const char* etc_localtime(void) {
         static const char *cached = NULL;
 
index 9a66a90859d67fc42fe72d93a55c7f4e477103bb..fdaf11edcbf6c2a5ebfad5628e3e37daaec6fbf9 100644 (file)
@@ -178,6 +178,7 @@ bool clock_supported(clockid_t clock);
 usec_t usec_shift_clock(usec_t x, clockid_t from, clockid_t to);
 
 int get_timezone(char **ret);
+int get_timezone_prefer_env(char **ret);
 const char* etc_localtime(void);
 
 int mktime_or_timegm_usec(struct tm *tm, bool utc, usec_t *ret);
diff --git a/src/sysinstall/meson.build b/src/sysinstall/meson.build
new file mode 100644 (file)
index 0000000..1d8be60
--- /dev/null
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+executables += [
+        executable_template + {
+                'name' : 'systemd-sysinstall',
+                'public' : true,
+                'conditions' : ['ENABLE_SYSINSTALL'],
+                'sources' : files('sysinstall.c'),
+        },
+]
diff --git a/src/sysinstall/sysinstall.c b/src/sysinstall/sysinstall.c
new file mode 100644 (file)
index 0000000..d8f5cbe
--- /dev/null
@@ -0,0 +1,1425 @@
+/* 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,
+                                &current_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);
index 0f7ce75bd89673b5f4885f404baf274bbad3bc3c..fca299aa8465a1b6a916c39ee1d2a55b4fc40885 100644 (file)
@@ -240,6 +240,7 @@ units = [
         },
         { 'file' : 'sysinit.target' },
         { 'file' : 'syslog.socket' },
+        { 'file' : 'system-install.target' },
         {
           'file' : 'system-systemd\\x2dcryptsetup.slice',
           'conditions' : ['HAVE_LIBCRYPTSETUP'],
@@ -778,6 +779,11 @@ units = [
           'file' : 'systemd-sysext@.service',
           'conditions' : ['ENABLE_SYSEXT'],
         },
+        {
+          'file' : 'systemd-sysinstall.service',
+          'conditions' : ['ENABLE_SYSINSTALL'],
+          'symlinks' : ['system-install.target.wants/'],
+        },
         {
           'file' : 'systemd-sysupdate-reboot.service.in',
           'conditions' : ['ENABLE_SYSUPDATE'],
diff --git a/units/system-install.target b/units/system-install.target
new file mode 100644 (file)
index 0000000..660110d
--- /dev/null
@@ -0,0 +1,15 @@
+#  SPDX-License-Identifier: LGPL-2.1-or-later
+#
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it
+#  under the terms of the GNU Lesser General Public License as published by
+#  the Free Software Foundation; either version 2.1 of the License, or
+#  (at your option) any later version.
+
+[Unit]
+Description=System Installer
+Documentation=man:systemd-sysinstall(8)
+Requires=sysinit.target
+After=sysinit.target
+AllowIsolate=yes
diff --git a/units/systemd-sysinstall.service b/units/systemd-sysinstall.service
new file mode 100644 (file)
index 0000000..a330db2
--- /dev/null
@@ -0,0 +1,22 @@
+#  SPDX-License-Identifier: LGPL-2.1-or-later
+#
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it
+#  under the terms of the GNU Lesser General Public License as published by
+#  the Free Software Foundation; either version 2.1 of the License, or
+#  (at your option) any later version.
+
+[Unit]
+Description=System Install Tool
+Documentation=man:systemd-sysinstall(8)
+Wants=systemd-logind.service
+After=systemd-logind.service
+
+[Service]
+ExecStart=systemd-sysinstall --variables=yes --reboot=yes --mute-console=yes
+StandardOutput=tty
+StandardInput=tty
+StandardError=tty
+TTYReset=yes
+FailureAction=halt