diff --git a/CMakeModules/CalamaresAddModuleSubdirectory.cmake b/CMakeModules/CalamaresAddModuleSubdirectory.cmake
index 1f1c0230056363d395b4ed093fae5fe1afd60854..91524a09e47f0bb50008a12efbf845c3a3b7bad5 100644
--- a/CMakeModules/CalamaresAddModuleSubdirectory.cmake
+++ b/CMakeModules/CalamaresAddModuleSubdirectory.cmake
@@ -42,6 +42,18 @@ include( CalamaresCheckModuleSelection )
 
 set( MODULE_DATA_DESTINATION share/calamares/modules )
 
+# We look for Pylint (just once) so that unittests can be added that
+# check the syntax / variables of Python modules. This should help
+# avoid more typo's-in-releases.
+if(BUILD_TESTING AND NOT PYLINT_COMMAND_SEARCHED)
+    set(PYLINT_COMMAND_SEARCHED TRUE)
+    find_program(
+        PYLINT_COMMAND
+        NAMES pylint3 pylint
+        PATHS $ENV{HOME}/.local/bin
+        )
+endif()
+
 function( _calamares_add_module_subdirectory_impl )
     set( SUBDIRECTORY ${ARGV0} )
 
@@ -241,6 +253,19 @@ function( _calamares_add_module_subdirectory_impl )
         if ( EXISTS ${_testdir}/CMakeTests.txt AND NOT EXISTS ${_mod_dir}/CMakeLists.txt )
             include( ${_testdir}/CMakeTests.txt )
         endif()
+        if ( PYLINT_COMMAND AND MODULE_INTERFACE MATCHES "python" )
+            # Python modules get an additional test via pylint; this
+            # needs to run at top-level because the ci/libcalamares directory
+            # contains API stubs.
+            #
+            # TODO: the entry point is assumed to be `main.py`, but that is
+            #       configurable through module.desc
+            add_test(
+                NAME lint-${SUBDIRECTORY}
+                COMMAND env PYTHONPATH=ci: ${PYLINT_COMMAND} -E ${_mod_dir}/main.py
+                WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+                )
+        endif()
     endif()
 endfunction()
 
diff --git a/ci/libcalamares/__init__.py b/ci/libcalamares/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f7a0943075140a8ddf2c449568a8935981548c9
--- /dev/null
+++ b/ci/libcalamares/__init__.py
@@ -0,0 +1,9 @@
+#   SPDX-FileCopyrightText: no
+#   SPDX-License-Identifier: CC0-1.0
+#
+# Stubs for part of the Python API from libcalamares
+# (although the **actual** API is presented through
+# Boost::Python, not as a bare C-extension) so that
+# pylint doesn't complain about libcalamares internals.
+
+VERSION_SHORT="1.0"
diff --git a/ci/libcalamares/globalstorage.py b/ci/libcalamares/globalstorage.py
new file mode 100644
index 0000000000000000000000000000000000000000..d40a2820485a787a0e65060f1fb71b87094153ae
--- /dev/null
+++ b/ci/libcalamares/globalstorage.py
@@ -0,0 +1,24 @@
+#   SPDX-FileCopyrightText: no
+#   SPDX-License-Identifier: CC0-1.0
+#
+# Stubs for part of the Python API from libcalamares
+# (although the **actual** API is presented through
+# Boost::Python, not as a bare C-extension) so that
+# pylint doesn't complain about libcalamares internals.
+
+def count(): return 1
+
+def keys(): return []
+
+def contains(_): return True
+
+def value(key):
+    if key in ("branding",):
+        return dict()
+    if key in ("partitions",):
+        return list()
+    return ""
+
+def insert(key, value): pass
+
+def remove(_): pass
diff --git a/ci/libcalamares/job.py b/ci/libcalamares/job.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ea38b878653afaf0c29321752b2f807336fc344
--- /dev/null
+++ b/ci/libcalamares/job.py
@@ -0,0 +1,15 @@
+#   SPDX-FileCopyrightText: no
+#   SPDX-License-Identifier: CC0-1.0
+#
+# Stubs for part of the Python API from libcalamares
+# (although the **actual** API is presented through
+# Boost::Python, not as a bare C-extension) so that
+# pylint doesn't complain about libcalamares internals.
+
+configuration = dict()
+
+def setprogress(_): pass
+
+def pretty_name(): return ""
+
+def working_path(): return ""
diff --git a/ci/libcalamares/utils.py b/ci/libcalamares/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..706e4a95a287d5a1093dd39d339b45199d9b4ad0
--- /dev/null
+++ b/ci/libcalamares/utils.py
@@ -0,0 +1,23 @@
+#   SPDX-FileCopyrightText: no
+#   SPDX-License-Identifier: CC0-1.0
+#
+# Stubs for part of the Python API from libcalamares
+# (although the **actual** API is presented through
+# Boost::Python, not as a bare C-extension) so that
+# pylint doesn't complain about libcalamares internals.
+
+def debug(_): pass
+
+def warning(_): pass
+
+def error(_): pass
+
+def gettext_path(): pass
+
+def gettext_languages(): pass
+
+def target_env_call(_): return 0
+
+def check_target_env_call(_): pass
+
+def mount(device, mountpoint, fstype, options): return 0
diff --git a/lang/calamares_en.ts b/lang/calamares_en.ts
index 884498c2ce46d160dd951b385fe101041b01f25f..5575b69645b28b090d9e799c16349a6da5ab89a0 100644
--- a/lang/calamares_en.ts
+++ b/lang/calamares_en.ts
@@ -171,7 +171,7 @@
   <context>
     <name>Calamares::JobThread</name>
     <message>
-      <location filename="../src/libcalamares/JobQueue.cpp" line="199"/>
+      <location filename="../src/libcalamares/JobQueue.cpp" line="201"/>
       <source>Done</source>
       <translation>Done</translation>
     </message>
@@ -320,17 +320,17 @@
       <translation>&amp;Close</translation>
     </message>
     <message>
-      <location filename="../src/libcalamaresui/utils/Paste.cpp" line="171"/>
+      <location filename="../src/libcalamaresui/utils/Paste.cpp" line="183"/>
       <source>Install Log Paste URL</source>
       <translation>Install Log Paste URL</translation>
     </message>
     <message>
-      <location filename="../src/libcalamaresui/utils/Paste.cpp" line="153"/>
+      <location filename="../src/libcalamaresui/utils/Paste.cpp" line="165"/>
       <source>The upload was unsuccessful. No web-paste was done.</source>
       <translation>The upload was unsuccessful. No web-paste was done.</translation>
     </message>
     <message>
-      <location filename="../src/libcalamaresui/utils/Paste.cpp" line="165"/>
+      <location filename="../src/libcalamaresui/utils/Paste.cpp" line="177"/>
       <source>Install log posted to
 
 %1
@@ -1926,35 +1926,35 @@ The installer will quit and all changes will be lost.</translation>
   <context>
     <name>LuksBootKeyFileJob</name>
     <message>
-      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="28"/>
+      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="30"/>
       <source>Configuring LUKS key file.</source>
       <translation>Configuring LUKS key file.</translation>
     </message>
     <message>
-      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="168"/>
-      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="176"/>
+      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="186"/>
+      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="194"/>
       <source>No partitions are defined.</source>
       <translation>No partitions are defined.</translation>
     </message>
     <message>
-      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="211"/>
-      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="218"/>
-      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="226"/>
+      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="229"/>
+      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="236"/>
+      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="244"/>
       <source>Encrypted rootfs setup error</source>
       <translation>Encrypted rootfs setup error</translation>
     </message>
     <message>
