diff --git a/CHANGES b/CHANGES index 896a9fee2..7094f5866 100644 --- a/CHANGES +++ b/CHANGES @@ -14,7 +14,14 @@ This release contains contributions from (alphabetically by first name): ## Core ## -There are no core changes in this version. + * The Calamares application now recognizes the `-X` or `--xdg-config` + option, which adds XDG_DATA_DIRS to the places used to find QML + and branding directories, and XDG_CONFIG_DIRS to the places used + to find the global settings and module configurations. This allows + a more fine-grained, and more layered, approach to setting up + Calamares configurations (in particular, distro's can **add** + configuration files and give them priority, instead of **forking** + configuration files). ## Modules ## diff --git a/src/calamares/CalamaresApplication.cpp b/src/calamares/CalamaresApplication.cpp index 018e2b677..0b715d6df 100644 --- a/src/calamares/CalamaresApplication.cpp +++ b/src/calamares/CalamaresApplication.cpp @@ -145,6 +145,9 @@ qmlDirCandidates( bool assumeBuilddir ) { if ( assumeBuilddir ) qmlDirs << QDir::current().absoluteFilePath( "src/qml" ); // In build-dir + if ( CalamaresUtils::haveExtraDirs() ) + for ( auto s : CalamaresUtils::extraDataDirs() ) + qmlDirs << ( s + QML ); qmlDirs << CalamaresUtils::appDataDir().absoluteFilePath( QML ); } @@ -164,6 +167,9 @@ settingsFileCandidates( bool assumeBuilddir ) { if ( assumeBuilddir ) settingsPaths << QDir::current().absoluteFilePath( settings ); + if ( CalamaresUtils::haveExtraDirs() ) + for ( auto s : CalamaresUtils::extraConfigDirs() ) + settingsPaths << ( s + settings ); settingsPaths << CMAKE_INSTALL_FULL_SYSCONFDIR "/calamares/settings.conf"; // String concat settingsPaths << CalamaresUtils::appDataDir().absoluteFilePath( settings ); } @@ -182,6 +188,9 @@ brandingFileCandidates( bool assumeBuilddir, const QString& brandingFilename ) { if ( assumeBuilddir ) brandingPaths << ( QDir::currentPath() + QStringLiteral( "/src/" ) + brandingFilename ); + if ( CalamaresUtils::haveExtraDirs() ) + for ( auto s : CalamaresUtils::extraDataDirs() ) + brandingPaths << ( s + brandingFilename ); brandingPaths << QDir( CMAKE_INSTALL_FULL_SYSCONFDIR "/calamares/" ).absoluteFilePath( brandingFilename ); brandingPaths << CalamaresUtils::appDataDir().absoluteFilePath( brandingFilename); } diff --git a/src/calamares/main.cpp b/src/calamares/main.cpp index 9893e6792..f855b060f 100644 --- a/src/calamares/main.cpp +++ b/src/calamares/main.cpp @@ -44,6 +44,8 @@ handle_args( CalamaresApplication& a ) "Verbose output for debugging purposes (0-8).", "level" ); QCommandLineOption configOption( QStringList{ "c", "config"}, "Configuration directory to use, for testing purposes.", "config" ); + QCommandLineOption xdgOption( QStringList{"X", "xdg-config"}, + "Use XDG_{CONFIG,DATA}_DIRS as well." ); QCommandLineParser parser; parser.setApplicationDescription( "Distribution-independent installer framework" ); @@ -53,6 +55,7 @@ handle_args( CalamaresApplication& a ) parser.addOption( debugOption ); parser.addOption( debugLevelOption ); parser.addOption( configOption ); + parser.addOption( xdgOption ); parser.process( a ); @@ -72,6 +75,8 @@ handle_args( CalamaresApplication& a ) } if ( parser.isSet( configOption ) ) CalamaresUtils::setAppDataDir( QDir( parser.value( configOption ) ) ); + if ( parser.isSet( xdgOption ) ) + CalamaresUtils::setXdgDirs(); } int diff --git a/src/libcalamares/Tests.cpp b/src/libcalamares/Tests.cpp index 7595718e4..acf5b03d3 100644 --- a/src/libcalamares/Tests.cpp +++ b/src/libcalamares/Tests.cpp @@ -42,7 +42,7 @@ LibCalamaresTests::testDebugLevels() { Logger::setupLogLevel( Logger::LOG_DISABLE ); - QCOMPARE( Logger::logLevel(), Logger::LOG_DISABLE ); + QCOMPARE( Logger::logLevel(), static_cast( Logger::LOG_DISABLE ) ); for ( unsigned int level = 0; level <= Logger::LOGVERBOSE ; ++level ) { diff --git a/src/libcalamares/utils/CalamaresUtils.cpp b/src/libcalamares/utils/CalamaresUtils.cpp index 6a892511a..3ab758522 100644 --- a/src/libcalamares/utils/CalamaresUtils.cpp +++ b/src/libcalamares/utils/CalamaresUtils.cpp @@ -49,6 +49,9 @@ static QTranslator* s_brandingTranslator = nullptr; static QTranslator* s_translator = nullptr; static QString s_translatorLocaleName; +static bool s_haveExtraDirs = false; +static QStringList s_extraConfigDirs; +static QStringList s_extraDataDirs; static bool isWritableDir( const QDir& dir ) @@ -94,6 +97,46 @@ setAppDataDir( const QDir& dir ) s_isAppDataDirOverridden = true; } +/* Split $ENV{@p name} on :, append to @p l, making sure each ends in / */ +static void +mungeEnvironment( QStringList& l, const char *name ) +{ + for ( auto s : QString( qgetenv( name ) ).split(':') ) + if ( s.endsWith( '/' ) ) + l << s; + else + l << ( s + '/' ); +} + +void +setXdgDirs() +{ + s_haveExtraDirs = true; + mungeEnvironment( s_extraConfigDirs, "XDG_CONFIG_DIRS" ); + mungeEnvironment( s_extraDataDirs, "XDG_DATA_DIRS" ); +} + +QStringList +extraConfigDirs() +{ + if ( s_haveExtraDirs ) + return s_extraConfigDirs; + return QStringList(); +} + +QStringList +extraDataDirs() +{ + if ( s_haveExtraDirs ) + return s_extraDataDirs; + return QStringList(); +} + +bool +haveExtraDirs() +{ + return s_haveExtraDirs && ( !s_extraConfigDirs.isEmpty() || !s_extraDataDirs.isEmpty() ); +} bool isAppDataDirOverridden() diff --git a/src/libcalamares/utils/CalamaresUtils.h b/src/libcalamares/utils/CalamaresUtils.h index e64fe4eec..baf7a12dc 100644 --- a/src/libcalamares/utils/CalamaresUtils.h +++ b/src/libcalamares/utils/CalamaresUtils.h @@ -79,6 +79,16 @@ namespace CalamaresUtils DLLEXPORT void setQmlModulesDir( const QDir& dir ); + /** @brief Setup extra config and data dirs from the XDG variables. + */ + DLLEXPORT void setXdgDirs(); + /** @brief Are any extra directories configured? */ + DLLEXPORT bool haveExtraDirs(); + /** @brief XDG_CONFIG_DIRS, each guaranteed to end with / */ + DLLEXPORT QStringList extraConfigDirs(); + /** @brief XDG_DATA_DIRS, each guaranteed to end with / */ + DLLEXPORT QStringList extraDataDirs(); + /** * @brief removeDiacritics replaces letters with diacritics and ligatures with * alternative forms and digraphs. diff --git a/src/libcalamaresui/modulesystem/Module.cpp b/src/libcalamaresui/modulesystem/Module.cpp index a1349c280..ef629ac4d 100644 --- a/src/libcalamaresui/modulesystem/Module.cpp +++ b/src/libcalamaresui/modulesystem/Module.cpp @@ -155,6 +155,10 @@ moduleConfigurationCandidates( bool assumeBuildDir, const QString& moduleName, c if ( assumeBuildDir && configFileName.contains( '/' ) ) paths << QDir().absoluteFilePath( configFileName ); + if ( CalamaresUtils::haveExtraDirs() ) + for ( auto s : CalamaresUtils::extraConfigDirs() ) + paths << ( s + QString( "modules/%1" ).arg( configFileName ) ); + paths << QString( "/etc/calamares/modules/%1" ).arg( configFileName ); paths << CalamaresUtils::appDataDir().absoluteFilePath( QString( "modules/%1" ).arg( configFileName ) ); } diff --git a/src/modules/partition/core/PartitionActions.cpp b/src/modules/partition/core/PartitionActions.cpp index 677c3778d..e06ff6c36 100644 --- a/src/modules/partition/core/PartitionActions.cpp +++ b/src/modules/partition/core/PartitionActions.cpp @@ -28,7 +28,6 @@ #include "utils/Units.h" #include "JobQueue.h" #include "utils/Logger.h" -#include "GlobalStorage.h" #include #include @@ -43,58 +42,38 @@ using CalamaresUtils::operator""_GiB; using CalamaresUtils::operator""_MiB; qint64 -swapSuggestion( const qint64 availableSpaceB ) +swapSuggestion( const qint64 availableSpaceB, Choices::SwapChoice swap ) { - /* If suspend-to-disk is demanded, then we always need enough - * swap to write the whole memory to disk -- between 2GB and 8GB - * RAM give proportionally more swap, and from 8GB RAM keep - * swap = RAM. - * - * If suspend-to-disk is not demanded, then ramp up more slowly, - * to 8GB swap at 16GB memory, and then drop to 4GB for "large - * memory" machines, on the assumption that those don't need swap - * because they have tons of memory (or whatever they are doing, - * had better not run into swap). - */ + if ( ( swap != Choices::SmallSwap ) && ( swap != Choices::FullSwap ) ) + return 0; + + // See partition.conf for explanation qint64 suggestedSwapSizeB = 0; auto memory = CalamaresUtils::System::instance()->getTotalMemoryB(); qint64 availableRamB = memory.first; qreal overestimationFactor = memory.second; - bool ensureSuspendToDisk = - Calamares::JobQueue::instance()->globalStorage()-> - value( "ensureSuspendToDisk" ).toBool(); + bool ensureSuspendToDisk = swap == Choices::FullSwap; - if ( ensureSuspendToDisk ) - { - if ( availableRamB < 4_GiB ) - suggestedSwapSizeB = qMax( 2_GiB, availableRamB * 2 ); - else if ( availableRamB >= 4_GiB && availableRamB < 8_GiB ) - suggestedSwapSizeB = 8_GiB; - else - suggestedSwapSizeB = availableRamB; + // Ramp up quickly to 8GiB, then follow memory size + if ( availableRamB <= 4_GiB ) + suggestedSwapSizeB = availableRamB * 2; + else if ( availableRamB <= 8_GiB ) + suggestedSwapSizeB = 8_GiB; + else + suggestedSwapSizeB = availableRamB; - suggestedSwapSizeB *= overestimationFactor; - } - else //if we don't care about suspend to disk - { - if ( availableRamB < 2_GiB ) - suggestedSwapSizeB = qMax( 2_GiB, availableRamB * 2 ); - else if ( availableRamB >= 2_GiB && availableRamB < 8_GiB ) - suggestedSwapSizeB = availableRamB; - else if ( availableRamB >= 8_GiB && availableRamB < 16_GiB ) - suggestedSwapSizeB = 8_GiB; - else - suggestedSwapSizeB = 4_GiB; + // .. top out at 8GiB if we don't care about suspend + if ( !ensureSuspendToDisk ) + suggestedSwapSizeB = qMin( 8_GiB, suggestedSwapSizeB ); - suggestedSwapSizeB *= overestimationFactor; - // don't use more than 10% of available space - qreal maxSwapDiskRatio = 0.10; - qint64 maxSwapSizeB = availableSpaceB * maxSwapDiskRatio; - if ( suggestedSwapSizeB > maxSwapSizeB ) - suggestedSwapSizeB = maxSwapSizeB; - } + // Allow for a fudge factor + suggestedSwapSizeB *= overestimationFactor; + + // don't use more than 10% of available space + if ( !ensureSuspendToDisk ) + suggestedSwapSizeB = qMin( suggestedSwapSizeB, qint64( 0.10 * availableSpaceB ) ); cDebug() << "Suggested swap size:" << suggestedSwapSizeB / 1024. / 1024. / 1024. << "GiB"; @@ -118,16 +97,14 @@ bytesToSectors( qint64 bytes, qint64 blocksize ) } void -doAutopartition( PartitionCoreModule* core, Device* dev, const QString& luksPassphrase ) +doAutopartition( PartitionCoreModule* core, Device* dev, Choices::AutoPartitionOptions o ) { - Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); - - bool isEfi = PartUtils::isEfiSystem(); - - QString defaultFsType = gs->value( "defaultFileSystemType" ).toString(); + QString defaultFsType = o.defaultFsType; if ( FileSystem::typeForName( defaultFsType ) == FileSystem::Unknown ) defaultFsType = "ext4"; + bool isEfi = PartUtils::isEfiSystem(); + // Partition sizes are expressed in MiB, should be multiples of // the logical sector size (usually 512B). EFI starts with 2MiB // empty and a 300MiB EFI boot partition, while BIOS starts at @@ -160,8 +137,7 @@ doAutopartition( PartitionCoreModule* core, Device* dev, const QString& luksPass PartitionTable::FlagNone ); PartitionInfo::setFormat( efiPartition, true ); - PartitionInfo::setMountPoint( efiPartition, gs->value( "efiSystemPartition" ) - .toString() ); + PartitionInfo::setMountPoint( efiPartition, o.efiPartitionMountPoint ); core->createPartition( dev, efiPartition, PartitionTable::FlagEsp ); firstFreeSector = lastSector + 1; } @@ -170,20 +146,18 @@ doAutopartition( PartitionCoreModule* core, Device* dev, const QString& luksPass core->createPartitionTable( dev, PartitionTable::msdos ); } - const bool mayCreateSwap = !gs->value( "neverCreateSwap" ).toBool(); + const bool mayCreateSwap = ( o.swap == Choices::SmallSwap ) || ( o.swap == Choices::FullSwap ); bool shouldCreateSwap = false; qint64 suggestedSwapSizeB = 0; if ( mayCreateSwap ) { qint64 availableSpaceB = ( dev->totalLogical() - firstFreeSector ) * dev->logicalSize(); - suggestedSwapSizeB = swapSuggestion( availableSpaceB ); + suggestedSwapSizeB = swapSuggestion( availableSpaceB, o.swap ); // Space required by this installation is what the distro claims is needed // (via global configuration) plus the swap size plus a fudge factor of // 0.6GiB (this was 2.1GiB up to Calamares 3.2.2). - qint64 requiredSpaceB = - GiBtoBytes( gs->value( "requiredStorageGB" ).toDouble() + 0.6 ) + - suggestedSwapSizeB; + qint64 requiredSpaceB = o.requiredSpaceB + 600_MiB + suggestedSwapSizeB; // If there is enough room for ESP + root + swap, create swap, otherwise don't. shouldCreateSwap = availableSpaceB > requiredSpaceB; @@ -196,7 +170,7 @@ doAutopartition( PartitionCoreModule* core, Device* dev, const QString& luksPass } Partition* rootPartition = nullptr; - if ( luksPassphrase.isEmpty() ) + if ( o.luksPassphrase.isEmpty() ) { rootPartition = KPMHelpers::createNewPartition( dev->partitionTable(), @@ -216,7 +190,7 @@ doAutopartition( PartitionCoreModule* core, Device* dev, const QString& luksPass FileSystem::typeForName( defaultFsType ), firstFreeSector, lastSectorForRoot, - luksPassphrase + o.luksPassphrase ); } PartitionInfo::setFormat( rootPartition, true ); @@ -226,7 +200,7 @@ doAutopartition( PartitionCoreModule* core, Device* dev, const QString& luksPass if ( shouldCreateSwap ) { Partition* swapPartition = nullptr; - if ( luksPassphrase.isEmpty() ) + if ( o.luksPassphrase.isEmpty() ) { swapPartition = KPMHelpers::createNewPartition( dev->partitionTable(), @@ -246,7 +220,7 @@ doAutopartition( PartitionCoreModule* core, Device* dev, const QString& luksPass FileSystem::LinuxSwap, lastSectorForRoot + 1, dev->totalLogical() - 1, - luksPassphrase + o.luksPassphrase ); } PartitionInfo::setFormat( swapPartition, true ); @@ -261,13 +235,11 @@ void doReplacePartition( PartitionCoreModule* core, Device* dev, Partition* partition, - const QString& luksPassphrase ) + Choices::ReplacePartitionOptions o ) { cDebug() << "doReplacePartition for device" << partition->partitionPath(); - QString defaultFsType = Calamares::JobQueue::instance()-> - globalStorage()-> - value( "defaultFileSystemType" ).toString(); + QString defaultFsType = o.defaultFsType; if ( FileSystem::typeForName( defaultFsType ) == FileSystem::Unknown ) defaultFsType = "ext4"; @@ -288,7 +260,7 @@ doReplacePartition( PartitionCoreModule* core, } Partition* newPartition = nullptr; - if ( luksPassphrase.isEmpty() ) + if ( o.luksPassphrase.isEmpty() ) { newPartition = KPMHelpers::createNewPartition( partition->parent(), @@ -308,7 +280,7 @@ doReplacePartition( PartitionCoreModule* core, FileSystem::typeForName( defaultFsType ), partition->firstSector(), partition->lastSector(), - luksPassphrase + o.luksPassphrase ); } PartitionInfo::setMountPoint( newPartition, "/" ); diff --git a/src/modules/partition/core/PartitionActions.h b/src/modules/partition/core/PartitionActions.h index bb624552f..5acf444fa 100644 --- a/src/modules/partition/core/PartitionActions.h +++ b/src/modules/partition/core/PartitionActions.h @@ -27,29 +27,74 @@ class Partition; namespace PartitionActions { +/** @brief Namespace for enums + * + * This namespace houses non-class enums..... + */ +namespace Choices +{ + /** @brief Ccchoice of swap (size and type) */ + enum SwapChoice + { + NoSwap, // don't create any swap, don't use any + ReuseSwap, // don't create, but do use existing + SmallSwap, // up to 8GiB of swap + FullSwap, // ensureSuspendToDisk -- at least RAM size + SwapFile // use a file (if supported) + }; + + struct ReplacePartitionOptions + { + QString defaultFsType; // e.g. "ext4" or "btrfs" + QString luksPassphrase; // optional + + ReplacePartitionOptions( const QString& fs, const QString& luks ) + : defaultFsType( fs ) + , luksPassphrase( luks ) + { + } + }; + + struct AutoPartitionOptions : ReplacePartitionOptions + { + QString efiPartitionMountPoint; // optional, e.g. "/boot" + quint64 requiredSpaceB; // estimated required space for root partition + SwapChoice swap; + + AutoPartitionOptions( const QString& fs, const QString& luks, const QString& efi, qint64 r, SwapChoice s ) + : ReplacePartitionOptions( fs, luks ) + , efiPartitionMountPoint( efi ) + , requiredSpaceB( r > 0 ? r : 0 ) + , swap( s ) + { + } + }; + +} // namespace Choices /** * @brief doAutopartition sets up an autopartitioning operation on the given Device. * @param core a pointer to the PartitionCoreModule instance. * @param dev the device to wipe. - * @param luksPassphrase the passphrase for LUKS encryption (optional, default is empty). + * @param options settings for autopartitioning. */ void doAutopartition( PartitionCoreModule* core, Device* dev, - const QString& luksPassphrase = QString() ); + Choices::AutoPartitionOptions options ); /** * @brief doReplacePartition sets up replace-partitioning with the given partition. * @param core a pointer to the PartitionCoreModule instance. * @param dev a pointer to the Device on which to replace a partition. * @param partition a pointer to the Partition to be replaced. - * @param luksPassphrase the passphrase for LUKS encryption (optional, default is empty). + * @param options settings for partitioning (not all fields apply) + * * @note this function also takes care of requesting PCM to delete the partition. */ void doReplacePartition( PartitionCoreModule* core, Device* dev, Partition* partition, - const QString& luksPassphrase = QString() ); -} + Choices::ReplacePartitionOptions options ); +} // namespace PartitionActions #endif // PARTITIONACTIONS_H diff --git a/src/modules/partition/gui/ChoicePage.cpp b/src/modules/partition/gui/ChoicePage.cpp index 9bc571459..cc7819778 100644 --- a/src/modules/partition/gui/ChoicePage.cpp +++ b/src/modules/partition/gui/ChoicePage.cpp @@ -20,31 +20,33 @@ #include "ChoicePage.h" #include "core/BootLoaderModel.h" -#include "core/PartitionActions.h" -#include "core/PartitionCoreModule.h" #include "core/DeviceModel.h" -#include "core/PartitionModel.h" +#include "core/KPMHelpers.h" #include "core/OsproberEntry.h" #include "core/PartUtils.h" +#include "core/PartitionActions.h" +#include "core/PartitionCoreModule.h" +#include "core/PartitionInfo.h" #include "core/PartitionIterator.h" +#include "core/PartitionModel.h" -#include "ReplaceWidget.h" -#include "PrettyRadioButton.h" +#include "BootInfoWidget.h" +#include "DeviceInfoWidget.h" #include "PartitionBarsView.h" #include "PartitionLabelsView.h" #include "PartitionSplitterWidget.h" -#include "BootInfoWidget.h" -#include "DeviceInfoWidget.h" +#include "PrettyRadioButton.h" +#include "ReplaceWidget.h" #include "ScanningDialog.h" #include "utils/CalamaresUtilsGui.h" #include "utils/Logger.h" #include "utils/Retranslator.h" +#include "utils/Units.h" + #include "Branding.h" -#include "core/KPMHelpers.h" -#include "JobQueue.h" #include "GlobalStorage.h" -#include "core/PartitionInfo.h" +#include "JobQueue.h" #include #include @@ -61,7 +63,7 @@ #include #include - +using PartitionActions::Choices::SwapChoice; /** * @brief ChoicePage::ChoicePage is the default constructor. Called on startup as part of @@ -81,6 +83,9 @@ ChoicePage::ChoicePage( QWidget* parent ) , m_eraseButton( nullptr ) , m_replaceButton( nullptr ) , m_somethingElseButton( nullptr ) + , m_eraseSwapChoices( nullptr ) + , m_replaceSwapChoices( nullptr ) + , m_alongsideSwapChoices( nullptr ) , m_deviceInfoWidget( nullptr ) , m_beforePartitionBarsView( nullptr ) , m_beforePartitionLabelsView( nullptr ) @@ -176,6 +181,19 @@ ChoicePage::init( PartitionCoreModule* core ) } +/** @brief Creates a combobox with the given choices in it. + * + * No texts are set -- that happens later by the translator functions. + */ +static inline QComboBox* +createCombo( std::initializer_list< SwapChoice > l ) +{ + QComboBox* box = new QComboBox; + for ( SwapChoice c : l ) + box->addItem( QString(), c ); + return box; +} + /** * @brief ChoicePage::setupChoices creates PrettyRadioButton objects for the action * choices. @@ -229,6 +247,19 @@ ChoicePage::setupChoices() iconSize ) ); m_grp->addButton( m_replaceButton->buttonWidget(), Replace ); + // Fill up swap options + // .. TODO: only if enabled in the config + m_eraseSwapChoices = createCombo( { SwapChoice::NoSwap, SwapChoice::SmallSwap, SwapChoice:: FullSwap } ); + m_eraseButton->addOptionsComboBox( m_eraseSwapChoices ); + +#if 0 + m_replaceSwapChoices = createCombo( { SwapChoice::NoSwap, SwapChoice::ReuseSwap, SwapChoice::SmallSwap, SwapChoice::FullSwap } ); + m_replaceButton->addOptionsComboBox( m_replaceSwapChoices ); + + m_alongsideSwapChoices = createCombo( { SwapChoice::NoSwap, SwapChoice::ReuseSwap, SwapChoice::SmallSwap, SwapChoice::FullSwap } ); + m_alongsideButton->addOptionsComboBox( m_alongsideSwapChoices ); +#endif + m_itemsLayout->addWidget( m_alongsideButton ); m_itemsLayout->addWidget( m_replaceButton ); m_itemsLayout->addWidget( m_eraseButton ); @@ -252,7 +283,7 @@ ChoicePage::setupChoices() { if ( checked ) // An action was picked. { - m_choice = static_cast< Choice >( id ); + m_choice = static_cast< InstallChoice >( id ); updateNextEnabled(); emit actionChosen(); @@ -282,6 +313,12 @@ ChoicePage::setupChoices() applyActionChoice( currentChoice() ); } } ); + + CALAMARES_RETRANSLATE( + updateSwapChoicesTr( m_eraseSwapChoices ); + updateSwapChoicesTr( m_alongsideSwapChoices ); + updateSwapChoicesTr( m_replaceSwapChoices ); + ) } @@ -376,7 +413,7 @@ ChoicePage::continueApplyDeviceChoice() void -ChoicePage::applyActionChoice( ChoicePage::Choice choice ) +ChoicePage::applyActionChoice( ChoicePage::InstallChoice choice ) { m_beforePartitionBarsView->selectionModel()-> disconnect( SIGNAL( currentRowChanged( QModelIndex, QModelIndex ) ) ); @@ -386,30 +423,37 @@ ChoicePage::applyActionChoice( ChoicePage::Choice choice ) switch ( choice ) { case Erase: - if ( m_core->isDirty() ) { - ScanningDialog::run( QtConcurrent::run( [ = ] - { - QMutexLocker locker( &m_coreMutex ); - m_core->revertDevice( selectedDevice() ); - } ), - [ = ] - { - PartitionActions::doAutopartition( m_core, - selectedDevice(), - m_encryptWidget->passphrase() ); - emit deviceChosen(); - }, - this ); - } - else - { - PartitionActions::doAutopartition( m_core, - selectedDevice(), - m_encryptWidget->passphrase() ); - emit deviceChosen(); - } + auto gs = Calamares::JobQueue::instance()->globalStorage(); + PartitionActions::Choices::AutoPartitionOptions options { + gs->value( "defaultFileSystemType" ).toString(), + m_encryptWidget->passphrase(), + gs->value( "efiSystemPartition" ).toString(), + CalamaresUtils::GiBtoBytes( gs->value( "requiredStorageGB" ).toDouble() ), + static_cast( m_eraseSwapChoices->currentData().toInt() ) + }; + + if ( m_core->isDirty() ) + { + ScanningDialog::run( QtConcurrent::run( [ = ] + { + QMutexLocker locker( &m_coreMutex ); + m_core->revertDevice( selectedDevice() ); + } ), + [ = ] + { + PartitionActions::doAutopartition( m_core, selectedDevice(), options ); + emit deviceChosen(); + }, + this ); + } + else + { + PartitionActions::doAutopartition( m_core, selectedDevice(), options ); + emit deviceChosen(); + } + } break; case Replace: if ( m_core->isDirty() ) @@ -487,6 +531,7 @@ ChoicePage::doAlongsideSetupSplitter( const QModelIndex& current, ->value( "requiredStorageGB" ) .toDouble(); + // TODO: make this consistent qint64 requiredStorageB = qRound64( requiredStorageGB + 0.1 + 2.0 ) * 1024 * 1024 * 1024; m_afterPartitionSplitterWidget->setSplitPartition( @@ -771,14 +816,19 @@ ChoicePage::doReplaceSelectedPartition( const QModelIndex& current ) if ( homePartitionPath->isEmpty() ) doReuseHomePartition = false; - PartitionActions::doReplacePartition( m_core, - selectedDevice(), - selectedPartition, - m_encryptWidget->passphrase() ); + Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); + + PartitionActions::doReplacePartition( + m_core, + selectedDevice(), + selectedPartition, + { + gs->value( "defaultFileSystemType" ).toString(), + m_encryptWidget->passphrase() + } ); Partition* homePartition = KPMHelpers::findPartitionByPath( { selectedDevice() }, *homePartitionPath ); - Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); if ( homePartition && doReuseHomePartition ) { PartitionInfo::setMountPoint( homePartition, "/home" ); @@ -891,7 +941,7 @@ ChoicePage::updateDeviceStatePreview() * @param choice the chosen partitioning action. */ void -ChoicePage::updateActionChoicePreview( ChoicePage::Choice choice ) +ChoicePage::updateActionChoicePreview( ChoicePage::InstallChoice choice ) { Device* currentDevice = selectedDevice(); Q_ASSERT( currentDevice ); @@ -1380,7 +1430,7 @@ ChoicePage::isNextEnabled() const } -ChoicePage::Choice +ChoicePage::InstallChoice ChoicePage::currentChoice() const { return m_choice; @@ -1428,3 +1478,42 @@ ChoicePage::updateNextEnabled() emit nextStatusChanged( enabled ); } +void +ChoicePage::updateSwapChoicesTr(QComboBox* box) +{ + if ( !box ) + return; + + static_assert(SwapChoice::NoSwap == 0, "Enum values out-of-sync"); + for ( int index = 0; index < box->count(); ++index ) + { + bool ok = false; + int value = 0; + + switch ( value = box->itemData( index ).toInt( &ok ) ) + { + // case 0: + case SwapChoice::NoSwap: + // toInt() returns 0 on failure, so check for ok + if ( ok ) // It was explicitly set to 0 + box->setItemText( index, tr( "No Swap" ) ); + else + cWarning() << "Box item" << index << box->itemText( index ) << "has non-integer role."; + break; + case SwapChoice::ReuseSwap: + box->setItemText( index, tr( "Reuse Swap" ) ); + break; + case SwapChoice::SmallSwap: + box->setItemText( index, tr( "Swap (no Hibernate)" ) ); + break; + case SwapChoice::FullSwap: + box->setItemText( index, tr( "Swap (with Hibernate)" ) ); + break; + case SwapChoice::SwapFile: + box->setItemText( index, tr( "Swap to file" ) ); + break; + default: + cWarning() << "Box item" << index << box->itemText( index ) << "has role" << value; + } + } +} diff --git a/src/modules/partition/gui/ChoicePage.h b/src/modules/partition/gui/ChoicePage.h index c36747e93..07d052c2d 100644 --- a/src/modules/partition/gui/ChoicePage.h +++ b/src/modules/partition/gui/ChoicePage.h @@ -53,7 +53,7 @@ class ChoicePage : public QWidget, private Ui::ChoicePage { Q_OBJECT public: - enum Choice + enum InstallChoice { NoChoice, Alongside, @@ -84,7 +84,7 @@ public: * currently selected partitioning mode (with a PrettyRadioButton). * @return the enum Choice value. */ - Choice currentChoice() const; + InstallChoice currentChoice() const; /** * @brief onLeave runs when control passes from this page to another one. @@ -95,7 +95,7 @@ public: * @brief applyActionChoice reacts to a choice of partitioning mode. * @param choice the partitioning action choice. */ - void applyActionChoice( ChoicePage::Choice choice ); + void applyActionChoice( ChoicePage::InstallChoice choice ); signals: void nextStatusChanged( bool ); @@ -121,18 +121,21 @@ private: void continueApplyDeviceChoice(); // .. called after scan void updateDeviceStatePreview(); - void updateActionChoicePreview( ChoicePage::Choice choice ); + void updateActionChoicePreview( ChoicePage::InstallChoice choice ); void setupActions(); OsproberEntryList getOsproberEntriesForDevice( Device* device ) const; void doAlongsideApply(); void setupEfiSystemPartitionSelector(); + // Translations support + void updateSwapChoicesTr( QComboBox* box ); + bool m_nextEnabled; PartitionCoreModule* m_core; QMutex m_previewsMutex; - Choice m_choice; + InstallChoice m_choice; bool m_isEfi; QComboBox* m_drivesCombo; @@ -142,6 +145,9 @@ private: PrettyRadioButton* m_eraseButton; PrettyRadioButton* m_replaceButton; PrettyRadioButton* m_somethingElseButton; + QComboBox* m_eraseSwapChoices; + QComboBox* m_replaceSwapChoices; + QComboBox* m_alongsideSwapChoices; DeviceInfoWidget* m_deviceInfoWidget; diff --git a/src/modules/partition/gui/PartitionViewStep.cpp b/src/modules/partition/gui/PartitionViewStep.cpp index b49b6c93b..bfd52b35e 100644 --- a/src/modules/partition/gui/PartitionViewStep.cpp +++ b/src/modules/partition/gui/PartitionViewStep.cpp @@ -21,6 +21,7 @@ #include "gui/PartitionViewStep.h" #include "core/DeviceModel.h" +#include "core/PartitionActions.h" #include "core/PartitionCoreModule.h" #include "core/PartitionModel.h" #include "core/KPMHelpers.h" @@ -138,7 +139,7 @@ PartitionViewStep::createSummaryWidget() const widget->setLayout( mainLayout ); mainLayout->setMargin( 0 ); - ChoicePage::Choice choice = m_choicePage->currentChoice(); + ChoicePage::InstallChoice choice = m_choicePage->currentChoice(); QFormLayout* formLayout = new QFormLayout( widget ); const int MARGIN = CalamaresUtils::defaultFontHeight() / 2; @@ -470,92 +471,115 @@ PartitionViewStep::onLeave() } +static PartitionActions::Choices::SwapChoice +nameToChoice( QString name, bool& ok ) +{ + ok = false; + name = name.toLower(); + + using namespace PartitionActions::Choices; + + // Each return here first sets ok to true, returns enum value + if ( name == QStringLiteral( "none" ) ) + return( ok=true, SwapChoice::NoSwap ); + else if ( name == QStringLiteral( "small" ) ) + return( ok=true, SwapChoice::SmallSwap); + else if ( name == QStringLiteral( "suspend" ) ) + return( ok=true, SwapChoice::FullSwap ); + else if ( name == QStringLiteral( "reuse" ) ) + return( ok=true, SwapChoice::ReuseSwap ); + else if ( name == QStringLiteral( "file" ) ) + return( ok=true, SwapChoice::SwapFile ); + + ok = false; + return SwapChoice::NoSwap; +} + + void PartitionViewStep::setConfigurationMap( const QVariantMap& configurationMap ) { // Copy the efiSystemPartition setting to the global storage. It is needed not only in // the EraseDiskPage, but also in the bootloader configuration modules (grub, bootloader). Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); - if ( configurationMap.contains( "efiSystemPartition" ) && - configurationMap.value( "efiSystemPartition" ).type() == QVariant::String && - !configurationMap.value( "efiSystemPartition" ).toString().isEmpty() ) - { - gs->insert( "efiSystemPartition", configurationMap.value( "efiSystemPartition" ).toString() ); - } - else - { - gs->insert( "efiSystemPartition", QStringLiteral( "/boot/efi" ) ); - } + QString efiSP = CalamaresUtils::getString( configurationMap, "efiSystemPartition" ); + if ( efiSP.isEmpty() ) + efiSP = QStringLiteral( "/boot/efi" ); + gs->insert( "efiSystemPartition", efiSP ); - if ( configurationMap.contains( "ensureSuspendToDisk" ) && - configurationMap.value( "ensureSuspendToDisk" ).type() == QVariant::Bool ) - { - gs->insert( "ensureSuspendToDisk", configurationMap.value( "ensureSuspendToDisk" ).toBool() ); - } - else - { - gs->insert( "ensureSuspendToDisk", true ); - } + // SWAP SETTINGS + // + // This is a bit convoluted because there's legacy settings to handle as well + // as the new-style list of choices, with mapping back-and-forth. + if ( configurationMap.contains( "userSwapChoices" ) && + ( configurationMap.contains( "ensureSuspendToDisk" ) || configurationMap.contains( "neverCreateSwap" ) ) ) + cError() << "Partition-module configuration mixes old- and new-style swap settings."; - if ( configurationMap.contains( "neverCreateSwap" ) && - configurationMap.value( "neverCreateSwap" ).type() == QVariant::Bool ) - { - gs->insert( "neverCreateSwap", configurationMap.value( "neverCreateSwap" ).toBool() ); - } - else - { - gs->insert( "neverCreateSwap", false ); - } + if ( configurationMap.contains( "ensureSuspendToDisk" ) ) + cWarning() << "Partition-module setting *ensureSuspendToDisk* is deprecated."; + bool ensureSuspendToDisk = CalamaresUtils::getBool( configurationMap, "ensureSuspendToDisk", true ); - if ( configurationMap.contains( "drawNestedPartitions" ) && - configurationMap.value( "drawNestedPartitions" ).type() == QVariant::Bool ) - { - gs->insert( "drawNestedPartitions", - configurationMap.value( "drawNestedPartitions", false ).toBool() ); - } - else - { - gs->insert( "drawNestedPartitions", false ); - } + if ( configurationMap.contains( "neverCreateSwap" ) ) + cWarning() << "Partition-module setting *neverCreateSwap* is deprecated."; + bool neverCreateSwap = CalamaresUtils::getBool( configurationMap, "neverCreateSwap", false ); - if ( configurationMap.contains( "alwaysShowPartitionLabels" ) && - configurationMap.value( "alwaysShowPartitionLabels" ).type() == QVariant::Bool ) + QSet< PartitionActions::Choices::SwapChoice > choices; // Available swap choices + if ( configurationMap.contains( "userSwapChoices" ) ) { - gs->insert( "alwaysShowPartitionLabels", - configurationMap.value( "alwaysShowPartitionLabels", true ).toBool() ); - } - else - { - gs->insert( "alwaysShowPartitionLabels", true ); - } + // We've already warned about overlapping settings with the + // legacy *ensureSuspendToDisk* and *neverCreateSwap*. + QStringList l = configurationMap[ "userSwapChoices" ].toStringList(); - if ( configurationMap.contains( "defaultFileSystemType" ) && - configurationMap.value( "defaultFileSystemType" ).type() == QVariant::String && - !configurationMap.value( "defaultFileSystemType" ).toString().isEmpty() ) - { - QString typeString = configurationMap.value( "defaultFileSystemType" ).toString(); - gs->insert( "defaultFileSystemType", typeString ); - if ( FileSystem::typeForName( typeString ) == FileSystem::Unknown ) + for ( const auto& item : l ) { - cWarning() << "bad default filesystem configuration for partition module. Reverting to ext4 as default."; - gs->insert( "defaultFileSystemType", "ext4" ); + bool ok = false; + auto v = nameToChoice( item, ok ); + if ( ok ) + choices.insert( v ); } + + if ( choices.isEmpty() ) + { + cWarning() << "Partition-module configuration for *userSwapChoices* is empty:" << l; + choices.insert( PartitionActions::Choices::SwapChoice::FullSwap ); + } + + // suspend if it's one of the possible choices; suppress swap only if it's + // the **only** choice available. + ensureSuspendToDisk = choices.contains( PartitionActions::Choices::SwapChoice::FullSwap ); + neverCreateSwap = ( choices.count() == 1 ) && choices.contains( PartitionActions::Choices::SwapChoice::NoSwap ); } else { - gs->insert( "defaultFileSystemType", QStringLiteral( "ext4" ) ); + // Convert the legacy settings into a single setting for now. + if ( neverCreateSwap ) + choices.insert( PartitionActions::Choices::SwapChoice::NoSwap ); + else if ( ensureSuspendToDisk ) + choices.insert( PartitionActions::Choices::SwapChoice::FullSwap ); + else + choices.insert( PartitionActions::Choices::SwapChoice::SmallSwap ); } - if ( configurationMap.contains( "enableLuksAutomatedPartitioning" ) && - configurationMap.value( "enableLuksAutomatedPartitioning" ).type() == QVariant::Bool ) + // These gs settings seem to be unused (in upstream Calamares) outside of + // the partition module itself. + gs->insert( "ensureSuspendToDisk", ensureSuspendToDisk ); + gs->insert( "neverCreateSwap", neverCreateSwap ); + + // OTHER SETTINGS + // + gs->insert( "drawNestedPartitions", CalamaresUtils::getBool( configurationMap, "drawNestedPartitions", false ) ); + gs->insert( "alwaysShowPartitionLabels", CalamaresUtils::getBool( configurationMap, "alwaysShowPartitionLabels", true ) ); + gs->insert( "enableLuksAutomatedPartitioning", CalamaresUtils::getBool( configurationMap, "enableLuksAutomatedPartitioning", true ) ); + + QString defaultFS = CalamaresUtils::getString( configurationMap, "defaultFileSystemType" ); + if ( defaultFS.isEmpty() ) + defaultFS = QStringLiteral( "ext4" ); + if ( FileSystem::typeForName( defaultFS ) == FileSystem::Unknown ) { - gs->insert( "enableLuksAutomatedPartitioning", - configurationMap.value( "enableLuksAutomatedPartitioning" ).toBool() ); - } - else - { - gs->insert( "enableLuksAutomatedPartitioning", true ); + cWarning() << "Partition-module setting *defaultFileSystemType* is bad (" << defaultFS << ") using ext4."; + defaultFS = QStringLiteral( "ext4" ); } + gs->insert( "defaultFileSystemType", defaultFS ); // Now that we have the config, we load the PartitionCoreModule in the background @@ -563,7 +587,7 @@ PartitionViewStep::setConfigurationMap( const QVariantMap& configurationMap ) // and remove the spinner. QFutureWatcher< void >* watcher = new QFutureWatcher< void >(); connect( watcher, &QFutureWatcher< void >::finished, - this, [ this, watcher ] + this, [ this, watcher, choices ] { continueLoading(); watcher->deleteLater(); diff --git a/src/modules/partition/gui/PrettyRadioButton.cpp b/src/modules/partition/gui/PrettyRadioButton.cpp index a697ed270..18627f41c 100644 --- a/src/modules/partition/gui/PrettyRadioButton.cpp +++ b/src/modules/partition/gui/PrettyRadioButton.cpp @@ -21,29 +21,34 @@ #include "utils/CalamaresUtilsGui.h" #include "widgets/ClickableLabel.h" +#include +#include +#include #include -#include PrettyRadioButton::PrettyRadioButton( QWidget* parent ) : QWidget( parent ) + , m_label( new ClickableLabel ) + , m_radio( new QRadioButton ) + , m_mainLayout( new QGridLayout ) + , m_optionsLayout( nullptr ) { - QHBoxLayout* mainLayout = new QHBoxLayout; - setLayout( mainLayout ); + setLayout( m_mainLayout ); - m_radio = new QRadioButton; - m_label = new ClickableLabel; - - connect( m_label, &ClickableLabel::clicked, - m_radio, &QRadioButton::click ); m_label->setBuddy( m_radio ); m_label->setWordWrap( true ); m_label->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred ); - mainLayout->addWidget( m_radio ); - mainLayout->addWidget( m_label ); - mainLayout->setContentsMargins( 0, 0, 0, 0 ); + m_mainLayout->addWidget( m_radio, 0, 0 ); + m_mainLayout->addWidget( m_label, 0, 1 ); + m_mainLayout->setContentsMargins( 0, 0, 0, 0 ); + + connect( m_label, &ClickableLabel::clicked, + m_radio, &QRadioButton::click ); + connect( m_radio, &QRadioButton::toggled, + this, &PrettyRadioButton::toggleOptions ); } @@ -80,3 +85,32 @@ PrettyRadioButton::buttonWidget() const { return m_radio; } + +void +PrettyRadioButton::addOptionsComboBox( QComboBox* box ) +{ + if ( !box ) + return; + + if ( !m_optionsLayout ) + { + QWidget* w = new QWidget; + m_optionsLayout = new QHBoxLayout; + m_optionsLayout->setAlignment( Qt::AlignmentFlag::AlignLeft ); + m_optionsLayout->addStretch( 1 ); + + w->setLayout( m_optionsLayout ); + m_mainLayout->addWidget( w, 1, 1 ); + + toggleOptions( m_radio->isChecked() ); + } + + m_optionsLayout->insertWidget( m_optionsLayout->count()-1, box ); +} + +void +PrettyRadioButton::toggleOptions( bool toggle ) +{ + if ( m_optionsLayout ) + m_optionsLayout->parentWidget()->setVisible( toggle ); +} diff --git a/src/modules/partition/gui/PrettyRadioButton.h b/src/modules/partition/gui/PrettyRadioButton.h index f475ce528..c88c00728 100644 --- a/src/modules/partition/gui/PrettyRadioButton.h +++ b/src/modules/partition/gui/PrettyRadioButton.h @@ -22,7 +22,17 @@ #include class ClickableLabel; +class QComboBox; +class QGridLayout; +class QHBoxLayout; +/** @brief A radio button with fancy label next to it. + * + * The radio button itself can be retrieved with buttonWidget(), + * and the whole behaves a lot like a label. Extra options can be + * added to the display (options are hidden when the button is + * not selected) with addOptionsComboBox(). + */ class PrettyRadioButton : public QWidget { Q_OBJECT @@ -40,9 +50,18 @@ public: virtual QRadioButton* buttonWidget() const; + /** @brief Add an options drop-down to this button. */ + void addOptionsComboBox( QComboBox* ); + +protected slots: + /// Options are hidden when the radio button is off + void toggleOptions( bool checked ); + protected: ClickableLabel* m_label; QRadioButton* m_radio; + QGridLayout* m_mainLayout; + QHBoxLayout* m_optionsLayout; }; #endif // PRETTYRADIOBUTTON_H diff --git a/src/modules/partition/gui/ReplaceWidget.cpp b/src/modules/partition/gui/ReplaceWidget.cpp index 524932057..faedc03d4 100644 --- a/src/modules/partition/gui/ReplaceWidget.cpp +++ b/src/modules/partition/gui/ReplaceWidget.cpp @@ -85,6 +85,8 @@ ReplaceWidget::reset() void ReplaceWidget::applyChanges() { + auto gs = Calamares::JobQueue::instance()->globalStorage(); + PartitionModel* model = qobject_cast< PartitionModel* >( m_ui->partitionTreeView->model() ); if ( model ) { @@ -93,7 +95,9 @@ ReplaceWidget::applyChanges() { Device* dev = model->device(); - PartitionActions::doReplacePartition( m_core, dev, partition ); + PartitionActions::doReplacePartition( + m_core, dev, partition, + { gs->value( "defaultFileSystemType" ).toString(), QString() } ); if ( m_isEfi ) { @@ -102,17 +106,13 @@ ReplaceWidget::applyChanges() { PartitionInfo::setMountPoint( efiSystemPartitions.first(), - Calamares::JobQueue::instance()-> - globalStorage()-> - value( "efiSystemPartition" ).toString() ); + gs->value( "efiSystemPartition" ).toString() ); } else if ( efiSystemPartitions.count() > 1 ) { PartitionInfo::setMountPoint( efiSystemPartitions.at( m_ui->bootComboBox->currentIndex() ), - Calamares::JobQueue::instance()-> - globalStorage()-> - value( "efiSystemPartition" ).toString() ); + gs->value( "efiSystemPartition" ).toString() ); } } diff --git a/src/modules/partition/partition.conf b/src/modules/partition/partition.conf index 78ad82f10..70d0ea17f 100644 --- a/src/modules/partition/partition.conf +++ b/src/modules/partition/partition.conf @@ -3,22 +3,37 @@ # etc.) use just /boot. efiSystemPartition: "/boot/efi" -# Make sure an autogenerated swap partition is big enough for hibernation in -# automated partitioning modes. Swap can be disabled through *neverCreateSwap*. +# In autogenerated partitioning, allow the user to select a swap size? +# If there is exactly one choice, no UI is presented, and the user +# cannot make a choice -- this setting is used. If there is more than +# one choice, a UI is presented. # -# When *ensureSuspendToDisk* is true, swap is never smaller than physical -# memory, follows the guideline 2 * memory until swap reaches 8GiB. -# When *ensureSuspendToDisk* is false, swap size scales up with memory -# size until 8GiB, then at roughly half of memory size. +# Legacy settings *neverCreateSwap* and *ensureSuspendToDisk* correspond +# to values of *userSwapChoices* as follows: +# - *neverCreateSwap* is true, means [none] +# - *neverCreateSwap* is false, *ensureSuspendToDisk* is false, [small] +# - *neverCreateSwap* is false, *ensureSuspendToDisk* is true, [suspend] # -# -# Default is true. -ensureSuspendToDisk: true +# Autogenerated swap sizes are as follows: +# - *suspend*: Swap is always at least total memory size, +# and up to 4GiB RAM follows the rule-of-thumb 2 * memory; +# from 4GiB to 8 GiB it stays steady at 8GiB, and over 8 GiB memory +# swap is the size of main memory. +# - *small*: Follows the rules above, but Swap is at +# most 8GiB, and no more than 10% of available disk. +# In both cases, a fudge factor (usually 10% extra) is applied so that there +# is some space for administrative overhead (e.g. 8 GiB swap will allocate +# 8.8GiB on disk in the end). +userSwapChoices: + - none # Create no swap, use no swap + # - reuse # Re-use existing swap, but don't create any + - small # Up to 4GB + - suspend # At least main memory size + # - file # To swap file instead of partition (unsupported right now) -# Never create swap partitions in automated partitioning modes. -# If this is true, ensureSuspendToDisk is ignored. -# Default is false. -neverCreateSwap: false +# LEGACY SETTINGS (these will generate a warning) +# ensureSuspendToDisk: true +# neverCreateSwap: false # Correctly draw nested (e.g. logical) partitions as such. drawNestedPartitions: false