-      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="212"/>
+      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="230"/>
       <source>Root partition %1 is LUKS but no passphrase has been set.</source>
       <translation>Root partition %1 is LUKS but no passphrase has been set.</translation>
     </message>
     <message>
-      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="219"/>
+      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="237"/>
       <source>Could not create LUKS key file for root partition %1.</source>
       <translation>Could not create LUKS key file for root partition %1.</translation>
     </message>
     <message>
-      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="227"/>
+      <location filename="../src/modules/luksbootkeyfile/LuksBootKeyFileJob.cpp" line="245"/>
       <source>Could not configure LUKS key file on partition %1.</source>
       <translation>Could not configure LUKS key file on partition %1.</translation>
     </message>
@@ -2929,14 +2929,14 @@ The installer will quit and all changes will be lost.</translation>
   <context>
     <name>ProcessResult</name>
     <message>
-      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="429"/>
+      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="425"/>
       <source>
 There was no output from the command.</source>
       <translation>
 There was no output from the command.</translation>
     </message>
     <message>
-      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="430"/>
+      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="426"/>
       <source>
 Output:
 </source>
@@ -2945,52 +2945,52 @@ Output:
 </translation>
     </message>
     <message>
-      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="434"/>
+      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="430"/>
       <source>External command crashed.</source>
       <translation>External command crashed.</translation>
     </message>
     <message>
-      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="435"/>
+      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="431"/>
       <source>Command &lt;i&gt;%1&lt;/i&gt; crashed.</source>
       <translation>Command &lt;i&gt;%1&lt;/i&gt; crashed.</translation>
     </message>
     <message>
-      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="440"/>
+      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="436"/>
       <source>External command failed to start.</source>
       <translation>External command failed to start.</translation>
     </message>
     <message>
-      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="441"/>
+      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="437"/>
       <source>Command &lt;i&gt;%1&lt;/i&gt; failed to start.</source>
       <translation>Command &lt;i&gt;%1&lt;/i&gt; failed to start.</translation>
     </message>
     <message>
-      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="445"/>
+      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="441"/>
       <source>Internal error when starting command.</source>
       <translation>Internal error when starting command.</translation>
     </message>
     <message>
-      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="446"/>
+      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="442"/>
       <source>Bad parameters for process job call.</source>
       <translation>Bad parameters for process job call.</translation>
     </message>
     <message>
-      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="450"/>
+      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="446"/>
       <source>External command failed to finish.</source>
       <translation>External command failed to finish.</translation>
     </message>
     <message>
-      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="451"/>
+      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="447"/>
       <source>Command &lt;i&gt;%1&lt;/i&gt; failed to finish in %2 seconds.</source>
       <translation>Command &lt;i&gt;%1&lt;/i&gt; failed to finish in %2 seconds.</translation>
     </message>
     <message>
-      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="458"/>
+      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="454"/>
       <source>External command finished with errors.</source>
       <translation>External command finished with errors.</translation>
     </message>
     <message>
-      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="459"/>
+      <location filename="../src/libcalamares/utils/CalamaresUtilsSystem.cpp" line="455"/>
       <source>Command &lt;i&gt;%1&lt;/i&gt; finished with exit code %2.</source>
       <translation>Command &lt;i&gt;%1&lt;/i&gt; finished with exit code %2.</translation>
     </message>
diff --git a/lang/python.pot b/lang/python.pot
index 335a89206b396c2253dfdd122e2f69959f18a75d..9520a6d6b979f32601f6244a550551acf6249f3b 100644
--- a/lang/python.pot
+++ b/lang/python.pot
@@ -2,32 +2,32 @@
 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-# 
+#
 #, fuzzy
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-09-08 13:31+0200\n"
+"POT-Creation-Date: 2021-09-22 11:02+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Language: \n"
 "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
 
 #: src/modules/grubcfg/main.py:28
 msgid "Configure GRUB."
-msgstr "Configure GRUB."
+msgstr ""
 
 #: src/modules/mount/main.py:30
 msgid "Mounting partitions."
-msgstr "Mounting partitions."
+msgstr ""
 
-#: src/modules/mount/main.py:144 src/modules/initcpiocfg/main.py:197
-#: src/modules/initcpiocfg/main.py:201
+#: src/modules/mount/main.py:144 src/modules/initcpiocfg/main.py:227
+#: src/modules/initcpiocfg/main.py:231
 #: src/modules/luksopenswaphookcfg/main.py:86
 #: src/modules/luksopenswaphookcfg/main.py:90 src/modules/rawfs/main.py:164
 #: src/modules/initramfscfg/main.py:85 src/modules/initramfscfg/main.py:89
@@ -36,372 +36,349 @@ msgstr "Mounting partitions."
 #: src/modules/fstab/main.py:361 src/modules/fstab/main.py:388
 #: src/modules/localecfg/main.py:135 src/modules/networkcfg/main.py:42
 msgid "Configuration Error"
-msgstr "Configuration Error"
+msgstr ""
 
-#: src/modules/mount/main.py:145 src/modules/initcpiocfg/main.py:198
+#: src/modules/mount/main.py:145 src/modules/initcpiocfg/main.py:228
 #: src/modules/luksopenswaphookcfg/main.py:87 src/modules/rawfs/main.py:165
 #: src/modules/initramfscfg/main.py:86 src/modules/openrcdmcryptcfg/main.py:73
 #: src/modules/fstab/main.py:356
 msgid "No partitions are defined for <pre>{!s}</pre> to use."
-msgstr "No partitions are defined for <pre>{!s}</pre> to use."
+msgstr ""
 
 #: src/modules/services-systemd/main.py:26
 msgid "Configure systemd services"
-msgstr "Configure systemd services"
+msgstr ""
 
 #: src/modules/services-systemd/main.py:59
 #: src/modules/services-openrc/main.py:93
 msgid "Cannot modify service"
-msgstr "Cannot modify service"
+msgstr ""
 
 #: src/modules/services-systemd/main.py:60
 msgid ""
 "<code>systemctl {arg!s}</code> call in chroot returned error code {num!s}."
 msgstr ""
-"<code>systemctl {arg!s}</code> call in chroot returned error code {num!s}."
 
 #: src/modules/services-systemd/main.py:63
 #: src/modules/services-systemd/main.py:67
 msgid "Cannot enable systemd service <code>{name!s}</code>."
-msgstr "Cannot enable systemd service <code>{name!s}</code>."
+msgstr ""
 
 #: src/modules/services-systemd/main.py:65
 msgid "Cannot enable systemd target <code>{name!s}</code>."
-msgstr "Cannot enable systemd target <code>{name!s}</code>."
+msgstr ""
 
 #: src/modules/services-systemd/main.py:69
 msgid "Cannot disable systemd target <code>{name!s}</code>."
-msgstr "Cannot disable systemd target <code>{name!s}</code>."
+msgstr ""
 
 #: src/modules/services-systemd/main.py:71
 msgid "Cannot mask systemd unit <code>{name!s}</code>."
-msgstr "Cannot mask systemd unit <code>{name!s}</code>."
+msgstr ""
 
 #: src/modules/services-systemd/main.py:73
 msgid ""
-"Unknown systemd commands <code>{command!s}</code> and "
-"<code>{suffix!s}</code> for unit {name!s}."
+"Unknown systemd commands <code>{command!s}</code> and <code>{suffix!s}</"
+"code> for unit {name!s}."
 msgstr ""
-"Unknown systemd commands <code>{command!s}</code> and "
-"<code>{suffix!s}</code> for unit {name!s}."
 
 #: src/modules/umount/main.py:31
 msgid "Unmount file systems."
-msgstr "Unmount file systems."
+msgstr ""
 
 #: src/modules/unpackfs/main.py:35
 msgid "Filling up filesystems."
-msgstr "Filling up filesystems."
+msgstr ""
 
 #: src/modules/unpackfs/main.py:255
 msgid "rsync failed with error code {}."
-msgstr "rsync failed with error code {}."
+msgstr ""
 
 #: src/modules/unpackfs/main.py:300
 msgid "Unpacking image {}/{}, file {}/{}"
-msgstr "Unpacking image {}/{}, file {}/{}"
+msgstr ""
 
 #: src/modules/unpackfs/main.py:315
 msgid "Starting to unpack {}"
-msgstr "Starting to unpack {}"
+msgstr ""
 
 #: src/modules/unpackfs/main.py:324 src/modules/unpackfs/main.py:464
 msgid "Failed to unpack image \"{}\""
-msgstr "Failed to unpack image \"{}\""
+msgstr ""
 
 #: src/modules/unpackfs/main.py:431
 msgid "No mount point for root partition"
-msgstr "No mount point for root partition"
+msgstr ""
 
 #: src/modules/unpackfs/main.py:432
 msgid "globalstorage does not contain a \"rootMountPoint\" key, doing nothing"
-msgstr "globalstorage does not contain a \"rootMountPoint\" key, doing nothing"
+msgstr ""
 
 #: src/modules/unpackfs/main.py:437
 msgid "Bad mount point for root partition"
-msgstr "Bad mount point for root partition"
+msgstr ""
 
 #: src/modules/unpackfs/main.py:438
 msgid "rootMountPoint is \"{}\", which does not exist, doing nothing"
-msgstr "rootMountPoint is \"{}\", which does not exist, doing nothing"
+msgstr ""
 
 #: src/modules/unpackfs/main.py:454 src/modules/unpackfs/main.py:458
 #: src/modules/unpackfs/main.py:478
 msgid "Bad unsquash configuration"
-msgstr "Bad unsquash configuration"
+msgstr ""
 
 #: src/modules/unpackfs/main.py:455
 msgid "The filesystem for \"{}\" ({}) is not supported by your current kernel"
-msgstr "The filesystem for \"{}\" ({}) is not supported by your current kernel"
+msgstr ""
 
 #: src/modules/unpackfs/main.py:459
 msgid "The source filesystem \"{}\" does not exist"
-msgstr "The source filesystem \"{}\" does not exist"
+msgstr ""
 
 #: src/modules/unpackfs/main.py:465
 msgid ""
 "Failed to find unsquashfs, make sure you have the squashfs-tools package "
 "installed"
 msgstr ""
-"Failed to find unsquashfs, make sure you have the squashfs-tools package "
-"installed"
 
 #: src/modules/unpackfs/main.py:479
 msgid "The destination \"{}\" in the target system is not a directory"
-msgstr "The destination \"{}\" in the target system is not a directory"
+msgstr ""
 
 #: src/modules/displaymanager/main.py:526
 msgid "Cannot write KDM configuration file"
-msgstr "Cannot write KDM configuration file"
+msgstr ""
 
 #: src/modules/displaymanager/main.py:527
 msgid "KDM config file {!s} does not exist"
-msgstr "KDM config file {!s} does not exist"
+msgstr ""
 
 #: src/modules/displaymanager/main.py:588
 msgid "Cannot write LXDM configuration file"
-msgstr "Cannot write LXDM configuration file"
+msgstr ""
 
 #: src/modules/displaymanager/main.py:589
 msgid "LXDM config file {!s} does not exist"
-msgstr "LXDM config file {!s} does not exist"
+msgstr ""
 
 #: src/modules/displaymanager/main.py:672
 msgid "Cannot write LightDM configuration file"
-msgstr "Cannot write LightDM configuration file"
+msgstr ""
 
 #: src/modules/displaymanager/main.py:673
 msgid "LightDM config file {!s} does not exist"
-msgstr "LightDM config file {!s} does not exist"
+msgstr ""
 
 #: src/modules/displaymanager/main.py:747
 msgid "Cannot configure LightDM"
-msgstr "Cannot configure LightDM"
+msgstr ""
 
 #: src/modules/displaymanager/main.py:748
 msgid "No LightDM greeter installed."
-msgstr "No LightDM greeter installed."
+msgstr ""
 
 #: src/modules/displaymanager/main.py:779
 msgid "Cannot write SLIM configuration file"
-msgstr "Cannot write SLIM configuration file"
+msgstr ""
 
 #: src/modules/displaymanager/main.py:780
 msgid "SLIM config file {!s} does not exist"
-msgstr "SLIM config file {!s} does not exist"
+msgstr ""
 
 #: src/modules/displaymanager/main.py:906
 msgid "No display managers selected for the displaymanager module."
-msgstr "No display managers selected for the displaymanager module."
+msgstr ""
 
 #: src/modules/displaymanager/main.py:907
 msgid ""
 "The displaymanagers list is empty or undefined in both globalstorage and "
 "displaymanager.conf."
 msgstr ""
-"The displaymanagers list is empty or undefined in both globalstorage and "
-"displaymanager.conf."
 
 #: src/modules/displaymanager/main.py:989
 msgid "Display manager configuration was incomplete"
-msgstr "Display manager configuration was incomplete"
+msgstr ""
 
 #: src/modules/initcpiocfg/main.py:28
 msgid "Configuring mkinitcpio."
-msgstr "Configuring mkinitcpio."
+msgstr ""
 
-#: src/modules/initcpiocfg/main.py:202
+#: src/modules/initcpiocfg/main.py:232
 #: src/modules/luksopenswaphookcfg/main.py:91
 #: src/modules/initramfscfg/main.py:90 src/modules/openrcdmcryptcfg/main.py:77
 #: src/modules/fstab/main.py:362 src/modules/localecfg/main.py:136
 #: src/modules/networkcfg/main.py:43
 msgid "No root mount point is given for <pre>{!s}</pre> to use."
-msgstr "No root mount point is given for <pre>{!s}</pre> to use."
+msgstr ""
 
 #: src/modules/luksopenswaphookcfg/main.py:26
 msgid "Configuring encrypted swap."
-msgstr "Configuring encrypted swap."
+msgstr ""
 
 #: src/modules/rawfs/main.py:26
 msgid "Installing data."
-msgstr "Installing data."
+msgstr ""
 
 #: src/modules/services-openrc/main.py:29
 msgid "Configure OpenRC services"
-msgstr "Configure OpenRC services"
+msgstr ""
 
 #: src/modules/services-openrc/main.py:57
 msgid "Cannot add service {name!s} to run-level {level!s}."
-msgstr "Cannot add service {name!s} to run-level {level!s}."
+msgstr ""
 
 #: src/modules/services-openrc/main.py:59
 msgid "Cannot remove service {name!s} from run-level {level!s}."
-msgstr "Cannot remove service {name!s} from run-level {level!s}."
+msgstr ""
 
 #: src/modules/services-openrc/main.py:61
 msgid ""
 "Unknown service-action <code>{arg!s}</code> for service {name!s} in run-"
 "level {level!s}."
 msgstr ""
-"Unknown service-action <code>{arg!s}</code> for service {name!s} in run-"
-"level {level!s}."
 
 #: src/modules/services-openrc/main.py:94
 msgid ""
 "<code>rc-update {arg!s}</code> call in chroot returned error code {num!s}."
 msgstr ""
-"<code>rc-update {arg!s}</code> call in chroot returned error code {num!s}."
 
 #: src/modules/services-openrc/main.py:101
 msgid "Target runlevel does not exist"
-msgstr "Target runlevel does not exist"
+msgstr ""
 
 #: src/modules/services-openrc/main.py:102
 msgid ""
 "The path for runlevel {level!s} is <code>{path!s}</code>, which does not "
 "exist."
 msgstr ""
-"The path for runlevel {level!s} is <code>{path!s}</code>, which does not "
-"exist."
 
 #: src/modules/services-openrc/main.py:110
 msgid "Target service does not exist"
-msgstr "Target service does not exist"
+msgstr ""
 
 #: src/modules/services-openrc/main.py:111
 msgid ""
-"The path for service {name!s} is <code>{path!s}</code>, which does not "
-"exist."
+"The path for service {name!s} is <code>{path!s}</code>, which does not exist."
 msgstr ""
-"The path for service {name!s} is <code>{path!s}</code>, which does not "
-"exist."
 
 #: src/modules/plymouthcfg/main.py:27
 msgid "Configure Plymouth theme"
-msgstr "Configure Plymouth theme"
+msgstr ""
 
 #: src/modules/packages/main.py:50 src/modules/packages/main.py:59
 #: src/modules/packages/main.py:69
 msgid "Install packages."
-msgstr "Install packages."
+msgstr ""
 
 #: src/modules/packages/main.py:57
 #, python-format
 msgid "Processing packages (%(count)d / %(total)d)"
-msgstr "Processing packages (%(count)d / %(total)d)"
+msgstr ""
 
 #: src/modules/packages/main.py:62
 #, python-format
 msgid "Installing one package."
 msgid_plural "Installing %(num)d packages."
-msgstr[0] "Installing one package."
-msgstr[1] "Installing %(num)d packages."
+msgstr[0] ""
+msgstr[1] ""
 
 #: src/modules/packages/main.py:65
 #, python-format
 msgid "Removing one package."
 msgid_plural "Removing %(num)d packages."
-msgstr[0] "Removing one package."
-msgstr[1] "Removing %(num)d packages."
+msgstr[0] ""
+msgstr[1] ""
 
 #: src/modules/packages/main.py:638 src/modules/packages/main.py:650
 #: src/modules/packages/main.py:678
 msgid "Package Manager error"
-msgstr "Package Manager error"
+msgstr ""
 
 #: src/modules/packages/main.py:639
 msgid ""
 "The package manager could not prepare updates. The command <pre>{!s}</pre> "
 "returned error code {!s}."
 msgstr ""
-"The package manager could not prepare updates. The command <pre>{!s}</pre> "
-"returned error code {!s}."
 
 #: src/modules/packages/main.py:651
 msgid ""
-"The package manager could not update the system. The command <pre>{!s}</pre>"
-" returned error code {!s}."
+"The package manager could not update the system. The command <pre>{!s}</pre> "
+"returned error code {!s}."
 msgstr ""
-"The package manager could not update the system. The command <pre>{!s}</pre>"
-" returned error code {!s}."
 
 #: src/modules/packages/main.py:679
 msgid ""
 "The package manager could not make changes to the installed system. The "
 "command <pre>{!s}</pre> returned error code {!s}."
 msgstr ""
-"The package manager could not make changes to the installed system. The "
-"command <pre>{!s}</pre> returned error code {!s}."
 
 #: src/modules/bootloader/main.py:43
 msgid "Install bootloader."
-msgstr "Install bootloader."
+msgstr ""
 
 #: src/modules/bootloader/main.py:508
 msgid "Bootloader installation error"
-msgstr "Bootloader installation error"
+msgstr ""
 
 #: src/modules/bootloader/main.py:509
 msgid ""
-"The bootloader could not be installed. The installation command "
-"<pre>{!s}</pre> returned error code {!s}."
+"The bootloader could not be installed. The installation command <pre>{!s}</"
+"pre> returned error code {!s}."
 msgstr ""
-"The bootloader could not be installed. The installation command "
-"<pre>{!s}</pre> returned error code {!s}."
 
 #: src/modules/hwclock/main.py:26
 msgid "Setting hardware clock."
-msgstr "Setting hardware clock."
+msgstr ""
 
 #: src/modules/mkinitfs/main.py:27
 msgid "Creating initramfs with mkinitfs."
-msgstr "Creating initramfs with mkinitfs."
+msgstr ""
 
 #: src/modules/mkinitfs/main.py:49
 msgid "Failed to run mkinitfs on the target"
-msgstr "Failed to run mkinitfs on the target"
+msgstr ""
 
 #: src/modules/mkinitfs/main.py:50 src/modules/dracut/main.py:50
 msgid "The exit code was {}"
-msgstr "The exit code was {}"
+msgstr ""
 
 #: src/modules/dracut/main.py:27
 msgid "Creating initramfs with dracut."
-msgstr "Creating initramfs with dracut."
+msgstr ""
 
 #: src/modules/dracut/main.py:49
 msgid "Failed to run dracut on the target"
-msgstr "Failed to run dracut on the target"
+msgstr ""
 
 #: src/modules/initramfscfg/main.py:32
 msgid "Configuring initramfs."
-msgstr "Configuring initramfs."
+msgstr ""
 
 #: src/modules/openrcdmcryptcfg/main.py:26
 msgid "Configuring OpenRC dmcrypt service."
-msgstr "Configuring OpenRC dmcrypt service."
+msgstr ""
 
 #: src/modules/fstab/main.py:29
 msgid "Writing fstab."
-msgstr "Writing fstab."
+msgstr ""
 
 #: src/modules/fstab/main.py:389
 msgid "No <pre>{!s}</pre> configuration is given for <pre>{!s}</pre> to use."
-msgstr "No <pre>{!s}</pre> configuration is given for <pre>{!s}</pre> to use."
+msgstr ""
 
 #: src/modules/dummypython/main.py:35
 msgid "Dummy python job."
-msgstr "Dummy python job."
+msgstr ""
 
 #: src/modules/dummypython/main.py:37 src/modules/dummypython/main.py:93
 #: src/modules/dummypython/main.py:94
 msgid "Dummy python step {}"
-msgstr "Dummy python step {}"
+msgstr ""
 
 #: src/modules/localecfg/main.py:30
 msgid "Configuring locales."
-msgstr "Configuring locales."
+msgstr ""
 
 #: src/modules/networkcfg/main.py:29
 msgid "Saving network configuration."
-msgstr "Saving network configuration."
+msgstr ""
diff --git a/src/calamares/testmain.cpp b/src/calamares/testmain.cpp
index 6d75a4fbb1aa73d5dd51d9d25212f2fea999db19..9d018903154cc2316601f8facc8b2fdf148328fe 100644
--- a/src/calamares/testmain.cpp
+++ b/src/calamares/testmain.cpp
@@ -71,22 +71,20 @@ handle_args( QCoreApplication& a )
 {
     QCommandLineOption debugLevelOption(
         QStringLiteral( "D" ), "Verbose output for debugging purposes (0-8), ignored.", "level" );
-    QCommandLineOption globalOption( QStringList() << QStringLiteral( "g" ) << QStringLiteral( "global " ),
+    QCommandLineOption globalOption( { QStringLiteral( "g" ), QStringLiteral( "global" ) },
                                      QStringLiteral( "Global settings document" ),
                                      "global.yaml" );
-    QCommandLineOption jobOption( QStringList() << QStringLiteral( "j" ) << QStringLiteral( "job" ),
-                                  QStringLiteral( "Job settings document" ),
-                                  "job.yaml" );
-    QCommandLineOption langOption( QStringList() << QStringLiteral( "l" ) << QStringLiteral( "language" ),
+    QCommandLineOption jobOption(
+        { QStringLiteral( "j" ), QStringLiteral( "job" ) }, QStringLiteral( "Job settings document" ), "job.yaml" );
+    QCommandLineOption langOption( { QStringLiteral( "l" ), QStringLiteral( "language" ) },
                                    QStringLiteral( "Language (global)" ),
                                    "languagecode" );
-    QCommandLineOption brandOption( QStringList() << QStringLiteral( "b" ) << QStringLiteral( "branding" ),
+    QCommandLineOption brandOption( { QStringLiteral( "b" ), QStringLiteral( "branding" ) },
                                     QStringLiteral( "Branding directory" ),
                                     "path/to/branding.desc",
                                     "src/branding/default/branding.desc" );
-    QCommandLineOption uiOption( QStringList() << QStringLiteral( "U" ) << QStringLiteral( "ui" ),
-                                 QStringLiteral( "Enable UI" ) );
-    QCommandLineOption slideshowOption( QStringList() << QStringLiteral( "s" ) << QStringLiteral( "slideshow" ),
+    QCommandLineOption uiOption( { QStringLiteral( "U" ), QStringLiteral( "ui" ) }, QStringLiteral( "Enable UI" ) );
+    QCommandLineOption slideshowOption( { QStringLiteral( "s" ), QStringLiteral( "slideshow" ) },
                                         QStringLiteral( "Run slideshow module" ) );
     QCommandLineParser parser;
     parser.setApplicationDescription( "Calamares module tester" );
@@ -101,7 +99,7 @@ handle_args( QCoreApplication& a )
     parser.addOption( uiOption );
     parser.addOption( slideshowOption );
 #ifdef WITH_PYTHON
-    QCommandLineOption pythonOption( QStringList() << QStringLiteral( "P" ) << QStringLiteral( "no-injected-python" ),
+    QCommandLineOption pythonOption( { QStringLiteral( "P" ), QStringLiteral( "no-injected-python" ) },
                                      QStringLiteral( "Do not disable potentially-harmful Python commands" ) );
     parser.addOption( pythonOption );
 #endif
@@ -143,8 +141,7 @@ handle_args( QCoreApplication& a )
                               parser.value( langOption ),
                               parser.value( brandOption ),
                               parser.isSet( slideshowOption ) || parser.isSet( uiOption ),
-                              pythonInjection
-        };
+                              pythonInjection };
     }
 }
 
@@ -299,7 +296,8 @@ load_module( const ModuleConfig& moduleConfig )
     bool ok = false;
     QVariantMap descriptor;
 
-    for ( const QString& prefix : QStringList { "./", "src/modules/", "modules/" } )
+    QStringList moduleDirectories { "./", "src/modules/", "modules/", CMAKE_INSTALL_FULL_LIBDIR "/calamares/modules/" };
+    for ( const QString& prefix : qAsConst( moduleDirectories ) )
     {
         // Could be a complete path, eg. src/modules/dummycpp/module.desc
         fi = QFileInfo( prefix + moduleName );
@@ -325,12 +323,23 @@ load_module( const ModuleConfig& moduleConfig )
             {
                 break;
             }
+            else
+            {
+                if ( !fi.exists() )
+                {
+                    cDebug() << "Expected a descriptor file" << fi.path();
+                }
+                else
+                {
+                    cDebug() << "Read descriptor" << fi.path() << "and it was empty.";
+                }
+            }
         }
     }
 
     if ( !ok )
     {
-        cWarning() << "No suitable module descriptor found.";
+        cWarning() << "No suitable module descriptor found in" << Logger::DebugList( moduleDirectories );
         return nullptr;
     }
 
@@ -461,7 +470,7 @@ main( int argc, char* argv[] )
 #ifdef WITH_PYTHON
     if ( module.m_pythonInjection )
     {
-        Calamares::PythonJob::setInjectedPreScript(pythonPreScript);
+        Calamares::PythonJob::setInjectedPreScript( pythonPreScript );
     }
 #endif
 #ifdef WITH_QML
diff --git a/src/libcalamares/PythonJob.cpp b/src/libcalamares/PythonJob.cpp
index 1a4683c29ce4d626daf258a9c19737e19def2b59..201f56a1593cf9608fbfb01df17e09711e0e593c 100644
--- a/src/libcalamares/PythonJob.cpp
+++ b/src/libcalamares/PythonJob.cpp
@@ -330,7 +330,8 @@ void
 PythonJob::setInjectedPreScript( const char* preScript )
 {
     s_preScript = preScript;
-    cDebug() << "Python pre-script set to" << Logger::Pointer( preScript );
+    cDebug() << "Python pre-script set to string" << Logger::Pointer( preScript ) << "length"
+             << ( preScript ? strlen( preScript ) : 0 );
 }
 
 }  // namespace Calamares
diff --git a/src/modules/displaymanager/main.py b/src/modules/displaymanager/main.py
index 8b63b9e8d8fd2232667f709239e1e14a8d722eba..5fb228682239c5b3dc77f64cd67d2350c2de5cf3 100644
--- a/src/modules/displaymanager/main.py
+++ b/src/modules/displaymanager/main.py
@@ -198,7 +198,7 @@ desktop_environments = [
     DesktopEnvironment('/usr/bin/fvwm3', 'fvwm3'),
     DesktopEnvironment('/usr/bin/sway', 'sway'),
     DesktopEnvironment('/usr/bin/ukui-session', 'ukui'),
-    DesktopEnvironment('/usr/bin/cutefish-session', 'cutefish-xsession'),    
+    DesktopEnvironment('/usr/bin/cutefish-session', 'cutefish-xsession'),
 ]
 
 
@@ -923,7 +923,7 @@ def run():
             else:
                 dm_instance = None
         else:
-            libcalamares.utils.debug("{!s} has {!d} implementation classes.".format(dm).format(len(impl)))
+            libcalamares.utils.debug("{!s} has {!s} implementation classes.".format(dm, len(impl)))
 
         if dm_instance is None:
             libcalamares.utils.debug("{!s} selected but not installed".format(dm))
diff --git a/src/modules/fstab/fstab.conf b/src/modules/fstab/fstab.conf
index 21f6ffce3001b6c1bba0eb66c43ba6d5d516547e..fdf0d41eceb7b6eaeb4df570615d1398192b8e12 100644
--- a/src/modules/fstab/fstab.conf
+++ b/src/modules/fstab/fstab.conf
@@ -13,7 +13,7 @@
 # options from this mapping.
 mountOptions:
     default: defaults,noatime
-    btrfs: defaults,noatime,space_cache,autodefrag
+    btrfs: defaults,noatime,space_cache,autodefrag,compress=zstd
 
 # Mount options to use for the EFI System Partition. If not defined, the
 # *mountOptions* for *vfat* are used, or if that is not set either,
@@ -38,10 +38,10 @@ efiMountOptions: umask=0077
 #     swap: discard
 #     btrfs: discard,compress=lzo
 #
-# The standard configuration applies only lzo compression to btrfs
+# The standard configuration applies asynchronous discard support and ssd optimizations to btrfs
 # and does nothing for other filesystems.
 ssdExtraMountOptions:
-    btrfs: compress=lzo
+    btrfs: discard=async,ssd
 
 # Additional options added to each line in /etc/crypttab
 crypttabOptions: luks
diff --git a/src/modules/initcpiocfg/main.py b/src/modules/initcpiocfg/main.py
index 6247aecccbc6e6d91c4c27040f590c30877df4bf..d61cbbaed85b60990d38292397fdefb0a58f711b 100644
--- a/src/modules/initcpiocfg/main.py
+++ b/src/modules/initcpiocfg/main.py
@@ -150,7 +150,8 @@ def find_initcpio_features(partitions, root_mount_point):
         "modconf",
         "block",
         "keyboard",
-        "keymap"
+        "keymap",
+        "consolefont",
     ]
     modules = []
     files = []
diff --git a/src/modules/keyboard/Config.cpp b/src/modules/keyboard/Config.cpp
index f1b6efebad88ead7a5cfefc30790e78d3d2d0a99..720588810aa1b4791d32dd62573dd8411c8fc387 100644
--- a/src/modules/keyboard/Config.cpp
+++ b/src/modules/keyboard/Config.cpp
@@ -472,7 +472,7 @@ Config::guessLocaleKeyboardLayout()
         { "el_GR", "gr" }, /* Greek in Greece */
         { "ig_NG", "igbo_NG" }, /* Igbo in Nigeria */
         { "ha_NG", "hausa_NG" }, /* Hausa */
-        { "en_IN", "eng_in" }, /* India, English with Rupee */
+        { "en_IN", "us" }, /* India, US English keyboards are common in India */
     } );
 
     // Try to preselect a layout, depending on language and locale
diff --git a/src/modules/locale/LocaleConfiguration.cpp b/src/modules/locale/LocaleConfiguration.cpp
index c208dc02d6b5bb102bb0a228cac4a88756c085e1..b7b895290e3f19b9dd0973ae853143d88a382fc6 100644
--- a/src/modules/locale/LocaleConfiguration.cpp
+++ b/src/modules/locale/LocaleConfiguration.cpp
@@ -201,6 +201,10 @@ LocaleConfiguration::fromLanguageAndLocation( const QString& languageLocale,
                 // but nearly all its native speakers also speak English,
                 // and migrants are likely to use English.
                 { "IE", "en" },
+                // India has many languages even though Hindi is known as
+                // national language but English is used in all computer
+                // and mobile devices.
+                { "IN", "en" },
                 { "IT", "it" },
                 { "MA", "ar" },
                 { "MK", "mk" },
diff --git a/src/modules/networkcfg/main.py b/src/modules/networkcfg/main.py
index 35bf67d63ebd317f4b01a17e0552801296297d08..0ee47c1aa7005e72be7b5eab7a0e0386cab90b97 100644
--- a/src/modules/networkcfg/main.py
+++ b/src/modules/networkcfg/main.py
@@ -29,23 +29,83 @@ def pretty_name():
     return _("Saving network configuration.")
 
 
+def get_live_user():
+    """
+    Gets the "live user" login. This might be "live", or "nitrux",
+    or something similar: it is the login name used *right now*,
+    and network configurations saved for that user, should be applied
+    also for the installed user (which probably has a different name).
+    """
+    # getlogin() is a thin-wrapper, and depends on getlogin(3),
+    # which reads utmp -- and utmp isn't always set up right.
+    try:
+        return os.getlogin()
+    except OSError:
+        pass
+    # getpass will return the **current** user, which is generally root.
+    # That isn't very useful, because the network settings have been
+    # made outside of Calamares-running-as-root, as a different user.
+    #
+    # If Calamares is running as non-root, though, this is fine.
+    import getpass
+    name = getpass.getuser()
+    if name != "root":
+        return name
+
+    # TODO: other mechanisms, e.g. guessing that "live" is the name
+    # TODO: support a what-is-the-live-user setting
+    return None
+
+
+def replace_username(nm_config_filename, live_user, target_user):
+    """
+    If @p live_user isn't None, then go through the given
+    file and replace @p live_user by the @p target_user.
+
+    Reads the file, then (re-)writes it with new permissions lives.
+    """
+    # FIXME: Perhaps if live_user is None, we should just replace **all**
+    #        permissions lines? After all, this is supposed to be a live
+    #        system so **whatever** NM networks are configured, should be
+    #        available to the new user.
+    if live_user is None:
+        return
+    if not os.path.exists(nm_config_filename):
+        return
+
+    with open(nm_config_filename, "r") as network_conf:
+        text = network_conf.readlines()
+
+    live_permissions = 'permissions=user:{}:;'.format(live_user)
+    target_permissions = 'permissions=user:{}:;\n'.format(target_user)
+    with open(nm_config_filename, "w") as network_conf:
+        for line in text:
+            if live_permissions in line:
+                line = target_permissions
+            network_conf.write(line)
+
+
+def path_pair(root_mount_point, relative_path):
+    """
+    Returns /relative_path and the relative path in the target system.
+    """
+    return ("/" + relative_path, os.path.join(root_mount_point, relative_path))
+
+
 def run():
     """
     Setup network configuration
     """
     root_mount_point = libcalamares.globalstorage.value("rootMountPoint")
     user = libcalamares.globalstorage.value("username")
-    live_user = os.getlogin()
+    live_user = get_live_user()
 
     if root_mount_point is None:
         libcalamares.utils.warning("rootMountPoint is empty, {!s}".format(root_mount_point))
         return (_("Configuration Error"),
                 _("No root mount point is given for <pre>{!s}</pre> to use." ).format("networkcfg"))
 
-    source_nm = "/etc/NetworkManager/system-connections/"
-    target_nm = os.path.join(
-        root_mount_point, "etc/NetworkManager/system-connections/"
-        )
+    source_nm, target_nm = path_pair(root_mount_point, "etc/NetworkManager/system-connections/")
 
     # Sanity checks.  We don't want to do anything if a network
     # configuration already exists on the target
@@ -63,27 +123,16 @@ def run():
 
             try:
                 shutil.copy(source_network, target_network, follow_symlinks=False)
-                if live_user in open(target_network).read():
-                    text = []
-                    with open(target_network, "r") as network_conf:
-                        text = network_conf.readlines()
-                        with open(target_network, "w") as network_conf:
-                            for line in text:
-                                if 'permissions=user:{}:;'.format(live_user) in line:
-                                    line = 'permissions=user:{}:;\n'.format(user)
-                                network_conf.write(line)
-                    network_conf.close()
+                replace_username(target_network, live_user, user)
             except FileNotFoundError:
                 libcalamares.utils.debug(
-                    "Can't copy network configuration files in "
-                    + "{}".format(source_network)
+                    "Can't copy network configuration files in {}".format(source_network)
                     )
             except FileExistsError:
                 pass
 
     # We need to overwrite the default resolv.conf in the chroot.
-    source_resolv = "/etc/resolv.conf"
-    target_resolv = os.path.join(root_mount_point, "etc/resolv.conf")
+    source_resolv, target_resolv = path_pair(root_mount_point, "etc/resolv.conf")
     if source_resolv != target_resolv and os.path.exists(source_resolv):
         try:
             os.remove(target_resolv)
diff --git a/src/modules/partition/Config.cpp b/src/modules/partition/Config.cpp
index 508231a75fe9b7fa358609ee4ecbafc6d5f1639d..05975213efa4f82b0de6c82d7fc9a244c89e20b0 100644
--- a/src/modules/partition/Config.cpp
+++ b/src/modules/partition/Config.cpp
@@ -13,6 +13,7 @@
 
 #include "GlobalStorage.h"
 #include "JobQueue.h"
+#include "partition/PartitionSize.h"
 #include "utils/Logger.h"
 #include "utils/Variant.h"
 
@@ -233,7 +234,25 @@ fillGSConfigurationEFI( Calamares::GlobalStorage* gs, const QVariantMap& configu
     // Read and parse key efiSystemPartitionSize
     if ( configurationMap.contains( "efiSystemPartitionSize" ) )
     {
-        gs->insert( "efiSystemPartitionSize", CalamaresUtils::getString( configurationMap, "efiSystemPartitionSize" ) );
+        const QString sizeString = CalamaresUtils::getString( configurationMap, "efiSystemPartitionSize" );
+        CalamaresUtils::Partition::PartitionSize part_size
+            = CalamaresUtils::Partition::PartitionSize( sizeString );
+        if (part_size.isValid())
+        {
+            // Insert once as string, once as a size-in-bytes;
+            // changes to these keys should be synchronized with PartUtils.cpp
+            gs->insert( "efiSystemPartitionSize",  sizeString );
+            gs->insert( "efiSystemPartitionSize_i", part_size.toBytes());
+
+            if (part_size.toBytes() != PartUtils::efiFilesystemMinimumSize())
+            {
+                cWarning() << "EFI partition size" << sizeString << "has been adjusted to" << PartUtils::efiFilesystemMinimumSize() << "bytes";
+            }
+        }
+        else
+        {
+            cWarning() << "EFI partition size" << sizeString << "is invalid, ignored";
+        }
     }
 
     // Read and parse key efiSystemPartitionName
diff --git a/src/modules/partition/PartitionViewStep.cpp b/src/modules/partition/PartitionViewStep.cpp
index c17954810101ca51d20437ba938cf8d31877c272..a6b5e1dd8b887fd0340fcfc54c0bff83c0bf983d 100644
--- a/src/modules/partition/PartitionViewStep.cpp
+++ b/src/modules/partition/PartitionViewStep.cpp
@@ -558,9 +558,10 @@ PartitionViewStep::onLeave()
             if ( !okSize )
             {
                 cDebug() << o << "ESP too small";
+                const auto atLeastBytes = PartUtils::efiFilesystemMinimumSize();
+                const auto atLeastMiB = CalamaresUtils::BytesToMiB( atLeastBytes );
                 description.append( ' ' );
-                description.append( tr( "The filesystem must be at least %1 MiB in size." )
-                                        .arg( PartUtils::efiFilesystemMinimumSize() ) );
+                description.append( tr( "The filesystem must be at least %1 MiB in size." ).arg( atLeastMiB ) );
             }
             if ( !okFlag )
             {
diff --git a/src/modules/partition/core/PartUtils.cpp b/src/modules/partition/core/PartUtils.cpp
index 806c0ceb37113a2c222246eba6c263ba98cc7dbe..507330a80b6edb4e540fa4f72892a6ccdf88a763 100644
--- a/src/modules/partition/core/PartUtils.cpp
+++ b/src/modules/partition/core/PartUtils.cpp
@@ -471,9 +471,12 @@ bool
 isEfiFilesystemSuitableSize( const Partition* candidate )
 {
     auto size = candidate->capacity();  // bytes
+    if ( size <= 0 )
+    {
+        return false;
+    }
 
-    using CalamaresUtils::Units::operator""_MiB;
-    if ( size >= 300_MiB )
+    if ( size_t( size ) >= efiFilesystemMinimumSize() )
     {
         return true;
     }
@@ -522,7 +525,22 @@ size_t
 efiFilesystemMinimumSize()
 {
     using CalamaresUtils::Units::operator""_MiB;
-    return 300_MiB;
+
+    auto uefisys_part_sizeB = 300_MiB;
+
+    // The default can be overridden; the key used here comes
+    // from the partition module Config.cpp
+    auto* gs = Calamares::JobQueue::instance()->globalStorage();
+    if ( gs->contains( "efiSystemPartitionSize_i" ) )
+    {
+        uefisys_part_sizeB = gs->value( "efiSystemPartitionSize_i" ).toLongLong();
+    }
+    // There is a lower limit of what can be configured
+    if ( uefisys_part_sizeB < 32_MiB )
+    {
+        uefisys_part_sizeB = 32_MiB;
+    }
+    return uefisys_part_sizeB;
 }
 
 
diff --git a/src/modules/partition/core/PartUtils.h b/src/modules/partition/core/PartUtils.h
index dd4efc8678a03a73c96c1c5664825b27dadb32cb..31b4cde84e0e1f857b1ec6e98874b98fddf90471 100644
--- a/src/modules/partition/core/PartUtils.h
+++ b/src/modules/partition/core/PartUtils.h
@@ -94,12 +94,18 @@ bool isEfiFilesystemSuitableType( const Partition* candidate );
  */
 bool isEfiFilesystemSuitableSize( const Partition* candidate );
 
-/** @brief Returns the minimum size of an EFI boot partition.
+/** @brief Returns the minimum size of an EFI boot partition in bytes.
  *
  * This is determined as 300MiB, based on the FAT32 standard
  * and EFI documentation (and not a little discussion in Calamares
  * issues about what works, what is effective, and what is mandated
  * by the standard and how all of those are different).
+ *
+ * This can be configured through the `partition.conf` file,
+ * key *efiSystemPartitionSize*, which will then apply to both
+ * automatic partitioning **and** the warning for manual partitioning.
+ *
+ * A minimum of 32MiB (which is bonkers-small) is enforced.
  */
 size_t efiFilesystemMinimumSize();
 
diff --git a/src/modules/partition/core/PartitionActions.cpp b/src/modules/partition/core/PartitionActions.cpp
index 422c1d38f185b0e042aa3743d5262321c403473c..8514bbe2cdc1cff9b9f13698a950b2a676cefc25 100644
--- a/src/modules/partition/core/PartitionActions.cpp
+++ b/src/modules/partition/core/PartitionActions.cpp
@@ -118,14 +118,7 @@ doAutopartition( PartitionCoreModule* core, Device* dev, Choices::AutoPartitionO
 
     if ( isEfi )
     {
-        int uefisys_part_sizeB = 300_MiB;
-        if ( gs->contains( "efiSystemPartitionSize" ) )
-        {
-            CalamaresUtils::Partition::PartitionSize part_size
-                = CalamaresUtils::Partition::PartitionSize( gs->value( "efiSystemPartitionSize" ).toString() );
-            uefisys_part_sizeB = part_size.toBytes( dev->capacity() );
-        }
-
+        size_t uefisys_part_sizeB = PartUtils::efiFilesystemMinimumSize();
         qint64 efiSectorCount = CalamaresUtils::bytesToSectors( uefisys_part_sizeB, dev->logicalSize() );
         Q_ASSERT( efiSectorCount > 0 );
 
diff --git a/src/modules/partition/partition.conf b/src/modules/partition/partition.conf
index 899eb6269db4e4589a4e50bb6f357658bbbf38c7..b03c855db645503c4fb86b4b4a554efbc4fb7834 100644
--- a/src/modules/partition/partition.conf
+++ b/src/modules/partition/partition.conf
@@ -10,6 +10,12 @@ efiSystemPartition:     "/boot/efi"
 
 # This optional setting specifies the size of the EFI system partition.
 # If nothing is specified, the default size of 300MiB will be used.
+#
+# This size applies both to automatic partitioning and the checks
+# during manual partitioning. A minimum of 32MiB is enforced,
+# 300MiB is the default, M is treated as MiB, and if you really want
+# one-million (10^6) bytes, use MB.
+#
 # efiSystemPartitionSize:     300M
 
 # This optional setting specifies the name of the EFI system partition (see
diff --git a/src/modules/unpackfs/main.py b/src/modules/unpackfs/main.py
index 8a5194843c1e1149dce1a0455e09400afb7a4c86..020db370c976932362cb82e31446ab635f3a92f1 100644
--- a/src/modules/unpackfs/main.py
+++ b/src/modules/unpackfs/main.py
@@ -22,13 +22,12 @@ import subprocess
 import sys
 import tempfile
 
-from libcalamares import *
-from libcalamares.utils import mount
+import libcalamares
 
 import gettext
 _ = gettext.translation("calamares-python",
-                        localedir=utils.gettext_path(),
-                        languages=utils.gettext_languages(),
+                        localedir=libcalamares.utils.gettext_path(),
+                        languages=libcalamares.utils.gettext_languages(),
                         fallback=True).gettext
 
 def pretty_name():
@@ -123,14 +122,14 @@ class UnpackEntry:
             return
 
         if os.path.isdir(self.source):
-            r = mount(self.source, imgmountdir, "", "--bind")
+            r = libcalamares.utils.mount(self.source, imgmountdir, "", "--bind")
         elif os.path.isfile(self.source):
-            r = mount(self.source, imgmountdir, self.sourcefs, "loop")
+            r = libcalamares.utils.mount(self.source, imgmountdir, self.sourcefs, "loop")
         else: # self.source is a device
-            r = mount(self.source, imgmountdir, self.sourcefs, "")
+            r = libcalamares.utils.mount(self.source, imgmountdir, self.sourcefs, "")
 
         if r != 0:
-            utils.debug("Failed to mount '{}' (fs={}) (target={})".format(self.source, self.sourcefs, imgmountdir))
+            libcalamares.utils.debug("Failed to mount '{}' (fs={}) (target={})".format(self.source, self.sourcefs, imgmountdir))
             raise subprocess.CalledProcessError(r, "mount")
 
 
@@ -142,7 +141,7 @@ def global_excludes():
     List excludes for rsync.
     """
     lst = []
-    extra_mounts = globalstorage.value("extraMounts")
+    extra_mounts = libcalamares.globalstorage.value("extraMounts")
     if extra_mounts is None:
         extra_mounts = []
 
@@ -251,7 +250,7 @@ def file_copy(source, entry, progress_cb):
     # https://bugzilla.redhat.com/show_bug.cgi?id=868755#c50
     # for the same issue in Anaconda, which uses a similar workaround.
     if process.returncode != 0 and process.returncode != 23:
-        utils.warning("rsync failed with error code {}.".format(process.returncode))
+        libcalamares.utils.warning("rsync failed with error code {}.".format(process.returncode))
         return _("rsync failed with error code {}.").format(process.returncode)
 
     return None
@@ -298,7 +297,7 @@ class UnpackOperation:
 
         global status
         status = _("Unpacking image {}/{}, file {}/{}").format((complete_count+1), len(self.entries), current_done, current_total)
-        job.setprogress(progress)
+        libcalamares.job.setprogress(progress)
 
     def run(self):
         """
@@ -313,7 +312,7 @@ class UnpackOperation:
             complete = 0
             for entry in self.entries:
                 status = _("Starting to unpack {}").format(entry.source)
-                job.setprogress( ( 1.0 * complete ) / len(self.entries) )
+                libcalamares.job.setprogress( ( 1.0 * complete ) / len(self.entries) )
                 entry.do_mount(source_mount_path)
                 entry.do_count()  # Fill in the entry.total
 
@@ -398,7 +397,7 @@ def repair_root_permissions(root_mount_point):
         try:
             os.chmod(root_mount_point, 0o755)  # Want / to be rwxr-xr-x
         except OSError as e:
-            utils.warning("Could not set / to safe permissions: {}".format(e))
+            libcalamares.utils.warning("Could not set / to safe permissions: {}".format(e))
             # But ignore it
 
 
@@ -414,9 +413,9 @@ def extract_weight(entry):
             wi = int(w)
             return wi if wi > 0 else 1
         except ValueError:
-            utils.warning("*weight* setting {!r} is not valid.".format(w))
+            libcalamares.utils.warning("*weight* setting {!r} is not valid.".format(w))
         except TypeError:
-            utils.warning("*weight* setting {!r} must be number.".format(w))
+            libcalamares.utils.warning("*weight* setting {!r} must be number.".format(w))
     return 1
 
 
@@ -424,16 +423,16 @@ def run():
     """
     Unsquash filesystem.
     """
-    root_mount_point = globalstorage.value("rootMountPoint")
+    root_mount_point = libcalamares.globalstorage.value("rootMountPoint")
 
     if not root_mount_point:
-        utils.warning("No mount point for root partition")
+        libcalamares.utils.warning("No mount point for root partition")
         return (_("No mount point for root partition"),
                 _("globalstorage does not contain a \"rootMountPoint\" key, "
                 "doing nothing"))
 
     if not os.path.exists(root_mount_point):
-        utils.warning("Bad root mount point \"{}\"".format(root_mount_point))
+        libcalamares.utils.warning("Bad root mount point \"{}\"".format(root_mount_point))
         return (_("Bad mount point for root partition"),
                 _("rootMountPoint is \"{}\", which does not "
                 "exist, doing nothing").format(root_mount_point))
@@ -444,41 +443,42 @@ def run():
     #   - unsupported filesystems
     #   - non-existent sources
     #   - missing tools for specific FS
-    for entry in job.configuration["unpack"]:
+    for entry in libcalamares.job.configuration["unpack"]:
         source = os.path.abspath(entry["source"])
         sourcefs = entry["sourcefs"]
 
         if sourcefs not in supported_filesystems:
-            utils.warning("The filesystem for \"{}\" ({}) is not supported by your current kernel".format(source, sourcefs))
-            utils.warning(" ... modprobe {} may solve the problem".format(sourcefs))
+            libcalamares.utils.warning("The filesystem for \"{}\" ({}) is not supported by your current kernel".format(source, sourcefs))
+            libcalamares.utils.warning(" ... modprobe {} may solve the problem".format(sourcefs))
             return (_("Bad unsquash configuration"),
                     _("The filesystem for \"{}\" ({}) is not supported by your current kernel").format(source, sourcefs))
         if not os.path.exists(source):
-            utils.warning("The source filesystem \"{}\" does not exist".format(source))
+            libcalamares.utils.warning("The source filesystem \"{}\" does not exist".format(source))
             return (_("Bad unsquash configuration"),
                     _("The source filesystem \"{}\" does not exist").format(source))
         if sourcefs == "squashfs":
             if shutil.which("unsquashfs") is None:
-                utils.warning("Failed to find unsquashfs")
+                libcalamares.utils.warning("Failed to find unsquashfs")
 
-                return (_("Failed to unpack image \"{}\"").format(self.source),
-                        _("Failed to find unsquashfs, make sure you have the squashfs-tools package installed"))
+                return (_("Bad unsquash configuration"),
+                        _("Failed to find unsquashfs, make sure you have the squashfs-tools package installed.") +
+                        " " + _("Failed to unpack image \"{}\"").format(source))
 
     unpack = list()
 
     is_first = True
-    for entry in job.configuration["unpack"]:
+    for entry in libcalamares.job.configuration["unpack"]:
         source = os.path.abspath(entry["source"])
         sourcefs = entry["sourcefs"]
         destination = os.path.abspath(root_mount_point + entry["destination"])
 
         if not os.path.isdir(destination) and sourcefs != "file":
-            utils.warning(("The destination \"{}\" in the target system is not a directory").format(destination))
+            libcalamares.utils.warning(("The destination \"{}\" in the target system is not a directory").format(destination))
             if is_first:
                 return (_("Bad unsquash configuration"),
                         _("The destination \"{}\" in the target system is not a directory").format(destination))
             else:
-                utils.debug(".. assuming that the previous targets will create that directory.")
+                libcalamares.utils.debug(".. assuming that the previous targets will create that directory.")
 
         unpack.append(UnpackEntry(source, sourcefs, destination))
         # Optional settings